Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-09-20 14:18:08 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-09-20 14:18:08 +0300
commit5afcbe03ead9ada87621888a31a62652b10a7e4f (patch)
tree9918b67a0d0f0bafa6542e839a8be37adf73102d /app/assets/javascripts/issues/show/components
parentc97c0201564848c1f53226fe19d71fdcc472f7d0 (diff)
Add latest changes from gitlab-org/gitlab@16-4-stable-eev16.4.0-rc42
Diffstat (limited to 'app/assets/javascripts/issues/show/components')
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue202
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue3
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue79
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue130
-rw-r--r--app/assets/javascripts/issues/show/components/task_list_item_actions.vue17
7 files changed, 240 insertions, 198 deletions
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 26c3db647a3..d59692d2a28 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -1,49 +1,36 @@
<script>
-import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import { createAlert } from '~/alert';
-import {
- issuableStatusText,
- STATUS_CLOSED,
- TYPE_EPIC,
- TYPE_INCIDENT,
- TYPE_ISSUE,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { TYPE_EPIC, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
+import updateDescription from '~/issues/show/utils/update_description';
+import { sanitize } from '~/lib/dompurify';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
+import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
-import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
import { ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH, POLLING_DELAY } from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
import Service from '../services/index';
-import Store from '../stores';
import DescriptionComponent from './description.vue';
import EditedComponent from './edited.vue';
import FormComponent from './form.vue';
import HeaderActions from './header_actions.vue';
import IssueHeader from './issue_header.vue';
import PinnedLinks from './pinned_links.vue';
+import StickyHeader from './sticky_header.vue';
import TitleComponent from './title.vue';
export default {
- WORKSPACE_PROJECT,
components: {
- GlIcon,
- GlBadge,
- GlIntersectionObserver,
HeaderActions,
IssueHeader,
TitleComponent,
EditedComponent,
FormComponent,
PinnedLinks,
- ConfidentialityBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ StickyHeader,
},
props: {
author: {
@@ -234,21 +221,26 @@ export default {
},
},
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,
- taskCompletionStatus: this.initialTaskCompletionStatus,
- lock_version: this.lockVersion,
- });
-
return {
- store,
- state: store.state,
+ formState: {
+ title: '',
+ description: '',
+ lockedWarningVisible: false,
+ updateLoading: false,
+ lock_version: 0,
+ issuableTemplates: {},
+ },
+ state: {
+ titleHtml: this.initialTitleHtml,
+ titleText: this.initialTitleText,
+ descriptionHtml: this.initialDescriptionHtml,
+ descriptionText: this.initialDescriptionText,
+ updatedAt: this.updatedAt,
+ updatedByName: this.updatedByName,
+ updatedByPath: this.updatedByPath,
+ taskCompletionStatus: this.initialTaskCompletionStatus,
+ lock_version: this.lockVersion,
+ },
showForm: false,
templatesRequested: false,
isStickyHeaderShowing: false,
@@ -264,17 +256,9 @@ export default {
headerClasses() {
return this.issuableType === TYPE_INCIDENT ? 'gl-mb-3' : 'gl-mb-6';
},
- issuableTemplates() {
- return this.store.formState.issuableTemplates;
- },
- formState() {
- return this.store.formState;
- },
issueChanged() {
const {
- store: {
- formState: { description, title },
- },
+ formState: { description, title },
initialDescriptionText,
initialTitleText,
} = this;
@@ -292,26 +276,13 @@ export default {
defaultErrorMessage() {
return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType });
},
- isClosed() {
- return this.issuableStatus === STATUS_CLOSED;
- },
+
pinnedLinkClasses() {
return this.showTitleBorder
? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'
: '';
},
- statusIcon() {
- if (this.issuableType === TYPE_EPIC) {
- return this.isClosed ? 'epic-closed' : 'epic';
- }
- return this.isClosed ? 'issue-closed' : 'issues';
- },
- statusVariant() {
- return this.isClosed ? 'info' : 'success';
- },
- statusText() {
- return issuableStatusText[this.issuableStatus];
- },
+
shouldShowStickyHeader() {
return [TYPE_INCIDENT, TYPE_ISSUE, TYPE_EPIC].includes(this.issuableType);
},
@@ -322,7 +293,7 @@ export default {
this.poll = new Poll({
resource: this.service,
method: 'getData',
- successCallback: (res) => this.store.updateState(res.data),
+ successCallback: (res) => this.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
@@ -360,23 +331,37 @@ export default {
}
return undefined;
},
+ updateState(data) {
+ const stateShouldUpdate =
+ this.state.titleText !== data.title_text ||
+ this.state.descriptionText !== data.description_text;
- updateStoreState() {
+ if (stateShouldUpdate) {
+ this.formState.lockedWarningVisible = true;
+ }
+
+ Object.assign(this.state, convertObjectPropsToCamelCase(data));
+ // find if there is an open details node inside of the issue description.
+ const descriptionSection = document.body.querySelector(
+ '.detail-page-description.content-block',
+ );
+ const details =
+ descriptionSection != null && descriptionSection.getElementsByTagName('details');
+
+ this.state.descriptionHtml = updateDescription(sanitize(data.description), details);
+ this.state.titleHtml = sanitize(data.title);
+ this.state.lock_version = data.lock_version;
+ },
+ refetchData() {
return this.service
.getData()
.then((res) => res.data)
- .then((data) => {
- this.store.updateState(data);
- })
- .catch(() => {
- createAlert({
- message: this.defaultErrorMessage,
- });
- });
+ .then(this.updateState)
+ .catch(() => createAlert({ message: this.defaultErrorMessage }));
},
setFormState(state) {
- this.store.setFormState(state);
+ this.formState = { ...this.formState, ...state };
},
updateFormState(templates = {}) {
@@ -416,7 +401,7 @@ export default {
this.templatesRequested = true;
this.requestTemplatesAndShowForm();
} else {
- this.updateAndShowForm(this.issuableTemplates);
+ this.updateAndShowForm(this.formState.issuableTemplates);
}
},
@@ -427,10 +412,7 @@ export default {
async updateIssuable() {
this.setFormState({ updateLoading: true });
- const {
- store: { formState },
- issueState,
- } = this;
+ const { formState, issueState } = this;
const issuablePayload = issueState.isDirty
? { ...formState, issue_type: issueState.issueType }
: formState;
@@ -464,7 +446,7 @@ export default {
visitUrl(URI);
}
})
- .then(this.updateStoreState)
+ .then(this.refetchData)
.then(() => {
eventHub.$emit('close.form');
})
@@ -518,7 +500,7 @@ export default {
this.poll.enable();
this.poll.makeDelayedRequest(POLLING_DELAY);
- this.updateStoreState();
+ this.refetchData();
},
},
};
@@ -531,7 +513,7 @@ export default {
:endpoint="endpoint"
:form-state="formState"
:initial-description-text="initialDescriptionText"
- :issuable-templates="issuableTemplates"
+ :issuable-templates="formState.issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
@@ -559,61 +541,19 @@ export default {
</template>
</title-component>
- <gl-intersection-observer
+ <sticky-header
v-if="shouldShowStickyHeader"
- @appear="hideStickyHeader"
- @disappear="showStickyHeader"
- >
- <transition name="issuable-header-slide">
- <div
- v-if="isStickyHeaderShowing"
- 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"
- data-testid="issue-sticky-header"
- >
- <div
- class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
- >
- <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
- >
- <span
- v-if="isLocked"
- v-gl-tooltip.bottom
- data-testid="locked"
- class="issuable-warning-icon"
- :title="__('This issue is locked. Only project members can comment.')"
- >
- <gl-icon name="lock" :aria-label="__('Locked')" />
- </span>
- <confidentiality-badge
- v-if="isConfidential"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
- :issuable-type="issuableType"
- />
- <span
- v-if="isHidden"
- v-gl-tooltip.bottom
- :title="__('This issue is hidden because its author has been banned')"
- data-testid="hidden"
- class="issuable-warning-icon"
- >
- <gl-icon name="spam" />
- </span>
- <a
- href="#top"
- class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0 gl-text-black-normal"
- :title="state.titleText"
- >
- {{ state.titleText }}
- </a>
- </div>
- </div>
- </transition>
- </gl-intersection-observer>
+ :is-confidential="isConfidential"
+ :is-hidden="isHidden"
+ :is-locked="isLocked"
+ :issuable-status="issuableStatus"
+ :issuable-type="issuableType"
+ :show="isStickyHeaderShowing"
+ :title="state.titleText"
+ :title-html="state.titleHtml"
+ @hide="hideStickyHeader"
+ @show="showStickyHeader"
+ />
<slot name="header">
<issue-header
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 90f01603f96..acbba216601 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -307,7 +307,8 @@ export default {
);
taskListItems?.forEach((item) => {
- const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate });
+ const provide = { canUpdate: this.canUpdate, issuableType: this.issuableType };
+ const dropdown = this.createTaskListItemActions(provide);
this.insertNextToTaskListItemText(dropdown, item);
this.addPointerEventListeners(item, '.task-list-item-actions');
this.hasTaskListItemActions = true;
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 1ade5e654e9..81e5c30a264 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -79,62 +79,25 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [trackingMixin, glFeatureFlagMixin()],
- inject: {
- canCreateIssue: {
- default: false,
- },
- canDestroyIssue: {
- default: false,
- },
- canPromoteToEpic: {
- default: false,
- },
- canReopenIssue: {
- default: false,
- },
- canReportSpam: {
- default: false,
- },
- canUpdateIssue: {
- default: false,
- },
- iid: {
- default: '',
- },
- issuableId: {
- default: '',
- },
- isIssueAuthor: {
- default: false,
- },
- issuePath: {
- default: '',
- },
- issueType: {
- default: TYPE_ISSUE,
- },
- newIssuePath: {
- default: '',
- },
- projectPath: {
- default: '',
- },
- submitAsSpamPath: {
- default: '',
- },
- reportedUserId: {
- default: '',
- },
- reportedFromUrl: {
- default: '',
- },
- issuableEmailAddress: {
- default: '',
- },
- fullPath: {
- default: '',
- },
- },
+ inject: [
+ 'canCreateIssue',
+ 'canDestroyIssue',
+ 'canPromoteToEpic',
+ 'canReopenIssue',
+ 'canReportSpam',
+ 'canUpdateIssue',
+ 'iid',
+ 'isIssueAuthor',
+ 'issuePath',
+ 'issueType',
+ 'newIssuePath',
+ 'projectPath',
+ 'submitAsSpamPath',
+ 'reportedUserId',
+ 'reportedFromUrl',
+ 'issuableEmailAddress',
+ 'fullPath',
+ ],
data() {
return {
isReportAbuseDrawerOpen: false,
@@ -256,7 +219,7 @@ export default {
mutation: updateIssueMutation,
variables: {
input: {
- iid: this.iid.toString(),
+ iid: String(this.iid),
projectPath: this.projectPath,
stateEvent: this.isClosed ? ISSUE_STATE_EVENT_REOPEN : ISSUE_STATE_EVENT_CLOSE,
},
@@ -501,7 +464,7 @@ export default {
>{{ copyMailAddressText }}</gl-dropdown-item
>
</template>
- <gl-dropdown-divider v-if="showToggleIssueStateButton || canDestroyIssue || canReportSpam" />
+ <gl-dropdown-divider v-if="canDestroyIssue || canReportSpam || !isIssueAuthor" />
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
diff --git a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
index ac64c35bf15..ab1bb9253f4 100644
--- a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
@@ -107,10 +107,7 @@ export default {
</script>
<template>
- <div
- class="create-timeline-event gl-relative gl-display-flex gl-align-items-start"
- :class="{ 'timeline-entry-vertical-line': hasTimelineEvents }"
- >
+ <div class="create-timeline-event gl-relative gl-display-flex gl-align-items-start">
<div
v-if="hasTimelineEvents"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-w-8 gl-h-8 gl-flex-shrink-0 gl-p-3 gl-z-index-1"
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 4ec64ef838d..2909a4d2666 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -43,7 +43,7 @@ export default {
variables() {
return {
fullPath: this.fullPath,
- iid: this.iid,
+ iid: String(this.iid),
};
},
update(data) {
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
new file mode 100644
index 00000000000..bcf10ee92bb
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -0,0 +1,130 @@
+<script>
+import { GlBadge, GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import {
+ issuableStatusText,
+ STATUS_CLOSED,
+ TYPE_EPIC,
+ WORKSPACE_PROJECT,
+} from '~/issues/constants';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+export default {
+ WORKSPACE_PROJECT,
+ components: {
+ ConfidentialityBadge,
+ GlBadge,
+ GlIcon,
+ GlIntersectionObserver,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ props: {
+ isConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isHidden: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLocked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ issuableStatus: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ show: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ titleHtml: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isClosed() {
+ return this.issuableStatus === STATUS_CLOSED;
+ },
+ statusIcon() {
+ if (this.issuableType === TYPE_EPIC) {
+ return this.isClosed ? 'epic-closed' : 'epic';
+ }
+ return this.isClosed ? 'issue-closed' : 'issues';
+ },
+ statusText() {
+ return issuableStatusText[this.issuableStatus];
+ },
+ statusVariant() {
+ return this.isClosed ? 'info' : 'success';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-intersection-observer @appear="$emit('hide')" @disappear="$emit('show')">
+ <transition name="issuable-header-slide">
+ <div
+ v-if="show"
+ 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"
+ data-testid="issue-sticky-header"
+ >
+ <div
+ class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-gap-2 gl-mx-auto gl-px-5"
+ >
+ <gl-badge :variant="statusVariant">
+ <gl-icon :name="statusIcon" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ statusText }}</span>
+ </gl-badge>
+ <span
+ v-if="isLocked"
+ v-gl-tooltip.bottom
+ data-testid="locked"
+ class="issuable-warning-icon"
+ :title="__('This issue is locked. Only project members can comment.')"
+ >
+ <gl-icon name="lock" :aria-label="__('Locked')" />
+ </span>
+ <confidentiality-badge
+ v-if="isConfidential"
+ :issuable-type="issuableType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ />
+ <span
+ v-if="isHidden"
+ v-gl-tooltip.bottom
+ :title="__('This issue is hidden because its author has been banned')"
+ data-testid="hidden"
+ class="issuable-warning-icon"
+ >
+ <gl-icon name="spam" />
+ </span>
+ <a
+ v-safe-html="titleHtml || title"
+ href="#top"
+ class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0 gl-text-black-normal"
+ >
+ </a>
+ </div>
+ </div>
+ </transition>
+ </gl-intersection-observer>
+</template>
diff --git a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
index 64b916caddb..55e2e857050 100644
--- a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
+++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
@@ -1,5 +1,6 @@
<script>
import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import { __, s__ } from '~/locale';
import eventHub from '../event_hub';
@@ -13,7 +14,12 @@ export default {
GlDisclosureDropdown,
GlDisclosureDropdownItem,
},
- inject: ['canUpdate'],
+ inject: ['canUpdate', 'issuableType'],
+ computed: {
+ showConvertToTaskItem() {
+ return [TYPE_INCIDENT, TYPE_ISSUE].includes(this.issuableType);
+ },
+ },
methods: {
convertToTask() {
eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos);
@@ -37,12 +43,17 @@ export default {
text-sr-only
toggle-class="task-list-item-actions gl-opacity-0 gl-p-2! "
>
- <gl-disclosure-dropdown-item class="gl-ml-2!" @action="convertToTask">
+ <gl-disclosure-dropdown-item
+ v-if="showConvertToTaskItem"
+ class="gl-ml-2!"
+ data-testid="convert"
+ @action="convertToTask"
+ >
<template #list-item>
{{ $options.i18n.convertToTask }}
</template>
</gl-disclosure-dropdown-item>
- <gl-disclosure-dropdown-item class="gl-ml-2!" @action="deleteTaskListItem">
+ <gl-disclosure-dropdown-item class="gl-ml-2!" data-testid="delete" @action="deleteTaskListItem">
<template #list-item>
<span class="gl-text-red-500!">{{ $options.i18n.delete }}</span>
</template>