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/vue_shared/components/markdown')
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue103
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue59
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue163
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js17
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js89
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue120
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue208
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/tracking.js14
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/utils.js7
13 files changed, 611 insertions, 231 deletions
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index f51ec715678..a570abae9d3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -12,7 +12,8 @@ export default {
},
defaultCommitMessage: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
batchSuggestionsCount: {
type: Number,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index 186f5619b87..966a5556d24 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -1,7 +1,6 @@
<script>
import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { updateText } from '~/lib/utils/text_markdown';
import savedRepliesQuery from './saved_replies.query.graphql';
export default {
@@ -54,20 +53,8 @@ export default {
},
onSelect(id) {
const savedReply = this.savedReplies.find((r) => r.id === id);
- const textArea = this.$el.closest('.md-area')?.querySelector('textarea');
-
- if (savedReply && textArea) {
- updateText({
- textArea,
- tag: savedReply.content,
- cursorOffset: 0,
- wrap: false,
- });
-
- // Wait for text to be added into textarea
- requestAnimationFrame(() => {
- textArea.focus();
- });
+ if (savedReply) {
+ this.$emit('select', savedReply.content);
}
},
},
@@ -81,13 +68,14 @@ export default {
:items="filteredSavedReplies"
:toggle-text="__('Insert comment template')"
text-sr-only
+ no-caret
toggle-class="js-comment-template-toggle"
icon="comment-lines"
category="tertiary"
placement="right"
searchable
size="small"
- class="comment-template-dropdown"
+ class="comment-template-dropdown gl-mr-3"
positioning-strategy="fixed"
:searching="$apollo.queries.savedReplies.loading"
@shown="fetchCommentTemplates"
@@ -104,7 +92,7 @@ export default {
</template>
<template #footer>
<div
- class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3"
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-display-flex gl-justify-content-center gl-p-2"
>
<gl-button
:href="newCommentTemplatePath"
@@ -130,4 +118,8 @@ export default {
.comment-template-dropdown .gl-new-dropdown-item-check-icon {
display: none;
}
+
+.comment-template-dropdown input {
+ border-radius: 0;
+}
</style>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
index 645975ca565..2426a917a53 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
@@ -1,10 +1,16 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlPopover, GlLink } from '@gitlab/ui';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { __ } from '~/locale';
+import RICH_TEXT_EDITOR_ILLUSTRATION from '../../../../images/callouts/rich_text_editor_illustration.svg?url';
+import { counter } from './utils';
export default {
components: {
GlButton,
+ GlLink,
+ GlPopover,
+ UserCalloutDismisser,
},
props: {
value: {
@@ -12,21 +18,102 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ counter: counter(),
+ };
+ },
computed: {
+ showPromoPopover() {
+ return this.markdownEditorSelected && this.counter === 0;
+ },
markdownEditorSelected() {
return this.value === 'markdown';
},
text() {
- return this.markdownEditorSelected ? __('Switch to rich text') : __('Switch to Markdown');
+ return this.markdownEditorSelected
+ ? __('Switch to rich text editing')
+ : __('Switch to plain text editing');
},
},
+ methods: {
+ switchEditorType(insertTemplate = false) {
+ this.$emit('switch', insertTemplate);
+ },
+ },
+ richTextEditorButtonId: 'switch-to-rich-text-editor',
+ RICH_TEXT_EDITOR_ILLUSTRATION,
};
</script>
<template>
- <gl-button
- class="btn btn-default btn-sm gl-button btn-default-tertiary"
- data-qa-selector="editing_mode_switcher"
- @click="$emit('input')"
- >{{ text }}</gl-button
- >
+ <div class="content-editor-switcher gl-display-inline-flex gl-align-items-center">
+ <user-callout-dismisser feature-name="rich_text_editor">
+ <template #default="{ dismiss, shouldShowCallout }">
+ <div>
+ <gl-popover
+ :target="$options.richTextEditorButtonId"
+ :show="Boolean(showPromoPopover && shouldShowCallout)"
+ show-close-button
+ :css-classes="['rich-text-promo-popover gl-p-2']"
+ triggers="manual"
+ data-testid="rich-text-promo-popover"
+ @close-button-clicked="dismiss"
+ >
+ <img
+ :src="$options.RICH_TEXT_EDITOR_ILLUSTRATION"
+ :alt="''"
+ class="rich-text-promo-popover-illustration"
+ width="280"
+ height="130"
+ />
+ <h5 class="gl-mt-3 gl-mb-3">{{ __('Writing just got easier') }}</h5>
+ <p class="gl-m-0">
+ {{
+ __(
+ 'Use the new rich text editor to see your text and tables fully formatted as you type. No need to remember any formatting syntax, or switch between preview and editing modes!',
+ )
+ }}
+ </p>
+ <gl-link
+ class="gl-button btn btn-confirm block gl-mb-2 gl-mt-4"
+ variant="confirm"
+ category="primary"
+ target="_blank"
+ block
+ @click="
+ switchEditorType(showPromoPopover);
+ dismiss();
+ "
+ >
+ {{ __('Try the rich text editor now') }}
+ </gl-link>
+ </gl-popover>
+ <gl-button
+ :id="$options.richTextEditorButtonId"
+ class="btn btn-default btn-sm gl-button btn-default-tertiary gl-font-sm! gl-text-secondary! gl-px-4!"
+ data-qa-selector="editing_mode_switcher"
+ @click="
+ switchEditorType();
+ dismiss();
+ "
+ >{{ text }}</gl-button
+ >
+ </div>
+ </template>
+ </user-callout-dismisser>
+ </div>
</template>
+<style>
+.rich-text-promo-popover {
+ box-shadow: 0 0 18px -1.9px rgba(119, 89, 194, 0.16), 0 0 12.9px -1.7px rgba(119, 89, 194, 0.16),
+ 0 0 9.2px -1.4px rgba(119, 89, 194, 0.16), 0 0 6.4px -1.1px rgba(119, 89, 194, 0.16),
+ 0 0 4.5px -0.8px rgba(119, 89, 194, 0.16), 0 0 3px -0.6px rgba(119, 89, 194, 0.16),
+ 0 0 1.8px -0.3px rgba(119, 89, 194, 0.16), 0 0 0.6px rgba(119, 89, 194, 0.16);
+ z-index: 999;
+}
+
+.rich-text-promo-popover-illustration {
+ width: calc(100% + 32px);
+ margin: -32px -16px 0;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 602a83132e4..7c569763a75 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -68,10 +68,10 @@ export default {
required: false,
default: false,
},
- quickActionsDocsPath: {
- type: String,
+ supportsQuickActions: {
+ type: Boolean,
required: false,
- default: '',
+ default: false,
},
canAttachFile: {
type: Boolean,
@@ -355,10 +355,7 @@ export default {
<template>
<div
ref="gl-form"
- :class="{
- 'gl-border-none! gl-shadow-none!': removeBorder,
- }"
- class="js-vue-markdown-field md-area position-relative gfm-form"
+ class="js-vue-markdown-field md-area position-relative gfm-form gl-overflow-hidden"
:data-uploads-path="uploadsPath"
>
<markdown-header
@@ -371,13 +368,12 @@ export default {
:uploads-path="uploadsPath"
:markdown-preview-path="markdownPreviewPath"
:drawio-enabled="drawioEnabled"
+ :supports-quick-actions="supportsQuickActions"
data-testid="markdownHeader"
:restricted-tool-bar-items="restrictedToolBarItems"
- :show-content-editor-switcher="showContentEditorSwitcher"
@showPreview="showPreview"
@hidePreview="hidePreview"
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
- @enableContentEditor="$emit('enableContentEditor')"
/>
<div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop">
@@ -391,36 +387,31 @@ export default {
</a>
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
:show-comment-tool-bar="showCommentToolBar"
+ :show-content-editor-switcher="showContentEditorSwitcher"
+ @enableContentEditor="$emit('enableContentEditor')"
/>
</div>
</div>
- <template v-if="hasSuggestion">
- <div
- v-show="previewMarkdown"
- ref="markdown-preview"
- class="js-vue-md-preview md-preview-holder gl-px-5"
- >
- <suggestions
- v-if="hasSuggestion"
- :note-html="markdownPreview"
- :line-type="lineType"
- :disabled="true"
- :suggestions="suggestions"
- :help-page-path="helpPagePath"
- />
- </div>
- </template>
- <template v-else>
- <div
- v-show="previewMarkdown"
- ref="markdown-preview"
- v-safe-html:[$options.safeHtmlConfig]="markdownPreview"
- class="js-vue-md-preview md md-preview-holder gl-px-5"
- ></div>
- </template>
+ <div
+ v-show="previewMarkdown"
+ ref="markdown-preview"
+ class="js-vue-md-preview md-preview-holder gl-px-5"
+ :class="{ md: !hasSuggestion }"
+ >
+ <suggestions
+ v-if="hasSuggestion"
+ :note-html="markdownPreview"
+ :line-type="lineType"
+ :disabled="true"
+ :suggestions="suggestions"
+ :help-page-path="helpPagePath"
+ />
+ <template v-else>
+ <div v-safe-html:[$options.safeHtmlConfig]="markdownPreview"></div>
+ </template>
+ </div>
<div
v-if="referencedCommands && previewMarkdown && !markdownPreviewLoading"
v-safe-html:[$options.safeHtmlConfig]="referencedCommands"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index af0b34f1389..0907e064e01 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -13,13 +13,13 @@ import {
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getModifierKey } from '~/constants';
import { getSelectedFragment } from '~/lib/utils/common_utils';
-import { s__, __ } from '~/locale';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { s__, __, sprintf } from '~/locale';
import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import { updateText } from '~/lib/utils/text_markdown';
import ToolbarButton from './toolbar_button.vue';
import DrawioToolbarButton from './drawio_toolbar_button.vue';
import CommentTemplatesDropdown from './comment_templates_dropdown.vue';
-import EditorModeSwitcher from './editor_mode_switcher.vue';
export default {
components: {
@@ -29,7 +29,6 @@ export default {
DrawioToolbarButton,
CommentTemplatesDropdown,
AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'),
- EditorModeSwitcher,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -40,6 +39,7 @@ export default {
default: null,
},
editorAiActions: { default: () => [] },
+ mrGeneratedContent: { default: null },
},
props: {
previewMarkdown: {
@@ -91,17 +91,19 @@ export default {
required: false,
default: false,
},
- showContentEditorSwitcher: {
+ supportsQuickActions: {
type: Boolean,
required: false,
default: false,
},
},
data() {
+ const modifierKey = getModifierKey();
return {
tag: '> ',
suggestPopoverVisible: false,
- modifierKey: getModifierKey(),
+ modifierKey,
+ shiftKey: modifierKey === '⌘' ? '⇧' : 'Shift+',
};
},
computed: {
@@ -126,9 +128,6 @@ export default {
const expandText = s__('MarkdownEditor|Click to expand');
return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n');
},
- showEditorModeSwitcher() {
- return this.showContentEditorSwitcher && !this.previewMarkdown;
- },
},
watch: {
showSuggestPopover() {
@@ -199,17 +198,25 @@ export default {
insertIntoTextarea(text) {
const textArea = this.$el.closest('.md-area')?.querySelector('textarea');
if (textArea) {
- const generatedByText = `${text}\n\n---\n\n_${__('This comment was generated by AI')}_`;
updateText({
textArea,
- tag: generatedByText,
+ tag: text,
cursorOffset: 0,
wrap: false,
});
}
},
- handleEditorModeChanged() {
- this.$emit('enableContentEditor');
+ replaceTextarea(text) {
+ const { description, descriptionForSha } = this.$options.i18n;
+ const headSha = document.getElementById('merge_request_diff_head_sha').value;
+ const addendum = headSha
+ ? sprintf(descriptionForSha, { revision: truncateSha(headSha) })
+ : description;
+
+ if (this.mrGeneratedContent) {
+ this.mrGeneratedContent.setGeneratedContent(`${text}\n\n---\n\n_${addendum}_`);
+ this.mrGeneratedContent.showWarning();
+ }
},
switchPreview() {
if (this.previewMarkdown) {
@@ -218,6 +225,12 @@ export default {
this.showMarkdownPreview();
}
},
+ insertAIAction(text) {
+ this.insertIntoTextarea(`${text}\n\n---\n\n_${__('This comment was generated by AI')}_`);
+ },
+ insertSavedReply(savedReply) {
+ this.insertIntoTextarea(savedReply);
+ },
},
shortcuts: {
bold: keysFor(BOLD_TEXT),
@@ -228,27 +241,36 @@ export default {
outdent: keysFor(OUTDENT_LINE),
},
i18n: {
- preview: __('Preview'),
+ comment: __('This comment was generated by AI'),
+ description: s__('MergeRequest|This description was generated using AI'),
+ descriptionForSha: s__(
+ 'MergeRequest|This description was generated for revision %{revision} using AI',
+ ),
hidePreview: __('Continue editing'),
+ preview: __('Preview'),
},
};
</script>
<template>
- <div class="md-header gl-bg-gray-50 gl-px-2 gl-rounded-base gl-mx-2 gl-mt-2">
- <div
- class="gl-display-flex gl-align-items-center gl-flex-wrap"
- :class="{
- 'gl-justify-content-end': previewMarkdown,
- 'gl-justify-content-space-between': !previewMarkdown,
- }"
- >
+ <div class="md-header gl-border-b gl-border-gray-100 gl-px-3">
+ <div class="gl-display-flex gl-align-items-center gl-flex-wrap">
<div
data-testid="md-header-toolbar"
- class="md-header-toolbar gl-display-flex gl-py-2 gl-flex-wrap"
- :class="{ 'gl-display-none!': previewMarkdown }"
+ class="md-header-toolbar gl-display-flex gl-py-3 gl-flex-wrap gl-row-gap-3"
>
- <template v-if="canSuggest">
+ <gl-button
+ v-if="enablePreview"
+ data-testid="preview-toggle"
+ value="preview"
+ :label="$options.i18n.previewTabTitle"
+ class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2"
+ size="small"
+ category="tertiary"
+ @click="switchPreview"
+ >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button
+ >
+ <template v-if="!previewMarkdown && canSuggest">
<toolbar-button
ref="suggestButton"
:tag="mdSuggestion"
@@ -289,11 +311,13 @@ export default {
</gl-popover>
</template>
<ai-actions-dropdown
- v-if="editorAiActions.length"
+ v-if="!previewMarkdown && editorAiActions.length"
:actions="editorAiActions"
- @input="insertIntoTextarea"
+ @input="insertAIAction"
+ @replace="replaceTextarea"
/>
<toolbar-button
+ v-show="!previewMarkdown"
tag="**"
:button-title="
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
@@ -305,6 +329,7 @@ export default {
icon="bold"
/>
<toolbar-button
+ v-show="!previewMarkdown"
tag="_"
:button-title="
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
@@ -317,11 +342,13 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('strikethrough')"
+ v-show="!previewMarkdown"
tag="~~"
:button-title="
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), {
- modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}%{shiftKey}X)'), {
+ modifierKey,
+ shiftKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
})
"
:shortcuts="$options.shortcuts.strikethrough"
@@ -329,14 +356,22 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('quote')"
+ v-show="!previewMarkdown"
:prepend="true"
:tag="tag"
:button-title="__('Insert a quote')"
icon="quote"
@click="handleQuote"
/>
- <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
<toolbar-button
+ v-show="!previewMarkdown"
+ tag="`"
+ tag-block="```"
+ :button-title="__('Insert code')"
+ icon="code"
+ />
+ <toolbar-button
+ v-show="!previewMarkdown"
tag="[{text}](url)"
tag-select="url"
:button-title="
@@ -350,6 +385,7 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('bullet-list')"
+ v-show="!previewMarkdown"
:prepend="true"
tag="- "
:button-title="__('Add a bullet list')"
@@ -357,6 +393,7 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('numbered-list')"
+ v-show="!previewMarkdown"
:prepend="true"
tag="1. "
:button-title="__('Add a numbered list')"
@@ -364,6 +401,7 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('task-list')"
+ v-show="!previewMarkdown"
:prepend="true"
tag="- [ ] "
:button-title="__('Add a checklist')"
@@ -371,6 +409,7 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('indent')"
+ v-show="!previewMarkdown"
class="gl-display-none"
:button-title="
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
@@ -384,6 +423,7 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('outdent')"
+ v-show="!previewMarkdown"
class="gl-display-none"
:button-title="
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
@@ -397,6 +437,7 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('collapsible-section')"
+ v-show="!previewMarkdown"
:tag="mdCollapsibleSection"
:prepend="true"
tag-select="Click to expand"
@@ -405,17 +446,18 @@ export default {
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('table')"
+ v-show="!previewMarkdown"
:tag="mdTable"
:prepend="true"
:button-title="__('Add a table')"
icon="table"
/>
<gl-button
- v-if="!restrictedToolBarItems.includes('attach-file')"
+ v-if="!previewMarkdown && !restrictedToolBarItems.includes('attach-file')"
v-gl-tooltip
:aria-label="__('Attach a file or image')"
:title="__('Attach a file or image')"
- class="gl-mr-2"
+ class="gl-mr-3"
data-testid="button-attach-file"
category="tertiary"
icon="paperclip"
@@ -423,46 +465,37 @@ export default {
@click="handleAttachFile"
/>
<drawio-toolbar-button
- v-if="drawioEnabled"
+ v-if="!previewMarkdown && drawioEnabled"
:uploads-path="uploadsPath"
:markdown-preview-path="markdownPreviewPath"
/>
+ <!-- TODO Add icon and trigger functionality from here -->
+ <toolbar-button
+ v-if="supportsQuickActions"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="/"
+ :button-title="__('Add a quick action')"
+ icon="quick-actions"
+ />
<comment-templates-dropdown
- v-if="newCommentTemplatePath && glFeatures.savedReplies"
+ v-if="!previewMarkdown && newCommentTemplatePath && glFeatures.savedReplies"
:new-comment-template-path="newCommentTemplatePath"
+ @select="insertSavedReply"
/>
- </div>
- <div class="switch-preview gl-py-2 gl-display-flex gl-align-items-center gl-ml-auto">
- <editor-mode-switcher
- v-if="showEditorModeSwitcher"
- size="small"
- class="gl-mr-2"
- value="markdown"
- @input="handleEditorModeChanged"
- />
- <gl-button
- v-if="enablePreview"
- data-testid="preview-toggle"
- value="preview"
- :label="$options.i18n.previewTabTitle"
- class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal!"
- size="small"
- category="tertiary"
- @click="switchPreview"
- >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button
- >
- <gl-button
- v-if="!restrictedToolBarItems.includes('full-screen')"
- v-gl-tooltip
- :class="{ 'gl-display-none!': previewMarkdown }"
- class="js-zen-enter gl-ml-2"
- category="tertiary"
- icon="maximize"
- size="small"
- :title="__('Go full screen')"
- :prepend="true"
- :aria-label="__('Go full screen')"
- />
+ <div v-if="!previewMarkdown" class="full-screen">
+ <gl-button
+ v-if="!restrictedToolBarItems.includes('full-screen')"
+ v-gl-tooltip
+ class="js-zen-enter"
+ category="tertiary"
+ icon="maximize"
+ size="small"
+ :title="__('Go full screen')"
+ :prepend="true"
+ :aria-label="__('Go full screen')"
+ />
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 9fd606d775d..8b8247a5b2c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -5,6 +5,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { updateDraft, clearDraft, getDraft } from '~/lib/utils/autosave';
import { setUrlParams, joinPaths } from '~/lib/utils/url_utility';
import {
+ EDITING_MODE_KEY,
EDITING_MODE_MARKDOWN_FIELD,
EDITING_MODE_CONTENT_EDITOR,
CLEAR_AUTOSAVE_ENTRY_EVENT,
@@ -80,11 +81,6 @@ export default {
required: false,
default: '',
},
- quickActionsDocsPath: {
- type: String,
- required: false,
- default: '',
- },
drawioEnabled: {
type: Boolean,
required: false,
@@ -100,6 +96,11 @@ export default {
required: false,
default: false,
},
+ codeSuggestionsConfig: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
data() {
return {
@@ -171,7 +172,7 @@ export default {
renderMarkdown(markdown) {
const url = setUrlParams(
{ render_quick_actions: this.supportsQuickActions },
- joinPaths(window.location.origin, gon.relative_url_root, this.renderMarkdownPath),
+ joinPaths(gon.relative_url_root || window.location.origin, this.renderMarkdownPath),
);
return axios.post(url, { text: markdown }).then(({ data }) => data.body);
},
@@ -223,14 +224,15 @@ export default {
}
},
},
+ EDITING_MODE_KEY,
};
</script>
<template>
- <div class="md-area gl-px-0! gl-overflow-hidden">
+ <div class="gl-px-0!">
<local-storage-sync
:value="editingMode"
as-string
- storage-key="gl-markdown-editor-mode"
+ :storage-key="$options.EDITING_MODE_KEY"
@input="onEditingModeRestored"
/>
<markdown-field
@@ -240,12 +242,16 @@ export default {
data-testid="markdown-field"
:markdown-preview-path="renderMarkdownPath"
:can-attach-file="!disableAttachments"
+ :can-suggest="codeSuggestionsConfig.canSuggest"
+ :line="codeSuggestionsConfig.line"
+ :lines="codeSuggestionsConfig.lines"
+ :show-suggest-popover="codeSuggestionsConfig.showPopover"
:textarea-value="markdown"
:uploads-path="uploadsPath"
:enable-autocomplete="enableAutocomplete"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
+ :supports-quick-actions="supportsQuickActions"
:show-content-editor-switcher="enableContentEditor"
:drawio-enabled="drawioEnabled"
:restricted-tool-bar-items="markdownFieldRestrictedToolBarItems"
@@ -272,9 +278,10 @@ export default {
<content-editor
ref="contentEditor"
:render-markdown="renderMarkdown"
+ :markdown-docs-path="markdownDocsPath"
:uploads-path="uploadsPath"
:markdown="markdown"
- :quick-actions-docs-path="quickActionsDocsPath"
+ :supports-quick-actions="supportsQuickActions"
:autofocus="contentEditorAutofocused"
:placeholder="formFieldProps.placeholder"
:drawio-enabled="drawioEnabled"
@@ -282,6 +289,7 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
:editable="!disabled"
:disable-attachments="disableAttachments"
+ :code-suggestions-config="codeSuggestionsConfig"
@initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
@keydown="$emit('keydown', $event)"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
index 8ff14220eab..0b0867ae84c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createApolloClient from '~/lib/graphql';
import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
+
import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '../../constants';
import MarkdownEditor from './markdown_editor.vue';
import eventHub from './eventhub';
@@ -51,8 +54,13 @@ function mountAutosaveClearOnSubmit(autosaveKey) {
}
}
-export function mountMarkdownEditor() {
+export function mountMarkdownEditor(options = {}) {
const el = document.querySelector('.js-markdown-editor');
+ const componentConfiguration = {
+ provide: {
+ ...options.provide,
+ },
+ };
if (!el) {
return null;
@@ -71,6 +79,7 @@ export function mountMarkdownEditor() {
const supportsQuickActions = parseBoolean(el.dataset.supportsQuickActions ?? true);
const enableAutocomplete = parseBoolean(el.dataset.enableAutocomplete ?? true);
const disableAttachments = parseBoolean(el.dataset.disableAttachments ?? false);
+ const autofocus = parseBoolean(el.dataset.autofocus ?? true);
const hiddenInput = el.querySelector('input[type="hidden"]');
const formFieldName = hiddenInput.getAttribute('name');
const formFieldId = hiddenInput.getAttribute('id');
@@ -86,6 +95,9 @@ export function mountMarkdownEditor() {
const setFacade = (props) => Object.assign(facade, props);
const autosaveKey = `autosave/${document.location.pathname}/${searchTerm}/description`;
+ componentConfiguration.apolloProvider =
+ options.apolloProvider || new VueApollo({ defaultClient: createApolloClient() });
+
// eslint-disable-next-line no-new
new Vue({
el,
@@ -110,10 +122,11 @@ export function mountMarkdownEditor() {
autocompleteDataSources: gl.GfmAutoComplete?.dataSources,
supportsQuickActions,
disableAttachments,
- autofocus: true,
+ autofocus,
},
});
},
+ ...componentConfiguration,
});
mountAutosaveClearOnSubmit(autosaveKey);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js
new file mode 100644
index 00000000000..0ba6a44d153
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js
@@ -0,0 +1,89 @@
+import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
+
+export default {
+ title: 'vue_shared/non_gfm_markdown',
+ component: Markdown,
+ parameters: {
+ docs: {
+ description: {
+ component: `
+This component is designed to render the markdown, which is **not** the GitLab Flavored Markdown.
+
+It renders the code snippets the same way GitLab Flavored Markdown code snippets are rendered
+respecting the user's preferred color scheme and featuring a copy-code button.
+
+This component can be used to render client-side markdown that doesn't have GitLab-specific markdown elements such as issue links.
+`,
+ },
+ },
+ },
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { Markdown },
+ props: Object.keys(argTypes),
+ template: '<markdown v-bind="$props" />',
+});
+
+const textWithCodeblock = `
+#### Here is the text with the code block.
+
+\`\`\`javascript
+function sayHi(name) {
+ console.log('Hi ' + name || 'Mark');
+}
+\`\`\`
+
+It *can* have **formatting** as well
+`;
+
+export const OneCodeBlock = Template.bind({});
+OneCodeBlock.args = { markdown: textWithCodeblock };
+
+const textWithMultipleCodeBlocks = `
+#### Here is the text with the code block.
+
+\`\`\`javascript
+function sayHi(name) {
+ console.log('Hi ' + name || 'Mark');
+}
+\`\`\`
+
+Note that the copy buttons are appearing independently
+
+\`\`\`yaml
+stages:
+ - build
+ - test
+ - deploy
+\`\`\`
+`;
+
+export const MultipleCodeBlocks = Template.bind({});
+MultipleCodeBlocks.args = { markdown: textWithMultipleCodeBlocks };
+
+const textUndefinedLanguage = `
+#### Here is the code block with no language provided.
+
+\`\`\`
+function sayHi(name) {
+ console.log('Hi ' + name || 'Mark');
+}
+\`\`\`
+`;
+
+export const UndefinedLanguage = Template.bind({});
+UndefinedLanguage.args = { markdown: textUndefinedLanguage };
+
+const textCodeOneLiner = `
+#### Here is the text with the one-liner code block.
+
+Note that copy button rendering is ok.
+
+\`\`\`javascript
+const foo = 'bar';
+\`\`\`
+`;
+
+export const CodeOneLiner = Template.bind({});
+CodeOneLiner.args = { markdown: textCodeOneLiner };
diff --git a/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue
new file mode 100644
index 00000000000..814e59681d0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue
@@ -0,0 +1,120 @@
+<script>
+/*
+This component is designed to render the markdown, which is **not** the GitLab Flavored Markdown.
+
+It renders the code snippets the same way GitLab Flavored Markdown code snippets are rendered
+respecting the user's preferred color scheme and featuring a copy-code button.
+
+This component can be used to render client-side markdown that doesn't have GitLab-specific markdown elements such as issue links.
+*/
+import { marked } from 'marked';
+import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { sanitize } from '~/lib/dompurify';
+import { markdownConfig } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+export default {
+ components: {
+ CodeBlockHighlighted,
+ ModalCopyButton,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ markdown: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ hoverMap: {},
+ };
+ },
+ computed: {
+ markdownBlocks() {
+ // we use lexer https://marked.js.org/using_pro#lexer
+ // to get an array of tokens that marked npm module uses.
+ // We will use these tokens to override rendering of some of them
+ // with our vue components
+ const tokens = marked.lexer(this.markdown);
+
+ // since we only want to differentiate between code and non-code blocks
+ // we want non-code blocks merged together so that the markdown parser could render
+ // them according to the markdown rules.
+ // This way we introduce minimum extra wrapper mark-up
+ const flattenedTokens = [];
+
+ for (const token of tokens) {
+ const lastFlattenedToken = flattenedTokens[flattenedTokens.length - 1];
+ if (token.type === 'code') {
+ flattenedTokens.push(token);
+ } else if (lastFlattenedToken?.type === 'markdown') {
+ lastFlattenedToken.raw += token.raw;
+ } else {
+ flattenedTokens.push({ type: 'markdown', raw: token.raw });
+ }
+ }
+
+ return flattenedTokens;
+ },
+ },
+ methods: {
+ getSafeHtml(markdown) {
+ return sanitize(marked.parse(markdown), markdownConfig);
+ },
+ setHoverOn(key) {
+ this.hoverMap = { ...this.hoverMap, [key]: true };
+ },
+ setHoverOff(key) {
+ this.hoverMap = { ...this.hoverMap, [key]: false };
+ },
+ isLastElement(index) {
+ return index === this.markdownBlocks.length - 1;
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
+ },
+ i18n: {
+ copyCodeTitle: __('Copy code'),
+ },
+ fallbackLanguage: 'text',
+};
+</script>
+<template>
+ <div>
+ <template v-for="(block, index) in markdownBlocks">
+ <div
+ v-if="block.type === 'code'"
+ :key="`code-${index}`"
+ :class="{ 'gl-relative': true, 'gl-mb-4': !isLastElement(index) }"
+ data-testid="code-block-wrapper"
+ @mouseenter="setHoverOn(`code-${index}`)"
+ @mouseleave="setHoverOff(`code-${index}`)"
+ >
+ <modal-copy-button
+ v-if="hoverMap[`code-${index}`]"
+ :title="$options.i18n.copyCodeTitle"
+ :text="block.text"
+ class="gl-absolute gl-top-3 gl-right-3 gl-z-index-1 gl-transition-duration-medium"
+ />
+ <code-block-highlighted
+ class="gl-border gl-rounded-0! gl-p-4 gl-mb-0 gl-overflow-y-auto"
+ :language="block.lang || $options.fallbackLanguage"
+ :code="block.text"
+ />
+ </div>
+ <div
+ v-else
+ :key="`text-${index}`"
+ v-safe-html:[$options.safeHtmlConfig]="getSafeHtml(block.raw)"
+ :class="{ 'non-gfm-markdown-block': true, 'gl-mb-4': !isLastElement(index) }"
+ data-testid="non-code-markdown"
+ ></div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 6d1cadf15be..4423b26560f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -40,7 +40,8 @@ export default {
},
defaultCommitMessage: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
suggestionsCount: {
type: Number,
@@ -124,7 +125,7 @@ export default {
suggestion,
batchSuggestionsInfo,
helpPagePath,
- defaultCommitMessage,
+ defaultCommitMessage: defaultCommitMessage || '',
suggestionsCount,
failedToLoadMetadata,
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 4733afb7504..d4b1abedc02 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,24 +1,26 @@
<script>
-import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlSprintf, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { updateText } from '~/lib/utils/text_markdown';
+import { __, sprintf } from '~/locale';
+import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
+import EditorModeSwitcher from './editor_mode_switcher.vue';
export default {
components: {
GlButton,
- GlLink,
GlLoadingIcon,
GlSprintf,
GlIcon,
+ EditorModeSwitcher,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
markdownDocsPath: {
type: String,
required: true,
},
- quickActionsDocsPath: {
- type: String,
- required: false,
- default: '',
- },
canAttachFile: {
type: Boolean,
required: false,
@@ -29,10 +31,46 @@ export default {
required: false,
default: true,
},
+ showContentEditorSwitcher: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
- hasQuickActionsDocsPath() {
- return this.quickActionsDocsPath !== '';
+ showEditorModeSwitcher() {
+ return this.showContentEditorSwitcher;
+ },
+ },
+ methods: {
+ insertIntoTextarea(...lines) {
+ const text = lines.join('\n');
+ const textArea = this.$el.closest('.md-area')?.querySelector('textarea');
+ if (textArea && !textArea.value) {
+ updateText({
+ textArea,
+ tag: text,
+ cursorOffset: 0,
+ wrap: false,
+ });
+ }
+ },
+ handleEditorModeChanged(isFirstSwitch) {
+ if (isFirstSwitch) {
+ this.insertIntoTextarea(
+ __(`### Rich text editor`),
+ '',
+ sprintf(
+ __(
+ 'Try out **styling** _your_ content right here or read the [direction](%{directionUrl}).',
+ ),
+ {
+ directionUrl: `${PROMO_URL}/direction/plan/knowledge/content_editor/`,
+ },
+ ),
+ );
+ }
+ this.$emit('enableContentEditor');
},
},
};
@@ -41,94 +79,80 @@ export default {
<template>
<div
v-if="showCommentToolBar"
- class="comment-toolbar gl-mx-2 gl-mb-2 gl-px-4 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base clearfix"
+ class="comment-toolbar gl-display-flex gl-flex-direction-row gl-px-2 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ :class="
+ showContentEditorSwitcher
+ ? 'gl-justify-content-space-between gl-align-items-center gl-border-t gl-border-gray-100'
+ : 'gl-justify-content-end gl-my-2'
+ "
>
- <div class="toolbar-text gl-font-sm">
- <template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
- <gl-sprintf
- :message="
- s__('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')
- "
- >
- <template #markdownDocsLink="{ content }">
- <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </template>
- <template v-if="hasQuickActionsDocsPath && markdownDocsPath">
- <gl-sprintf
- :message="
- s__(
- 'NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.',
- )
- "
- >
- <template #markdownDocsLink="{ content }">
- <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- <template #keyboard="{ content }">
- <kbd>{{ content }}</kbd>
- </template>
- <template #quickActionsDocsLink="{ content }">
- <gl-link :href="quickActionsDocsPath" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </template>
- </div>
- <span v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32">
- <span class="uploading-progress-container hide">
- <gl-icon name="paperclip" />
- <span class="attaching-file-message"></span>
- <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
- <span class="uploading-progress">0%</span>
- <gl-loading-icon size="sm" inline />
- </span>
- <span class="uploading-error-container hide">
- <span class="uploading-error-icon">
+ <editor-mode-switcher
+ v-if="showEditorModeSwitcher"
+ size="small"
+ value="markdown"
+ @switch="handleEditorModeChanged"
+ />
+ <div class="gl-display-flex">
+ <div v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32 gl-mr-3">
+ <span class="uploading-progress-container hide">
<gl-icon name="paperclip" />
+ <span class="attaching-file-message"></span>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <span class="uploading-progress">0%</span>
+ <gl-loading-icon size="sm" inline />
</span>
- <span class="uploading-error-message"></span>
+ <span class="uploading-error-container hide">
+ <span class="uploading-error-icon">
+ <gl-icon name="paperclip" />
+ </span>
+ <span class="uploading-error-message"></span>
- <gl-sprintf
- :message="
- __(
- '%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.',
- )
- "
+ <gl-sprintf
+ :message="
+ __(
+ '%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.',
+ )
+ "
+ >
+ <template #retryButton="{ content }">
+ <gl-button
+ variant="link"
+ category="primary"
+ class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!"
+ >
+ {{ content }}
+ </gl-button>
+ </template>
+ <template #newFileButton="{ content }">
+ <gl-button
+ variant="link"
+ category="primary"
+ class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!"
+ >
+ {{ content }}
+ </gl-button>
+ </template>
+ </gl-sprintf>
+ </span>
+ <gl-button
+ variant="link"
+ category="primary"
+ class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!"
>
- <template #retryButton="{ content }">
- <gl-button
- variant="link"
- category="primary"
- class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!"
- >
- {{ content }}
- </gl-button>
- </template>
- <template #newFileButton="{ content }">
- <gl-button
- variant="link"
- category="primary"
- class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!"
- >
- {{ content }}
- </gl-button>
- </template>
- </gl-sprintf>
- </span>
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
<gl-button
- variant="link"
- category="primary"
- class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!"
- >
- {{ __('Cancel') }}
- </gl-button>
- </span>
+ v-if="markdownDocsPath"
+ v-gl-tooltip
+ icon="markdown-mark"
+ :href="markdownDocsPath"
+ target="_blank"
+ category="tertiary"
+ size="small"
+ title="Markdown is supported"
+ class="gl-px-3!"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/tracking.js b/app/assets/javascripts/vue_shared/components/markdown/tracking.js
new file mode 100644
index 00000000000..2628054ae5f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/tracking.js
@@ -0,0 +1,14 @@
+import Tracking from '~/tracking';
+
+export const EDITOR_TRACKING_LABEL = 'editor_tracking';
+export const EDITOR_TYPE_ACTION = 'editor_type_used';
+export const EDITOR_TYPE_PLAIN_TEXT_EDITOR = 'editor_type_plain_text_editor';
+export const EDITOR_TYPE_RICH_TEXT_EDITOR = 'editor_type_rich_text_editor';
+
+export const trackSavedUsingEditor = (isRichText, context) => {
+ Tracking.event(undefined, EDITOR_TYPE_ACTION, {
+ label: EDITOR_TRACKING_LABEL,
+ editorType: isRichText ? EDITOR_TYPE_RICH_TEXT_EDITOR : EDITOR_TYPE_PLAIN_TEXT_EDITOR,
+ context,
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/utils.js b/app/assets/javascripts/vue_shared/components/markdown/utils.js
new file mode 100644
index 00000000000..0227d5a0fbc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/utils.js
@@ -0,0 +1,7 @@
+let i = 0;
+
+export const counter = () => {
+ const n = i;
+ i += 1;
+ return n;
+};