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) +}) diff --git a/src/components/Suggestion/LinkPicker/suggestions.js b/src/components/Suggestion/LinkPicker/suggestions.js index 88ff8feea9c..7ec2e9b869c 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,52 @@ 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) => { + let label = p.title + if (p.id === 'files') { + // Rename "Files" to "Link a file", less ambiguous + label = t('text', 'Link a file') + } + return { + label, + icon: p.icon_url, + providerId: p.id, + order: p.order, + } + }) + .filter((e) => e?.label?.toLowerCase?.()?.includes(query.toLowerCase())) } export default () => @@ -94,25 +118,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 })), ] }, })