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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
commitb595cb0c1dec83de5bdee18284abe86614bed33b (patch)
tree8c3d4540f193c5ff98019352f554e921b3a41a72 /app/assets/javascripts/work_items
parent2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff)
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/work_items')
-rw-r--r--app/assets/javascripts/work_items/components/item_state.vue2
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue235
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue70
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue138
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_information.vue57
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue246
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue51
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue116
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue101
-rw-r--r--app/assets/javascripts/work_items/components/work_item_weight.vue128
-rw-r--r--app/assets/javascripts/work_items/constants.js6
-rw-r--r--app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql13
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql8
-rw-r--r--app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql14
-rw-r--r--app/assets/javascripts/work_items/graphql/provider.js58
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql15
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql5
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql26
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql10
-rw-r--r--app/assets/javascripts/work_items/index.js1
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue37
27 files changed, 1170 insertions, 203 deletions
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue
index 69670d3471c..2dc8e3a1101 100644
--- a/app/assets/javascripts/work_items/components/item_state.vue
+++ b/app/assets/javascripts/work_items/components/item_state.vue
@@ -54,7 +54,7 @@ export default {
:label-for="$options.labelId"
label-cols="3"
label-cols-lg="2"
- label-class="gl-pb-0!"
+ label-class="gl-pb-0! gl-overflow-wrap-break"
class="gl-align-items-center"
>
<gl-form-select
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
index ce2fa158596..1cdc9c28f05 100644
--- a/app/assets/javascripts/work_items/components/item_title.vue
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -1,5 +1,4 @@
<script>
-import { escape } from 'lodash';
import { __ } from '~/locale';
export default {
@@ -21,15 +20,11 @@ export default {
},
},
methods: {
- getSanitizedTitle(inputEl) {
- const { innerText } = inputEl;
- return escape(innerText);
- },
handleBlur({ target }) {
- this.$emit('title-changed', this.getSanitizedTitle(target));
+ this.$emit('title-changed', target.innerText);
},
handleInput({ target }) {
- this.$emit('title-input', this.getSanitizedTitle(target));
+ this.$emit('title-input', target.innerText);
},
handleSubmit() {
this.$refs.titleEl.blur();
@@ -40,7 +35,7 @@ export default {
<template>
<h2
- class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-w-full"
+ class="gl-font-weight-normal gl-sm-font-weight-bold gl-mb-5 gl-mt-0 gl-w-full"
:class="{ 'gl-cursor-not-allowed': disabled }"
aria-labelledby="item-title"
>
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 31e4a932c5a..77002eeaf55 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -5,7 +5,7 @@ import Tracking from '~/tracking';
export default {
i18n: {
- deleteWorkItem: s__('WorkItem|Delete work item'),
+ deleteTask: s__('WorkItem|Delete task'),
},
components: {
GlDropdown,
@@ -54,7 +54,7 @@ export default {
right
>
<gl-dropdown-item v-gl-modal="'work-item-confirm-delete'">{{
- $options.i18n.deleteWorkItem
+ $options.i18n.deleteTask
}}</gl-dropdown-item>
</gl-dropdown>
<gl-modal
@@ -66,9 +66,7 @@ export default {
@hide="handleCancelDeleteWorkItem"
>
{{
- s__(
- 'WorkItem|Are you sure you want to delete the work item? This action cannot be reversed.',
- )
+ s__('WorkItem|Are you sure you want to delete the task? This action cannot be reversed.')
}}
</gl-modal>
</div>
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 4d1c171772e..9ff424aa20f 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -1,10 +1,35 @@
<script>
-import { GlTokenSelector, GlIcon, GlAvatar, GlLink } from '@gitlab/ui';
+import {
+ GlTokenSelector,
+ GlIcon,
+ GlAvatar,
+ GlLink,
+ GlSkeletonLoader,
+ GlButton,
+ GlDropdownItem,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
+import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
+import { n__, s__ } from '~/locale';
+import Tracking from '~/tracking';
+import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
-function isClosingIcon(el) {
- return el?.classList.contains('gl-token-close');
+function isTokenSelectorElement(el) {
+ return el?.classList.contains('gl-token-close') || el?.classList.contains('dropdown-item');
+}
+
+function addClass(el) {
+ return {
+ ...el,
+ class: 'gl-bg-transparent',
+ };
}
export default {
@@ -13,7 +38,15 @@ export default {
GlIcon,
GlAvatar,
GlLink,
+ GlSkeletonLoader,
+ GlButton,
+ SidebarParticipant,
+ InviteMembersTrigger,
+ GlDropdownItem,
+ GlDropdownDivider,
},
+ mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
@@ -23,67 +56,188 @@ export default {
type: Array,
required: true,
},
+ allowsMultipleAssignees: {
+ type: Boolean,
+ required: true,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
isEditing: false,
- localAssignees: this.assignees.map((assignee) => ({
- ...assignee,
- class: 'gl-bg-transparent!',
- })),
+ searchStarted: false,
+ localAssignees: this.assignees.map(addClass),
+ searchKey: '',
+ searchUsers: [],
+ currentUser: null,
};
},
+ apollo: {
+ searchUsers: {
+ query() {
+ return userSearchQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ search: this.searchKey,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace?.users?.nodes.map((node) => addClass({ ...node, ...node.user }));
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ currentUser: {
+ query: currentUserQuery,
+ },
+ },
computed: {
- assigneeIds() {
- return this.localAssignees.map((assignee) => assignee.id);
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_assignees',
+ property: `type_${this.workItemType}`,
+ };
},
assigneeListEmpty() {
return this.assignees.length === 0;
},
containerClass() {
- return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
+ return !this.isEditing ? 'gl-shadow-none!' : '';
+ },
+ isLoadingUsers() {
+ return this.$apollo.queries.searchUsers.loading;
+ },
+ assigneeText() {
+ return n__('WorkItem|Assignee', 'WorkItem|Assignees', this.localAssignees.length);
+ },
+ dropdownItems() {
+ if (this.currentUser && this.searchEmpty) {
+ if (this.searchUsers.some((user) => user.username === this.currentUser.username)) {
+ return this.moveCurrentUserToStart(this.searchUsers);
+ }
+ return [this.currentUser, ...this.searchUsers];
+ }
+ return this.searchUsers;
+ },
+ searchEmpty() {
+ return this.searchKey.length === 0;
+ },
+ addAssigneesText() {
+ return this.allowsMultipleAssignees
+ ? s__('WorkItem|Add assignees')
+ : s__('WorkItem|Add assignee');
},
},
+ watch: {
+ assignees(newVal) {
+ if (!this.isEditing) {
+ this.localAssignees = newVal.map(addClass);
+ }
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
methods: {
getUserId(id) {
return getIdFromGraphQLId(id);
},
- setAssignees(e) {
- if (isClosingIcon(e.relatedTarget) || !this.isEditing) return;
+ handleAssigneesInput(assignees) {
+ if (!this.allowsMultipleAssignees) {
+ this.localAssignees = assignees.length > 0 ? [assignees[assignees.length - 1]] : [];
+ this.isEditing = false;
+ return;
+ }
+ this.localAssignees = assignees;
+ this.focusTokenSelector();
+ },
+ handleBlur(e) {
+ if (isTokenSelectorElement(e.relatedTarget) || !this.isEditing) return;
this.isEditing = false;
+ this.setAssignees(this.localAssignees);
+ },
+ setAssignees(assignees) {
this.$apollo.mutate({
mutation: localUpdateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
- assigneeIds: this.assigneeIds,
+ assignees,
},
},
});
+ this.track('updated_assignees');
},
- async focusTokenSelector() {
+ handleFocus() {
this.isEditing = true;
+ this.searchStarted = true;
+ },
+ async focusTokenSelector() {
+ this.handleFocus();
await this.$nextTick();
this.$refs.tokenSelector.focusTextInput();
},
+ handleMouseOver() {
+ this.timeout = setTimeout(() => {
+ this.searchStarted = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ handleMouseOut() {
+ clearTimeout(this.timeout);
+ },
+ setSearchKey(value) {
+ this.searchKey = value;
+ },
+ moveCurrentUserToStart(users = []) {
+ if (this.currentUser) {
+ return [this.currentUser, ...users.filter((user) => user.id !== this.currentUser.id)];
+ }
+ return users;
+ },
+ closeDropdown() {
+ this.$refs.tokenSelector.closeDropdown();
+ },
},
};
</script>
<template>
- <div class="gl-display-flex gl-mb-4 work-item-assignees gl-relative">
- <span class="gl-font-weight-bold gl-w-15 gl-pt-2" data-testid="assignees-title">{{
- __('Assignee(s)')
- }}</span>
+ <div class="form-row gl-mb-5 work-item-assignees gl-relative">
+ <span
+ class="gl-font-weight-bold col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
+ data-testid="assignees-title"
+ >{{ assigneeText }}</span
+ >
<gl-token-selector
ref="tokenSelector"
- v-model="localAssignees"
- hide-dropdown-with-no-items
+ :selected-tokens="localAssignees"
:container-class="containerClass"
- class="gl-w-full gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
- @token-remove="focusTokenSelector"
- @focus="isEditing = true"
- @blur="setAssignees"
+ class="assignees-selector gl-flex-grow-1 gl-border gl-border-white gl-rounded-base col-9 gl-align-self-start gl-px-0!"
+ :class="{ 'gl-hover-border-gray-200': canUpdate }"
+ :dropdown-items="dropdownItems"
+ :loading="isLoadingUsers"
+ :view-only="!canUpdate"
+ @input="handleAssigneesInput"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @blur="handleBlur"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
>
<template #empty-placeholder>
<div
@@ -91,7 +245,15 @@ export default {
data-testid="empty-state"
>
<gl-icon name="profile" />
- <span class="gl-ml-2">{{ __('Add assignees') }}</span>
+ <span class="gl-ml-2 gl-mr-4">{{ addAssigneesText }}</span>
+ <gl-button
+ v-if="currentUser"
+ size="small"
+ class="assign-myself"
+ data-testid="assign-self"
+ @click.stop="setAssignees([currentUser])"
+ >{{ __('Assign myself') }}</gl-button
+ >
</div>
</template>
<template #token-content="{ token }">
@@ -106,6 +268,29 @@ export default {
<span class="gl-pl-2">{{ token.name }}</span>
</gl-link>
</template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <sidebar-participant :user="dropdownItem" />
+ </template>
+ <template #loading-content>
+ <gl-skeleton-loader :height="170">
+ <rect width="380" height="20" x="10" y="15" rx="4" />
+ <rect width="280" height="20" x="10" y="50" rx="4" />
+ <rect width="380" height="20" x="10" y="95" rx="4" />
+ <rect width="280" height="20" x="10" y="130" rx="4" />
+ </gl-skeleton-loader>
+ </template>
+ <template #dropdown-footer>
+ <gl-dropdown-divider />
+ <gl-dropdown-item @click="closeDropdown">
+ <invite-members-trigger
+ :display-text="__('Invite members')"
+ trigger-element="side-nav"
+ icon="plus"
+ trigger-source="work-item-assignees-dropdown"
+ classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2"
+ />
+ </gl-dropdown-item>
+ </template>
</gl-token-selector>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 5a85fcdd7ac..90e3cd45cb4 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -35,7 +35,7 @@ export default {
isEditing: false,
isSubmitting: false,
isSubmittingWithKeydown: false,
- desc: '',
+ descriptionText: '',
};
},
apollo: {
@@ -71,16 +71,17 @@ export default {
descriptionHtml() {
return this.workItemDescription?.descriptionHtml;
},
- descriptionText: {
- get() {
- return this.desc;
- },
- set(desc) {
- this.desc = desc;
- },
+ descriptionEmpty() {
+ return this.descriptionHtml?.trim() === '';
},
workItemDescription() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
+ const descriptionWidget = this.workItem?.widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
+ );
+ return {
+ ...descriptionWidget,
+ description: descriptionWidget?.description || '',
+ };
},
workItemType() {
return this.workItem?.workItemType?.name;
@@ -95,14 +96,14 @@ export default {
async startEditing() {
this.isEditing = true;
- this.desc = getDraft(this.autosaveKey) || this.workItemDescription?.description || '';
+ this.descriptionText = getDraft(this.autosaveKey) || this.workItemDescription?.description;
await this.$nextTick();
this.$refs.textarea.focus();
},
async cancelEditing() {
- const isDirty = this.desc !== this.workItemDescription?.description;
+ const isDirty = this.descriptionText !== this.workItemDescription?.description;
if (isDirty) {
const msg = s__('WorkItem|Are you sure you want to cancel editing?');
@@ -125,7 +126,7 @@ export default {
return;
}
- updateDraft(this.autosaveKey, this.desc);
+ updateDraft(this.autosaveKey, this.descriptionText);
},
async updateWorkItem(event) {
if (event.key) {
@@ -171,25 +172,10 @@ export default {
<template>
<gl-form-group
v-if="isEditing"
- class="gl-pt-5 gl-mb-5 gl-mt-0! gl-border-t! gl-border-b"
+ class="gl-my-5"
:label="__('Description')"
label-for="work-item-description"
- label-class="gl-float-left"
>
- <div class="gl-display-flex gl-justify-content-flex-end">
- <gl-button class="gl-ml-auto" data-testid="cancel" @click="cancelEditing">{{
- __('Cancel')
- }}</gl-button>
- <gl-button
- class="js-no-auto-disable gl-ml-4"
- category="primary"
- variant="confirm"
- :loading="isSubmitting"
- data-testid="save-description"
- @click="updateWorkItem"
- >{{ __('Save') }}</gl-button
- >
- </div>
<markdown-field
can-attach-file
:textarea-value="descriptionText"
@@ -216,19 +202,35 @@ export default {
></textarea>
</template>
</markdown-field>
- </gl-form-group>
- <div v-else class="gl-pt-5 gl-mb-5 gl-border-t gl-border-b">
+
<div class="gl-display-flex">
- <h3 class="gl-font-base gl-mt-0">{{ __('Description') }}</h3>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ data-testid="save-description"
+ @click="updateWorkItem"
+ >{{ __('Save') }}</gl-button
+ >
+ <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing">{{
+ __('Cancel')
+ }}</gl-button>
+ </div>
+ </gl-form-group>
+ <div v-else class="gl-mb-5">
+ <div class="gl-display-flex gl-align-items-center gl-mb-5">
+ <h3 class="gl-font-base gl-my-0">{{ __('Description') }}</h3>
<gl-button
v-if="canEdit"
class="gl-ml-auto"
icon="pencil"
data-testid="edit-description"
+ :aria-label="__('Edit')"
@click="startEditing"
- >{{ __('Edit') }}</gl-button
- >
+ />
</div>
- <div v-safe-html="descriptionHtml" class="md gl-mb-5"></div>
+
+ <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
+ <div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div>
</div>
</template>
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 5272df2d53f..ad90fe88947 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -1,11 +1,15 @@
<script>
-import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlSkeletonLoader, GlIcon, GlButton } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import {
i18n,
- WIDGET_TYPE_ASSIGNEE,
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_LABELS,
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_WEIGHT,
+ WIDGET_TYPE_HIERARCHY,
+ WORK_ITEM_VIEWED_STORAGE_KEY,
} from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
@@ -14,22 +18,34 @@ import WorkItemState from './work_item_state.vue';
import WorkItemTitle from './work_item_title.vue';
import WorkItemDescription from './work_item_description.vue';
import WorkItemAssignees from './work_item_assignees.vue';
+import WorkItemLabels from './work_item_labels.vue';
import WorkItemWeight from './work_item_weight.vue';
+import WorkItemInformation from './work_item_information.vue';
export default {
i18n,
components: {
GlAlert,
+ GlButton,
GlSkeletonLoader,
+ GlIcon,
WorkItemAssignees,
WorkItemActions,
WorkItemDescription,
+ WorkItemLabels,
WorkItemTitle,
WorkItemState,
WorkItemWeight,
+ WorkItemInformation,
+ LocalStorageSync,
},
mixins: [glFeatureFlagMixin()],
props: {
+ isModal: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
workItemId: {
type: String,
required: false,
@@ -45,6 +61,7 @@ export default {
return {
error: undefined,
workItem: {},
+ showInfoBanner: true,
};
},
apollo: {
@@ -91,17 +108,40 @@ export default {
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
},
workItemAssignees() {
- return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEE);
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
+ },
+ workItemLabels() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
},
workItemWeight() {
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
},
+ workItemHierarchy() {
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
+ },
+ parentWorkItem() {
+ return this.workItemHierarchy?.parent;
+ },
+ parentUrl() {
+ return `../../issues/${this.parentWorkItem?.iid}`;
+ },
+ },
+ beforeDestroy() {
+ /** make sure that if the user has not even dismissed the alert ,
+ * should no be able to see the information next time and update the local storage * */
+ this.dismissBanner();
},
+ methods: {
+ dismissBanner() {
+ this.showInfoBanner = false;
+ },
+ },
+ WORK_ITEM_VIEWED_STORAGE_KEY,
};
</script>
<template>
- <section>
+ <section class="gl-pt-5">
<gl-alert v-if="error" variant="danger" @dismiss="error = undefined">
{{ error }}
</gl-alert>
@@ -113,39 +153,95 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <div class="gl-display-flex gl-align-items-start">
- <work-item-title
- :work-item-id="workItem.id"
- :work-item-title="workItem.title"
- :work-item-type="workItemType"
- :work-item-parent-id="workItemParentId"
- class="gl-mr-5"
- @error="error = $event"
- />
+ <div class="gl-display-flex gl-align-items-center">
+ <ul
+ v-if="parentWorkItem"
+ class="list-unstyled gl-display-flex gl-mr-auto"
+ data-testid="work-item-parent"
+ >
+ <li class="gl-ml-n4">
+ <gl-button icon="issues" category="tertiary" :href="parentUrl">{{
+ parentWorkItem.title
+ }}</gl-button>
+ <gl-icon name="chevron-right" :size="16" />
+ </li>
+ <li class="gl-px-4 gl-py-3 gl-line-height-0">
+ <gl-icon name="task-done" />
+ {{ workItemType }}
+ </li>
+ </ul>
+ <span
+ v-else
+ class="gl-font-weight-bold gl-text-secondary gl-mr-auto"
+ data-testid="work-item-type"
+ >{{ workItemType }}</span
+ >
<work-item-actions
:work-item-id="workItem.id"
:can-delete="canDelete"
- class="gl-ml-auto gl-mt-6"
@deleteWorkItem="$emit('deleteWorkItem')"
@error="error = $event"
/>
+ <gl-button
+ v-if="isModal"
+ category="tertiary"
+ data-testid="work-item-close"
+ icon="close"
+ :aria-label="__('Close')"
+ @click="$emit('close')"
+ />
</div>
- <template v-if="workItemsMvc2Enabled">
- <work-item-assignees
- v-if="workItemAssignees"
- :work-item-id="workItem.id"
- :assignees="workItemAssignees.nodes"
+ <local-storage-sync
+ v-model="showInfoBanner"
+ :storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY"
+ >
+ <work-item-information
+ v-if="showInfoBanner"
+ :show-info-banner="showInfoBanner"
+ @work-item-banner-dismissed="dismissBanner"
/>
- <work-item-weight v-if="workItemWeight" :weight="workItemWeight.weight" />
- </template>
+ </local-storage-sync>
+ <work-item-title
+ :work-item-id="workItem.id"
+ :work-item-title="workItem.title"
+ :work-item-type="workItemType"
+ :work-item-parent-id="workItemParentId"
+ @error="error = $event"
+ />
<work-item-state
:work-item="workItem"
:work-item-parent-id="workItemParentId"
@error="error = $event"
/>
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-assignees
+ v-if="workItemAssignees"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :assignees="workItemAssignees.assignees.nodes"
+ :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
+ :work-item-type="workItemType"
+ @error="error = $event"
+ />
+ <work-item-labels
+ v-if="workItemLabels"
+ :work-item-id="workItem.id"
+ :can-update="canUpdate"
+ @error="error = $event"
+ />
+ <work-item-weight
+ v-if="workItemWeight"
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :weight="workItemWeight.weight"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ />
+ </template>
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
+ class="gl-pt-5"
@error="error = $event"
/>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index d1c8022ac57..df7c6cab7ef 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -80,13 +80,16 @@ export default {
.catch((e) => {
this.error =
e.message ||
- s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
+ s__('WorkItem|Something went wrong when deleting the task. Please try again.');
});
},
closeModal() {
this.error = '';
this.$emit('close');
},
+ hide() {
+ this.$refs.modal.hide();
+ },
setErrorMessage(message) {
this.error = message;
},
@@ -104,7 +107,6 @@ export default {
size="lg"
modal-id="work-item-detail-modal"
header-class="gl-p-0 gl-pb-2!"
- body-class="gl-pb-6!"
@hide="closeModal"
>
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
@@ -112,9 +114,11 @@ export default {
</gl-alert>
<work-item-detail
+ is-modal
:work-item-parent-id="issueGid"
:work-item-id="workItemId"
class="gl-p-5 gl-mt-n3"
+ @close="hide"
@deleteWorkItem="deleteWorkItem"
/>
</gl-modal>
diff --git a/app/assets/javascripts/work_items/components/work_item_information.vue b/app/assets/javascripts/work_items/components/work_item_information.vue
new file mode 100644
index 00000000000..2ff7ba169ea
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_information.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ i18n: {
+ learnTasksButtonText: s__('WorkItem|Learn about tasks'),
+ workItemsText: s__('WorkItem|work items'),
+ tasksInformationTitle: s__('WorkItem|Introducing tasks'),
+ tasksInformationBody: s__(
+ 'WorkItem|A task provides the ability to break down your work into smaller pieces tied to an issue. Tasks are the first items using our new %{workItemsLink} objects. Additional work item types will be coming soon.',
+ ),
+ },
+ helpPageLinks: {
+ tasksDocLinkPath: helpPagePath('user/tasks'),
+ workItemsLinkPath: helpPagePath(`development/work_items`),
+ },
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ },
+ props: {
+ showInfoBanner: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ emits: ['work-item-banner-dismissed'],
+};
+</script>
+
+<template>
+ <section class="gl-display-block gl-mb-2">
+ <gl-alert
+ v-if="showInfoBanner"
+ variant="tip"
+ :title="$options.i18n.tasksInformationTitle"
+ :primary-button-link="$options.helpPageLinks.tasksDocLinkPath"
+ :primary-button-text="$options.i18n.learnTasksButtonText"
+ data-testid="work-item-information"
+ class="gl-mt-3"
+ @dismiss="$emit('work-item-banner-dismissed')"
+ >
+ <gl-sprintf :message="$options.i18n.tasksInformationBody">
+ <template #workItemsLink>
+ <gl-link :href="$options.helpPageLinks.workItemsLinkPath">{{
+ $options.i18n.workItemsText
+ }}</gl-link>
+ </template>
+ ></gl-sprintf
+ >
+ </gl-alert>
+ </section>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
new file mode 100644
index 00000000000..78ed67998d7
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -0,0 +1,246 @@
+<script>
+import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import Tracking from '~/tracking';
+import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
+import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
+import workItemQuery from '../graphql/work_item.query.graphql';
+import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+
+import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_LABELS } from '../constants';
+
+function isTokenSelectorElement(el) {
+ return el?.classList.contains('gl-label-close') || el?.classList.contains('dropdown-item');
+}
+
+function addClass(el) {
+ return {
+ ...el,
+ class: 'gl-bg-transparent',
+ };
+}
+
+export default {
+ components: {
+ GlTokenSelector,
+ GlLabel,
+ GlSkeletonLoader,
+ LabelItem,
+ },
+ mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isEditing: false,
+ searchStarted: false,
+ localLabels: [],
+ searchKey: '',
+ searchLabels: [],
+ };
+ },
+ apollo: {
+ workItem: {
+ query: workItemQuery,
+ variables() {
+ return {
+ id: this.workItemId,
+ };
+ },
+ skip() {
+ return !this.workItemId;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ searchLabels: {
+ query: labelSearchQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ search: this.searchKey,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace?.labels?.nodes.map((node) => addClass({ ...node, ...node.label }));
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_labels',
+ property: `type_${this.workItem.workItemType?.name}`,
+ };
+ },
+ allowScopedLabels() {
+ return this.labelsWidget.allowScopedLabels;
+ },
+ listEmpty() {
+ return this.labels.length === 0;
+ },
+ containerClass() {
+ return !this.isEditing ? 'gl-shadow-none!' : '';
+ },
+ isLoading() {
+ return this.$apollo.queries.searchLabels.loading;
+ },
+ labelsWidget() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ },
+ labels() {
+ return this.labelsWidget?.nodes || [];
+ },
+ },
+ watch: {
+ labels(newVal) {
+ if (!this.isEditing) {
+ this.localLabels = newVal.map(addClass);
+ }
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ methods: {
+ getId(id) {
+ return getIdFromGraphQLId(id);
+ },
+ removeLabel({ id }) {
+ this.localLabels = this.localLabels.filter((label) => label.id !== id);
+ },
+ setLabels(event) {
+ this.searchKey = '';
+ if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return;
+ this.isEditing = false;
+ this.$apollo
+ .mutate({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ labels: this.localLabels,
+ },
+ },
+ })
+ .catch((e) => {
+ this.$emit('error', e);
+ });
+ this.track('updated_labels');
+ },
+ handleFocus() {
+ this.isEditing = true;
+ this.searchStarted = true;
+ },
+ async focusTokenSelector(labels) {
+ if (this.allowScopedLabels) {
+ const newLabel = labels[labels.length - 1];
+ const existingLabels = labels.slice(0, labels.length - 1);
+
+ const newLabelKey = scopedLabelKey(newLabel);
+
+ const removeLabelsWithSameScope = existingLabels.filter((label) => {
+ const sameKey = newLabelKey === scopedLabelKey(label);
+ return !sameKey;
+ });
+
+ this.localLabels = [...removeLabelsWithSameScope, newLabel];
+ }
+ this.handleFocus();
+ await this.$nextTick();
+ this.$refs.tokenSelector.focusTextInput();
+ },
+ handleMouseOver() {
+ this.timeout = setTimeout(() => {
+ this.searchStarted = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ handleMouseOut() {
+ clearTimeout(this.timeout);
+ },
+ setSearchKey(value) {
+ this.searchKey = value;
+ },
+ scopedLabel(label) {
+ return this.allowScopedLabels && isScopedLabel(label);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="form-row gl-mb-5 work-item-labels gl-relative">
+ <span
+ class="gl-font-weight-bold gl-mt-2 col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
+ data-testid="labels-title"
+ >{{ __('Labels') }}</span
+ >
+ <gl-token-selector
+ ref="tokenSelector"
+ v-model="localLabels"
+ :container-class="containerClass"
+ :dropdown-items="searchLabels"
+ :loading="isLoading"
+ :view-only="!canUpdate"
+ class="gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2!"
+ @input="focusTokenSelector"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @blur="setLabels"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
+ >
+ <template #empty-placeholder>
+ <div
+ class="add-labels gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-400 gl-pr-4 gl-top-2"
+ data-testid="empty-state"
+ >
+ <span v-if="canUpdate" class="gl-ml-2">{{ __('Select labels') }}</span>
+ <span v-else class="gl-ml-2">{{ __('None') }}</span>
+ </div>
+ </template>
+ <template #token-content="{ token }">
+ <gl-label
+ :data-qa-label-name="token.title"
+ :title="token.title"
+ :description="token.description"
+ :background-color="token.color"
+ :scoped="scopedLabel(token)"
+ :show-close-button="canUpdate"
+ @close="removeLabel(token)"
+ />
+ </template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <label-item :label="dropdownItem" />
+ </template>
+ <template #loading-content>
+ <gl-skeleton-loader :height="170">
+ <rect width="380" height="20" x="10" y="15" rx="4" />
+ <rect width="280" height="20" x="10" y="50" rx="4" />
+ <rect width="380" height="20" x="10" y="95" rx="4" />
+ <rect width="280" height="20" x="10" y="130" rx="4" />
+ </gl-skeleton-loader>
+ </template>
+ </gl-token-selector>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 320a4a213e3..176f84f6c1a 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -1,9 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlToast } from '@gitlab/ui';
import createDefaultClient from '~/lib/graphql';
import WorkItemLinks from './work_item_links.vue';
Vue.use(VueApollo);
+Vue.use(GlToast);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -19,6 +21,7 @@ export default function initWorkItemLinks() {
if (!workItemLinksRoot) {
return;
}
+
// eslint-disable-next-line no-new
new Vue({
el: workItemLinksRoot,
@@ -27,6 +30,9 @@ export default function initWorkItemLinks() {
components: {
workItemLinks: WorkItemLinks,
},
+ provide: {
+ projectPath: workItemLinksRoot.dataset.projectPath,
+ },
render: (createElement) =>
createElement('work-item-links', {
props: {
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 bdfff100333..89f086cfca5 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
@@ -11,6 +11,7 @@ import {
} from '../../constants';
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import WorkItemLinksForm from './work_item_links_form.vue';
+import WorkItemLinksMenu from './work_item_links_menu.vue';
export default {
components: {
@@ -19,6 +20,7 @@ export default {
GlIcon,
GlLoadingIcon,
WorkItemLinksForm,
+ WorkItemLinksMenu,
},
props: {
workItemId: {
@@ -77,6 +79,9 @@ export default {
isLoading() {
return this.$apollo.queries.children.loading;
},
+ childrenIds() {
+ return this.children.map((c) => c.id);
+ },
},
methods: {
badgeVariant(state) {
@@ -88,13 +93,16 @@ export default {
toggleAddForm() {
this.isShownAddForm = !this.isShownAddForm;
},
+ addChild(child) {
+ this.children = [child, ...this.children];
+ },
},
i18n: {
title: s__('WorkItem|Child items'),
emptyStateMessage: s__(
'WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!',
),
- addChildButtonLabel: s__('WorkItem|Add a child'),
+ addChildButtonLabel: s__('WorkItem|Add a task'),
},
WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK,
WORK_ITEM_STATUS_TEXT,
@@ -107,8 +115,16 @@ export default {
class="gl-p-4 gl-display-flex gl-justify-content-space-between"
:class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
>
- <h5 class="gl-m-0 gl-line-height-32">{{ $options.i18n.title }}</h5>
- <div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-50 gl-pl-4">
+ <h5 class="gl-m-0 gl-line-height-32 gl-flex-grow-1">{{ $options.i18n.title }}</h5>
+ <gl-button
+ v-if="!isShownAddForm"
+ category="secondary"
+ data-testid="toggle-add-form"
+ @click="toggleAddForm"
+ >
+ {{ $options.i18n.addChildButtonLabel }}
+ </gl-button>
+ <div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-50 gl-pl-4 gl-ml-3">
<gl-button
category="tertiary"
:icon="toggleIcon"
@@ -126,37 +142,38 @@ export default {
<gl-loading-icon v-if="isLoading" color="dark" class="gl-my-3" />
<template v-else>
- <div v-if="isChildrenEmpty" class="gl-px-8" data-testid="links-empty">
- <p>
+ <div v-if="isChildrenEmpty && !isShownAddForm" data-testid="links-empty">
+ <p class="gl-my-3">
{{ $options.i18n.emptyStateMessage }}
</p>
- <gl-button
- v-if="!isShownAddForm"
- category="secondary"
- variant="confirm"
- data-testid="toggle-add-form"
- @click="toggleAddForm"
- >
- {{ $options.i18n.addChildButtonLabel }}
- </gl-button>
- <work-item-links-form v-else data-testid="add-links-form" @cancel="toggleAddForm" />
</div>
+ <work-item-links-form
+ v-if="isShownAddForm"
+ data-testid="add-links-form"
+ :issuable-gid="issuableGid"
+ :children-ids="childrenIds"
+ @cancel="toggleAddForm"
+ @addWorkItemChild="addChild"
+ />
<div
v-for="child in children"
:key="child.id"
- class="gl-relative gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base"
+ class="gl-relative gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
data-testid="links-child"
>
<div>
<gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-3 gl-text-gray-700" />
<span class="gl-word-break-all">{{ child.title }}</span>
</div>
- <div class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0">
+ <div
+ class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0 gl-display-inline-flex gl-align-items-center"
+ >
<gl-badge :variant="badgeVariant(child.state)">
<span class="gl-sm-display-block">{{
$options.WORK_ITEM_STATUS_TEXT[child.state]
}}</span>
</gl-badge>
+ <work-item-links-menu :work-item-id="child.id" :parent-work-item-id="issuableGid" />
</div>
</div>
</template>
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 22728f58026..fadba0753db 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
@@ -1,27 +1,127 @@
<script>
-import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
+import { GlAlert, GlForm, GlFormCombobox, GlButton } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { __, s__ } from '~/locale';
+import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
+import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
export default {
components: {
+ GlAlert,
GlForm,
- GlFormInput,
+ GlFormCombobox,
GlButton,
},
+ inject: ['projectPath'],
+ props: {
+ issuableGid: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ childrenIds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ apollo: {
+ availableWorkItems: {
+ query: projectWorkItemsQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ searchTerm: this.search?.title || this.search,
+ types: ['TASK'],
+ };
+ },
+ skip() {
+ return this.search.length === 0;
+ },
+ update(data) {
+ return data.workspace.workItems.edges
+ .filter((wi) => !this.childrenIds.includes(wi.node.id))
+ .map((wi) => wi.node);
+ },
+ },
+ },
data() {
return {
- relatedWorkItem: '',
+ availableWorkItems: [],
+ search: '',
+ error: null,
};
},
+ methods: {
+ getIdFromGraphQLId,
+ unsetError() {
+ this.error = null;
+ },
+ addChild() {
+ this.$apollo
+ .mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.issuableGid,
+ hierarchyWidget: {
+ childrenIds: [this.search.id],
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate?.errors?.length) {
+ [this.error] = data.workItemUpdate.errors;
+ } else {
+ this.unsetError();
+ this.$emit('addWorkItemChild', this.search);
+ }
+ })
+ .catch(() => {
+ this.error = this.$options.i18n.errorMessage;
+ })
+ .finally(() => {
+ this.search = '';
+ });
+ },
+ },
+ i18n: {
+ inputLabel: __('Children'),
+ errorMessage: s__(
+ 'WorkItem|Something went wrong when trying to add a child. Please try again.',
+ ),
+ },
};
</script>
<template>
- <gl-form @submit.prevent>
- <gl-form-input v-model="relatedWorkItem" class="gl-mb-4" />
- <gl-button type="submit" category="secondary" variant="confirm">
- {{ s__('WorkItem|Add') }}
+ <gl-form
+ class="gl-mb-3 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base"
+ >
+ <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
+ {{ error }}
+ </gl-alert>
+ <gl-form-combobox
+ v-model="search"
+ :token-list="availableWorkItems"
+ match-value-to-attr="title"
+ class="gl-mb-4"
+ :label-text="$options.i18n.inputLabel"
+ label-sr-only
+ autofocus
+ >
+ <template #result="{ item }">
+ <div class="gl-display-flex">
+ <div class="gl-text-gray-400 gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div>
+ <div>{{ item.title }}</div>
+ </div>
+ </template>
+ </gl-form-combobox>
+ <gl-button category="secondary" data-testid="add-child-button" @click="addChild">
+ {{ s__('WorkItem|Add task') }}
</gl-button>
- <gl-button category="tertiary" class="gl-float-right" @click="$emit('cancel')">
+ <gl-button category="tertiary" @click="$emit('cancel')">
{{ s__('WorkItem|Cancel') }}
</gl-button>
</gl-form>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
new file mode 100644
index 00000000000..6deb87c5dca
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
@@ -0,0 +1,101 @@
+<script>
+import { GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { produce } from 'immer';
+import { s__ } from '~/locale';
+import changeWorkItemParentMutation from '../../graphql/change_work_item_parent_link.mutation.graphql';
+import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
+import { WIDGET_TYPE_HIERARCHY } from '../../constants';
+
+export default {
+ components: {
+ GlDropdownItem,
+ GlDropdown,
+ GlIcon,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ parentWorkItemId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ activeToast: null,
+ };
+ },
+ methods: {
+ toggleChildFromCache(data, store) {
+ const sourceData = store.readQuery({
+ query: getWorkItemLinksQuery,
+ variables: { id: this.parentWorkItemId },
+ });
+
+ const newData = produce(sourceData, (draftState) => {
+ const widgetHierarchy = draftState.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+
+ const index = widgetHierarchy.children.nodes.findIndex(
+ (child) => child.id === this.workItemId,
+ );
+
+ if (index >= 0) {
+ widgetHierarchy.children.nodes.splice(index, 1);
+ } else {
+ widgetHierarchy.children.nodes.push(data.workItemUpdate.workItem);
+ }
+ });
+
+ store.writeQuery({
+ query: getWorkItemLinksQuery,
+ variables: { id: this.parentWorkItemId },
+ data: newData,
+ });
+ },
+ async addChild(data) {
+ const { data: resp } = await this.$apollo.mutate({
+ mutation: changeWorkItemParentMutation,
+ variables: { id: this.workItemId, parentId: this.parentWorkItemId },
+ update: this.toggleChildFromCache.bind(this, data),
+ });
+
+ if (resp.workItemUpdate.errors.length === 0) {
+ this.activeToast?.hide();
+ }
+ },
+ async removeChild() {
+ const { data } = await this.$apollo.mutate({
+ mutation: changeWorkItemParentMutation,
+ variables: { id: this.workItemId, parentId: null },
+ update: this.toggleChildFromCache.bind(this, null),
+ });
+
+ if (data.workItemUpdate.errors.length === 0) {
+ this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
+ action: {
+ text: s__('WorkItem|Undo'),
+ onClick: this.addChild.bind(this, data),
+ },
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-ml-2">
+ <gl-dropdown category="tertiary" toggle-class="btn-icon" :right="true">
+ <template #button-content>
+ <gl-icon name="ellipsis_v" :size="14" />
+ </template>
+ <gl-dropdown-item @click="removeChild">
+ {{ s__('WorkItem|Remove') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </span>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_weight.vue b/app/assets/javascripts/work_items/components/work_item_weight.vue
index b0f2b3aa14a..30e2c1e56b8 100644
--- a/app/assets/javascripts/work_items/components/work_item_weight.vue
+++ b/app/assets/javascripts/work_items/components/work_item_weight.vue
@@ -1,26 +1,142 @@
<script>
+import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { __ } from '~/locale';
+import Tracking from '~/tracking';
+import { TRACKING_CATEGORY_SHOW } from '../constants';
+import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+
+/* eslint-disable @gitlab/require-i18n-strings */
+const allowedKeys = [
+ 'Alt',
+ 'ArrowDown',
+ 'ArrowLeft',
+ 'ArrowRight',
+ 'ArrowUp',
+ 'Backspace',
+ 'Control',
+ 'Delete',
+ 'End',
+ 'Enter',
+ 'Home',
+ 'Meta',
+ 'PageDown',
+ 'PageUp',
+ 'Tab',
+ '0',
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+];
+/* eslint-enable @gitlab/require-i18n-strings */
export default {
+ inputId: 'weight-widget-input',
+ components: {
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ },
+ mixins: [Tracking.mixin()],
inject: ['hasIssueWeightsFeature'],
props: {
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
weight: {
type: Number,
required: false,
default: undefined,
},
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isEditing: false,
+ };
},
computed: {
- weightText() {
- return this.weight ?? __('None');
+ placeholder() {
+ return this.canUpdate && this.isEditing ? __('Enter a number') : __('None');
+ },
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_weight',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ type() {
+ return this.canUpdate && this.isEditing ? 'number' : 'text';
+ },
+ },
+ methods: {
+ blurInput() {
+ this.$refs.input.$el.blur();
+ },
+ handleFocus() {
+ this.isEditing = true;
+ },
+ handleKeydown(event) {
+ if (!allowedKeys.includes(event.key)) {
+ event.preventDefault();
+ }
+ },
+ updateWeight(event) {
+ this.isEditing = false;
+ this.track('updated_weight');
+ this.$apollo.mutate({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ weight: event.target.value === '' ? null : Number(event.target.value),
+ },
+ },
+ });
},
},
};
</script>
<template>
- <div v-if="hasIssueWeightsFeature" class="gl-mb-5">
- <span class="gl-display-inline-block gl-font-weight-bold gl-w-15">{{ __('Weight') }}</span>
- {{ weightText }}
- </div>
+ <gl-form v-if="hasIssueWeightsFeature" @submit.prevent="blurInput">
+ <gl-form-group
+ class="gl-align-items-center"
+ :label="__('Weight')"
+ :label-for="$options.inputId"
+ label-class="gl-pb-0! gl-overflow-wrap-break"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <gl-form-input
+ :id="$options.inputId"
+ ref="input"
+ min="0"
+ :placeholder="placeholder"
+ :readonly="!canUpdate"
+ size="sm"
+ :type="type"
+ :value="weight"
+ @blur="updateWeight"
+ @focus="handleFocus"
+ @keydown="handleKeydown"
+ @keydown.exact.esc.stop="blurInput"
+ />
+ </gl-form-group>
+ </gl-form>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 2df4978a319..2140b418e6d 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -13,12 +13,14 @@ export const i18n = {
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
};
-export const DEFAULT_MODAL_TYPE = 'Task';
+export const TASK_TYPE_NAME = 'Task';
-export const WIDGET_TYPE_ASSIGNEE = 'ASSIGNEES';
+export const WIDGET_TYPE_ASSIGNEES = 'ASSIGNEES';
export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
+export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
+export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
export const WIDGET_TYPE_TASK_ICON = 'task-done';
diff --git a/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql b/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql
new file mode 100644
index 00000000000..dc5286174d8
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql
@@ -0,0 +1,13 @@
+mutation changeWorkItemParentLink($id: WorkItemID!, $parentId: WorkItemID) {
+ workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) {
+ workItem {
+ id
+ workItemType {
+ id
+ }
+ title
+ state
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
index b25210f5c74..ccfe62cc585 100644
--- a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
@@ -1,8 +1,12 @@
+#import "./work_item.fragment.graphql"
+
mutation workItemCreateFromTask($input: WorkItemCreateFromTaskInput!) {
workItemCreateFromTask(input: $input) {
workItem {
- id
- descriptionHtml
+ ...WorkItem
+ }
+ newWorkItem {
+ ...WorkItem
}
errors
}
diff --git a/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql
index 0d31ecef6f8..43c92cf89ec 100644
--- a/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql
@@ -1,6 +1,6 @@
#import "./work_item.fragment.graphql"
-mutation localUpdateWorkItem($input: LocalWorkItemAssigneesInput) {
+mutation localUpdateWorkItem($input: LocalUpdateWorkItemInput) {
localUpdateWorkItem(input: $input) @client {
workItem {
...WorkItem
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
new file mode 100644
index 00000000000..7d38d203b84
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -0,0 +1,14 @@
+query projectWorkItems($searchTerm: String, $projectPath: ID!, $types: [IssueType!]) {
+ workspace: project(fullPath: $projectPath) {
+ id
+ workItems(search: $searchTerm, types: $types) {
+ edges {
+ node {
+ id
+ title
+ state
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js
index 09d929faae2..8788ad21e7b 100644
--- a/app/assets/javascripts/work_items/graphql/provider.js
+++ b/app/assets/javascripts/work_items/graphql/provider.js
@@ -2,7 +2,7 @@ import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import { WIDGET_TYPE_ASSIGNEE } from '../constants';
+import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_LABELS, WIDGET_TYPE_WEIGHT } from '../constants';
import typeDefs from './typedefs.graphql';
import workItemQuery from './work_item.query.graphql';
@@ -10,7 +10,7 @@ export const temporaryConfig = {
typeDefs,
cacheConfig: {
possibleTypes: {
- LocalWorkItemWidget: ['LocalWorkItemAssignees'],
+ LocalWorkItemWidget: ['LocalWorkItemLabels', 'LocalWorkItemWeight'],
},
typePolicies: {
WorkItem: {
@@ -20,33 +20,15 @@ export const temporaryConfig = {
return (
widgets || [
{
- __typename: 'LocalWorkItemAssignees',
- type: 'ASSIGNEES',
- nodes: [
- {
- __typename: 'UserCore',
- id: 'gid://gitlab/User/1',
- avatarUrl: '',
- webUrl: '',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- name: 'John Doe',
- username: 'doe_I',
- },
- {
- __typename: 'UserCore',
- id: 'gid://gitlab/User/2',
- avatarUrl: '',
- webUrl: '',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- name: 'Marcus Rutherford',
- username: 'ruthfull',
- },
- ],
+ __typename: 'LocalWorkItemLabels',
+ type: WIDGET_TYPE_LABELS,
+ allowScopedLabels: true,
+ nodes: [],
},
{
__typename: 'LocalWorkItemWeight',
type: 'WEIGHT',
- weight: 0,
+ weight: null,
},
]
);
@@ -67,12 +49,26 @@ export const resolvers = {
});
const data = produce(sourceData, (draftData) => {
- const assigneesWidget = draftData.workItem.mockWidgets.find(
- (widget) => widget.type === WIDGET_TYPE_ASSIGNEE,
- );
- assigneesWidget.nodes = assigneesWidget.nodes.filter((assignee) =>
- input.assigneeIds.includes(assignee.id),
- );
+ if (input.assignees) {
+ const assigneesWidget = draftData.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_ASSIGNEES,
+ );
+ assigneesWidget.assignees.nodes = [...input.assignees];
+ }
+
+ if (input.weight != null) {
+ const weightWidget = draftData.workItem.mockWidgets.find(
+ (widget) => widget.type === WIDGET_TYPE_WEIGHT,
+ );
+ weightWidget.weight = input.weight;
+ }
+
+ if (input.labels) {
+ const labelsWidget = draftData.workItem.mockWidgets.find(
+ (widget) => widget.type === WIDGET_TYPE_LABELS,
+ );
+ labelsWidget.nodes = [...input.labels];
+ }
});
cache.writeQuery({
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index bfe2f0fe0ce..48228b15a53 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -1,5 +1,6 @@
enum LocalWidgetType {
ASSIGNEES
+ LABELS
WEIGHT
}
@@ -12,6 +13,12 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
nodes: [UserCore]
}
+type LocalWorkItemLabels implements LocalWorkItemWidget {
+ type: LocalWidgetType!
+ allowScopedLabels: Boolean!
+ nodes: [Label!]
+}
+
type LocalWorkItemWeight implements LocalWorkItemWidget {
type: LocalWidgetType!
weight: Int
@@ -21,9 +28,11 @@ extend type WorkItem {
mockWidgets: [LocalWorkItemWidget]
}
-type LocalWorkItemAssigneesInput {
+input LocalUpdateWorkItemInput {
id: WorkItemID!
- assigneeIds: [ID!]
+ assignees: [UserCore!]
+ labels: [Label]
+ weight: Int
}
type LocalWorkItemPayload {
@@ -32,5 +41,5 @@ type LocalWorkItemPayload {
}
extend type Mutation {
- localUpdateWorkItem(input: LocalWorkItemAssigneesInput!): LocalWorkItemPayload
+ localUpdateWorkItem(input: LocalUpdateWorkItemInput!): LocalWorkItemPayload
}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
index c0b6e856411..25eb8099251 100644
--- a/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
@@ -5,5 +5,6 @@ mutation workItemUpdate($input: WorkItemUpdateInput!) {
workItem {
...WorkItem
}
+ errors
}
}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
index 470de060ee3..ad861a60d15 100644
--- a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
@@ -1,8 +1,13 @@
+#import "./work_item.fragment.graphql"
+
mutation workItemUpdateTask($input: WorkItemUpdateTaskInput!) {
workItemUpdate: workItemUpdateTask(input: $input) {
workItem {
id
descriptionHtml
}
+ task {
+ ...WorkItem
+ }
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 04701f6899e..5f64eda96aa 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment WorkItem on WorkItem {
id
title
@@ -17,5 +19,29 @@ fragment WorkItem on WorkItem {
description
descriptionHtml
}
+ ... on WorkItemWidgetAssignees {
+ type
+ allowsMultipleAssignees
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ ... on WorkItemWidgetHierarchy {
+ type
+ parent {
+ id
+ iid
+ title
+ }
+ children {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ }
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 30bc61f5c59..61cb8802187 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -1,17 +1,15 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "./work_item.fragment.graphql"
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
mockWidgets @client {
- ... on LocalWorkItemAssignees {
+ ... on LocalWorkItemLabels {
type
+ allowScopedLabels
nodes {
- id
- avatarUrl
- name
- username
- webUrl
+ ...Label
}
}
... on LocalWorkItemWeight {
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index 33e28831b54..6437df597b4 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -10,6 +10,7 @@ export const initWorkItemsRoot = () => {
return new Vue({
el,
+ name: 'WorkItemsRoot',
router: createRouter(el.dataset.fullPath),
apolloProvider: createApolloProvider(),
provide: {
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 04c6a61689c..482da5419c6 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -6,12 +6,11 @@ import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
-import { DEFAULT_MODAL_TYPE } from '../constants';
import ItemTitle from '../components/item_title.vue';
export default {
- createErrorText: s__('WorkItem|Something went wrong when creating a work item. Please try again'),
+ createErrorText: s__('WorkItem|Something went wrong when creating a task. Please try again'),
fetchTypesErrorText: s__(
'WorkItem|Something went wrong when fetching work item types. Please try again',
),
@@ -24,11 +23,6 @@ export default {
},
inject: ['fullPath'],
props: {
- isModal: {
- type: Boolean,
- required: false,
- default: false,
- },
initialTitle: {
type: String,
required: false,
@@ -78,13 +72,6 @@ export default {
text: node.name,
}));
},
- result() {
- if (!this.selectedWorkItemType && this.isModal) {
- this.selectedWorkItemType = this.formOptions.find(
- (options) => options.text === DEFAULT_MODAL_TYPE,
- )?.value;
- }
- },
error() {
this.error = this.$options.fetchTypesErrorText;
},
@@ -104,11 +91,7 @@ export default {
methods: {
async createWorkItem() {
this.loading = true;
- if (this.isModal) {
- await this.createWorkItemFromTask();
- } else {
- await this.createStandaloneWorkItem();
- }
+ await this.createStandaloneWorkItem();
this.loading = false;
},
async createStandaloneWorkItem() {
@@ -174,11 +157,7 @@ export default {
this.title = title;
},
handleCancelClick() {
- if (!this.isModal) {
- this.$router.go(-1);
- return;
- }
- this.$emit('closeModal');
+ this.$router.go(-1);
},
},
};
@@ -187,7 +166,7 @@ export default {
<template>
<form @submit.prevent="createWorkItem">
<gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
- <div :class="{ 'gl-px-5': isModal }" data-testid="content">
+ <div data-testid="content">
<item-title :title="initialTitle" data-testid="title-input" @title-input="handleTitleInput" />
<div>
<gl-loading-icon
@@ -203,14 +182,11 @@ export default {
/>
</div>
</div>
- <div
- class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4"
- :class="{ 'gl-display-flex gl-justify-content-end': isModal }"
- >
+ <div class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4">
<gl-button
variant="confirm"
:disabled="isButtonDisabled"
- :class="{ 'gl-mr-3': !isModal }"
+ class="gl-mr-3"
:loading="loading"
data-testid="create-button"
type="submit"
@@ -221,7 +197,6 @@ export default {
type="button"
data-testid="cancel-button"
class="gl-order-n1"
- :class="{ 'gl-mr-3': isModal }"
@click="handleCancelClick"
>
{{ __('Cancel') }}