Skip to content
代码片段 群组 项目
未验证 提交 be757ff7 编辑于 作者: Jack Chapman's avatar Jack Chapman 提交者: GitLab
浏览文件

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
上级 f01db1d0
No related branches found
No related tags found
无相关合并请求
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
WORK_ITEMS_TYPE_MAP, WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT, WORK_ITEM_TYPE_ENUM_KEY_RESULT,
WORK_ITEM_TYPE_ENUM_EPIC,
I18N_WORK_ITEM_SHOW_LABELS, I18N_WORK_ITEM_SHOW_LABELS,
CHILD_ITEMS_ANCHOR, CHILD_ITEMS_ANCHOR,
} from '../../constants'; } from '../../constants';
...@@ -18,6 +19,7 @@ import WidgetWrapper from '../widget_wrapper.vue'; ...@@ -18,6 +19,7 @@ import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemActionsSplitButton from './work_item_actions_split_button.vue'; import WorkItemActionsSplitButton from './work_item_actions_split_button.vue';
import WorkItemLinksForm from './work_item_links_form.vue'; import WorkItemLinksForm from './work_item_links_form.vue';
import WorkItemChildrenWrapper from './work_item_children_wrapper.vue'; import WorkItemChildrenWrapper from './work_item_children_wrapper.vue';
import WorkItemTreeActions from './work_item_tree_actions.vue';
export default { export default {
FORM_TYPES, FORM_TYPES,
...@@ -29,6 +31,7 @@ export default { ...@@ -29,6 +31,7 @@ export default {
WidgetWrapper, WidgetWrapper,
WorkItemLinksForm, WorkItemLinksForm,
WorkItemChildrenWrapper, WorkItemChildrenWrapper,
WorkItemTreeActions,
GlToggle, GlToggle,
}, },
props: { props: {
...@@ -121,6 +124,14 @@ export default { ...@@ -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: { methods: {
genericActionItems(workItem) { genericActionItems(workItem) {
...@@ -179,7 +190,12 @@ export default { ...@@ -179,7 +190,12 @@ export default {
label-id="relationship-toggle-labels" label-id="relationship-toggle-labels"
@change="showLabels = $event" @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>
<template #body> <template #body>
<div class="gl-new-card-content gl-px-0"> <div class="gl-new-card-content gl-px-0">
......
<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>
...@@ -82,3 +82,15 @@ export const isReference = (input) => { ...@@ -82,3 +82,15 @@ export const isReference = (input) => {
export const sortNameAlphabetically = (a, b) => { export const sortNameAlphabetically = (a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); 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}`;
};
...@@ -58330,6 +58330,9 @@ msgstr "" ...@@ -58330,6 +58330,9 @@ msgstr ""
msgid "WorkItem|Mint green" msgid "WorkItem|Mint green"
msgstr "" msgstr ""
   
msgid "WorkItem|More actions"
msgstr ""
msgid "WorkItem|Must be a valid hex code" msgid "WorkItem|Must be a valid hex code"
msgstr "" msgstr ""
   
...@@ -58573,6 +58576,9 @@ msgstr "" ...@@ -58573,6 +58576,9 @@ msgstr ""
msgid "WorkItem|View current version" msgid "WorkItem|View current version"
msgstr "" msgstr ""
   
msgid "WorkItem|View on a roadmap"
msgstr ""
msgid "WorkItem|Work item" msgid "WorkItem|Work item"
msgstr "" msgstr ""
   
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');
});
});
...@@ -9,11 +9,14 @@ import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree ...@@ -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 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 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 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 getAllowedWorkItemChildTypes from '~/work_items//graphql/work_item_allowed_children.query.graphql';
import { import {
FORM_TYPES, FORM_TYPES,
WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT, WORK_ITEM_TYPE_ENUM_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_EPIC,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
} from '~/work_items/constants'; } from '~/work_items/constants';
import { childrenWorkItems, allowedChildrenTypesResponse } from '../../mock_data'; import { childrenWorkItems, allowedChildrenTypesResponse } from '../../mock_data';
...@@ -28,11 +31,13 @@ describe('WorkItemTree', () => { ...@@ -28,11 +31,13 @@ describe('WorkItemTree', () => {
const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper); const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper); const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
const findShowLabelsToggle = () => wrapper.findComponent(GlToggle); const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
const findTreeActions = () => wrapper.findComponent(WorkItemTreeActions);
const allowedChildrenTypesHandler = jest.fn().mockResolvedValue(allowedChildrenTypesResponse); const allowedChildrenTypesHandler = jest.fn().mockResolvedValue(allowedChildrenTypesResponse);
const createComponent = ({ const createComponent = ({
workItemType = 'Objective', workItemType = 'Objective',
workItemIid = '2',
parentWorkItemType = 'Objective', parentWorkItemType = 'Objective',
confidential = false, confidential = false,
children = childrenWorkItems, children = childrenWorkItems,
...@@ -45,6 +50,7 @@ describe('WorkItemTree', () => { ...@@ -45,6 +50,7 @@ describe('WorkItemTree', () => {
propsData: { propsData: {
fullPath: 'test/project', fullPath: 'test/project',
workItemType, workItemType,
workItemIid,
parentWorkItemType, parentWorkItemType,
workItemId: 'gid://gitlab/WorkItem/515', workItemId: 'gid://gitlab/WorkItem/515',
confidential, confidential,
...@@ -163,4 +169,19 @@ describe('WorkItemTree', () => { ...@@ -163,4 +169,19 @@ describe('WorkItemTree', () => {
expect(findWorkItemLinkChildrenWrapper().props('showLabels')).toBe(toggleValue); 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);
},
);
});
}); });
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
markdownPreviewPath, markdownPreviewPath,
isReference, isReference,
getWorkItemIcon, getWorkItemIcon,
workItemRoadmapPath,
} from '~/work_items/utils'; } from '~/work_items/utils';
describe('autocompleteDataSources', () => { describe('autocompleteDataSources', () => {
...@@ -72,3 +73,10 @@ describe('isReference', () => { ...@@ -72,3 +73,10 @@ describe('isReference', () => {
expect(isReference(referenceId)).toBe(result); 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');
});
});
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册