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:
Diffstat (limited to 'app/assets/javascripts/issues')
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue52
-rw-r--r--app/assets/javascripts/issues/list/constants.js19
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue27
-rw-r--r--app/assets/javascripts/issues/show/components/delete_issue_modal.vue3
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue10
-rw-r--r--app/assets/javascripts/issues/show/components/edit_actions.vue86
-rw-r--r--app/assets/javascripts/issues/show/components/edited.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue17
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js11
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue31
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue47
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql13
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue242
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue94
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue66
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue27
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/utils.js11
-rw-r--r--app/assets/javascripts/issues/show/graphql.js2
-rw-r--r--app/assets/javascripts/issues/show/index.js2
-rw-r--r--app/assets/javascripts/issues/show/utils/update_description.js1
22 files changed, 428 insertions, 345 deletions
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 11911adb401..0b424d105b9 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -24,6 +24,7 @@ import axios from '~/lib/utils/axios_utils';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
import {
DEFAULT_NONE_ANY,
OPERATOR_IS_ONLY,
@@ -37,6 +38,7 @@ import {
TOKEN_TITLE_ORGANIZATION,
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
+ FILTERED_SEARCH_TERM,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
@@ -462,6 +464,12 @@ export default {
page_before: this.pageParams.beforeCursor ?? undefined,
};
},
+ issuesHelpPagePath() {
+ return helpPagePath('user/project/issues/index');
+ },
+ shouldDisableSomeFilters() {
+ return this.isAnonymousSearchDisabled && !this.isSignedIn;
+ },
},
watch: {
$route(newValue, oldValue) {
@@ -578,13 +586,9 @@ export default {
this.issuesError = null;
},
handleFilter(filter) {
- if (this.isAnonymousSearchDisabled && !this.isSignedIn) {
- this.showAnonymousSearchingMessage();
- return;
- }
+ this.setFilterTokens(filter);
this.pageParams = getInitialPageParams(this.pageSize);
- this.filterTokens = filter;
this.$router.push({ query: this.urlParams });
},
@@ -674,6 +678,28 @@ export default {
Sentry.captureException(error);
});
},
+ setFilterTokens(filtersArg) {
+ const filters = this.removeDisabledSearchTerms(filtersArg);
+
+ this.filterTokens = filters;
+
+ // If we filtered something out, let's show a warning message
+ if (filters.length < filtersArg.length) {
+ this.showAnonymousSearchingMessage();
+ }
+ },
+ removeDisabledSearchTerms(filters) {
+ // If we shouldn't disable anything, let's return the same thing
+ if (!this.shouldDisableSomeFilters) {
+ return filters;
+ }
+
+ const filtersWithoutSearchTerms = filters.filter(
+ (token) => !(token.type === FILTERED_SEARCH_TERM && token.value?.data),
+ );
+
+ return filtersWithoutSearchTerms;
+ },
showAnonymousSearchingMessage() {
createFlash({
message: this.$options.i18n.anonymousSearchingMessage,
@@ -720,17 +746,9 @@ export default {
sortKey = defaultSortKey;
}
- const isSearchDisabled =
- this.isAnonymousSearchDisabled &&
- !this.isSignedIn &&
- window.location.search.includes('search=');
-
- if (isSearchDisabled) {
- this.showAnonymousSearchingMessage();
- }
-
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
- this.filterTokens = isSearchDisabled ? [] : getFilterTokens(window.location.search);
+ this.setFilterTokens(getFilterTokens(window.location.search));
+
this.pageParams = getInitialPageParams(
this.pageSize,
isPositiveInteger(firstPageSize) ? parseInt(firstPageSize, 10) : undefined,
@@ -899,7 +917,9 @@ export default {
<template v-else-if="isSignedIn">
<gl-empty-state :title="$options.i18n.noIssuesSignedInTitle" :svg-path="emptyStateSvgPath">
<template #description>
- <p>{{ $options.i18n.noIssuesSignedInDescription }}</p>
+ <gl-link :href="issuesHelpPagePath" target="_blank">{{
+ $options.i18n.noIssuesSignedInDescription
+ }}</gl-link>
<p v-if="canCreateProjects">
<strong>{{ $options.i18n.noGroupIssuesSignedInDescription }}</strong>
</p>
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 38fe4c33792..27738d7a3e6 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -41,12 +41,8 @@ export const i18n = {
),
noOpenIssuesDescription: __('To keep this project going, create a new issue'),
noOpenIssuesTitle: __('There are no open issues'),
- noIssuesSignedInDescription: __(
- 'Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.',
- ),
- noIssuesSignedInTitle: __(
- 'The Issue Tracker is the place to add things that need to be improved or solved in a project',
- ),
+ noIssuesSignedInDescription: __('Learn more about issues.'),
+ noIssuesSignedInTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noIssuesSignedOutButtonText: __('Register / Sign In'),
noIssuesSignedOutDescription: __(
'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
@@ -151,6 +147,7 @@ export const TOKEN_TYPE_EPIC = 'epic_id';
export const TOKEN_TYPE_WEIGHT = 'weight';
export const TOKEN_TYPE_CONTACT = 'crm_contact';
export const TOKEN_TYPE_ORGANIZATION = 'crm_organization';
+export const TOKEN_TYPE_HEALTH = 'health_status';
export const TYPE_TOKEN_TASK_OPTION = { icon: 'task-done', title: 'task', value: 'task' };
@@ -327,6 +324,16 @@ export const filters = {
},
},
},
+ [TOKEN_TYPE_HEALTH]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'healthStatus',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'health_status',
+ },
+ },
+ },
[TOKEN_TYPE_CONTACT]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'crmContactId',
diff --git a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
index a5cba3daafa..149049247fb 100644
--- a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
@@ -65,7 +65,7 @@ export default {
<template>
<div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)">
- <div class="card card-slim gl-mt-5">
+ <div class="card card-slim gl-mt-5 gl-mb-0">
<div class="card-header gl-bg-gray-10">
<div
class="card-title gl-relative gl-display-flex gl-align-items-center gl-line-height-20 gl-font-weight-bold gl-m-0"
@@ -112,7 +112,7 @@ export default {
</div>
<div
v-if="hasClosingMergeRequest && !isFetchingMergeRequests"
- class="issue-closed-by-widget second-block"
+ class="issue-closed-by-widget second-block gl-mt-3"
>
{{ closingMergeRequestsText }}
</div>
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index c664135f30e..0daf77e03dc 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -17,11 +17,11 @@ 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 DescriptionComponent from './description.vue';
+import EditedComponent from './edited.vue';
+import FormComponent from './form.vue';
import PinnedLinks from './pinned_links.vue';
-import titleComponent from './title.vue';
+import TitleComponent from './title.vue';
export default {
WorkspaceType,
@@ -29,9 +29,9 @@ export default {
GlIcon,
GlBadge,
GlIntersectionObserver,
- titleComponent,
- editedComponent,
- formComponent,
+ TitleComponent,
+ EditedComponent,
+ FormComponent,
PinnedLinks,
ConfidentialityBadge,
},
@@ -51,20 +51,11 @@ export default {
required: true,
type: Boolean,
},
- canDestroy: {
- required: true,
- type: Boolean,
- },
showInlineEditButton: {
type: Boolean,
required: false,
default: true,
},
- showDeleteButton: {
- type: Boolean,
- required: false,
- default: true,
- },
enableAutocomplete: {
type: Boolean,
required: false,
@@ -181,7 +172,7 @@ export default {
type: Object,
required: false,
default: () => {
- return descriptionComponent;
+ return DescriptionComponent;
},
},
showTitleBorder: {
@@ -494,14 +485,12 @@ export default {
:endpoint="endpoint"
:form-state="formState"
:initial-description-text="initialDescriptionText"
- :can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-id="projectId"
:project-namespace="projectNamespace"
- :show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
:issuable-type="issuableType"
diff --git a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
index 47b09bd6aa0..f86ee11e64b 100644
--- a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
+++ b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
@@ -13,7 +13,8 @@ export default {
props: {
issuePath: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
issueType: {
type: String,
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index a6747d67611..5c2a154362f 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -7,6 +7,7 @@ import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import createFlash from '~/flash';
import { IssuableType } 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';
import { __, s__, sprintf } from '~/locale';
@@ -20,6 +21,8 @@ import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_ite
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,
TRACKING_CATEGORY_SHOW,
TASK_TYPE_NAME,
WIDGET_TYPE_DESCRIPTION,
@@ -226,6 +229,7 @@ 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>
</svg>`;
@@ -330,6 +334,9 @@ export default {
this.addHoverListeners(taskLink, workItemId);
taskLink.classList.add('gl-link');
taskLink.addEventListener('click', (e) => {
+ if (isMetaKey(e)) {
+ return;
+ }
e.preventDefault();
this.openWorkItemDetailModal(taskLink);
this.workItemId = workItemId;
@@ -358,6 +365,7 @@ export default {
);
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>
@@ -460,7 +468,7 @@ export default {
this.openWorkItemDetailModal(el);
} catch (error) {
createFlash({
- message: s__('WorkItem|Something went wrong when creating a work item. Please try again'),
+ message: sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemTypes.TASK),
error,
captureError: true,
});
diff --git a/app/assets/javascripts/issues/show/components/edit_actions.vue b/app/assets/javascripts/issues/show/components/edit_actions.vue
index 358b53bd131..120034b8d67 100644
--- a/app/assets/javascripts/issues/show/components/edit_actions.vue
+++ b/app/assets/javascripts/issues/show/components/edit_actions.vue
@@ -1,12 +1,10 @@
<script>
-import { GlButton, GlModalDirective } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import { __, sprintf } from '~/locale';
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
import Tracking from '~/tracking';
import eventHub from '../event_hub';
import updateMixin from '../mixins/update';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
-import DeleteIssueModal from './delete_issue_modal.vue';
const issuableTypes = {
issue: __('Issue'),
@@ -18,18 +16,10 @@ const trackingMixin = Tracking.mixin({ label: 'delete_issue' });
export default {
components: {
- DeleteIssueModal,
GlButton,
},
- directives: {
- GlModal: GlModalDirective,
- },
mixins: [trackingMixin, updateMixin],
props: {
- canDestroy: {
- type: Boolean,
- required: true,
- },
endpoint: {
required: true,
type: String,
@@ -38,11 +28,6 @@ export default {
type: Object,
required: true,
},
- showDeleteButton: {
- type: Boolean,
- required: false,
- default: true,
- },
issuableType: {
type: String,
required: true,
@@ -53,7 +38,6 @@ export default {
deleteLoading: false,
skipApollo: false,
issueState: {},
- modalId: uniqueId('delete-issuable-modal-'),
};
},
apollo: {
@@ -68,17 +52,9 @@ export default {
},
},
computed: {
- deleteIssuableButtonText() {
- return sprintf(__('Delete %{issuableType}'), {
- issuableType: this.typeToShow.toLowerCase(),
- });
- },
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
- shouldShowDeleteButton() {
- return this.canDestroy && this.showDeleteButton && this.typeToShow;
- },
typeToShow() {
const { issueState, issuableType } = this;
const type = issueState.issueType ?? issuableType;
@@ -89,52 +65,26 @@ export default {
closeForm() {
eventHub.$emit('close.form');
},
- deleteIssuable() {
- this.deleteLoading = true;
- eventHub.$emit('delete.issuable');
- },
},
};
</script>
<template>
- <div class="gl-mt-3 gl-mb-3 gl-display-flex gl-justify-content-space-between">
- <div>
- <gl-button
- :loading="formState.updateLoading"
- :disabled="formState.updateLoading || !isSubmitEnabled"
- category="primary"
- variant="confirm"
- class="gl-mr-3"
- data-testid="issuable-save-button"
- type="submit"
- @click.prevent="updateIssuable"
- >
- {{ __('Save changes') }}
- </gl-button>
- <gl-button data-testid="issuable-cancel-button" @click="closeForm">
- {{ __('Cancel') }}
- </gl-button>
- </div>
- <div v-if="shouldShowDeleteButton">
- <gl-button
- v-gl-modal="modalId"
- :loading="deleteLoading"
- :disabled="deleteLoading"
- category="secondary"
- variant="danger"
- data-testid="issuable-delete-button"
- @click="track('click_button')"
- >
- {{ deleteIssuableButtonText }}
- </gl-button>
- <delete-issue-modal
- :issue-path="endpoint"
- :issue-type="typeToShow"
- :modal-id="modalId"
- :title="deleteIssuableButtonText"
- @delete="deleteIssuable"
- />
- </div>
+ <div class="gl-mt-3 gl-mb-3 gl-display-flex">
+ <gl-button
+ :loading="formState.updateLoading"
+ :disabled="formState.updateLoading || !isSubmitEnabled"
+ category="primary"
+ variant="confirm"
+ class="gl-mr-3"
+ data-testid="issuable-save-button"
+ type="submit"
+ @click.prevent="updateIssuable"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <gl-button data-testid="issuable-cancel-button" @click="closeForm">
+ {{ __('Cancel') }}
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/edited.vue b/app/assets/javascripts/issues/show/components/edited.vue
index 41cc3964055..4c5f783cd66 100644
--- a/app/assets/javascripts/issues/show/components/edited.vue
+++ b/app/assets/javascripts/issues/show/components/edited.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- timeAgoTooltip,
+ TimeAgoTooltip,
},
props: {
updatedAt: {
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index f45af47374a..c2ab7c4f298 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -1,11 +1,11 @@
<script>
-import markdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import updateMixin from '../../mixins/update';
export default {
components: {
- markdownField,
+ MarkdownField,
},
mixins: [updateMixin],
props: {
diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index e2c12edf46d..f479c8ae78d 100644
--- a/app/assets/javascripts/issues/show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -22,10 +22,6 @@ export default {
LockedWarning,
},
props: {
- canDestroy: {
- type: Boolean,
- required: true,
- },
endpoint: {
type: String,
required: true,
@@ -63,11 +59,6 @@ export default {
type: String,
required: true,
},
- showDeleteButton: {
- type: Boolean,
- required: false,
- default: true,
- },
canAttachFile: {
type: Boolean,
required: false,
@@ -231,12 +222,6 @@ export default {
:enable-autocomplete="enableAutocomplete"
/>
- <edit-actions
- :endpoint="endpoint"
- :form-state="formState"
- :can-destroy="canDestroy"
- :show-delete-button="showDeleteButton"
- :issuable-type="issuableType"
- />
+ <edit-actions :endpoint="endpoint" :form-state="formState" :issuable-type="issuableType" />
</form>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index 77d13fe085a..aa7b9805b5f 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -26,4 +26,15 @@ export const timelineListI18n = Object.freeze({
'Incident|Something went wrong while deleting the incident timeline event.',
),
deleteModal: s__('Incident|Are you sure you want to delete this event?'),
+ editError: s__('Incident|Error updating incident timeline event: %{error}'),
+ editErrorGeneric: s__(
+ 'Incident|Something went wrong while updating the incident timeline event.',
+ ),
+});
+
+export const timelineItemI18n = Object.freeze({
+ delete: __('Delete'),
+ edit: __('Edit'),
+ moreActions: __('More actions'),
+ timeUTC: __('%{time} UTC'),
});
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 c902895702e..6bb72e82778 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
@@ -1,6 +1,7 @@
<script>
import { produce } from 'immer';
import { sortBy } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { createAlert } from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -16,6 +17,7 @@ export default {
i18n: timelineFormI18n,
components: {
TimelineEventsForm,
+ GlIcon,
},
inject: ['fullPath', 'issuableId'],
props: {
@@ -31,9 +33,6 @@ export default {
clearForm() {
this.$refs.eventForm.clear();
},
- focusDate() {
- this.$refs.eventForm.focusDate();
- },
updateCache(store, { data }) {
const { timelineEvent: event, errors } = data?.timelineEventCreate || {};
@@ -107,11 +106,23 @@ export default {
</script>
<template>
- <timeline-events-form
- ref="eventForm"
- :is-event-processed="createTimelineEventActive"
- :has-timeline-events="hasTimelineEvents"
- @save-event="createIncidentTimelineEvent"
- @cancel="$emit('hide-new-timeline-events-form')"
- />
+ <div
+ class="create-timeline-event gl-relative gl-display-flex gl-align-items-start"
+ :class="{ 'timeline-entry-vertical-line': hasTimelineEvents }"
+ >
+ <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"
+ >
+ <gl-icon name="comment" class="note-icon" />
+ </div>
+ <timeline-events-form
+ ref="eventForm"
+ :class="{ 'gl-border-gray-50 gl-border-t': hasTimelineEvents }"
+ :is-event-processed="createTimelineEventActive"
+ show-save-and-add
+ @save-event="createIncidentTimelineEvent"
+ @cancel="$emit('hide-new-timeline-events-form')"
+ />
+ </div>
</template>
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
new file mode 100644
index 00000000000..60fa8cb949b
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import TimelineEventsForm from './timeline_events_form.vue';
+
+export default {
+ name: 'EditTimelineEvent',
+ components: {
+ TimelineEventsForm,
+ GlIcon,
+ },
+ props: {
+ event: {
+ type: Object,
+ required: true,
+ validator: (item) => ['occurredAt', 'note'].every((key) => item[key]),
+ },
+ editTimelineEventActive: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ saveEvent(eventDetails) {
+ this.$emit('handle-save-edit', { ...eventDetails, id: this.event.id }, false);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="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"
+ >
+ <gl-icon name="comment" class="note-icon" />
+ </div>
+ <timeline-events-form
+ ref="eventForm"
+ class="timeline-event-border"
+ :is-event-processed="editTimelineEventActive"
+ :previous-occurred-at="event.occurredAt"
+ :previous-note="event.note"
+ @save-event="saveEvent"
+ @cancel="$emit('hide-edit')"
+ />
+ </div>
+</template>
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
new file mode 100644
index 00000000000..54f036268cc
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql
@@ -0,0 +1,13 @@
+mutation UpdateTimelineEvent($input: TimelineEventUpdateInput!) {
+ timelineEventUpdate(input: $input) {
+ timelineEvent {
+ id
+ note
+ noteHtml
+ action
+ occurredAt
+ createdAt
+ }
+ errors
+ }
+}
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 0d84fabb1be..b7ae18372ab 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,9 +1,9 @@
<script>
-import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlIcon } from '@gitlab/ui';
+import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { timelineFormI18n } from './constants';
-import { getUtcShiftedDateNow } from './utils';
+import { getUtcShiftedDate } from './utils';
export default {
name: 'TimelineEventsForm',
@@ -15,6 +15,7 @@ export default {
'task-list',
'collapsible-section',
'table',
+ 'attach-file',
'full-screen',
],
components: {
@@ -23,175 +24,168 @@ export default {
GlFormInput,
GlFormGroup,
GlButton,
- GlIcon,
},
i18n: timelineFormI18n,
directives: {
autofocusonshow,
},
props: {
- hasTimelineEvents: {
+ showSaveAndAdd: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
isEventProcessed: {
type: Boolean,
required: true,
},
+ previousOccurredAt: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ previousNote: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
- // if occurredAt is undefined, returns "now" in UTC
- const placeholderDate = getUtcShiftedDateNow();
+ // if occurredAt is null, returns "now" in UTC
+ const placeholderDate = getUtcShiftedDate(this.previousOccurredAt);
return {
- timelineText: '',
+ timelineText: this.previousNote,
placeholderDate,
hourPickerInput: placeholderDate.getHours(),
minutePickerInput: placeholderDate.getMinutes(),
- datepickerTextInput: null,
+ datePickerInput: placeholderDate,
};
},
computed: {
- occurredAt() {
- const [years, months, days] = this.datepickerTextInput.split('-');
+ occurredAtString() {
+ const year = this.datePickerInput.getFullYear();
+ const month = this.datePickerInput.getMonth();
+ const day = this.datePickerInput.getDate();
+
const utcDate = new Date(
- Date.UTC(years, months - 1, days, this.hourPickerInput, this.minutePickerInput),
+ Date.UTC(year, month, day, this.hourPickerInput, this.minutePickerInput),
);
return utcDate.toISOString();
},
},
+ mounted() {
+ this.focusDate();
+ },
methods: {
clear() {
- const utcShiftedDateNow = getUtcShiftedDateNow();
- this.placeholderDate = utcShiftedDateNow;
- this.hourPickerInput = utcShiftedDateNow.getHours();
- this.minutePickerInput = utcShiftedDateNow.getMinutes();
+ const newPlaceholderDate = getUtcShiftedDate();
+ this.datePickerInput = newPlaceholderDate;
+ this.hourPickerInput = newPlaceholderDate.getHours();
+ this.minutePickerInput = newPlaceholderDate.getMinutes();
this.timelineText = '';
},
focusDate() {
- this.$refs.datepicker.$el.focus();
+ this.$refs.datepicker.$el.querySelector('input').focus();
},
handleSave(addAnotherEvent) {
- const eventDetails = {
+ const event = {
note: this.timelineText,
- occurredAt: this.occurredAt,
+ occurredAt: this.occurredAtString,
};
- this.$emit('save-event', eventDetails, addAnotherEvent);
+ this.$emit('save-event', event, addAnotherEvent);
},
},
};
</script>
<template>
- <div
- class="gl-relative gl-display-flex gl-align-items-center"
- :class="{ 'timeline-entry-vertical-line': hasTimelineEvents }"
- >
- <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"
- >
- <gl-icon name="comment" class="note-icon" />
- </div>
- <form class="gl-flex-grow-1 gl-border-gray-50" :class="{ 'gl-border-t': hasTimelineEvents }">
- <div
- class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row datetime-picker"
- >
- <gl-form-group :label="__('Date')" class="gl-mt-5 gl-mr-5">
- <gl-datepicker id="incident-date" #default="{ formattedDate }" v-model="placeholderDate">
+ <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">
+ <gl-datepicker id="incident-date" ref="datepicker" v-model="datePickerInput" />
+ </gl-form-group>
+ <div class="gl-display-flex gl-mt-5">
+ <gl-form-group :label="__('Time')">
+ <div class="gl-display-flex">
+ <label label-for="timeline-input-hours" class="sr-only"></label>
<gl-form-input
- id="incident-date"
- ref="datepicker"
- v-model="datepickerTextInput"
- data-testid="input-datepicker"
- class="gl-datepicker-input gl-pr-7!"
- :value="formattedDate"
- :placeholder="__('YYYY-MM-DD')"
- @keydown.enter="onKeydown"
+ id="timeline-input-hours"
+ v-model="hourPickerInput"
+ data-testid="input-hours"
+ size="xs"
+ type="number"
+ min="00"
+ max="23"
/>
- </gl-datepicker>
- </gl-form-group>
- <div class="gl-display-flex gl-mt-5">
- <gl-form-group :label="__('Time')">
- <div class="gl-display-flex">
- <label label-for="timeline-input-hours" class="sr-only"></label>
- <gl-form-input
- id="timeline-input-hours"
- v-model="hourPickerInput"
- data-testid="input-hours"
- size="xs"
- type="number"
- min="00"
- max="23"
- />
- <label label-for="timeline-input-minutes" class="sr-only"></label>
- <gl-form-input
- id="timeline-input-minutes"
- v-model="minutePickerInput"
- class="gl-ml-3"
- data-testid="input-minutes"
- size="xs"
- type="number"
- min="00"
- max="59"
- />
- </div>
- </gl-form-group>
- <p class="gl-ml-3 gl-align-self-end gl-line-height-32">{{ __('UTC') }}</p>
- </div>
- </div>
- <div class="common-note-form">
- <gl-form-group class="gl-mb-3" :label="$options.i18n.areaLabel">
- <markdown-field
- :can-attach-file="false"
- :add-spacing-classes="false"
- :show-comment-tool-bar="false"
- :textarea-value="timelineText"
- :restricted-tool-bar-items="$options.restrictedToolBarItems"
- markdown-docs-path=""
- :enable-preview="false"
- class="bordered-box gl-mt-0"
- >
- <template #textarea>
- <textarea
- v-model="timelineText"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- data-testid="input-note"
- dir="auto"
- data-supports-quick-actions="false"
- :aria-label="$options.i18n.description"
- :placeholder="$options.i18n.areaPlaceholder"
- >
- </textarea>
- </template>
- </markdown-field>
+ <label label-for="timeline-input-minutes" class="sr-only"></label>
+ <gl-form-input
+ id="timeline-input-minutes"
+ v-model="minutePickerInput"
+ class="gl-ml-3"
+ data-testid="input-minutes"
+ size="xs"
+ type="number"
+ min="00"
+ max="59"
+ />
+ </div>
</gl-form-group>
+ <p class="gl-ml-3 gl-align-self-end gl-line-height-32">{{ __('UTC') }}</p>
</div>
- <gl-form-group class="gl-mb-0">
- <gl-button
- variant="confirm"
- category="primary"
- class="gl-mr-3"
- :loading="isEventProcessed"
- @click="handleSave(false)"
- >
- {{ $options.i18n.save }}
- </gl-button>
- <gl-button
- variant="confirm"
- category="secondary"
- class="gl-mr-3 gl-ml-n2"
- :loading="isEventProcessed"
- @click="handleSave(true)"
+ </div>
+ <div class="common-note-form">
+ <gl-form-group class="gl-mb-3" :label="$options.i18n.areaLabel">
+ <markdown-field
+ :can-attach-file="false"
+ :add-spacing-classes="false"
+ :show-comment-tool-bar="false"
+ :textarea-value="timelineText"
+ :restricted-tool-bar-items="$options.restrictedToolBarItems"
+ markdown-docs-path=""
+ :enable-preview="false"
+ class="bordered-box gl-mt-0"
>
- {{ $options.i18n.saveAndAdd }}
- </gl-button>
- <gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
- {{ $options.i18n.cancel }}
- </gl-button>
- <div class="gl-border-b gl-pt-5"></div>
+ <template #textarea>
+ <textarea
+ v-model="timelineText"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ data-testid="input-note"
+ dir="auto"
+ data-supports-quick-actions="false"
+ :aria-label="$options.i18n.description"
+ :placeholder="$options.i18n.areaPlaceholder"
+ >
+ </textarea>
+ </template>
+ </markdown-field>
</gl-form-group>
- </form>
- </div>
+ </div>
+ <gl-form-group class="gl-mb-0">
+ <gl-button
+ variant="confirm"
+ category="primary"
+ class="gl-mr-3"
+ :loading="isEventProcessed"
+ @click="handleSave(false)"
+ >
+ {{ $options.i18n.save }}
+ </gl-button>
+ <gl-button
+ v-if="showSaveAndAdd"
+ variant="confirm"
+ category="secondary"
+ class="gl-mr-3 gl-ml-n2"
+ :loading="isEventProcessed"
+ @click="handleSave(true)"
+ >
+ {{ $options.i18n.saveAndAdd }}
+ </gl-button>
+ <gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ <div class="timeline-event-bottom-border"></div>
+ </gl-form-group>
+ </form>
</template>
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 6175c9969ec..cbf3c387fa3 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
@@ -1,25 +1,13 @@
<script>
-import {
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlIcon,
- GlSafeHtmlDirective,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
+import { timelineItemI18n } from './constants';
import { getEventIcon } from './utils';
export default {
name: 'IncidentTimelineEventListItem',
- i18n: {
- delete: __('Delete'),
- moreActions: __('More actions'),
- timeUTC: __('%{time} UTC'),
- },
+ i18n: timelineItemI18n,
components: {
- GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
@@ -28,12 +16,8 @@ export default {
directives: {
SafeHtml: GlSafeHtmlDirective,
},
- inject: ['canUpdate'],
+ inject: ['canUpdateTimelineEvent'],
props: {
- isLastItem: {
- type: Boolean,
- required: true,
- },
occurredAt: {
type: String,
required: true,
@@ -58,43 +42,41 @@ export default {
};
</script>
<template>
- <li
- class="timeline-entry timeline-entry-vertical-line note system-note note-wrapper gl-my-2! gl-pr-0!"
- >
- <div class="gl-display-flex gl-align-items-center">
- <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-n2 gl-mr-3 gl-w-8 gl-h-8 gl-p-3 gl-z-index-1"
- >
- <gl-icon :name="getEventIcon(action)" class="note-icon" />
+ <div class="gl-display-flex gl-align-items-start">
+ <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"
+ >
+ <gl-icon :name="getEventIcon(action)" class="note-icon" />
+ </div>
+ <div
+ class="timeline-event-note timeline-event-border gl-w-full gl-display-flex gl-flex-direction-row"
+ data-testid="event-text-container"
+ >
+ <div>
+ <strong class="gl-font-lg" data-testid="event-time">
+ <gl-sprintf :message="$options.i18n.timeUTC">
+ <template #time>{{ time }}</template>
+ </gl-sprintf>
+ </strong>
+ <div v-safe-html="noteHtml"></div>
</div>
- <div
- class="timeline-event-note gl-w-full gl-display-flex gl-flex-direction-row"
- :class="{ 'gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid': !isLastItem }"
- data-testid="event-text-container"
+ <gl-dropdown
+ v-if="canUpdateTimelineEvent"
+ right
+ class="event-note-actions gl-ml-auto gl-align-self-start"
+ icon="ellipsis_v"
+ text-sr-only
+ :text="$options.i18n.moreActions"
+ category="tertiary"
+ no-caret
>
- <div>
- <strong class="gl-font-lg" data-testid="event-time">
- <gl-sprintf :message="$options.i18n.timeUTC">
- <template #time>{{ time }}</template>
- </gl-sprintf>
- </strong>
- <div v-safe-html="noteHtml"></div>
- </div>
- <gl-dropdown
- v-if="canUpdate"
- right
- class="event-note-actions gl-ml-auto gl-align-self-center"
- icon="ellipsis_v"
- text-sr-only
- :text="$options.i18n.moreActions"
- category="tertiary"
- no-caret
- >
- <gl-dropdown-item @click="$emit('delete')">
- <gl-button>{{ $options.i18n.delete }}</gl-button>
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
+ <gl-dropdown-item @click="$emit('edit')">
+ {{ $options.i18n.edit }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="$emit('delete')">
+ {{ $options.i18n.delete }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
- </li>
+ </div>
</template>
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 80ac1c372cd..321b7ccc14a 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
@@ -5,7 +5,9 @@ import { sprintf } from '~/locale';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import IncidentTimelineEventItem from './timeline_events_item.vue';
+import EditTimelineEvent from './edit_timeline_event.vue';
import deleteTimelineEvent from './graphql/queries/delete_timeline_event.mutation.graphql';
+import editTimelineEvent from './graphql/queries/edit_timeline_event.mutation.graphql';
import { timelineListI18n } from './constants';
export default {
@@ -13,6 +15,7 @@ export default {
i18n: timelineListI18n,
components: {
IncidentTimelineEventItem,
+ EditTimelineEvent,
},
props: {
timelineEventLoading: {
@@ -26,6 +29,9 @@ export default {
default: () => [],
},
},
+ data() {
+ return { eventToEdit: null, editTimelineEventActive: false };
+ },
computed: {
dateGroupedEvents() {
const groupedEvents = new Map();
@@ -44,11 +50,12 @@ export default {
},
},
methods: {
- isLastItem(groups, groupIndex, events, eventIndex) {
- if (groupIndex < groups.size - 1) {
- return false;
- }
- return eventIndex === events.length - 1;
+ handleEditSelection(event) {
+ this.eventToEdit = event.id;
+ this.$emit('hide-new-incident-timeline-event-form');
+ },
+ hideEdit() {
+ this.eventToEdit = null;
},
handleDelete: ignoreWhilePending(async function handleDelete(event) {
const msg = this.$options.i18n.deleteModal;
@@ -85,6 +92,38 @@ export default {
createAlert({ message: this.$options.i18n.deleteErrorGeneric, captureError: true, error });
}
}),
+ handleSaveEdit(eventDetails) {
+ this.editTimelineEventActive = true;
+ return this.$apollo
+ .mutate({
+ mutation: editTimelineEvent,
+ variables: {
+ input: {
+ id: eventDetails.id,
+ note: eventDetails.note,
+ occurredAt: eventDetails.occurredAt,
+ },
+ },
+ })
+ .then(({ data }) => {
+ this.editTimelineEventActive = false;
+ const errors = data.timelineEventUpdate?.errors;
+ if (errors.length) {
+ createAlert({
+ message: sprintf(this.$options.i18n.editError, { error: errors.join('. ') }, false),
+ });
+ } else {
+ this.hideEdit();
+ }
+ })
+ .catch((error) => {
+ createAlert({
+ message: this.$options.i18n.editErrorGeneric,
+ captureError: true,
+ error,
+ });
+ });
+ },
},
};
</script>
@@ -92,9 +131,10 @@ export default {
<template>
<div class="issuable-discussion incident-timeline-events">
<div
- v-for="([eventDate, events], groupIndex) in dateGroupedEvents"
+ 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>
@@ -103,15 +143,25 @@ export default {
<li
v-for="(event, eventIndex) in events"
:key="eventIndex"
- class="timeline-entry-vertical-line 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-2! gl-pr-0!"
>
+ <edit-timeline-event
+ v-if="eventToEdit === event.id"
+ :key="`edit-${event.id}`"
+ ref="eventForm"
+ :event="event"
+ :edit-timeline-event-active="editTimelineEventActive"
+ @handle-save-edit="handleSaveEdit"
+ @hide-edit="hideEdit()"
+ />
<incident-timeline-event-item
+ v-else
:key="event.id"
:action="event.action"
:occurred-at="event.occurredAt"
:note-html="event.noteHtml"
- :is-last-item="isLastItem(dateGroupedEvents, groupIndex, events, eventIndex)"
@delete="handleDelete(event)"
+ @edit="handleEditSelection(event)"
/>
</li>
</ul>
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 7c2a7878c58..5f70d9acac9 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
@@ -3,10 +3,10 @@ import { GlButton, GlEmptyState, GlLoadingIcon, GlTab } from '@gitlab/ui';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_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';
import { displayAndLogError } from './utils';
import { timelineTabI18n } from './constants';
-
import CreateTimelineEvent from './create_timeline_event.vue';
import IncidentTimelineEventsList from './timeline_events_list.vue';
@@ -20,7 +20,7 @@ export default {
IncidentTimelineEventsList,
},
i18n: timelineTabI18n,
- inject: ['canUpdate', 'fullPath', 'issuableId'],
+ inject: ['canUpdateTimelineEvent', 'fullPath', 'issuableId'],
data() {
return {
isEventFormVisible: false,
@@ -56,15 +56,21 @@ export default {
return !this.timelineEventLoading && !this.hasTimelineEvents;
},
},
+ mounted() {
+ notesEventHub.$on('comment-promoted-to-timeline-event', this.refreshTimelineEvents);
+ },
+ destroyed() {
+ notesEventHub.$off('comment-promoted-to-timeline-event', this.refreshTimelineEvents);
+ },
methods: {
+ refreshTimelineEvents() {
+ this.$apollo.queries.timelineEvents.refetch();
+ },
hideEventForm() {
this.isEventFormVisible = false;
},
- async showEventForm() {
- this.$refs.createEventForm.clearForm();
+ showEventForm() {
this.isEventFormVisible = true;
- await this.$nextTick();
- this.$refs.createEventForm.focusDate();
},
},
};
@@ -85,14 +91,19 @@ export default {
@hide-new-timeline-events-form="hideEventForm"
/>
<create-timeline-event
- v-show="isEventFormVisible"
+ v-if="isEventFormVisible"
ref="createEventForm"
:has-timeline-events="hasTimelineEvents"
class="timeline-event-note timeline-event-note-form"
:class="{ 'gl-pl-0': !hasTimelineEvents }"
@hide-new-timeline-events-form="hideEventForm"
/>
- <gl-button v-if="canUpdate" variant="default" class="gl-mb-3 gl-mt-7" @click="showEventForm">
+ <gl-button
+ v-if="canUpdateTimelineEvent"
+ variant="default"
+ class="gl-mb-3 gl-mt-7"
+ @click="showEventForm"
+ >
{{ $options.i18n.addEventButton }}
</gl-button>
</gl-tab>
diff --git a/app/assets/javascripts/issues/show/components/incidents/utils.js b/app/assets/javascripts/issues/show/components/incidents/utils.js
index cf790a11b67..5a009debd75 100644
--- a/app/assets/javascripts/issues/show/components/incidents/utils.js
+++ b/app/assets/javascripts/issues/show/components/incidents/utils.js
@@ -21,13 +21,14 @@ export const getEventIcon = (actionName) => {
};
/**
- * Returns a date shifted by the current timezone offset. Allows
- * date.getHours() and similar to return UTC values.
- *
+ * Returns a date shifted by the current timezone offset set to now
+ * by default but can accept an existing date as an ISO date string
+ * @param {string} ISOString
* @returns {Date}
*/
-export const getUtcShiftedDateNow = () => {
- const date = new Date();
+export const getUtcShiftedDate = (ISOString = null) => {
+ const date = ISOString ? new Date(ISOString) : new Date();
date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
+
return date;
};
diff --git a/app/assets/javascripts/issues/show/graphql.js b/app/assets/javascripts/issues/show/graphql.js
index 5b8630f7d63..deee034f9d1 100644
--- a/app/assets/javascripts/issues/show/graphql.js
+++ b/app/assets/javascripts/issues/show/graphql.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { defaultClient } from '~/sidebar/graphql';
+import { defaultClient } from '~/graphql_shared/issuable_client';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index 459a3804837..e5eed9f6b79 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -32,6 +32,7 @@ export function initIncidentApp(issueData = {}) {
const {
canCreateIncident,
canUpdate,
+ canUpdateTimelineEvent,
iid,
issuableId,
projectNamespace,
@@ -51,6 +52,7 @@ export function initIncidentApp(issueData = {}) {
provide: {
issueType: INCIDENT_TYPE,
canCreateIncident,
+ canUpdateTimelineEvent,
canUpdate,
fullPath,
iid,
diff --git a/app/assets/javascripts/issues/show/utils/update_description.js b/app/assets/javascripts/issues/show/utils/update_description.js
index c5811290e61..aeb547b9194 100644
--- a/app/assets/javascripts/issues/show/utils/update_description.js
+++ b/app/assets/javascripts/issues/show/utils/update_description.js
@@ -13,6 +13,7 @@ const updateDescription = (descriptionHtml = '', details) => {
}
const placeholder = document.createElement('div');
+ // eslint-disable-next-line no-unsanitized/property
placeholder.innerHTML = descriptionHtml;
const newDetails = placeholder.getElementsByTagName('details');