summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/issues
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issues')
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js14
-rw-r--r--app/assets/javascripts/issues/index.js12
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_time_info.vue15
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue95
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql2
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql2
-rw-r--r--app/assets/javascripts/issues/manual_ordering.js7
-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
18 files changed, 240 insertions, 171 deletions
diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index 247f8dd0bd6..c96af6da720 100644
--- a/app/assets/javascripts/issues/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -43,7 +43,7 @@ export default class CreateMergeRequestDropdown {
this.refInput = this.wrapperEl.querySelector('.js-ref');
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
- this.unavailableButtonSpinner = this.unavailableButton.querySelector('.gl-spinner');
+ this.unavailableButtonSpinner = this.unavailableButton.querySelector('.js-create-mr-spinner');
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
this.branchCreated = false;
@@ -453,7 +453,7 @@ export default class CreateMergeRequestDropdown {
removeMessage(target) {
const { input, message } = this.getTargetData(target);
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
- const messageClasses = ['text-muted', 'text-danger', 'text-success'];
+ const messageClasses = ['gl-text-gray-600', 'gl-text-red-500', 'gl-text-green-500'];
inputClasses.forEach((cssClass) => input.classList.remove(cssClass));
messageClasses.forEach((cssClass) => message.classList.remove(cssClass));
@@ -462,10 +462,10 @@ export default class CreateMergeRequestDropdown {
setUnavailableButtonState(isLoading = true) {
if (isLoading) {
- this.unavailableButtonSpinner.classList.remove('hide');
+ this.unavailableButtonSpinner.classList.remove('gl-display-none');
this.unavailableButtonText.textContent = __('Checking branch availability...');
} else {
- this.unavailableButtonSpinner.classList.add('hide');
+ this.unavailableButtonSpinner.classList.add('gl-display-none');
this.unavailableButtonText.textContent = __('New branch unavailable');
}
}
@@ -476,7 +476,7 @@ export default class CreateMergeRequestDropdown {
this.removeMessage(target);
input.classList.add('gl-field-success-outline');
- message.classList.add('text-success');
+ message.classList.add('gl-text-green-500');
message.textContent = sprintf(__('%{text} is available'), { text });
message.style.display = 'inline-block';
}
@@ -486,7 +486,7 @@ export default class CreateMergeRequestDropdown {
const text = target === 'branch' ? __('branch name') : __('source');
this.removeMessage(target);
- message.classList.add('text-muted');
+ message.classList.add('gl-text-gray-600');
message.textContent = sprintf(__('Checking %{text} availability…'), { text });
message.style.display = 'inline-block';
}
@@ -498,7 +498,7 @@ export default class CreateMergeRequestDropdown {
this.removeMessage(target);
input.classList.add('gl-field-error-outline');
- message.classList.add('text-danger');
+ message.classList.add('gl-text-red-500');
message.textContent = text;
message.style.display = 'inline-block';
}
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index 2ee9ac2a682..bcd729785b3 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -1,6 +1,5 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
-import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
@@ -22,6 +21,7 @@ import MilestoneSelect from '~/milestones/milestone_select';
import initNotesApp from '~/notes';
import { store } from '~/notes/stores';
import ZenMode from '~/zen_mode';
+import initAwardsApp from '~/emoji/awards_app';
import FilteredSearchServiceDesk from './filtered_search_service_desk';
export function initFilteredSearchServiceDesk() {
@@ -72,15 +72,7 @@ export function initShow() {
initRelatedMergeRequests();
initSentryErrorStackTrace();
- const awardEmojiEl = document.getElementById('js-vue-awards-block');
-
- if (awardEmojiEl) {
- import('~/emoji/awards_app')
- .then((m) => m.default(awardEmojiEl))
- .catch(() => {});
- } else {
- loadAwardsHandler();
- }
+ initAwardsApp(document.getElementById('js-vue-awards-block'));
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then((module) => module.default())
diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
index aece7372182..1139861ae78 100644
--- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
@@ -1,11 +1,13 @@
<script>
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { IssuableStatus } from '~/issues/constants';
import {
dateInWords,
getTimeRemainingInWords,
isInFuture,
isInPast,
isToday,
+ newDateAsLocaleTime,
} from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
@@ -27,7 +29,7 @@ export default {
milestoneDate() {
if (this.issue.milestone?.dueDate) {
const { dueDate, startDate } = this.issue.milestone;
- const date = dateInWords(new Date(dueDate), true);
+ const date = dateInWords(newDateAsLocaleTime(dueDate), true);
const remainingTime = this.milestoneRemainingTime(dueDate, startDate);
return `${date} (${remainingTime})`;
}
@@ -37,10 +39,13 @@ export default {
return this.issue.milestone.webPath || this.issue.milestone.webUrl;
},
dueDate() {
- return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
+ return this.issue.dueDate && dateInWords(newDateAsLocaleTime(this.issue.dueDate), true);
},
showDueDateInRed() {
- return isInPast(new Date(this.issue.dueDate)) && !this.issue.closedAt;
+ return (
+ isInPast(newDateAsLocaleTime(this.issue.dueDate)) &&
+ this.issue.state !== IssuableStatus.Closed
+ );
},
timeEstimate() {
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
@@ -48,8 +53,8 @@ export default {
},
methods: {
milestoneRemainingTime(dueDate, startDate) {
- const due = new Date(dueDate);
- const start = new Date(startDate);
+ const due = newDateAsLocaleTime(dueDate);
+ const start = newDateAsLocaleTime(startDate);
if (dueDate && isInPast(due)) {
return __('Past due');
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index a532fa5b771..a43aed6c521 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -19,6 +19,7 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ITEM_TYPE } from '~/groups/constants';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
+import { IssuableStatus } from '~/issues/constants';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
@@ -260,6 +261,9 @@ export default {
showCsvButtons() {
return this.isProject && this.isSignedIn;
},
+ showIssuableByEmail() {
+ return this.initialEmail && this.isSignedIn;
+ },
showNewIssueDropdown() {
return !this.isProject && this.hasAnyProjects;
},
@@ -477,10 +481,10 @@ export default {
return `${this.exportCsvPath}${window.location.search}`;
},
getStatus(issue) {
- if (issue.closedAt && issue.moved) {
+ if (issue.state === IssuableStatus.Closed && issue.moved) {
return this.$options.i18n.closedMoved;
}
- if (issue.closedAt) {
+ if (issue.state === IssuableStatus.Closed) {
return this.$options.i18n.closed;
}
return undefined;
@@ -624,8 +628,9 @@ export default {
</script>
<template>
- <div v-if="hasAnyIssues">
+ <div>
<issuable-list
+ v-if="hasAnyIssues"
:namespace="fullPath"
recent-searches-storage-key="issues"
:search-input-placeholder="$options.i18n.searchPlaceholder"
@@ -768,50 +773,50 @@ export default {
</template>
</issuable-list>
- <issuable-by-email v-if="initialEmail" class="gl-text-center gl-pt-5 gl-pb-7" />
- </div>
+ <template v-else-if="isSignedIn">
+ <gl-empty-state
+ :description="$options.i18n.noIssuesSignedInDescription"
+ :title="$options.i18n.noIssuesSignedInTitle"
+ :svg-path="emptyStateSvgPath"
+ >
+ <template #actions>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ <csv-import-export-buttons
+ v-if="showCsvButtons"
+ class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
+ :export-csv-path="exportCsvPathWithQuery"
+ :issuable-count="currentTabCount"
+ />
+ <new-issue-dropdown v-if="showNewIssueDropdown" />
+ </template>
+ </gl-empty-state>
+ <hr />
+ <p class="gl-text-center gl-font-weight-bold gl-mb-0">
+ {{ $options.i18n.jiraIntegrationTitle }}
+ </p>
+ <p class="gl-text-center gl-mb-0">
+ <gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
+ <template #jiraDocsLink="{ content }">
+ <gl-link :href="jiraIntegrationPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p class="gl-text-center gl-text-gray-500">
+ {{ $options.i18n.jiraIntegrationSecondaryMessage }}
+ </p>
+ </template>
- <div v-else-if="isSignedIn">
<gl-empty-state
- :description="$options.i18n.noIssuesSignedInDescription"
- :title="$options.i18n.noIssuesSignedInTitle"
+ v-else
+ :description="$options.i18n.noIssuesSignedOutDescription"
+ :title="$options.i18n.noIssuesSignedOutTitle"
:svg-path="emptyStateSvgPath"
- >
- <template #actions>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- <csv-import-export-buttons
- v-if="showCsvButtons"
- class="gl-mr-3"
- :export-csv-path="exportCsvPathWithQuery"
- :issuable-count="currentTabCount"
- />
- <new-issue-dropdown v-if="showNewIssueDropdown" />
- </template>
- </gl-empty-state>
- <hr />
- <p class="gl-text-center gl-font-weight-bold gl-mb-0">
- {{ $options.i18n.jiraIntegrationTitle }}
- </p>
- <p class="gl-text-center gl-mb-0">
- <gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
- <template #jiraDocsLink="{ content }">
- <gl-link :href="jiraIntegrationPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <p class="gl-text-center gl-text-gray-500">
- {{ $options.i18n.jiraIntegrationSecondaryMessage }}
- </p>
- </div>
+ :primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
+ :primary-button-link="signInPath"
+ />
- <gl-empty-state
- v-else
- :description="$options.i18n.noIssuesSignedOutDescription"
- :title="$options.i18n.noIssuesSignedOutTitle"
- :svg-path="emptyStateSvgPath"
- :primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
- :primary-button-link="signInPath"
- />
+ <issuable-by-email v-if="showIssuableByEmail" class="gl-text-center gl-pt-5 gl-pb-7" />
+ </div>
</template>
diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
index 529262d2162..ec24ea7c56a 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -1,4 +1,4 @@
-#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "./issue.fragment.graphql"
query getIssues(
diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index 430d494deab..d09e4d9df2b 100644
--- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
@@ -2,7 +2,6 @@ fragment IssueFragment on Issue {
__typename
id
iid
- closedAt
confidential
createdAt
downvotes
@@ -11,6 +10,7 @@ fragment IssueFragment on Issue {
humanTimeEstimate
mergeRequestsCount
moved
+ state
title
updatedAt
upvotes
diff --git a/app/assets/javascripts/issues/manual_ordering.js b/app/assets/javascripts/issues/manual_ordering.js
index 8fb891f62f7..bc1cffef943 100644
--- a/app/assets/javascripts/issues/manual_ordering.js
+++ b/app/assets/javascripts/issues/manual_ordering.js
@@ -1,11 +1,8 @@
import Sortable from 'sortablejs';
-import {
- getBoardSortableDefaultOptions,
- sortableStart,
-} from '~/boards/mixins/sortable_default_options';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
+import { getSortableDefaultOptions, sortableStart } from '~/sortable/utils';
const updateIssue = (url, { move_before_id, move_after_id }) =>
axios
@@ -28,7 +25,7 @@ const initManualOrdering = () => {
Sortable.create(
issueList,
- getBoardSortableDefaultOptions({
+ getSortableDefaultOptions({
scroll: true,
fallbackTolerance: 1,
dataIdAttr: 'data-id',
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');
},
},