debian-mirror-gitlab/app/assets/javascripts/issues/show/components/description.vue

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

470 lines
13 KiB
Vue
Raw Normal View History

2017-09-10 17:25:29 +05:30
<script>
2022-04-04 11:22:00 +05:30
import {
GlSafeHtmlDirective as SafeHtml,
GlModal,
2022-06-21 17:19:12 +05:30
GlToast,
GlTooltip,
2022-04-04 11:22:00 +05:30
GlModalDirective,
} from '@gitlab/ui';
2021-03-11 19:13:27 +05:30
import $ from 'jquery';
2022-07-16 23:28:13 +05:30
import Sortable from 'sortablejs';
2022-06-21 17:19:12 +05:30
import Vue from 'vue';
2022-07-16 23:28:13 +05:30
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
2022-06-21 17:19:12 +05:30
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
2021-09-04 01:27:46 +05:30
import createFlash from '~/flash';
2022-07-16 23:28:13 +05:30
import { IssuableType } from '~/issues/constants';
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';
2022-07-16 23:28:13 +05:30
import workItemQuery from '~/work_items/graphql/work_item.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-04-04 11:22:00 +05:30
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
2021-03-11 19:13:27 +05:30
import animateMixin from '../mixins/animate';
2022-07-16 23:28:13 +05:30
import { convertDescriptionWithNewSort } from '../utils';
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: {
GlModal,
CreateWorkItem,
2022-06-21 17:19:12 +05:30
GlTooltip,
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()],
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,
2022-07-16 23:28:13 +05:30
default: IssuableType.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,
},
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 {
preAnimation: false,
pulseAnimation: false,
2020-11-24 15:15:51 +05:30
initialUpdate: true,
2022-04-04 11:22:00 +05:30
taskButtons: [],
activeTask: {},
2022-06-21 17:19:12 +05:30
workItemId: isPositiveInteger(workItemId)
? convertToGraphQLId(TYPE_WORK_ITEM, workItemId)
: undefined,
2018-12-13 13:39:08 +05:30
};
},
2022-07-16 23:28:13 +05:30
apollo: {
workItem: {
query: workItemQuery,
variables() {
return {
id: this.workItemId,
};
},
skip() {
return !this.workItemId || !this.workItemsEnabled;
},
2022-05-07 20:08:51 +05:30
},
2022-07-16 23:28:13 +05:30
},
computed: {
2022-04-04 11:22:00 +05:30
workItemsEnabled() {
return this.glFeatures.workItems;
},
2022-06-21 17:19:12 +05:30
issueGid() {
return this.issueId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issueId) : null;
},
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();
2022-06-21 17:19:12 +05:30
if (this.workItemsEnabled) {
this.renderTaskActions();
}
2018-12-13 13:39:08 +05:30
});
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() {
this.renderGFM();
this.updateTaskStatusText();
2022-04-04 11:22:00 +05:30
if (this.workItemsEnabled) {
this.renderTaskActions();
}
2022-07-16 23:28:13 +05:30
if (this.workItemId) {
const taskLink = this.$el.querySelector(
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
);
this.openWorkItemDetailModal(taskLink);
}
},
beforeDestroy() {
this.removeAllPointerEventListeners();
2018-12-13 13:39:08 +05:30
},
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
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
if (this.issuableType === IssuableType.Issue) {
this.renderSortableLists();
}
2018-12-13 13:39:08 +05:30
}
},
2022-07-16 23:28:13 +05:30
renderSortableLists() {
this.removeAllPointerEventListeners();
const lists = document.querySelectorAll('.description ul, .description ol');
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());
this.addPointerEventListeners(listItem);
});
Sortable.create(
list,
getSortableDefaultOptions({
handle: '.drag-icon',
onUpdate: (event) => {
const description = convertDescriptionWithNewSort(this.descriptionText, event.to);
this.$emit('listItemReorder', description);
},
}),
);
});
},
createDragIconElement() {
const container = document.createElement('div');
container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true">
<use href="${gon.sprite_icons}#drag-vertical"></use>
</svg>`;
return container.firstChild;
},
addPointerEventListeners(listItem) {
const pointeroverListener = (event) => {
const dragIcon = event.target.closest('li').querySelector('.drag-icon');
if (!dragIcon || isDragging() || this.isUpdating) {
return;
}
dragIcon.style.visibility = 'visible';
};
const pointeroutListener = (event) => {
const dragIcon = event.target.closest('li').querySelector('.drag-icon');
if (!dragIcon) {
return;
}
dragIcon.style.visibility = 'hidden';
};
// We use pointerover/pointerout instead of CSS so that when we hover over a
// list item with children, the drag icons of its children do not become visible.
listItem.addEventListener('pointerover', pointeroverListener);
listItem.addEventListener('pointerout', pointeroutListener);
this.pointerEventListeners = this.pointerEventListeners || new Map();
this.pointerEventListeners.set(listItem, [
{ type: 'pointerover', listener: pointeroverListener },
{ type: 'pointerout', listener: pointeroutListener },
]);
},
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() {
2021-09-04 01:27:46 +05:30
createFlash({
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(
`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`,
);
} else {
$tasks.text('');
$tasksShort.text('');
}
2017-09-10 17:25:29 +05:30
},
2022-04-04 11:22:00 +05:30
renderTaskActions() {
if (!this.$el?.querySelectorAll) {
return;
}
2022-06-21 17:19:12 +05:30
this.taskButtons = [];
2022-04-04 11:22:00 +05:30
const taskListFields = this.$el.querySelectorAll('.task-list-item');
taskListFields.forEach((item, index) => {
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;
}
const workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue);
this.addHoverListeners(taskLink, workItemId);
2022-06-21 17:19:12 +05:30
taskLink.addEventListener('click', (e) => {
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', {
category: 'workItems:show',
label: 'work_item_view',
property: `type_${referenceType}`,
});
});
return;
}
2022-04-04 11:22:00 +05:30
const button = document.createElement('button');
button.classList.add(
'btn',
'btn-default',
'btn-md',
'gl-button',
'btn-default-tertiary',
'gl-p-0!',
2022-07-16 23:28:13 +05:30
'gl-mt-n1',
'gl-ml-3',
2022-04-04 11:22:00 +05:30
'js-add-task',
);
button.id = `js-task-button-${index}`;
this.taskButtons.push(button.id);
button.innerHTML = `
<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
2022-06-21 17:19:12 +05:30
<use href="${gon.sprite_icons}#doc-new"></use>
2022-04-04 11:22:00 +05:30
</svg>
`;
2022-06-21 17:19:12 +05:30
button.setAttribute('aria-label', s__('WorkItem|Convert to work item'));
2022-07-16 23:28:13 +05:30
button.addEventListener('click', () => this.openCreateTaskModal(button));
item.append(button);
});
},
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
});
},
2022-07-16 23:28:13 +05:30
setActiveTask(el) {
const { parentElement } = el;
2022-06-21 17:19:12 +05:30
const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g);
this.activeTask = {
title: parentElement.innerText,
lineNumberStart: lineNumbers[0],
lineNumberEnd: lineNumbers[1],
};
2022-07-16 23:28:13 +05:30
},
openCreateTaskModal(el) {
this.setActiveTask(el);
2022-04-04 11:22:00 +05:30
this.$refs.modal.show();
},
closeCreateTaskModal() {
this.$refs.modal.hide();
},
2022-07-16 23:28:13 +05:30
openWorkItemDetailModal(el) {
if (!el) {
return;
}
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
},
2022-06-21 17:19:12 +05:30
handleCreateTask(description) {
this.$emit('updateDescription', description);
2022-04-04 11:22:00 +05:30
this.closeCreateTaskModal();
},
2022-07-16 23:28:13 +05:30
handleDeleteTask(description) {
this.$emit('updateDescription', description);
2022-06-21 17:19:12 +05:30
this.$toast.show(s__('WorkItem|Work item 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>
<div
v-if="descriptionHtml"
:class="{
2019-02-15 15:39:39 +05:30
'js-task-list-container': canUpdate,
2022-04-04 11:22:00 +05:30
'work-items-enabled': workItemsEnabled,
2018-03-17 18:26:18 +05:30
}"
2018-11-08 19:23:39 +05:30
class="description"
2018-03-17 18:26:18 +05:30
>
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,
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>
2022-06-21 17:19:12 +05:30
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-06-21 17:19:12 +05:30
2022-04-04 11:22:00 +05:30
<gl-modal
ref="modal"
modal-id="create-task-modal"
:title="s__('WorkItem|New Task')"
hide-footer
body-class="gl-p-0!"
>
<create-work-item
2022-06-21 17:19:12 +05:30
is-modal
2022-04-04 11:22:00 +05:30
:initial-title="activeTask.title"
2022-06-21 17:19:12 +05:30
:issue-gid="issueGid"
:lock-version="lockVersion"
:line-number-start="activeTask.lineNumberStart"
:line-number-end="activeTask.lineNumberEnd"
2022-04-04 11:22:00 +05:30
@closeModal="closeCreateTaskModal"
@onCreate="handleCreateTask"
/>
</gl-modal>
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"
/>
2022-04-04 11:22:00 +05:30
<template v-if="workItemsEnabled">
2022-06-21 17:19:12 +05:30
<gl-tooltip v-for="item in taskButtons" :key="item" :target="item">
{{ s__('WorkItem|Convert to work item') }}
</gl-tooltip>
2022-04-04 11:22:00 +05:30
</template>
2017-09-10 17:25:29 +05:30
</div>
</template>