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

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

581 lines
15 KiB
Vue
Raw Normal View History

2017-09-10 17:25:29 +05:30
<script>
2022-07-16 23:28:13 +05:30
import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
2018-12-13 13:39:08 +05:30
import Visibility from 'visibilityjs';
2023-05-27 22:25:52 +05:30
import { createAlert } from '~/alert';
2022-07-16 23:28:13 +05:30
import {
IssuableStatusText,
2023-04-23 21:23:45 +05:30
STATUS_CLOSED,
TYPE_EPIC,
2023-05-27 22:25:52 +05:30
TYPE_INCIDENT,
2023-04-23 21:23:45 +05:30
TYPE_ISSUE,
2023-05-27 22:25:52 +05:30
WORKSPACE_PROJECT,
2022-07-16 23:28:13 +05:30
} from '~/issues/constants';
2020-06-23 00:09:42 +05:30
import Poll from '~/lib/utils/poll';
2021-03-11 19:13:27 +05:30
import { visitUrl } from '~/lib/utils/url_utility';
2021-12-11 22:18:48 +05:30
import { __, sprintf } from '~/locale';
2022-07-16 23:28:13 +05:30
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
2023-05-27 22:25:52 +05:30
import { ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH, POLLING_DELAY } from '../constants';
2018-12-13 13:39:08 +05:30
import eventHub from '../event_hub';
2021-09-04 01:27:46 +05:30
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
2018-12-13 13:39:08 +05:30
import Service from '../services/index';
import Store from '../stores';
2022-10-11 01:57:18 +05:30
import DescriptionComponent from './description.vue';
import EditedComponent from './edited.vue';
import FormComponent from './form.vue';
2019-09-04 21:01:54 +05:30
import PinnedLinks from './pinned_links.vue';
2022-10-11 01:57:18 +05:30
import TitleComponent from './title.vue';
2017-09-10 17:25:29 +05:30
2018-12-13 13:39:08 +05:30
export default {
2023-05-27 22:25:52 +05:30
WORKSPACE_PROJECT,
2018-12-13 13:39:08 +05:30
components: {
2020-06-23 00:09:42 +05:30
GlIcon,
2022-07-16 23:28:13 +05:30
GlBadge,
2020-06-23 00:09:42 +05:30
GlIntersectionObserver,
2022-10-11 01:57:18 +05:30
TitleComponent,
EditedComponent,
FormComponent,
2019-09-04 21:01:54 +05:30
PinnedLinks,
2022-07-16 23:28:13 +05:30
ConfidentialityBadge,
2018-12-13 13:39:08 +05:30
},
2021-11-11 11:23:49 +05:30
directives: {
GlTooltip: GlTooltipDirective,
},
2018-12-13 13:39:08 +05:30
props: {
endpoint: {
required: true,
type: String,
2017-09-10 17:25:29 +05:30
},
2018-12-13 13:39:08 +05:30
updateEndpoint: {
required: true,
type: String,
},
canUpdate: {
required: true,
type: Boolean,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
2019-09-30 21:07:59 +05:30
zoomMeetingUrl: {
type: String,
required: false,
2020-06-23 00:09:42 +05:30
default: '',
},
publishedIncidentUrl: {
type: String,
required: false,
default: '',
2019-09-30 21:07:59 +05:30
},
2018-12-13 13:39:08 +05:30
issuableRef: {
type: String,
required: true,
},
2020-06-23 00:09:42 +05:30
issuableStatus: {
type: String,
required: false,
default: '',
},
2018-12-13 13:39:08 +05:30
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
2019-12-21 20:55:43 +05:30
issuableTemplateNamesPath: {
type: String,
2018-12-13 13:39:08 +05:30
required: false,
2019-12-21 20:55:43 +05:30
default: '',
2018-12-13 13:39:08 +05:30
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
2021-03-11 19:13:27 +05:30
projectId: {
type: Number,
required: true,
},
2018-12-13 13:39:08 +05:30
projectNamespace: {
type: String,
required: true,
},
2021-01-29 00:20:46 +05:30
isConfidential: {
type: Boolean,
required: false,
default: false,
},
isLocked: {
type: Boolean,
required: false,
default: false,
},
2018-12-13 13:39:08 +05:30
issuableType: {
type: String,
required: false,
2023-04-23 21:23:45 +05:30
default: TYPE_ISSUE,
2018-12-13 13:39:08 +05:30
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
2019-03-02 22:35:43 +05:30
lockVersion: {
type: Number,
required: false,
default: 0,
},
2020-11-24 15:15:51 +05:30
descriptionComponent: {
type: Object,
required: false,
default: () => {
2022-10-11 01:57:18 +05:30
return DescriptionComponent;
2020-11-24 15:15:51 +05:30
},
},
showTitleBorder: {
type: Boolean,
required: false,
default: true,
},
2021-11-11 11:23:49 +05:30
isHidden: {
type: Boolean,
required: false,
default: false,
},
2022-06-21 17:19:12 +05:30
issueId: {
type: Number,
required: false,
default: null,
},
2018-12-13 13:39:08 +05:30
},
data() {
const store = new Store({
titleHtml: this.initialTitleHtml,
titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
2019-03-02 22:35:43 +05:30
lock_version: this.lockVersion,
2018-12-13 13:39:08 +05:30
});
2017-09-10 17:25:29 +05:30
2018-12-13 13:39:08 +05:30
return {
store,
state: store.state,
showForm: false,
2019-12-21 20:55:43 +05:30
templatesRequested: false,
2020-06-23 00:09:42 +05:30
isStickyHeaderShowing: false,
2021-09-04 01:27:46 +05:30
issueState: {},
2018-12-13 13:39:08 +05:30
};
},
2021-09-04 01:27:46 +05:30
apollo: {
issueState: {
query: getIssueStateQuery,
},
},
2018-12-13 13:39:08 +05:30
computed: {
2019-12-21 20:55:43 +05:30
issuableTemplates() {
return this.store.formState.issuableTemplates;
},
2018-12-13 13:39:08 +05:30
formState() {
return this.store.formState;
2017-09-10 17:25:29 +05:30
},
2018-12-13 13:39:08 +05:30
hasUpdated() {
2019-09-04 21:01:54 +05:30
return Boolean(this.state.updatedAt);
2018-12-13 13:39:08 +05:30
},
issueChanged() {
2019-07-31 22:56:46 +05:30
const {
store: {
formState: { description, title },
},
initialDescriptionText,
initialTitleText,
} = this;
if (initialDescriptionText || description) {
return initialDescriptionText !== description;
}
if (initialTitleText || title) {
return initialTitleText !== title;
}
return false;
2018-12-13 13:39:08 +05:30
},
2019-03-02 22:35:43 +05:30
defaultErrorMessage() {
2021-12-11 22:18:48 +05:30
return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType });
2019-03-02 22:35:43 +05:30
},
2021-01-29 00:20:46 +05:30
isClosed() {
2023-04-23 21:23:45 +05:30
return this.issuableStatus === STATUS_CLOSED;
2020-06-23 00:09:42 +05:30
},
2020-11-24 15:15:51 +05:30
pinnedLinkClasses() {
return this.showTitleBorder
? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'
: '';
},
2020-06-23 00:09:42 +05:30
statusIcon() {
2023-04-23 21:23:45 +05:30
if (this.issuableType === TYPE_ISSUE) {
2022-07-16 23:28:13 +05:30
return this.isClosed ? 'issue-closed' : 'issues';
}
return this.isClosed ? 'epic-closed' : 'epic';
},
statusVariant() {
return this.isClosed ? 'info' : 'success';
2020-06-23 00:09:42 +05:30
},
statusText() {
return IssuableStatusText[this.issuableStatus];
},
shouldShowStickyHeader() {
2023-04-23 21:23:45 +05:30
return [TYPE_ISSUE, TYPE_EPIC].includes(this.issuableType);
2020-06-23 00:09:42 +05:30
},
2018-12-13 13:39:08 +05:30
},
created() {
2023-05-27 22:25:52 +05:30
this.alert = null;
2018-12-13 13:39:08 +05:30
this.service = new Service(this.endpoint);
this.poll = new Poll({
resource: this.service,
method: 'getData',
2021-03-08 18:12:59 +05:30
successCallback: (res) => this.store.updateState(res.data),
2018-12-13 13:39:08 +05:30
errorCallback(err) {
throw new Error(err);
},
});
if (!Visibility.hidden()) {
2021-12-11 22:18:48 +05:30
this.poll.makeDelayedRequest(POLLING_DELAY);
2018-12-13 13:39:08 +05:30
}
2018-03-17 18:26:18 +05:30
2018-12-13 13:39:08 +05:30
Visibility.change(() => {
2018-03-17 18:26:18 +05:30
if (!Visibility.hidden()) {
2018-12-13 13:39:08 +05:30
this.poll.restart();
} else {
this.poll.stop();
2017-09-10 17:25:29 +05:30
}
2018-12-13 13:39:08 +05:30
});
2018-03-17 18:26:18 +05:30
2018-12-13 13:39:08 +05:30
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
2018-03-17 18:26:18 +05:30
2018-12-13 13:39:08 +05:30
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
},
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
2021-09-04 01:27:46 +05:30
if (this.showForm && this.issueChanged && !this.issueState.isDirty) {
2019-03-02 22:35:43 +05:30
event.returnValue = __('Are you sure you want to lose your issue information?');
2018-12-13 13:39:08 +05:30
}
return undefined;
},
2019-12-21 20:55:43 +05:30
2019-03-02 22:35:43 +05:30
updateStoreState() {
return this.service
.getData()
2021-03-08 18:12:59 +05:30
.then((res) => res.data)
.then((data) => {
2019-03-02 22:35:43 +05:30
this.store.updateState(data);
})
.catch(() => {
2023-03-04 22:38:38 +05:30
createAlert({
2021-09-04 01:27:46 +05:30
message: this.defaultErrorMessage,
});
2019-03-02 22:35:43 +05:30
});
},
2017-09-10 17:25:29 +05:30
2022-07-16 23:28:13 +05:30
setFormState(state) {
2022-06-21 17:19:12 +05:30
this.store.setFormState(state);
},
2022-07-16 23:28:13 +05:30
updateFormState(templates = {}) {
this.setFormState({
title: this.state.titleText,
description: this.state.descriptionText,
lock_version: this.state.lock_version,
lockedWarningVisible: false,
updateLoading: false,
issuableTemplates: templates,
});
},
updateAndShowForm(templates) {
2018-12-13 13:39:08 +05:30
if (!this.showForm) {
2022-07-16 23:28:13 +05:30
this.updateFormState(templates);
2022-06-21 17:19:12 +05:30
this.showForm = true;
2019-12-21 20:55:43 +05:30
}
},
requestTemplatesAndShowForm() {
return this.service
.loadTemplates(this.issuableTemplateNamesPath)
2021-03-08 18:12:59 +05:30
.then((res) => {
2019-12-21 20:55:43 +05:30
this.updateAndShowForm(res.data);
})
.catch(() => {
2023-03-04 22:38:38 +05:30
createAlert({
2021-09-04 01:27:46 +05:30
message: this.defaultErrorMessage,
});
2019-12-21 20:55:43 +05:30
this.updateAndShowForm();
2018-12-13 13:39:08 +05:30
});
2019-12-21 20:55:43 +05:30
},
openForm() {
if (!this.templatesRequested) {
this.templatesRequested = true;
this.requestTemplatesAndShowForm();
} else {
this.updateAndShowForm(this.issuableTemplates);
2018-12-13 13:39:08 +05:30
}
},
2019-12-21 20:55:43 +05:30
2018-12-13 13:39:08 +05:30
closeForm() {
this.showForm = false;
},
2017-09-10 17:25:29 +05:30
2018-12-13 13:39:08 +05:30
updateIssuable() {
2022-07-16 23:28:13 +05:30
this.setFormState({ updateLoading: true });
2022-06-21 17:19:12 +05:30
2021-09-04 01:27:46 +05:30
const {
store: { formState },
issueState,
} = this;
const issuablePayload = issueState.isDirty
? { ...formState, issue_type: issueState.issueType }
: formState;
2022-06-21 17:19:12 +05:30
2023-05-27 22:25:52 +05:30
this.alert?.dismiss();
2022-06-21 17:19:12 +05:30
2018-12-13 13:39:08 +05:30
return this.service
2021-09-04 01:27:46 +05:30
.updateIssuable(issuablePayload)
2021-03-08 18:12:59 +05:30
.then((res) => res.data)
.then((data) => {
2021-09-04 01:27:46 +05:30
if (
!window.location.pathname.includes(data.web_url) &&
2023-05-27 22:25:52 +05:30
issueState.issueType !== TYPE_INCIDENT
2021-09-04 01:27:46 +05:30
) {
2018-12-13 13:39:08 +05:30
visitUrl(data.web_url);
}
2021-09-04 01:27:46 +05:30
if (issueState.isDirty) {
const URI =
2023-05-27 22:25:52 +05:30
issueState.issueType === TYPE_INCIDENT
2022-03-02 08:16:31 +05:30
? data.web_url.replace(ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH)
2021-09-04 01:27:46 +05:30
: data.web_url;
visitUrl(URI);
}
2018-12-13 13:39:08 +05:30
})
2019-03-02 22:35:43 +05:30
.then(this.updateStoreState)
.then(() => {
2018-12-13 13:39:08 +05:30
eventHub.$emit('close.form');
})
2019-03-02 22:35:43 +05:30
.catch((error = {}) => {
2021-04-17 20:07:23 +05:30
const { message, response = {} } = error;
2019-03-02 22:35:43 +05:30
2021-04-17 20:07:23 +05:30
let errMsg = this.defaultErrorMessage;
2019-03-02 22:35:43 +05:30
2021-04-17 20:07:23 +05:30
if (response.data && response.data.errors) {
errMsg += `. ${response.data.errors.join(' ')}`;
} else if (message) {
errMsg += `. ${message}`;
2018-12-13 13:39:08 +05:30
}
2017-09-10 17:25:29 +05:30
2023-05-27 22:25:52 +05:30
this.alert = createAlert({
2021-09-04 01:27:46 +05:30
message: errMsg,
});
2022-07-16 23:28:13 +05:30
})
.finally(() => {
this.setFormState({ updateLoading: false });
2021-04-17 20:07:23 +05:30
});
2018-12-13 13:39:08 +05:30
},
2017-09-10 17:25:29 +05:30
2020-06-23 00:09:42 +05:30
hideStickyHeader() {
this.isStickyHeaderShowing = false;
},
showStickyHeader() {
this.isStickyHeaderShowing = true;
},
2021-04-17 20:07:23 +05:30
2023-04-23 21:23:45 +05:30
handleSaveDescription(description) {
2022-07-16 23:28:13 +05:30
this.updateFormState();
this.setFormState({ description });
this.updateIssuable();
},
2021-12-11 22:18:48 +05:30
taskListUpdateStarted() {
this.poll.stop();
},
taskListUpdateSucceeded() {
this.poll.enable();
this.poll.makeDelayedRequest(POLLING_DELAY);
},
taskListUpdateFailed() {
this.poll.enable();
this.poll.makeDelayedRequest(POLLING_DELAY);
this.updateStoreState();
},
2018-12-13 13:39:08 +05:30
},
};
2017-09-10 17:25:29 +05:30
</script>
<template>
<div>
2018-03-17 18:26:18 +05:30
<div v-if="canUpdate && showForm">
<form-component
2022-01-26 12:08:38 +05:30
:endpoint="endpoint"
2018-03-17 18:26:18 +05:30
:form-state="formState"
2021-06-08 01:23:25 +05:30
:initial-description-text="initialDescriptionText"
2018-03-17 18:26:18 +05:30
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
2021-03-11 19:13:27 +05:30
:project-id="projectId"
2018-03-17 18:26:18 +05:30
:project-namespace="projectNamespace"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
2018-12-05 23:21:45 +05:30
:issuable-type="issuableType"
2022-07-16 23:28:13 +05:30
@updateForm="setFormState"
2018-03-17 18:26:18 +05:30
/>
</div>
2017-09-10 17:25:29 +05:30
<div v-else>
<title-component
:issuable-ref="issuableRef"
2018-03-17 18:26:18 +05:30
:can-update="canUpdate"
2017-09-10 17:25:29 +05:30
:title-html="state.titleHtml"
2018-03-17 18:26:18 +05:30
:title-text="state.titleText"
/>
2020-06-23 00:09:42 +05:30
2021-01-29 00:20:46 +05:30
<gl-intersection-observer
v-if="shouldShowStickyHeader"
@appear="hideStickyHeader"
@disappear="showStickyHeader"
>
2020-06-23 00:09:42 +05:30
<transition name="issuable-header-slide">
<div
2021-01-29 00:20:46 +05:30
v-if="isStickyHeaderShowing"
2020-07-28 23:09:34 +05:30
class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
2020-06-23 00:09:42 +05:30
data-testid="issue-sticky-header"
>
<div
class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
>
2022-07-16 23:28:13 +05:30
<gl-badge :variant="statusVariant" class="gl-mr-2">
<gl-icon :name="statusIcon" />
<span class="gl-display-none gl-sm-display-block gl-ml-2">{{
statusText
}}</span></gl-badge
2020-06-23 00:09:42 +05:30
>
2021-01-29 00:20:46 +05:30
<span v-if="isLocked" data-testid="locked" class="issuable-warning-icon">
<gl-icon name="lock" :aria-label="__('Locked')" />
</span>
2022-07-16 23:28:13 +05:30
<confidentiality-badge
v-if="isConfidential"
data-testid="confidential"
2023-05-27 22:25:52 +05:30
:workspace-type="$options.WORKSPACE_PROJECT"
2022-07-16 23:28:13 +05:30
:issuable-type="issuableType"
/>
2021-11-11 11:23:49 +05:30
<span
v-if="isHidden"
v-gl-tooltip
:title="__('This issue is hidden because its author has been banned')"
data-testid="hidden"
class="issuable-warning-icon"
>
<gl-icon name="spam" />
</span>
2020-06-23 00:09:42 +05:30
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="state.titleText"
>
{{ state.titleText }}
</p>
</div>
</div>
</transition>
</gl-intersection-observer>
<pinned-links
:zoom-meeting-url="zoomMeetingUrl"
:published-incident-url="publishedIncidentUrl"
2020-11-24 15:15:51 +05:30
:class="pinnedLinkClasses"
2020-06-23 00:09:42 +05:30
/>
2020-11-24 15:15:51 +05:30
<component
:is="descriptionComponent"
2022-06-21 17:19:12 +05:30
:issue-id="issueId"
2017-09-10 17:25:29 +05:30
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
2018-03-17 18:26:18 +05:30
:task-status="state.taskStatus"
:issuable-type="issuableType"
:update-url="updateEndpoint"
2019-03-02 22:35:43 +05:30
:lock-version="state.lock_version"
2022-07-16 23:28:13 +05:30
:is-updating="formState.updateLoading"
2023-04-23 21:23:45 +05:30
@saveDescription="handleSaveDescription"
2021-12-11 22:18:48 +05:30
@taskListUpdateStarted="taskListUpdateStarted"
@taskListUpdateSucceeded="taskListUpdateSucceeded"
@taskListUpdateFailed="taskListUpdateFailed"
2022-06-21 17:19:12 +05:30
@updateDescription="state.descriptionHtml = $event"
2018-03-17 18:26:18 +05:30
/>
2020-06-23 00:09:42 +05:30
2017-09-10 17:25:29 +05:30
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
2018-03-17 18:26:18 +05:30
:updated-by-path="state.updatedByPath"
/>
2017-09-10 17:25:29 +05:30
</div>
</div>
</template>