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