From af483fd4fac2e67ed5cfa34aa14e5ab220187870 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 f5475c26109590f21056ae252547397c7ca49f7f 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 38852ad5c93c493011ebbea4ac540f45eda82c49 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 7bd3c86355491e9bceec28448b22638c21579a22 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 b3e683ed65c1b20e6fb9e9e5b62a36e333055b53 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 b3bca9653313a45dddc2ce91f259861f95855ef3 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 51787f58ffd0393bd7a87bb455c1ebceb61f870f 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 3922fa1e697219281321e656c70bb0f6d11ed0c3 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 51e13a1b032833be827cef072edf10a9ef78302b 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 b91bd3b57ca17b727124b86ef3d4f0cf2de34a9c 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 bf5b6d940ef6e0..019c856465cd33 100644 --- a/app/assets/javascripts/batch_comments/components/review_drawer.vue +++ b/app/assets/javascripts/batch_comments/components/review_drawer.vue @@ -328,6 +328,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 058447790791f7e4877546ef6f9bb293c27040f7 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 116428e943e83dc4865acf939bf94f157d391562 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 b69a0058e9638db147bb09bcbf21b404b99efa3e 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 2e3687d8b24af0c8f19c758060d2fa0764aa4719 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 5890bd8bfea9a55f0da67b58c336a1318b248ecd 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 68b7cdd1f93f9d..6e5dc4470722d1 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -592,6 +592,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 b053286fa4abd3..3020be2ae29d10 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -156,11 +156,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 d49f72b5b0bd9139f139117dd2cfca1a79e6914c 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 e6c2b37b9de3b6ff13c726d0a5c5b2b31ef0118f 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