From be757ff7ba12ae440777bbbb3c09977f9a014161 Mon Sep 17 00:00:00 2001 From: Jack Chapman <jachapman@gitlab.com> Date: Fri, 19 Apr 2024 19:19:58 +0000 Subject: [PATCH] Add actions dropdown to work item links Adds disclosure dropdown to header area of work item tree widget which contains a link to a pre-filtered roadmap page. Changelog: added --- .../work_item_links/work_item_tree.vue | 18 ++++- .../work_item_tree_actions.vue | 71 +++++++++++++++++++ app/assets/javascripts/work_items/utils.js | 12 ++++ locale/gitlab.pot | 6 ++ .../work_item_tree_actions_spec.js | 54 ++++++++++++++ .../work_item_links/work_item_tree_spec.js | 21 ++++++ spec/frontend/work_items/utils_spec.js | 8 +++ 7 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/work_items/components/work_item_links/work_item_tree_actions.vue create mode 100644 spec/frontend/work_items/components/work_item_links/work_item_tree_actions_spec.js diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index 48d50d88df956..e34923cca9c80 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -9,6 +9,7 @@ import { WORK_ITEMS_TYPE_MAP, WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_KEY_RESULT, + WORK_ITEM_TYPE_ENUM_EPIC, I18N_WORK_ITEM_SHOW_LABELS, CHILD_ITEMS_ANCHOR, } from '../../constants'; @@ -18,6 +19,7 @@ import WidgetWrapper from '../widget_wrapper.vue'; import WorkItemActionsSplitButton from './work_item_actions_split_button.vue'; import WorkItemLinksForm from './work_item_links_form.vue'; import WorkItemChildrenWrapper from './work_item_children_wrapper.vue'; +import WorkItemTreeActions from './work_item_tree_actions.vue'; export default { FORM_TYPES, @@ -29,6 +31,7 @@ export default { WidgetWrapper, WorkItemLinksForm, WorkItemChildrenWrapper, + WorkItemTreeActions, GlToggle, }, props: { @@ -121,6 +124,14 @@ export default { }; }); }, + /** + * Based on the type of work item, should the actions menu be rendered? + * + * Currently only renders for `epic` work items + */ + canShowActionsMenu() { + return this.workItemType.toUpperCase() === WORK_ITEM_TYPE_ENUM_EPIC && this.workItemIid; + }, }, methods: { genericActionItems(workItem) { @@ -179,7 +190,12 @@ export default { label-id="relationship-toggle-labels" @change="showLabels = $event" /> - <work-item-actions-split-button v-if="canUpdate" :actions="addItemsActions" /> + <work-item-actions-split-button v-if="canUpdate" :actions="addItemsActions" class="gl-mr-3" /> + <work-item-tree-actions + v-if="canShowActionsMenu" + :work-item-iid="workItemIid" + :full-path="fullPath" + /> </template> <template #body> <div class="gl-new-card-content gl-px-0"> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_actions.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_actions.vue new file mode 100644 index 0000000000000..2a975cdb6804c --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_actions.vue @@ -0,0 +1,71 @@ +<script> +import { GlDisclosureDropdown, GlTooltipDirective } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { workItemRoadmapPath } from '../../utils'; + +export default { + i18n: { + moreActions: s__('WorkItem|More actions'), + }, + components: { + GlDisclosureDropdown, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + workItemIid: { + type: String, + required: true, + }, + fullPath: { + type: String, + required: true, + }, + }, + data() { + return { + isDropdownVisible: false, + }; + }, + computed: { + tooltipText() { + return !this.isDropdownVisible ? this.$options.i18n.moreActions : ''; + }, + actionDropdownItems() { + return [ + { + text: s__('WorkItem|View on a roadmap'), + href: workItemRoadmapPath(this.fullPath, this.workItemIid), + }, + ]; + }, + }, + methods: { + showDropdown() { + this.isDropdownVisible = true; + }, + hideDropdown() { + this.isDropdownVisible = false; + }, + }, +}; +</script> +<template> + <div> + <gl-disclosure-dropdown + ref="workItemsMoreActions" + v-gl-tooltip="tooltipText" + icon="ellipsis_v" + text-sr-only + :toggle-text="$options.i18n.moreActions" + :items="actionDropdownItems" + size="small" + category="tertiary" + no-caret + placement="bottom-end" + @shown="showDropdown" + @hidden="hideDropdown" + /> + </div> +</template> diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js index 534ac1ccdf24c..3274855617e8f 100644 --- a/app/assets/javascripts/work_items/utils.js +++ b/app/assets/javascripts/work_items/utils.js @@ -82,3 +82,15 @@ export const isReference = (input) => { export const sortNameAlphabetically = (a, b) => { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }; + +/** + * Builds path to Roadmap page pre-filtered by + * work item iid + * + * @param {string} fullPath the path to the group + * @param {string} iid the iid of the work item + */ +export const workItemRoadmapPath = (fullPath, iid) => { + const domain = gon.relative_url_root || ''; + return `${domain}/groups/${fullPath}/-/roadmap?epic_iid=${iid}`; +}; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ad39104531418..b772bf9ce6f08 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -58330,6 +58330,9 @@ msgstr "" msgid "WorkItem|Mint green" msgstr "" +msgid "WorkItem|More actions" +msgstr "" + msgid "WorkItem|Must be a valid hex code" msgstr "" @@ -58573,6 +58576,9 @@ msgstr "" msgid "WorkItem|View current version" msgstr "" +msgid "WorkItem|View on a roadmap" +msgstr "" + msgid "WorkItem|Work item" msgstr "" diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_actions_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_actions_spec.js new file mode 100644 index 0000000000000..d2cc46ee45931 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_actions_spec.js @@ -0,0 +1,54 @@ +import { GlDisclosureDropdown } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +import WorkItemTreeActions from '~/work_items/components/work_item_links/work_item_tree_actions.vue'; + +describe('WorkItemTreeActions', () => { + /** + * @type {import('@vue/test-utils').Wrapper} + */ + let wrapper; + + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findTooltip = () => getBinding(findDropdown().element, 'gl-tooltip'); + const findDropdownButton = () => findDropdown().find('button'); + const findLink = () => findDropdown().find('a'); + + const createComponent = () => { + wrapper = mount(WorkItemTreeActions, { + propsData: { + workItemIid: '2', + fullPath: 'project/group', + }, + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('contains the correct tooltip text', () => { + expect(findTooltip().value).toBe('More actions'); + }); + + it('does not render the tooltip when the dropdown is shown', async () => { + await findDropdownButton().trigger('click'); + + await nextTick(); + + expect(findTooltip().value).toBe(''); + }); + + it('contains a link to the roadmap page', () => { + const link = findLink(); + + expect(link.text()).toBe('View on a roadmap'); + + expect(link.attributes('href')).toBe('/groups/project/group/-/roadmap?epic_iid=2'); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js index 49a674e73c8b5..237dfb6ee2c22 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -9,11 +9,14 @@ import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue'; import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue'; +import WorkItemTreeActions from '~/work_items/components/work_item_links/work_item_tree_actions.vue'; import getAllowedWorkItemChildTypes from '~/work_items//graphql/work_item_allowed_children.query.graphql'; import { FORM_TYPES, WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_KEY_RESULT, + WORK_ITEM_TYPE_VALUE_EPIC, + WORK_ITEM_TYPE_VALUE_OBJECTIVE, } from '~/work_items/constants'; import { childrenWorkItems, allowedChildrenTypesResponse } from '../../mock_data'; @@ -28,11 +31,13 @@ describe('WorkItemTree', () => { const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper); const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper); const findShowLabelsToggle = () => wrapper.findComponent(GlToggle); + const findTreeActions = () => wrapper.findComponent(WorkItemTreeActions); const allowedChildrenTypesHandler = jest.fn().mockResolvedValue(allowedChildrenTypesResponse); const createComponent = ({ workItemType = 'Objective', + workItemIid = '2', parentWorkItemType = 'Objective', confidential = false, children = childrenWorkItems, @@ -45,6 +50,7 @@ describe('WorkItemTree', () => { propsData: { fullPath: 'test/project', workItemType, + workItemIid, parentWorkItemType, workItemId: 'gid://gitlab/WorkItem/515', confidential, @@ -163,4 +169,19 @@ describe('WorkItemTree', () => { expect(findWorkItemLinkChildrenWrapper().props('showLabels')).toBe(toggleValue); }, ); + + describe('action menu', () => { + it.each` + visible | workItemType + ${true} | ${WORK_ITEM_TYPE_VALUE_EPIC} + ${false} | ${WORK_ITEM_TYPE_VALUE_OBJECTIVE} + `( + 'When displaying a $workItemType, it is $visible that the action menu is rendered', + ({ workItemType, visible }) => { + createComponent({ workItemType }); + + expect(findTreeActions().exists()).toBe(visible); + }, + ); + }); }); diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js index c3d6d55da802c..73d19598c4ddf 100644 --- a/spec/frontend/work_items/utils_spec.js +++ b/spec/frontend/work_items/utils_spec.js @@ -3,6 +3,7 @@ import { markdownPreviewPath, isReference, getWorkItemIcon, + workItemRoadmapPath, } from '~/work_items/utils'; describe('autocompleteDataSources', () => { @@ -72,3 +73,10 @@ describe('isReference', () => { expect(isReference(referenceId)).toBe(result); }); }); + +describe('workItemRoadmapPath', () => { + it('constructs a path to the roadmap page', () => { + const path = workItemRoadmapPath('project/group', '2'); + expect(path).toBe('/groups/project/group/-/roadmap?epic_iid=2'); + }); +}); -- GitLab