2017-09-10 17:25:29 +05:30
|
|
|
<script>
|
2023-04-23 21:23:45 +05:30
|
|
|
import { GlModalDirective, GlToast } from '@gitlab/ui';
|
2021-03-11 19:13:27 +05:30
|
|
|
import $ from 'jquery';
|
2023-04-23 21:23:45 +05:30
|
|
|
import { uniqueId } from 'lodash';
|
2022-07-16 23:28:13 +05:30
|
|
|
import Sortable from 'sortablejs';
|
2022-06-21 17:19:12 +05:30
|
|
|
import Vue from 'vue';
|
2023-04-23 21:23:45 +05:30
|
|
|
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
|
2023-03-04 22:38:38 +05:30
|
|
|
import SafeHtml from '~/vue_shared/directives/safe_html';
|
2022-07-16 23:28:13 +05:30
|
|
|
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
|
2023-04-23 21:23:45 +05:30
|
|
|
import { TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
|
2023-03-04 22:38:38 +05:30
|
|
|
import { createAlert } from '~/flash';
|
2023-04-23 21:23:45 +05:30
|
|
|
import { TYPE_ISSUE } from '~/issues/constants';
|
2022-10-11 01:57:18 +05:30
|
|
|
import { isMetaKey } from '~/lib/utils/common_utils';
|
2022-06-21 17:19:12 +05:30
|
|
|
import { isPositiveInteger } from '~/lib/utils/number_utils';
|
|
|
|
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
|
|
|
|
import { __, s__, sprintf } from '~/locale';
|
2022-07-16 23:28:13 +05:30
|
|
|
import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';
|
2022-01-26 12:08:38 +05:30
|
|
|
import TaskList from '~/task_list';
|
2022-05-07 20:08:51 +05:30
|
|
|
import Tracking from '~/tracking';
|
2023-04-23 21:23:45 +05:30
|
|
|
import addHierarchyChildMutation from '~/work_items/graphql/add_hierarchy_child.mutation.graphql';
|
|
|
|
import removeHierarchyChildMutation from '~/work_items/graphql/remove_hierarchy_child.mutation.graphql';
|
|
|
|
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
|
|
|
|
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
|
2022-07-16 23:28:13 +05:30
|
|
|
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
2022-08-13 15:12:31 +05:30
|
|
|
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
|
2022-04-04 11:22:00 +05:30
|
|
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
2022-05-07 20:08:51 +05:30
|
|
|
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
|
2022-08-13 15:12:31 +05:30
|
|
|
import {
|
2022-10-11 01:57:18 +05:30
|
|
|
sprintfWorkItem,
|
|
|
|
I18N_WORK_ITEM_ERROR_CREATING,
|
2023-04-23 21:23:45 +05:30
|
|
|
I18N_WORK_ITEM_ERROR_DELETING,
|
2022-08-13 15:12:31 +05:30
|
|
|
TRACKING_CATEGORY_SHOW,
|
|
|
|
TASK_TYPE_NAME,
|
|
|
|
} from '~/work_items/constants';
|
2023-03-04 22:38:38 +05:30
|
|
|
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
2023-04-23 21:23:45 +05:30
|
|
|
import eventHub from '../event_hub';
|
2021-03-11 19:13:27 +05:30
|
|
|
import animateMixin from '../mixins/animate';
|
2023-04-23 21:23:45 +05:30
|
|
|
import {
|
|
|
|
deleteTaskListItem,
|
|
|
|
convertDescriptionWithNewSort,
|
|
|
|
extractTaskTitleAndDescription,
|
|
|
|
} from '../utils';
|
|
|
|
import TaskListItemActions from './task_list_item_actions.vue';
|
2017-09-10 17:25:29 +05:30
|
|
|
|
2022-06-21 17:19:12 +05:30
|
|
|
Vue.use(GlToast);
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
const workItemTypes = {
|
|
|
|
TASK: 'task',
|
|
|
|
};
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
export default {
|
2020-11-24 15:15:51 +05:30
|
|
|
directives: {
|
|
|
|
SafeHtml,
|
2022-04-04 11:22:00 +05:30
|
|
|
GlModal: GlModalDirective,
|
2020-11-24 15:15:51 +05:30
|
|
|
},
|
2022-04-04 11:22:00 +05:30
|
|
|
components: {
|
2022-05-07 20:08:51 +05:30
|
|
|
WorkItemDetailModal,
|
2022-04-04 11:22:00 +05:30
|
|
|
},
|
2022-05-07 20:08:51 +05:30
|
|
|
mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
|
2023-04-23 21:23:45 +05:30
|
|
|
inject: ['fullPath', 'hasIterationsFeature'],
|
2018-12-13 13:39:08 +05:30
|
|
|
props: {
|
|
|
|
canUpdate: {
|
|
|
|
type: Boolean,
|
|
|
|
required: true,
|
2017-09-10 17:25:29 +05:30
|
|
|
},
|
2018-12-13 13:39:08 +05:30
|
|
|
descriptionHtml: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
2017-09-10 17:25:29 +05:30
|
|
|
},
|
2018-12-13 13:39:08 +05:30
|
|
|
descriptionText: {
|
|
|
|
type: String,
|
2020-11-24 15:15:51 +05:30
|
|
|
required: false,
|
|
|
|
default: '',
|
2018-12-13 13:39:08 +05:30
|
|
|
},
|
|
|
|
taskStatus: {
|
|
|
|
type: String,
|
|
|
|
required: false,
|
|
|
|
default: '',
|
|
|
|
},
|
|
|
|
issuableType: {
|
|
|
|
type: String,
|
|
|
|
required: false,
|
2023-04-23 21:23:45 +05:30
|
|
|
default: TYPE_ISSUE,
|
2018-12-13 13:39:08 +05:30
|
|
|
},
|
|
|
|
updateUrl: {
|
|
|
|
type: String,
|
|
|
|
required: false,
|
|
|
|
default: null,
|
|
|
|
},
|
2019-03-02 22:35:43 +05:30
|
|
|
lockVersion: {
|
|
|
|
type: Number,
|
|
|
|
required: false,
|
|
|
|
default: 0,
|
|
|
|
},
|
2022-06-21 17:19:12 +05:30
|
|
|
issueId: {
|
|
|
|
type: Number,
|
|
|
|
required: false,
|
|
|
|
default: null,
|
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
issueIid: {
|
|
|
|
type: Number,
|
|
|
|
required: false,
|
|
|
|
default: null,
|
|
|
|
},
|
2022-07-16 23:28:13 +05:30
|
|
|
isUpdating: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: false,
|
|
|
|
},
|
2018-12-13 13:39:08 +05:30
|
|
|
},
|
|
|
|
data() {
|
2022-06-21 17:19:12 +05:30
|
|
|
const workItemId = getParameterByName('work_item_id');
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
return {
|
2023-04-23 21:23:45 +05:30
|
|
|
hasTaskListItemActions: false,
|
2018-12-13 13:39:08 +05:30
|
|
|
preAnimation: false,
|
|
|
|
pulseAnimation: false,
|
2020-11-24 15:15:51 +05:30
|
|
|
initialUpdate: true,
|
2023-04-23 21:23:45 +05:30
|
|
|
issueDetails: {},
|
2022-04-04 11:22:00 +05:30
|
|
|
activeTask: {},
|
2022-06-21 17:19:12 +05:30
|
|
|
workItemId: isPositiveInteger(workItemId)
|
2023-04-23 21:23:45 +05:30
|
|
|
? convertToGraphQLId(TYPENAME_WORK_ITEM, workItemId)
|
2022-06-21 17:19:12 +05:30
|
|
|
: undefined,
|
2022-08-13 15:12:31 +05:30
|
|
|
workItemTypes: [],
|
2018-12-13 13:39:08 +05:30
|
|
|
};
|
|
|
|
},
|
2022-07-16 23:28:13 +05:30
|
|
|
apollo: {
|
2023-04-23 21:23:45 +05:30
|
|
|
issueDetails: {
|
|
|
|
query: getIssueDetailsQuery,
|
|
|
|
variables() {
|
|
|
|
return {
|
|
|
|
fullPath: this.fullPath,
|
|
|
|
iid: String(this.issueIid),
|
|
|
|
};
|
|
|
|
},
|
|
|
|
update: (data) => data.workspace?.issuable,
|
|
|
|
},
|
2022-07-16 23:28:13 +05:30
|
|
|
workItem: {
|
|
|
|
query: workItemQuery,
|
|
|
|
variables() {
|
|
|
|
return {
|
|
|
|
id: this.workItemId,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
skip() {
|
2023-04-23 21:23:45 +05:30
|
|
|
return !this.workItemId || !this.workItemsMvcEnabled;
|
2022-07-16 23:28:13 +05:30
|
|
|
},
|
2022-05-07 20:08:51 +05:30
|
|
|
},
|
2022-08-13 15:12:31 +05:30
|
|
|
workItemTypes: {
|
|
|
|
query: projectWorkItemTypesQuery,
|
|
|
|
variables() {
|
|
|
|
return {
|
|
|
|
fullPath: this.fullPath,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
update(data) {
|
|
|
|
return data.workspace?.workItemTypes?.nodes;
|
|
|
|
},
|
|
|
|
skip() {
|
2023-04-23 21:23:45 +05:30
|
|
|
return !this.workItemsMvcEnabled;
|
2022-08-13 15:12:31 +05:30
|
|
|
},
|
|
|
|
},
|
2022-07-16 23:28:13 +05:30
|
|
|
},
|
|
|
|
computed: {
|
2023-04-23 21:23:45 +05:30
|
|
|
workItemsMvcEnabled() {
|
|
|
|
return this.glFeatures.workItemsMvc;
|
2022-04-04 11:22:00 +05:30
|
|
|
},
|
2022-08-13 15:12:31 +05:30
|
|
|
taskWorkItemType() {
|
|
|
|
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
|
|
|
|
},
|
2022-06-21 17:19:12 +05:30
|
|
|
issueGid() {
|
2023-04-23 21:23:45 +05:30
|
|
|
return this.issueId ? convertToGraphQLId(TYPENAME_WORK_ITEM, this.issueId) : null;
|
2022-06-21 17:19:12 +05:30
|
|
|
},
|
2022-04-04 11:22:00 +05:30
|
|
|
},
|
2018-12-13 13:39:08 +05:30
|
|
|
watch: {
|
2020-11-24 15:15:51 +05:30
|
|
|
descriptionHtml(newDescription, oldDescription) {
|
|
|
|
if (!this.initialUpdate && newDescription !== oldDescription) {
|
|
|
|
this.animateChange();
|
|
|
|
} else {
|
|
|
|
this.initialUpdate = false;
|
|
|
|
}
|
2017-09-10 17:25:29 +05:30
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
this.$nextTick(() => {
|
|
|
|
this.renderGFM();
|
|
|
|
});
|
2017-09-10 17:25:29 +05:30
|
|
|
},
|
2018-12-13 13:39:08 +05:30
|
|
|
taskStatus() {
|
2018-03-17 18:26:18 +05:30
|
|
|
this.updateTaskStatusText();
|
|
|
|
},
|
2018-12-13 13:39:08 +05:30
|
|
|
},
|
|
|
|
mounted() {
|
2023-04-23 21:23:45 +05:30
|
|
|
eventHub.$on('convert-task-list-item', this.convertTaskListItem);
|
|
|
|
eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
this.renderGFM();
|
|
|
|
this.updateTaskStatusText();
|
2022-04-04 11:22:00 +05:30
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
if (this.workItemId && this.workItemsMvcEnabled) {
|
2022-07-16 23:28:13 +05:30
|
|
|
const taskLink = this.$el.querySelector(
|
|
|
|
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
|
|
|
|
);
|
|
|
|
this.openWorkItemDetailModal(taskLink);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
beforeDestroy() {
|
2023-04-23 21:23:45 +05:30
|
|
|
eventHub.$off('convert-task-list-item', this.convertTaskListItem);
|
|
|
|
eventHub.$off('delete-task-list-item', this.deleteTaskListItem);
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
this.removeAllPointerEventListeners();
|
2018-12-13 13:39:08 +05:30
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
renderGFM() {
|
2023-03-04 22:38:38 +05:30
|
|
|
renderGFM(this.$refs['gfm-content']);
|
2017-09-10 17:25:29 +05:30
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
if (this.canUpdate) {
|
|
|
|
// eslint-disable-next-line no-new
|
|
|
|
new TaskList({
|
|
|
|
dataType: this.issuableType,
|
|
|
|
fieldName: 'description',
|
2019-03-02 22:35:43 +05:30
|
|
|
lockVersion: this.lockVersion,
|
2018-12-13 13:39:08 +05:30
|
|
|
selector: '.detail-page-description',
|
2021-12-11 22:18:48 +05:30
|
|
|
onUpdate: this.taskListUpdateStarted.bind(this),
|
|
|
|
onSuccess: this.taskListUpdateSuccess.bind(this),
|
2019-03-02 22:35:43 +05:30
|
|
|
onError: this.taskListUpdateError.bind(this),
|
2018-12-13 13:39:08 +05:30
|
|
|
});
|
2022-07-16 23:28:13 +05:30
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
this.removeAllPointerEventListeners();
|
|
|
|
|
|
|
|
this.renderSortableLists();
|
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
if (this.workItemsMvcEnabled) {
|
|
|
|
this.renderTaskListItemActions();
|
2022-07-16 23:28:13 +05:30
|
|
|
}
|
2018-12-13 13:39:08 +05:30
|
|
|
}
|
|
|
|
},
|
2022-07-16 23:28:13 +05:30
|
|
|
renderSortableLists() {
|
2022-07-23 23:45:48 +05:30
|
|
|
// We exclude GLFM table of contents which have a `section-nav` class on the root `ul`.
|
|
|
|
const lists = document.querySelectorAll(
|
|
|
|
'.description .md > ul:not(.section-nav), .description .md > ul:not(.section-nav) ul, .description ol',
|
|
|
|
);
|
2022-07-16 23:28:13 +05:30
|
|
|
lists.forEach((list) => {
|
|
|
|
if (list.children.length <= 1) {
|
|
|
|
return;
|
|
|
|
}
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
Array.from(list.children).forEach((listItem) => {
|
|
|
|
listItem.prepend(this.createDragIconElement());
|
2022-07-23 23:45:48 +05:30
|
|
|
this.addPointerEventListeners(listItem, '.drag-icon');
|
2022-07-16 23:28:13 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
Sortable.create(
|
|
|
|
list,
|
|
|
|
getSortableDefaultOptions({
|
|
|
|
handle: '.drag-icon',
|
|
|
|
onUpdate: (event) => {
|
|
|
|
const description = convertDescriptionWithNewSort(this.descriptionText, event.to);
|
2023-04-23 21:23:45 +05:30
|
|
|
this.$emit('saveDescription', description);
|
2022-07-16 23:28:13 +05:30
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
createDragIconElement() {
|
|
|
|
const container = document.createElement('div');
|
2022-10-11 01:57:18 +05:30
|
|
|
// eslint-disable-next-line no-unsanitized/property
|
2023-04-23 21:23:45 +05:30
|
|
|
container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-opacity-0" role="img" aria-hidden="true">
|
|
|
|
<use href="${gon.sprite_icons}#grip"></use>
|
2022-07-16 23:28:13 +05:30
|
|
|
</svg>`;
|
|
|
|
return container.firstChild;
|
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
addPointerEventListeners(listItem, elementSelector) {
|
2022-07-16 23:28:13 +05:30
|
|
|
const pointeroverListener = (event) => {
|
2023-04-23 21:23:45 +05:30
|
|
|
const element = event.target.closest('li').querySelector(elementSelector);
|
|
|
|
if (!element || isDragging() || this.isUpdating) {
|
2022-07-16 23:28:13 +05:30
|
|
|
return;
|
|
|
|
}
|
2023-04-23 21:23:45 +05:30
|
|
|
element.classList.add('gl-opacity-10');
|
2022-07-16 23:28:13 +05:30
|
|
|
};
|
|
|
|
const pointeroutListener = (event) => {
|
2023-04-23 21:23:45 +05:30
|
|
|
const element = event.target.closest('li').querySelector(elementSelector);
|
|
|
|
if (!element) {
|
2022-07-16 23:28:13 +05:30
|
|
|
return;
|
|
|
|
}
|
2023-04-23 21:23:45 +05:30
|
|
|
element.classList.remove('gl-opacity-10');
|
2022-07-16 23:28:13 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
// We use pointerover/pointerout instead of CSS so that when we hover over a
|
2023-04-23 21:23:45 +05:30
|
|
|
// list item with children, the grip icons of its children do not become visible.
|
2022-07-16 23:28:13 +05:30
|
|
|
listItem.addEventListener('pointerover', pointeroverListener);
|
|
|
|
listItem.addEventListener('pointerout', pointeroutListener);
|
|
|
|
|
|
|
|
this.pointerEventListeners = this.pointerEventListeners || new Map();
|
2022-07-23 23:45:48 +05:30
|
|
|
const events = [
|
2022-07-16 23:28:13 +05:30
|
|
|
{ type: 'pointerover', listener: pointeroverListener },
|
|
|
|
{ type: 'pointerout', listener: pointeroutListener },
|
2022-07-23 23:45:48 +05:30
|
|
|
];
|
|
|
|
if (this.pointerEventListeners.has(listItem)) {
|
|
|
|
const concatenatedEvents = this.pointerEventListeners.get(listItem).concat(events);
|
|
|
|
this.pointerEventListeners.set(listItem, concatenatedEvents);
|
|
|
|
} else {
|
|
|
|
this.pointerEventListeners.set(listItem, events);
|
|
|
|
}
|
2022-07-16 23:28:13 +05:30
|
|
|
},
|
|
|
|
removeAllPointerEventListeners() {
|
|
|
|
this.pointerEventListeners?.forEach((events, listItem) => {
|
|
|
|
events.forEach((event) => listItem.removeEventListener(event.type, event.listener));
|
|
|
|
this.pointerEventListeners.delete(listItem);
|
|
|
|
});
|
|
|
|
},
|
2021-12-11 22:18:48 +05:30
|
|
|
taskListUpdateStarted() {
|
|
|
|
this.$emit('taskListUpdateStarted');
|
|
|
|
},
|
|
|
|
taskListUpdateSuccess() {
|
|
|
|
this.$emit('taskListUpdateSucceeded');
|
|
|
|
},
|
2019-03-02 22:35:43 +05:30
|
|
|
taskListUpdateError() {
|
2023-03-04 22:38:38 +05:30
|
|
|
createAlert({
|
2021-09-04 01:27:46 +05:30
|
|
|
message: sprintf(
|
2021-12-11 22:18:48 +05:30
|
|
|
__(
|
2019-03-02 22:35:43 +05:30
|
|
|
'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
|
|
|
|
),
|
|
|
|
{
|
|
|
|
issueType: this.issuableType,
|
|
|
|
},
|
|
|
|
),
|
2021-09-04 01:27:46 +05:30
|
|
|
});
|
2019-03-02 22:35:43 +05:30
|
|
|
|
|
|
|
this.$emit('taskListUpdateFailed');
|
|
|
|
},
|
2018-12-13 13:39:08 +05:30
|
|
|
updateTaskStatusText() {
|
|
|
|
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
|
|
|
|
const $issuableHeader = $('.issuable-meta');
|
|
|
|
const $tasks = $('#task_status', $issuableHeader);
|
|
|
|
const $tasksShort = $('#task_status_short', $issuableHeader);
|
2017-09-10 17:25:29 +05:30
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
if (taskRegexMatches) {
|
|
|
|
$tasks.text(this.taskStatus);
|
|
|
|
$tasksShort.text(
|
2022-08-27 11:52:29 +05:30
|
|
|
`${taskRegexMatches[1]}/${taskRegexMatches[2]} checklist item${
|
|
|
|
taskRegexMatches[2] > 1 ? 's' : ''
|
|
|
|
}`,
|
2018-12-13 13:39:08 +05:30
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$tasks.text('');
|
|
|
|
$tasksShort.text('');
|
|
|
|
}
|
2017-09-10 17:25:29 +05:30
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
createTaskListItemActions(provide) {
|
|
|
|
const app = new Vue({
|
|
|
|
el: document.createElement('div'),
|
|
|
|
provide,
|
|
|
|
render: (createElement) => createElement(TaskListItemActions),
|
|
|
|
});
|
|
|
|
return app.$el;
|
|
|
|
},
|
|
|
|
convertTaskListItem(sourcepos) {
|
|
|
|
const oldDescription = this.descriptionText;
|
|
|
|
const { newDescription, taskDescription, taskTitle } = deleteTaskListItem(
|
|
|
|
oldDescription,
|
|
|
|
sourcepos,
|
|
|
|
);
|
|
|
|
this.$emit('saveDescription', newDescription);
|
|
|
|
this.createTask({ taskTitle, taskDescription, oldDescription });
|
|
|
|
},
|
|
|
|
deleteTaskListItem(sourcepos) {
|
|
|
|
const { newDescription } = deleteTaskListItem(this.descriptionText, sourcepos);
|
|
|
|
this.$emit('saveDescription', newDescription);
|
|
|
|
},
|
|
|
|
renderTaskListItemActions() {
|
2022-04-04 11:22:00 +05:30
|
|
|
if (!this.$el?.querySelectorAll) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
const taskListFields = this.$el.querySelectorAll('.task-list-item:not(.inapplicable)');
|
2022-04-04 11:22:00 +05:30
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
taskListFields.forEach((item) => {
|
2022-06-21 17:19:12 +05:30
|
|
|
const taskLink = item.querySelector('.gfm-issue');
|
|
|
|
if (taskLink) {
|
2022-07-16 23:28:13 +05:30
|
|
|
const { issue, referenceType, issueType } = taskLink.dataset;
|
|
|
|
if (issueType !== workItemTypes.TASK) {
|
|
|
|
return;
|
|
|
|
}
|
2023-04-23 21:23:45 +05:30
|
|
|
const workItemId = convertToGraphQLId(TYPENAME_WORK_ITEM, issue);
|
2022-07-16 23:28:13 +05:30
|
|
|
this.addHoverListeners(taskLink, workItemId);
|
2022-08-27 11:52:29 +05:30
|
|
|
taskLink.classList.add('gl-link');
|
2022-06-21 17:19:12 +05:30
|
|
|
taskLink.addEventListener('click', (e) => {
|
2022-10-11 01:57:18 +05:30
|
|
|
if (isMetaKey(e)) {
|
|
|
|
return;
|
|
|
|
}
|
2022-06-21 17:19:12 +05:30
|
|
|
e.preventDefault();
|
2022-07-16 23:28:13 +05:30
|
|
|
this.openWorkItemDetailModal(taskLink);
|
|
|
|
this.workItemId = workItemId;
|
2022-06-21 17:19:12 +05:30
|
|
|
this.updateWorkItemIdUrlQuery(issue);
|
|
|
|
this.track('viewed_work_item_from_modal', {
|
2022-07-23 23:45:48 +05:30
|
|
|
category: TRACKING_CATEGORY_SHOW,
|
2022-06-21 17:19:12 +05:30
|
|
|
label: 'work_item_view',
|
|
|
|
property: `type_${referenceType}`,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2023-04-23 21:23:45 +05:30
|
|
|
|
|
|
|
const toggleClass = uniqueId('task-list-item-actions-');
|
|
|
|
const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate, toggleClass });
|
|
|
|
this.addPointerEventListeners(item, `.${toggleClass}`);
|
|
|
|
this.insertNextToTaskListItemText(dropdown, item);
|
|
|
|
this.hasTaskListItemActions = true;
|
2022-07-16 23:28:13 +05:30
|
|
|
});
|
|
|
|
},
|
|
|
|
addHoverListeners(taskLink, id) {
|
|
|
|
let workItemPrefetch;
|
|
|
|
taskLink.addEventListener('mouseover', () => {
|
|
|
|
workItemPrefetch = setTimeout(() => {
|
|
|
|
this.workItemId = id;
|
|
|
|
}, 150);
|
|
|
|
});
|
|
|
|
taskLink.addEventListener('mouseout', () => {
|
|
|
|
if (workItemPrefetch) {
|
|
|
|
clearTimeout(workItemPrefetch);
|
|
|
|
}
|
2022-04-04 11:22:00 +05:30
|
|
|
});
|
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
insertNextToTaskListItemText(element, listItem) {
|
|
|
|
const children = Array.from(listItem.children);
|
|
|
|
const paragraph = children.find((el) => el.tagName === 'P');
|
|
|
|
const list = children.find((el) => el.classList.contains('task-list'));
|
2022-07-23 23:45:48 +05:30
|
|
|
if (paragraph) {
|
|
|
|
// If there's a `p` element, then it's a multi-paragraph task item
|
|
|
|
// and the task text exists within the `p` element as the last child
|
2023-04-23 21:23:45 +05:30
|
|
|
paragraph.append(element);
|
|
|
|
} else if (list) {
|
2022-07-23 23:45:48 +05:30
|
|
|
// Otherwise, the task item can have a child list which exists directly after the task text
|
2023-04-23 21:23:45 +05:30
|
|
|
list.insertAdjacentElement('beforebegin', element);
|
2022-07-23 23:45:48 +05:30
|
|
|
} else {
|
|
|
|
// Otherwise, the task item is a simple one where the task text exists as the last child
|
2023-04-23 21:23:45 +05:30
|
|
|
listItem.append(element);
|
2022-07-23 23:45:48 +05:30
|
|
|
}
|
|
|
|
},
|
2022-07-16 23:28:13 +05:30
|
|
|
setActiveTask(el) {
|
|
|
|
const { parentElement } = el;
|
2022-07-23 23:45:48 +05:30
|
|
|
const lineNumbers = parentElement.dataset.sourcepos.match(/\b\d+(?=:)/g);
|
2022-06-21 17:19:12 +05:30
|
|
|
this.activeTask = {
|
|
|
|
title: parentElement.innerText,
|
|
|
|
lineNumberStart: lineNumbers[0],
|
|
|
|
lineNumberEnd: lineNumbers[1],
|
|
|
|
};
|
2022-07-16 23:28:13 +05:30
|
|
|
},
|
|
|
|
openWorkItemDetailModal(el) {
|
|
|
|
if (!el) {
|
|
|
|
return;
|
|
|
|
}
|
2022-08-13 15:12:31 +05:30
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
this.setActiveTask(el);
|
|
|
|
this.$refs.detailsModal.show();
|
|
|
|
},
|
2022-05-07 20:08:51 +05:30
|
|
|
closeWorkItemDetailModal() {
|
2022-06-21 17:19:12 +05:30
|
|
|
this.workItemId = undefined;
|
|
|
|
this.updateWorkItemIdUrlQuery(undefined);
|
2022-05-07 20:08:51 +05:30
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
async createTask({ taskTitle, taskDescription, oldDescription }) {
|
2022-08-13 15:12:31 +05:30
|
|
|
try {
|
2023-04-23 21:23:45 +05:30
|
|
|
const { title, description } = extractTaskTitleAndDescription(taskTitle, taskDescription);
|
|
|
|
const iterationInput = {
|
|
|
|
iterationWidget: {
|
|
|
|
iterationId: this.issueDetails.iteration?.id ?? null,
|
2022-08-13 15:12:31 +05:30
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
};
|
|
|
|
const input = {
|
|
|
|
confidential: this.issueDetails.confidential,
|
|
|
|
description,
|
|
|
|
hierarchyWidget: {
|
|
|
|
parentId: this.issueGid,
|
|
|
|
},
|
|
|
|
...(this.hasIterationsFeature && iterationInput),
|
|
|
|
milestoneWidget: {
|
|
|
|
milestoneId: this.issueDetails.milestone?.id ?? null,
|
2022-08-13 15:12:31 +05:30
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
projectPath: this.fullPath,
|
|
|
|
title,
|
|
|
|
workItemTypeId: this.taskWorkItemType,
|
|
|
|
};
|
|
|
|
|
|
|
|
const { data } = await this.$apollo.mutate({
|
|
|
|
mutation: createWorkItemMutation,
|
|
|
|
variables: { input },
|
2022-08-13 15:12:31 +05:30
|
|
|
});
|
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
const { workItem, errors } = data.workItemCreate;
|
|
|
|
|
|
|
|
if (errors?.length) {
|
|
|
|
throw new Error(errors);
|
|
|
|
}
|
2022-08-13 15:12:31 +05:30
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
await this.$apollo.mutate({
|
|
|
|
mutation: addHierarchyChildMutation,
|
|
|
|
variables: { id: this.issueGid, workItem },
|
|
|
|
});
|
2022-08-13 15:12:31 +05:30
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
this.$toast.show(s__('WorkItem|Converted to task'), {
|
|
|
|
action: {
|
|
|
|
text: s__('WorkItem|Undo'),
|
|
|
|
onClick: (_, toast) => {
|
|
|
|
this.undoCreateTask(oldDescription, workItem.id);
|
|
|
|
toast.hide();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
2022-08-13 15:12:31 +05:30
|
|
|
} catch (error) {
|
2023-04-23 21:23:45 +05:30
|
|
|
this.showAlert(I18N_WORK_ITEM_ERROR_CREATING, error);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
async undoCreateTask(oldDescription, id) {
|
|
|
|
this.$emit('saveDescription', oldDescription);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const { data } = await this.$apollo.mutate({
|
|
|
|
mutation: deleteWorkItemMutation,
|
|
|
|
variables: { input: { id } },
|
|
|
|
});
|
|
|
|
|
|
|
|
const { errors } = data.workItemDelete;
|
|
|
|
|
|
|
|
if (errors?.length) {
|
|
|
|
throw new Error(errors);
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.$apollo.mutate({
|
|
|
|
mutation: removeHierarchyChildMutation,
|
|
|
|
variables: { id: this.issueGid, workItem: { id } },
|
2022-08-13 15:12:31 +05:30
|
|
|
});
|
2023-04-23 21:23:45 +05:30
|
|
|
|
|
|
|
this.$toast.show(s__('WorkItem|Task reverted'));
|
|
|
|
} catch (error) {
|
|
|
|
this.showAlert(I18N_WORK_ITEM_ERROR_DELETING, error);
|
2022-08-13 15:12:31 +05:30
|
|
|
}
|
2022-04-04 11:22:00 +05:30
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
showAlert(message, error) {
|
|
|
|
createAlert({
|
|
|
|
message: sprintfWorkItem(message, workItemTypes.TASK),
|
|
|
|
error,
|
|
|
|
captureError: true,
|
|
|
|
});
|
|
|
|
},
|
2022-07-16 23:28:13 +05:30
|
|
|
handleDeleteTask(description) {
|
|
|
|
this.$emit('updateDescription', description);
|
2022-08-13 15:12:31 +05:30
|
|
|
this.$toast.show(s__('WorkItem|Task deleted'));
|
2022-05-07 20:08:51 +05:30
|
|
|
},
|
2022-06-21 17:19:12 +05:30
|
|
|
updateWorkItemIdUrlQuery(workItemId) {
|
|
|
|
updateHistory({
|
|
|
|
url: setUrlParams({ work_item_id: workItemId }),
|
|
|
|
replace: true,
|
|
|
|
});
|
2022-04-04 11:22:00 +05:30
|
|
|
},
|
2018-12-13 13:39:08 +05:30
|
|
|
},
|
2022-01-26 12:08:38 +05:30
|
|
|
safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'] },
|
2018-12-13 13:39:08 +05:30
|
|
|
};
|
2017-09-10 17:25:29 +05:30
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
2023-04-23 21:23:45 +05:30
|
|
|
<div v-if="descriptionHtml" :class="{ 'js-task-list-container': canUpdate }" class="description">
|
2017-09-10 17:25:29 +05:30
|
|
|
<div
|
2018-11-08 19:23:39 +05:30
|
|
|
ref="gfm-content"
|
2021-11-11 11:23:49 +05:30
|
|
|
v-safe-html:[$options.safeHtmlConfig]="descriptionHtml"
|
2022-04-04 11:22:00 +05:30
|
|
|
data-testid="gfm-content"
|
2017-09-10 17:25:29 +05:30
|
|
|
:class="{
|
|
|
|
'issue-realtime-pre-pulse': preAnimation,
|
2019-02-15 15:39:39 +05:30
|
|
|
'issue-realtime-trigger-pulse': pulseAnimation,
|
2023-04-23 21:23:45 +05:30
|
|
|
'has-task-list-item-actions': hasTaskListItemActions,
|
2017-09-10 17:25:29 +05:30
|
|
|
}"
|
2019-07-07 11:18:12 +05:30
|
|
|
class="md"
|
2019-02-15 15:39:39 +05:30
|
|
|
></div>
|
2017-09-10 17:25:29 +05:30
|
|
|
<textarea
|
|
|
|
v-if="descriptionText"
|
2022-06-21 17:19:12 +05:30
|
|
|
:value="descriptionText"
|
2018-03-17 18:26:18 +05:30
|
|
|
:data-update-url="updateUrl"
|
2018-11-08 19:23:39 +05:30
|
|
|
class="hidden js-task-list-field"
|
2019-07-31 22:56:46 +05:30
|
|
|
dir="auto"
|
2022-04-04 11:22:00 +05:30
|
|
|
data-testid="textarea"
|
2018-03-17 18:26:18 +05:30
|
|
|
>
|
2017-09-10 17:25:29 +05:30
|
|
|
</textarea>
|
2022-05-07 20:08:51 +05:30
|
|
|
<work-item-detail-modal
|
2022-07-16 23:28:13 +05:30
|
|
|
ref="detailsModal"
|
2022-06-21 17:19:12 +05:30
|
|
|
:can-update="canUpdate"
|
2022-05-07 20:08:51 +05:30
|
|
|
:work-item-id="workItemId"
|
2022-07-16 23:28:13 +05:30
|
|
|
:issue-gid="issueGid"
|
|
|
|
:lock-version="lockVersion"
|
|
|
|
:line-number-start="activeTask.lineNumberStart"
|
|
|
|
:line-number-end="activeTask.lineNumberEnd"
|
2022-06-21 17:19:12 +05:30
|
|
|
@workItemDeleted="handleDeleteTask"
|
2022-05-07 20:08:51 +05:30
|
|
|
@close="closeWorkItemDetailModal"
|
|
|
|
/>
|
2017-09-10 17:25:29 +05:30
|
|
|
</div>
|
|
|
|
</template>
|