From 6e2195bf1da9dd19fd7024bf983c944807b3de54 Mon Sep 17 00:00:00 2001 From: Jonas Date: Sun, 9 Aug 2026 18:04:21 +0200 Subject: [PATCH 1/3] fix(LinkPicker): improve ordering of suggestions * List smart picker items with `order === -1` first (e.g. "Link to page in collective" inside Collectives) * List some items declared as "important" second * List remaining smart picker items third * List remaining formatting items last Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5 --- .../Suggestion/LinkPicker/suggestions.js | 101 +++++++++++------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/src/components/Suggestion/LinkPicker/suggestions.js b/src/components/Suggestion/LinkPicker/suggestions.js index 88ff8feea9c..a222ed33586 100644 --- a/src/components/Suggestion/LinkPicker/suggestions.js +++ b/src/components/Suggestion/LinkPicker/suggestions.js @@ -15,14 +15,15 @@ import createSuggestions from '../suggestions.js' import { getMenuEntries } from './../../Menu/entries.ts' import LinkPickerList from './LinkPickerList.vue' +const suggestGroupImportant = t('text', 'Suggestions') const suggestGroupFormat = t('text', 'Formatting') const suggestGroupPicker = t('text', 'Smart picker') -const filterOut = (e) => { - return ['undo', 'redo', 'outline', 'emoji-picker'].indexOf(e.key) > -1 -} +const important = ['task-list', 'table', 'callout-info'] +const excludedFormatting = ['undo', 'redo', 'outline', 'emoji-picker'] -const important = ['task-list', 'table'] +const isImportant = (item) => + important.includes(item.key) || important.includes(item.providerId) const isValidUrl = (url) => { try { @@ -32,29 +33,47 @@ const isValidUrl = (url) => { } } -const sortImportantFirst = (list) => { +/** + * + * @param {string} query to filter by + * @param {object} editor the editor instance + */ +function formattingItems(query, editor) { + const menuEntries = getMenuEntries(false, false) return [ - ...list.filter((e) => important.indexOf(e.key) > -1), - ...list.filter((e) => important.indexOf(e.key) === -1), + ...menuEntries.find((e) => e.key === 'headings').children, + ...menuEntries.find((e) => e.key === 'lists').children, + ...menuEntries.filter( + (e) => e.action && !excludedFormatting.includes(e.key), + ), + ...menuEntries.find((e) => e.key === 'blocks').children, + { + ...menuEntries.find((e) => e.key === 'emoji-picker'), + action: (command) => command.insertContent(':'), + }, ] + .filter((e) => e?.label?.toLowerCase?.()?.includes(query.toLowerCase())) + .filter(({ action, isActive }) => { + const canRunState = action(editor?.can()) + const isActiveState = isActive && getIsActive({ isActive }, editor) + return canRunState && !isActiveState + }) } -const formattingSuggestions = (query) => { - const menuEntries = getMenuEntries(false, false) - return sortImportantFirst( - [ - ...menuEntries.find((e) => e.key === 'headings').children, - ...menuEntries.find((e) => e.key === 'lists').children, - ...menuEntries.filter((e) => e.action && !filterOut(e)), - ...menuEntries.find((e) => e.key === 'blocks').children, - { - ...menuEntries.find((e) => e.key === 'emoji-picker'), - action: (command) => command.insertContent(':'), - }, - ] - .filter((e) => e?.label?.toLowerCase?.()?.includes(query.toLowerCase())) - .map((e) => ({ ...e, suggestGroup: suggestGroupFormat })), - ) +/** + * @param {string} query to filter by + */ +function pickerItems(query) { + return searchProvider(query) + .map((p) => { + return { + label: p.title, + icon: p.icon_url, + providerId: p.id, + order: p.order, + } + }) + .filter((e) => e?.label?.toLowerCase?.()?.includes(query.toLowerCase())) } export default () => @@ -94,25 +113,25 @@ export default () => }) }, items: ({ editor, query }) => { + const pickers = pickerItems(query) + const formatting = formattingItems(query, editor) return [ - ...searchProvider(query) - .map((p) => { - return { - suggestGroup: suggestGroupPicker, - label: p.title, - icon: p.icon_url, - providerId: p.id, - } - }) - .filter((e) => - e?.label?.toLowerCase?.()?.includes(query.toLowerCase()), - ), - ...formattingSuggestions(query).filter(({ action, isActive }) => { - const canRunState = action(editor?.can()) - const isActiveState = - isActive && getIsActive({ isActive }, editor) - return canRunState && !isActiveState - }), + // pickers with order -1, then important pickers, then important formatting + ...[ + ...pickers.filter((e) => e.order === -1), + ...pickers.filter((e) => e.order !== -1 && isImportant(e)), + ...formatting.filter(isImportant), + ].map((e) => ({ ...e, suggestGroup: suggestGroupImportant })), + + // Smart picker: remaining (non-important) pickers + ...pickers + .filter((e) => e.order !== -1 && !isImportant(e)) + .map((e) => ({ ...e, suggestGroup: suggestGroupPicker })), + + // Formatting: remaining (non-important) formatting entries + ...formatting + .filter((e) => !isImportant(e)) + .map((e) => ({ ...e, suggestGroup: suggestGroupFormat })), ] }, }) From ea532df9d8f3a90f297e07424be77aafbf144712 Mon Sep 17 00:00:00 2001 From: Jonas Date: Sun, 9 Aug 2026 18:19:06 +0200 Subject: [PATCH 2/3] fix(LinkPicker): rename 'Files' to 'Link a file' The generic 'Files' is too ambiguous in document editing context. Signed-off-by: Jonas --- src/components/Suggestion/LinkPicker/suggestions.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Suggestion/LinkPicker/suggestions.js b/src/components/Suggestion/LinkPicker/suggestions.js index a222ed33586..7ec2e9b869c 100644 --- a/src/components/Suggestion/LinkPicker/suggestions.js +++ b/src/components/Suggestion/LinkPicker/suggestions.js @@ -66,8 +66,13 @@ function formattingItems(query, editor) { function pickerItems(query) { return searchProvider(query) .map((p) => { + let label = p.title + if (p.id === 'files') { + // Rename "Files" to "Link a file", less ambiguous + label = t('text', 'Link a file') + } return { - label: p.title, + label, icon: p.icon_url, providerId: p.id, order: p.order, From dfb7feb4c81039e1fc8dc479738cb0a51b26f2fb Mon Sep 17 00:00:00 2001 From: Jonas Date: Sun, 9 Aug 2026 18:32:40 +0200 Subject: [PATCH 3/3] test(playwright): test custom suggestions order Signed-off-by: Jonas --- playwright/e2e/smart-picker.spec.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/playwright/e2e/smart-picker.spec.ts b/playwright/e2e/smart-picker.spec.ts index 707f1b5224e..3e15531a70b 100644 --- a/playwright/e2e/smart-picker.spec.ts +++ b/playwright/e2e/smart-picker.spec.ts @@ -34,3 +34,27 @@ test('Insert Link', async ({ editor }) => { await editor.referencePicker.press('Enter') await expect(editor.content.getByRole('link')).toContainText('github.com') }) + +test('Files provider is renamed to "Link a file"', async ({ editor }) => { + await editor.type('/') + await expect(editor.getSuggestion('Link a file')).toBeVisible() +}) + +test('Important items appear before remaining items', async ({ editor }) => { + await editor.type('/') + await expect(editor.getSuggestion('To-Do list')).toBeVisible() + const allTexts = await editor.suggestions + .locator('.suggestion-list__item') + .allTextContents() + + const todoIdx = allTexts.findIndex((t) => t.includes('To-Do list')) + const tableIdx = allTexts.findIndex((t) => t.includes('Table')) + const filesIdx = allTexts.findIndex((t) => t.includes('Link a file')) + const heading1Idx = allTexts.findIndex((t) => t.includes('Heading 1')) + + expect(todoIdx).toBeLessThan(filesIdx) + expect(todoIdx).toBeLessThan(heading1Idx) + expect(tableIdx).toBeLessThan(filesIdx) + expect(tableIdx).toBeLessThan(heading1Idx) + expect(filesIdx).toBeLessThan(heading1Idx) +})