summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/issues/show
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issues/show')
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue22
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue143
-rw-r--r--app/assets/javascripts/issues/show/components/edited.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue18
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description_template.vue9
-rw-r--r--app/assets/javascripts/issues/show/components/fields/title.vue9
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue21
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue30
-rw-r--r--app/assets/javascripts/issues/show/components/locked_warning.vue7
-rw-r--r--app/assets/javascripts/issues/show/index.js2
-rw-r--r--app/assets/javascripts/issues/show/mixins/update.js1
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');
},
},