diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
commit | 36a59d088eca61b834191dacea009677a96c052f (patch) | |
tree | e4f33972dab5d8ef79e3944a9f403035fceea43f /app/assets/javascripts/work_items | |
parent | a1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff) |
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/work_items')
14 files changed, 379 insertions, 95 deletions
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue new file mode 100644 index 00000000000..0b6c1a75bb2 --- /dev/null +++ b/app/assets/javascripts/work_items/components/item_state.vue @@ -0,0 +1,62 @@ +<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, + }, + loading: { + 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"> + <gl-form-select + :id="$options.labelId" + :value="state" + :options="$options.states" + :disabled="loading" + class="gl-w-auto" + @change="setState" + /> + </gl-form-group> +</template> 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 40b6fcdd204..31e4a932c5a 100644 --- a/app/assets/javascripts/work_items/components/work_item_actions.vue +++ b/app/assets/javascripts/work_items/components/work_item_actions.vue @@ -1,7 +1,7 @@ <script> import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui'; import { s__ } from '~/locale'; -import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql'; +import Tracking from '~/tracking'; export default { i18n: { @@ -15,55 +15,36 @@ export default { directives: { GlModal: GlModalDirective, }, + mixins: [Tracking.mixin({ label: 'actions_menu' })], props: { workItemId: { type: String, required: false, default: null, }, - canUpdate: { + canDelete: { type: Boolean, required: false, default: false, }, }, - emits: ['workItemDeleted', 'error'], + emits: ['deleteWorkItem'], methods: { - deleteWorkItem() { - this.$apollo - .mutate({ - mutation: deleteWorkItemMutation, - variables: { - input: { - id: this.workItemId, - }, - }, - }) - .then(({ data: { workItemDelete, errors } }) => { - if (errors?.length) { - throw new Error(errors[0].message); - } - - if (workItemDelete?.errors.length) { - throw new Error(workItemDelete.errors[0]); - } - - this.$emit('workItemDeleted'); - }) - .catch((e) => { - this.$emit( - 'error', - e.message || - s__('WorkItem|Something went wrong when deleting the work item. Please try again.'), - ); - }); + handleDeleteWorkItem() { + this.track('click_delete_work_item'); + this.$emit('deleteWorkItem'); + }, + handleCancelDeleteWorkItem({ trigger }) { + if (trigger !== 'ok') { + this.track('cancel_delete_work_item'); + } }, }, }; </script> <template> - <div v-if="canUpdate"> + <div v-if="canDelete"> <gl-dropdown icon="ellipsis_v" text-sr-only @@ -81,7 +62,8 @@ export default { :title="$options.i18n.deleteWorkItem" :ok-title="$options.i18n.deleteWorkItem" ok-variant="danger" - @ok="deleteWorkItem" + @ok="handleDeleteWorkItem" + @hide="handleCancelDeleteWorkItem" > {{ s__( 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 f2fb1e3ccbc..4222ffe42fe 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -1,15 +1,20 @@ <script> -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; import { i18n } from '../constants'; import workItemQuery from '../graphql/work_item.query.graphql'; import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql'; +import WorkItemActions from './work_item_actions.vue'; +import WorkItemState from './work_item_state.vue'; import WorkItemTitle from './work_item_title.vue'; export default { i18n, components: { GlAlert, + GlSkeletonLoader, + WorkItemActions, WorkItemTitle, + WorkItemState, }, props: { workItemId: { @@ -49,9 +54,18 @@ export default { }, }, computed: { + workItemLoading() { + return this.$apollo.queries.workItem.loading; + }, workItemType() { return this.workItem.workItemType?.name; }, + canUpdate() { + return this.workItem?.userPermissions?.updateWorkItem; + }, + canDelete() { + return this.workItem?.userPermissions?.deleteWorkItem; + }, }, }; </script> @@ -62,12 +76,35 @@ export default { {{ error }} </gl-alert> - <work-item-title - :loading="$apollo.queries.workItem.loading" - :work-item-id="workItem.id" - :work-item-title="workItem.title" - :work-item-type="workItemType" - @error="error = $event" - /> + <div v-if="workItemLoading" class="gl-max-w-26 gl-py-5"> + <gl-skeleton-loader :height="65" :width="240"> + <rect width="240" height="20" x="5" y="0" rx="4" /> + <rect width="100" height="20" x="5" y="45" rx="4" /> + </gl-skeleton-loader> + </div> + <template v-else> + <div class="gl-display-flex"> + <work-item-title + :work-item-id="workItem.id" + :work-item-title="workItem.title" + :work-item-type="workItemType" + class="gl-mr-5" + @error="error = $event" + @updated="$emit('workItemUpdated')" + /> + <work-item-actions + :work-item-id="workItem.id" + :can-delete="canDelete" + class="gl-ml-auto gl-mt-5" + @deleteWorkItem="$emit('deleteWorkItem')" + @error="error = $event" + /> + </div> + <work-item-state + :work-item="workItem" + @error="error = $event" + @updated="$emit('workItemUpdated')" + /> + </template> </section> </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 a79091fb8b2..172a40a6e56 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 @@ -1,42 +1,87 @@ <script> -import { GlAlert, GlButton, GlModal } from '@gitlab/ui'; -import WorkItemActions from './work_item_actions.vue'; +import { GlAlert, GlModal } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import deleteWorkItemFromTaskMutation from '../graphql/delete_task_from_work_item.mutation.graphql'; import WorkItemDetail from './work_item_detail.vue'; export default { components: { GlAlert, - GlButton, GlModal, WorkItemDetail, - WorkItemActions, }, props: { - canUpdate: { - type: Boolean, + workItemId: { + type: String, required: false, - default: false, + default: null, }, - visible: { - type: Boolean, - required: true, + issueGid: { + type: String, + required: false, + default: '', }, - workItemId: { + lockVersion: { + type: Number, + required: false, + default: null, + }, + lineNumberStart: { + type: String, + required: false, + default: null, + }, + lineNumberEnd: { type: String, required: false, default: null, }, }, - emits: ['workItemDeleted', 'close'], + emits: ['workItemDeleted', 'workItemUpdated', 'close'], data() { return { error: undefined, }; }, methods: { - handleWorkItemDeleted() { - this.$emit('workItemDeleted'); - this.closeModal(); + deleteWorkItem() { + this.$apollo + .mutate({ + mutation: deleteWorkItemFromTaskMutation, + variables: { + input: { + id: this.issueGid, + lockVersion: this.lockVersion, + taskData: { + id: this.workItemId, + lineNumberStart: Number(this.lineNumberStart), + lineNumberEnd: Number(this.lineNumberEnd), + }, + }, + }, + }) + .then( + ({ + data: { + workItemDeleteTask: { + workItem: { descriptionHtml }, + errors, + }, + }, + }) => { + if (errors?.length) { + throw new Error(errors[0].message); + } + + this.$emit('workItemDeleted', descriptionHtml); + this.$refs.modal.hide(); + }, + ) + .catch((e) => { + this.error = + e.message || + s__('WorkItem|Something went wrong when deleting the work item. Please try again.'); + }); }, closeModal() { this.error = ''; @@ -45,37 +90,31 @@ export default { setErrorMessage(message) { this.error = message; }, + show() { + this.$refs.modal.show(); + }, }, }; </script> <template> - <gl-modal hide-footer modal-id="work-item-detail-modal" :visible="visible" @hide="closeModal"> - <template #modal-header> - <div class="gl-w-full gl-display-flex gl-align-items-center gl-justify-content-end"> - <h2 class="modal-title gl-mr-auto">{{ s__('WorkItem|Work Item') }}</h2> - <work-item-actions - :work-item-id="workItemId" - :can-update="canUpdate" - @workItemDeleted="handleWorkItemDeleted" - @error="setErrorMessage" - /> - <gl-button category="tertiary" icon="close" :aria-label="__('Close')" @click="closeModal" /> - </div> - </template> + <gl-modal ref="modal" hide-footer size="lg" modal-id="work-item-detail-modal" @hide="closeModal"> <gl-alert v-if="error" variant="danger" @dismiss="error = false"> {{ error }} </gl-alert> - <work-item-detail :work-item-id="workItemId" /> + <work-item-detail + :work-item-id="workItemId" + @deleteWorkItem="deleteWorkItem" + @workItemUpdated="$emit('workItemUpdated')" + /> </gl-modal> </template> <style> -/* hide the existing close button until we can do it - * with https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2710 +/* hide the existing modal header */ -#work-item-detail-modal .modal-header > .gl-button { +#work-item-detail-modal .modal-header { display: none; } </style> diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state.vue new file mode 100644 index 00000000000..51db4c804eb --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_state.vue @@ -0,0 +1,98 @@ +<script> +import * as Sentry from '@sentry/browser'; +import Tracking from '~/tracking'; +import { + i18n, + STATE_OPEN, + STATE_CLOSED, + STATE_EVENT_CLOSE, + STATE_EVENT_REOPEN, +} from '../constants'; +import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; +import ItemState from './item_state.vue'; + +export default { + components: { + ItemState, + }, + mixins: [Tracking.mixin()], + props: { + workItem: { + type: Object, + required: true, + }, + }, + data() { + return { + updateInProgress: false, + }; + }, + computed: { + workItemType() { + return this.workItem.workItemType?.name; + }, + tracking() { + return { + category: 'workItems:show', + label: 'item_state', + property: `type_${this.workItemType}`, + }; + }, + }, + methods: { + async updateWorkItemState(newState) { + const stateEventMap = { + [STATE_OPEN]: STATE_EVENT_REOPEN, + [STATE_CLOSED]: STATE_EVENT_CLOSE, + }; + + const stateEvent = stateEventMap[newState]; + + await this.updateWorkItem(stateEvent); + }, + async updateWorkItem(updatedState) { + if (!updatedState) { + return; + } + + this.updateInProgress = true; + + try { + this.track('updated_state'); + + const { + data: { workItemUpdate }, + } = await this.$apollo.mutate({ + mutation: updateWorkItemMutation, + variables: { + input: { + id: this.workItem.id, + stateEvent: updatedState, + }, + }, + }); + + if (workItemUpdate?.errors?.length) { + throw new Error(workItemUpdate.errors[0]); + } + + this.$emit('updated'); + } catch (error) { + this.$emit('error', i18n.updateError); + Sentry.captureException(error); + } + + this.updateInProgress = false; + }, + }, +}; +</script> + +<template> + <item-state + v-if="workItem.state" + :state="workItem.state" + :loading="updateInProgress" + @changed="updateWorkItemState" + /> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue index 88a825853cc..d2e6d3c0bbf 100644 --- a/app/assets/javascripts/work_items/components/work_item_title.vue +++ b/app/assets/javascripts/work_items/components/work_item_title.vue @@ -1,5 +1,4 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; import Tracking from '~/tracking'; import { i18n } from '../constants'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; @@ -7,16 +6,10 @@ import ItemTitle from './item_title.vue'; export default { components: { - GlLoadingIcon, ItemTitle, }, mixins: [Tracking.mixin()], props: { - loading: { - type: Boolean, - required: false, - default: false, - }, workItemId: { type: String, required: false, @@ -59,6 +52,7 @@ export default { }, }); this.track('updated_title'); + this.$emit('updated'); } catch { this.$emit('error', i18n.updateError); } @@ -68,6 +62,5 @@ export default { </script> <template> - <gl-loading-icon v-if="loading" class="gl-mt-3" size="md" /> - <item-title v-else :title="workItemTitle" @title-changed="updateTitle" /> + <item-title :title="workItemTitle" @title-changed="updateTitle" /> </template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index d3bcaf0f95f..e914500108f 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -1,6 +1,14 @@ import { s__ } from '~/locale'; +export const STATE_OPEN = 'OPEN'; +export const STATE_CLOSED = 'CLOSED'; + +export const STATE_EVENT_REOPEN = 'REOPEN'; +export const STATE_EVENT_CLOSE = 'CLOSE'; + export const i18n = { fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'), updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'), }; + +export const DEFAULT_MODAL_TYPE = 'Task'; diff --git a/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql new file mode 100644 index 00000000000..32c07ed48c7 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql @@ -0,0 +1,9 @@ +mutation workItemDeleteTask($input: WorkItemDeleteTaskInput!) { + workItemDeleteTask(input: $input) { + workItem { + id + descriptionHtml + } + errors + } +} 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 2707d6bb790..e25fd102699 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql @@ -1,8 +1,14 @@ fragment WorkItem on WorkItem { id title + state + description workItemType { id name } + userPermissions { + deleteWorkItem + updateWorkItem + } } 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 1d3dae0649d..3b46fed97ec 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql @@ -1,6 +1,6 @@ #import "./work_item.fragment.graphql" -query workItem($id: ID!) { +query workItem($id: WorkItemID!) { workItem(id: $id) { ...WorkItem } diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index 10fae9b9cc0..e39b0d6a353 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -5,7 +5,7 @@ import { createApolloProvider } from './graphql/provider'; export const initWorkItemsRoot = () => { const el = document.querySelector('#js-work-items'); - const { fullPath } = el.dataset; + const { fullPath, issuesListPath } = el.dataset; return new Vue({ el, @@ -13,6 +13,7 @@ export const initWorkItemsRoot = () => { apolloProvider: createApolloProvider(), provide: { fullPath, + issuesListPath, }, render(createElement) { return createElement(App); 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 a95da80ac95..04c6a61689c 100644 --- a/app/assets/javascripts/work_items/pages/create_work_item.vue +++ b/app/assets/javascripts/work_items/pages/create_work_item.vue @@ -6,6 +6,7 @@ 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'; @@ -77,6 +78,13 @@ 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; }, @@ -115,20 +123,15 @@ export default { }, }, update(store, { data: { workItemCreate } }) { - const { id, title, workItemType } = workItemCreate.workItem; + const { workItem } = workItemCreate; store.writeQuery({ query: workItemQuery, variables: { - id, + id: workItem.id, }, data: { - workItem: { - __typename: 'WorkItem', - id, - title, - workItemType, - }, + workItem, }, }); }, @@ -185,11 +188,11 @@ export default { <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"> - <item-title :title="title" data-testid="title-input" @title-input="handleTitleInput" /> + <item-title :title="initialTitle" data-testid="title-input" @title-input="handleTitleInput" /> <div> <gl-loading-icon v-if="$apollo.queries.workItemTypes.loading" - size="md" + size="lg" data-testid="loading-types" /> <gl-form-select diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue index b8f2bcff25d..6dc3dc3b3c9 100644 --- a/app/assets/javascripts/work_items/pages/work_item_root.vue +++ b/app/assets/javascripts/work_items/pages/work_item_root.vue @@ -1,26 +1,70 @@ <script> +import { GlAlert } from '@gitlab/ui'; import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { s__ } from '~/locale'; import WorkItemDetail from '../components/work_item_detail.vue'; +import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql'; export default { components: { + GlAlert, WorkItemDetail, }, + inject: ['issuesListPath'], props: { id: { type: String, required: true, }, }, + data() { + return { + error: '', + }; + }, computed: { gid() { return convertToGraphQLId(TYPE_WORK_ITEM, this.id); }, }, + methods: { + deleteWorkItem() { + this.$apollo + .mutate({ + mutation: deleteWorkItemMutation, + variables: { + input: { + id: this.gid, + }, + }, + }) + .then(({ data: { workItemDelete, errors } }) => { + if (errors?.length) { + throw new Error(errors[0].message); + } + + if (workItemDelete?.errors.length) { + throw new Error(workItemDelete.errors[0]); + } + + this.$toast.show(s__('WorkItem|Work item deleted')); + visitUrl(this.issuesListPath); + }) + .catch((e) => { + this.error = + e.message || + s__('WorkItem|Something went wrong when deleting the work item. Please try again.'); + }); + }, + }, }; </script> <template> - <work-item-detail :work-item-id="gid" /> + <div> + <gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert> + <work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem" /> + </div> </template> diff --git a/app/assets/javascripts/work_items/router/index.js b/app/assets/javascripts/work_items/router/index.js index 142fab8cfa6..2b39a298720 100644 --- a/app/assets/javascripts/work_items/router/index.js +++ b/app/assets/javascripts/work_items/router/index.js @@ -1,8 +1,10 @@ +import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueRouter from 'vue-router'; import { joinPaths } from '~/lib/utils/url_utility'; import { routes } from './routes'; +Vue.use(GlToast); Vue.use(VueRouter); export function createRouter(fullPath) { |