fix: open inbox artifacts by type
This commit is contained in:
parent
db47d31183
commit
91b5629e01
|
|
@ -106,6 +106,13 @@ func (a *App) OpenLink(id string) error {
|
||||||
return openExternalURL(l.URL)
|
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) {
|
func (a *App) createResolvedLink(nodeID, rawURL, title, note, source, capturedAt string) (*LinkDTO, error) {
|
||||||
rawURL = strings.TrimSpace(rawURL)
|
rawURL = strings.TrimSpace(rawURL)
|
||||||
if rawURL == "" {
|
if rawURL == "" {
|
||||||
|
|
|
||||||
|
|
@ -898,6 +898,69 @@
|
||||||
} catch (e) { error = String(e) }
|
} 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) {
|
async function moveNodeToRoot(node) {
|
||||||
closeContextMenu()
|
closeContextMenu()
|
||||||
try {
|
try {
|
||||||
|
|
@ -2267,17 +2330,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if previewItem}
|
|
||||||
<FilePreviewModal
|
|
||||||
item={previewItem}
|
|
||||||
content={previewContent}
|
|
||||||
loading={previewLoading}
|
|
||||||
error={previewError}
|
|
||||||
on:close={closePreview}
|
|
||||||
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{:else if activeTab === 'inbox'}
|
{:else if activeTab === 'inbox'}
|
||||||
<div class="inbox-tab">
|
<div class="inbox-tab">
|
||||||
{#if localInboxNodes.length === 0}
|
{#if localInboxNodes.length === 0}
|
||||||
|
|
@ -2285,7 +2337,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="inbox-list">
|
<div class="inbox-list">
|
||||||
{#each localInboxNodes as item}
|
{#each localInboxNodes as item}
|
||||||
<div class="inbox-item" role="button" tabindex="0" on:click={() => openNodeById(item.id)} on:keydown={(e) => e.key === 'Enter' && openNodeById(item.id)}>
|
<div class="inbox-item" role="button" tabindex="0" on:click={() => openInboxArtifact(item)} on:keydown={(e) => e.key === 'Enter' && openInboxArtifact(item)}>
|
||||||
<div class="inbox-item-main">
|
<div class="inbox-item-main">
|
||||||
<span class="inbox-item-title">{item.title}</span>
|
<span class="inbox-item-title">{item.title}</span>
|
||||||
<span class="inbox-item-meta">
|
<span class="inbox-item-meta">
|
||||||
|
|
@ -2517,7 +2569,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="inbox-list">
|
<div class="inbox-list">
|
||||||
{#each inboxNodes as item}
|
{#each inboxNodes as item}
|
||||||
<div class="inbox-item" role="button" tabindex="0" on:click={() => openNodeById(item.id)} on:keydown={(e) => e.key === 'Enter' && openNodeById(item.id)}>
|
<div class="inbox-item" role="button" tabindex="0" on:click={() => openInboxArtifact(item)} on:keydown={(e) => e.key === 'Enter' && openInboxArtifact(item)}>
|
||||||
<div class="inbox-item-main">
|
<div class="inbox-item-main">
|
||||||
<span class="inbox-item-title">{item.title}</span>
|
<span class="inbox-item-title">{item.title}</span>
|
||||||
<span class="inbox-item-meta">
|
<span class="inbox-item-meta">
|
||||||
|
|
@ -2531,7 +2583,7 @@
|
||||||
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => resolveInboxHere(item)}>{t('inbox.keepHere')}</button>
|
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => resolveInboxHere(item)}>{t('inbox.keepHere')}</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => openAssignInbox(item)}>{t('inbox.assign')}</button>
|
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => openAssignInbox(item)}>{t('inbox.assign')}</button>
|
||||||
<button class="btn btn-sm" on:click|stopPropagation={() => openNodeById(item.id)}>{t('common.open')}</button>
|
<button class="btn btn-sm" on:click|stopPropagation={() => openInboxArtifact(item)}>{t('common.open')}</button>
|
||||||
<button class="btn btn-sm" on:click|stopPropagation={() => openNodeFolder(item)}>{t('file.showInExplorer')}</button>
|
<button class="btn btn-sm" on:click|stopPropagation={() => openNodeFolder(item)}>{t('file.showInExplorer')}</button>
|
||||||
<button class="btn btn-sm btn-danger" on:click|stopPropagation={() => confirmDeleteInbox(item)}>{t('common.delete')}</button>
|
<button class="btn btn-sm btn-danger" on:click|stopPropagation={() => confirmDeleteInbox(item)}>{t('common.delete')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3223,6 +3275,17 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if previewItem}
|
||||||
|
<FilePreviewModal
|
||||||
|
item={previewItem}
|
||||||
|
content={previewContent}
|
||||||
|
loading={previewLoading}
|
||||||
|
error={previewError}
|
||||||
|
on:close={closePreview}
|
||||||
|
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showSettings}
|
{#if showSettings}
|
||||||
<SettingsWindow onClose={closeSettings} onSyncRefresh={loadSyncStatus} initialSection={settingsInitialSection} />
|
<SettingsWindow onClose={closeSettings} onSyncRefresh={loadSyncStatus} initialSection={settingsInitialSection} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,10 @@ export function OpenLink(arg1) {
|
||||||
return window['go']['main']['App']['OpenLink'](arg1);
|
return window['go']['main']['App']['OpenLink'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OpenURL(arg1) {
|
||||||
|
return window['go']['main']['App']['OpenURL'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function ListTrash() {
|
export function ListTrash() {
|
||||||
return window['go']['main']['App']['ListTrash']();
|
return window['go']['main']['App']['ListTrash']();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,8 @@ async function runReadyScenario(cdp, url) {
|
||||||
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
|
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
|
||||||
await assertText(cdp, 'example.test', 'inbox: clipboard URL captured')
|
await assertText(cdp, 'example.test', 'inbox: clipboard URL captured')
|
||||||
await assertText(cdp, 'Ссылка', 'inbox: clipboard URL kind visible')
|
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 clickInboxItemButton(cdp, 'example.test', 'Разложить')
|
||||||
await waitForSelector(cdp, '.modal input[type="text"]')
|
await waitForSelector(cdp, '.modal input[type="text"]')
|
||||||
await setInputValue(cdp, '.modal input[type="text"]', 'Smoke')
|
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 emitDroppedFiles(cdp, ['/tmp/smoke-drop-folder'])
|
||||||
await assertText(cdp, 'smoke-drop-folder', 'inbox: dropped folder captured')
|
await assertText(cdp, 'smoke-drop-folder', 'inbox: dropped folder captured')
|
||||||
await assertText(cdp, 'Перетаскивание', 'inbox: dropped source visible')
|
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 dispatchPasteImage(cdp, 'pasted-smoke.png', 'image/png', 'c21va2UtaW1hZ2U=')
|
||||||
await assertText(cdp, 'pasted-smoke.png', 'inbox: clipboard image captured')
|
await assertText(cdp, 'pasted-smoke.png', 'inbox: clipboard image captured')
|
||||||
await assertText(cdp, 'Изображение', 'inbox: clipboard image kind visible')
|
await assertText(cdp, 'Изображение', 'inbox: clipboard image kind visible')
|
||||||
|
|
@ -167,12 +173,17 @@ async function runReadyScenario(cdp, url) {
|
||||||
await clickText(cdp, '.modal-actions .btn', 'Разложить')
|
await clickText(cdp, '.modal-actions .btn', 'Разложить')
|
||||||
await waitForGone(cdp, '.modal-overlay')
|
await waitForGone(cdp, '.modal-overlay')
|
||||||
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('smoke-drop-folder')`, 'inbox: assigned item leaves inbox')
|
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 clickInboxItemButton(cdp, 'pasted-smoke.png', 'Удалить')
|
||||||
await clickText(cdp, '.overlay .btn', 'Удалить')
|
await clickText(cdp, '.overlay .btn', 'Удалить')
|
||||||
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('pasted-smoke.png')`, 'inbox: deleted item leaves inbox')
|
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('pasted-smoke.png')`, 'inbox: deleted item leaves inbox')
|
||||||
await screenshot(cdp, 'inbox.png')
|
await screenshot(cdp, 'inbox.png')
|
||||||
await clickText(cdp, '.inbox-item', 'Inbox Smoke Item')
|
await clickText(cdp, '.inbox-item', 'Inbox Smoke Item')
|
||||||
await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list')
|
await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list')
|
||||||
|
await clickText(cdp, '.note-editor-actions .btn', 'Закрыть')
|
||||||
|
|
||||||
await clickText(cdp, '.nav-item', 'Корзина')
|
await clickText(cdp, '.nav-item', 'Корзина')
|
||||||
await assertText(cdp, 'Trash Smoke Folder', 'trash: deleted node visible')
|
await assertText(cdp, 'Trash Smoke Folder', 'trash: deleted node visible')
|
||||||
|
|
@ -739,6 +750,7 @@ function wailsMockSource() {
|
||||||
links: {
|
links: {
|
||||||
'node-project': [],
|
'node-project': [],
|
||||||
},
|
},
|
||||||
|
openedUrls: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileNodeDetails = {
|
const fileNodeDetails = {
|
||||||
|
|
@ -904,6 +916,11 @@ function wailsMockSource() {
|
||||||
const title = String(sourcePath || '').split('/').filter(Boolean).pop() || 'Dropped file';
|
const title = String(sourcePath || '').split('/').filter(Boolean).pop() || 'Dropped file';
|
||||||
const kind = title.includes('folder') ? 'folder' : '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 };
|
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);
|
state.nodes.push(node);
|
||||||
return clone(inboxDTO(node));
|
return clone(inboxDTO(node));
|
||||||
},
|
},
|
||||||
|
|
@ -911,6 +928,7 @@ function wailsMockSource() {
|
||||||
CaptureFileDataWithContext: async (filename, _dataBase64, source, contextJSON) => {
|
CaptureFileDataWithContext: async (filename, _dataBase64, source, contextJSON) => {
|
||||||
const ctx = parseCaptureContext(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 };
|
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);
|
state.nodes.push(node);
|
||||||
return clone(inboxDTO(node));
|
return clone(inboxDTO(node));
|
||||||
},
|
},
|
||||||
|
|
@ -966,6 +984,7 @@ function wailsMockSource() {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
OpenLink: async () => true,
|
OpenLink: async () => true,
|
||||||
|
OpenURL: async (url) => { state.openedUrls.push(url); return true },
|
||||||
ListTrash: async () => clone({
|
ListTrash: async () => clone({
|
||||||
trashPath: '/tmp/verstak-smoke-vault/.verstak/trash',
|
trashPath: '/tmp/verstak-smoke-vault/.verstak/trash',
|
||||||
nodes: [{ id: 'node-trash', title: 'Trash Smoke Folder', type: 'folder', fsPath: 'Trash Smoke Folder', deletedAt: now }],
|
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,
|
AddPathLink: async () => true,
|
||||||
DeleteFileOrFolder: async () => true,
|
DeleteFileOrFolder: async () => true,
|
||||||
OpenFile: async () => true,
|
OpenFile: async () => true,
|
||||||
GetFileBase64: async () => '',
|
GetFileBase64: async () => 'data:image/png;base64,c21va2UtaW1hZ2U=',
|
||||||
ReadFileText: async () => 'Smoke file content',
|
ReadFileText: async () => 'Smoke file content',
|
||||||
|
|
||||||
ListActions: async (nodeId) => clone(state.actions[nodeId] || []),
|
ListActions: async (nodeId) => clone(state.actions[nodeId] || []),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue