summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/issues/show/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issues/show/components')
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue23
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue297
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue20
-rw-r--r--app/assets/javascripts/issues/show/components/fields/type.vue55
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue15
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js12
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue10
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql6
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue33
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue30
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue20
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_tags_popover.vue42
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/utils.js8
-rw-r--r--app/assets/javascripts/issues/show/components/task_list_item_actions.vue47
18 files changed, 421 insertions, 214 deletions
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index e5428f87095..decb559ee81 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -3,10 +3,11 @@ import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gi
import Visibility from 'visibilityjs';
import { createAlert } from '~/flash';
import {
- IssuableStatus,
IssuableStatusText,
+ STATUS_CLOSED,
+ TYPE_EPIC,
+ TYPE_ISSUE,
WorkspaceType,
- IssuableType,
} from '~/issues/constants';
import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -156,7 +157,7 @@ export default {
issuableType: {
type: String,
required: false,
- default: IssuableType.Issue,
+ default: TYPE_ISSUE,
},
canAttachFile: {
type: Boolean,
@@ -190,6 +191,11 @@ export default {
required: false,
default: null,
},
+ issueIid: {
+ type: Number,
+ required: false,
+ default: null,
+ },
},
data() {
const store = new Store({
@@ -251,7 +257,7 @@ export default {
return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType });
},
isClosed() {
- return this.issuableStatus === IssuableStatus.Closed;
+ return this.issuableStatus === STATUS_CLOSED;
},
pinnedLinkClasses() {
return this.showTitleBorder
@@ -259,7 +265,7 @@ export default {
: '';
},
statusIcon() {
- if (this.issuableType === IssuableType.Issue) {
+ if (this.issuableType === TYPE_ISSUE) {
return this.isClosed ? 'issue-closed' : 'issues';
}
return this.isClosed ? 'epic-closed' : 'epic';
@@ -271,7 +277,7 @@ export default {
return IssuableStatusText[this.issuableStatus];
},
shouldShowStickyHeader() {
- return [IssuableType.Issue, IssuableType.Epic].includes(this.issuableType);
+ return [TYPE_ISSUE, TYPE_EPIC].includes(this.issuableType);
},
},
created() {
@@ -453,7 +459,7 @@ export default {
}
},
- handleListItemReorder(description) {
+ handleSaveDescription(description) {
this.updateFormState();
this.setFormState({ description });
this.updateIssuable();
@@ -564,6 +570,7 @@ export default {
<component
:is="descriptionComponent"
:issue-id="issueId"
+ :issue-iid="issueIid"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
@@ -573,7 +580,7 @@ export default {
:update-url="updateEndpoint"
:lock-version="state.lock_version"
:is-updating="formState.updateLoading"
- @listItemReorder="handleListItemReorder"
+ @saveDescription="handleSaveDescription"
@taskListUpdateStarted="taskListUpdateStarted"
@taskListUpdateSucceeded="taskListUpdateSucceeded"
@taskListUpdateFailed="taskListUpdateFailed"
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 78e729b97da..188a6f6b15e 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -1,13 +1,15 @@
<script>
-import { GlToast, GlTooltip, GlModalDirective } from '@gitlab/ui';
+import { GlModalDirective, GlToast } from '@gitlab/ui';
import $ from 'jquery';
+import { uniqueId } from 'lodash';
import Sortable from 'sortablejs';
import Vue from 'vue';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
+import { TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
import { createAlert } from '~/flash';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import { isMetaKey } from '~/lib/utils/common_utils';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -15,22 +17,30 @@ import { __, s__, sprintf } from '~/locale';
import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';
import TaskList from '~/task_list';
import Tracking from '~/tracking';
+import addHierarchyChildMutation from '~/work_items/graphql/add_hierarchy_child.mutation.graphql';
+import removeHierarchyChildMutation from '~/work_items/graphql/remove_hierarchy_child.mutation.graphql';
+import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
+import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
-import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
-
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_CREATING,
+ I18N_WORK_ITEM_ERROR_DELETING,
TRACKING_CATEGORY_SHOW,
TASK_TYPE_NAME,
- WIDGET_TYPE_DESCRIPTION,
} from '~/work_items/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import eventHub from '../event_hub';
import animateMixin from '../mixins/animate';
-import { convertDescriptionWithNewSort } from '../utils';
+import {
+ deleteTaskListItem,
+ convertDescriptionWithNewSort,
+ extractTaskTitleAndDescription,
+} from '../utils';
+import TaskListItemActions from './task_list_item_actions.vue';
Vue.use(GlToast);
@@ -44,11 +54,10 @@ export default {
GlModal: GlModalDirective,
},
components: {
- GlTooltip,
WorkItemDetailModal,
},
mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['fullPath', 'hasIterationsFeature'],
props: {
canUpdate: {
type: Boolean,
@@ -71,7 +80,7 @@ export default {
issuableType: {
type: String,
required: false,
- default: IssuableType.Issue,
+ default: TYPE_ISSUE,
},
updateUrl: {
type: String,
@@ -88,6 +97,11 @@ export default {
required: false,
default: null,
},
+ issueIid: {
+ type: Number,
+ required: false,
+ default: null,
+ },
isUpdating: {
type: Boolean,
required: false,
@@ -98,18 +112,29 @@ export default {
const workItemId = getParameterByName('work_item_id');
return {
+ hasTaskListItemActions: false,
preAnimation: false,
pulseAnimation: false,
initialUpdate: true,
- taskButtons: [],
+ issueDetails: {},
activeTask: {},
workItemId: isPositiveInteger(workItemId)
- ? convertToGraphQLId(TYPE_WORK_ITEM, workItemId)
+ ? convertToGraphQLId(TYPENAME_WORK_ITEM, workItemId)
: undefined,
workItemTypes: [],
};
},
apollo: {
+ issueDetails: {
+ query: getIssueDetailsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: String(this.issueIid),
+ };
+ },
+ update: (data) => data.workspace?.issuable,
+ },
workItem: {
query: workItemQuery,
variables() {
@@ -118,7 +143,7 @@ export default {
};
},
skip() {
- return !this.workItemId || !this.workItemsEnabled;
+ return !this.workItemId || !this.workItemsMvcEnabled;
},
},
workItemTypes: {
@@ -132,19 +157,19 @@ export default {
return data.workspace?.workItemTypes?.nodes;
},
skip() {
- return !this.workItemsEnabled;
+ return !this.workItemsMvcEnabled;
},
},
},
computed: {
- workItemsEnabled() {
- return this.glFeatures.workItemsCreateFromMarkdown;
+ workItemsMvcEnabled() {
+ return this.glFeatures.workItemsMvc;
},
taskWorkItemType() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
},
issueGid() {
- return this.issueId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issueId) : null;
+ return this.issueId ? convertToGraphQLId(TYPENAME_WORK_ITEM, this.issueId) : null;
},
},
watch: {
@@ -164,10 +189,13 @@ export default {
},
},
mounted() {
+ eventHub.$on('convert-task-list-item', this.convertTaskListItem);
+ eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
+
this.renderGFM();
this.updateTaskStatusText();
- if (this.workItemId && this.workItemsEnabled) {
+ if (this.workItemId && this.workItemsMvcEnabled) {
const taskLink = this.$el.querySelector(
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
);
@@ -175,6 +203,9 @@ export default {
}
},
beforeDestroy() {
+ eventHub.$off('convert-task-list-item', this.convertTaskListItem);
+ eventHub.$off('delete-task-list-item', this.deleteTaskListItem);
+
this.removeAllPointerEventListeners();
},
methods: {
@@ -197,8 +228,8 @@ export default {
this.renderSortableLists();
- if (this.workItemsEnabled) {
- this.renderTaskActions();
+ if (this.workItemsMvcEnabled) {
+ this.renderTaskListItemActions();
}
}
},
@@ -223,7 +254,7 @@ export default {
handle: '.drag-icon',
onUpdate: (event) => {
const description = convertDescriptionWithNewSort(this.descriptionText, event.to);
- this.$emit('listItemReorder', description);
+ this.$emit('saveDescription', description);
},
}),
);
@@ -232,29 +263,29 @@ export default {
createDragIconElement() {
const container = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
- container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true">
- <use href="${gon.sprite_icons}#drag-vertical"></use>
+ container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-opacity-0" role="img" aria-hidden="true">
+ <use href="${gon.sprite_icons}#grip"></use>
</svg>`;
return container.firstChild;
},
- addPointerEventListeners(listItem, iconSelector) {
+ addPointerEventListeners(listItem, elementSelector) {
const pointeroverListener = (event) => {
- const icon = event.target.closest('li').querySelector(iconSelector);
- if (!icon || isDragging() || this.isUpdating) {
+ const element = event.target.closest('li').querySelector(elementSelector);
+ if (!element || isDragging() || this.isUpdating) {
return;
}
- icon.style.visibility = 'visible';
+ element.classList.add('gl-opacity-10');
};
const pointeroutListener = (event) => {
- const icon = event.target.closest('li').querySelector(iconSelector);
- if (!icon) {
+ const element = event.target.closest('li').querySelector(elementSelector);
+ if (!element) {
return;
}
- icon.style.visibility = 'hidden';
+ element.classList.remove('gl-opacity-10');
};
// We use pointerover/pointerout instead of CSS so that when we hover over a
- // list item with children, the drag icons of its children do not become visible.
+ // list item with children, the grip icons of its children do not become visible.
listItem.addEventListener('pointerover', pointeroverListener);
listItem.addEventListener('pointerout', pointeroutListener);
@@ -279,11 +310,9 @@ export default {
taskListUpdateStarted() {
this.$emit('taskListUpdateStarted');
},
-
taskListUpdateSuccess() {
this.$emit('taskListUpdateSucceeded');
},
-
taskListUpdateError() {
createAlert({
message: sprintf(
@@ -298,7 +327,6 @@ export default {
this.$emit('taskListUpdateFailed');
},
-
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
@@ -317,22 +345,42 @@ export default {
$tasksShort.text('');
}
},
- renderTaskActions() {
+ createTaskListItemActions(provide) {
+ const app = new Vue({
+ el: document.createElement('div'),
+ provide,
+ render: (createElement) => createElement(TaskListItemActions),
+ });
+ return app.$el;
+ },
+ convertTaskListItem(sourcepos) {
+ const oldDescription = this.descriptionText;
+ const { newDescription, taskDescription, taskTitle } = deleteTaskListItem(
+ oldDescription,
+ sourcepos,
+ );
+ this.$emit('saveDescription', newDescription);
+ this.createTask({ taskTitle, taskDescription, oldDescription });
+ },
+ deleteTaskListItem(sourcepos) {
+ const { newDescription } = deleteTaskListItem(this.descriptionText, sourcepos);
+ this.$emit('saveDescription', newDescription);
+ },
+ renderTaskListItemActions() {
if (!this.$el?.querySelectorAll) {
return;
}
- this.taskButtons = [];
const taskListFields = this.$el.querySelectorAll('.task-list-item:not(.inapplicable)');
- taskListFields.forEach((item, index) => {
+ taskListFields.forEach((item) => {
const taskLink = item.querySelector('.gfm-issue');
if (taskLink) {
const { issue, referenceType, issueType } = taskLink.dataset;
if (issueType !== workItemTypes.TASK) {
return;
}
- const workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue);
+ const workItemId = convertToGraphQLId(TYPENAME_WORK_ITEM, issue);
this.addHoverListeners(taskLink, workItemId);
taskLink.classList.add('gl-link');
taskLink.addEventListener('click', (e) => {
@@ -351,31 +399,12 @@ export default {
});
return;
}
- this.addPointerEventListeners(item, '.js-add-task');
- const button = document.createElement('button');
- button.classList.add(
- 'btn',
- 'btn-default',
- 'btn-md',
- 'gl-button',
- 'btn-default-tertiary',
- 'gl-visibility-hidden',
- 'gl-p-0!',
- 'gl-mt-n1',
- 'gl-ml-3',
- 'js-add-task',
- );
- button.id = `js-task-button-${index}`;
- this.taskButtons.push(button.id);
- // eslint-disable-next-line no-unsanitized/property
- button.innerHTML = `
- <svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
- <use href="${gon.sprite_icons}#doc-new"></use>
- </svg>
- `;
- button.setAttribute('aria-label', s__('WorkItem|Create task'));
- button.addEventListener('click', () => this.handleCreateTask(button));
- this.insertButtonNextToTaskText(item, button);
+
+ const toggleClass = uniqueId('task-list-item-actions-');
+ const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate, toggleClass });
+ this.addPointerEventListeners(item, `.${toggleClass}`);
+ this.insertNextToTaskListItemText(dropdown, item);
+ this.hasTaskListItemActions = true;
});
},
addHoverListeners(taskLink, id) {
@@ -391,19 +420,20 @@ export default {
}
});
},
- insertButtonNextToTaskText(listItem, button) {
- const paragraph = Array.from(listItem.children).find((element) => element.tagName === 'P');
- const lastChild = listItem.lastElementChild;
+ insertNextToTaskListItemText(element, listItem) {
+ const children = Array.from(listItem.children);
+ const paragraph = children.find((el) => el.tagName === 'P');
+ const list = children.find((el) => el.classList.contains('task-list'));
if (paragraph) {
// If there's a `p` element, then it's a multi-paragraph task item
// and the task text exists within the `p` element as the last child
- paragraph.append(button);
- } else if (lastChild.tagName === 'OL' || lastChild.tagName === 'UL') {
+ paragraph.append(element);
+ } else if (list) {
// Otherwise, the task item can have a child list which exists directly after the task text
- lastChild.insertAdjacentElement('beforebegin', button);
+ list.insertAdjacentElement('beforebegin', element);
} else {
// Otherwise, the task item is a simple one where the task text exists as the last child
- listItem.append(button);
+ listItem.append(element);
}
},
setActiveTask(el) {
@@ -427,55 +457,90 @@ export default {
this.workItemId = undefined;
this.updateWorkItemIdUrlQuery(undefined);
},
- async handleCreateTask(el) {
- this.setActiveTask(el);
+ async createTask({ taskTitle, taskDescription, oldDescription }) {
try {
- const { data } = await this.$apollo.mutate({
- mutation: createWorkItemFromTaskMutation,
- variables: {
- input: {
- id: this.issueGid,
- workItemData: {
- lockVersion: this.lockVersion,
- title: this.activeTask.title,
- lineNumberStart: Number(this.activeTask.lineNumberStart),
- lineNumberEnd: Number(this.activeTask.lineNumberEnd),
- workItemTypeId: this.taskWorkItemType,
- },
- },
+ const { title, description } = extractTaskTitleAndDescription(taskTitle, taskDescription);
+ const iterationInput = {
+ iterationWidget: {
+ iterationId: this.issueDetails.iteration?.id ?? null,
},
- update(store, { data: { workItemCreateFromTask } }) {
- const { newWorkItem } = workItemCreateFromTask;
-
- store.writeQuery({
- query: workItemQuery,
- variables: {
- id: newWorkItem.id,
- },
- data: {
- workItem: newWorkItem,
- },
- });
+ };
+ const input = {
+ confidential: this.issueDetails.confidential,
+ description,
+ hierarchyWidget: {
+ parentId: this.issueGid,
+ },
+ ...(this.hasIterationsFeature && iterationInput),
+ milestoneWidget: {
+ milestoneId: this.issueDetails.milestone?.id ?? null,
},
+ projectPath: this.fullPath,
+ title,
+ workItemTypeId: this.taskWorkItemType,
+ };
+
+ const { data } = await this.$apollo.mutate({
+ mutation: createWorkItemMutation,
+ variables: { input },
});
- const { workItem, newWorkItem } = data.workItemCreateFromTask;
+ const { workItem, errors } = data.workItemCreate;
+
+ if (errors?.length) {
+ throw new Error(errors);
+ }
- const updatedDescription = workItem?.widgets?.find(
- (widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
- )?.descriptionHtml;
+ await this.$apollo.mutate({
+ mutation: addHierarchyChildMutation,
+ variables: { id: this.issueGid, workItem },
+ });
- this.$emit('updateDescription', updatedDescription);
- this.workItemId = newWorkItem.id;
- this.openWorkItemDetailModal(el);
+ this.$toast.show(s__('WorkItem|Converted to task'), {
+ action: {
+ text: s__('WorkItem|Undo'),
+ onClick: (_, toast) => {
+ this.undoCreateTask(oldDescription, workItem.id);
+ toast.hide();
+ },
+ },
+ });
} catch (error) {
- createAlert({
- message: sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemTypes.TASK),
- error,
- captureError: true,
+ this.showAlert(I18N_WORK_ITEM_ERROR_CREATING, error);
+ }
+ },
+ async undoCreateTask(oldDescription, id) {
+ this.$emit('saveDescription', oldDescription);
+
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: deleteWorkItemMutation,
+ variables: { input: { id } },
+ });
+
+ const { errors } = data.workItemDelete;
+
+ if (errors?.length) {
+ throw new Error(errors);
+ }
+
+ await this.$apollo.mutate({
+ mutation: removeHierarchyChildMutation,
+ variables: { id: this.issueGid, workItem: { id } },
});
+
+ this.$toast.show(s__('WorkItem|Task reverted'));
+ } catch (error) {
+ this.showAlert(I18N_WORK_ITEM_ERROR_DELETING, error);
}
},
+ showAlert(message, error) {
+ createAlert({
+ message: sprintfWorkItem(message, workItemTypes.TASK),
+ error,
+ captureError: true,
+ });
+ },
handleDeleteTask(description) {
this.$emit('updateDescription', description);
this.$toast.show(s__('WorkItem|Task deleted'));
@@ -492,14 +557,7 @@ export default {
</script>
<template>
- <div
- v-if="descriptionHtml"
- :class="{
- 'js-task-list-container': canUpdate,
- 'work-items-enabled': workItemsEnabled,
- }"
- class="description"
- >
+ <div v-if="descriptionHtml" :class="{ 'js-task-list-container': canUpdate }" class="description">
<div
ref="gfm-content"
v-safe-html:[$options.safeHtmlConfig]="descriptionHtml"
@@ -507,10 +565,10 @@ export default {
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
+ 'has-task-list-item-actions': hasTaskListItemActions,
}"
class="md"
></div>
-
<textarea
v-if="descriptionText"
:value="descriptionText"
@@ -531,10 +589,5 @@ export default {
@workItemDeleted="handleDeleteTask"
@close="closeWorkItemDetailModal"
/>
- <template v-if="workItemsEnabled">
- <gl-tooltip v-for="item in taskButtons" :key="item" :target="item">
- {{ s__('WorkItem|Create task') }}
- </gl-tooltip>
- </template>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 04c5007dbec..3bc24e8ce01 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
@@ -35,6 +36,16 @@ export default {
default: true,
},
},
+ data() {
+ return {
+ formFieldProps: {
+ id: 'issue-description',
+ name: 'issue-description',
+ placeholder: __('Write a comment or drag your files here…'),
+ 'aria-label': __('Description'),
+ },
+ };
+ },
computed: {
quickActionsDocsPath() {
return helpPagePath('user/project/quick_actions');
@@ -60,10 +71,7 @@ export default {
:value="value"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :form-field-aria-label="__('Description')"
- :form-field-placeholder="__('Write a comment or drag your files here…')"
- form-field-id="issue-description"
- form-field-name="issue-description"
+ :form-field-props="formFieldProps"
:quick-actions-docs-path="quickActionsDocsPath"
:enable-autocomplete="enableAutocomplete"
supports-quick-actions
@@ -84,15 +92,13 @@ export default {
>
<template #textarea>
<textarea
- id="issue-description"
+ v-bind="formFieldProps"
ref="textarea"
:value="value"
class="note-textarea js-gfm-input js-autosize markdown-area"
data-qa-selector="description_field"
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"
diff --git a/app/assets/javascripts/issues/show/components/fields/type.vue b/app/assets/javascripts/issues/show/components/fields/type.vue
index 5695efd7114..5ade1a86d30 100644
--- a/app/assets/javascripts/issues/show/components/fields/type.vue
+++ b/app/assets/javascripts/issues/show/components/fields/type.vue
@@ -1,6 +1,6 @@
<script>
-import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { capitalize } from 'lodash';
+import { GlFormGroup, GlIcon, GlListbox } from '@gitlab/ui';
+import { TYPE_ISSUE } from '~/issues/constants';
import { __ } from '~/locale';
import { issuableTypes, INCIDENT_TYPE } from '../../constants';
import getIssueStateQuery from '../../queries/get_issue_state.query.graphql';
@@ -16,34 +16,35 @@ export default {
components: {
GlFormGroup,
GlIcon,
- GlDropdown,
- GlDropdownItem,
+ GlListbox,
},
inject: {
canCreateIncident: {
default: false,
},
issueType: {
- default: 'issue',
+ default: TYPE_ISSUE,
},
},
data() {
return {
issueState: {},
+ selectedIssueType: '',
};
},
apollo: {
issueState: {
query: getIssueStateQuery,
+ result({
+ data: {
+ issueState: { issueType },
+ },
+ }) {
+ this.selectedIssueType = issueType;
+ },
},
},
computed: {
- dropdownText() {
- const {
- issueState: { issueType },
- } = this;
- return issuableTypes.find((type) => type.value === issueType)?.text || capitalize(issueType);
- },
shouldShowIncident() {
return this.issueType === INCIDENT_TYPE || this.canCreateIncident;
},
@@ -72,25 +73,21 @@ export default {
label-for="issuable-type"
class="mb-2 mb-md-0"
>
- <gl-dropdown
- id="issuable-type"
- :aria-labelledby="$options.i18n.label"
- :text="dropdownText"
+ <gl-listbox
+ v-model="selectedIssueType"
+ toggle-class="gl-mb-0"
+ :items="$options.issuableTypes"
:header-text="$options.i18n.label"
- class="gl-w-full"
- toggle-class="dropdown-menu-toggle"
+ :list-aria-labelled-by="$options.i18n.label"
+ block
+ @select="updateIssueType"
>
- <gl-dropdown-item
- v-for="type in $options.issuableTypes"
- v-show="isShown(type)"
- :key="type.value"
- :is-checked="issueState.issueType === type.value"
- is-check-item
- @click="updateIssueType(type.value)"
- >
- <gl-icon :name="type.icon" />
- {{ type.text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item="{ item }">
+ <span v-show="isShown(item)" data-testid="issue-type-list-item">
+ <gl-icon :name="item.icon" />
+ {{ item.text }}
+ </span>
+ </template>
+ </gl-listbox>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index b56c91d7983..bcea9cf57a7 100644
--- a/app/assets/javascripts/issues/show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { getDraft, updateDraft, getLockVersion, clearDraft } from '~/lib/utils/autosave';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import eventHub from '../event_hub';
import EditActions from './edit_actions.vue';
import DescriptionField from './fields/description.vue';
@@ -98,7 +98,7 @@ export default {
return this.formState.lockedWarningVisible && !this.formState.updateLoading;
},
isIssueType() {
- return this.issuableType === IssuableType.Issue;
+ return this.issuableType === TYPE_ISSUE;
},
},
watch: {
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 56e360c75e3..9d92b5cf954 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -12,7 +12,7 @@ import {
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { IssuableStatus, IssueType } from '~/issues/constants';
+import { IssueType, STATUS_CLOSED } from '~/issues/constants';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -98,6 +98,12 @@ export default {
submitAsSpamPath: {
default: '',
},
+ reportedUserId: {
+ default: '',
+ },
+ reportedFromUrl: {
+ default: '',
+ },
},
data() {
return {
@@ -108,7 +114,7 @@ export default {
...mapState(['isToggleStateButtonLoading']),
...mapGetters(['openState', 'getBlockedByIssues']),
isClosed() {
- return this.openState === IssuableStatus.Closed;
+ return this.openState === STATUS_CLOSED;
},
issueTypeText() {
const issueTypeTexts = {
@@ -368,7 +374,12 @@ export default {
:title="deleteButtonText"
/>
+ <!-- IMPORTANT: show this component lazily because it causes layout thrashing -->
+ <!-- https://gitlab.com/gitlab-org/gitlab/-/issues/331172#note_1269378396 -->
<abuse-category-selector
+ v-if="isReportAbuseDrawerOpen"
+ :reported-user-id="reportedUserId"
+ :reported-from-url="reportedFromUrl"
:show-drawer="isReportAbuseDrawerOpen"
@close-drawer="toggleReportAbuseDrawer(false)"
/>
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index 2fdae538902..c0aadf9c14e 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -47,9 +47,21 @@ export const timelineItemI18n = Object.freeze({
export const timelineEventTagsI18n = Object.freeze({
startTime: __('Start time'),
+ impactDetected: __('Impact detected'),
+ responseInitiated: __('Response initiated'),
+ impactMitigated: __('Impact mitigated'),
+ causeIdentified: __('Cause identified'),
endTime: __('End time'),
});
+export const timelineEventTagsPopover = Object.freeze({
+ title: s__('Incident|Event tag'),
+ message: s__(
+ 'Incident|Adding an event tag associates the timeline comment with specific incident metrics.',
+ ),
+ link: __('Learn more'),
+});
+
export const MAX_TEXT_LENGTH = 280;
export const TIMELINE_EVENT_TAGS = Object.values(timelineEventTagsI18n).map((item) => ({
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 81111d42b39..40cb7fbb0ff 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
@@ -5,7 +5,7 @@ import { GlIcon } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { createAlert } from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_ISSUE } from '~/graphql_shared/constants';
+import { TYPENAME_ISSUE } from '~/graphql_shared/constants';
import { timelineFormI18n } from './constants';
import TimelineEventsForm from './timeline_events_form.vue';
@@ -41,7 +41,7 @@ export default {
}
const variables = {
- incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
+ incidentId: convertToGraphQLId(TYPENAME_ISSUE, this.issuableId),
fullPath: this.fullPath,
};
@@ -71,7 +71,7 @@ export default {
mutation: CreateTimelineEvent,
variables: {
input: {
- incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
+ incidentId: convertToGraphQLId(TYPENAME_ISSUE, this.issuableId),
note: eventDetails.note,
occurredAt: eventDetails.occurredAt,
timelineEventTagNames: eventDetails.timelineEventTags,
@@ -113,13 +113,13 @@ export default {
>
<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-mr-3 gl-w-8 gl-h-8 gl-z-index-1"
+ 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-z-index-1"
>
<gl-icon name="comment" class="note-icon" />
</div>
<timeline-events-form
ref="eventForm"
- :class="{ 'gl-border-gray-50 gl-border-t': hasTimelineEvents }"
+ :class="{ 'gl-border-gray-50 gl-border-t gl-pt-3': hasTimelineEvents }"
:is-event-processed="createTimelineEventActive"
show-save-and-add
@save-event="createIncidentTimelineEvent"
diff --git a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
index 4ef9b9c5a99..c2fb8b6f683 100644
--- a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
@@ -28,9 +28,9 @@ export default {
</script>
<template>
- <div class="gl-relative gl-display-flex gl-align-items-center">
+ <div class="edit-timeline-event gl-relative gl-display-flex gl-align-items-center">
<div
- 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-mr-3 gl-w-8 gl-h-8 gl-z-index-1"
+ 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-z-index-1"
>
<gl-icon name="comment" class="note-icon" />
</div>
@@ -40,6 +40,7 @@ export default {
:is-event-processed="editTimelineEventActive"
:previous-occurred-at="event.occurredAt"
:previous-note="event.note"
+ :previous-tags="event.timelineEventTags.nodes"
is-editing
@save-event="saveEvent"
@cancel="$emit('hide-edit')"
diff --git a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql
index 54f036268cc..77f955c08dc 100644
--- a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql
@@ -7,6 +7,12 @@ mutation UpdateTimelineEvent($input: TimelineEventUpdateInput!) {
action
occurredAt
createdAt
+ timelineEventTags {
+ nodes {
+ id
+ name
+ }
+ }
}
errors
}
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 53956fcb4b2..997fadec602 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -125,8 +125,8 @@ export default {
item.classList.toggle('gl-display-none', !isSummaryTab);
});
- editButton.classList.toggle('gl-display-none', !isSummaryTab);
- editButton.classList.toggle('gl-sm-display-inline-flex!', isSummaryTab);
+ editButton?.classList.toggle('gl-display-none', !isSummaryTab);
+ editButton?.classList.toggle('gl-sm-display-inline-flex!', isSummaryTab);
}
},
},
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index 6648e20865d..7944362a40f 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -1,10 +1,11 @@
<script>
-import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlListbox } from '@gitlab/ui';
+import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlCollapsibleListbox } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, sprintf } from '~/locale';
+import TimelineEventsTagsPopover from './timeline_events_tags_popover.vue';
import { MAX_TEXT_LENGTH, TIMELINE_EVENT_TAGS, timelineFormI18n } from './constants';
-import { getUtcShiftedDate } from './utils';
+import { getUtcShiftedDate, getPreviousEventTags } from './utils';
export default {
name: 'TimelineEventsForm',
@@ -21,11 +22,12 @@ export default {
],
components: {
MarkdownField,
+ TimelineEventsTagsPopover,
GlDatepicker,
GlFormInput,
GlFormGroup,
GlButton,
- GlListbox,
+ GlCollapsibleListbox,
},
mixins: [glFeatureFlagsMixin()],
i18n: timelineFormI18n,
@@ -77,7 +79,7 @@ export default {
hourPickerInput: placeholderDate.getHours(),
minutePickerInput: placeholderDate.getMinutes(),
datePickerInput: placeholderDate,
- selectedTags: [...this.previousTags],
+ selectedTags: getPreviousEventTags(this.previousTags),
};
},
computed: {
@@ -101,19 +103,19 @@ export default {
timelineTextCount() {
return this.timelineText.length;
},
- dropdownText() {
+ listboxText() {
if (!this.selectedTags.length) {
return timelineFormI18n.selectTags;
}
- const dropdownText =
+ const listboxText =
this.selectedTags.length === 1
? this.selectedTags[0]
: sprintf(__('%{numberOfSelectedTags} tags'), {
numberOfSelectedTags: this.selectedTags.length,
});
- return dropdownText;
+ return listboxText;
},
},
mounted() {
@@ -164,11 +166,11 @@ export default {
<template>
<form class="gl-flex-grow-1 gl-border-gray-50">
- <div class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row">
- <gl-form-group :label="__('Date')" class="gl-mt-5 gl-mr-5">
+ <div class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-mt-3">
+ <gl-form-group :label="__('Date')" class="gl-mr-5">
<gl-datepicker id="incident-date" ref="datepicker" v-model="datePickerInput" />
</gl-form-group>
- <div class="gl-display-flex gl-mt-5">
+ <div class="gl-display-flex">
<gl-form-group :label="__('Time')">
<div class="gl-display-flex">
<label label-for="timeline-input-hours" class="sr-only"></label>
@@ -197,10 +199,15 @@ export default {
<p class="gl-ml-3 gl-align-self-end gl-line-height-32">{{ __('UTC') }}</p>
</div>
</div>
- <gl-form-group v-if="glFeatures.incidentEventTags" :label="$options.i18n.tagsLabel">
- <gl-listbox
+ <gl-form-group v-if="glFeatures.incidentEventTags">
+ <label class="gl-display-flex gl-align-items-center gl-gap-3" for="timeline-input-tags">
+ {{ $options.i18n.tagsLabel }}
+ <timeline-events-tags-popover />
+ </label>
+ <gl-collapsible-listbox
+ id="timeline-input-tags"
:selected="selectedTags"
- :toggle-text="dropdownText"
+ :toggle-text="listboxText"
:items="tags"
:is-check-centered="true"
:multiple="true"
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
index 90ee4351e39..d33f3146d64 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
@@ -32,16 +32,19 @@ export default {
type: String,
required: true,
},
- eventTag: {
- type: String,
+ eventTags: {
+ type: Array,
required: false,
- default: null,
+ default: () => [],
},
},
computed: {
time() {
return formatDate(this.occurredAt, 'HH:MM', true);
},
+ canEditEvent() {
+ return this.action === 'comment';
+ },
},
methods: {
getEventIcon,
@@ -51,19 +54,24 @@ export default {
<template>
<div class="timeline-event gl-display-grid">
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-mr-3 gl-w-8 gl-h-8 gl-p-3 gl-z-index-1"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center 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-p-3 gl-z-index-1"
>
<gl-icon :name="getEventIcon(action)" class="note-icon" />
</div>
<div class="timeline-event-note timeline-event-border" data-testid="event-text-container">
- <div class="gl-display-flex gl-align-items-center gl-mb-3">
- <strong class="gl-font-lg" data-testid="event-time">
+ <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-gap-3 gl-mb-2">
+ <h3
+ class="timeline-event-note-date gl-font-weight-bold gl-font-sm gl-my-0"
+ data-testid="event-time"
+ >
<gl-sprintf :message="$options.i18n.timeUTC">
- <template #time>{{ time }}</template>
+ <template #time>
+ <span class="gl-font-lg">{{ time }}</span>
+ </template>
</gl-sprintf>
- </strong>
- <gl-badge v-if="eventTag" variant="muted" icon="tag" class="gl-ml-3">
- {{ eventTag }}
+ </h3>
+ <gl-badge v-for="tag in eventTags" :key="tag.key" variant="muted" icon="tag">
+ {{ tag.name }}
</gl-badge>
</div>
<div v-safe-html="noteHtml" class="md"></div>
@@ -78,7 +86,7 @@ export default {
category="tertiary"
no-caret
>
- <gl-dropdown-item @click="$emit('edit')">
+ <gl-dropdown-item v-if="canEditEvent" @click="$emit('edit')">
{{ $options.i18n.edit }}
</gl-dropdown-item>
<gl-dropdown-item @click="$emit('delete')">
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
index c6b93201c97..10b80529a66 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
@@ -50,9 +50,6 @@ export default {
},
},
methods: {
- getFirstTag(eventTag) {
- return eventTag.nodes?.[0]?.name;
- },
handleEditSelection(event) {
this.eventToEdit = event.id;
this.$emit('hide-new-incident-timeline-event-form');
@@ -105,6 +102,7 @@ export default {
id: eventDetails.id,
note: eventDetails.note,
occurredAt: eventDetails.occurredAt,
+ timelineEventTagNames: eventDetails.timelineEventTags,
},
},
})
@@ -132,21 +130,25 @@ export default {
</script>
<template>
- <div class="issuable-discussion incident-timeline-events">
+ <div class="issuable-discussion incident-timeline-events gl-mt-n3">
<div
v-for="[eventDate, events] in dateGroupedEvents"
:key="eventDate"
data-testid="timeline-group"
class="timeline-group"
>
- <div class="gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid">
- <strong class="gl-font-size-h2" data-testid="event-date">{{ eventDate }}</strong>
- </div>
+ <h2
+ class="gl-font-size-h2 gl-my-0 gl-py-5 gl-border-gray-50 gl-border-1 gl-border-b-solid"
+ data-testid="event-date"
+ >
+ {{ eventDate }}
+ </h2>
+
<ul class="notes main-notes-list">
<li
v-for="(event, eventIndex) in events"
:key="eventIndex"
- class="timeline-entry-vertical-line timeline-entry note system-note note-wrapper gl-my-2! gl-pr-0!"
+ class="timeline-entry-vertical-line timeline-entry note system-note note-wrapper gl-my-0! gl-pr-0!"
>
<edit-timeline-event
v-if="eventToEdit === event.id"
@@ -164,7 +166,7 @@ export default {
:action="event.action"
:occurred-at="event.occurredAt"
:note-html="event.noteHtml"
- :event-tag="getFirstTag(event.timelineEventTags)"
+ :event-tags="event.timelineEventTags.nodes"
@delete="handleDelete(event)"
@edit="handleEditSelection(event)"
/>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
index c8237766505..cb18d34b70b 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_ISSUE } from '~/graphql_shared/constants';
+import { TYPENAME_ISSUE } from '~/graphql_shared/constants';
import { fetchPolicies } from '~/lib/graphql';
import notesEventHub from '~/notes/event_hub';
import getTimelineEvents from './graphql/queries/get_timeline_events.query.graphql';
@@ -33,7 +33,7 @@ export default {
variables() {
return {
fullPath: this.fullPath,
- incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
+ incidentId: convertToGraphQLId(TYPENAME_ISSUE, this.issuableId),
};
},
update(data) {
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tags_popover.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tags_popover.vue
new file mode 100644
index 00000000000..772a16e9ba2
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tags_popover.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlIcon, GlPopover, GlLink } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { timelineEventTagsPopover } from './constants';
+
+export default {
+ name: 'TimelineEventsTagsPopover',
+ components: {
+ GlIcon,
+ GlPopover,
+ GlLink,
+ },
+ i18n: timelineEventTagsPopover,
+ learnMoreLink: helpPagePath('ee/operations/incident_management/incident_timeline_events', {
+ anchor: 'incident-tags',
+ }),
+};
+</script>
+
+<template>
+ <span>
+ <gl-icon id="timeline-events-tag-question" name="question-o" class="gl-text-blue-600" />
+
+ <gl-popover
+ target="timeline-events-tag-question"
+ triggers="hover focus"
+ placement="top"
+ container="viewport"
+ :title="$options.i18n.title"
+ >
+ <div>
+ <p class="gl-mb-0">
+ {{ $options.i18n.message }}
+ </p>
+ <gl-link target="_blank" class="gl-font-sm" :href="$options.learnMoreLink">{{
+ $options.i18n.link
+ }}</gl-link
+ >.
+ </div>
+ </gl-popover>
+ </span>
+</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/utils.js b/app/assets/javascripts/issues/show/components/incidents/utils.js
index 5a009debd75..ce33e91c3b8 100644
--- a/app/assets/javascripts/issues/show/components/incidents/utils.js
+++ b/app/assets/javascripts/issues/show/components/incidents/utils.js
@@ -32,3 +32,11 @@ export const getUtcShiftedDate = (ISOString = null) => {
return date;
};
+
+/**
+ * Returns an array of previously set event tags
+ * @param {array} timelineEventTagsNodes
+ * @returns {array}
+ */
+export const getPreviousEventTags = (timelineEventTagsNodes = []) =>
+ timelineEventTagsNodes.map(({ name }) => name);
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
new file mode 100644
index 00000000000..d0beb0f39b3
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import eventHub from '../event_hub';
+
+export default {
+ i18n: {
+ convertToTask: s__('WorkItem|Convert to task'),
+ delete: __('Delete'),
+ taskActions: s__('WorkItem|Task actions'),
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ inject: ['canUpdate', 'toggleClass'],
+ methods: {
+ convertToTask() {
+ eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos);
+ },
+ deleteTaskListItem() {
+ eventHub.$emit('delete-task-list-item', this.$el.closest('li').dataset.sourcepos);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ class="task-list-item-actions-wrapper"
+ category="tertiary"
+ icon="ellipsis_v"
+ lazy
+ no-caret
+ right
+ :text="$options.i18n.taskActions"
+ text-sr-only
+ :toggle-class="`task-list-item-actions gl-opacity-0 gl-p-2! ${toggleClass}`"
+ >
+ <gl-dropdown-item v-if="canUpdate" @click="convertToTask">
+ {{ $options.i18n.convertToTask }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="canUpdate" variant="danger" @click="deleteTaskListItem">
+ {{ $options.i18n.delete }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>