diff --git a/plugins/files/PLAN_FILES_IMPROVEMENTS.md b/plugins/files/PLAN_FILES_IMPROVEMENTS.md index a1169d8..8913082 100644 --- a/plugins/files/PLAN_FILES_IMPROVEMENTS.md +++ b/plugins/files/PLAN_FILES_IMPROVEMENTS.md @@ -9,7 +9,7 @@ | # | Feature | Приоритет | Зависит от | |---|---------|-----------|------------| | 1 | Контекстное меню (правый клик) | 🔥 High | — | -| 2 | Open External / Show in Explorer fallback | ✅ Done | — | +| 2 | Open External / Show in Explorer | ✅ Done | `files.openExternal` | | 3 | Кастомный ConfirmModal | 🔥 High | — | | 4 | Duplicate | 🔥 High | — | | 5 | Cut/Copy/Paste | 🔥 High | — | @@ -38,16 +38,15 @@ --- -### Feature 2: Open External / Show in Explorer fallback +### Feature 2: Open External / Show in Explorer -**Описание:** Пункты меню для будущего открытия файла во внешнем приложении и -показа папки в системном файловом менеджере. +**Описание:** Пункты меню для открытия файла во внешнем приложении и показа +файла/папки в системном файловом менеджере. -**Текущий v2 статус:** core/runtime API для внешнего открытия пока нет, поэтому -пункты меню показывают fallback modal с vault-relative path и кнопкой `Copy Path`. -Никаких v1 Wails мостов или прямых backend calls. - -Полное внешнее открытие остается deferred до отдельного публичного v2 API. +**Текущий v2 статус:** плагин использует публичные методы +`api.files.openExternal(relativePath)` и `api.files.showInFolder(relativePath)`, +guarded by `files.openExternal`. Fallback modal с vault-relative path и кнопкой +`Copy Path` показывается только если API недоступен или вернул ошибку. **Файлы:** `plugins/files/frontend/src/index.js` diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index 3d3066b..7b06fc5 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -271,14 +271,14 @@ return Promise.reject(new Error('clipboard unavailable')); } - function showExternalFallback(entry, mode) { + function showExternalFallback(entry, mode, reason) { if (!entry) return; var pathToShow = entry.relativePath; if (mode === 'explorer' && entry.type !== 'folder') { pathToShow = parentPath(entry.relativePath) || entry.relativePath; } var title = mode === 'explorer' ? 'Show in Explorer' : 'Open External'; - var message = title + ' is not available in the current v2 runtime yet.\nVault-relative path:\n' + pathToShow; + var message = title + ' failed.\n' + (reason ? String(reason) + '\n' : '') + 'Vault-relative path:\n' + pathToShow; confirmModal(message, { confirmText: 'Copy Path', cancelText: 'Close' }).then(function (copy) { if (!copy) return; copyTextToClipboard(pathToShow).catch(function (err) { @@ -796,6 +796,19 @@ return el('div', { className: 'files-ctx-menu-sep' }); } + function openExternalEntry(entry, mode) { + if (!entry) return; + var filesApi = api && api.files; + var action = mode === 'explorer' ? filesApi && filesApi.showInFolder : filesApi && filesApi.openExternal; + if (typeof action !== 'function') { + showExternalFallback(entry, mode, 'files external-open API is unavailable.'); + return; + } + action(entry.relativePath).catch(function (err) { + showExternalFallback(entry, mode, err && err.message ? err.message : err); + }); + } + function showCtxMenu(x, y, entry) { ctxTarget = entry; ctxMenu.innerHTML = ''; @@ -808,8 +821,8 @@ } var isFolder = entry.type === 'folder'; ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }, 'open', 'open')); - ctxMenu.appendChild(ctxItem('Open External', '', function () { showExternalFallback(entry, 'external'); }, 'open-external', 'external')); - ctxMenu.appendChild(ctxItem('Show in Explorer', '', function () { showExternalFallback(entry, 'explorer'); }, 'show-in-explorer', 'explorer')); + ctxMenu.appendChild(ctxItem('Open External', '', function () { openExternalEntry(entry, 'external'); }, 'open-external', 'external')); + ctxMenu.appendChild(ctxItem('Show in Explorer', '', function () { openExternalEntry(entry, 'explorer'); }, 'show-in-explorer', 'explorer')); ctxMenu.appendChild(ctxSep()); ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename')); if (entry.type !== 'folder') { diff --git a/plugins/files/plugin.json b/plugins/files/plugin.json index a62470e..387661c 100644 --- a/plugins/files/plugin.json +++ b/plugins/files/plugin.json @@ -18,6 +18,7 @@ "files.read", "files.write", "files.delete", + "files.openExternal", "workbench.open", "ui.register" ], diff --git a/scripts/smoke-files-plugin.js b/scripts/smoke-files-plugin.js index e1a3eb9..122e1a5 100755 --- a/scripts/smoke-files-plugin.js +++ b/scripts/smoke-files-plugin.js @@ -196,7 +196,9 @@ function loadFilesComponent(document) { } function makeApi() { + const externalCalls = []; return { + externalCalls, files: { list: async () => [ { @@ -214,6 +216,8 @@ function makeApi() { createFolder: async () => undefined, move: async () => undefined, trash: async () => undefined, + openExternal: async (relativePath) => { externalCalls.push({ action: 'open', path: relativePath }); }, + showInFolder: async (relativePath) => { externalCalls.push({ action: 'show', path: relativePath }); }, }, workbench: { openResource: async () => ({ status: 'opened' }), @@ -229,7 +233,8 @@ async function flush() { const document = makeDocument(); const { component, clipboard } = loadFilesComponent(document); const container = new FakeNode('div'); - component.mount(container, {}, makeApi()); + const api = makeApi(); + component.mount(container, {}, api); await flush(); const row = walk(container, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/readme.md'); @@ -245,12 +250,21 @@ async function flush() { if (!showInExplorer) throw new Error('Show in Explorer menu item not found'); openExternal.click(); - const copyButton = walk(document.body, (node) => node.tagName === 'BUTTON' && node.textContent === 'Copy Path'); - if (!copyButton) throw new Error('external fallback did not show Copy Path action'); - copyButton.click(); await flush(); - if (clipboard.written[0] !== 'Docs/readme.md') { - throw new Error(`expected copied path Docs/readme.md, got ${clipboard.written[0] || ''}`); + if (!api.externalCalls.some((call) => call.action === 'open' && call.path === 'Docs/readme.md')) { + throw new Error(`expected openExternal call for Docs/readme.md, got ${JSON.stringify(api.externalCalls)}`); + } + if (walk(document.body, (node) => node.tagName === 'BUTTON' && node.textContent === 'Copy Path')) { + throw new Error('external fallback should not show Copy Path after successful API call'); + } + + showInExplorer.click(); + await flush(); + if (!api.externalCalls.some((call) => call.action === 'show' && call.path === 'Docs/readme.md')) { + throw new Error(`expected showInFolder call for Docs/readme.md, got ${JSON.stringify(api.externalCalls)}`); + } + if (clipboard.written.length !== 0) { + throw new Error(`expected no copied path after successful external API calls, got ${clipboard.written.join(', ')}`); } console.log('files frontend smoke passed');