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 284df7e2becffb8e4b5789feccabd36aba90f121..29eb4edc4c4335f662a9cfcc5e88a2a04e61bd96 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -561,6 +561,7 @@ export default { @toggleWorkItemConfidentiality="toggleConfidentiality" @error="updateError = $event" @promotedToObjective="$emit('promotedToObjective', workItemIid)" + @toggleEditMode="enableEditMode" /> <div data-testid="work-item-overview" class="work-item-overview"> <section> diff --git a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue index eb58bc26eb21f2e2ee91c9824686f2427be64dae..014c80a2414c26f60349b451f918e612f7da2b50 100644 --- a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue +++ b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue @@ -1,11 +1,12 @@ <script> -import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; +import { GlLoadingIcon, GlIntersectionObserver, GlButton, GlLink } from '@gitlab/ui'; import LockedBadge from '~/issuable/components/locked_badge.vue'; import { WORKSPACE_PROJECT } from '~/issues/constants'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import { isNotesWidget } from '../utils'; import WorkItemActions from './work_item_actions.vue'; import WorkItemTodos from './work_item_todos.vue'; +import WorkItemStateBadge from './work_item_state_badge.vue'; export default { components: { @@ -15,6 +16,9 @@ export default { WorkItemActions, WorkItemTodos, ConfidentialityBadge, + WorkItemStateBadge, + GlButton, + GlLink, }, props: { workItem: { @@ -78,6 +82,9 @@ export default { projectFullPath() { return this.workItem.namespace?.fullPath; }, + workItemState() { + return this.workItem.state; + }, }, WORKSPACE_PROJECT, }; @@ -95,18 +102,33 @@ export default { data-testid="work-item-sticky-header" > <div - class="work-item-sticky-header-text gl-align-items-center gl-mx-auto gl-px-6 gl-display-flex gl-gap-3" + class="work-item-sticky-header-text gl-items-center gl-mx-auto gl-px-5 xl:gl-px-6 gl-flex gl-gap-3" > - <span class="gl-text-truncate gl-font-weight-bold gl-pr-3 gl-mr-auto"> - {{ workItem.title }} - </span> + <work-item-state-badge v-if="workItemState" :work-item-state="workItemState" /> <gl-loading-icon v-if="updateInProgress" /> <confidentiality-badge v-if="workItem.confidential" :issuable-type="workItemType" :workspace-type="$options.WORKSPACE_PROJECT" + hide-text-in-small-screens /> <locked-badge v-if="isDiscussionLocked" :issuable-type="workItemType" /> + <gl-link + class="gl-truncate gl-block gl-font-bold gl-pr-3 gl-mr-auto gl-text-black" + href="#top" + :title="workItem.title" + > + {{ workItem.title }} + </gl-link> + <gl-button + v-if="canUpdate" + category="secondary" + data-testid="work-item-edit-button-sticky" + class="shortcut-edit-wi-description" + @click="$emit('toggleEditMode')" + > + {{ __('Edit') }} + </gl-button> <work-item-todos v-if="showWorkItemCurrentUserTodos" :work-item-id="workItem.id" 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 51c311c67046e9717b9aab634a1317907d988244..4ad017d917b9a49254a5a6cec71ae15f8bc255c6 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -684,7 +684,7 @@ describe('WorkItemDetail component', () => { describe('work item two column view', () => { beforeEach(async () => { - createComponent({ workItemsBeta: true }); + createComponent(); await waitForPromises(); }); @@ -701,10 +701,24 @@ describe('WorkItemDetail component', () => { }); }); + describe('work item sticky header', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); + + it('enables the edit mode when event `toggleEditMode` is emitted', async () => { + findStickyHeader().vm.$emit('toggleEditMode'); + await nextTick(); + + expect(findWorkItemDescription().props('editMode')).toBe(true); + }); + }); + describe('edit button for work item title and description', () => { describe('with permissions to update', () => { beforeEach(async () => { - createComponent({ workItemsBeta: true }); + createComponent(); await waitForPromises(); }); diff --git a/spec/frontend/work_items/components/work_item_sticky_header_spec.js b/spec/frontend/work_items/components/work_item_sticky_header_spec.js index 0a9de8dc3444759ca3afeadaa7aca1b48ec5f90b..553f7cb5e1470793fa20bec656875f13210e6c60 100644 --- a/spec/frontend/work_items/components/work_item_sticky_header_spec.js +++ b/spec/frontend/work_items/components/work_item_sticky_header_spec.js @@ -1,4 +1,4 @@ -import { GlIntersectionObserver } from '@gitlab/ui'; +import { GlIntersectionObserver, GlLink } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { STATE_OPEN } from '~/work_items/constants'; @@ -8,14 +8,19 @@ import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_heade import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; +import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue'; describe('WorkItemStickyHeader', () => { let wrapper; - const createComponent = ({ confidential = false, discussionLocked = false } = {}) => { + const createComponent = ({ + confidential = false, + discussionLocked = false, + canUpdate = true, + } = {}) => { wrapper = shallowMountExtended(WorkItemStickyHeader, { propsData: { - workItem: workItemResponseFactory({ canUpdate: true, confidential, discussionLocked }).data + workItem: workItemResponseFactory({ canUpdate, confidential, discussionLocked }).data .workItem, fullPath: '/test', isStickyHeaderShowing: true, @@ -36,6 +41,9 @@ describe('WorkItemStickyHeader', () => { const findWorkItemActions = () => wrapper.findComponent(WorkItemActions); const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos); const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + const findWorkItemStateBadge = () => wrapper.findComponent(WorkItemStateBadge); + const findEditButton = () => wrapper.findByTestId('work-item-edit-button-sticky'); + const findWorkItemTitle = () => wrapper.findComponent(GlLink); const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear'); it('has the sticky header when the page is scrolled', async () => { @@ -50,11 +58,35 @@ describe('WorkItemStickyHeader', () => { it('renders title, todos, and actions', () => { createComponent(); - expect(wrapper.findByText('Updated title').exists()).toBe(true); + expect(findWorkItemTitle().exists()).toBe(true); expect(findWorkItemTodos().exists()).toBe(true); expect(findWorkItemActions().exists()).toBe(true); }); + it('has title with the link to the top', () => { + createComponent(); + expect(findWorkItemTitle().attributes('href')).toBe('#top'); + }); + + it('renders the state badge', () => { + createComponent(); + expect(findWorkItemStateBadge().exists()).toBe(true); + }); + + describe('edit button', () => { + it('renders the button when it has permissions to edit', () => { + createComponent({ canUpdate: true }); + + expect(findEditButton().exists()).toBe(true); + }); + + it('does not render the button when it does not have permissions to edit', () => { + createComponent({ canUpdate: false }); + + expect(findEditButton().exists()).toBe(false); + }); + }); + describe('confidential badge', () => { describe('when not confidential', () => { beforeEach(() => {