summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-29 06:09:31 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-29 06:09:31 +0000
commitf2ba923aa70596b5ca56cbf8b58ac33dc208c6a8 (patch)
treedc8838b0323bdc7a0763b7caa18776052aa65a3a
parent5e5c529ef67c6902c69613dd0d490613fa9ef505 (diff)
downloadgitlab-ce-f2ba923aa70596b5ca56cbf8b58ac33dc208c6a8.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue11
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue15
-rw-r--r--app/assets/javascripts/notes/components/note_actions/timeline_event_button.vue49
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/graphql/promote_timeline_event.mutation.graphql8
-rw-r--r--app/assets/javascripts/notes/index.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js55
-rw-r--r--app/assets/javascripts/notes/stores/getters.js7
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue4
-rw-r--r--app/views/projects/issues/_discussion.html.haml3
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/frontend/notes/components/note_actions/timeline_event_button_spec.js35
-rw-r--r--spec/frontend/notes/stores/actions_spec.js101
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js7
18 files changed, 312 insertions, 7 deletions
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..090d5473d1d 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';
@@ -56,7 +56,16 @@ 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;
},
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index c7f293a219a..9806f8e5dc2 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui';
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import Api from '~/api';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import createFlash from '~/flash';
@@ -11,6 +11,7 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { splitCamelCase } from '~/lib/utils/text_utility';
import ReplyButton from './note_actions/reply_button.vue';
+import TimelineEventButton from './note_actions/timeline_event_button.vue';
export default {
i18n: {
@@ -23,6 +24,7 @@ export default {
components: {
GlIcon,
ReplyButton,
+ TimelineEventButton,
GlButton,
GlDropdownItem,
UserAccessRoleBadge,
@@ -133,7 +135,8 @@ export default {
},
},
computed: {
- ...mapGetters(['getUserDataByProp', 'getNoteableData']),
+ ...mapState(['isPromoteCommentToTimelineEventInProgress']),
+ ...mapGetters(['getUserDataByProp', 'getNoteableData', 'canUserAddIncidentTimelineEvents']),
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
@@ -199,7 +202,7 @@ export default {
},
},
methods: {
- ...mapActions(['toggleAwardRequest']),
+ ...mapActions(['toggleAwardRequest', 'promoteCommentToTimelineEvent']),
onEdit() {
this.$emit('handleEdit');
},
@@ -292,6 +295,12 @@ export default {
class="line-resolve-btn note-action-button"
@click="onResolve"
/>
+ <timeline-event-button
+ v-if="canUserAddIncidentTimelineEvents"
+ :note-id="noteId"
+ :is-promotion-in-progress="isPromoteCommentToTimelineEventInProgress"
+ @click-promote-comment-to-event="promoteCommentToTimelineEvent"
+ />
<emoji-picker
v-if="canAwardEmoji"
toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary"
diff --git a/app/assets/javascripts/notes/components/note_actions/timeline_event_button.vue b/app/assets/javascripts/notes/components/note_actions/timeline_event_button.vue
new file mode 100644
index 00000000000..4dd0c968282
--- /dev/null
+++ b/app/assets/javascripts/notes/components/note_actions/timeline_event_button.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ buttonText: __('Add comment to incident timeline'),
+ addError: __('Error promoting the note to timeline event: %{error}'),
+ addGenericError: __('Something went wrong while promoting the note to timeline event.'),
+ },
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ noteId: {
+ type: [String, Number],
+ required: true,
+ },
+ isPromotionInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ handleButtonClick() {
+ this.$emit('click-promote-comment-to-event', {
+ noteId: this.noteId,
+ addError: this.$options.i18n.addError,
+ addGenericError: this.$options.i18n.addGenericError,
+ });
+ },
+ },
+};
+</script>
+<template>
+ <span v-gl-tooltip :title="$options.i18n.buttonText">
+ <gl-button
+ category="tertiary"
+ icon="clock"
+ :aria-label="$options.i18n.buttonText"
+ :disabled="isPromotionInProgress"
+ @click="handleButtonClick"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index a5f459c8910..88f438975f6 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -13,6 +13,7 @@ export const MERGED = 'merged';
export const ISSUE_NOTEABLE_TYPE = 'Issue';
export const EPIC_NOTEABLE_TYPE = 'Epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
+export const INCIDENT_NOTEABLE_TYPE = 'INCIDENT'; // TODO: check if value can be converted to `Incident`
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
@@ -31,6 +32,7 @@ export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
Epic: EPIC_NOTEABLE_TYPE,
+ Incident: INCIDENT_NOTEABLE_TYPE,
};
export const DISCUSSION_FILTER_TYPES = {
diff --git a/app/assets/javascripts/notes/graphql/promote_timeline_event.mutation.graphql b/app/assets/javascripts/notes/graphql/promote_timeline_event.mutation.graphql
new file mode 100644
index 00000000000..c9df9cfd6d3
--- /dev/null
+++ b/app/assets/javascripts/notes/graphql/promote_timeline_event.mutation.graphql
@@ -0,0 +1,8 @@
+mutation PromoteTimelineEvent($input: TimelineEventPromoteFromNoteInput!) {
+ timelineEventPromoteFromNote(input: $input) {
+ timelineEvent {
+ id
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 27e54a1ea69..054a5bd36e2 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import NotesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters';
import { store } from './stores';
@@ -39,6 +40,7 @@ export default () => {
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
+ can_add_timeline_events: parseBoolean(notesDataset.canAddTimelineEvents),
};
}
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 82417c9134b..fcef26d720c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -6,6 +6,7 @@ import createFlash from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
+import toast from '~/vue_shared/plugins/global_toast';
import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
@@ -18,6 +19,12 @@ import sidebarTimeTrackingEventHub from '~/sidebar/event_hub';
import TaskList from '~/task_list';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
import SidebarStore from '~/sidebar/stores/sidebar_store';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_NOTE } from '~/graphql_shared/constants';
+import notesEventHub from '../event_hub';
+
+import promoteTimelineEvent from '../graphql/promote_timeline_event.mutation.graphql';
+
import * as constants from '../constants';
import * as types from './mutation_types';
import * as utils from './utils';
@@ -226,6 +233,54 @@ export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes)
});
};
+export const promoteCommentToTimelineEvent = (
+ { commit },
+ { noteId, addError, addGenericError },
+) => {
+ commit(types.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS, true); // Set loading state
+ return utils.gqClient
+ .mutate({
+ mutation: promoteTimelineEvent,
+ variables: {
+ input: {
+ noteId: convertToGraphQLId(TYPE_NOTE, noteId),
+ },
+ },
+ })
+ .then(({ data = {} }) => {
+ const errors = data.timelineEventPromoteFromNote?.errors;
+ if (errors.length) {
+ const errorMessage = sprintf(addError, {
+ error: errors.join('. '),
+ });
+ throw new Error(errorMessage);
+ } else {
+ notesEventHub.$emit('comment-promoted-to-timeline-event');
+ toast(__('Comment added to the timeline.'));
+ }
+ })
+ .catch((error) => {
+ const message = error.message || addGenericError;
+
+ let captureError = false;
+ let errorObj = null;
+
+ if (message === addGenericError) {
+ captureError = true;
+ errorObj = error;
+ }
+
+ createFlash({
+ message,
+ captureError,
+ error: errorObj,
+ });
+ })
+ .finally(() => {
+ commit(types.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS, false); // Revert loading state
+ });
+};
+
export const replyToDiscussion = (
{ commit, state, getters, dispatch },
{ endpoint, data: reply },
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 1fe82d96435..6876220f75c 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -93,6 +93,13 @@ export const getUserDataByProp = (state) => (prop) => state.userData && state.us
export const descriptionVersions = (state) => state.descriptionVersions;
+export const canUserAddIncidentTimelineEvents = (state) => {
+ return (
+ state.userData.can_add_timeline_events &&
+ state.noteableData.type === constants.NOTEABLE_TYPE_MAPPING.Incident
+ );
+};
+
export const notesById = (state) =>
state.discussions.reduce((acc, note) => {
note.notes.every((n) => Object.assign(acc, { [n.id]: n }));
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index f779aad5679..7ba1f470b05 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -30,6 +30,7 @@ export default () => ({
isNotesFetched: false,
isLoading: true,
isLoadingDescriptionVersion: false,
+ isPromoteCommentToTimelineEventInProgress: false,
// holds endpoints and permissions provided through haml
notesData: {
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index e28a7bc5cdd..42df6bc0980 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -57,3 +57,6 @@ export const RECEIVE_DESCRIPTION_VERSION_ERROR = 'RECEIVE_DESCRIPTION_VERSION_ER
export const REQUEST_DELETE_DESCRIPTION_VERSION = 'REQUEST_DELETE_DESCRIPTION_VERSION';
export const RECEIVE_DELETE_DESCRIPTION_VERSION = 'RECEIVE_DELETE_DESCRIPTION_VERSION';
export const RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR = 'RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR';
+
+// Incidents
+export const SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS = 'SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 0823eacf1b7..83c15c12eac 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -425,4 +425,7 @@ export default {
[types.SET_DONE_FETCHING_BATCH_DISCUSSIONS](state, value) {
state.doneFetchingBatchDiscussions = value;
},
+ [types.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS](state, value) {
+ state.isPromoteCommentToTimelineEventInProgress = value;
+ },
};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index cc930d67fa4..30f57f506a6 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -81,6 +81,7 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = {
protobuf: 'protobuf',
puppet: 'puppet',
python: 'python',
+ python3: 'python',
q: 'q',
qml: 'qml',
r: 'r',
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index f471db24889..9c6c12eac7d 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -42,7 +42,7 @@ export default {
return {
languageDefinition: null,
content: this.blob.rawTextBlob,
- language: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language],
+ language: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()],
hljs: null,
firstChunk: null,
chunks: {},
@@ -62,7 +62,7 @@ export default {
const supportedLanguages = Object.keys(languageLoader);
return (
!supportedLanguages.includes(this.language) &&
- !supportedLanguages.includes(this.blob.language)
+ !supportedLanguages.includes(this.blob.language?.toLowerCase())
);
},
},
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 16b795ee3c9..11b652cc818 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -7,4 +7,5 @@
noteable_data: serialize_issuable(@issue, with_blocking_issues: true),
noteable_type: 'Issue',
target_type: 'issue',
- current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json } }
+ current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json,
+ can_add_timeline_events: "#{can?(current_user, :admin_incident_management_timeline_event, @issue)}" } }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e30601d3011..2244e016228 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2227,6 +2227,9 @@ msgstr ""
msgid "Add comment to design"
msgstr ""
+msgid "Add comment to incident timeline"
+msgstr ""
+
msgid "Add comment..."
msgstr ""
@@ -9451,6 +9454,9 @@ msgstr ""
msgid "Comment '%{label}' position"
msgstr ""
+msgid "Comment added to the timeline."
+msgstr ""
+
msgid "Comment form position"
msgstr ""
@@ -15253,6 +15259,9 @@ msgstr ""
msgid "Error parsing CSV file. Please make sure it has"
msgstr ""
+msgid "Error promoting the note to timeline event: %{error}"
+msgstr ""
+
msgid "Error rendering Markdown preview"
msgstr ""
@@ -37002,6 +37011,9 @@ msgstr ""
msgid "Something went wrong while promoting the issue to an epic. Please try again."
msgstr ""
+msgid "Something went wrong while promoting the note to timeline event."
+msgstr ""
+
msgid "Something went wrong while reopening a requirement."
msgstr ""
diff --git a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
new file mode 100644
index 00000000000..658e844a9b1
--- /dev/null
+++ b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
@@ -0,0 +1,35 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
+
+const emitData = {
+ noteId: '1',
+ addError: 'Error promoting the note to timeline event: %{error}',
+ addGenericError: 'Something went wrong while promoting the note to timeline event.',
+};
+
+describe('NoteTimelineEventButton', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(TimelineEventButton, {
+ propsData: {
+ noteId: '1',
+ isPromotionInProgress: true,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTimelineButton = () => wrapper.findComponent(GlButton);
+
+ it('emits click-promote-comment-to-event', async () => {
+ findTimelineButton().vm.$emit('click');
+
+ expect(wrapper.emitted('click-promote-comment-to-event')).toEqual([[emitData]]);
+ expect(findTimelineButton().props('disabled')).toEqual(true);
+ });
+});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 02b27eca196..989dd74b6d0 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
import createFlash from '~/flash';
+import toast from '~/vue_shared/plugins/global_toast';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import * as notesConstants from '~/notes/constants';
@@ -14,7 +15,9 @@ import mutations from '~/notes/stores/mutations';
import * as utils from '~/notes/stores/utils';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import promoteTimelineEvent from '~/notes/graphql/promote_timeline_event.mutation.graphql';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
+import notesEventHub from '~/notes/event_hub';
import waitForPromises from 'helpers/wait_for_promises';
import { resetStore } from '../helpers';
import {
@@ -38,6 +41,8 @@ jest.mock('~/flash', () => {
return flash;
});
+jest.mock('~/vue_shared/plugins/global_toast');
+
describe('Actions Notes Store', () => {
let commit;
let dispatch;
@@ -1324,6 +1329,102 @@ describe('Actions Notes Store', () => {
});
});
+ describe('promoteCommentToTimelineEvent', () => {
+ const actionArgs = {
+ noteId: '1',
+ addError: 'addError: Create error',
+ addGenericError: 'addGenericError',
+ };
+ const commitSpy = jest.fn();
+
+ describe('for successful request', () => {
+ const timelineEventSuccessResponse = {
+ data: {
+ timelineEventPromoteFromNote: {
+ timelineEvent: {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/19',
+ },
+ errors: [],
+ },
+ },
+ };
+
+ beforeEach(() => {
+ jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(timelineEventSuccessResponse);
+ });
+
+ it('calls gqClient mutation with the correct values', () => {
+ actions.promoteCommentToTimelineEvent({ commit: () => {} }, actionArgs);
+
+ expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
+ expect(utils.gqClient.mutate).toHaveBeenCalledWith({
+ mutation: promoteTimelineEvent,
+ variables: {
+ input: {
+ noteId: 'gid://gitlab/Note/1',
+ },
+ },
+ });
+ });
+
+ it('returns success response', () => {
+ jest.spyOn(notesEventHub, '$emit').mockImplementation(() => {});
+
+ return actions.promoteCommentToTimelineEvent({ commit: commitSpy }, actionArgs).then(() => {
+ expect(notesEventHub.$emit).toHaveBeenLastCalledWith(
+ 'comment-promoted-to-timeline-event',
+ );
+ expect(toast).toHaveBeenCalledWith('Comment added to the timeline.');
+ expect(commitSpy).toHaveBeenCalledWith(
+ mutationTypes.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS,
+ false,
+ );
+ });
+ });
+ });
+
+ describe('for failing request', () => {
+ const errorResponse = {
+ data: {
+ timelineEventPromoteFromNote: {
+ timelineEvent: null,
+ errors: ['Create error'],
+ },
+ },
+ };
+
+ it.each`
+ mockReject | message | captureError | error
+ ${true} | ${'addGenericError'} | ${true} | ${new Error()}
+ ${false} | ${'addError: Create error'} | ${false} | ${null}
+ `(
+ 'should show an error when submission fails',
+ ({ mockReject, message, captureError, error }) => {
+ const expectedAlertArgs = {
+ captureError,
+ error,
+ message,
+ };
+ if (mockReject) {
+ jest.spyOn(utils.gqClient, 'mutate').mockRejectedValueOnce(new Error());
+ } else {
+ jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(errorResponse);
+ }
+
+ return actions
+ .promoteCommentToTimelineEvent({ commit: commitSpy }, actionArgs)
+ .then(() => {
+ expect(createFlash).toHaveBeenCalledWith(expectedAlertArgs);
+ expect(commitSpy).toHaveBeenCalledWith(
+ mutationTypes.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS,
+ false,
+ );
+ });
+ },
+ );
+ });
+ });
+
describe('setFetchingState', () => {
it('commits SET_NOTES_FETCHING_STATE', () => {
return testAction(
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 4fbc907a813..f2e3fa8d433 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -110,6 +110,13 @@ describe('Source Viewer component', () => {
expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
});
+ it('correctly maps languages starting with uppercase', async () => {
+ await createComponent({ language: 'Python3' });
+ const languageDefinition = await import(`highlight.js/lib/languages/python`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('python', languageDefinition.default);
+ });
+
it('highlights the first chunk', () => {
expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
});