From 8a53c04c73328474c1f031f18572a3ff44651118 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Thu, 20 Nov 2025 16:17:32 +1100 Subject: [PATCH 01/19] Don't render TOC in note previews or RTE; remove option from RTE These aren't actually rendered in notes, so don't render them in the preview pane for notes either! This same render feeds into what's shown in the RTE. We also remove the option to add a TOC from the rich text editor in such contexts. --- .../content_editor/components/content_editor.vue | 7 +++++++ .../components/toolbar_more_dropdown.vue | 12 ++++++++---- .../content_editor/services/content_editor.js | 2 ++ .../services/create_content_editor.js | 2 ++ .../vue_shared/components/markdown/field.vue | 8 +++++++- .../components/markdown/markdown_editor.vue | 12 +++++++++++- .../components/notes/work_item_comment_form.vue | 1 + app/controllers/concerns/preview_markdown.rb | 3 ++- .../components/toolbar_more_dropdown_spec.js | 9 ++++++++- .../components/markdown/markdown_editor_spec.js | 15 +++++++++++++-- 10 files changed, 61 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index cc714b8f5b8224..4a73a21b463785 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: true, + }, 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 2591d8bdca2b54..f5c3117d0b9434 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 11d63a4c57535b..3e5dd540936a12 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 f3c622c9b7e18b..decfe2f95c9c44 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 = true, enableAutocomplete, autocompleteDataSources = {}, sidebarMediator = {}, @@ -75,6 +76,7 @@ export const createContentEditor = ({ deserializer, assetResolver, drawioEnabled, + supportsTableOfContents, codeSuggestionsConfig, autocompleteHelper, }); diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 11d49244129786..c58109ca8e2b1d 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: true, + }, 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 e9a0c0e4cc3f36..72f045a9fcb75d 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: true, + }, 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/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue index 21cd6b64b2a0c7..56a98d4a2ebfe8 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/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index 3d62139eccda13..623e03f6546ddc 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -31,7 +31,8 @@ def resource_parent def projects_filter_params { issuable_reference_expansion_enabled: true, - suggestions_filter_enabled: params[:preview_suggestions].present? + suggestions_filter_enabled: params[:preview_suggestions].present?, + no_header_anchors: params[:no_header_anchors].present? } end 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 c29d09233af1c5..2b2bcf35019b1f 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/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index c9c406cbcb0408..8148b364070a07 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=false`, ); }); @@ -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=false`, + ); + }); + + it('passes no_header_anchors param to renderMarkdownPath if table of contents are unsupported', async () => { + buildWrapper({ propsData: { supportsTableOfContents: false } }); + + 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=true`, ); }); -- GitLab From 8ad80e649415c59cef34f54f23133fda26ca1fc0 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 25 Nov 2025 17:13:58 +1100 Subject: [PATCH 02/19] Check for 'true' value, not presence --- app/controllers/concerns/preview_markdown.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index 623e03f6546ddc..7a2a93d36afb81 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -32,7 +32,8 @@ def projects_filter_params { issuable_reference_expansion_enabled: true, suggestions_filter_enabled: params[:preview_suggestions].present?, - no_header_anchors: params[:no_header_anchors].present? + # This comes from both a URL parameter (string) and JSON body. + no_header_anchors: params[:no_header_anchors].to_s == 'true' } end -- GitLab From fb38b98b06b29bc028222d41702d9947f4be41e7 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 25 Nov 2025 17:13:58 +1100 Subject: [PATCH 03/19] Apply the same change where the values are bools as query parameters These don't work correctly right now when you actually pass false and don't omit the parameter entirely, which many sites in fact do! The tests don't catch it because they're setting the parameters to Ruby false, and not the string "false" which they'll actually be getting. --- app/controllers/concerns/preview_markdown.rb | 2 +- app/services/preview_markdown_service.rb | 6 ++-- .../services/preview_markdown_service_spec.rb | 34 ++++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index 7a2a93d36afb81..d896e02eb0d7ad 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: params[:preview_suggestions] == 'true', # This comes from both a URL parameter (string) and JSON body. no_header_anchors: params[:no_header_anchors].to_s == 'true' } diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index f9f3a7be621f0b..dfb448a2dd49dc 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -30,7 +30,7 @@ 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: params[:render_quick_actions] == 'true') end def find_user_references(text) @@ -52,11 +52,11 @@ def find_suggestions(text) Gitlab::Diff::SuggestionsParser.parse(text, position: position, project: project, - supports_suggestion: params[:preview_suggestions]) + supports_suggestion: params[:preview_suggestions] == 'true') end def preview_sugestions? - params[:preview_suggestions] && + params[:preview_suggestions] == 'true' && target_type == 'MergeRequest' && Ability.allowed?(current_user, :download_code, project) end diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb index 42d27c50fc5ed2..a04576ab22b490 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 -- GitLab From 0c386a54794a1d55be21765efbe259c685cf0dd8 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 25 Nov 2025 17:53:39 +1100 Subject: [PATCH 04/19] Don't add the TOC extension if disabled This stops the input rule triggering, and avoids any other way we might accidentally come across it in input HTML and parse it! --- .../content_editor/services/create_content_editor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 decfe2f95c9c44..e11c146717f34f 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -45,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({ @@ -63,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 }); -- GitLab From 5c5d2ed8d8baebfbe3d6c9a64ee6ed45fe15b399 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Mon, 1 Dec 2025 11:56:49 +1100 Subject: [PATCH 05/19] Use Gitlab::Utils.to_boolean where possible Co-authored-by: Marco Gregorius --- app/controllers/concerns/preview_markdown.rb | 4 ++-- app/services/preview_markdown_service.rb | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index d896e02eb0d7ad..f9f90c3a05305f 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -31,9 +31,9 @@ def resource_parent def projects_filter_params { issuable_reference_expansion_enabled: true, - suggestions_filter_enabled: params[:preview_suggestions] == 'true', + suggestions_filter_enabled: Gitlab::Utils.to_boolean(params[:preview_suggestions]), # This comes from both a URL parameter (string) and JSON body. - no_header_anchors: params[:no_header_anchors].to_s == 'true' + no_header_anchors: Gitlab::Utils.to_boolean(params[:no_header_anchors]) } end diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index dfb448a2dd49dc..301d242256785d 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] == 'true') + 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] == 'true') + supports_suggestion: Gitlab::Utils.to_boolean(params[:preview_suggestions])) end def preview_sugestions? - params[:preview_suggestions] == 'true' && + Gitlab::Utils.to_boolean(params[:preview_suggestions]) && target_type == 'MergeRequest' && Ability.allowed?(current_user, :download_code, project) end -- GitLab From c5e5213e1897e32271afa6f29e02bde592754f9a Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Mon, 1 Dec 2025 12:27:02 +1100 Subject: [PATCH 06/19] Assert behaviour of supportsTableOfContents and no_header_anchors --- .../components/markdown/field_spec.js | 73 +++++++++++++++---- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index acdadf2022172a..bd5714c62866fe 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,26 @@ describe('Markdown field component', () => { axiosMock.restore(); }); - function createSubject({ lines = [], enablePreview = true, showContentEditorSwitcher } = {}) { + function createSubject({ + lines = [], + enablePreview = true, + supportsTableOfContents = null, + showContentEditorSwitcher, + } = {}) { + const propsData = { + markdownDocsPath, + markdownPreviewPath, + isSubmitting: false, + textareaValue, + lines, + enablePreview, + restrictedToolBarItems, + showContentEditorSwitcher, + supportsQuickActions: true, + }; + if (supportsTableOfContents !== null) + propsData.supportsTableOfContents = supportsTableOfContents; + // 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( @@ -56,17 +75,7 @@ describe('Markdown field component', () => { `, }, { - propsData: { - markdownDocsPath, - markdownPreviewPath, - isSubmitting: false, - textareaValue, - lines, - enablePreview, - restrictedToolBarItems, - showContentEditorSwitcher, - supportsQuickActions: true, - }, + propsData, mocks: { $apollo: { queries: { @@ -280,6 +289,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=false by default', async () => { + expectedNoHeaderAnchors = false; + + 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=true when supportsTableOfContents is set to false', async () => { + createSubject({ supportsTableOfContents: false }); + expectedNoHeaderAnchors = true; + + 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', () => { -- GitLab From bf6510bcd16bfabf3fdcbf64976920b75642eac1 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Mon, 1 Dec 2025 12:27:02 +1100 Subject: [PATCH 07/19] Assert work item note editor disables TOC --- .../components/notes/work_item_comment_form_spec.js | 6 ++++++ 1 file changed, 6 insertions(+) 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 77118079488f1d..6323a3b8565333 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(); -- GitLab From ec97a674bf61bb07b7d0cd7fad261c25f5299cc4 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Mon, 1 Dec 2025 13:54:00 +1100 Subject: [PATCH 08/19] Comment form disables TOC in Markdown editor Used on the main MR discussion page for leaving non-diff-attached comments. --- app/assets/javascripts/notes/components/comment_form.vue | 1 + spec/frontend/notes/components/comment_form_spec.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index a55b37085e0d94..9a26998595b58b 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/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 665f45d80a8fd6..0e218c718103ee 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); + }); }); -- GitLab From f0c69141dcf5f33be3e0d3e1ddcec4476d74be96 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Mon, 1 Dec 2025 13:54:00 +1100 Subject: [PATCH 09/19] Note form disables TOC in Markdown editor Used on MR comments. --- app/assets/javascripts/notes/components/note_form.vue | 1 + spec/frontend/notes/components/note_form_spec.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 1a4ffda17d4337..382e2af8e62f4f 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/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 4e6502fe8e4afd..9bba42b624fd34 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); + }); }); -- GitLab From 50816974d777b2453ca2e9dffc5925ce9c32d1bf Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Mon, 1 Dec 2025 14:59:17 +1100 Subject: [PATCH 10/19] Don't show TOCs when Markdown previewed on commit endpoints --- app/controllers/concerns/preview_markdown.rb | 2 +- spec/controllers/projects_controller_spec.rb | 24 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index f9f90c3a05305f..5865991ef26273 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -33,7 +33,7 @@ def projects_filter_params issuable_reference_expansion_enabled: true, suggestions_filter_enabled: Gitlab::Utils.to_boolean(params[:preview_suggestions]), # This comes from both a URL parameter (string) and JSON body. - no_header_anchors: Gitlab::Utils.to_boolean(params[:no_header_anchors]) + no_header_anchors: params[:target_type] == 'Commit' || Gitlab::Utils.to_boolean(params[:no_header_anchors]) } end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 36f3659e743539..21f157538ec356 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -1696,6 +1696,30 @@ def update_project_feature expect(json_response['body']).to include(expanded_path) end end + + context 'when Markdown is previewed on commit' do + let(:preview_markdown_params) do + { + namespace_id: public_project.namespace, + project_id: public_project, + target_type: 'Commit', + text: <<~MARKDOWN + [[_TOC_]] + + # Hello + ## Tere + ### よしよし + MARKDOWN + } + end + + it 'does not render TOCs' do + post :preview_markdown, params: preview_markdown_params + + expect(json_response['body']).to include('TOC') + expect(json_response['body']).not_to include('

Date: Mon, 1 Dec 2025 15:17:13 +1100 Subject: [PATCH 11/19] Don't show TOCs in MR batch comment draft renders --- .../projects/merge_requests/drafts_controller.rb | 2 +- .../merge_requests/drafts_controller_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb index 5278893bbd66eb..e8724406ac546c 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/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb index 8ecf8512954665..f57aeb490e526d 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('

Date: Mon, 1 Dec 2025 15:30:50 +1100 Subject: [PATCH 12/19] Don't show TOCs in MR batch comment review note preview --- .../batch_comments/components/review_drawer.vue | 1 + .../batch_comments/components/review_drawer_spec.js | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/app/assets/javascripts/batch_comments/components/review_drawer.vue b/app/assets/javascripts/batch_comments/components/review_drawer.vue index 82d3748e95171b..440830f1e585aa 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/spec/frontend/batch_comments/components/review_drawer_spec.js b/spec/frontend/batch_comments/components/review_drawer_spec.js index a5d3a55fe5fea6..a6f0e32bfcf588 100644 --- a/spec/frontend/batch_comments/components/review_drawer_spec.js +++ b/spec/frontend/batch_comments/components/review_drawer_spec.js @@ -14,6 +14,7 @@ import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs'; import { useNotes } from '~/notes/store/legacy_notes'; import { useBatchComments } from '~/batch_comments/store'; import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants'; import userCanApproveQuery from '~/batch_comments/queries/can_approve.query.graphql'; import toast from '~/vue_shared/plugins/global_toast'; @@ -39,6 +40,7 @@ describe('ReviewDrawer', () => { 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); + }); }); -- GitLab From 459b72aa3cd972c2a06ce8b85b3d89d3462ee03c Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 2 Dec 2025 11:27:45 +1100 Subject: [PATCH 13/19] Avoid mutation --- .../components/markdown/field_spec.js | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index bd5714c62866fe..edfcfee04d14f8 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -33,26 +33,7 @@ describe('Markdown field component', () => { axiosMock.restore(); }); - function createSubject({ - lines = [], - enablePreview = true, - supportsTableOfContents = null, - showContentEditorSwitcher, - } = {}) { - const propsData = { - markdownDocsPath, - markdownPreviewPath, - isSubmitting: false, - textareaValue, - lines, - enablePreview, - restrictedToolBarItems, - showContentEditorSwitcher, - supportsQuickActions: true, - }; - if (supportsTableOfContents !== null) - propsData.supportsTableOfContents = supportsTableOfContents; - + 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( @@ -75,7 +56,17 @@ describe('Markdown field component', () => { `, }, { - propsData, + propsData: { + markdownDocsPath, + markdownPreviewPath, + isSubmitting: false, + textareaValue, + lines, + enablePreview, + restrictedToolBarItems, + supportsQuickActions: true, + ...props, + }, mocks: { $apollo: { queries: { -- GitLab From 8b2808ecf1a43208d8b2ee8624dd068548eb27f9 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 2 Dec 2025 12:33:31 +1100 Subject: [PATCH 14/19] Spec for createContentEditor --- .../services/create_content_editor_spec.js | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 cb871c37b82807..d2369acc1dd24f 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 supporting table of contents', () => { + expect(editor.supportsTableOfContents).toBe(true); + }); + + it('allows configuring table of contents support', () => { + expect( + createContentEditor({ renderMarkdown, uploadsPath, supportsTableOfContents: false }) + .supportsTableOfContents, + ).toBe(false); + }); }); -- GitLab From 885d6549787dd284e82bac42042211aeeed86040 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 2 Dec 2025 12:36:08 +1100 Subject: [PATCH 15/19] Reverse the default for TOC support in editors --- .../content_editor/components/content_editor.vue | 2 +- .../content_editor/services/create_content_editor.js | 2 +- .../vue_shared/components/markdown/field.vue | 2 +- .../vue_shared/components/markdown/markdown_editor.vue | 2 +- .../services/create_content_editor_spec.js | 8 ++++---- .../vue_shared/components/markdown/field_spec.js | 10 +++++----- .../components/markdown/markdown_editor_spec.js | 10 +++++----- .../content_editor/content_editor_integration_spec.js | 1 + 8 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index 4a73a21b463785..d0a03415341117 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -89,7 +89,7 @@ export default { supportsTableOfContents: { type: Boolean, required: false, - default: true, + default: false, }, drawioEnabled: { type: Boolean, 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 e11c146717f34f..dade2500948d54 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -23,7 +23,7 @@ export const createContentEditor = ({ serializerConfig = { marks: {}, nodes: {} }, tiptapOptions, drawioEnabled = false, - supportsTableOfContents = true, + supportsTableOfContents = false, enableAutocomplete, autocompleteDataSources = {}, sidebarMediator = {}, diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index c58109ca8e2b1d..79b0bf54ab2e16 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -72,7 +72,7 @@ export default { supportsTableOfContents: { type: Boolean, required: false, - default: true, + default: false, }, canAttachFile: { type: Boolean, 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 72f045a9fcb75d..d806d9bd58cf2d 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -96,7 +96,7 @@ export default { supportsTableOfContents: { type: Boolean, required: false, - default: true, + default: false, }, autosaveKey: { type: String, 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 d2369acc1dd24f..7cde2b734b4921 100644 --- a/spec/frontend/content_editor/services/create_content_editor_spec.js +++ b/spec/frontend/content_editor/services/create_content_editor_spec.js @@ -60,14 +60,14 @@ describe('content_editor/services/create_content_editor', () => { }); }); - it('defaults to supporting table of contents', () => { - expect(editor.supportsTableOfContents).toBe(true); + 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: false }) + createContentEditor({ renderMarkdown, uploadsPath, supportsTableOfContents: true }) .supportsTableOfContents, - ).toBe(false); + ).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index edfcfee04d14f8..168dcde78eec22 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -293,8 +293,8 @@ describe('Markdown field component', () => { }); }); - it('passes no_header_anchors=false by default', async () => { - expectedNoHeaderAnchors = false; + it('passes no_header_anchors=true by default', async () => { + expectedNoHeaderAnchors = true; previewToggle = getPreviewToggle(); previewToggle.vm.$emit('click', true); @@ -305,9 +305,9 @@ describe('Markdown field component', () => { ); }); - it('passes no_header_anchors=true when supportsTableOfContents is set to false', async () => { - createSubject({ supportsTableOfContents: false }); - expectedNoHeaderAnchors = true; + 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); 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 8148b364070a07..1a71d4665a4f0c 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&no_header_anchors=false`, + `${window.location.origin}/api/markdown?render_quick_actions=true&no_header_anchors=true`, ); }); @@ -141,18 +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&no_header_anchors=false`, + `${window.location.origin}/api/markdown?render_quick_actions=false&no_header_anchors=true`, ); }); - it('passes no_header_anchors param to renderMarkdownPath if table of contents are unsupported', async () => { - buildWrapper({ propsData: { supportsTableOfContents: false } }); + 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=true`, + `${window.location.origin}/api/markdown?render_quick_actions=false&no_header_anchors=false`, ); }); 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 aad1722c174b71..20bec6591f0869 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, -- GitLab From a1d525f39e86ca5fa171ce33ebe35996df0be1de Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 2 Dec 2025 13:16:59 +1100 Subject: [PATCH 16/19] Explicitly support TOC in wiki page editor --- app/assets/javascripts/wikis/components/wiki_form.vue | 1 + spec/frontend/wikis/components/wiki_form_spec.js | 1 + 2 files changed, 2 insertions(+) diff --git a/app/assets/javascripts/wikis/components/wiki_form.vue b/app/assets/javascripts/wikis/components/wiki_form.vue index 0dc026aeb81405..ddf4a8c37e2b39 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/spec/frontend/wikis/components/wiki_form_spec.js b/spec/frontend/wikis/components/wiki_form_spec.js index dad6f1e3383724..af3d4a4d62d329 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, }), ); -- GitLab From c6bf6d3ea30b07eb41d9f5f17ae2cb1702125a52 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 2 Dec 2025 13:16:59 +1100 Subject: [PATCH 17/19] Explicitly support TOC in work item (issue) description editor --- .../work_items/components/work_item_description.vue | 1 + .../work_items/components/work_item_description_spec.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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 001d6341c09648..26eb4841047a15 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/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index 94cf06ee89cc1c..3b9cdc1e008583 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, }); -- GitLab From e84eb582cbd6b43d04850c51b2cc35f945ac8e9e Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 2 Dec 2025 13:16:59 +1100 Subject: [PATCH 18/19] Support no_anchor_headers in all PreviewMarkdown endpoints i.e. not just ProjectsController! --- app/controllers/concerns/preview_markdown.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index 5865991ef26273..5c8c369f19902a 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -31,9 +31,7 @@ def resource_parent def projects_filter_params { issuable_reference_expansion_enabled: true, - suggestions_filter_enabled: Gitlab::Utils.to_boolean(params[:preview_suggestions]), - # This comes from both a URL parameter (string) and JSON body. - no_header_anchors: params[:target_type] == 'Commit' || Gitlab::Utils.to_boolean(params[:no_header_anchors]) + suggestions_filter_enabled: Gitlab::Utils.to_boolean(params[:preview_suggestions]) } end @@ -75,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 -- GitLab From 9d0cffec3af61887327be7bd222068c02a2d0689 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Tue, 2 Dec 2025 13:16:59 +1100 Subject: [PATCH 19/19] Enable TOC support through mountMarkdownEditor/js-markdown-editor This enables it for the MR editor, as well as admin/topics, and milestones in group and project level, as these features all render TOC on the backend. --- .../vue_shared/components/markdown/mount_markdown_editor.js | 2 ++ app/views/admin/topics/_form.html.haml | 1 + app/views/groups/milestones/_form.html.haml | 1 + app/views/projects/milestones/_form.html.haml | 1 + app/views/shared/form_elements/_description.html.haml | 1 + 5 files changed, 6 insertions(+) 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 74b254071dbb15..609af2fd93ec08 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/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml index b209c5969c0f27..60d1a1dfc4b2ab 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 f710c02cd25bd8..b1a58659ef8b76 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 892b696df10957..c89abece5d1386 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 27f6e22ce53d1f..48bdf977e389b9 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, -- GitLab