From 91b5629e010bbcaeeb6b16b9738bfc83714658f3 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Fri, 5 Jun 2026 08:06:06 +0800 Subject: [PATCH] fix: open inbox artifacts by type --- cmd/verstak-gui/bindings_links.go | 7 +++ frontend/src/App.svelte | 91 ++++++++++++++++++++++++----- frontend/src/wailsjs/go/main/App.js | 4 ++ scripts/check-gui-render.mjs | 21 ++++++- 4 files changed, 108 insertions(+), 15 deletions(-) diff --git a/cmd/verstak-gui/bindings_links.go b/cmd/verstak-gui/bindings_links.go index f8ba85d..6c06f1d 100644 --- a/cmd/verstak-gui/bindings_links.go +++ b/cmd/verstak-gui/bindings_links.go @@ -106,6 +106,13 @@ func (a *App) OpenLink(id string) error { return openExternalURL(l.URL) } +func (a *App) OpenURL(rawURL string) error { + if err := a.requireVault(); err != nil { + return err + } + return openExternalURL(rawURL) +} + func (a *App) createResolvedLink(nodeID, rawURL, title, note, source, capturedAt string) (*LinkDTO, error) { rawURL = strings.TrimSpace(rawURL) if rawURL == "" { diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 0ac27d9..a2eac89 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -898,6 +898,69 @@ } catch (e) { error = String(e) } } + function inboxArtifactKind(item) { + return item?.sourceKind || item?.captureKind || item?.type || '' + } + + function fileRecordToPreviewItem(item, record) { + return { + id: item.id, + nodeId: item.id, + fileId: record.id || record.fileId, + name: record.name || item.title, + type: 'file', + size: record.size || 0, + mime: record.mime || '' + } + } + + async function openInboxArtifact(item) { + const kind = inboxArtifactKind(item) + try { + if (kind === 'url' || item.type === 'link') { + if (!item.url) throw new Error('url required') + await wailsCall('OpenURL', item.url) + return + } + + if (kind === 'text' || item.type === 'note') { + const content = await wailsCall('ReadNote', item.id) + noteEditor = { id: item.id, title: item.title, content: content || '', dirty: false } + return + } + + if (kind === 'folder' || item.type === 'folder') { + selectedSection = '' + selectedNode = item + activeTab = 'files' + folderStack = [] + currentFolderId = null + selectedIds = [] + previewItem = null + await loadTabData(item.id) + await loadFolder(item.id) + return + } + + if (kind === 'file' || kind === 'image' || item.type === 'file') { + const records = await wailsCall('ListFiles', item.id) || [] + const record = records[0] + if (!record) throw new Error('file record not found') + const preview = fileRecordToPreviewItem(item, record) + if (canPreviewFile(preview)) { + await openPreview(preview) + } else { + await wailsCall('OpenFile', preview.fileId) + } + return + } + + await openNodeById(item.id) + } catch (e) { + error = String(e) + } + } + async function moveNodeToRoot(node) { closeContextMenu() try { @@ -2267,17 +2330,6 @@ {/if} - {#if previewItem} - wailsCall('OpenFile', e.detail)} - /> - {/if} - {:else if activeTab === 'inbox'}
{#if localInboxNodes.length === 0} @@ -2285,7 +2337,7 @@ {:else}
{#each localInboxNodes as item} -
openNodeById(item.id)} on:keydown={(e) => e.key === 'Enter' && openNodeById(item.id)}> +
openInboxArtifact(item)} on:keydown={(e) => e.key === 'Enter' && openInboxArtifact(item)}>
{item.title} @@ -2517,7 +2569,7 @@ {:else}
{#each inboxNodes as item} -
openNodeById(item.id)} on:keydown={(e) => e.key === 'Enter' && openNodeById(item.id)}> +
openInboxArtifact(item)} on:keydown={(e) => e.key === 'Enter' && openInboxArtifact(item)}>
{item.title} @@ -2531,7 +2583,7 @@ {/if} - +
@@ -3223,6 +3275,17 @@ /> {/if} + {#if previewItem} + wailsCall('OpenFile', e.detail)} + /> + {/if} + {#if showSettings} {/if} diff --git a/frontend/src/wailsjs/go/main/App.js b/frontend/src/wailsjs/go/main/App.js index 1a6e73a..49e1cd9 100644 --- a/frontend/src/wailsjs/go/main/App.js +++ b/frontend/src/wailsjs/go/main/App.js @@ -158,6 +158,10 @@ export function OpenLink(arg1) { return window['go']['main']['App']['OpenLink'](arg1); } +export function OpenURL(arg1) { + return window['go']['main']['App']['OpenURL'](arg1); +} + export function ListTrash() { return window['go']['main']['App']['ListTrash'](); } diff --git a/scripts/check-gui-render.mjs b/scripts/check-gui-render.mjs index cb8087c..ac327dd 100755 --- a/scripts/check-gui-render.mjs +++ b/scripts/check-gui-render.mjs @@ -144,6 +144,8 @@ async function runReadyScenario(cdp, url) { await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера') await assertText(cdp, 'example.test', 'inbox: clipboard URL captured') await assertText(cdp, 'Ссылка', 'inbox: clipboard URL kind visible') + await clickInboxItemButton(cdp, 'example.test', 'Открыть') + await assertEval(cdp, `window.__VERSTAK_GUI_SMOKE__.state.openedUrls.includes('https://example.test/from-clipboard')`, 'inbox: open URL launches external URL') await clickInboxItemButton(cdp, 'example.test', 'Разложить') await waitForSelector(cdp, '.modal input[type="text"]') await setInputValue(cdp, '.modal input[type="text"]', 'Smoke') @@ -155,6 +157,10 @@ async function runReadyScenario(cdp, url) { await emitDroppedFiles(cdp, ['/tmp/smoke-drop-folder']) await assertText(cdp, 'smoke-drop-folder', 'inbox: dropped folder captured') await assertText(cdp, 'Перетаскивание', 'inbox: dropped source visible') + await clickInboxItemButton(cdp, 'smoke-drop-folder', 'Открыть') + await assertEval(cdp, `document.querySelector('.tab.active')?.innerText.includes('Файлы')`, 'inbox: open folder switches to files tab') + await assertText(cdp, 'captured-folder-file.txt', 'inbox: open folder shows captured folder contents') + await clickText(cdp, '.nav-item', 'Неразобранное') await dispatchPasteImage(cdp, 'pasted-smoke.png', 'image/png', 'c21va2UtaW1hZ2U=') await assertText(cdp, 'pasted-smoke.png', 'inbox: clipboard image captured') await assertText(cdp, 'Изображение', 'inbox: clipboard image kind visible') @@ -167,12 +173,17 @@ async function runReadyScenario(cdp, url) { await clickText(cdp, '.modal-actions .btn', 'Разложить') await waitForGone(cdp, '.modal-overlay') await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('smoke-drop-folder')`, 'inbox: assigned item leaves inbox') + await clickInboxItemButton(cdp, 'pasted-smoke.png', 'Открыть') + await waitForSelector(cdp, '.preview-image') + await assertText(cdp, 'pasted-smoke.png', 'inbox: open image shows preview') + await click(cdp, '.action-btn-close') await clickInboxItemButton(cdp, 'pasted-smoke.png', 'Удалить') await clickText(cdp, '.overlay .btn', 'Удалить') await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('pasted-smoke.png')`, 'inbox: deleted item leaves inbox') await screenshot(cdp, 'inbox.png') await clickText(cdp, '.inbox-item', 'Inbox Smoke Item') await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list') + await clickText(cdp, '.note-editor-actions .btn', 'Закрыть') await clickText(cdp, '.nav-item', 'Корзина') await assertText(cdp, 'Trash Smoke Folder', 'trash: deleted node visible') @@ -739,6 +750,7 @@ function wailsMockSource() { links: { 'node-project': [], }, + openedUrls: [], }; const fileNodeDetails = { @@ -904,6 +916,11 @@ function wailsMockSource() { const title = String(sourcePath || '').split('/').filter(Boolean).pop() || 'Dropped file'; const kind = title.includes('folder') ? 'folder' : 'file'; const node = { id: 'node-capture-path-' + Date.now(), title, type: kind === 'folder' ? 'folder' : 'file', section: '', captureInbox: true, captureKind: kind, sourceKind: kind, captureSource: source || 'drop', captureStatus: 'unresolved', createdAt: now, capturedAt: now, has_children: false, children: [], ...ctx }; + if (kind === 'folder') { + state.files[node.id] = [{ id: 'captured-folder-file', nodeId: 'captured-folder-file', fileId: 'captured-folder-file', name: 'captured-folder-file.txt', type: 'file', size: 42, createdAt: now }]; + } else { + state.files[node.id] = [{ id: node.id, nodeId: node.id, fileId: node.id, name: title, type: 'file', size: 42, createdAt: now }]; + } state.nodes.push(node); return clone(inboxDTO(node)); }, @@ -911,6 +928,7 @@ function wailsMockSource() { CaptureFileDataWithContext: async (filename, _dataBase64, source, contextJSON) => { const ctx = parseCaptureContext(contextJSON); const node = { id: 'node-capture-data-' + Date.now(), title: filename, type: 'file', section: '', captureInbox: true, captureKind: filename.endsWith('.png') ? 'image' : 'file', sourceKind: filename.endsWith('.png') ? 'image' : 'file', captureSource: source || 'paste', captureStatus: 'unresolved', createdAt: now, capturedAt: now, has_children: false, children: [], ...ctx }; + state.files[node.id] = [{ id: node.id, nodeId: node.id, fileId: node.id, name: filename, type: 'file', size: 32, mime: filename.endsWith('.png') ? 'image/png' : 'text/plain', createdAt: now }]; state.nodes.push(node); return clone(inboxDTO(node)); }, @@ -966,6 +984,7 @@ function wailsMockSource() { return true; }, OpenLink: async () => true, + OpenURL: async (url) => { state.openedUrls.push(url); return true }, ListTrash: async () => clone({ trashPath: '/tmp/verstak-smoke-vault/.verstak/trash', nodes: [{ id: 'node-trash', title: 'Trash Smoke Folder', type: 'folder', fsPath: 'Trash Smoke Folder', deletedAt: now }], @@ -1032,7 +1051,7 @@ function wailsMockSource() { AddPathLink: async () => true, DeleteFileOrFolder: async () => true, OpenFile: async () => true, - GetFileBase64: async () => '', + GetFileBase64: async () => 'data:image/png;base64,c21va2UtaW1hZ2U=', ReadFileText: async () => 'Smoke file content', ListActions: async (nodeId) => clone(state.actions[nodeId] || []),