diff --git a/app/assets/javascripts/batch_comments/components/review_drawer.vue b/app/assets/javascripts/batch_comments/components/review_drawer.vue index 82d3748e95171b0895add9bb4b0bbd51c0a5f346..440830f1e585aa634a38e82c9c49efc1798d96b6 100644 --- a/app/assets/javascripts/batch_comments/components/review_drawer.vue +++ b/app/assets/javascripts/batch_comments/components/review_drawer.vue @@ -325,6 +325,7 @@ export default { :force-autosize="false" :autosave-key="autosaveKey" supports-quick-actions + :supports-table-of-contents="false" autofocus @input="$emit('input', $event)" @keydown.meta.enter="submitReview" diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index cc714b8f5b82244b25160552b4426546f505f7e9..d0a0341534111768ae1afead312219d2c6122634 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -86,6 +86,11 @@ export default { required: false, default: false, }, + supportsTableOfContents: { + type: Boolean, + required: false, + default: false, + }, drawioEnabled: { type: Boolean, required: false, @@ -162,6 +167,7 @@ export default { serializerConfig, autofocus, drawioEnabled, + supportsTableOfContents, editable, enableAutocomplete, autocompleteDataSources, @@ -176,6 +182,7 @@ export default { extensions, serializerConfig, drawioEnabled, + supportsTableOfContents, enableAutocomplete, autocompleteDataSources, codeSuggestionsConfig, diff --git a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue index 2591d8bdca2b54b247fee3d8242cae3193580516..f5c3117d0b9434d7400015d28b399807111e9de8 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue @@ -77,10 +77,14 @@ export default { }, ] : []), - { - text: __('Table of contents'), - action: () => this.execute('insertTableOfContents', 'tableOfContents'), - }, + ...(this.contentEditor.supportsTableOfContents + ? [ + { + text: __('Table of contents'), + action: () => this.execute('insertTableOfContents', 'tableOfContents'), + }, + ] + : []), ], }; }, diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js index 11d63a4c57535b79063afc8004dddb7110c25676..3e5dd540936a121ad186b63e160ab45bf45ef260 100644 --- a/app/assets/javascripts/content_editor/services/content_editor.js +++ b/app/assets/javascripts/content_editor/services/content_editor.js @@ -9,6 +9,7 @@ export class ContentEditor { assetResolver, eventHub, drawioEnabled, + supportsTableOfContents, codeSuggestionsConfig, autocompleteHelper, }) { @@ -22,6 +23,7 @@ export class ContentEditor { this.codeSuggestionsConfig = codeSuggestionsConfig; this.drawioEnabled = drawioEnabled; + this.supportsTableOfContents = supportsTableOfContents; } /** diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index f3c622c9b7e18ba800283fa5da333519befdaadd..dade2500948d54ea3b6fb0c8dfe3263cdb85e092 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -23,6 +23,7 @@ export const createContentEditor = ({ serializerConfig = { marks: {}, nodes: {} }, tiptapOptions, drawioEnabled = false, + supportsTableOfContents = false, enableAutocomplete, autocompleteDataSources = {}, sidebarMediator = {}, @@ -44,7 +45,7 @@ export const createContentEditor = ({ render: renderMarkdown, }); - const { Suggestions, DrawioDiagram, ...otherExtensions } = builtInExtensions; + const { Suggestions, DrawioDiagram, TableOfContents, ...otherExtensions } = builtInExtensions; const builtInContentEditorExtensions = flatMap(otherExtensions).map((ext) => ext.configure({ @@ -62,6 +63,7 @@ export const createContentEditor = ({ if (enableAutocomplete) allExtensions.push(Suggestions.configure({ autocompleteHelper, serializer })); if (drawioEnabled) allExtensions.push(DrawioDiagram.configure({ uploadsPath, assetResolver })); + if (supportsTableOfContents) allExtensions.push(TableOfContents); const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts); const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions }); @@ -75,6 +77,7 @@ export const createContentEditor = ({ deserializer, assetResolver, drawioEnabled, + supportsTableOfContents, codeSuggestionsConfig, autocompleteHelper, }); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index a55b37085e0d9407358ca663f974800992889bd0..9a26998595b58b75785db4fe1bfe3ef0e65f6ab0 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -421,6 +421,7 @@ export default { :autocomplete-data-sources="autocompleteDataSources" :noteable-type="noteableType" supports-quick-actions + :supports-table-of-contents="false" @keydown.up="editCurrentUserLastNote()" @keydown.shift.meta.enter="handleSave()" @keydown.shift.ctrl.enter="handleSave()" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 1a4ffda17d433777c1bec89ea78295224a72cf06..382e2af8e62f4fe0cf70787b3427e3b8a8ffd0de 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -409,6 +409,7 @@ export default { :autocomplete-data-sources="autocompleteDataSources" :disabled="shouldDisableField" supports-quick-actions + :supports-table-of-contents="false" :autofocus="autofocus" :restore-from-autosave="restoreFromAutosave" @keydown.shift.meta.enter="handleKeySubmit((forceUpdate = true))" diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 11d492441297864723d413422f87cecd425515ad..79b0bf54ab2e16cb0369dc2bb8fb39d7bfea3530 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -69,6 +69,11 @@ export default { required: false, default: false, }, + supportsTableOfContents: { + type: Boolean, + required: false, + default: false, + }, canAttachFile: { type: Boolean, required: false, @@ -316,7 +321,8 @@ export default { }, fetchMarkdown() { - return axios.post(this.markdownPreviewPath, { text: this.textareaValue }).then(({ data }) => { + const params = { text: this.textareaValue, no_header_anchors: !this.supportsTableOfContents }; + return axios.post(this.markdownPreviewPath, params).then(({ data }) => { const { references } = data; if (references) { this.referencedCommands = references.commands; 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 e9a0c0e4cc3f36e43b6859be699ff004c30b7d75..d806d9bd58cf2da620c46ab64cadee830da61bd5 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -93,6 +93,11 @@ export default { required: false, default: false, }, + supportsTableOfContents: { + type: Boolean, + required: false, + default: false, + }, autosaveKey: { type: String, required: false, @@ -280,7 +285,10 @@ export default { }, renderMarkdown(markdown) { const url = setUrlParams( - { render_quick_actions: this.supportsQuickActions }, + { + render_quick_actions: this.supportsQuickActions, + no_header_anchors: !this.supportsTableOfContents, + }, { url: joinPaths(window.location.origin, this.renderMarkdownPath) }, ); return axios @@ -419,6 +427,7 @@ export default { :autocomplete-data-sources="autocompleteDataSources" :markdown-docs-path="markdownDocsPath" :supports-quick-actions="supportsQuickActions" + :supports-table-of-contents="supportsTableOfContents" :show-content-editor-switcher="enableContentEditor" :drawio-enabled="drawioEnabled" :restricted-tool-bar-items="markdownFieldRestrictedToolBarItems" @@ -462,6 +471,7 @@ export default { :uploads-path="uploadsPath" :markdown="markdown" :supports-quick-actions="supportsQuickActions" + :supports-table-of-contents="supportsTableOfContents" :autofocus="contentEditorAutofocused" :placeholder="formFieldProps.placeholder" :drawio-enabled="drawioEnabled" 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 74b254071dbb15f36ec678fe2dddcbf2273ae5b8..609af2fd93ec08c0d6e3dc6eb22e34e47922a50c 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 @@ -77,6 +77,7 @@ export function mountMarkdownEditor(options = {}) { } = el.dataset; const supportsQuickActions = parseBoolean(el.dataset.supportsQuickActions ?? true); + const supportsTableOfContents = parseBoolean(el.dataset.supportsTableOfContents ?? false); const enableAutocomplete = parseBoolean(el.dataset.enableAutocomplete ?? true); const disableAttachments = parseBoolean(el.dataset.disableAttachments ?? false); const canUseComposer = parseBoolean(el.dataset.canUseComposer ?? false); @@ -123,6 +124,7 @@ export function mountMarkdownEditor(options = {}) { enableAutocomplete, autocompleteDataSources: gl.GfmAutoComplete?.dataSources, supportsQuickActions, + supportsTableOfContents, disableAttachments, autofocus, }, diff --git a/app/assets/javascripts/wikis/components/wiki_form.vue b/app/assets/javascripts/wikis/components/wiki_form.vue index 0dc026aeb81405860ff9c5b477eee6b52064d44a..ddf4a8c37e2b391ac65a0f40a3d22e6a6cc280f9 100644 --- a/app/assets/javascripts/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/wikis/components/wiki_form.vue @@ -595,6 +595,7 @@ export default { :enable-autocomplete="true" :autocomplete-data-sources="autocompleteDataSources" :drawio-enabled="drawioEnabled" + supports-table-of-contents :disable-attachments="isTemplate" :immersive="glFeatures.wikiImmersiveEditor" @contentEditor="notifyContentEditorActive" diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue index 21cd6b64b2a0c7e4b1639693ea6f6f57ae6826c0..56a98d4a2ebfe826cdf7f1fc515a249e57bdf127 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue @@ -393,6 +393,7 @@ export default { :data-work-item-type-id="workItemTypeId" use-bottom-toolbar supports-quick-actions + :supports-table-of-contents="false" :autofocus="autofocus" :restricted-tool-bar-items="restrictedToolBarItems" @focus="$emit('focus')" diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index 001d6341c096484d328e4144625e05922c64486a..26eb4841047a15040d222d94b8025d8bc4898a3f 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -585,6 +585,7 @@ export default { :editor-ai-actions="editorAiActions" enable-autocomplete supports-quick-actions + supports-table-of-contents :autofocus="autofocus" class="gl-mt-3" @input="setDescriptionText" diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index 3d62139eccda1369cc0314d51468c5ccb1893732..5c8c369f19902a46ff6f4afc4f92ce85f080f09b 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -31,7 +31,7 @@ def resource_parent def projects_filter_params { issuable_reference_expansion_enabled: true, - suggestions_filter_enabled: params[:preview_suggestions].present? + suggestions_filter_enabled: Gitlab::Utils.to_boolean(params[:preview_suggestions]) } end @@ -73,7 +73,8 @@ def markdown_context_params ref: params[:ref], # Disable comments in markdown for IE browsers because comments in IE # could allow script execution. - allow_comments: !browser.ie? + allow_comments: !browser.ie?, + no_header_anchors: params[:target_type] == 'Commit' || Gitlab::Utils.to_boolean(params[:no_header_anchors]) ) end end diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb index 5278893bbd66eb9b66584d61f52e21a88bb6e8ba..e8724406ac546c923d3598198bd1e9b99f08dd87 100644 --- a/app/controllers/projects/merge_requests/drafts_controller.rb +++ b/app/controllers/projects/merge_requests/drafts_controller.rb @@ -165,7 +165,7 @@ def render_draft_note(note) params = { target_id: merge_request.iid, target_type: 'MergeRequest', text: note.note } result = PreviewMarkdownService.new(container: @project, current_user: current_user, params: params) .execute do |text| - markdown_params = { issuable_reference_expansion_enabled: true } + markdown_params = { issuable_reference_expansion_enabled: true, no_header_anchors: true } view_context.markdown(text, markdown_params) end diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index f9f3a7be621f0b45b1dc036f7acc67c986193cdb..301d242256785d771260fa001ce0ef53dccb277c 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -30,7 +30,8 @@ def explain_quick_actions(text) return text, [] unless quick_action_types.include?(target_type) quick_actions_service = QuickActions::InterpretService.new(container: container, current_user: current_user) - quick_actions_service.explain(text, find_commands_target, keep_actions: params[:render_quick_actions]) + quick_actions_service.explain(text, find_commands_target, + keep_actions: Gitlab::Utils.to_boolean(params[:render_quick_actions])) end def find_user_references(text) @@ -52,11 +53,11 @@ def find_suggestions(text) Gitlab::Diff::SuggestionsParser.parse(text, position: position, project: project, - supports_suggestion: params[:preview_suggestions]) + supports_suggestion: Gitlab::Utils.to_boolean(params[:preview_suggestions])) end def preview_sugestions? - params[:preview_suggestions] && + Gitlab::Utils.to_boolean(params[:preview_suggestions]) && target_type == 'MergeRequest' && Ability.allowed?(current_user, :download_code, project) end diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml index b209c5969c0f27875352353383c8b8572cf3a683..60d1a1dfc4b2ab731b9401cefa8bdc9495fe7607 100644 --- a/app/views/admin/topics/_form.html.haml +++ b/app/views/admin/topics/_form.html.haml @@ -23,6 +23,7 @@ testid: 'topic-form-description', form_field_placeholder: _('Write a description…'), supports_quick_actions: 'false', + supports_table_of_contents: 'true', enable_autocomplete: 'false', disable_attachments: 'true', autofocus: 'false', diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index f710c02cd25bd8e7c2a92fad5b99c3cbb7be8071..b1a58659ef8b7695c59db78f8e4dbc19bed02098 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -16,6 +16,7 @@ testid: 'milestone-description-field', form_field_placeholder: _('Write milestone description…'), supports_quick_actions: 'false', + supports_table_of_contents: 'true', enable_autocomplete: 'true', autofocus: 'false', form_field_classes: 'note-textarea js-gfm-input markdown-area' } } diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 892b696df1095788d086f7f003fd271fb3c8991c..c89abece5d13869054d7e3db3dee64982f975804 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -18,6 +18,7 @@ testid: 'milestone-description-field', form_field_placeholder: _('Write milestone description...'), supports_quick_actions: 'false', + supports_table_of_contents: 'true', enable_autocomplete: 'true', autofocus: 'false', form_field_classes: 'note-textarea js-gfm-input markdown-area' } } diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index 27f6e22ce53d1f429444bd74cc2c3155ca8f0124..48bdf977e389b9293779ec7c2f2b06f69f999b00 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -26,6 +26,7 @@ testid: 'issuable-form-description-field', form_field_placeholder: placeholder, autofocus: 'false', + supports_table_of_contents: 'true', form_field_classes: 'js-gfm-input markdown-area note-textarea rspec-issuable-form-description', project_id: @project.id, can_use_composer: is_merge_request ? can_use_description_composer(current_user, model).to_s : nil, diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb index 8ecf8512954665e4ba492845aed3e254a7b87606..f57aeb490e526d5ac41aa1dff1f1f077d10b065f 100644 --- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb @@ -106,6 +106,20 @@ def create_draft_note(draft_overrides: {}, overrides: {}) expect(json_response['references']['users']).to include(user2.username) end + it 'does not render tables of contents in draft nodes' do + note = <<~MARKDOWN + [[_TOC_]] + + # Bonjour + ## Zdravo + ### Здраво + MARKDOWN + create_draft_note(draft_overrides: { note: note }) + + expect(json_response['note_html']).to include('TOC') + expect(json_response['note_html']).not_to include('

{ const findPlaceholderField = () => wrapper.findByTestId('placeholder-input-field'); const findDiscardReviewButton = () => wrapper.findByTestId('discard-review-btn'); const findDiscardReviewModal = () => wrapper.findByTestId('discard-review-modal'); + const findMarkdownField = () => wrapper.findComponent(MarkdownField); const submitForm = async () => { await findPlaceholderField().vm.$emit('focus'); @@ -374,4 +376,14 @@ describe('ReviewDrawer', () => { expect(useBatchComments().publishReviewInBatches).toHaveBeenCalled(); }); }); + + it('disables table of contents support in the markdown editor', async () => { + useBatchComments().drawerOpened = true; + + createComponent(); + + await findPlaceholderField().vm.$emit('focus'); + + expect(findMarkdownField().props('supportsTableOfContents')).toBe(false); + }); }); diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js index c29d09233af1c5585b1606efdb1c74c207c6a92d..2b2bcf35019b1fab69a70c3f19f6568aa072fc4f 100644 --- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js +++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js @@ -20,7 +20,7 @@ describe('content_editor/components/toolbar_more_dropdown', () => { tiptapEditor = createTestEditor({ extensions: [Diagram, HorizontalRule], }); - contentEditor = { drawioEnabled: true }; + contentEditor = { drawioEnabled: true, supportsTableOfContents: true }; eventHub = eventHubFactory(); }; @@ -85,6 +85,13 @@ describe('content_editor/components/toolbar_more_dropdown', () => { expect(wrapper.findByRole('button', { name: 'Create or edit diagram' }).exists()).toBe(false); }); + it('does not show TOC option when supportsTableOfContents is false', () => { + contentEditor.supportsTableOfContents = false; + buildWrapper(); + + expect(wrapper.findByRole('button', { name: 'Table of contents' }).exists()).toBe(false); + }); + describe('a11y tests', () => { it('sets toggleText and text-sr-only properties to the table button dropdown', () => { buildWrapper(); diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js index cb871c37b82807c5efec510448c64a6c1dc4e20b..7cde2b734b49217b16e718e662aa966115ea4d2f 100644 --- a/spec/frontend/content_editor/services/create_content_editor_spec.js +++ b/spec/frontend/content_editor/services/create_content_editor_spec.js @@ -59,4 +59,15 @@ describe('content_editor/services/create_content_editor', () => { assetResolver: expect.any(AssetResolver), }); }); + + it('defaults to not supporting table of contents', () => { + expect(editor.supportsTableOfContents).toBe(false); + }); + + it('allows configuring table of contents support', () => { + expect( + createContentEditor({ renderMarkdown, uploadsPath, supportsTableOfContents: true }) + .supportsTableOfContents, + ).toBe(true); + }); }); diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 665f45d80a8fd660945fa2ebd82c69c6fd64144a..0e218c718103ee89db5562a8ecfc6e22d03a203b 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -905,4 +905,9 @@ describe('issue_comment_form component', () => { wrapper.vm.append('foo'); expect(spy).toHaveBeenCalledWith('foo'); }); + + it('disables table of contents support in the markdown editor', () => { + mountComponent(); + expect(findMarkdownEditor().props('supportsTableOfContents')).toBe(false); + }); }); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 4e6502fe8e4afd7b1939c4c2fb9e510fd2c2d6ce..9bba42b624fd34c8ff77b429f659e3bf7601f9e5 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -379,4 +379,9 @@ describe('issue_note_form component', () => { wrapper.vm.append('foo'); expect(spy).toHaveBeenCalledWith('foo'); }); + + it('disables table of contents support in the markdown editor', () => { + createComponentWrapper(); + expect(findMarkdownField().props('supportsTableOfContents')).toBe(false); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index acdadf2022172a8aeef74e13d6ba60f4a31a8ac4..168dcde78eec22df6dedac2cbe8ef4bb4ecb749a 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -3,7 +3,7 @@ import { nextTick } from 'vue'; import AxiosMockAdapter from 'axios-mock-adapter'; import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants'; import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue'; import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue'; @@ -33,7 +33,7 @@ describe('Markdown field component', () => { axiosMock.restore(); }); - function createSubject({ lines = [], enablePreview = true, showContentEditorSwitcher } = {}) { + function createSubject({ lines = [], enablePreview = true, ...props } = {}) { // We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression // caused by mixing Vanilla JS and Vue. subject = mountExtended( @@ -64,8 +64,8 @@ describe('Markdown field component', () => { lines, enablePreview, restrictedToolBarItems, - showContentEditorSwitcher, supportsQuickActions: true, + ...props, }, mocks: { $apollo: { @@ -280,6 +280,44 @@ describe('Markdown field component', () => { }); }); }); + + describe('no_header_anchors', () => { + let expectedNoHeaderAnchors; + + beforeEach(() => { + axiosMock.onPost(markdownPreviewPath).reply((config) => { + if (JSON.parse(config.data).no_header_anchors === expectedNoHeaderAnchors) { + return [HTTP_STATUS_OK, `{"body":"Successful match"}`]; + } + return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ``]; + }); + }); + + it('passes no_header_anchors=true by default', async () => { + expectedNoHeaderAnchors = true; + + previewToggle = getPreviewToggle(); + previewToggle.vm.$emit('click', true); + await axios.waitFor(markdownPreviewPath); + + expect(subject.find('.md-preview-holder').element.innerHTML).toContain( + 'Successful match', + ); + }); + + it('passes no_header_anchors=false when supportsTableOfContents is set to true', async () => { + createSubject({ supportsTableOfContents: true }); + expectedNoHeaderAnchors = false; + + previewToggle = getPreviewToggle(); + previewToggle.vm.$emit('click', true); + await axios.waitFor(markdownPreviewPath); + + expect(subject.find('.md-preview-holder').element.innerHTML).toContain( + 'Successful match', + ); + }); + }); }); describe('markdown buttons', () => { diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index c9c406cbcb04082d00f46051e7324ac1c8ea1509..1a71d4665a4f0c49f90429b09ffcd5fddf68d0c6 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -130,7 +130,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(mock.history.post).toHaveLength(1); expect(mock.history.post[0].url).toBe( - `${window.location.origin}/api/markdown?render_quick_actions=true`, + `${window.location.origin}/api/markdown?render_quick_actions=true&no_header_anchors=true`, ); }); @@ -141,7 +141,18 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(mock.history.post).toHaveLength(1); expect(mock.history.post[0].url).toBe( - `${window.location.origin}/api/markdown?render_quick_actions=false`, + `${window.location.origin}/api/markdown?render_quick_actions=false&no_header_anchors=true`, + ); + }); + + it('passes no_header_anchors=false to renderMarkdownPath if table of contents are supported', async () => { + buildWrapper({ propsData: { supportsTableOfContents: true } }); + + await enableContentEditor(); + + expect(mock.history.post).toHaveLength(1); + expect(mock.history.post[0].url).toBe( + `${window.location.origin}/api/markdown?render_quick_actions=false&no_header_anchors=false`, ); }); diff --git a/spec/frontend/wikis/components/wiki_form_spec.js b/spec/frontend/wikis/components/wiki_form_spec.js index dad6f1e33837249274b8d54f98df19c07a43e5ec..af3d4a4d62d3290339dec9e3175034fe491d6f6b 100644 --- a/spec/frontend/wikis/components/wiki_form_spec.js +++ b/spec/frontend/wikis/components/wiki_form_spec.js @@ -157,6 +157,7 @@ describe('WikiForm', () => { uploadsPath: pageInfoPersisted.uploadsPath, autofocus: pageInfoPersisted.persisted, immersive: false, + supportsTableOfContents: true, }), ); diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js index 77118079488f1d37d2cb2ecc56f9c924e6d529a8..6323a3b8565333d294cc2d037662a6006cf2023a 100644 --- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js @@ -145,6 +145,12 @@ describe('Work item comment form component', () => { ); }); + it('disables table of contents in markdown editor', () => { + createComponent(); + + expect(findMarkdownEditor().props('supportsTableOfContents')).toBe(false); + }); + it('passes correct form field props to markdown editor', () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index 94cf06ee89cc1c110ed792a3e6c4fdc324924e0d..3b9cdc1e00858327b4eec270f2b3f30f52c7e5ba 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -154,11 +154,12 @@ describe('WorkItemDescription', () => { }; describe('editing description', () => { - it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => { + it('passes correct autocompletion data, preview markdown sources, enables quick actions and table of contents', async () => { await createComponent({ isEditing: true }); expect(findMarkdownEditor().props()).toMatchObject({ supportsQuickActions: true, + supportsTableOfContents: true, renderMarkdownPath: markdownPaths.markdownPreviewPath, autocompleteDataSources: markdownPaths.autocompleteSourcesPath, }); diff --git a/spec/frontend_integration/content_editor/content_editor_integration_spec.js b/spec/frontend_integration/content_editor/content_editor_integration_spec.js index aad1722c174b71a73ccf9cb1ed4693c7607cdac2..20bec6591f0869fbefe65a2e7e3125dd425cf88e 100644 --- a/spec/frontend_integration/content_editor/content_editor_integration_spec.js +++ b/spec/frontend_integration/content_editor/content_editor_integration_spec.js @@ -20,6 +20,7 @@ describe('content_editor', () => { renderMarkdown, uploadsPath: '/', markdown, + supportsTableOfContents: true, }, listeners: { ...listeners, diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb index 42d27c50fc5ed2a4d6f79f7a7681d0090f53307e..a04576ab22b4904fe207155658215c755fbda1fd 100644 --- a/spec/services/preview_markdown_service_spec.rb +++ b/spec/services/preview_markdown_service_spec.rb @@ -54,7 +54,7 @@ let(:suggestion_params) do { - preview_suggestions: true, + preview_suggestions: 'true', file_path: path, line: line, base_sha: diff_refs.base_sha, @@ -92,14 +92,16 @@ end end - context 'when preview markdown param is not present' do + context 'when preview markdown param is false' do let(:suggestion_params) do { - preview_suggestions: false + preview_suggestions: 'false' } end it 'returns suggestions referenced in text' do + expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse) + result = service.execute expect(result[:suggestions]).to eq([]) @@ -117,26 +119,34 @@ } end - it 'removes quick actions from text' do - result = service.execute + subject(:result) { service.execute } + it 'removes quick actions from text' do expect(result[:text]).to eq 'Please do it' end - context 'when render_quick_actions' do - it 'keeps quick actions' do - params[:render_quick_actions] = true + it 'explains quick actions effect' do + expect(result[:commands]).to eq "Assigns #{user.to_reference}." + end - result = service.execute + context 'when render_quick_actions is true' do + before do + params[:render_quick_actions] = 'true' + end + it 'keeps quick actions' do expect(result[:text]).to eq "Please do it\n

/assign #{user.to_reference}

" end end - it 'explains quick actions effect' do - result = service.execute + context 'when render_quick_actions is false' do + before do + params[:render_quick_actions] = 'false' + end - expect(result[:commands]).to eq "Assigns #{user.to_reference}." + it 'removes quick actions from text' do + expect(result[:text]).to eq 'Please do it' + end end end