diff --git a/package.json b/package.json index 5e921f95..cbce39b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "selection-command-monorepo", - "version": "1.0.0", + "version": "1.1.0", "private": true, "description": "Selection Command - Monorepo for Chrome Extension and Hub", "author": "ujiro99", diff --git a/packages/extension/e2e/generated-command-urls.ts b/packages/extension/e2e/generated-command-urls.ts index f226b6f4..8c77a542 100644 --- a/packages/extension/e2e/generated-command-urls.ts +++ b/packages/extension/e2e/generated-command-urls.ts @@ -12,22 +12,36 @@ export const COMMAND_URLS: UrlEntry[] = [ { title: "Netflix", locale: "en", searchUrl: "https://www.netflix.com/search?q=%s" }, { title: "Pinterest", locale: "en", searchUrl: "https://www.pinterest.com/search/pins/?q=%s" }, { title: "Drive", locale: "en", searchUrl: "https://drive.google.com/drive/search?q=%s" }, + { title: "Search Commands on Hub", locale: "en", searchUrl: "https://selection-command.com/en?q=%pageUrl" }, { title: "Amazon", locale: "ja", searchUrl: "https://www.amazon.co.jp/s?k=%s" }, { title: "Yahoo! Japan", locale: "ja", searchUrl: "https://search.yahoo.co.jp/search?p=%s" }, { title: "en to ja", locale: "ja", searchUrl: "https://translate.google.co.jp/?hl=ja&sl=auto&text=%s&op=translate" }, + { title: "コマンドをHubで検索", locale: "ja", searchUrl: "https://selection-command.com/ja?q=%pageUrl" }, { title: "百度", locale: "zh", searchUrl: "https://www.baidu.com/s?wd=%s" }, { title: "京东", locale: "zh", searchUrl: "https://search.jd.com/Search?keyword=%s" }, { title: "哔哩哔哩", locale: "zh", searchUrl: "https://search.bilibili.com/all?keyword=%s" }, + { title: "在 Hub 中搜索命令", locale: "zh", searchUrl: "https://selection-command.com/zh-CN?q=%pageUrl" }, { title: "네이버", locale: "ko", searchUrl: "https://search.naver.com/search.naver?query=%s" }, + { title: "Hub에서 명령어 검색", locale: "ko", searchUrl: "https://selection-command.com/ko?q=%pageUrl" }, { title: "Яндекс", locale: "ru", searchUrl: "https://yandex.ru/search/?text=%s" }, { title: "ВКонтакте", locale: "ru", searchUrl: "https://vk.com/search?c%5Bq%5D=%s" }, + { title: "Поиск команд в Hub", locale: "ru", searchUrl: "https://selection-command.com/ru?q=%pageUrl" }, + { title: "Befehle im Hub suchen", locale: "de", searchUrl: "https://selection-command.com/de?q=%pageUrl" }, + { title: "Rechercher des commandes sur Hub", locale: "fr", searchUrl: "https://selection-command.com/fr?q=%pageUrl" }, { title: "Amazon", locale: "es", searchUrl: "https://www.amazon.es/s?k=%s" }, { title: "El Corte Inglés", locale: "es", searchUrl: "https://www.elcorteingles.es/search-nwx/?s=%s" }, { title: "AliExpress", locale: "es", searchUrl: "https://es.aliexpress.com/w/wholesale-%s.html" }, + { title: "Buscar comandos en Hub", locale: "es", searchUrl: "https://selection-command.com/es?q=%pageUrl" }, + { title: "Pesquisar comandos no Hub", locale: "pt-br", searchUrl: "https://selection-command.com/pt-BR?q=%pageUrl" }, { title: "OLX", locale: "pt", searchUrl: "https://www.olx.pt/ads/?q=%s" }, + { title: "Pesquisar comandos no Hub", locale: "pt", searchUrl: "https://selection-command.com/pt-PT?q=%pageUrl" }, + { title: "Hub पर कमांड खोजें", locale: "hi", searchUrl: "https://selection-command.com/hi?q=%pageUrl" }, { title: "Tokopedia", locale: "id", searchUrl: "https://www.tokopedia.com/search?st=product&q=%s" }, { title: "Shopee", locale: "id", searchUrl: "https://shopee.co.id/search?keyword=%s" }, + { title: "Cari Perintah di Hub", locale: "id", searchUrl: "https://selection-command.com/id?q=%pageUrl" }, { title: "Shopee", locale: "ms", searchUrl: "https://shopee.com.my/search?keyword=%s" }, { title: "Lazada", locale: "ms", searchUrl: "https://www.lazada.com.my/catalog/?q=%s" }, + { title: "Cari Perintah di Hub", locale: "ms", searchUrl: "https://selection-command.com/ms?q=%pageUrl" }, { title: "Zalando.it", locale: "it", searchUrl: "https://www.zalando.it/catalogo/?q=%s" }, + { title: "Cerca comandi su Hub", locale: "it", searchUrl: "https://selection-command.com/it?q=%pageUrl" }, ] diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index 6395d9d6..15a68e3a 100644 --- a/packages/extension/manifest.json +++ b/packages/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "description": "__MSG_extDescription__", - "version": "1.0.0", + "version": "1.1.0", "default_locale": "en", "icons": { "128": "icon128.png" @@ -81,6 +81,7 @@ ], "resources": [ "assets/*.css", + "images/*", "icon128.png" ] } diff --git a/packages/extension/package.json b/packages/extension/package.json index aa544e58..3da12c78 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@selection-command/extension", - "version": "1.0.0", + "version": "1.1.0", "private": true, "author": "ujiro99", "license": "MIT", diff --git a/packages/extension/public/images/search_command.png b/packages/extension/public/images/search_command.png new file mode 100644 index 00000000..a10c2c60 Binary files /dev/null and b/packages/extension/public/images/search_command.png differ diff --git a/packages/extension/src/action/aiPrompt.test.ts b/packages/extension/src/action/aiPrompt.test.ts index b067112c..7d57f7ce 100644 --- a/packages/extension/src/action/aiPrompt.test.ts +++ b/packages/extension/src/action/aiPrompt.test.ts @@ -106,11 +106,6 @@ const baseCommand = { describe("AiPrompt.execute", () => { beforeEach(() => { vi.clearAllMocks() - // Mock location.href for content script context - Object.defineProperty(window, "location", { - value: { href: "https://example.com/page" }, - writable: true, - }) }) describe("DOM input approach (no queryUrl)", () => { @@ -565,10 +560,10 @@ describe("AiPrompt.execute", () => { }, } as any, position: { x: 0, y: 0 }, + pageUrl: "https://example.com/page", }) const sentArgs = vi.mocked(Ipc.send).mock.calls[0][1] as any - // location.href is "https://example.com/page" from beforeEach mock expect(sentArgs.url.selectionText).toBe( "Summarize [https://example.com/page](https://example.com/page)", ) diff --git a/packages/extension/src/action/aiPrompt.ts b/packages/extension/src/action/aiPrompt.ts index 8f986079..af0a762f 100644 --- a/packages/extension/src/action/aiPrompt.ts +++ b/packages/extension/src/action/aiPrompt.ts @@ -69,6 +69,7 @@ export const AiPrompt = { position, useSecondary, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isAiPromptType(command)) { console.error("command is not for AiPrompt.") @@ -128,7 +129,7 @@ export const AiPrompt = { // is false and the DOM input approach is used instead. const expandedPrompt = safeInterpolate(aiPromptOption.prompt, { [InsertSymbol[INSERT.SELECTED_TEXT]]: selectionText, - [InsertSymbol[INSERT.URL]]: location.href, + [InsertSymbol[INSERT.URL]]: pageUrl ?? "", [InsertSymbol[INSERT.LANG]]: getUILanguage(), }) @@ -359,7 +360,7 @@ export const AiPrompt = { url: serviceUrl, steps, selectedText: selectionText, - srcUrl: location.href, + srcUrl: pageUrl ?? "", clipboardText: "", useClipboard: !useQueryUrl && (needClipboard || (useClipboard ?? false)), @@ -407,7 +408,7 @@ export const AiPrompt = { height: command.popupOption?.height ?? PopupOption.height, width: command.popupOption?.width ?? PopupOption.width, selectedText: selectionText, - srcUrl: location.href, + srcUrl: pageUrl ?? "", openMode, pageHtml, selectionHtml, diff --git a/packages/extension/src/action/background.ts b/packages/extension/src/action/background.ts index b9a22711..23047ab2 100644 --- a/packages/extension/src/action/background.ts +++ b/packages/extension/src/action/background.ts @@ -27,6 +27,7 @@ export async function execute({ useSecondary = false, useClipboard = false, changeState, + pageUrl, }: ExecuteCommandParams) { return executeAction({ command, @@ -36,6 +37,7 @@ export async function execute({ useSecondary, useClipboard, changeState, + pageUrl, actions: actionsForBackground, }) } diff --git a/packages/extension/src/action/backgroundTab.ts b/packages/extension/src/action/backgroundTab.ts index f06ae7c8..d01900c2 100644 --- a/packages/extension/src/action/backgroundTab.ts +++ b/packages/extension/src/action/backgroundTab.ts @@ -9,6 +9,7 @@ export const BackgroundTab = { selectionText, command, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -21,6 +22,7 @@ export const BackgroundTab = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: pageUrl ?? "", }, active: false, }) diff --git a/packages/extension/src/action/executor.ts b/packages/extension/src/action/executor.ts index bf6651ce..52fcddac 100644 --- a/packages/extension/src/action/executor.ts +++ b/packages/extension/src/action/executor.ts @@ -12,6 +12,7 @@ export async function executeAction({ useSecondary = false, useClipboard = false, changeState, + pageUrl, }: ExecuteCommandParams & { actions: Record }) { let mode = command.openMode as OPEN_MODE if ( @@ -30,6 +31,7 @@ export async function executeAction({ useClipboard, changeState: changeState ?? (() => {}), target: target ?? null, + pageUrl, }) const { sourceType, sourceId } = resolveCommandSource(command) diff --git a/packages/extension/src/action/index.ts b/packages/extension/src/action/index.ts index 2ecfb215..ddf710ef 100644 --- a/packages/extension/src/action/index.ts +++ b/packages/extension/src/action/index.ts @@ -48,6 +48,9 @@ export async function execute({ useSecondary, useClipboard, changeState, + // This module only ever runs in the content script's page context, so + // location.href here correctly reflects the visited page. + pageUrl: location.href, actions, }) } diff --git a/packages/extension/src/action/pageAction.ts b/packages/extension/src/action/pageAction.ts index f6651301..9c92ec15 100644 --- a/packages/extension/src/action/pageAction.ts +++ b/packages/extension/src/action/pageAction.ts @@ -19,6 +19,7 @@ export const PageAction = { useSecondary, useClipboard, userVariables, + pageUrl, }: ExecuteCommandParams & PageActionParams) { if (!isPageActionCommand(command)) { console.error("command is not for PageAction.") @@ -46,6 +47,7 @@ export const PageAction = { searchUrl: command.pageActionOption.startUrl, selectionText, useClipboard: needClipboard || (useClipboard ?? false), + pageUrl: pageUrl ?? "", } const openMode = useSecondary @@ -68,7 +70,7 @@ export const PageAction = { height: command.popupOption?.height ?? PopupOption.height, width: command.popupOption?.width ?? PopupOption.width, selectedText: selectionText, - srcUrl: location.href, + srcUrl: pageUrl ?? "", openMode, userVariables: userVariables ?? command.pageActionOption.userVariables, }) diff --git a/packages/extension/src/action/popup.ts b/packages/extension/src/action/popup.ts index 744a0ff7..9e5ddc5e 100644 --- a/packages/extension/src/action/popup.ts +++ b/packages/extension/src/action/popup.ts @@ -12,6 +12,7 @@ export const Popup = { command, position, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -31,6 +32,7 @@ export const Popup = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: pageUrl ?? "", }, top: Math.floor(windowPosition.top + position.y), left: Math.floor(windowPosition.left + position.x), diff --git a/packages/extension/src/action/sidePanel.ts b/packages/extension/src/action/sidePanel.ts index bd25ec90..5da4bef6 100644 --- a/packages/extension/src/action/sidePanel.ts +++ b/packages/extension/src/action/sidePanel.ts @@ -10,6 +10,7 @@ export const SidePanel = { selectionText, command, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -39,6 +40,7 @@ export const SidePanel = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: pageUrl ?? "", }, clipboardText, ) diff --git a/packages/extension/src/action/tab.ts b/packages/extension/src/action/tab.ts index 5c00341b..be36d062 100644 --- a/packages/extension/src/action/tab.ts +++ b/packages/extension/src/action/tab.ts @@ -9,6 +9,7 @@ export const Tab = { selectionText, command, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -21,6 +22,7 @@ export const Tab = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: pageUrl ?? "", }, active: true, }) diff --git a/packages/extension/src/action/window.ts b/packages/extension/src/action/window.ts index f73dee71..527302ec 100644 --- a/packages/extension/src/action/window.ts +++ b/packages/extension/src/action/window.ts @@ -12,6 +12,7 @@ export const Window = { command, position, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -31,6 +32,7 @@ export const Window = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: pageUrl ?? "", }, top: Math.floor(windowPosition.top + position.y), left: Math.floor(windowPosition.left + position.x), diff --git a/packages/extension/src/background_script.test.ts b/packages/extension/src/background_script.test.ts index 8bce14bb..587632e5 100644 --- a/packages/extension/src/background_script.test.ts +++ b/packages/extension/src/background_script.test.ts @@ -353,6 +353,7 @@ describe("Background Script Migration", () => { selectionText: "test selection text", target: null, useClipboard: false, + pageUrl: "https://example.com", }), ) }) diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index d246ac10..83b7108c 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -585,6 +585,7 @@ chrome.commands.onCommand.addListener(async (commandName) => { selectionText, target: null, useClipboard, + pageUrl: tab?.url ?? "", }) } sendEvent( diff --git a/packages/extension/src/components/option/editor/ShortcutList.tsx b/packages/extension/src/components/option/editor/ShortcutList.tsx index 2fef9bb0..f803ba9a 100644 --- a/packages/extension/src/components/option/editor/ShortcutList.tsx +++ b/packages/extension/src/components/option/editor/ShortcutList.tsx @@ -9,6 +9,7 @@ import type { Command, CommandFolder, ShortcutCommand } from "@/types" import { OPEN_MODE, OPEN_MODE_BG, + PAGE_ACTION_OPEN_MODE, SHORTCUT_PLACEHOLDER, SHORTCUT_NO_SELECTION_BEHAVIOR, } from "@/const" @@ -19,8 +20,9 @@ import { } from "@/services/option/commandTree" import { cn } from "@/lib/utils" import css from "./ShortcutList.module.css" -import { isAiPromptType } from "@/types/schema" +import { isAiPromptType, isSearchType, isPageActionType } from "@/types/schema" import { INSERT, toInsertTemplate } from "@/services/pageAction" +import { paramToStr } from "@/services/pageAction/helper" const t = (key: string, p?: string[]) => _t(`Option_${key}`, p) @@ -28,20 +30,73 @@ type ShortcutListProps = { control: any } +// Checks whether the given text references the selected text or clipboard placeholder. +const hasSelectionOrClipboardPlaceholder = (text: string) => + text.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) || + text.includes(toInsertTemplate(INSERT.CLIPBOARD)) + +const hasSelectionPlaceholder = (text: string) => + text.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) + +// Checks whether the command references a placeholder matched by +// `hasPlaceholder`, regardless of openMode. +const referencesPlaceholder = ( + command: Command, + hasPlaceholder: (text: string) => boolean, +): boolean => { + if (isSearchType(command) || command.openMode === OPEN_MODE.API) { + return command.searchUrl?.includes("%s") ?? false + } + + if (isPageActionType(command)) { + return command.pageActionOption.steps.some((step) => + hasPlaceholder(paramToStr(step.param)), + ) + } + + if (isAiPromptType(command)) { + return hasPlaceholder(command.aiPromptOption.prompt) + } + + // copy / getTextStyles / linkPopup always operate on the current selection. + return true +} + +// Checks whether the command references the selected text or clipboard at +// all, regardless of openMode. Used to decide whether the "no selection +// behavior" setting has any effect on the command. +const willUseClipboard = (command: Command): boolean => + referencesPlaceholder(command, hasSelectionOrClipboardPlaceholder) + const isTextSelectionOnly = (command: Command) => { const { openMode } = command - if (isAiPromptType(command)) { - const option = command.aiPromptOption - const willUseClipboard = - option.prompt.includes(toInsertTemplate(INSERT.CLIPBOARD)) || - option.prompt.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) - return option.openMode === OPEN_MODE.SIDE_PANEL && willUseClipboard + if (isSearchType(command)) { + // Clipboard access doesn't work in SidePanel mode, so a selection is + // required only when the command actually needs text (%s). + return openMode === OPEN_MODE.SIDE_PANEL && willUseClipboard(command) + } + + if (isPageActionType(command)) { + return ( + command.pageActionOption.openMode === PAGE_ACTION_OPEN_MODE.CURRENT_TAB && + willUseClipboard(command) + ) + } + + if (isAiPromptType(command)) { + return ( + command.aiPromptOption.openMode === OPEN_MODE.SIDE_PANEL && + willUseClipboard(command) + ) } return !Object.values(OPEN_MODE_BG).includes(openMode as any) } +const referencesSelection = (command: Command): boolean => + referencesPlaceholder(command, hasSelectionPlaceholder) + const createNameRender = (command: Command) => { return isTextSelectionOnly(command) ? (name: string) => ( @@ -126,6 +181,8 @@ export function ShortcutList({ control }: ShortcutListProps) { name: "shortcuts.shortcuts", }) + const isShortcutsEmpty = fields.length === 0 + useEffect(() => { if (!shortcutValues) return shortcutValues.forEach((shortcut: ShortcutCommand, index: number) => { @@ -179,14 +236,14 @@ export function ShortcutList({ control }: ShortcutListProps) { useEffect(() => { // Initialize the shortcuts - if (fields.length !== 0) return + if (!isShortcutsEmpty) return const initialData = commands.map((cmd) => ({ id: cmd.name || "", commandId: SHORTCUT_PLACEHOLDER, noSelectionBehavior: SHORTCUT_NO_SELECTION_BEHAVIOR.USE_CLIPBOARD, })) replace(initialData) - }, [replace, commands, userCommands]) + }, [isShortcutsEmpty, replace, commands, userCommands]) const options = useMemo( () => flattenCommandsAndFolders(userCommands, folders), @@ -245,7 +302,10 @@ export function ShortcutList({ control }: ShortcutListProps) { const selectedCmd = userCommands.find( (c: Command) => c?.id === targetId, ) - const showNoSel = selectedCmd && !isTextSelectionOnly(selectedCmd) + const showNoSel = + selectedCmd && + !isTextSelectionOnly(selectedCmd) && + referencesSelection(selectedCmd) return (
diff --git a/packages/extension/src/lib/utils.test.ts b/packages/extension/src/lib/utils.test.ts index 18431c52..113a6181 100644 --- a/packages/extension/src/lib/utils.test.ts +++ b/packages/extension/src/lib/utils.test.ts @@ -199,6 +199,47 @@ describe("toUrl", () => { "https://example.com/search?q=hello%0Aworld%09test", ) }) + + it("TU-14: replaces %pageUrl with the encoded pageUrl", () => { + const param: UrlParam = { + searchUrl: "https://example.com/share?url=%pageUrl", + selectionText: "", + pageUrl: "https://source.example.com/article", + } + expect(toUrl(param)).toBe( + "https://example.com/share?url=https%3A%2F%2Fsource.example.com%2Farticle", + ) + }) + + it("TU-15: replaces both %s and %pageUrl in the same searchUrl", () => { + const param: UrlParam = { + searchUrl: "https://example.com/share?url=%pageUrl&text=%s", + selectionText: "hello world", + pageUrl: "https://source.example.com/article", + } + expect(toUrl(param)).toBe( + "https://example.com/share?url=https%3A%2F%2Fsource.example.com%2Farticle&text=hello+world", + ) + }) + + it("TU-16: replaces %pageUrl with an empty string when pageUrl is not set", () => { + const param: UrlParam = { + searchUrl: "https://example.com/share?url=%pageUrl", + selectionText: "", + } + expect(toUrl(param)).toBe("https://example.com/share?url=") + }) + + it("TU-17: URL encodes reserved characters in pageUrl", () => { + const param: UrlParam = { + searchUrl: "https://example.com/share?url=%pageUrl", + selectionText: "", + pageUrl: "https://source.example.com/search?q=a&b=1#section", + } + expect(toUrl(param)).toBe( + "https://example.com/share?url=https%3A%2F%2Fsource.example.com%2Fsearch%3Fq%3Da%26b%3D1%23section", + ) + }) }) describe("matchesPageActionUrl", () => { diff --git a/packages/extension/src/lib/utils.ts b/packages/extension/src/lib/utils.ts index 7aed90bb..1071cf06 100644 --- a/packages/extension/src/lib/utils.ts +++ b/packages/extension/src/lib/utils.ts @@ -84,7 +84,9 @@ export function toUrl( } else if (spaceEncoding === SPACE_ENCODING.UNDERSCORE) { textEncoded = textEncoded.replaceAll("%20", "_") } - return searchUrl?.replace("%s", textEncoded) + let url = searchUrl?.replace("%s", textEncoded) + url = url?.replace("%pageUrl", encodeURIComponent(param.pageUrl ?? "")) + return url } export function escapeJson(str: string) { diff --git a/packages/extension/src/services/option/defaultSettings.test.ts b/packages/extension/src/services/option/defaultSettings.test.ts index 8f99ddc2..afc48198 100644 --- a/packages/extension/src/services/option/defaultSettings.test.ts +++ b/packages/extension/src/services/option/defaultSettings.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest" -import { DefaultCommands, getDefaultCommands } from "./defaultSettings" +import { + DefaultCommands, + getDefaultCommands, + COMMAND_SEARCH_ID, +} from "./defaultSettings" import { isLinkCommand } from "@/lib/utils" import { INSERT, toInsertTemplate } from "@/services/pageAction" import { getAiServicesFallback } from "@/services/aiPrompt" @@ -279,4 +283,17 @@ describe("getDefaultCommands", () => { } } }) + + it("DS-24: all locale command sets should include a Search Commands on Hub command", () => { + for (const locale of ALL_LOCALES) { + const commands = getDefaultCommands(locale) + const cmd = commands.find((c) => c.id === COMMAND_SEARCH_ID) + expect( + cmd, + `Missing command search command for locale: ${locale}`, + ).toBeDefined() + expect((cmd as any).openMode).toBe("popup") + expect((cmd as any).searchUrl).toContain("%pageUrl") + } + }) }) diff --git a/packages/extension/src/services/option/defaultSettings.ts b/packages/extension/src/services/option/defaultSettings.ts index 767b32a2..87930daf 100644 --- a/packages/extension/src/services/option/defaultSettings.ts +++ b/packages/extension/src/services/option/defaultSettings.ts @@ -80,6 +80,10 @@ const FOLDER_AI = "e4994c63-cfa7-4e49-9dfe-a79e6120a1ae" const FOLDER_MEDIA = "a3495269-0a4d-4866-a519-bca75ed1c246" const FOLDER_WORK = "01710cf1-ec8b-497f-8d1f-9cb716567bc4" +// Fixed ID for the "Search Commands on Hub" default command, used by the +// settings migration to detect whether a user already has it. +export const COMMAND_SEARCH_ID = "019f470a-cea5-7d6f-86cf-e7df9fb14ff1" + export default { settingVersion: VERSION, popupPlacement: PopupPlacement, @@ -383,8 +387,85 @@ export const DefaultCommands = [ height: PopupOption.height, }, }, + createCommandSearchCommand("Search Commands on Hub", "en"), ] as Command[] +// Creates a "Search Commands on Hub" command for the given locale. Every +// variant shares COMMAND_SEARCH_ID so the settings migration can detect +// whether a user already has one, regardless of which locale it was added in. +function createCommandSearchCommand(title: string, hubLocale: string): Command { + const iconUrl = + typeof chrome !== "undefined" && chrome.runtime?.getURL + ? chrome.runtime.getURL("images/search_command.png") + : "" + return { + id: COMMAND_SEARCH_ID, + title, + iconUrl, + openMode: OPEN_MODE.POPUP, + openModeSecondary: OPEN_MODE.TAB, + searchUrl: `https://selection-command.com/${hubLocale}?q=%pageUrl`, + popupOption: { + width: PopupOption.width, + height: PopupOption.height, + }, + spaceEncoding: SPACE_ENCODING.PLUS, + } as Command +} + +const CMD_COMMAND_SEARCH_JA = createCommandSearchCommand( + "コマンドをHubで検索", + "ja", +) +const CMD_COMMAND_SEARCH_ZH = createCommandSearchCommand( + "在 Hub 中搜索命令", + "zh-CN", +) +const CMD_COMMAND_SEARCH_KO = createCommandSearchCommand( + "Hub에서 명령어 검색", + "ko", +) +const CMD_COMMAND_SEARCH_RU = createCommandSearchCommand( + "Поиск команд в Hub", + "ru", +) +const CMD_COMMAND_SEARCH_DE = createCommandSearchCommand( + "Befehle im Hub suchen", + "de", +) +const CMD_COMMAND_SEARCH_FR = createCommandSearchCommand( + "Rechercher des commandes sur Hub", + "fr", +) +const CMD_COMMAND_SEARCH_ES = createCommandSearchCommand( + "Buscar comandos en Hub", + "es", +) +const CMD_COMMAND_SEARCH_PT_BR = createCommandSearchCommand( + "Pesquisar comandos no Hub", + "pt-BR", +) +const CMD_COMMAND_SEARCH_PT = createCommandSearchCommand( + "Pesquisar comandos no Hub", + "pt-PT", +) +const CMD_COMMAND_SEARCH_HI = createCommandSearchCommand( + "Hub पर कमांड खोजें", + "hi", +) +const CMD_COMMAND_SEARCH_ID_LANG = createCommandSearchCommand( + "Cari Perintah di Hub", + "id", +) +const CMD_COMMAND_SEARCH_MS = createCommandSearchCommand( + "Cari Perintah di Hub", + "ms", +) +const CMD_COMMAND_SEARCH_IT = createCommandSearchCommand( + "Cerca comandi su Hub", + "it", +) + // Common commands shared across locales const CMD_LINK_PREVIEW = { id: "$$drag-1", @@ -1744,6 +1825,7 @@ export const LOCALE_COMMANDS = { CMD_TRANSLATE_JA, CMD_DRIVE, CMD_EN_TO_JA, + CMD_COMMAND_SEARCH_JA, ], zh: [ CMD_LINK_PREVIEW, @@ -1759,6 +1841,7 @@ export const LOCALE_COMMANDS = { CMD_DRIVE, CMD_ZHIHU, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_ZH, ], ko: [ CMD_LINK_PREVIEW, @@ -1774,6 +1857,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_KO, ], ru: [ CMD_LINK_PREVIEW, @@ -1790,6 +1874,7 @@ export const LOCALE_COMMANDS = { CMD_VK, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_RU, ], de: [ CMD_LINK_PREVIEW, @@ -1805,6 +1890,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_DE, ], fr: [ CMD_LINK_PREVIEW, @@ -1820,6 +1906,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_FR, ], es: [ CMD_LINK_PREVIEW, @@ -1837,6 +1924,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_ES, ], "pt-br": [ CMD_LINK_PREVIEW, @@ -1852,6 +1940,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_PT_BR, ], pt: [ CMD_LINK_PREVIEW, @@ -1867,6 +1956,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_PT, ], hi: [ CMD_LINK_PREVIEW, @@ -1882,6 +1972,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_HI, ], id: [ CMD_LINK_PREVIEW, @@ -1897,6 +1988,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_ID_LANG, ], ms: [ CMD_LINK_PREVIEW, @@ -1912,6 +2004,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_MS, ], it: [ CMD_LINK_PREVIEW, @@ -1928,6 +2021,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_IT, ], } as Record diff --git a/packages/extension/src/services/settings/settings.test.ts b/packages/extension/src/services/settings/settings.test.ts index 34adfff0..4e08af6a 100644 --- a/packages/extension/src/services/settings/settings.test.ts +++ b/packages/extension/src/services/settings/settings.test.ts @@ -5,6 +5,7 @@ import { OptionSettings } from "../option/optionSettings" import DefaultSettings, { DefaultCommands, getDefaultCommands, + COMMAND_SEARCH_ID, } from "../option/defaultSettings" import { toDataURL } from "../dom" import { OPTION_FOLDER, VERSION, OPEN_MODE } from "@/const" @@ -670,4 +671,40 @@ describe("migrate function", () => { expect(result.windowOption).toBeDefined() expect(result.windowOption.sidePanelAutoHide).toBe(true) // Should preserve existing value }) + + it("ST-34: should add Search Commands on Hub command if not exists", async () => { + const oldData = { + settingVersion: "1.0.0", + commands: [], + folders: [], + pageRules: [], + } as any + + const result = await migrate(oldData) + + const cmd = result.commands.find((c: Command) => c.id === COMMAND_SEARCH_ID) + expect(cmd).toBeDefined() + expect(mockStorage.setCommands).toHaveBeenCalled() + }) + + it("ST-34-a: should not duplicate Search Commands on Hub command if already exists", async () => { + const existing = { + id: COMMAND_SEARCH_ID, + title: "Search Commands on Hub", + } + const oldData = { + settingVersion: "1.0.0", + commands: [existing], + folders: [], + pageRules: [], + } as any + + const result = await migrate(oldData) + + const matches = result.commands.filter( + (c: Command) => c.id === COMMAND_SEARCH_ID, + ) + expect(matches).toHaveLength(1) + expect(matches[0]).toBe(existing) + }) }) diff --git a/packages/extension/src/services/settings/settings.ts b/packages/extension/src/services/settings/settings.ts index eb7afdc3..a31d746b 100644 --- a/packages/extension/src/services/settings/settings.ts +++ b/packages/extension/src/services/settings/settings.ts @@ -4,6 +4,7 @@ import DefaultSettings, { DefaultCommands, getDefaultCommands, PopupPlacement, + COMMAND_SEARCH_ID, } from "../option/defaultSettings" import { OPTION_FOLDER, @@ -262,6 +263,9 @@ export const migrate = async (data: SettingsType): Promise => { if (versionDiff(currentVersion, "0.15.1") === VersionDiff.Old) { data = migrate0_15_1(data) } + if (versionDiff(currentVersion, "1.1.0") === VersionDiff.Old) { + data = await migrate1_1_0(data) + } data.settingVersion = VERSION as Version return data @@ -381,3 +385,19 @@ const migrate0_15_1 = (data: SettingsType): SettingsType => { } return data } + +const migrate1_1_0 = async (data: SettingsType): Promise => { + // Add the "Search Commands on Hub" command if not exists. + const hasCommandSearch = data.commands.some((c) => c.id === COMMAND_SEARCH_ID) + if (!hasCommandSearch) { + const defaultCommand = getDefaultCommands(getUILanguage()).find( + (c) => c.id === COMMAND_SEARCH_ID, + ) + if (defaultCommand != null) { + data.commands.push(defaultCommand) + await Storage.setCommands(data.commands) + console.debug("migrate 1.1.0: added command search") + } + } + return data +} diff --git a/packages/extension/src/types/index.ts b/packages/extension/src/types/index.ts index bedbdaa7..2fa75be7 100644 --- a/packages/extension/src/types/index.ts +++ b/packages/extension/src/types/index.ts @@ -238,6 +238,7 @@ export type ExecuteCommandParams = { useSecondary?: boolean useClipboard?: boolean changeState?: (state: ExecState, message?: string) => void + pageUrl?: string } export type ActionTypes = PAGE_ACTION_EVENT | PAGE_ACTION_CONTROL @@ -304,6 +305,7 @@ export type UrlParam = { selectionText: string spaceEncoding?: SPACE_ENCODING useClipboard?: boolean + pageUrl?: string } export type ShowToastParam = {