From 5c20470b280c46da09cf080a41263d6c427a235c Mon Sep 17 00:00:00 2001 From: Deepika Guliani <dguliani@gitlab.com> Date: Wed, 20 Dec 2023 07:05:48 +0000 Subject: [PATCH] Add editable title and description - Behind :work_items_mvc_2 feature flag --- .../components/work_item_description.vue | 58 ++++++++++-- .../work_item_description_rendered.vue | 26 +++++- .../components/work_item_detail.vue | 90 +++++++++++++++++- .../components/work_item_title_with_edit.vue | 43 +++++++++ .../stylesheets/page_bundles/work_items.scss | 9 +- .../work_item_description_rendered_spec.js | 15 ++- .../components/work_item_description_spec.js | 36 ++++++++ .../components/work_item_detail_spec.js | 64 +++++++++++++ .../work_item_with_title_edit_spec.js | 59 ++++++++++++ .../features/work_items_shared_examples.rb | 92 +++++++++++-------- 10 files changed, 438 insertions(+), 54 deletions(-) create mode 100644 app/assets/javascripts/work_items/components/work_item_title_with_edit.vue create mode 100644 spec/frontend/work_items/components/work_item_with_title_edit_spec.js 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 77c573b47e4c6..4301dcca30bfd 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -40,12 +40,27 @@ export default { type: String, required: true, }, + disableInlineEditing: { + type: Boolean, + required: false, + default: false, + }, + editMode: { + type: Boolean, + required: false, + default: false, + }, + updateInProgress: { + type: Boolean, + required: false, + default: false, + }, }, markdownDocsPath: helpPagePath('user/markdown'), data() { return { workItem: {}, - isEditing: false, + isEditing: this.editMode, isSubmitting: false, isSubmittingWithKeydown: false, descriptionText: '', @@ -126,6 +141,26 @@ export default { autocompleteDataSources() { return autocompleteDataSources(this.fullPath, this.workItem.iid); }, + saveButtonText() { + return this.editMode ? __('Save changes') : __('Save'); + }, + formGroupClass() { + return { + 'gl-border-t gl-pt-6': !this.disableInlineEditing, + 'gl-mb-5 common-note-form': true, + }; + }, + }, + watch: { + updateInProgress(newValue) { + this.isSubmitting = newValue; + }, + editMode(newValue) { + this.isEditing = newValue; + if (newValue) { + this.startEditing(); + } + }, }, methods: { checkForConflicts() { @@ -159,6 +194,7 @@ export default { } this.isEditing = false; + this.$emit('cancelEditing'); clearDraft(this.autosaveKey); }, onInput() { @@ -175,6 +211,11 @@ export default { this.isSubmittingWithKeydown = true; } + if (this.disableInlineEditing) { + this.$emit('updateWorkItem'); + return; + } + this.isSubmitting = true; try { @@ -210,6 +251,9 @@ export default { }, setDescriptionText(newText) { this.descriptionText = newText; + if (this.disableInlineEditing) { + this.$emit('updateDraft', this.descriptionText); + } updateDraft(this.autosaveKey, this.descriptionText); }, handleDescriptionTextUpdated(newText) { @@ -224,12 +268,13 @@ export default { <div> <gl-form v-if="isEditing" @submit.prevent="updateWorkItem" @reset.prevent="cancelEditing"> <gl-form-group - class="gl-mb-5 gl-border-t gl-pt-6 common-note-form" + :class="formGroupClass" :label="__('Description')" + :label-sr-only="disableInlineEditing" label-for="work-item-description" > <markdown-editor - class="gl-my-5" + class="gl-mb-5" :value="descriptionText" :render-markdown-path="markdownPreviewPath" :markdown-docs-path="$options.markdownDocsPath" @@ -285,9 +330,9 @@ export default { :loading="isSubmitting" data-testid="save-description" type="submit" - >{{ __('Save') }} + >{{ saveButtonText }} </gl-button> - <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" type="reset" + <gl-button category="secondary" class="gl-ml-3" data-testid="cancel" type="reset" >{{ __('Cancel') }} </gl-button> </template> @@ -296,13 +341,14 @@ export default { </gl-form> <work-item-description-rendered v-else + :disable-inline-editing="disableInlineEditing" :work-item-description="workItemDescription" :can-edit="canEdit" @startEditing="startEditing" @descriptionUpdated="handleDescriptionTextUpdated" /> <edited-at - v-if="lastEditedAt" + v-if="lastEditedAt && !editMode" :updated-at="lastEditedAt" :updated-by-name="lastEditedByName" :updated-by-path="lastEditedByPath" diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue index 124e05db431a1..1699f6c419e3a 100644 --- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue +++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue @@ -22,6 +22,16 @@ export default { type: Boolean, required: true, }, + disableInlineEditing: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + checkboxes: [], + }; }, computed: { descriptionText() { @@ -33,6 +43,12 @@ export default { descriptionEmpty() { return this.descriptionHtml?.trim() === ''; }, + showEmptyDescription() { + return this.descriptionEmpty && !this.disableInlineEditing; + }, + showEditButton() { + return this.canEdit && !this.disableInlineEditing; + }, }, watch: { descriptionHtml: { @@ -96,9 +112,11 @@ export default { <template> <div class="gl-mb-5"> <div class="gl-display-inline-flex gl-align-items-center gl-mb-3"> - <label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label> + <label v-if="!disableInlineEditing" class="d-block col-form-label gl-mr-5">{{ + __('Description') + }}</label> <gl-button - v-if="canEdit" + v-if="showEditButton" v-gl-tooltip class="gl-ml-auto" icon="pencil" @@ -109,9 +127,9 @@ export default { /> </div> - <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div> + <div v-if="showEmptyDescription" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div> <div - v-else + v-else-if="!descriptionEmpty" ref="gfm-content" v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8 gl-clearfix" diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index b74cbc8537993..93f552bfa4c2a 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -2,6 +2,7 @@ import { isEmpty } from 'lodash'; import { GlAlert, GlSkeletonLoader, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui'; import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg?raw'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { s__ } from '~/locale'; import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -41,6 +42,7 @@ import WorkItemAwardEmoji from './work_item_award_emoji.vue'; import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue'; import WorkItemStickyHeader from './work_item_sticky_header.vue'; import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue'; +import WorkItemTitleWithEdit from './work_item_title_with_edit.vue'; export default { i18n, @@ -67,6 +69,7 @@ export default { WorkItemRelationships, WorkItemStickyHeader, WorkItemAncestors, + WorkItemTitleWithEdit, }, mixins: [glFeatureFlagMixin()], inject: ['fullPath', 'isGroup', 'reportAbusePath'], @@ -94,6 +97,8 @@ export default { reportedUrl: '', reportedUserId: 0, isStickyHeaderShowing: false, + editMode: false, + draftData: {}, }; }, apollo: { @@ -219,7 +224,7 @@ export default { }; }, showIntersectionObserver() { - return !this.isModal && this.workItemsMvc2Enabled; + return !this.isModal && this.workItemsMvc2Enabled && !this.editMode; }, hasLinkedWorkItems() { return this.glFeatures.linkedWorkItems; @@ -233,13 +238,15 @@ export default { titleClassHeader() { return { 'gl-sm-display-none!': this.parentWorkItem, - 'gl-w-full': !this.parentWorkItem, + 'gl-w-full': !this.parentWorkItem && !this.editMode, + 'editable-wi-title': this.editMode && !this.parentWorkItem, }; }, titleClassComponent() { return { 'gl-sm-display-block!': !this.parentWorkItem, 'gl-display-none gl-sm-display-block!': this.parentWorkItem, + 'gl-mt-3 editable-wi-title': this.workItemsMvc2Enabled, }; }, headerWrapperClass() { @@ -258,6 +265,9 @@ export default { } }, methods: { + enableEditMode() { + this.editMode = true; + }, isWidgetPresent(type) { return this.workItem.widgets?.find((widget) => widget.type === type); }, @@ -349,6 +359,45 @@ export default { this.isStickyHeaderShowing = true; } }, + updateDraft(type, value) { + this.draftData[type] = value; + }, + async updateWorkItem() { + this.updateInProgress = true; + try { + const { + data: { workItemUpdate }, + } = await this.$apollo.mutate({ + mutation: updateWorkItemMutation, + variables: { + input: { + id: this.workItem.id, + title: this.draftData.title, + descriptionWidget: { + description: this.draftData.description, + }, + }, + }, + }); + + const { errors } = workItemUpdate; + + if (errors?.length) { + this.updateError = errors.join('\n'); + throw new Error(this.updateError); + } + + this.editMode = false; + } catch (error) { + Sentry.captureException(error); + } finally { + this.updateInProgress = false; + } + }, + cancelEditing() { + this.draftData = {}; + this.editMode = false; + }, }, WORK_ITEM_TYPE_VALUE_OBJECTIVE, WORKSPACE_PROJECT, @@ -388,8 +437,16 @@ export default { :class="titleClassHeader" data-testid="work-item-type" > + <work-item-title-with-edit + v-if="workItem.title && workItemsMvc2Enabled" + ref="title" + class="gl-mt-3 gl-sm-display-block!" + :is-editing="editMode" + :title="workItem.title" + @updateDraft="updateDraft('title', $event)" + /> <work-item-title - v-if="workItem.title" + v-else-if="workItem.title" ref="title" class="gl-sm-display-block!" :work-item-id="workItem.id" @@ -402,6 +459,14 @@ export default { <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-ml-auto gl-gap-3" > + <gl-button + v-if="workItemsMvc2Enabled && !editMode" + category="secondary" + data-testid="work-item-edit-form-button" + @click="enableEditMode" + > + {{ __('Edit') }} + </gl-button> <work-item-todos v-if="showWorkItemCurrentUserTodos" :work-item-id="workItem.id" @@ -441,8 +506,16 @@ export default { /> </div> <div> + <work-item-title-with-edit + v-if="workItem.title && workItemsMvc2Enabled && parentWorkItem" + ref="title" + :is-editing="editMode" + :class="titleClassComponent" + :title="workItem.title" + @updateDraft="updateDraft('title', $event)" + /> <work-item-title - v-if="workItem.title && parentWorkItem" + v-else-if="workItem.title && parentWorkItem" ref="title" :class="titleClassComponent" :work-item-id="workItem.id" @@ -453,6 +526,7 @@ export default { @error="updateError = $event" /> <work-item-created-updated + v-if="!editMode" :full-path="fullPath" :work-item-iid="workItemIid" :update-in-progress="updateInProgress" @@ -490,10 +564,16 @@ export default { /> <work-item-description v-if="hasDescriptionWidget" + :class="workItemsMvc2Enabled ? '' : 'gl-pt-5'" + :disable-inline-editing="workItemsMvc2Enabled" + :edit-mode="editMode" :full-path="fullPath" :work-item-id="workItem.id" :work-item-iid="workItem.iid" - class="gl-pt-5" + :update-in-progress="updateInProgress" + @updateWorkItem="updateWorkItem" + @updateDraft="updateDraft('description', $event)" + @cancelEditing="cancelEditing" @error="updateError = $event" /> <work-item-award-emoji diff --git a/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue new file mode 100644 index 0000000000000..02ed25f98e4f9 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue @@ -0,0 +1,43 @@ +<script> +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlFormGroup, + GlFormInput, + }, + i18n: { + titleLabel: __('Title (required)'), + }, + props: { + title: { + type: String, + required: true, + }, + isEditing: { + type: Boolean, + required: false, + default: false, + }, + }, +}; +</script> + +<template> + <gl-form-group v-if="isEditing" :label="$options.i18n.titleLabel" label-for="work-item-title"> + <gl-form-input + id="work-item-title" + class="gl-w-full" + :value="title" + @change="$emit('updateDraft', $event)" + /> + </gl-form-group> + <h1 + v-else + data-testid="work-item-title" + class="gl-w-full gl-font-weight-normal gl-sm-font-weight-bold gl-mb-1 gl-mt-0 gl-font-size-h-display" + > + {{ title }} + </h1> +</template> diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index b9ab2450ff921..5b354f3575cfa 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -4,6 +4,7 @@ $work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important; $work-item-overview-right-sidebar-width: 23rem; $work-item-sticky-header-height: 52px; +$work-item-overview-gap-width: 2rem; .gl-token-selector-token-container { display: flex; @@ -146,7 +147,7 @@ $work-item-sticky-header-height: 52px; @include media-breakpoint-up(md) { display: grid; grid-template-columns: 1fr $work-item-overview-right-sidebar-width; - gap: 2rem; + gap: $work-item-overview-gap-width; } } @@ -216,6 +217,12 @@ $work-item-sticky-header-height: 52px; } } +.editable-wi-title { + width: 100%; + @include media-breakpoint-up(md) { + width: calc(100% - #{$work-item-overview-right-sidebar-width} - #{$work-item-overview-gap-width}); + } +} // Disclosure hierarchy component, used for Ancestors widget $disclosure-hierarchy-chevron-dimension: 1.2rem; diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js index 4f1d49ee2e5f5..c4c88c7643f45 100644 --- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js +++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js @@ -20,11 +20,13 @@ describe('WorkItemDescription', () => { const createComponent = ({ workItemDescription = defaultWorkItemDescription, canEdit = false, + disableInlineEditing = false, } = {}) => { wrapper = shallowMount(WorkItemDescriptionRendered, { propsData: { workItemDescription, canEdit, + disableInlineEditing, }, }); }; @@ -81,8 +83,8 @@ describe('WorkItemDescription', () => { }); describe('Edit button', () => { - it('is not visible when canUpdate = false', async () => { - await createComponent({ + it('is not visible when canUpdate = false', () => { + createComponent({ canUpdate: false, }); @@ -100,5 +102,14 @@ describe('WorkItemDescription', () => { expect(wrapper.emitted('startEditing')).toEqual([[]]); }); + + it('is not visible when `disableInlineEditing` is true and the user can edit', () => { + createComponent({ + disableInlineEditing: true, + canEdit: true, + }); + + expect(findEditButton().exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index 1d25bb74986f0..3b137008b5b9c 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -56,6 +56,8 @@ describe('WorkItemDescription', () => { isEditing = false, isGroup = false, workItemIid = '1', + disableInlineEditing = false, + editMode = false, } = {}) => { workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); groupWorkItemResponseHandler = jest @@ -73,6 +75,8 @@ describe('WorkItemDescription', () => { fullPath: 'test-project-path', workItemId: id, workItemIid, + disableInlineEditing, + editMode, }, provide: { isGroup, @@ -283,4 +287,36 @@ describe('WorkItemDescription', () => { expect(groupWorkItemResponseHandler).toHaveBeenCalled(); }); }); + + describe('when inline editing is disabled', () => { + describe('when edit mode is inactive', () => { + beforeEach(() => { + createComponent({ disableInlineEditing: true }); + }); + + it('passes the correct props for work item rendered description', () => { + expect(findRenderedDescription().props('disableInlineEditing')).toBe(true); + }); + + it('does not show edit mode of markdown editor in default mode', () => { + expect(findMarkdownEditor().exists()).toBe(false); + }); + }); + + describe('when edit mode is active', () => { + beforeEach(() => { + createComponent({ disableInlineEditing: true, editMode: true }); + }); + + it('shows markdown editor in edit mode only when the correct props are passed', () => { + expect(findMarkdownEditor().exists()).toBe(true); + }); + + it('emits the `updateDraft` event when clicked on submit button in edit mode', () => { + const updatedDesc = 'updated desc with inline editing disabled'; + findMarkdownEditor().vm.$emit('input', updatedDesc); + expect(wrapper.emitted('updateDraft')).toEqual([[updatedDesc]]); + }); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index d63bb94c3f0c6..e43c4d3c74d93 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -19,6 +19,7 @@ import WorkItemRelationships from '~/work_items/components/work_item_relationshi import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue'; +import WorkItemTitleWithEdit from '~/work_items/components/work_item_title_with_edit.vue'; import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; import { i18n } from '~/work_items/constants'; @@ -81,6 +82,8 @@ describe('WorkItemDetail component', () => { const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader); const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview'); const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar'); + const findEditButton = () => wrapper.findByTestId('work-item-edit-form-button'); + const findWorkItemTitleWithEdit = () => wrapper.findComponent(WorkItemTitleWithEdit); const createComponent = ({ isGroup = false, @@ -686,4 +689,65 @@ describe('WorkItemDetail component', () => { }); }); }); + + describe('edit button for work item title and description', () => { + describe('when `workItemsMvc2Enabled` is false', () => { + beforeEach(async () => { + createComponent({ workItemsMvc2Enabled: false }); + await waitForPromises(); + }); + + it('does not show the edit button', () => { + expect(findEditButton().exists()).toBe(false); + }); + + it('renders the work item title inline editable component', () => { + expect(findWorkItemTitle().exists()).toBe(true); + }); + + it('does not render the work item title with edit component', () => { + expect(findWorkItemTitleWithEdit().exists()).toBe(false); + }); + }); + + describe('when `workItemsMvc2Enabled` is true', () => { + beforeEach(async () => { + createComponent({ workItemsMvc2Enabled: true }); + await waitForPromises(); + }); + + it('shows the edit button', () => { + expect(findEditButton().exists()).toBe(true); + }); + + it('does not render the work item title inline editable component', () => { + expect(findWorkItemTitle().exists()).toBe(false); + }); + + it('renders the work item title with edit component', () => { + expect(findWorkItemTitleWithEdit().exists()).toBe(true); + expect(findWorkItemTitleWithEdit().props('isEditing')).toBe(false); + }); + + it('work item description is not shown in edit mode by default', () => { + expect(findWorkItemDescription().props('editMode')).toBe(false); + }); + + describe('when edit is clicked', () => { + beforeEach(async () => { + findEditButton().vm.$emit('click'); + await nextTick(); + }); + + it('work item title component shows in edit mode', () => { + expect(findWorkItemTitleWithEdit().props('isEditing')).toBe(true); + }); + + it('work item description component shows in edit mode', () => { + expect(findWorkItemDescription().props('disableInlineEditing')).toBe(true); + expect(findWorkItemDescription().props('editMode')).toBe(true); + }); + }); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_with_title_edit_spec.js b/spec/frontend/work_items/components/work_item_with_title_edit_spec.js new file mode 100644 index 0000000000000..db9551b6ec36b --- /dev/null +++ b/spec/frontend/work_items/components/work_item_with_title_edit_spec.js @@ -0,0 +1,59 @@ +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemTitleWithEdit from '~/work_items/components/work_item_title_with_edit.vue'; + +describe('Work Item title with edit', () => { + let wrapper; + const mockTitle = 'Work Item title'; + + const createComponent = ({ isEditing = false } = {}) => { + wrapper = shallowMountExtended(WorkItemTitleWithEdit, { + propsData: { + title: mockTitle, + isEditing, + }, + }); + }; + + const findTitle = () => wrapper.findByTestId('work-item-title'); + const findEditableTitleForm = () => wrapper.findComponent(GlFormGroup); + const findEditableTitleInput = () => wrapper.findComponent(GlFormInput); + + describe('Default mode', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders title', () => { + expect(findTitle().exists()).toBe(true); + expect(findTitle().text()).toBe(mockTitle); + }); + + it('does not render edit mode', () => { + expect(findEditableTitleForm().exists()).toBe(false); + }); + }); + + describe('Edit mode', () => { + beforeEach(() => { + createComponent({ isEditing: true }); + }); + + it('does not render read only title', () => { + expect(findTitle().exists()).toBe(false); + }); + + it('renders the editable title with label', () => { + expect(findEditableTitleForm().exists()).toBe(true); + expect(findEditableTitleForm().attributes('label')).toBe( + WorkItemTitleWithEdit.i18n.titleLabel, + ); + }); + + it('emits `updateDraft` event on change of the input', () => { + findEditableTitleInput().vm.$emit('change', 'updated title'); + + expect(wrapper.emitted('updateDraft')).toEqual([['updated title']]); + }); + }); +}); diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb index 3dfd7604914e3..a5b467da45deb 100644 --- a/spec/support/shared_examples/features/work_items_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items_shared_examples.rb @@ -3,6 +3,13 @@ RSpec.shared_examples 'work items title' do let(:title_selector) { '[data-testid="work-item-title"]' } + before do + stub_feature_flags(work_items_mvc_2: false) + + page.refresh + wait_for_all_requests + end + it 'successfully shows and changes the title of the work item' do expect(work_item.reload.title).to eq work_item.title @@ -299,54 +306,67 @@ def click_reply_and_enter_slash end RSpec.shared_examples 'work items description' do - it 'shows GFM autocomplete', :aggregate_failures do - click_button "Edit description" - fill_in _('Description'), with: "@#{user.username}" + context 'for work_items_mvc_2 FF' do + [true, false].each do |work_items_mvc_2_flag| # rubocop:disable RSpec/UselessDynamicDefinition -- check it for both off and on + let(:edit_button) { work_items_mvc_2_flag ? 'Edit' : 'Edit description' } - page.within('.atwho-container') do - expect(page).to have_text(user.name) - end - end + before do + stub_feature_flags(work_items_mvc_2: work_items_mvc_2_flag) + + page.refresh + wait_for_all_requests + end - it 'autocompletes available quick actions', :aggregate_failures do - click_button "Edit description" - fill_in _('Description'), with: '/' + it 'shows GFM autocomplete', :aggregate_failures do + click_button edit_button + fill_in _('Description'), with: "@#{user.username}" - page.within('#at-view-commands') do - expect(page).to have_text("title") - expect(page).to have_text("shrug") - expect(page).to have_text("tableflip") - expect(page).to have_text("close") - expect(page).to have_text("cc") - end - end + page.within('.atwho-container') do + expect(page).to have_text(user.name) + end + end - context 'on conflict' do - let_it_be(:other_user) { create(:user) } - let(:expected_warning) { 'Someone edited the description at the same time you did.' } + it 'autocompletes available quick actions', :aggregate_failures do + click_button edit_button + fill_in _('Description'), with: '/' - before do - project.add_developer(other_user) - end + page.within('#at-view-commands') do + expect(page).to have_text("title") + expect(page).to have_text("shrug") + expect(page).to have_text("tableflip") + expect(page).to have_text("close") + expect(page).to have_text("cc") + end + end - it 'shows conflict message when description changes', :aggregate_failures do - click_button "Edit description" + context 'on conflict' do + let_it_be(:other_user) { create(:user) } + let(:expected_warning) { 'Someone edited the description at the same time you did.' } - ::WorkItems::UpdateService.new( - container: work_item.project, - current_user: other_user, - params: { description: "oh no!" } - ).execute(work_item) + before do + project.add_developer(other_user) + end - wait_for_requests + it 'shows conflict message when description changes', :aggregate_failures do + click_button edit_button + + ::WorkItems::UpdateService.new( + container: work_item.project, + current_user: other_user, + params: { description: "oh no!" } + ).execute(work_item) - fill_in _('Description'), with: 'oh yeah!' + wait_for_requests - expect(page).to have_text(expected_warning) + fill_in _('Description'), with: 'oh yeah!' - click_button s_('WorkItem|Save and overwrite') + expect(page).to have_text(expected_warning) - expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!") + click_button s_('WorkItem|Save and overwrite') + + expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!") + end + end end end end -- GitLab