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/components')
-rw-r--r--app/assets/javascripts/work_items/components/item_state.vue79
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue1
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue81
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue5
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue196
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue (renamed from app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue)0
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue (renamed from app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue)0
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue76
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_award_emoji.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue53
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue70
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue15
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue58
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue130
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue40
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_badge.vue41
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue (renamed from app/assets/javascripts/work_items/components/work_item_state.vue)71
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue13
24 files changed, 520 insertions, 438 deletions
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue
deleted file mode 100644
index 9053d8972de..00000000000
--- a/app/assets/javascripts/work_items/components/item_state.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<script>
-import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { STATE_OPEN, STATE_CLOSED } from '../constants';
-
-export default {
- i18n: {
- status: __('Status'),
- },
- states: [
- {
- value: STATE_OPEN,
- text: __('Open'),
- },
- {
- value: STATE_CLOSED,
- text: __('Closed'),
- },
- ],
- components: {
- GlFormGroup,
- GlFormSelect,
- },
- props: {
- state: {
- type: String,
- required: true,
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- currentState() {
- return this.$options.states[this.state];
- },
- },
- methods: {
- setState(newState) {
- if (newState !== this.state) {
- this.$emit('changed', newState);
- }
- },
- },
- labelId: 'work-item-state-select',
-};
-</script>
-
-<template>
- <gl-form-group
- :label="$options.i18n.status"
- :label-for="$options.labelId"
- label-cols="3"
- label-cols-lg="2"
- label-class="gl-pb-0! gl-overflow-wrap-break work-item-field-label"
- class="gl-align-items-center"
- >
- <gl-form-select
- :id="$options.labelId"
- :value="state"
- :options="$options.states"
- :disabled="disabled"
- data-testid="work-item-state-select"
- class="gl-w-auto hide-select-decoration gl-pl-4 gl-my-1 work-item-field-value"
- :class="{ 'gl-bg-transparent! gl-cursor-text!': disabled }"
- @change="setState"
- />
- </gl-form-group>
-</template>
-
-<style>
-.hide-select-decoration:not(:focus, :hover),
-.hide-select-decoration:disabled {
- background-image: none;
- box-shadow: none;
-}
-</style>
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
index 1dc6d341811..74bcc2717bd 100644
--- a/app/assets/javascripts/work_items/components/item_title.vue
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -52,7 +52,7 @@ export default {
:aria-label="__('Title')"
:data-placeholder="placeholder"
:contenteditable="!disabled"
- class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base gl-display-block"
+ class="hide-unfocused-input-decoration gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-rounded-base gl-display-block"
:class="{ 'gl-hover-border-gray-200 gl-pseudo-placeholder': !disabled }"
@paste="handlePaste"
@blur="handleBlur"
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 c330eccb186..66ad3d50287 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
@@ -265,6 +265,7 @@ export default {
:comment-button-text="commentButtonText"
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
+ @error="$emit('error', $event)"
/>
<textarea
v-else
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 c317ec48732..b143c529014 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
@@ -1,22 +1,13 @@
<script>
import { GlButton, GlFormCheckbox, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__, __, sprintf } from '~/locale';
+import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import {
- I18N_WORK_ITEM_ERROR_UPDATING,
- sprintfWorkItem,
- STATE_OPEN,
- STATE_EVENT_REOPEN,
- STATE_EVENT_CLOSE,
- TRACKING_CATEGORY_SHOW,
- i18n,
-} from '~/work_items/constants';
+import { STATE_OPEN, TRACKING_CATEGORY_SHOW } 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 { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
+import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
export default {
i18n: {
@@ -25,6 +16,7 @@ export default {
'Notes|Internal notes are only visible to members with the role of Reporter or higher',
),
addInternalNote: __('Add internal note'),
+ cancelButtonText: __('Cancel'),
},
constantOptions: {
markdownDocsPath: helpPagePath('user/markdown'),
@@ -34,6 +26,7 @@ export default {
MarkdownEditor,
GlFormCheckbox,
GlIcon,
+ WorkItemStateToggleButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -123,14 +116,6 @@ export default {
isWorkItemOpen() {
return this.workItemState === STATE_OPEN;
},
- toggleWorkItemStateText() {
- return this.isWorkItemOpen
- ? sprintf(__('Close %{workItemType}'), { workItemType: this.workItemType.toLowerCase() })
- : sprintf(__('Reopen %{workItemType}'), { workItemType: this.workItemType.toLowerCase() });
- },
- cancelButtonText() {
- return this.isNewDiscussion ? this.toggleWorkItemStateText : __('Cancel');
- },
commentButtonTextComputed() {
return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText;
},
@@ -166,48 +151,6 @@ export default {
this.$emit('cancelEditing');
clearDraft(this.autosaveKey);
},
- async toggleWorkItemState() {
- const input = {
- id: this.workItemId,
- stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
- };
-
- this.updateInProgress = true;
-
- try {
- this.track('updated_state');
-
- const { mutation, variables } = getUpdateWorkItemMutation({
- workItemParentId: this.workItemParentId,
- input,
- });
-
- const { data } = await this.$apollo.mutate({
- mutation,
- variables,
- });
-
- const errors = data.workItemUpdate?.errors;
-
- if (errors?.length) {
- this.$emit('error', i18n.updateError);
- }
- } catch (error) {
- const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
-
- this.$emit('error', msg);
- Sentry.captureException(error);
- }
-
- this.updateInProgress = false;
- },
- cancelButtonAction() {
- if (this.isNewDiscussion) {
- this.toggleWorkItemState();
- } else {
- this.cancelEditing();
- }
- },
},
};
</script>
@@ -257,13 +200,23 @@ export default {
@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="cancelButtonAction"
- >{{ cancelButtonText }}
+ @click="cancelEditing"
+ >{{ $options.i18n.cancelButtonText }}
</gl-button>
</form>
</div>
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 a2667a379e1..92560f2da9e 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
@@ -3,7 +3,7 @@ import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
-import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_ASSIGNEES } from '~/work_items/constants';
+import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import Tracking from '~/tracking';
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import { renderMarkdown } from '~/notes/utils';
@@ -17,6 +17,7 @@ import NoteActions from '~/work_items/components/notes/work_item_note_actions.vu
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import { isAssigneesWidget } from '../../utils';
import WorkItemCommentForm from './work_item_comment_form.vue';
import WorkItemNoteAwardsList from './work_item_note_awards_list.vue';
@@ -228,8 +229,6 @@ export default {
newAssignees = [...this.assignees, this.author];
}
- const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
-
const assigneesWidgetIndex = this.workItem.widgets.findIndex(isAssigneesWidget);
const editedWorkItemWidgets = [...this.workItem.widgets];
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
new file mode 100644
index 00000000000..0a38dcb77f6
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -0,0 +1,196 @@
+<script>
+import { GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
+import {
+ STATE_OPEN,
+ TASK_TYPE_NAME,
+ WIDGET_TYPE_PROGRESS,
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_HEALTH_STATUS,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_LABELS,
+ WORK_ITEM_NAME_TO_ICON_MAP,
+} from '../../constants';
+import WorkItemLinksMenu from './work_item_links_menu.vue';
+
+export default {
+ i18n: {
+ confidential: __('Confidential'),
+ created: __('Created'),
+ closed: __('Closed'),
+ },
+ components: {
+ GlLabel,
+ GlLink,
+ GlIcon,
+ RichTimestampTooltip,
+ WorkItemLinkChildMetadata,
+ WorkItemLinksMenu,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ childItem: {
+ type: Object,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ parentWorkItemId: {
+ type: String,
+ required: true,
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ childPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ labels() {
+ return this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes || [];
+ },
+ metadataWidgets() {
+ return this.childItem.widgets?.reduce((metadataWidgets, widget) => {
+ // Skip Hierarchy widget as it is not part of metadata.
+ if (widget.type && widget.type !== WIDGET_TYPE_HIERARCHY) {
+ // eslint-disable-next-line no-param-reassign
+ metadataWidgets[widget.type] = widget;
+ }
+ return metadataWidgets;
+ }, {});
+ },
+ allowsScopedLabels() {
+ return this.metadataWidgets[WIDGET_TYPE_LABELS]?.allowsScopedLabels;
+ },
+ isChildItemOpen() {
+ return this.childItem.state === STATE_OPEN;
+ },
+ iconName() {
+ if (this.childItemType === TASK_TYPE_NAME) {
+ return this.isChildItemOpen ? 'issue-open-m' : 'issue-close';
+ }
+ return WORK_ITEM_NAME_TO_ICON_MAP[this.childItemType];
+ },
+ childItemType() {
+ return this.childItem.workItemType.name;
+ },
+ iconClass() {
+ if (this.childItemType === TASK_TYPE_NAME) {
+ return this.isChildItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
+ }
+ return '';
+ },
+ stateTimestamp() {
+ return this.isChildItemOpen ? this.childItem.createdAt : this.childItem.closedAt;
+ },
+ stateTimestampTypeText() {
+ return this.isChildItemOpen ? this.$options.i18n.created : this.$options.i18n.closed;
+ },
+ hasMetadata() {
+ if (this.metadataWidgets) {
+ return (
+ Number.isInteger(this.metadataWidgets[WIDGET_TYPE_PROGRESS]?.progress) ||
+ Boolean(this.metadataWidgets[WIDGET_TYPE_HEALTH_STATUS]?.healthStatus) ||
+ Boolean(this.metadataWidgets[WIDGET_TYPE_MILESTONE]?.milestone) ||
+ this.metadataWidgets[WIDGET_TYPE_ASSIGNEES]?.assignees?.nodes.length > 0 ||
+ this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes.length > 0
+ );
+ }
+ return false;
+ },
+ },
+ methods: {
+ showScopedLabel(label) {
+ return isScopedLabel(label) && this.allowsScopedLabels;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base"
+ data-testid="links-child"
+ >
+ <div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
+ <div
+ class="gl-display-flex gl-flex-grow-1 gl-flex-wrap flex-xl-nowrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-min-w-0"
+ >
+ <div class="item-title gl-display-flex gl-gap-3 gl-min-w-0">
+ <span
+ :id="`stateIcon-${childItem.id}`"
+ class="gl-cursor-help"
+ data-testid="item-status-icon"
+ >
+ <gl-icon
+ class="gl-text-secondary"
+ :class="iconClass"
+ :name="iconName"
+ :aria-label="stateTimestampTypeText"
+ />
+ </span>
+ <rich-timestamp-tooltip
+ :target="`stateIcon-${childItem.id}`"
+ :raw-timestamp="stateTimestamp"
+ :timestamp-type-text="stateTimestampTypeText"
+ />
+ <span v-if="childItem.confidential">
+ <gl-icon
+ v-gl-tooltip.top
+ name="eye-slash"
+ class="gl-text-orange-500"
+ data-testid="confidential-icon"
+ :aria-label="$options.i18n.confidential"
+ :title="$options.i18n.confidential"
+ />
+ </span>
+ <gl-link
+ :href="childPath"
+ class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
+ data-testid="item-title"
+ @click="$emit('click', $event)"
+ @mouseover="$emit('mouseover')"
+ @mouseout="$emit('mouseout')"
+ >
+ {{ childItem.title }}
+ </gl-link>
+ </div>
+ <work-item-link-child-metadata
+ v-if="hasMetadata"
+ :metadata-widgets="metadataWidgets"
+ class="gl-ml-6 ml-xl-0"
+ />
+ </div>
+ <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
+ <gl-label
+ v-for="label in labels"
+ :key="label.id"
+ :title="label.title"
+ :background-color="label.color"
+ :description="label.description"
+ :scoped="showScopedLabel(label)"
+ class="gl-my-2 gl-mr-2 gl-mb-auto gl-label-sm"
+ tooltip-placement="top"
+ />
+ </div>
+ </div>
+ <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex">
+ <work-item-links-menu
+ data-testid="links-menu"
+ @removeChild="$emit('removeChild', childItem)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
index ddeac2b92ae..ddeac2b92ae 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
index 53e8eedf060..53e8eedf060 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
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 76a04bede61..e8fe64c932b 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -8,6 +8,7 @@ import {
GlModalDirective,
GlToggle,
} from '@gitlab/ui';
+import { produce } from 'immer';
import * as Sentry from '@sentry/browser';
@@ -15,6 +16,7 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import toast from '~/vue_shared/plugins/global_toast';
import { isLoggedIn } from '~/lib/utils/common_utils';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
sprintfWorkItem,
@@ -127,6 +129,10 @@ export default {
required: false,
default: false,
},
+ workItemIid: {
+ type: String,
+ required: true,
+ },
},
apollo: {
workItemTypes: {
@@ -168,16 +174,6 @@ export default {
return this.workItemTypes.find((type) => type.name === WORK_ITEM_TYPE_VALUE_OBJECTIVE).id;
},
},
- watch: {
- subscribedToNotifications() {
- /**
- * To toggle the value if mutation fails, assign the
- * subscribedToNotifications boolean value directly
- * to data prop.
- */
- this.initialSubscribed = this.subscribedToNotifications;
- },
- },
methods: {
copyToClipboard(text, message) {
if (this.isModal) {
@@ -203,10 +199,9 @@ export default {
},
toggleNotifications(subscribed) {
const inputVariables = {
- id: this.workItemId,
- notificationsWidget: {
- subscribed,
- },
+ projectPath: this.fullPath,
+ iid: this.workItemIid,
+ subscribedState: subscribed,
};
this.$apollo
.mutate({
@@ -215,27 +210,34 @@ export default {
input: inputVariables,
},
optimisticResponse: {
- workItemUpdate: {
- errors: [],
- workItem: {
+ updateWorkItemNotificationsSubscription: {
+ issue: {
id: this.workItemId,
- widgets: [
- {
- type: WIDGET_TYPE_NOTIFICATIONS,
- subscribed,
- __typename: 'WorkItemWidgetNotifications',
- },
- ],
- __typename: 'WorkItem',
+ subscribed,
+ },
+ errors: [],
+ },
+ },
+ update: (
+ cache,
+ {
+ data: {
+ updateWorkItemNotificationsSubscription: { issue = {} },
},
- __typename: 'WorkItemUpdatePayload',
},
+ ) => {
+ // As the mutation and the query both are different,
+ // overwrite the subscribed value in the cache
+ this.updateWorkItemNotificationsWidgetCache({
+ cache,
+ issue,
+ });
},
})
.then(
({
data: {
- workItemUpdate: { errors },
+ updateWorkItemNotificationsSubscription: { errors },
},
}) => {
if (errors?.length) {
@@ -251,6 +253,25 @@ export default {
Sentry.captureException(error);
});
},
+ updateWorkItemNotificationsWidgetCache({ cache, issue }) {
+ const query = {
+ query: workItemByIidQuery,
+ variables: { fullPath: this.fullPath, iid: this.workItemIid },
+ };
+ // Read the work item object
+ const sourceData = cache.readQuery(query);
+
+ const newData = produce(sourceData, (draftState) => {
+ const { widgets } = draftState.workspace.workItems.nodes[0];
+
+ const widgetNotifications = widgets.find(({ type }) => type === WIDGET_TYPE_NOTIFICATIONS);
+ // overwrite the subscribed value
+ widgetNotifications.subscribed = issue.subscribed;
+ });
+
+ // write to the cache
+ cache.writeQuery({ ...query, data: newData });
+ },
throwConvertError() {
this.$emit('error', this.i18n.convertError);
},
@@ -275,6 +296,7 @@ export default {
}
this.$toast.show(s__('WorkItem|Promoted to objective.'));
this.track('promote_kr_to_objective');
+ this.$emit('promotedToObjective');
} catch (error) {
this.throwConvertError();
Sentry.captureException(error);
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 f7ac63e16c3..4b4aa7f96ca 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -148,7 +148,7 @@ export default {
};
},
containerClass() {
- return !this.isEditing ? 'gl-shadow-none!' : '';
+ return !this.isEditing ? 'gl-shadow-none! hide-unfocused-input-decoration' : '';
},
isLoadingUsers() {
return this.$apollo.queries.users.loading;
@@ -318,7 +318,7 @@ export default {
:loading="isLoadingUsers && !isLoadingMore"
:view-only="!canUpdate"
:allow-clear-all="isEditing"
- class="assignees-selector gl-flex-grow-1 gl-border gl-border-white gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2 work-item-field-value"
+ class="assignees-selector hide-unfocused-input-decoration work-item-field-value gl-flex-grow-1 gl-border gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2"
data-testid="work-item-assignees-input"
@input="handleAssigneesInput"
@text-input="debouncedSearchKeyUpdate"
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index c727075eaac..139f0f7919c 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -11,7 +11,6 @@ import {
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_WEIGHT,
} from '../constants';
-import WorkItemState from './work_item_state.vue';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
@@ -23,7 +22,6 @@ export default {
WorkItemMilestone,
WorkItemAssignees,
WorkItemDueDate,
- WorkItemState,
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
@@ -97,12 +95,6 @@ export default {
<template>
<div class="work-item-attributes-wrapper">
- <work-item-state
- :work-item="workItem"
- :work-item-parent-id="workItemParentId"
- :can-update="canUpdate"
- @error="$emit('error', $event)"
- />
<work-item-assignees
v-if="workItemAssignees"
:can-update="canUpdate"
diff --git a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
index 3dd3a072d0f..44bd17b59a2 100644
--- a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
+++ b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
@@ -51,7 +51,7 @@ export default {
return window.gon.current_user_fullname;
},
/**
- * Parse and convert award emoji list to a format that AwardsList can understand
+ * Parse and convert emoji reactions list to a format that AwardsList can understand
*/
awards() {
if (!this.awardEmoji) {
@@ -91,12 +91,15 @@ export default {
skip() {
return !this.workItemIid;
},
- result() {
+ result({ data }) {
if (this.hasNextPage) {
this.fetchAwardEmojis();
} else {
this.isLoading = false;
}
+ if (data) {
+ this.$emit('emoji-updated', data.workspace?.workItems?.nodes[0]);
+ }
},
error() {
this.$emit('error', I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR);
@@ -125,7 +128,7 @@ export default {
);
},
/**
- * Prepare award emoji nodes based on emoji name
+ * Prepare emoji reactions nodes based on emoji name
* and whether the user has toggled the emoji off or on
*/
getAwardEmojiNodes(name, toggledOn) {
@@ -204,7 +207,7 @@ export default {
},
},
) => {
- // update the cache of award emoji widget object
+ // update the cache of emoji reactions widget object
this.updateWorkItemAwardEmojiWidgetCache({ cache, name, toggledOn });
},
})
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 78a86aa49a4..f93ea4a0753 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
@@ -1,7 +1,11 @@
<script>
-import { GlAvatarLink, GlSprintf } from '@gitlab/ui';
+import { GlAvatarLink, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { WORKSPACE_PROJECT } from '~/issues/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
export default {
@@ -9,6 +13,10 @@ export default {
GlAvatarLink,
GlSprintf,
TimeAgoTooltip,
+ WorkItemStateBadge,
+ WorkItemTypeIcon,
+ ConfidentialityBadge,
+ GlLoadingIcon,
},
inject: ['fullPath'],
props: {
@@ -17,6 +25,11 @@ export default {
required: false,
default: null,
},
+ updateInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
createdAt() {
@@ -31,6 +44,18 @@ export default {
authorId() {
return getIdFromGraphQLId(this.author.id);
},
+ workItemState() {
+ return this.workItem?.state;
+ },
+ workItemType() {
+ return this.workItem?.workItemType?.name;
+ },
+ workItemIconName() {
+ return this.workItem?.workItemType?.iconName;
+ },
+ isWorkItemConfidential() {
+ return this.workItem?.confidential;
+ },
},
apollo: {
workItem: {
@@ -49,13 +74,29 @@ export default {
},
},
},
+ WORKSPACE_PROJECT,
};
</script>
<template>
- <div class="gl-mb-3">
- <span data-testid="work-item-created">
- <gl-sprintf v-if="author.name" :message="__('Created %{timeAgo} by %{author}')">
+ <div class="gl-mb-3 gl-text-gray-700">
+ <work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
+ <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"
+ :issuable-type="workItemType"
+ />
+ <work-item-type-icon
+ class="gl-vertical-align-middle gl-mr-0!"
+ :work-item-icon-name="workItemIconName"
+ :work-item-type="workItemType"
+ show-text
+ />
+ <span data-testid="work-item-created" class="gl-vertical-align-middle">
+ <gl-sprintf v-if="author.name" :message="__('created %{timeAgo} by %{author}')">
<template #timeAgo>
<time-ago-tooltip :time="createdAt" />
</template>
@@ -70,7 +111,7 @@ export default {
</gl-avatar-link>
</template>
</gl-sprintf>
- <gl-sprintf v-else-if="createdAt" :message="__('Created %{timeAgo}')">
+ <gl-sprintf v-else-if="createdAt" :message="__('created %{timeAgo}')">
<template #timeAgo>
<time-ago-tooltip :time="createdAt" />
</template>
@@ -79,7 +120,7 @@ export default {
<span
v-if="updatedAt"
- class="gl-ml-5 gl-display-none gl-sm-display-inline-block"
+ class="gl-ml-5 gl-display-none gl-sm-display-inline-block gl-vertical-align-middle"
data-testid="work-item-updated"
>
<gl-sprintf :message="__('Updated %{timeAgo}')">
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 1402b313cee..d826ef9cbe7 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -5,7 +5,6 @@ import {
GlSkeletonLoader,
GlLoadingIcon,
GlIcon,
- GlBadge,
GlButton,
GlTooltipDirective,
GlEmptyState,
@@ -19,8 +18,9 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+import { WORKSPACE_PROJECT } from '~/issues/constants';
import {
- sprintfWorkItem,
i18n,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_NOTIFICATIONS,
@@ -49,6 +49,7 @@ import WorkItemDescription from './work_item_description.vue';
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';
export default {
i18n,
@@ -57,8 +58,8 @@ export default {
},
isLoggedIn: isLoggedIn(),
components: {
+ WorkItemStateToggleButton,
GlAlert,
- GlBadge,
GlButton,
GlLoadingIcon,
GlSkeletonLoader,
@@ -77,6 +78,7 @@ export default {
WorkItemDetailModal,
AbuseCategorySelector,
GlIntersectionObserver,
+ ConfidentialityBadge,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'reportAbusePath'],
@@ -134,6 +136,7 @@ export default {
if (!res.data) {
return;
}
+ this.$emit('work-item-updated', this.workItem);
if (isEmpty(this.workItem)) {
this.setEmptyState();
}
@@ -169,7 +172,7 @@ export default {
return this.workItem.workItemType?.id;
},
workItemBreadcrumbReference() {
- return this.workItemType ? `${this.workItemType} #${this.workItem.iid}` : '';
+ return this.workItemType ? `#${this.workItem.iid}` : '';
},
canUpdate() {
return this.workItem?.userPermissions?.updateWorkItem;
@@ -183,9 +186,6 @@ export default {
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
- confidentialTooltip() {
- return sprintfWorkItem(this.$options.i18n.confidentialTooltip, this.workItemType);
- },
fullPath() {
return this.workItem?.project.fullPath;
},
@@ -374,8 +374,8 @@ export default {
}
},
},
-
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WORKSPACE_PROJECT,
};
</script>
@@ -397,13 +397,13 @@ export default {
<div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
<ul
v-if="parentWorkItem"
- class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0 gl-z-index-0"
+ class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0"
data-testid="work-item-parent"
>
- <li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-overflow-hidden">
+ <li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-min-w-0">
<gl-button
v-gl-tooltip.hover
- class="gl-text-truncate gl-max-w-full"
+ class="gl-text-truncate"
:icon="parentWorkItemIconName"
category="tertiary"
:href="parentUrl"
@@ -418,7 +418,8 @@ export default {
>
<work-item-type-icon
:work-item-icon-name="workItemIconName"
- :work-item-type="workItemType && workItemType.toUpperCase()"
+ :work-item-type="workItemType"
+ show-text
/>
{{ workItemBreadcrumbReference }}
</li>
@@ -430,20 +431,19 @@ export default {
>
<work-item-type-icon
:work-item-icon-name="workItemIconName"
- :work-item-type="workItemType && workItemType.toUpperCase()"
+ :work-item-type="workItemType"
+ show-text
/>
{{ workItemBreadcrumbReference }}
</div>
- <gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
- <gl-badge
- v-if="workItem.confidential"
- v-gl-tooltip.bottom
- :title="confidentialTooltip"
- variant="warning"
- icon="eye-slash"
- class="gl-mr-3 gl-cursor-help"
- >{{ __('Confidential') }}</gl-badge
- >
+ <work-item-state-toggle-button
+ v-if="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-state="workItem.state"
+ :work-item-parent-id="workItemParentId"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
+ />
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
@@ -464,9 +464,11 @@ export default {
:work-item-reference="workItem.reference"
:work-item-create-note-email="workItem.createNoteEmail"
:is-modal="isModal"
+ :work-item-iid="workItemIid"
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
+ @promotedToObjective="$emit('promotedToObjective', workItemIid)"
/>
<gl-button
v-if="isModal"
@@ -488,7 +490,10 @@ export default {
:can-update="canUpdate"
@error="updateError = $event"
/>
- <work-item-created-updated :work-item-iid="workItemIid" />
+ <work-item-created-updated
+ :work-item-iid="workItemIid"
+ :update-in-progress="updateInProgress"
+ />
</div>
<gl-intersection-observer
v-if="showIntersectionObserver"
@@ -508,15 +513,12 @@ export default {
{{ workItem.title }}
</span>
<gl-loading-icon v-if="updateInProgress" class="gl-mr-3" />
- <gl-badge
+ <confidentiality-badge
v-if="workItem.confidential"
- v-gl-tooltip.bottom
- :title="confidentialTooltip"
- variant="warning"
- icon="eye-slash"
- class="gl-mr-3 gl-cursor-help"
- >{{ __('Confidential') }}</gl-badge
- >
+ data-testid="confidential"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ :issuable-type="workItemType"
+ />
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
@@ -537,11 +539,13 @@ export default {
:work-item-reference="workItem.reference"
:work-item-create-note-email="workItem.createNoteEmail"
:is-modal="isModal"
+ :work-item-iid="workItemIid"
@deleteWorkItem="
$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })
"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
+ @promotedToObjective="$emit('promotedToObjective', workItemIid)"
/>
</div>
</div>
@@ -573,6 +577,7 @@ export default {
:award-emoji="workItemAwardEmoji.awardEmoji"
:work-item-iid="workItemIid"
@error="updateError = $event"
+ @emoji-updated="$emit('work-item-emoji-updated', $event)"
/>
<work-item-tree
v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
@@ -584,6 +589,7 @@ export default {
:can-update="canUpdate"
:confidential="workItem.confidential"
@show-modal="openInModal"
+ @addChild="$emit('addChild')"
/>
<work-item-notes
v-if="workItemNotes"
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index b4b3049d669..1aa62a2b906 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -219,7 +219,6 @@ export default {
ref="startDatePicker"
v-model="dirtyStartDate"
container="body"
- data-testid="work-item-start-date-picker"
:disabled="isDatepickerDisabled"
:input-id="$options.startDateInputId"
show-clear-button
@@ -250,7 +249,6 @@ export default {
ref="dueDatePicker"
v-model="dirtyDueDate"
container="body"
- data-testid="work-item-due-date-picker"
:disabled="isDatepickerDisabled"
:input-id="$options.dueDateInputId"
:min-date="dirtyStartDate"
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 8676456a6a4..1405a12a101 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -9,13 +9,8 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
-
-import {
- i18n,
- I18N_WORK_ITEM_ERROR_FETCHING_LABELS,
- TRACKING_CATEGORY_SHOW,
- WIDGET_TYPE_LABELS,
-} from '../constants';
+import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS, TRACKING_CATEGORY_SHOW } from '../constants';
+import { isLabelsWidget } from '../utils';
function isTokenSelectorElement(el) {
return (
@@ -121,13 +116,13 @@ export default {
return this.labelsWidget?.allowsScopedLabels;
},
containerClass() {
- return !this.isEditing ? 'gl-shadow-none!' : '';
+ return !this.isEditing ? 'gl-shadow-none! hide-unfocused-input-decoration' : '';
},
isLoading() {
return this.$apollo.queries.searchLabels.loading;
},
labelsWidget() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ return this.workItem?.widgets?.find(isLabelsWidget);
},
labels() {
return this.labelsWidget?.labels?.nodes || [];
@@ -272,7 +267,7 @@ export default {
:loading="isLoading"
:view-only="!canUpdate"
:allow-clear-all="isEditing"
- class="gl-flex-grow-1 gl-border gl-border-white gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2! work-item-field-value"
+ class="hide-unfocused-input-decoration work-item-field-value gl-flex-grow-1 gl-border gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2!"
menu-class="token-selector-menu-class"
data-testid="work-item-labels-input"
:class="{ 'gl-hover-border-gray-200': canUpdate }"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
index dc5bcdc3dcc..c5be1a3ead3 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
@@ -1,7 +1,7 @@
<script>
-import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
const objectiveActionItems = [
{
@@ -29,10 +29,30 @@ export default {
keyResultActionItems,
objectiveActionItems,
components: {
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDisclosureDropdown,
+ },
+ computed: {
+ objectiveDropdownItems() {
+ return {
+ name: __('Objective'),
+ items: this.$options.objectiveActionItems.map((item) => ({
+ text: item.title,
+ action: () => this.change(item),
+ })),
+ };
+ },
+ keyResultDropdownItems() {
+ return {
+ name: __('Key result'),
+ items: this.$options.keyResultActionItems.map((item) => ({
+ text: item.title,
+ action: () => this.change(item),
+ })),
+ };
+ },
+ dropdownItems() {
+ return [this.objectiveDropdownItems, this.keyResultDropdownItems];
+ },
},
methods: {
change({ eventName }) {
@@ -43,24 +63,10 @@ export default {
</script>
<template>
- <gl-dropdown :text="__('Add')" size="small" right>
- <gl-dropdown-section-header>{{ __('Objective') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="item in $options.objectiveActionItems"
- :key="item.eventName"
- @click="change(item)"
- >
- {{ item.title }}
- </gl-dropdown-item>
-
- <gl-dropdown-divider />
- <gl-dropdown-section-header>{{ __('Key result') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="item in $options.keyResultActionItems"
- :key="item.eventName"
- @click="change(item)"
- >
- {{ item.title }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-disclosure-dropdown
+ :toggle-text="__('Add')"
+ size="small"
+ placement="right"
+ :items="dropdownItems"
+ />
</template>
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 ec44a654e89..a9b0c2b98bf 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
@@ -1,39 +1,27 @@
<script>
-import { GlButton, GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import * as Sentry from '@sentry/browser';
import { __, s__ } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { createAlert } from '~/alert';
-import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
-import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import {
STATE_OPEN,
TASK_TYPE_NAME,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
- WIDGET_TYPE_PROGRESS,
- WIDGET_TYPE_HEALTH_STATUS,
- WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_HIERARCHY,
- WIDGET_TYPE_ASSIGNEES,
- WIDGET_TYPE_LABELS,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
-import WorkItemLinksMenu from './work_item_links_menu.vue';
+import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
import WorkItemTreeChildren from './work_item_tree_children.vue';
export default {
components: {
- GlLabel,
- GlLink,
GlButton,
- GlIcon,
- RichTimestampTooltip,
- WorkItemLinkChildMetadata,
- WorkItemLinksMenu,
WorkItemTreeChildren,
+ WorkItemLinkChildContents,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -74,25 +62,9 @@ export default {
};
},
computed: {
- labels() {
- return this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes || [];
- },
- allowsScopedLabels() {
- return this.metadataWidgets[WIDGET_TYPE_LABELS]?.allowsScopedLabels;
- },
canHaveChildren() {
return this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE;
},
- metadataWidgets() {
- return this.childItem.widgets?.reduce((metadataWidgets, widget) => {
- // Skip Hierarchy widget as it is not part of metadata.
- if (widget.type && widget.type !== WIDGET_TYPE_HIERARCHY) {
- // eslint-disable-next-line no-param-reassign
- metadataWidgets[widget.type] = widget;
- }
- return metadataWidgets;
- }, {});
- },
isItemOpen() {
return this.childItem.state === STATE_OPEN;
},
@@ -126,18 +98,6 @@ export default {
chevronTooltip() {
return this.isExpanded ? __('Collapse') : __('Expand');
},
- hasMetadata() {
- if (this.metadataWidgets) {
- return (
- Number.isInteger(this.metadataWidgets[WIDGET_TYPE_PROGRESS]?.progress) ||
- Boolean(this.metadataWidgets[WIDGET_TYPE_HEALTH_STATUS]?.healthStatus) ||
- Boolean(this.metadataWidgets[WIDGET_TYPE_MILESTONE]?.milestone) ||
- this.metadataWidgets[WIDGET_TYPE_ASSIGNEES]?.assignees?.nodes.length > 0 ||
- this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes.length > 0
- );
- }
- return false;
- },
},
watch: {
childItem: {
@@ -270,81 +230,15 @@ export default {
data-testid="expand-child"
@click="toggleItem"
/>
- <div
- class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base"
- data-testid="links-child"
- >
- <div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-wrap flex-xl-nowrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-min-w-0"
- >
- <div class="item-title gl-display-flex gl-gap-3 gl-min-w-0">
- <span
- :id="`stateIcon-${childItem.id}`"
- class="gl-cursor-help"
- data-testid="item-status-icon"
- >
- <gl-icon
- class="gl-text-secondary"
- :class="iconClass"
- :name="iconName"
- :aria-label="stateTimestampTypeText"
- />
- </span>
- <rich-timestamp-tooltip
- :target="`stateIcon-${childItem.id}`"
- :raw-timestamp="stateTimestamp"
- :timestamp-type-text="stateTimestampTypeText"
- />
- <span v-if="childItem.confidential">
- <gl-icon
- v-gl-tooltip.top
- name="eye-slash"
- class="gl-text-orange-500"
- data-testid="confidential-icon"
- :aria-label="__('Confidential')"
- :title="__('Confidential')"
- />
- </span>
- <gl-link
- :href="childPath"
- class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
- data-testid="item-title"
- @click="$emit('click', $event)"
- @mouseover="$emit('mouseover')"
- @mouseout="$emit('mouseout')"
- >
- {{ childItem.title }}
- </gl-link>
- </div>
- <work-item-link-child-metadata
- v-if="hasMetadata"
- :metadata-widgets="metadataWidgets"
- class="gl-ml-6 ml-xl-0"
- />
- </div>
- <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
- <gl-label
- v-for="label in labels"
- :key="label.id"
- :title="label.title"
- :background-color="label.color"
- :description="label.description"
- :scoped="showScopedLabel(label)"
- class="gl-my-2 gl-mr-2 gl-mb-auto gl-label-sm"
- tooltip-placement="top"
- />
- </div>
- </div>
- <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex">
- <work-item-links-menu
- :work-item-id="childItem.id"
- :parent-work-item-id="issuableGid"
- data-testid="links-menu"
- @removeChild="$emit('removeChild', childItem)"
- />
- </div>
- </div>
+ <work-item-link-child-contents
+ :child-item="childItem"
+ :can-update="canUpdate"
+ :parent-work-item-id="issuableGid"
+ :work-item-type="workItemType"
+ :child-path="childPath"
+ @click="$emit('click', $event)"
+ @removeChild="$emit('removeChild', childItem)"
+ />
</div>
<work-item-tree-children
v-if="isExpanded"
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 bfc6ceefccc..a0ff693e156 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
@@ -1,5 +1,11 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlIcon,
+ GlLoadingIcon,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { s__ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -20,8 +26,8 @@ import WorkItemChildrenWrapper from './work_item_children_wrapper.vue';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlIcon,
GlLoadingIcon,
WidgetWrapper,
@@ -211,26 +217,30 @@ export default {
</span>
</template>
<template #header-right>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="canUpdate && canAddTask"
- right
+ placement="right"
size="small"
- :text="$options.i18n.addChildButtonLabel"
+ :toggle-text="$options.i18n.addChildButtonLabel"
data-testid="toggle-form"
>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
data-testid="toggle-create-form"
- @click="showAddForm($options.FORM_TYPES.create)"
+ @action="showAddForm($options.FORM_TYPES.create)"
>
- {{ $options.i18n.createChildOptionLabel }}
- </gl-dropdown-item>
- <gl-dropdown-item
+ <template #list-item>
+ {{ $options.i18n.createChildOptionLabel }}
+ </template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
data-testid="toggle-add-form"
- @click="showAddForm($options.FORM_TYPES.add)"
+ @action="showAddForm($options.FORM_TYPES.add)"
>
- {{ $options.i18n.addChildOptionLabel }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item>
+ {{ $options.i18n.addChildOptionLabel }}
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
</template>
<template #body>
<div class="gl-new-card-content">
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 db649913602..4960189fb48 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
@@ -306,6 +306,7 @@ export default {
[this.error] = data.workItemCreate.errors;
} else {
this.unsetError();
+ this.$emit('addChild');
}
})
.catch(() => {
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 83f3c391769..246eac82c78 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
@@ -145,6 +145,7 @@ export default {
:children-ids="childrenIds"
:parent-confidential="confidential"
@cancel="hideAddForm"
+ @addChild="$emit('addChild')"
/>
<work-item-children-wrapper
:children="children"
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
new file mode 100644
index 00000000000..1d1bc7352b1
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { STATE_OPEN } from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ props: {
+ workItemState: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isWorkItemOpen() {
+ return this.workItemState === STATE_OPEN;
+ },
+ stateText() {
+ return this.isWorkItemOpen ? __('Open') : __('Closed');
+ },
+ workItemStateIcon() {
+ return this.isWorkItemOpen ? 'issue-open-m' : 'issue-close';
+ },
+ workItemStateVariant() {
+ return this.isWorkItemOpen ? 'success' : 'info';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge
+ :icon="workItemStateIcon"
+ :variant="workItemStateVariant"
+ class="gl-mr-2 gl-vertical-align-middle"
+ >
+ {{ stateText }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
index 3880ae25c8c..0ea30845466 100644
--- a/app/assets/javascripts/work_items/components/work_item_state.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
@@ -1,26 +1,35 @@
<script>
+import { GlButton } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
+import { __, sprintf } from '~/locale';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
STATE_OPEN,
- STATE_CLOSED,
STATE_EVENT_CLOSE,
STATE_EVENT_REOPEN,
TRACKING_CATEGORY_SHOW,
} from '../constants';
-import { getUpdateWorkItemMutation } from './update_work_item';
-import ItemState from './item_state.vue';
export default {
components: {
- ItemState,
+ GlButton,
},
mixins: [Tracking.mixin()],
props: {
- workItem: {
- type: Object,
+ workItemState: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemType: {
+ type: String,
required: true,
},
workItemParentId: {
@@ -28,11 +37,6 @@ export default {
required: false,
default: null,
},
- canUpdate: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -40,8 +44,16 @@ export default {
};
},
computed: {
- workItemType() {
- return this.workItem.workItemType?.name;
+ isWorkItemOpen() {
+ return this.workItemState === STATE_OPEN;
+ },
+ toggleWorkItemStateText() {
+ const baseText = this.isWorkItemOpen
+ ? __('Close %{workItemType}')
+ : __('Reopen %{workItemType}');
+ return capitalizeFirstCharacter(
+ sprintf(baseText, { workItemType: this.workItemType.toLowerCase() }),
+ );
},
tracking() {
return {
@@ -52,25 +64,10 @@ export default {
},
},
methods: {
- updateWorkItemState(newState) {
- const stateEventMap = {
- [STATE_OPEN]: STATE_EVENT_REOPEN,
- [STATE_CLOSED]: STATE_EVENT_CLOSE,
- };
-
- const stateEvent = stateEventMap[newState];
-
- this.updateWorkItem(stateEvent);
- },
-
- async updateWorkItem(updatedState) {
- if (!updatedState) {
- return;
- }
-
+ async updateWorkItem() {
const input = {
- id: this.workItem.id,
- stateEvent: updatedState,
+ id: this.workItemId,
+ stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
};
this.updateInProgress = true;
@@ -107,10 +104,10 @@ export default {
</script>
<template>
- <item-state
- v-if="workItem.state"
- :state="workItem.state"
- :disabled="updateInProgress || !canUpdate"
- @changed="updateWorkItemState"
- />
+ <gl-button
+ :loading="updateInProgress"
+ data-testid="work-item-state-toggle"
+ @click="updateWorkItem"
+ >{{ toggleWorkItemStateText }}</gl-button
+ >
</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 96a6493357c..f27ae5f4e6d 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
@@ -32,13 +32,18 @@ export default {
},
},
computed: {
+ workItemTypeUppercase() {
+ return this.workItemType.toUpperCase().split(' ').join('_');
+ },
iconName() {
return (
- this.workItemIconName || WORK_ITEMS_TYPE_MAP[this.workItemType]?.icon || 'issue-type-issue'
+ this.workItemIconName ||
+ WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.icon ||
+ 'issue-type-issue'
);
},
workItemTypeName() {
- return WORK_ITEMS_TYPE_MAP[this.workItemType]?.name;
+ return WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.name;
},
workItemTooltipTitle() {
return this.showTooltipOnHover ? this.workItemTypeName : '';
@@ -48,12 +53,12 @@ export default {
</script>
<template>
- <span>
+ <span class="gl-mr-2">
<gl-icon
v-gl-tooltip.hover="showTooltipOnHover"
:name="iconName"
:title="workItemTooltipTitle"
- class="gl-mr-2 gl-text-secondary"
+ class="gl-text-secondary"
/>
<span v-if="workItemTypeName" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeName }}</span>
</span>