Use external open API in Files plugin

This commit is contained in:
mirivlad 2026-06-27 13:34:16 +08:00
parent bf21df14f1
commit 084580d3fd
4 changed files with 46 additions and 19 deletions

View File

@ -9,7 +9,7 @@
| # | Feature | Приоритет | Зависит от | | # | Feature | Приоритет | Зависит от |
|---|---------|-----------|------------| |---|---------|-----------|------------|
| 1 | Контекстное меню (правый клик) | 🔥 High | — | | 1 | Контекстное меню (правый клик) | 🔥 High | — |
| 2 | Open External / Show in Explorer fallback | ✅ Done | — | | 2 | Open External / Show in Explorer | ✅ Done | `files.openExternal` |
| 3 | Кастомный ConfirmModal | 🔥 High | — | | 3 | Кастомный ConfirmModal | 🔥 High | — |
| 4 | Duplicate | 🔥 High | — | | 4 | Duplicate | 🔥 High | — |
| 5 | Cut/Copy/Paste | 🔥 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 для внешнего открытия пока нет, поэтому **Текущий v2 статус:** плагин использует публичные методы
пункты меню показывают fallback modal с vault-relative path и кнопкой `Copy Path`. `api.files.openExternal(relativePath)` и `api.files.showInFolder(relativePath)`,
Никаких v1 Wails мостов или прямых backend calls. guarded by `files.openExternal`. Fallback modal с vault-relative path и кнопкой
`Copy Path` показывается только если API недоступен или вернул ошибку.
Полное внешнее открытие остается deferred до отдельного публичного v2 API.
**Файлы:** `plugins/files/frontend/src/index.js` **Файлы:** `plugins/files/frontend/src/index.js`

View File

@ -271,14 +271,14 @@
return Promise.reject(new Error('clipboard unavailable')); return Promise.reject(new Error('clipboard unavailable'));
} }
function showExternalFallback(entry, mode) { function showExternalFallback(entry, mode, reason) {
if (!entry) return; if (!entry) return;
var pathToShow = entry.relativePath; var pathToShow = entry.relativePath;
if (mode === 'explorer' && entry.type !== 'folder') { if (mode === 'explorer' && entry.type !== 'folder') {
pathToShow = parentPath(entry.relativePath) || entry.relativePath; pathToShow = parentPath(entry.relativePath) || entry.relativePath;
} }
var title = mode === 'explorer' ? 'Show in Explorer' : 'Open External'; 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) { confirmModal(message, { confirmText: 'Copy Path', cancelText: 'Close' }).then(function (copy) {
if (!copy) return; if (!copy) return;
copyTextToClipboard(pathToShow).catch(function (err) { copyTextToClipboard(pathToShow).catch(function (err) {
@ -796,6 +796,19 @@
return el('div', { className: 'files-ctx-menu-sep' }); 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) { function showCtxMenu(x, y, entry) {
ctxTarget = entry; ctxTarget = entry;
ctxMenu.innerHTML = ''; ctxMenu.innerHTML = '';
@ -808,8 +821,8 @@
} }
var isFolder = entry.type === 'folder'; var isFolder = entry.type === 'folder';
ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }, 'open', 'open')); 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('Open External', '', function () { openExternalEntry(entry, 'external'); }, 'open-external', 'external'));
ctxMenu.appendChild(ctxItem('Show in Explorer', '', function () { showExternalFallback(entry, 'explorer'); }, 'show-in-explorer', 'explorer')); ctxMenu.appendChild(ctxItem('Show in Explorer', '', function () { openExternalEntry(entry, 'explorer'); }, 'show-in-explorer', 'explorer'));
ctxMenu.appendChild(ctxSep()); ctxMenu.appendChild(ctxSep());
ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename')); ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename'));
if (entry.type !== 'folder') { if (entry.type !== 'folder') {

View File

@ -18,6 +18,7 @@
"files.read", "files.read",
"files.write", "files.write",
"files.delete", "files.delete",
"files.openExternal",
"workbench.open", "workbench.open",
"ui.register" "ui.register"
], ],

View File

@ -196,7 +196,9 @@ function loadFilesComponent(document) {
} }
function makeApi() { function makeApi() {
const externalCalls = [];
return { return {
externalCalls,
files: { files: {
list: async () => [ list: async () => [
{ {
@ -214,6 +216,8 @@ function makeApi() {
createFolder: async () => undefined, createFolder: async () => undefined,
move: async () => undefined, move: async () => undefined,
trash: async () => undefined, trash: async () => undefined,
openExternal: async (relativePath) => { externalCalls.push({ action: 'open', path: relativePath }); },
showInFolder: async (relativePath) => { externalCalls.push({ action: 'show', path: relativePath }); },
}, },
workbench: { workbench: {
openResource: async () => ({ status: 'opened' }), openResource: async () => ({ status: 'opened' }),
@ -229,7 +233,8 @@ async function flush() {
const document = makeDocument(); const document = makeDocument();
const { component, clipboard } = loadFilesComponent(document); const { component, clipboard } = loadFilesComponent(document);
const container = new FakeNode('div'); const container = new FakeNode('div');
component.mount(container, {}, makeApi()); const api = makeApi();
component.mount(container, {}, api);
await flush(); await flush();
const row = walk(container, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/readme.md'); 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'); if (!showInExplorer) throw new Error('Show in Explorer menu item not found');
openExternal.click(); 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(); await flush();
if (clipboard.written[0] !== 'Docs/readme.md') { if (!api.externalCalls.some((call) => call.action === 'open' && call.path === 'Docs/readme.md')) {
throw new Error(`expected copied path Docs/readme.md, got ${clipboard.written[0] || '<none>'}`); 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'); console.log('files frontend smoke passed');