diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 10:00:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 10:00:54 +0000 |
commit | 3cccd102ba543e02725d247893729e5c73b38295 (patch) | |
tree | f36a04ec38517f5deaaacb5acc7d949688d1e187 /app/assets/javascripts/issues/show | |
parent | 205943281328046ef7b4528031b90fbda70c75ac (diff) | |
download | gitlab-ce-3cccd102ba543e02725d247893729e5c73b38295.tar.gz |
Add latest changes from gitlab-org/gitlab@14-10-stable-eev14.10.0-rc42
Diffstat (limited to 'app/assets/javascripts/issues/show')
11 files changed, 167 insertions, 97 deletions
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue index 0490728c6bc..456a2029703 100644 --- a/app/assets/javascripts/issues/show/components/app.vue +++ b/app/assets/javascripts/issues/show/components/app.vue @@ -185,6 +185,11 @@ export default { required: false, default: false, }, + issueId: { + type: Number, + required: false, + default: null, + }, }, data() { const store = new Store({ @@ -322,9 +327,12 @@ export default { }); }, + updateFormState(state) { + this.store.setFormState(state); + }, + updateAndShowForm(templates = {}) { if (!this.showForm) { - this.showForm = true; this.store.setFormState({ title: this.state.titleText, description: this.state.descriptionText, @@ -333,6 +341,7 @@ export default { updateLoading: false, issuableTemplates: templates, }); + this.showForm = true; } }, @@ -364,6 +373,10 @@ export default { }, updateIssuable() { + this.store.setFormState({ + updateLoading: true, + }); + const { store: { formState }, issueState, @@ -371,7 +384,9 @@ export default { const issuablePayload = issueState.isDirty ? { ...formState, issue_type: issueState.issueType } : formState; + this.clearFlash(); + return this.service .updateIssuable(issuablePayload) .then((res) => res.data) @@ -426,7 +441,7 @@ export default { clearFlash() { if (this.flashContainer) { - this.flashContainer.style.display = 'none'; + this.flashContainer.close(); this.flashContainer = null; } }, @@ -468,6 +483,7 @@ export default { :can-attach-file="canAttachFile" :enable-autocomplete="enableAutocomplete" :issuable-type="issuableType" + @updateForm="updateFormState" /> </div> <div v-else> @@ -534,6 +550,7 @@ export default { <component :is="descriptionComponent" + :issue-id="issueId" :can-update="canUpdate" :description-html="state.descriptionHtml" :description-text="state.descriptionText" @@ -545,6 +562,7 @@ export default { @taskListUpdateStarted="taskListUpdateStarted" @taskListUpdateSucceeded="taskListUpdateSucceeded" @taskListUpdateFailed="taskListUpdateFailed" + @updateDescription="state.descriptionHtml = $event" /> <edited-component diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 68ed7bb4062..0b7e128c47b 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -2,13 +2,18 @@ import { GlSafeHtmlDirective as SafeHtml, GlModal, + GlToast, + GlTooltip, GlModalDirective, - GlPopover, - GlButton, } from '@gitlab/ui'; import $ from 'jquery'; +import Vue from 'vue'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; import createFlash from '~/flash'; -import { __, sprintf } from '~/locale'; +import { isPositiveInteger } from '~/lib/utils/number_utils'; +import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; +import { __, s__, sprintf } from '~/locale'; import TaskList from '~/task_list'; import Tracking from '~/tracking'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -16,6 +21,8 @@ import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal. import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import animateMixin from '../mixins/animate'; +Vue.use(GlToast); + export default { directives: { SafeHtml, @@ -23,9 +30,8 @@ export default { }, components: { GlModal, - GlPopover, CreateWorkItem, - GlButton, + GlTooltip, WorkItemDetailModal, }, mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()], @@ -63,15 +69,24 @@ export default { required: false, default: 0, }, + issueId: { + type: Number, + required: false, + default: null, + }, }, data() { + const workItemId = getParameterByName('work_item_id'); + return { preAnimation: false, pulseAnimation: false, initialUpdate: true, taskButtons: [], activeTask: {}, - workItemId: null, + workItemId: isPositiveInteger(workItemId) + ? convertToGraphQLId(TYPE_WORK_ITEM, workItemId) + : undefined, }; }, computed: { @@ -81,6 +96,9 @@ export default { workItemsEnabled() { return this.glFeatures.workItems; }, + issueGid() { + return this.issueId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issueId) : null; + }, }, watch: { descriptionHtml(newDescription, oldDescription) { @@ -92,6 +110,9 @@ export default { this.$nextTick(() => { this.renderGFM(); + if (this.workItemsEnabled) { + this.renderTaskActions(); + } }); }, taskStatus() { @@ -168,9 +189,25 @@ export default { return; } + this.taskButtons = []; const taskListFields = this.$el.querySelectorAll('.task-list-item'); taskListFields.forEach((item, index) => { + const taskLink = item.querySelector('.gfm-issue'); + if (taskLink) { + const { issue, referenceType } = taskLink.dataset; + taskLink.addEventListener('click', (e) => { + e.preventDefault(); + this.workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue); + this.updateWorkItemIdUrlQuery(issue); + this.track('viewed_work_item_from_modal', { + category: 'workItems:show', + label: 'work_item_view', + property: `type_${referenceType}`, + }); + }); + return; + } const button = document.createElement('button'); button.classList.add( 'btn', @@ -188,59 +225,44 @@ export default { this.taskButtons.push(button.id); button.innerHTML = ` <svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14"> - <use href="${gon.sprite_icons}#ellipsis_v"></use> + <use href="${gon.sprite_icons}#doc-new"></use> </svg> `; + button.setAttribute('aria-label', s__('WorkItem|Convert to work item')); + button.addEventListener('click', () => this.openCreateTaskModal(button.id)); item.prepend(button); }); }, openCreateTaskModal(id) { - this.activeTask = { id, title: this.$el.querySelector(`#${id}`).parentElement.innerText }; + const { parentElement } = this.$el.querySelector(`#${id}`); + const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g); + this.activeTask = { + id, + title: parentElement.innerText, + lineNumberStart: lineNumbers[0], + lineNumberEnd: lineNumbers[1], + }; this.$refs.modal.show(); }, closeCreateTaskModal() { this.$refs.modal.hide(); }, closeWorkItemDetailModal() { - this.workItemId = null; + this.workItemId = undefined; + this.updateWorkItemIdUrlQuery(undefined); }, - handleWorkItemDetailModalError(message) { - createFlash({ message }); - }, - handleCreateTask({ id, title, type }) { - const listItem = this.$el.querySelector(`#${this.activeTask.id}`).parentElement; - const taskBadge = document.createElement('span'); - taskBadge.innerHTML = ` - <svg data-testid="issue-open-m-icon" role="img" aria-hidden="true" class="gl-icon gl-fill-green-500 s12"> - <use href="${gon.sprite_icons}#issue-open-m"></use> - </svg> - <span class="badge badge-info badge-pill gl-badge sm gl-mr-1"> - ${__('Task')} - </span> - `; - const button = this.createWorkItemDetailButton(id, title, type); - taskBadge.append(button); - - listItem.insertBefore(taskBadge, listItem.lastChild); - listItem.removeChild(listItem.lastChild); + handleCreateTask(description) { + this.$emit('updateDescription', description); this.closeCreateTaskModal(); }, - createWorkItemDetailButton(id, title, type) { - const button = document.createElement('button'); - button.addEventListener('click', () => { - this.workItemId = id; - this.track('viewed_work_item_from_modal', { - category: 'workItems:show', - label: 'work_item_view', - property: `type_${type}`, - }); - }); - button.classList.add('btn-link'); - button.innerText = title; - return button; + handleDeleteTask() { + this.$toast.show(s__('WorkItem|Work item deleted')); }, - focusButton() { - this.$refs.convertButton[0].$el.focus(); + updateWorkItemIdUrlQuery(workItemId) { + updateHistory({ + url: setUrlParams({ work_item_id: workItemId }), + replace: true, + }); }, }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'] }, @@ -266,17 +288,17 @@ export default { }" class="md" ></div> - <!-- eslint-disable vue/no-mutating-props --> + <textarea v-if="descriptionText" - v-model="descriptionText" + :value="descriptionText" :data-update-url="updateUrl" class="hidden js-task-list-field" dir="auto" data-testid="textarea" > </textarea> - <!-- eslint-enable vue/no-mutating-props --> + <gl-modal ref="modal" modal-id="create-task-modal" @@ -285,36 +307,27 @@ export default { body-class="gl-p-0!" > <create-work-item - :is-modal="true" + is-modal :initial-title="activeTask.title" + :issue-gid="issueGid" + :lock-version="lockVersion" + :line-number-start="activeTask.lineNumberStart" + :line-number-end="activeTask.lineNumberEnd" @closeModal="closeCreateTaskModal" @onCreate="handleCreateTask" /> </gl-modal> <work-item-detail-modal + :can-update="canUpdate" :visible="showWorkItemDetailModal" :work-item-id="workItemId" + @workItemDeleted="handleDeleteTask" @close="closeWorkItemDetailModal" - @error="handleWorkItemDetailModalError" /> <template v-if="workItemsEnabled"> - <gl-popover - v-for="item in taskButtons" - :key="item" - :target="item" - placement="top" - triggers="focus" - @shown="focusButton" - > - <gl-button - ref="convertButton" - variant="link" - data-testid="convert-to-task" - class="gl-text-gray-900! gl-text-decoration-none! gl-outline-0!" - @click="openCreateTaskModal(item)" - >{{ s__('WorkItem|Convert to work item') }}</gl-button - > - </gl-popover> + <gl-tooltip v-for="item in taskButtons" :key="item" :target="item"> + {{ s__('WorkItem|Convert to work item') }} + </gl-tooltip> </template> </div> </template> diff --git a/app/assets/javascripts/issues/show/components/edited.vue b/app/assets/javascripts/issues/show/components/edited.vue index 0da1900a6d0..41cc3964055 100644 --- a/app/assets/javascripts/issues/show/components/edited.vue +++ b/app/assets/javascripts/issues/show/components/edited.vue @@ -32,7 +32,7 @@ export default { </script> <template> - <small class="edited-text"> + <small class="edited-text js-issue-widgets"> Edited <time-ago-tooltip v-if="updatedAt" :time="updatedAt" tooltip-placement="bottom" /> <span v-if="hasUpdatedBy"> diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue index d5ac7b28afc..0bb5e7cb2ee 100644 --- a/app/assets/javascripts/issues/show/components/fields/description.vue +++ b/app/assets/javascripts/issues/show/components/fields/description.vue @@ -1,5 +1,6 @@ <script> import markdownField from '~/vue_shared/components/markdown/field.vue'; +import { helpPagePath } from '~/helpers/help_page_helper'; import updateMixin from '../../mixins/update'; export default { @@ -8,8 +9,8 @@ export default { }, mixins: [updateMixin], props: { - formState: { - type: Object, + value: { + type: String, required: true, }, markdownPreviewPath: { @@ -31,6 +32,11 @@ export default { default: true, }, }, + computed: { + quickActionsDocsPath() { + return helpPagePath('user/project/quick_actions'); + }, + }, mounted() { this.$refs.textarea.focus(); }, @@ -43,26 +49,26 @@ export default { <markdown-field :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" :enable-autocomplete="enableAutocomplete" - :textarea-value="formState.description" + :textarea-value="value" > <template #textarea> - <!-- eslint-disable vue/no-mutating-props --> <textarea id="issue-description" ref="textarea" - v-model="formState.description" + :value="value" class="note-textarea js-gfm-input js-autosize markdown-area qa-description-textarea" dir="auto" data-supports-quick-actions="true" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files hereā¦')" + @input="$emit('input', $event.target.value)" @keydown.meta.enter="updateIssuable" @keydown.ctrl.enter="updateIssuable" > </textarea> - <!-- eslint-enable vue/no-mutating-props --> </template> </markdown-field> </div> diff --git a/app/assets/javascripts/issues/show/components/fields/description_template.vue b/app/assets/javascripts/issues/show/components/fields/description_template.vue index d528641dcb6..98f92c97f77 100644 --- a/app/assets/javascripts/issues/show/components/fields/description_template.vue +++ b/app/assets/javascripts/issues/show/components/fields/description_template.vue @@ -8,8 +8,8 @@ export default { GlIcon, }, props: { - formState: { - type: Object, + value: { + type: String, required: true, }, issuableTemplates: { @@ -39,10 +39,9 @@ export default { // Create the editor for the template const editor = document.querySelector('.detail-page-description .note-textarea') || {}; editor.setValue = (val) => { - // eslint-disable-next-line vue/no-mutating-props - this.formState.description = val; + this.$emit('input', val); }; - editor.getValue = () => this.formState.description; + editor.getValue = () => this.value; this.issuableTemplate = new IssuableTemplateSelectors({ $dropdowns: $(this.$refs.toggle), diff --git a/app/assets/javascripts/issues/show/components/fields/title.vue b/app/assets/javascripts/issues/show/components/fields/title.vue index a73926575d0..594d1a65700 100644 --- a/app/assets/javascripts/issues/show/components/fields/title.vue +++ b/app/assets/javascripts/issues/show/components/fields/title.vue @@ -4,8 +4,8 @@ import updateMixin from '../../mixins/update'; export default { mixins: [updateMixin], props: { - formState: { - type: Object, + value: { + type: String, required: true, }, }, @@ -15,19 +15,18 @@ export default { <template> <fieldset> <label class="sr-only" for="issuable-title">{{ __('Title') }}</label> - <!-- eslint-disable vue/no-mutating-props --> <input id="issuable-title" ref="input" - v-model="formState.title" + :value="value" class="form-control qa-title-input gl-border-gray-200" dir="auto" type="text" :placeholder="__('Title')" :aria-label="__('Title')" + @input="$emit('input', $event.target.value)" @keydown.meta.enter="updateIssuable" @keydown.ctrl.enter="updateIssuable" /> - <!-- eslint-enable vue/no-mutating-props --> </fieldset> </template> diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue index 6447ec85b4e..e2c12edf46d 100644 --- a/app/assets/javascripts/issues/show/components/form.vue +++ b/app/assets/javascripts/issues/show/components/form.vue @@ -86,6 +86,10 @@ export default { }, data() { return { + formData: { + title: this.formState.title, + description: this.formState.description, + }, showOutdatedDescriptionWarning: false, }; }, @@ -100,6 +104,14 @@ export default { return this.issuableType === IssuableType.Issue; }, }, + watch: { + formData: { + handler(value) { + this.$emit('updateForm', value); + }, + deep: true, + }, + }, created() { eventHub.$on('delete.issuable', this.resetAutosave); eventHub.$on('update.issuable', this.resetAutosave); @@ -191,16 +203,17 @@ export default { > <div class="row gl-mb-3"> <div class="col-12"> - <issuable-title-field ref="title" :form-state="formState" /> + <issuable-title-field ref="title" v-model="formData.title" /> </div> </div> <div class="row"> <div v-if="isIssueType" class="col-12 col-md-4 pr-md-0"> <issuable-type-field ref="issue-type" /> </div> + <div v-if="hasIssuableTemplates" class="col-12 col-md-4 pl-md-2"> <description-template-field - :form-state="formState" + v-model="formData.description" :issuable-templates="issuableTemplates" :project-path="projectPath" :project-id="projectId" @@ -208,14 +221,16 @@ export default { /> </div> </div> + <description-field ref="description" - :form-state="formState" + v-model="formData.description" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :can-attach-file="canAttachFile" :enable-autocomplete="enableAutocomplete" /> + <edit-actions :endpoint="endpoint" :form-state="formState" diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue index 04ddc7f3501..ea0e15adfed 100644 --- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue +++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue @@ -17,12 +17,13 @@ export default { GlTab, GlTabs, HighlightBar, - MetricsTab: () => import('ee_component/issues/show/components/incidents/metrics_tab.vue'), TimelineTab: () => import('ee_component/issues/show/components/incidents/timeline_events_tab.vue'), + IncidentMetricTab: () => + import('ee_component/issues/show/components/incidents/incident_metric_tab.vue'), }, mixins: [glFeatureFlagsMixin()], - inject: ['fullPath', 'iid', 'uploadMetricsFeatureAvailable'], + inject: ['fullPath', 'iid'], apollo: { alert: { query: getAlert, @@ -52,7 +53,7 @@ export default { return this.$apollo.queries.alert.loading; }, incidentTabEnabled() { - return this.glFeatures.incidentTimelineEvents && this.glFeatures.incidentTimelineEventTab; + return this.glFeatures.incidentTimelineEvents && this.glFeatures.incidentTimeline; }, }, mounted() { @@ -63,18 +64,37 @@ export default { const { category, action } = trackIncidentDetailsViewsOptions; Tracking.event(category, action); }, + handleTabChange(tabIndex) { + const parent = document.querySelector('.js-issue-details'); + + if (parent !== null) { + const itemsToHide = parent.querySelectorAll('.js-issue-widgets'); + const lineSeparator = parent.querySelector('.js-detail-page-description'); + + lineSeparator.classList.toggle('gl-border-b-0', tabIndex > 0); + + itemsToHide.forEach(function hide(item) { + item.classList.toggle('gl-display-none', tabIndex > 0); + }); + } + }, }, }; </script> <template> <div> - <gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs"> + <gl-tabs + content-class="gl-reset-line-height" + class="gl-mt-n3" + data-testid="incident-tabs" + @input="handleTabChange" + > <gl-tab :title="s__('Incident|Summary')"> <highlight-bar :alert="alert" /> <description-component v-bind="$attrs" /> </gl-tab> - <metrics-tab v-if="uploadMetricsFeatureAvailable" data-testid="metrics-tab" /> + <incident-metric-tab /> <gl-tab v-if="alert" class="alert-management-details" diff --git a/app/assets/javascripts/issues/show/components/locked_warning.vue b/app/assets/javascripts/issues/show/components/locked_warning.vue index 4b99888ae73..12feacb027b 100644 --- a/app/assets/javascripts/issues/show/components/locked_warning.vue +++ b/app/assets/javascripts/issues/show/components/locked_warning.vue @@ -1,5 +1,5 @@ <script> -import { GlSprintf, GlLink } from '@gitlab/ui'; +import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui'; import { __ } from '~/locale'; const alertMessage = __( @@ -11,6 +11,7 @@ export default { components: { GlSprintf, GlLink, + GlAlert, }, computed: { currentPath() { @@ -21,7 +22,7 @@ export default { </script> <template> - <div class="alert alert-danger"> + <gl-alert variant="danger" class="gl-mb-5" :dismissible="false"> <gl-sprintf :message="$options.alertMessage"> <template #link="{ content }"> <gl-link :href="currentPath" target="_blank" rel="nofollow"> @@ -29,5 +30,5 @@ export default { </gl-link> </template> </gl-sprintf> - </div> + </gl-alert> </template> diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js index c9af5d9b4a7..4a5ebf9615b 100644 --- a/app/assets/javascripts/issues/show/index.js +++ b/app/assets/javascripts/issues/show/index.js @@ -102,7 +102,7 @@ export function initIssueApp(issueData, store) { isConfidential: this.getNoteableData?.confidential, isLocked: this.getNoteableData?.discussion_locked, issuableStatus: this.getNoteableData?.state, - id: this.getNoteableData?.id, + issueId: this.getNoteableData?.id, }, }); }, diff --git a/app/assets/javascripts/issues/show/mixins/update.js b/app/assets/javascripts/issues/show/mixins/update.js index 72be65b426f..31b29de580c 100644 --- a/app/assets/javascripts/issues/show/mixins/update.js +++ b/app/assets/javascripts/issues/show/mixins/update.js @@ -3,7 +3,6 @@ import eventHub from '../event_hub'; export default { methods: { updateIssuable() { - this.formState.updateLoading = true; eventHub.$emit('update.issuable'); }, }, |