Commit 87556be9 authored by Simon Knox's avatar Simon Knox

Merge branch 'ntepluhina-create-task-followup' into 'master'

Add dropdown with work item types to create task component

See merge request gitlab-org/gitlab!80076
parents 179e55f0 b19091f3
...@@ -95,7 +95,7 @@ export default { ...@@ -95,7 +95,7 @@ export default {
this.renderGFM(); this.renderGFM();
this.updateTaskStatusText(); this.updateTaskStatusText();
if (this.workItemsEnabled) { if (this.workItemsEnabled && this.$el) {
this.renderTaskActions(); this.renderTaskActions();
} }
}, },
...@@ -174,8 +174,11 @@ export default { ...@@ -174,8 +174,11 @@ export default {
); );
button.id = `js-task-button-${index}`; button.id = `js-task-button-${index}`;
this.taskButtons.push(button.id); this.taskButtons.push(button.id);
button.innerHTML = button.innerHTML = `
'<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14"><use href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#ellipsis_v"></use></svg>'; <svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
<use href="${gon.sprite_icons}#ellipsis_v"></use>
</svg>
`;
item.prepend(button); item.prepend(button);
}); });
}, },
...@@ -191,7 +194,7 @@ export default { ...@@ -191,7 +194,7 @@ export default {
const taskBadge = document.createElement('span'); const taskBadge = document.createElement('span');
taskBadge.innerHTML = ` taskBadge.innerHTML = `
<svg data-testid="issue-open-m-icon" role="img" aria-hidden="true" class="gl-icon gl-fill-green-500 s12"> <svg data-testid="issue-open-m-icon" role="img" aria-hidden="true" class="gl-icon gl-fill-green-500 s12">
<use href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#issue-open-m"></use> <use href="${gon.sprite_icons}#issue-open-m"></use>
</svg> </svg>
<span class="badge badge-info badge-pill gl-badge sm gl-mr-1"> <span class="badge badge-info badge-pill gl-badge sm gl-mr-1">
${__('Task')} ${__('Task')}
...@@ -245,7 +248,7 @@ export default { ...@@ -245,7 +248,7 @@ export default {
modal-id="create-task-modal" modal-id="create-task-modal"
:title="s__('WorkItem|New Task')" :title="s__('WorkItem|New Task')"
hide-footer hide-footer
body-class="gl-py-0!" body-class="gl-p-0!"
> >
<create-work-item <create-work-item
:is-modal="true" :is-modal="true"
......
...@@ -74,6 +74,8 @@ export function initIssueApp(issueData, store) { ...@@ -74,6 +74,8 @@ export function initIssueApp(issueData, store) {
return undefined; return undefined;
} }
const { fullPath } = el.dataset;
if (gon?.features?.fixCommentScroll) { if (gon?.features?.fixCommentScroll) {
scrollToTargetOnResize(); scrollToTargetOnResize();
} }
...@@ -88,6 +90,7 @@ export function initIssueApp(issueData, store) { ...@@ -88,6 +90,7 @@ export function initIssueApp(issueData, store) {
store, store,
provide: { provide: {
canCreateIncident, canCreateIncident,
fullPath,
}, },
computed: { computed: {
...mapGetters(['getNoteableData']), ...mapGetters(['getNoteableData']),
......
query projectWorkItemTypes($fullPath: ID!) {
workspace: project(fullPath: $fullPath) {
id
workItemTypes {
nodes {
id
name
}
}
}
}
...@@ -5,11 +5,15 @@ import { createApolloProvider } from './graphql/provider'; ...@@ -5,11 +5,15 @@ import { createApolloProvider } from './graphql/provider';
export const initWorkItemsRoot = () => { export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items'); const el = document.querySelector('#js-work-items');
const { fullPath } = el.dataset;
return new Vue({ return new Vue({
el, el,
router: createRouter(el.dataset.fullPath), router: createRouter(el.dataset.fullPath),
apolloProvider: createApolloProvider(), apolloProvider: createApolloProvider(),
provide: {
fullPath,
},
render(createElement) { render(createElement) {
return createElement(App); return createElement(App);
}, },
......
<script> <script>
import { GlButton, GlAlert } from '@gitlab/ui'; import { GlButton, GlAlert, GlLoadingIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql'; import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import ItemTitle from '../components/item_title.vue'; import ItemTitle from '../components/item_title.vue';
...@@ -8,8 +10,12 @@ export default { ...@@ -8,8 +10,12 @@ export default {
components: { components: {
GlButton, GlButton,
GlAlert, GlAlert,
GlLoadingIcon,
GlDropdown,
GlDropdownItem,
ItemTitle, ItemTitle,
}, },
inject: ['fullPath'],
props: { props: {
isModal: { isModal: {
type: Boolean, type: Boolean,
...@@ -25,9 +31,34 @@ export default { ...@@ -25,9 +31,34 @@ export default {
data() { data() {
return { return {
title: this.initialTitle, title: this.initialTitle,
error: false, error: null,
workItemTypes: [],
selectedWorkItemType: null,
}; };
}, },
apollo: {
workItemTypes: {
query: projectWorkItemTypesQuery,
variables() {
return {
fullPath: this.fullPath,
};
},
update(data) {
return data.workspace?.workItemTypes?.nodes;
},
error() {
this.error = s__(
'WorkItem|Something went wrong when fetching work item types. Please try again',
);
},
},
},
computed: {
dropdownButtonText() {
return this.selectedWorkItemType?.name || s__('WorkItem|Type');
},
},
methods: { methods: {
async createWorkItem() { async createWorkItem() {
try { try {
...@@ -53,7 +84,9 @@ export default { ...@@ -53,7 +84,9 @@ export default {
this.$emit('onCreate', this.title); this.$emit('onCreate', this.title);
} }
} catch { } catch {
this.error = true; this.error = s__(
'WorkItem|Something went wrong when creating a work item. Please try again',
);
} }
}, },
handleTitleInput(title) { handleTitleInput(title) {
...@@ -66,18 +99,43 @@ export default { ...@@ -66,18 +99,43 @@ export default {
} }
this.$emit('closeModal'); this.$emit('closeModal');
}, },
selectWorkItemType(type) {
this.selectedWorkItemType = type;
},
}, },
}; };
</script> </script>
<template> <template>
<form @submit.prevent="createWorkItem"> <form @submit.prevent="createWorkItem">
<gl-alert v-if="error" variant="danger" @dismiss="error = false">{{ <gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
__('Something went wrong when creating a work item. Please try again') <div :class="{ 'gl-px-5': isModal }" data-testid="content">
}}</gl-alert> <item-title
<item-title :initial-title="title" data-testid="title-input" @title-input="handleTitleInput" /> :initial-title="title"
data-testid="title-input"
@title-input="handleTitleInput"
/>
<div>
<gl-dropdown :text="dropdownButtonText">
<gl-loading-icon
v-if="$apollo.queries.workItemTypes.loading"
size="md"
data-testid="loading-types"
/>
<template v-else>
<gl-dropdown-item
v-for="type in workItemTypes"
:key="type.id"
@click="selectWorkItemType(type)"
>
{{ type.name }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</div>
</div>
<div <div
class="gl-bg-gray-10 gl-py-5 gl-px-6" class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4"
:class="{ 'gl-display-flex gl-justify-content-end': isModal }" :class="{ 'gl-display-flex gl-justify-content-end': isModal }"
> >
<gl-button <gl-button
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json} } #js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, full_path: @project.full_path } }
.title-container .title-container
%h2.title= markdown_field(issuable, :title) %h2.title= markdown_field(issuable, :title)
- if issuable.description.present? - if issuable.description.present?
......
...@@ -33812,9 +33812,6 @@ msgstr "" ...@@ -33812,9 +33812,6 @@ msgstr ""
msgid "Something went wrong trying to load issue contacts." msgid "Something went wrong trying to load issue contacts."
msgstr "" msgstr ""
msgid "Something went wrong when creating a work item. Please try again"
msgstr ""
msgid "Something went wrong when reordering designs. Please try again" msgid "Something went wrong when reordering designs. Please try again"
msgstr "" msgstr ""
...@@ -41096,6 +41093,15 @@ msgstr "" ...@@ -41096,6 +41093,15 @@ msgstr ""
msgid "WorkItem|New Task" msgid "WorkItem|New Task"
msgstr "" msgstr ""
msgid "WorkItem|Something went wrong when creating a work item. Please try again"
msgstr ""
msgid "WorkItem|Something went wrong when fetching work item types. Please try again"
msgstr ""
msgid "WorkItem|Type"
msgstr ""
msgid "WorkItem|Work Items" msgid "WorkItem|Work Items"
msgstr "" msgstr ""
......
...@@ -34,3 +34,17 @@ export const updateWorkItemMutationResponse = { ...@@ -34,3 +34,17 @@ export const updateWorkItemMutationResponse = {
}, },
}, },
}; };
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
id: '1',
workItemTypes: {
nodes: [
{ id: 'work-item-1', name: 'Issue' },
{ id: 'work-item-2', name: 'Incident' },
],
},
},
},
};
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { GlAlert } from '@gitlab/ui'; import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue'; import ItemTitle from '~/work_items/components/item_title.vue';
import { resolvers } from '~/work_items/graphql/resolvers'; import { resolvers } from '~/work_items/graphql/resolvers';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import { projectWorkItemTypesQueryResponse } from '../mock_data';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -14,13 +16,20 @@ describe('Create work item component', () => { ...@@ -14,13 +16,20 @@ describe('Create work item component', () => {
let wrapper; let wrapper;
let fakeApollo; let fakeApollo;
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findTitleInput = () => wrapper.findComponent(ItemTitle); const findTitleInput = () => wrapper.findComponent(ItemTitle);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findCreateButton = () => wrapper.find('[data-testid="create-button"]'); const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]'); const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findContent = () => wrapper.find('[data-testid="content"]');
const findLoadingTypesIcon = () => wrapper.find('[data-testid="loading-types"]');
const createComponent = ({ data = {}, props = {} } = {}) => { const createComponent = ({ data = {}, props = {}, queryHandler = querySuccessHandler } = {}) => {
fakeApollo = createMockApollo([], resolvers); fakeApollo = createMockApollo([[projectWorkItemTypesQuery, queryHandler]], resolvers);
wrapper = shallowMount(CreateWorkItem, { wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
data() { data() {
...@@ -37,6 +46,9 @@ describe('Create work item component', () => { ...@@ -37,6 +46,9 @@ describe('Create work item component', () => {
push: jest.fn(), push: jest.fn(),
}, },
}, },
provide: {
fullPath: 'full-path',
},
}); });
}; };
...@@ -84,6 +96,10 @@ describe('Create work item component', () => { ...@@ -84,6 +96,10 @@ describe('Create work item component', () => {
it('does not add right margin for cancel button', () => { it('does not add right margin for cancel button', () => {
expect(findCancelButton().classes()).not.toContain('gl-mr-3'); expect(findCancelButton().classes()).not.toContain('gl-mr-3');
}); });
it('does not add padding for content', () => {
expect(findContent().classes('gl-px-5')).toBe(false);
});
}); });
describe('when displayed in a modal', () => { describe('when displayed in a modal', () => {
...@@ -118,6 +134,44 @@ describe('Create work item component', () => { ...@@ -118,6 +134,44 @@ describe('Create work item component', () => {
it('adds right margin for cancel button', () => { it('adds right margin for cancel button', () => {
expect(findCancelButton().classes()).toContain('gl-mr-3'); expect(findCancelButton().classes()).toContain('gl-mr-3');
}); });
it('adds padding for content', () => {
expect(findContent().classes('gl-px-5')).toBe(true);
});
});
it('displays a loading icon inside dropdown when work items query is loading', () => {
createComponent();
expect(findLoadingTypesIcon().exists()).toBe(true);
});
it('displays an alert when work items query is rejected', async () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem') });
await waitForPromises();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toContain('fetching work item types');
});
describe('when work item types are fetched', () => {
beforeEach(() => {
createComponent();
return waitForPromises();
});
it('displays a list of work item types', () => {
expect(findDropdownItems()).toHaveLength(2);
expect(findDropdownItems().at(0).text()).toContain('Issue');
});
it('selects a work item type on click', async () => {
expect(findDropdown().props('text')).toBe('Type');
findDropdownItems().at(0).vm.$emit('click');
await nextTick();
expect(findDropdown().props('text')).toBe('Issue');
});
}); });
it('hides the alert on dismissing the error', async () => { it('hides the alert on dismissing the error', async () => {
......
...@@ -15,6 +15,16 @@ describe('Work items router', () => { ...@@ -15,6 +15,16 @@ describe('Work items router', () => {
wrapper = mount(App, { wrapper = mount(App, {
router, router,
provide: {
fullPath: 'full-path',
},
mocks: {
$apollo: {
queries: {
workItemTypes: {},
},
},
},
}); });
}; };
......
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