diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js index 1f13392539007ceea3aef4a9cac0fb9e011b1b92..0fc283d46691bade7820e253a5e06fc922ed3148 100644 --- a/app/assets/javascripts/graphql_shared/issuable_client.js +++ b/app/assets/javascripts/graphql_shared/issuable_client.js @@ -45,6 +45,29 @@ export const config = { toReference({ __typename: 'LocalWorkItemChildIsExpanded', id: variables.id }), }, }, + WorkItemDescriptionTemplateConnection: { + fields: { + nodes: { + read(_, { variables }) { + const templates = [ + /* eslint-disable @gitlab/require-i18n-strings */ + { name: 'template 1', content: 'A template' }, + { name: 'template 2', content: 'Another template' }, + { name: 'template 3', content: 'Secret template omg wow' }, + { name: 'template 4', content: 'Another another template' }, + /* eslint-enable @gitlab/require-i18n-strings */ + ]; + if (variables.search) { + return templates.filter(({ name }) => name.includes(variables.search)); + } + if (variables.name) { + return templates.filter(({ name }) => name === variables.name); + } + return templates; + }, + }, + }, + }, Project: { fields: { projectMembers: { diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index 22c74b390ad8e78b38d3402143837fa9c1bb055e..c3b7a5b1a87922aec590c47f7efd9d7d0760d4c2 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -1,6 +1,7 @@ @@ -309,9 +353,46 @@ export default { +
+ + +

+ {{ + __( + 'Applying a template will replace the existing description. Any changes you have made will be lost.', + ) + }} +

+ +
+
{ const findEditedAt = () => wrapper.findComponent(EditedAt); const findConflictsAlert = () => wrapper.findComponent(GlAlert); const findConflictedDescription = () => wrapper.find('[data-testid="conflicted-description"]'); + const findDescriptionTemplateListbox = () => + wrapper.findComponent(WorkItemDescriptionTemplatesListbox); + const findDescriptionTemplateWarning = () => + wrapper.find('[data-testid="description-template-warning"]'); + const findDescriptionTemplateWarningButton = (type) => + findDescriptionTemplateWarning().find(`[data-testid="template-${type}"]`); const editDescription = (newText) => findMarkdownEditor().vm.$emit('input', newText); @@ -42,6 +50,19 @@ describe('WorkItemDescription', () => { const findSubmitButton = () => wrapper.find('[data-testid="save-description"]'); const clickCancel = () => findForm().vm.$emit('reset', new Event('reset')); + const descriptionTemplateHandler = jest.fn().mockResolvedValue({ + data: { + namespace: { + id: 'gid://gitlab/Namespaces::ProjectNamespace/34', + workItemDescriptionTemplates: { + __typename: 'WorkItemDescriptionTemplateConnection', + nodes: [{ name: 'example', content: 'A template' }], + }, + __typename: 'Namespace', + }, + }, + }); + const createComponent = async ({ mutationHandler = mutationSuccessHandler, canUpdate = true, @@ -60,6 +81,7 @@ describe('WorkItemDescription', () => { apolloProvider: createMockApollo([ [workItemByIidQuery, workItemResponseHandler], [updateWorkItemMutation, mutationHandler], + [workItemDescriptionTemplateQuery, descriptionTemplateHandler], ]), propsData: { fullPath: 'test-project-path', @@ -269,6 +291,56 @@ describe('WorkItemDescription', () => { expect(wrapper.emitted('updateWorkItem')).toEqual([[{ clearDraft: expect.any(Function) }]]); }); + describe('description templates', () => { + beforeEach(async () => { + await createComponent({ isEditing: true }); + }); + + it('displays the description template selection listbox', () => { + expect(findDescriptionTemplateListbox().exists()).toBe(true); + }); + + describe('selecting a template', () => { + beforeEach(async () => { + findDescriptionTemplateListbox().vm.$emit('selectTemplate', 'example'); + await nextTick(); + await waitForPromises(); + }); + + it('queries for the template content when a template is selected', () => { + expect(descriptionTemplateHandler).toHaveBeenCalledWith({ + name: 'example', + fullPath: 'test-project-path', + }); + }); + + it('displays a warning when a description template is selected', () => { + expect(findDescriptionTemplateWarning().exists()).toBe(true); + expect(findDescriptionTemplateWarningButton('cancel').exists()).toBe(true); + expect(findDescriptionTemplateWarningButton('apply').exists()).toBe(true); + }); + + it('hides the warning when the cancel button is clicked', async () => { + expect(findDescriptionTemplateWarning().exists()).toBe(true); + findDescriptionTemplateWarningButton('cancel').vm.$emit('click'); + await nextTick(); + expect(findDescriptionTemplateWarning().exists()).toBe(false); + }); + + it('applies the template when the apply button is clicked', async () => { + findDescriptionTemplateWarningButton('apply').vm.$emit('click'); + await nextTick(); + expect(findMarkdownEditor().props('value')).toBe('A template'); + }); + + it('hides the warning when the template is applied', async () => { + findDescriptionTemplateWarningButton('apply').vm.$emit('click'); + await nextTick(); + expect(findDescriptionTemplateWarning().exists()).toBe(false); + }); + }); + }); + describe('when description has conflicts', () => { beforeEach(async () => { const workItemResponseHandler = jest