Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/work_items')
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue35
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue6
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue150
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue6
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue4
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue11
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue3
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue25
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue145
-rw-r--r--app/assets/javascripts/work_items/components/widget_wrapper.vue19
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue80
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue3
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue94
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue61
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue185
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_badge.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue2
-rw-r--r--app/assets/javascripts/work_items/constants.js27
-rw-r--r--app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql11
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql27
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue25
-rw-r--r--app/assets/javascripts/work_items/list/index.js15
-rw-r--r--app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql38
-rw-r--r--app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql5
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue11
-rw-r--r--app/assets/javascripts/work_items/utils.js23
35 files changed, 816 insertions, 292 deletions
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue b/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
index 1ead16c944b..425679c1400 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
@@ -1,13 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Tracking from '~/tracking';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
LocalStorageSync,
},
mixins: [Tracking.mixin()],
@@ -25,7 +24,7 @@ export default {
type: String,
required: true,
},
- filterOptions: {
+ items: {
type: Array,
required: true,
},
@@ -63,8 +62,7 @@ export default {
},
selectedSortOption() {
return (
- this.filterOptions.find(({ key }) => this.sortFilterProp === key) ||
- this.defaultSortFilterProp
+ this.items.find(({ key }) => this.sortFilterProp === key) || this.defaultSortFilterProp
);
},
},
@@ -94,23 +92,14 @@ export default {
as-string
@input="setDiscussionFilterOption"
/>
- <gl-dropdown
- class="gl-xs-w-full"
- size="small"
- :text="getDropdownSelectedText"
+ <gl-collapsible-listbox
+ :toggle-text="getDropdownSelectedText"
:disabled="loading"
- right
- >
- <gl-dropdown-item
- v-for="{ text, key, testid } in filterOptions"
- :key="text"
- :data-testid="testid"
- is-check-item
- :is-checked="isSortDropdownItemActive(key)"
- @click="fetchFilteredDiscussions(key)"
- >
- {{ text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ :items="items"
+ :selected="sortFilterProp"
+ placement="right"
+ size="small"
+ @select="fetchFilteredDiscussions"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index 66ad3d50287..57faed61280 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -74,6 +74,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -263,6 +268,7 @@ export default {
:work-item-id="workItemId"
:autofocus="autofocus"
:comment-button-text="commentButtonText"
+ :is-work-item-confidential="isWorkItemConfidential"
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
@error="$emit('error', $event)"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index b143c529014..a79169bde1e 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -3,11 +3,13 @@ import { GlButton, GlFormCheckbox, GlIcon, GlTooltipDirective } from '@gitlab/ui
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import { STATE_OPEN, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items/constants';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
export default {
i18n: {
@@ -22,6 +24,7 @@ export default {
markdownDocsPath: helpPagePath('user/markdown'),
},
components: {
+ CommentFieldLayout,
GlButton,
MarkdownEditor,
GlFormCheckbox,
@@ -89,6 +92,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -119,6 +127,23 @@ export default {
commentButtonTextComputed() {
return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText;
},
+ workItemDocPath() {
+ return this.workItemType === TASK_TYPE_NAME ? 'user/tasks.html' : 'user/okrs.html';
+ },
+ workItemDocAnchor() {
+ return this.workItemType === TASK_TYPE_NAME ? 'confidential-tasks' : 'confidential-okrs';
+ },
+ getWorkItemData() {
+ return {
+ confidential: this.isWorkItemConfidential,
+ confidential_issues_docs_path: helpPagePath(this.workItemDocPath, {
+ anchor: this.workItemDocAnchor,
+ }),
+ };
+ },
+ workItemTypeKey() {
+ return capitalizeFirstCharacter(this.workItemType).replace(' ', '');
+ },
},
methods: {
setCommentText(newText) {
@@ -158,66 +183,73 @@ export default {
<template>
<div class="timeline-discussion-body gl-overflow-visible!">
<div class="note-body gl-p-0! gl-overflow-visible!">
- <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
- <markdown-editor
- :value="commentText"
- :render-markdown-path="markdownPreviewPath"
- :markdown-docs-path="$options.constantOptions.markdownDocsPath"
- :autocomplete-data-sources="autocompleteDataSources"
- :form-field-props="formFieldProps"
- :add-spacing-classes="false"
- data-testid="work-item-add-comment"
- class="gl-mb-5"
- use-bottom-toolbar
- supports-quick-actions
- :autofocus="autofocus"
- @input="setCommentText"
- @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })"
- @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })"
- @keydown.esc.stop="cancelEditing"
- />
- <gl-form-checkbox
- v-if="isNewDiscussion"
- v-model="isNoteInternal"
- class="gl-mb-2"
- data-testid="internal-note-checkbox"
+ <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1 new-note">
+ <comment-field-layout
+ :with-alert-container="isWorkItemConfidential"
+ :noteable-data="getWorkItemData"
+ :noteable-type="workItemTypeKey"
>
- {{ $options.i18n.internal }}
- <gl-icon
- v-gl-tooltip:tooltipcontainer.bottom
- name="question-o"
- :size="16"
- :title="$options.i18n.internalVisibility"
- class="gl-text-blue-500"
+ <markdown-editor
+ :value="commentText"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="$options.constantOptions.markdownDocsPath"
+ :autocomplete-data-sources="autocompleteDataSources"
+ :form-field-props="formFieldProps"
+ :add-spacing-classes="false"
+ data-testid="work-item-add-comment"
+ use-bottom-toolbar
+ supports-quick-actions
+ :autofocus="autofocus"
+ @input="setCommentText"
+ @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })"
+ @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })"
+ @keydown.esc.stop="cancelEditing"
+ />
+ </comment-field-layout>
+ <div class="note-form-actions">
+ <gl-form-checkbox
+ v-if="isNewDiscussion"
+ v-model="isNoteInternal"
+ class="gl-mb-2"
+ data-testid="internal-note-checkbox"
+ >
+ {{ $options.i18n.internal }}
+ <gl-icon
+ v-gl-tooltip:tooltipcontainer.bottom
+ name="question-o"
+ :size="16"
+ :title="$options.i18n.internalVisibility"
+ class="gl-text-blue-500"
+ />
+ </gl-form-checkbox>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ data-testid="confirm-button"
+ :disabled="!commentText.length"
+ :loading="isSubmitting"
+ @click="$emit('submitForm', { commentText, isNoteInternal })"
+ >{{ commentButtonTextComputed }}
+ </gl-button>
+ <work-item-state-toggle-button
+ v-if="isNewDiscussion"
+ class="gl-ml-3"
+ :work-item-id="workItemId"
+ :work-item-state="workItemState"
+ :work-item-type="workItemType"
+ can-update
+ @error="$emit('error', $event)"
/>
- </gl-form-checkbox>
- <gl-button
- category="primary"
- variant="confirm"
- data-testid="confirm-button"
- :disabled="!commentText.length"
- :loading="isSubmitting"
- @click="$emit('submitForm', { commentText, isNoteInternal })"
- >{{ commentButtonTextComputed }}
- </gl-button>
- <work-item-state-toggle-button
- v-if="isNewDiscussion"
- class="gl-ml-3"
- :work-item-id="workItemId"
- :work-item-state="workItemState"
- :work-item-type="workItemType"
- can-update
- @error="$emit('error', $event)"
- />
- <gl-button
- v-else
- data-testid="cancel-button"
- category="primary"
- class="gl-ml-3"
- :loading="updateInProgress"
- @click="cancelEditing"
- >{{ $options.i18n.cancelButtonText }}
- </gl-button>
+ <gl-button
+ v-else
+ data-testid="cancel-button"
+ category="primary"
+ class="gl-ml-3"
+ :loading="updateInProgress"
+ @click="cancelEditing"
+ >{{ $options.i18n.cancelButtonText }}
+ </gl-button>
+ </div>
</form>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index f030363664f..fd8842aa01a 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -65,6 +65,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -235,6 +240,7 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
:is-internal-thread="note.internal"
+ :is-work-item-confidential="isWorkItemConfidential"
@startReplying="showReplyForm"
@cancelEditing="hideReplyForm"
@replied="onReplied"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index 92560f2da9e..b5e3ea68725 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -163,6 +163,9 @@ export default {
projectName() {
return this.workItem?.project?.name;
},
+ isWorkItemConfidential() {
+ return this.workItem?.confidential;
+ },
},
apollo: {
workItem: {
@@ -314,6 +317,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:work-item-id="workItemId"
:autofocus="isEditing"
+ :is-work-item-confidential="isWorkItemConfidential"
class="gl-pl-3 gl-mt-3"
@cancelEditing="isEditing = false"
@submitForm="updateNote"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
index 0c1419e983f..1578c78ac4f 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
@@ -64,7 +64,7 @@ export default {
:work-item-type="workItemType"
:loading="disableActivityFilterSort"
:sort-filter-prop="discussionFilter"
- :filter-options="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
+ :items="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
:storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY"
:default-sort-filter-prop="$options.WORK_ITEM_NOTES_FILTER_ALL_NOTES"
tracking-action="work_item_notes_filter_changed"
@@ -77,7 +77,7 @@ export default {
:work-item-type="workItemType"
:loading="disableActivityFilterSort"
:sort-filter-prop="sortOrder"
- :filter-options="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
+ :items="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
:storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
:default-sort-filter-prop="$options.ASC"
tracking-action="work_item_notes_sort_order_changed"
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
index 0a38dcb77f6..f50cfac90f7 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -43,15 +43,6 @@ export default {
type: Boolean,
required: true,
},
- parentWorkItemId: {
- type: String,
- required: true,
- },
- workItemType: {
- type: String,
- required: false,
- default: '',
- },
childPath: {
type: String,
required: true,
@@ -158,7 +149,7 @@ export default {
</span>
<gl-link
:href="childPath"
- class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
+ class="gl-text-truncate gl-font-weight-semibold"
data-testid="item-title"
@click="$emit('click', $event)"
@mouseover="$emit('mouseover')"
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
index ddeac2b92ae..38d8d239a7e 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
@@ -42,7 +42,8 @@ export default {
assigneesContainerClass() {
if (this.assignees.length === 2) {
return 'fixed-width-avatars-2';
- } else if (this.assignees.length > 2) {
+ }
+ if (this.assignees.length > 2) {
return 'fixed-width-avatars-3';
}
return '';
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
index 53e8eedf060..12b7bade31d 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
@@ -1,29 +1,28 @@
<script>
-import { GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
export default {
components: {
- GlDropdownItem,
- GlDropdown,
- GlIcon,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdown,
},
};
</script>
<template>
<div class="gl-ml-5">
- <gl-dropdown
+ <gl-disclosure-dropdown
category="tertiary"
toggle-class="btn-icon btn-sm"
- :right="true"
+ icon="ellipsis_v"
data-testid="work_items_links_menu"
+ :aria-label="__(`More actions`)"
+ text-sr-only
+ no-caret
>
- <template #button-content>
- <gl-icon name="ellipsis_v" :size="14" />
- </template>
- <gl-dropdown-item @click="$emit('removeChild')">
- {{ s__('WorkItem|Remove') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-disclosure-dropdown-item @action="$emit('removeChild')">
+ <template #list-item>{{ s__('WorkItem|Remove') }}</template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
new file mode 100644
index 00000000000..7b38e838033
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -0,0 +1,145 @@
+<script>
+import { GlTokenSelector } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
+import {
+ WORK_ITEMS_TYPE_MAP,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
+ sprintfWorkItem,
+} from '../../constants';
+
+export default {
+ components: {
+ GlTokenSelector,
+ },
+ props: {
+ value: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ childrenType: {
+ type: String,
+ required: false,
+ default: WORK_ITEM_TYPE_ENUM_TASK,
+ },
+ childrenIds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ parentWorkItemId: {
+ type: String,
+ required: true,
+ },
+ areWorkItemsToAddValid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ apollo: {
+ availableWorkItems: {
+ query: projectWorkItemsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ searchTerm: this.search?.title || this.search,
+ types: [this.childrenType],
+ in: this.search ? 'TITLE' : undefined,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace.workItems.nodes.filter(
+ (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
+ );
+ },
+ },
+ },
+ data() {
+ return {
+ availableWorkItems: [],
+ search: '',
+ searchStarted: false,
+ };
+ },
+ computed: {
+ workItemsToAdd: {
+ get() {
+ return this.value;
+ },
+ set(workItemsToAdd) {
+ this.$emit('input', workItemsToAdd);
+ },
+ },
+ isLoading() {
+ return this.$apollo.queries.availableWorkItems.loading;
+ },
+ addInputPlaceholder() {
+ return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
+ },
+ childrenTypeName() {
+ return WORK_ITEMS_TYPE_MAP[this.childrenType]?.name;
+ },
+ tokenSelectorContainerClass() {
+ return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ methods: {
+ getIdFromGraphQLId,
+ setSearchKey(value) {
+ this.search = value;
+ },
+ handleFocus() {
+ this.searchStarted = true;
+ },
+ handleMouseOver() {
+ this.timeout = setTimeout(() => {
+ this.searchStarted = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ handleMouseOut() {
+ clearTimeout(this.timeout);
+ },
+ },
+};
+</script>
+<template>
+ <gl-token-selector
+ v-model="workItemsToAdd"
+ :dropdown-items="availableWorkItems"
+ :loading="isLoading"
+ :placeholder="addInputPlaceholder"
+ menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
+ :container-class="tokenSelectorContainerClass"
+ data-testid="work-item-token-select-input"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
+ >
+ <template #token-content="{ token }">
+ {{ token.title }}
+ </template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <div class="gl-display-flex">
+ <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
+ <div class="gl-text-truncate">{{ dropdownItem.title }}</div>
+ </div>
+ </template>
+ </gl-token-selector>
+</template>
diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue
index f343f787358..27de858fe4e 100644
--- a/app/assets/javascripts/work_items/components/widget_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue
@@ -14,6 +14,11 @@ export default {
required: false,
default: '',
},
+ widgetName: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -30,6 +35,12 @@ export default {
isOpenString() {
return this.isOpen ? 'true' : 'false';
},
+ anchorLink() {
+ return `#${this.widgetName}`;
+ },
+ anchorLinkId() {
+ return `user-content-${this.widgetName}-links`;
+ },
},
methods: {
hide() {
@@ -46,14 +57,14 @@ export default {
</script>
<template>
- <div id="tasks" class="gl-new-card" :aria-expanded="isOpenString">
+ <div :id="widgetName" class="gl-new-card" :aria-expanded="isOpenString">
<div class="gl-new-card-header">
<div class="gl-new-card-title-wrapper">
<h3 class="gl-new-card-title">
<gl-link
- id="user-content-tasks-links"
- class="anchor position-absolute gl-text-decoration-none"
- href="#tasks"
+ :id="anchorLinkId"
+ class="gl-text-decoration-none"
+ :href="anchorLink"
aria-hidden="true"
/>
<slot name="header"></slot>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index e8fe64c932b..18aa4d55086 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -1,8 +1,7 @@
<script>
import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownForm,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlDropdownDivider,
GlModal,
GlModalDirective,
@@ -53,9 +52,8 @@ export default {
emailAddressCopied: __('Email address copied'),
},
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownForm,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlDropdownDivider,
GlModal,
GlToggle,
@@ -180,13 +178,16 @@ export default {
navigator.clipboard.writeText(text);
}
toast(message);
+ this.closeDropdown();
},
handleToggleWorkItemConfidentiality() {
this.track('click_toggle_work_item_confidentiality');
this.$emit('toggleWorkItemConfidentiality', !this.isConfidential);
+ this.closeDropdown();
},
handleDelete() {
this.$refs.modal.show();
+ this.closeDropdown();
},
handleDeleteWorkItem() {
this.track('click_delete_work_item');
@@ -275,6 +276,9 @@ export default {
throwConvertError() {
this.$emit('error', this.i18n.convertError);
},
+ closeDropdown() {
+ this.$refs.workItemsMoreActions.close();
+ },
async promoteToObjective() {
try {
const {
@@ -300,6 +304,8 @@ export default {
} catch (error) {
this.throwConvertError();
Sentry.captureException(error);
+ } finally {
+ this.closeDropdown();
}
},
},
@@ -308,77 +314,87 @@ export default {
<template>
<div>
- <gl-dropdown
+ <gl-disclosure-dropdown
+ ref="workItemsMoreActions"
icon="ellipsis_v"
data-testid="work-item-actions-dropdown"
text-sr-only
:text="__('More actions')"
category="tertiary"
+ :auto-close="false"
no-caret
right
>
<template v-if="$options.isLoggedIn">
- <gl-dropdown-form
- class="work-item-notifications-form"
+ <gl-disclosure-dropdown-item
+ class="gl-display-flex gl-justify-content-end gl-w-full"
:data-testid="$options.notificationsToggleFormTestId"
>
- <div class="gl-px-5 gl-pb-2 gl-pt-1">
+ <template #list-item>
<gl-toggle
:value="subscribedToNotifications"
:label="$options.i18n.notifications"
:data-testid="$options.notificationsToggleTestId"
+ class="work-item-notification-toggle"
label-position="left"
label-id="notifications-toggle"
@change="toggleNotifications($event)"
/>
- </div>
- </gl-dropdown-form>
+ </template>
+ </gl-disclosure-dropdown-item>
<gl-dropdown-divider />
</template>
- <gl-dropdown-item
+
+ <gl-disclosure-dropdown-item
v-if="canPromoteToObjective"
:data-testid="$options.promoteActionTestId"
- @click="promoteToObjective"
+ @action="promoteToObjective"
>
- {{ __('Promote to objective') }}
- </gl-dropdown-item>
+ <template #list-item>{{ __('Promote to objective') }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="canUpdate && !isParentConfidential">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
:data-testid="$options.confidentialityTestId"
- @click="handleToggleWorkItemConfidentiality"
- >{{
+ @action="handleToggleWorkItemConfidentiality"
+ ><template #list-item>{{
isConfidential
? $options.i18n.disableTaskConfidentiality
: $options.i18n.enableTaskConfidentiality
- }}</gl-dropdown-item
+ }}</template></gl-disclosure-dropdown-item
>
</template>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
ref="workItemReference"
:data-testid="$options.copyReferenceTestId"
:data-clipboard-text="workItemReference"
- @click="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
- >{{ $options.i18n.copyReference }}</gl-dropdown-item
+ @action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
+ ><template #list-item>{{
+ $options.i18n.copyReference
+ }}</template></gl-disclosure-dropdown-item
>
<template v-if="$options.isLoggedIn && workItemCreateNoteEmail">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
ref="workItemCreateNoteEmail"
:data-testid="$options.copyCreateNoteEmailTestId"
:data-clipboard-text="workItemCreateNoteEmail"
- @click="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
- >{{ i18n.copyCreateNoteEmail }}</gl-dropdown-item
+ @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
+ ><template #list-item>{{
+ i18n.copyCreateNoteEmail
+ }}</template></gl-disclosure-dropdown-item
>
- <gl-dropdown-divider v-if="canDelete" />
</template>
- <gl-dropdown-item
+ <gl-dropdown-divider v-if="canDelete" />
+ <gl-disclosure-dropdown-item
v-if="canDelete"
:data-testid="$options.deleteActionTestId"
variant="danger"
- @click="handleDelete"
+ @action="handleDelete"
>
- {{ i18n.deleteWorkItem }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item
+ ><span class="text-danger">{{ i18n.deleteWorkItem }}</span></template
+ >
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
<gl-modal
ref="modal"
modal-id="work-item-confirm-delete"
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 4b4aa7f96ca..f9527884adc 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -388,7 +388,7 @@ export default {
:display-text="__('Invite members')"
trigger-element="side-nav"
icon="plus"
- trigger-source="work-item-assignees-dropdown"
+ trigger-source="work_item_assignees_dropdown"
classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2"
/>
</gl-dropdown-item>
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index f93ea4a0753..14e55134048 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -84,13 +84,13 @@ export default {
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<confidentiality-badge
v-if="isWorkItemConfidential"
- class="gl-vertical-align-middle gl-display-inline-flex!"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
+ class="gl-vertical-align-middle gl-display-inline-flex! gl-mr-2"
:issuable-type="workItemType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ hide-text-in-small-screens
/>
<work-item-type-icon
- class="gl-vertical-align-middle gl-mr-0!"
+ class="gl-vertical-align-middle"
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType"
show-text
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 d826ef9cbe7..edecd7addcc 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -31,6 +31,7 @@ import {
WORK_ITEM_TYPE_VALUE_ISSUE,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WIDGET_TYPE_NOTES,
+ WIDGET_TYPE_LINKED_ITEMS,
} from '../constants';
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
@@ -50,6 +51,7 @@ import WorkItemNotes from './work_item_notes.vue';
import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
+import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
export default {
i18n,
@@ -79,6 +81,7 @@ export default {
AbuseCategorySelector,
GlIntersectionObserver,
ConfidentialityBadge,
+ WorkItemRelationships,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'reportAbusePath'],
@@ -259,6 +262,15 @@ export default {
showIntersectionObserver() {
return !this.isModal && this.workItemsMvc2Enabled;
},
+ hasLinkedWorkItems() {
+ return this.glFeatures.linkedWorkItems;
+ },
+ workItemLinkedItems() {
+ return this.isWidgetPresent(WIDGET_TYPE_LINKED_ITEMS);
+ },
+ showWorkItemLinkedItems() {
+ return this.hasLinkedWorkItems && this.workItemLinkedItems;
+ },
},
mounted() {
if (this.modalWorkItemIid) {
@@ -515,9 +527,9 @@ export default {
<gl-loading-icon v-if="updateInProgress" class="gl-mr-3" />
<confidentiality-badge
v-if="workItem.confidential"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
+ class="gl-mr-3"
:issuable-type="workItemType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
/>
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
@@ -591,6 +603,12 @@ export default {
@show-modal="openInModal"
@addChild="$emit('addChild')"
/>
+ <work-item-relationships
+ v-if="showWorkItemLinkedItems"
+ :work-item-iid="workItemIid"
+ :work-item-full-path="workItem.project.fullPath"
+ @showModal="openInModal"
+ />
<work-item-notes
v-if="workItemNotes"
:work-item-id="workItem.id"
@@ -600,6 +618,7 @@ export default {
:assignees="workItemAssignees && workItemAssignees.assignees.nodes"
:can-set-work-item-metadata="canAssignUnassignUser"
:report-abuse-path="reportAbusePath"
+ :is-work-item-confidential="workItem.confidential"
class="gl-pt-5"
@error="updateError = $event"
@has-notes="updateHasNotes"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
index bf427feaa35..9d9414b5399 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
@@ -56,14 +56,14 @@ export default {
return isLoggedIn() && this.canUpdate;
},
treeRootWrapper() {
- return this.canReorder ? Draggable : 'div';
+ return this.canReorder ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
...defaultSortableOptions,
fallbackOnBody: false,
group: 'sortable-container',
- tag: 'div',
+ tag: 'ul',
'ghost-class': 'tree-item-drag-active',
'data-parent-id': this.workItemId,
value: this.children,
@@ -248,6 +248,7 @@ export default {
<component
:is="treeRootWrapper"
v-bind="treeRootOptions"
+ class="content-list"
:class="{ 'gl-cursor-grab sortable-container': canReorder }"
@end="handleDragOnEnd"
>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index a9b0c2b98bf..679287338c8 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -13,6 +13,7 @@ import {
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
+import { workItemPath } from '../../utils';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
import WorkItemTreeChildren from './work_item_tree_children.vue';
@@ -90,7 +91,7 @@ export default {
return this.isItemOpen ? __('Created') : __('Closed');
},
childPath() {
- return `${gon?.relative_url_root || ''}/${this.fullPath}/-/work_items/${this.childItem.iid}`;
+ return workItemPath(this.fullPath, this.childItem.iid);
},
chevronType() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
@@ -212,7 +213,7 @@ export default {
</script>
<template>
- <div class="tree-item">
+ <li class="tree-item">
<div
class="gl-display-flex gl-align-items-flex-start"
:class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }"
@@ -249,5 +250,5 @@ export default {
@removeChild="removeChild"
@click="$emit('click', $event)"
/>
- </div>
+ </li>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index a0ff693e156..eb836007e75 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -103,6 +103,7 @@ export default {
isReportDrawerOpen: false,
reportedUserId: 0,
reportedUrl: '',
+ widgetName: 'tasks',
};
},
computed: {
@@ -166,7 +167,6 @@ export default {
this.updateWorkItemIdUrlQuery(child);
},
async closeModal() {
- this.activeChild = {};
this.updateWorkItemIdUrlQuery();
},
handleWorkItemDeleted(child) {
@@ -206,6 +206,7 @@ export default {
<widget-wrapper
ref="wrapper"
:error="error"
+ :widget-name="widgetName"
data-testid="work-item-links"
@dismissAlert="error = undefined"
>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 4960189fb48..55440e1603c 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -3,19 +3,15 @@ import {
GlAlert,
GlFormGroup,
GlForm,
- GlTokenSelector,
GlButton,
GlFormInput,
GlFormCheckbox,
GlTooltip,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
+import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import { addHierarchyChild } from '../../graphql/cache_utils';
import projectWorkItemTypesQuery from '../../graphql/project_work_item_types.query.graphql';
-import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
import {
@@ -23,7 +19,6 @@ import {
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_TASK,
I18N_WORK_ITEM_CREATE_BUTTON_LABEL,
- I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
I18N_WORK_ITEM_ADD_BUTTON_LABEL,
I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL,
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL,
@@ -35,12 +30,12 @@ export default {
components: {
GlAlert,
GlForm,
- GlTokenSelector,
GlButton,
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlTooltip,
+ WorkItemTokenInput,
},
inject: ['fullPath', 'hasIterationsFeature'],
props: {
@@ -101,35 +96,14 @@ export default {
return data.workspace?.workItemTypes?.nodes;
},
},
- availableWorkItems: {
- query: projectWorkItemsQuery,
- variables() {
- return {
- fullPath: this.fullPath,
- searchTerm: this.search?.title || this.search,
- types: [this.childrenType],
- in: this.search ? 'TITLE' : undefined,
- };
- },
- skip() {
- return !this.searchStarted;
- },
- update(data) {
- return data.workspace.workItems.nodes.filter(
- (wi) => !this.childrenIds.includes(wi.id) && this.issuableGid !== wi.id,
- );
- },
- },
},
data() {
return {
workItemTypes: [],
- availableWorkItems: [],
- search: '',
- searchStarted: false,
+ workItemsToAdd: [],
error: null,
+ search: '',
childToCreateTitle: null,
- workItemsToAdd: [],
confidential: this.parentConfidential,
};
},
@@ -177,7 +151,8 @@ export default {
addOrCreateButtonLabel() {
if (this.isCreateForm) {
return sprintfWorkItem(I18N_WORK_ITEM_CREATE_BUTTON_LABEL, this.childrenTypeName);
- } else if (this.workItemsToAdd.length > 1) {
+ }
+ if (this.workItemsToAdd.length > 1) {
return sprintfWorkItem(I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL, this.childrenTypeName);
}
return sprintfWorkItem(I18N_WORK_ITEM_ADD_BUTTON_LABEL, this.childrenTypeName);
@@ -216,15 +191,6 @@ export default {
}
return this.workItemsToAdd.length === 0 || !this.areWorkItemsToAddValid;
},
- isLoading() {
- return this.$apollo.queries.availableWorkItems.loading;
- },
- addInputPlaceholder() {
- return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
- },
- tokenSelectorContainerClass() {
- return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
- },
invalidWorkItemsToAdd() {
return this.parentConfidential
? this.workItemsToAdd.filter((workItem) => !workItem.confidential)
@@ -249,11 +215,7 @@ export default {
);
},
},
- created() {
- this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
methods: {
- getIdFromGraphQLId,
getConfidentialityTooltipTarget() {
// We want tooltip to be anchored to `input` within checkbox component
// but `$el.querySelector('input')` doesn't work. 🤷‍♂️
@@ -317,20 +279,6 @@ export default {
this.childToCreateTitle = null;
});
},
- setSearchKey(value) {
- this.search = value;
- },
- handleFocus() {
- this.searchStarted = true;
- },
- handleMouseOver() {
- this.timeout = setTimeout(() => {
- this.searchStarted = true;
- }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
- handleMouseOut() {
- clearTimeout(this.timeout);
- },
},
i18n: {
inputLabel: __('Title'),
@@ -385,30 +333,16 @@ export default {
>{{ confidentialityCheckboxTooltip }}</gl-tooltip
>
<div class="gl-mb-4">
- <gl-token-selector
+ <work-item-token-input
v-if="!isCreateForm"
v-model="workItemsToAdd"
- :dropdown-items="availableWorkItems"
- :loading="isLoading"
- :placeholder="addInputPlaceholder"
- menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
- :container-class="tokenSelectorContainerClass"
- data-testid="work-item-token-select-input"
- @text-input="debouncedSearchKeyUpdate"
- @focus="handleFocus"
- @mouseover.native="handleMouseOver"
- @mouseout.native="handleMouseOut"
- >
- <template #token-content="{ token }">
- {{ token.title }}
- </template>
- <template #dropdown-item-content="{ dropdownItem }">
- <div class="gl-display-flex">
- <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
- <div class="gl-text-truncate">{{ dropdownItem.title }}</div>
- </div>
- </template>
- </gl-token-selector>
+ :is-create-form="isCreateForm"
+ :parent-work-item-id="issuableGid"
+ :children-type="childrenType"
+ :children-ids="childrenIds"
+ :are-work-items-to-add-valid="areWorkItemsToAddValid"
+ :full-path="fullPath"
+ />
<div
v-if="showWorkItemsToAddInvalidMessage"
class="gl-text-red-500"
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 246eac82c78..bc3f5201fb8 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
@@ -64,6 +64,7 @@ export default {
isShownAddForm: false,
formType: null,
childType: null,
+ widgetName: 'tasks',
};
},
computed: {
@@ -101,6 +102,7 @@ export default {
<template>
<widget-wrapper
ref="wrapper"
+ :widget-name="widgetName"
:error="error"
data-testid="work-item-tree"
@dismissAlert="error = undefined"
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 8fc460294e6..256f8ed53d1 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -79,6 +79,11 @@ export default {
type: String,
required: true,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -124,6 +129,7 @@ export default {
isNewDiscussion: true,
markdownPreviewPath: this.markdownPreviewPath,
autocompleteDataSources: this.autocompleteDataSources,
+ isWorkItemConfidential: this.isWorkItemConfidential,
};
},
notesArray() {
@@ -366,6 +372,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:assignees="assignees"
:can-set-work-item-metadata="canSetWorkItemMetadata"
+ :is-work-item-confidential="isWorkItemConfidential"
@deleteNote="showDeleteNoteModal($event, discussion)"
@reportAbuse="reportAbuse(true, $event)"
@error="$emit('error', $event)"
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
new file mode 100644
index 00000000000..cbe830f9565
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -0,0 +1,61 @@
+<script>
+import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
+import { workItemPath } from '../../utils';
+
+export default {
+ components: {
+ WorkItemLinkChildContents,
+ },
+ props: {
+ linkedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ heading: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ workItemFullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ linkedItemPath(fullPath, id) {
+ return workItemPath(fullPath, id);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h4
+ v-if="heading"
+ data-testid="work-items-list-heading"
+ class="gl-font-sm gl-font-weight-semibold gl-text-gray-700 gl-mx-2 gl-mt-3 gl-mb-2"
+ >
+ {{ heading }}
+ </h4>
+ <div class="work-items-list-body">
+ <ul ref="list" class="work-items-list content-list">
+ <li
+ v-for="linkedItem in linkedItems"
+ :key="linkedItem.workItem.id"
+ class="gl-pt-0! gl-pb-0! gl-border-b-0!"
+ >
+ <work-item-link-child-contents
+ :child-item="linkedItem.workItem"
+ :can-update="canUpdate"
+ :child-path="linkedItemPath(workItemFullPath, linkedItem.workItem.iid)"
+ @click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
+ />
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
new file mode 100644
index 00000000000..4f6879e9605
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
@@ -0,0 +1,185 @@
+<script>
+import { GlLoadingIcon, GlIcon, GlButton } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants';
+
+import WidgetWrapper from '../widget_wrapper.vue';
+import WorkItemRelationshipList from './work_item_relationship_list.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlIcon,
+ GlButton,
+ WidgetWrapper,
+ WorkItemRelationshipList,
+ },
+ props: {
+ workItemIid: {
+ type: String,
+ required: true,
+ },
+ workItemFullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ workItem: {
+ query: workItemByIidQuery,
+ variables() {
+ return {
+ fullPath: this.workItemFullPath,
+ iid: this.workItemIid,
+ };
+ },
+ update(data) {
+ return data.workspace.workItems.nodes[0] ?? {};
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ skip() {
+ return !this.workItemIid;
+ },
+ error(e) {
+ this.error = e.message || this.$options.i18n.fetchError;
+ },
+ async result() {
+ // When work items are switched in a modal, the data props are not getting reset.
+ // Thus, duplicating the work items in the list.
+ // Here, the existing list are cleared before the new items are pushed.
+ this.linksRelatesTo = [];
+ this.linksIsBlockedBy = [];
+ this.linksBlocks = [];
+
+ this.linkedWorkItems.forEach((item) => {
+ if (item.linkType === LINKED_CATEGORIES_MAP.RELATES_TO) {
+ this.linksRelatesTo.push(item);
+ } else if (item.linkType === LINKED_CATEGORIES_MAP.IS_BLOCKED_BY) {
+ this.linksIsBlockedBy.push(item);
+ } else if (item.linkType === LINKED_CATEGORIES_MAP.BLOCKS) {
+ this.linksBlocks.push(item);
+ }
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ error: '',
+ linksRelatesTo: [],
+ linksIsBlockedBy: [],
+ linksBlocks: [],
+ widgetName: 'linkeditems',
+ };
+ },
+ computed: {
+ canUpdate() {
+ // This will be false untill we implement remove item mutation
+ return false;
+ },
+ isLoading() {
+ return this.$apollo.queries.workItem.loading;
+ },
+ linkedWorkItemsWidget() {
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS);
+ },
+ linkedWorkItems() {
+ return this.linkedWorkItemsWidget?.linkedItems?.nodes || [];
+ },
+ linkedWorkItemsCount() {
+ return this.linkedWorkItems.length;
+ },
+ isEmptyRelatedWorkItems() {
+ return !this.error && this.linkedWorkItems.length === 0;
+ },
+ },
+ i18n: {
+ title: s__('WorkItem|Linked Items'),
+ fetchError: s__('WorkItem|Something went wrong when fetching tasks. Please refresh this page.'),
+ emptyStateMessage: s__(
+ "WorkItem|Link work items together to show that they're related or that one is blocking others.",
+ ),
+ addChildButtonLabel: s__('WorkItem|Add'),
+ relatedToTitle: s__('WorkItem|Related to'),
+ blockingTitle: s__('WorkItem|Blocking'),
+ blockedByTitle: s__('WorkItem|Blocked by'),
+ addLinkedWorkItemButtonLabel: s__('WorkItem|Add'),
+ },
+};
+</script>
+<template>
+ <widget-wrapper
+ :error="error"
+ class="work-item-relationships"
+ :widget-name="widgetName"
+ @dismissAlert="error = undefined"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper">
+ <h3 class="gl-new-card-title">
+ {{ $options.i18n.title }}
+ </h3>
+ <div v-if="linkedWorkItemsCount" class="gl-new-card-count">
+ <gl-icon name="link" class="gl-mr-2" />
+ <span data-testid="linked-items-count">{{ linkedWorkItemsCount }}</span>
+ </div>
+ </div>
+ </template>
+ <template #header-right>
+ <gl-button size="small" class="gl-ml-3">
+ <slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
+ </gl-button>
+ </template>
+ <template #body>
+ <div class="gl-new-card-content">
+ <gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" />
+ <template v-else>
+ <div v-if="isEmptyRelatedWorkItems" data-testid="links-empty">
+ <p class="gl-new-card-empty">
+ {{ $options.i18n.emptyStateMessage }}
+ </p>
+ </div>
+ <template v-else>
+ <work-item-relationship-list
+ v-if="linksBlocks.length"
+ :class="{
+ 'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
+ linksIsBlockedBy.length,
+ }"
+ :linked-items="linksBlocks"
+ :heading="$options.i18n.blockingTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ <work-item-relationship-list
+ v-if="linksIsBlockedBy.length"
+ :class="{
+ 'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
+ linksRelatesTo.length,
+ }"
+ :linked-items="linksIsBlockedBy"
+ :heading="$options.i18n.blockedByTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ <work-item-relationship-list
+ v-if="linksRelatesTo.length"
+ :linked-items="linksRelatesTo"
+ :heading="$options.i18n.relatedToTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ </template>
+ </template>
+ </div>
+ </template>
+ </widget-wrapper>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_state_badge.vue b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
index 1d1bc7352b1..5c5b41b38e6 100644
--- a/app/assets/javascripts/work_items/components/work_item_state_badge.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
@@ -1,11 +1,12 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { STATE_OPEN } from '../constants';
export default {
components: {
GlBadge,
+ GlIcon,
},
props: {
workItemState: {
@@ -31,11 +32,8 @@ export default {
</script>
<template>
- <gl-badge
- :icon="workItemStateIcon"
- :variant="workItemStateVariant"
- class="gl-mr-2 gl-vertical-align-middle"
- >
- {{ stateText }}
+ <gl-badge :variant="workItemStateVariant" class="gl-mr-2 gl-vertical-align-middle">
+ <gl-icon :name="workItemStateIcon" :size="16" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ stateText }}</span>
</gl-badge>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index f27ae5f4e6d..5426f3965b3 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -53,7 +53,7 @@ export default {
</script>
<template>
- <span class="gl-mr-2">
+ <span>
<gl-icon
v-gl-tooltip.hover="showTooltipOnHover"
:name="iconName"
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 57206550328..2b118247426 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -26,6 +26,7 @@ export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
export const WIDGET_TYPE_ITERATION = 'ITERATION';
export const WIDGET_TYPE_NOTES = 'NOTES';
export const WIDGET_TYPE_HEALTH_STATUS = 'HEALTH_STATUS';
+export const WIDGET_TYPE_LINKED_ITEMS = 'LINKED_ITEMS';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
@@ -35,6 +36,7 @@ export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT';
+export const WORK_ITEM_TYPE_VALUE_EPIC = 'Epic';
export const WORK_ITEM_TYPE_VALUE_INCIDENT = 'Incident';
export const WORK_ITEM_TYPE_VALUE_ISSUE = 'Issue';
export const WORK_ITEM_TYPE_VALUE_TASK = 'Task';
@@ -57,6 +59,9 @@ export const i18n = {
export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
'WorkItem|Something went wrong when fetching labels. Please try again.',
);
+export const I18N_WORK_ITEM_ERROR_FETCHING_TYPES = s__(
+ 'WorkItem|Something went wrong when fetching work item types. Please try again',
+);
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
);
@@ -208,24 +213,22 @@ export const WORK_ITEM_NOTES_FILTER_KEY = 'filter_key_work_item';
export const WORK_ITEM_ACTIVITY_FILTER_OPTIONS = [
{
- key: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ value: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
text: s__('WorkItem|All activity'),
},
{
- key: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ value: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
text: s__('WorkItem|Comments only'),
- testid: 'comments-activity',
},
{
- key: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+ value: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
text: s__('WorkItem|History only'),
- testid: 'history-activity',
},
];
export const WORK_ITEM_ACTIVITY_SORT_OPTIONS = [
- { key: DESC, text: __('Newest first'), testid: 'newest-first' },
- { key: ASC, text: __('Oldest first') },
+ { value: DESC, text: __('Newest first') },
+ { value: ASC, text: __('Oldest first') },
];
export const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
@@ -241,10 +244,6 @@ export const TODO_DONE_ICON = 'todo-done';
export const TODO_DONE_STATE = 'done';
export const TODO_PENDING_STATE = 'pending';
-export const CURRENT_USER_TODOS_TYPENAME = 'WorkItemWidgetCurrentUserTodos';
-
-export const EMOJI_ACTION_ADD = 'ADD';
-export const EMOJI_ACTION_REMOVE = 'REMOVE';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
@@ -257,3 +256,9 @@ export const WORK_ITEM_TO_ISSUE_MAP = {
[WIDGET_TYPE_HEALTH_STATUS]: 'healthStatus',
[WIDGET_TYPE_AWARD_EMOJI]: 'awardEmoji',
};
+
+export const LINKED_CATEGORIES_MAP = {
+ RELATES_TO: 'relates_to',
+ IS_BLOCKED_BY: 'is_blocked_by',
+ BLOCKS: 'blocks',
+};
diff --git a/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql
new file mode 100644
index 00000000000..30757f57234
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql
@@ -0,0 +1,11 @@
+query groupWorkItemTypes($fullPath: ID!) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItemTypes {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index 383d003e78c..ffc9fe2f7f7 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -100,4 +100,31 @@ fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetAwardEmoji {
type
}
+
+ ... on WorkItemWidgetLinkedItems {
+ type
+ linkedItems {
+ nodes {
+ linkId
+ linkType
+ workItem {
+ id
+ iid
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ createdAt
+ closedAt
+ widgets {
+ ...WorkItemMetadataWidgets
+ }
+ }
+ }
+ }
+ }
}
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index fe7cb719bbb..a853018a931 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,5 +1,7 @@
<script>
import * as Sentry from '@sentry/browser';
+import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import { STATUS_OPEN } from '~/issues/constants';
import { __, s__ } from '~/locale';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
@@ -8,12 +10,11 @@ import { STATE_CLOSED } from '../../constants';
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
export default {
- i18n: {
- searchPlaceholder: __('Search or filter results...'),
- },
issuableListTabs,
components: {
IssuableList,
+ IssueCardStatistics,
+ IssueCardTimeInfo,
},
inject: ['fullPath'],
data() {
@@ -57,17 +58,33 @@ export default {
:current-tab="state"
:error="error"
:issuables="workItems"
+ :issuables-loading="$apollo.queries.workItems.loading"
namespace="work-items"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
show-work-item-type-icon
:sort-options="sortOptions"
:tabs="$options.issuableListTabs"
@dismiss-alert="error = undefined"
>
+ <template #nav-actions>
+ <slot name="nav-actions"></slot>
+ </template>
+
+ <template #timeframe="{ issuable = {} }">
+ <issue-card-time-info :issue="issuable" />
+ </template>
+
<template #status="{ issuable }">
{{ getStatus(issuable) }}
</template>
+
+ <template #statistics="{ issuable = {} }">
+ <issue-card-statistics :issue="issuable" />
+ </template>
+
+ <template #list-body>
+ <slot name="list-body"></slot>
+ </template>
</issuable-list>
</template>
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
index 5cd38600779..885cea2c1d6 100644
--- a/app/assets/javascripts/work_items/list/index.js
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import WorkItemsListApp from './components/work_items_list_app.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import WorkItemsListApp from 'ee_else_ce/work_items/list/components/work_items_list_app.vue';
export const mountWorkItemsListApp = () => {
const el = document.querySelector('.js-work-items-list-root');
@@ -12,6 +13,13 @@ export const mountWorkItemsListApp = () => {
Vue.use(VueApollo);
+ const {
+ fullPath,
+ hasEpicsFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+ } = el.dataset;
+
return new Vue({
el,
name: 'WorkItemsListRoot',
@@ -19,7 +27,10 @@ export const mountWorkItemsListApp = () => {
defaultClient: createDefaultClient(),
}),
provide: {
- fullPath: el.dataset.fullPath,
+ fullPath,
+ hasEpicsFeature: parseBoolean(hasEpicsFeature),
+ hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
+ hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
},
render: (createComponent) => createComponent(WorkItemsListApp),
});
diff --git a/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql
new file mode 100644
index 00000000000..1198973d184
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql
@@ -0,0 +1,38 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment BaseWorkItemWidgets on WorkItemWidget {
+ ... on WorkItemWidgetAssignees {
+ type
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ ... on WorkItemWidgetLabels {
+ type
+ allowsScopedLabels
+ labels {
+ nodes {
+ id
+ color
+ description
+ title
+ }
+ }
+ }
+ ... on WorkItemWidgetMilestone {
+ type
+ milestone {
+ id
+ dueDate
+ startDate
+ title
+ webPath
+ }
+ }
+ ... on WorkItemWidgetStartAndDueDate {
+ type
+ dueDate
+ }
+}
diff --git a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
index 7ada2cf12dd..623527302f1 100644
--- a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
@@ -1,3 +1,5 @@
+#import "ee_else_ce/work_items/list/queries/work_item_widgets.fragment.graphql"
+
query getWorkItems($fullPath: ID!) {
group(fullPath: $fullPath) {
id
@@ -21,30 +23,7 @@ query getWorkItems($fullPath: ID!) {
updatedAt
webUrl
widgets {
- ... on WorkItemWidgetAssignees {
- assignees {
- nodes {
- id
- avatarUrl
- name
- username
- webUrl
- }
- }
- type
- }
- ... on WorkItemWidgetLabels {
- allowsScopedLabels
- labels {
- nodes {
- id
- color
- description
- title
- }
- }
- type
- }
+ ...WorkItemWidgets
}
workItemType {
id
diff --git a/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql
new file mode 100644
index 00000000000..6862df5d330
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql
@@ -0,0 +1,5 @@
+#import "./base_work_item_widgets.fragment.graphql"
+
+fragment WorkItemWidgets on WorkItemWidget {
+ ...BaseWorkItemWidgets
+}
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 49ec12db4e1..b5705b21b5a 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -3,7 +3,11 @@ import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { getPreferredLocales, s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
+import {
+ I18N_WORK_ITEM_ERROR_CREATING,
+ I18N_WORK_ITEM_ERROR_FETCHING_TYPES,
+ sprintfWorkItem,
+} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
@@ -11,9 +15,6 @@ import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import ItemTitle from '../components/item_title.vue';
export default {
- fetchTypesErrorText: s__(
- 'WorkItem|Something went wrong when fetching work item types. Please try again',
- ),
components: {
GlButton,
GlAlert,
@@ -53,7 +54,7 @@ export default {
}));
},
error() {
- this.error = this.$options.fetchTypesErrorText;
+ this.error = I18N_WORK_ITEM_ERROR_FETCHING_TYPES;
},
},
},
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 5a882977bc2..1443e4b509d 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,9 +1,26 @@
-import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_HIERARCHY, WIDGET_TYPE_LABELS } from './constants';
+import { joinPaths } from '~/lib/utils/url_utility';
+import {
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_HEALTH_STATUS,
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_LABELS,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_START_AND_DUE_DATE,
+ WIDGET_TYPE_WEIGHT,
+} from './constants';
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
+export const isHealthStatusWidget = (widget) => widget.type === WIDGET_TYPE_HEALTH_STATUS;
+
export const isLabelsWidget = (widget) => widget.type === WIDGET_TYPE_LABELS;
+export const isMilestoneWidget = (widget) => widget.type === WIDGET_TYPE_MILESTONE;
+
+export const isStartAndDueDateWidget = (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE;
+
+export const isWeightWidget = (widget) => widget.type === WIDGET_TYPE_WEIGHT;
+
export const findHierarchyWidgets = (widgets) =>
widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
@@ -26,3 +43,7 @@ export const markdownPreviewPath = (fullPath, iid) =>
`${
gon.relative_url_root || ''
}/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`;
+
+export const workItemPath = (fullPath, workItemIid) => {
+ return joinPaths(gon?.relative_url_root || '/', fullPath, '-', 'work_items', workItemIid);
+};