diff options
Diffstat (limited to 'app/assets/javascripts/issues/show/components/description.vue')
-rw-r--r-- | app/assets/javascripts/issues/show/components/description.vue | 156 |
1 files changed, 140 insertions, 16 deletions
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 0b7e128c47b..4f97458dcd1 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -7,22 +7,31 @@ import { GlModalDirective, } from '@gitlab/ui'; import $ from 'jquery'; +import Sortable from 'sortablejs'; import Vue from 'vue'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; import createFlash from '~/flash'; import { isPositiveInteger } from '~/lib/utils/number_utils'; import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { __, s__, sprintf } from '~/locale'; +import { getSortableDefaultOptions, isDragging } from '~/sortable/utils'; import TaskList from '~/task_list'; import Tracking from '~/tracking'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; + import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import animateMixin from '../mixins/animate'; +import { convertDescriptionWithNewSort } from '../utils'; Vue.use(GlToast); +const workItemTypes = { + TASK: 'task', +}; + export default { directives: { SafeHtml, @@ -74,6 +83,11 @@ export default { required: false, default: null, }, + isUpdating: { + type: Boolean, + required: false, + default: false, + }, }, data() { const workItemId = getParameterByName('work_item_id'); @@ -89,10 +103,20 @@ export default { : undefined, }; }, - computed: { - showWorkItemDetailModal() { - return Boolean(this.workItemId); + apollo: { + workItem: { + query: workItemQuery, + variables() { + return { + id: this.workItemId, + }; + }, + skip() { + return !this.workItemId || !this.workItemsEnabled; + }, }, + }, + computed: { workItemsEnabled() { return this.glFeatures.workItems; }, @@ -126,6 +150,16 @@ export default { if (this.workItemsEnabled) { this.renderTaskActions(); } + + if (this.workItemId) { + const taskLink = this.$el.querySelector( + `.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`, + ); + this.openWorkItemDetailModal(taskLink); + } + }, + beforeDestroy() { + this.removeAllPointerEventListeners(); }, methods: { renderGFM() { @@ -142,9 +176,67 @@ export default { onSuccess: this.taskListUpdateSuccess.bind(this), onError: this.taskListUpdateError.bind(this), }); + + this.renderSortableLists(); } }, + renderSortableLists() { + this.removeAllPointerEventListeners(); + + const lists = document.querySelectorAll('.description ul, .description ol'); + lists.forEach((list) => { + 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) => { + if (isDragging() || this.isUpdating) { + return; + } + event.target.closest('li').querySelector('.drag-icon').style.visibility = 'visible'; // eslint-disable-line no-param-reassign + }; + const pointeroutListener = (event) => { + event.target.closest('li').querySelector('.drag-icon').style.visibility = 'hidden'; // eslint-disable-line no-param-reassign + }; + + // 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); + }); + }, taskListUpdateStarted() { this.$emit('taskListUpdateStarted'); }, @@ -195,10 +287,16 @@ export default { taskListFields.forEach((item, index) => { const taskLink = item.querySelector('.gfm-issue'); if (taskLink) { - const { issue, referenceType } = taskLink.dataset; + const { issue, referenceType, issueType } = taskLink.dataset; + if (issueType !== workItemTypes.TASK) { + return; + } + const workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue); + this.addHoverListeners(taskLink, workItemId); taskLink.addEventListener('click', (e) => { e.preventDefault(); - this.workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue); + this.openWorkItemDetailModal(taskLink); + this.workItemId = workItemId; this.updateWorkItemIdUrlQuery(issue); this.track('viewed_work_item_from_modal', { category: 'workItems:show', @@ -215,10 +313,9 @@ export default { 'btn-md', 'gl-button', 'btn-default-tertiary', - 'gl-left-0', 'gl-p-0!', - 'gl-top-2', - 'gl-absolute', + 'gl-mt-n1', + 'gl-ml-3', 'js-add-task', ); button.id = `js-task-button-${index}`; @@ -229,24 +326,46 @@ export default { </svg> `; button.setAttribute('aria-label', s__('WorkItem|Convert to work item')); - button.addEventListener('click', () => this.openCreateTaskModal(button.id)); - item.prepend(button); + button.addEventListener('click', () => this.openCreateTaskModal(button)); + item.append(button); }); }, - openCreateTaskModal(id) { - const { parentElement } = this.$el.querySelector(`#${id}`); + addHoverListeners(taskLink, id) { + let workItemPrefetch; + taskLink.addEventListener('mouseover', () => { + workItemPrefetch = setTimeout(() => { + this.workItemId = id; + }, 150); + }); + taskLink.addEventListener('mouseout', () => { + if (workItemPrefetch) { + clearTimeout(workItemPrefetch); + } + }); + }, + setActiveTask(el) { + const { parentElement } = el; const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g); this.activeTask = { - id, title: parentElement.innerText, lineNumberStart: lineNumbers[0], lineNumberEnd: lineNumbers[1], }; + }, + openCreateTaskModal(el) { + this.setActiveTask(el); this.$refs.modal.show(); }, closeCreateTaskModal() { this.$refs.modal.hide(); }, + openWorkItemDetailModal(el) { + if (!el) { + return; + } + this.setActiveTask(el); + this.$refs.detailsModal.show(); + }, closeWorkItemDetailModal() { this.workItemId = undefined; this.updateWorkItemIdUrlQuery(undefined); @@ -255,7 +374,8 @@ export default { this.$emit('updateDescription', description); this.closeCreateTaskModal(); }, - handleDeleteTask() { + handleDeleteTask(description) { + this.$emit('updateDescription', description); this.$toast.show(s__('WorkItem|Work item deleted')); }, updateWorkItemIdUrlQuery(workItemId) { @@ -318,9 +438,13 @@ export default { /> </gl-modal> <work-item-detail-modal + ref="detailsModal" :can-update="canUpdate" - :visible="showWorkItemDetailModal" :work-item-id="workItemId" + :issue-gid="issueGid" + :lock-version="lockVersion" + :line-number-start="activeTask.lineNumberStart" + :line-number-end="activeTask.lineNumberEnd" @workItemDeleted="handleDeleteTask" @close="closeWorkItemDetailModal" /> |