Commit 951a7b42 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '346158-show-work-item-details-in-drawer' into 'master'

Show work item detail from issues task

See merge request gitlab-org/gitlab!80727
parents 5a000354 6b9dd157
......@@ -10,7 +10,9 @@ import $ from 'jquery';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import TaskList from '~/task_list';
import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import animateMixin from '../mixins/animate';
......@@ -24,8 +26,9 @@ export default {
GlPopover,
CreateWorkItem,
GlButton,
WorkItemDetailModal,
},
mixins: [animateMixin, glFeatureFlagMixin()],
mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
props: {
canUpdate: {
type: Boolean,
......@@ -68,9 +71,13 @@ export default {
initialUpdate: true,
taskButtons: [],
activeTask: {},
workItemId: null,
};
},
computed: {
showWorkItemDetailModal() {
return Boolean(this.workItemId);
},
workItemsEnabled() {
return this.glFeatures.workItems;
},
......@@ -194,7 +201,13 @@ export default {
closeCreateTaskModal() {
this.$refs.modal.hide();
},
handleCreateTask(title) {
closeWorkItemDetailModal() {
this.workItemId = null;
},
handleWorkItemDetailModalError(message) {
createFlash({ message });
},
handleCreateTask({ id, title, type }) {
const listItem = this.$el.querySelector(`#${this.activeTask.id}`).parentElement;
const taskBadge = document.createElement('span');
taskBadge.innerHTML = `
......@@ -204,12 +217,28 @@ export default {
<span class="badge badge-info badge-pill gl-badge sm gl-mr-1">
${__('Task')}
</span>
<a href="#">${title}</a>
`;
const button = this.createWorkItemDetailButton(id, title, type);
taskBadge.append(button);
listItem.insertBefore(taskBadge, listItem.lastChild);
listItem.removeChild(listItem.lastChild);
this.closeCreateTaskModal();
},
createWorkItemDetailButton(id, title, type) {
const button = document.createElement('button');
button.addEventListener('click', () => {
this.workItemId = id;
this.track('viewed_work_item_from_modal', {
category: 'workItems:show',
label: 'work_item_view',
property: `type_${type}`,
});
});
button.classList.add('btn-link');
button.innerText = title;
return button;
},
focusButton() {
this.$refs.convertButton[0].$el.focus();
},
......@@ -262,6 +291,12 @@ export default {
@onCreate="handleCreateTask"
/>
</gl-modal>
<work-item-detail-modal
:visible="showWorkItemDetailModal"
:work-item-id="workItemId"
@close="closeWorkItemDetailModal"
@error="handleWorkItemDetailModalError"
/>
<template v-if="workItemsEnabled">
<gl-popover
v-for="item in taskButtons"
......
<script>
import { GlModal } from '@gitlab/ui';
import { s__ } from '~/locale';
import workItemQuery from '../graphql/work_item.query.graphql';
import ItemTitle from './item_title.vue';
export default {
components: {
GlModal,
ItemTitle,
},
props: {
visible: {
type: Boolean,
required: true,
},
workItemId: {
type: String,
required: false,
default: null,
},
},
data() {
return {
workItem: {},
};
},
apollo: {
workItem: {
query: workItemQuery,
variables() {
return {
id: this.workItemId,
};
},
update(data) {
return data.localWorkItem;
},
error() {
this.$emit(
'error',
s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
);
},
},
},
computed: {
workItemTitle() {
return this.workItem?.widgets?.nodes.find(
// eslint-disable-next-line no-underscore-dangle
(widget) => widget.__typename === 'LocalTitleWidget',
)?.contentText;
},
},
};
</script>
<template>
<gl-modal hide-footer modal-id="work-item-detail-modal" :visible="visible" @hide="$emit('close')">
<item-title class="gl-m-0!" :initial-title="workItemTitle" />
</gl-modal>
</template>
......@@ -74,14 +74,14 @@ export default {
const {
data: {
localCreateWorkItem: {
workItem: { id },
workItem: { id, type },
},
},
} = response;
if (!this.isModal) {
this.$router.push({ name: 'workItem', params: { id } });
} else {
this.$emit('onCreate', this.title);
this.$emit('onCreate', { id, title: this.title, type });
}
} catch {
this.error = s__(
......
......@@ -41553,6 +41553,9 @@ msgstr ""
msgid "WorkItem|Something went wrong when creating a work item. Please try again"
msgstr ""
msgid "WorkItem|Something went wrong when fetching the work item. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong when fetching work item types. Please try again"
msgstr ""
......
......@@ -2,17 +2,21 @@ import $ from 'jquery';
import { nextTick } from 'vue';
import '~/behaviors/markdown/render_gfm';
import { GlPopover, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createFlash from '~/flash';
import Description from '~/issues/show/components/description.vue';
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import {
descriptionProps as initialProps,
descriptionHtmlWithCheckboxes,
} from '../mock_data/mock_data';
jest.mock('~/flash');
jest.mock('~/task_list');
const showModal = jest.fn();
......@@ -30,9 +34,10 @@ describe('Description component', () => {
const findPopovers = () => wrapper.findAllComponents(GlPopover);
const findModal = () => wrapper.findComponent(GlModal);
const findCreateWorkItem = () => wrapper.findComponent(CreateWorkItem);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
function createComponent({ props = {}, provide = {} } = {}) {
wrapper = shallowMount(Description, {
wrapper = shallowMountExtended(Description, {
propsData: {
...initialProps,
...props,
......@@ -210,7 +215,7 @@ describe('Description component', () => {
describe('with work items feature flag is enabled', () => {
describe('empty description', () => {
beforeEach(async () => {
beforeEach(() => {
createComponent({
props: {
descriptionHtml: '',
......@@ -221,7 +226,7 @@ describe('Description component', () => {
},
},
});
await nextTick();
return nextTick();
});
it('renders without error', () => {
......@@ -230,7 +235,7 @@ describe('Description component', () => {
});
describe('description with checkboxes', () => {
beforeEach(async () => {
beforeEach(() => {
createComponent({
props: {
descriptionHtml: descriptionHtmlWithCheckboxes,
......@@ -241,7 +246,7 @@ describe('Description component', () => {
},
},
});
await nextTick();
return nextTick();
});
it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
......@@ -275,7 +280,7 @@ describe('Description component', () => {
it('updates description HTML on `onCreate` event', async () => {
const newTitle = 'New title';
findConvertToTaskButton().vm.$emit('click');
findCreateWorkItem().vm.$emit('onCreate', newTitle);
findCreateWorkItem().vm.$emit('onCreate', { title: newTitle });
expect(hideModal).toHaveBeenCalled();
await nextTick();
......@@ -283,5 +288,69 @@ describe('Description component', () => {
expect(wrapper.text()).toContain(newTitle);
});
});
describe('work items detail', () => {
const id = '1';
const title = 'my first task';
const type = 'task';
const createThenClickOnTask = () => {
findConvertToTaskButton().vm.$emit('click');
findCreateWorkItem().vm.$emit('onCreate', { id, title, type });
return wrapper.findByRole('button', { name: title }).trigger('click');
};
beforeEach(() => {
createComponent({
props: {
descriptionHtml: descriptionHtmlWithCheckboxes,
},
provide: {
glFeatures: { workItems: true },
},
});
return nextTick();
});
it('opens when task button is clicked', async () => {
expect(findWorkItemDetailModal().props('visible')).toBe(false);
await createThenClickOnTask();
expect(findWorkItemDetailModal().props('visible')).toBe(true);
});
it('closes from an open state', async () => {
await createThenClickOnTask();
expect(findWorkItemDetailModal().props('visible')).toBe(true);
findWorkItemDetailModal().vm.$emit('close');
await nextTick();
expect(findWorkItemDetailModal().props('visible')).toBe(false);
});
it('shows error on error', async () => {
const message = 'I am error';
await createThenClickOnTask();
findWorkItemDetailModal().vm.$emit('error', message);
expect(createFlash).toHaveBeenCalledWith({ message });
});
it('tracks when opened', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
await createThenClickOnTask();
expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'viewed_work_item_from_modal', {
category: 'workItems:show',
label: 'work_item_view',
property: 'type_task',
});
});
});
});
});
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import WorkItemTitle from '~/work_items/components/item_title.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { resolvers } from '~/work_items/graphql/resolvers';
describe('WorkItemDetailModal component', () => {
let wrapper;
Vue.use(VueApollo);
const findModal = () => wrapper.findComponent(GlModal);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const createComponent = () => {
wrapper = shallowMount(WorkItemDetailModal, {
apolloProvider: createMockApollo([], resolvers),
propsData: { visible: true },
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders modal', () => {
createComponent();
expect(findModal().props()).toMatchObject({ visible: true });
});
it('renders work item title', () => {
createComponent();
expect(findWorkItemTitle().exists()).toBe(true);
});
});
......@@ -10,6 +10,8 @@ import { resolvers } from '~/work_items/graphql/resolvers';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import { projectWorkItemTypesQueryResponse } from '../mock_data';
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
Vue.use(VueApollo);
describe('Create work item component', () => {
......@@ -124,7 +126,8 @@ describe('Create work item component', () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(wrapper.emitted('onCreate')).toEqual([[mockTitle]]);
const expected = { id: 'testuuid', title: mockTitle, type: 'FEATURE' };
expect(wrapper.emitted('onCreate')).toEqual([[expected]]);
});
it('does not right margin for create button', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment