feat: unify frontend capture pipeline
This commit is contained in:
+124
-17
@@ -142,13 +142,12 @@ async function runReadyScenario(cdp, url) {
|
||||
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('Manual Root Item')`, 'inbox: manual root is hidden')
|
||||
await setClipboardText(cdp, 'https://example.test/from-clipboard')
|
||||
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
|
||||
await assertText(cdp, 'https://example.test/from-clipboard', 'inbox: clipboard URL captured')
|
||||
await assertText(cdp, 'example.test', 'inbox: clipboard URL captured')
|
||||
await assertText(cdp, 'Ссылка', 'inbox: clipboard URL kind visible')
|
||||
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 setClipboardImage(cdp, 'pasted-smoke.png', 'image/png', 'c21va2UtaW1hZ2U=')
|
||||
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
|
||||
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')
|
||||
await clickInboxItemButton(cdp, 'smoke-drop-folder', 'Разложить')
|
||||
@@ -379,6 +378,24 @@ async function setClipboardImage(cdp, name, type, base64) {
|
||||
})
|
||||
}
|
||||
|
||||
async function dispatchPasteImage(cdp, name, type, base64) {
|
||||
await cdp.send('Runtime.evaluate', {
|
||||
expression: `
|
||||
(() => {
|
||||
const bytes = Uint8Array.from(atob(${JSON.stringify(base64)}), c => c.charCodeAt(0));
|
||||
const file = new File([bytes], ${JSON.stringify(name)}, { type: ${JSON.stringify(type)} });
|
||||
const data = new DataTransfer();
|
||||
data.items.add(file);
|
||||
const event = new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: data });
|
||||
window.dispatchEvent(event);
|
||||
})();
|
||||
`,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
})
|
||||
await sleep(300)
|
||||
}
|
||||
|
||||
async function emitDroppedFiles(cdp, paths) {
|
||||
await cdp.send('Runtime.evaluate', {
|
||||
expression: `window.__VERSTAK_GUI_SMOKE__.dropFiles(${JSON.stringify(paths)})`,
|
||||
@@ -684,6 +701,9 @@ function wailsMockSource() {
|
||||
worklog: {
|
||||
'node-project': [{ id: 'wl-1', nodeId: 'node-project', summary: 'Manual smoke entry', details: 'Smoke details', minutes: 45, billable: true, approximate: false, source: 'manual', createdAt: now, date: '2026-06-04' }],
|
||||
},
|
||||
links: {
|
||||
'node-project': [],
|
||||
},
|
||||
};
|
||||
|
||||
const fileNodeDetails = {
|
||||
@@ -718,6 +738,50 @@ function wailsMockSource() {
|
||||
return node?.children || [];
|
||||
}
|
||||
|
||||
function parseCaptureContext(contextJSON) {
|
||||
let raw = {};
|
||||
try { raw = JSON.parse(contextJSON || '{}') || {}; } catch {}
|
||||
const out = {};
|
||||
if (raw.contextType === 'node' && raw.nodeId) {
|
||||
out.captureContextType = 'node';
|
||||
out.captureContextNodeId = raw.nodeId;
|
||||
out.suggestedTargetNodeId = raw.suggestedTargetNodeId || raw.nodeId;
|
||||
out.captureContextLabel = findNode(raw.nodeId)?.title || raw.nodeId;
|
||||
out.suggestedTargetLabel = findNode(out.suggestedTargetNodeId)?.title || out.suggestedTargetNodeId;
|
||||
} else if (raw.contextType === 'section') {
|
||||
out.captureContextType = 'section';
|
||||
out.captureContextSection = raw.section || 'root';
|
||||
out.captureContextLabel = out.captureContextSection;
|
||||
} else {
|
||||
out.captureContextType = 'global';
|
||||
out.captureContextSection = raw.section || 'root';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function hostnameForURL(value) {
|
||||
try { return new URL(value).hostname; } catch { return ''; }
|
||||
}
|
||||
|
||||
function inboxDTO(node) {
|
||||
return {
|
||||
...node,
|
||||
captureKind: node.captureKind || '',
|
||||
sourceKind: node.sourceKind || node.captureKind || '',
|
||||
captureSource: node.captureSource || '',
|
||||
captureStatus: node.captureStatus || 'unresolved',
|
||||
captureContextType: node.captureContextType || 'global',
|
||||
captureContextNodeId: node.captureContextNodeId || '',
|
||||
captureContextSection: node.captureContextSection || '',
|
||||
suggestedTargetNodeId: node.suggestedTargetNodeId || '',
|
||||
captureContextLabel: node.captureContextLabel || '',
|
||||
suggestedTargetLabel: node.suggestedTargetLabel || '',
|
||||
capturedAt: node.capturedAt || node.createdAt || '',
|
||||
url: node.url || '',
|
||||
hostname: node.hostname || '',
|
||||
};
|
||||
}
|
||||
|
||||
function detachNode(id, items = state.nodes) {
|
||||
const idx = items.findIndex((node) => node.id === id);
|
||||
if (idx >= 0) {
|
||||
@@ -783,33 +847,55 @@ function wailsMockSource() {
|
||||
],
|
||||
ListWorkspaceTree: async () => clone(state.nodes.filter((node) => node.captureInbox !== true)),
|
||||
ListWorkspaceChildren: async (id) => clone(childrenOf(id)),
|
||||
ListInboxNodes: async () => clone(state.nodes.filter((node) => !node.parent_id && node.captureInbox === true).map((node) => ({ ...node, captureKind: node.captureKind || '', captureSource: node.captureSource || '' }))),
|
||||
CaptureText: async (text) => {
|
||||
const node = { id: 'node-capture-text-' + Date.now(), title: String(text || '').trim().split('\\n').find(Boolean) || 'Captured text', type: 'note', section: '', captureInbox: true, captureKind: 'text', captureSource: 'clipboard', createdAt: now, has_children: false, children: [] };
|
||||
ListInboxNodes: async () => clone(state.nodes.filter((node) => !node.parent_id && node.captureInbox === true).map(inboxDTO)),
|
||||
ListInboxNodesForTarget: async (nodeId) => clone(state.nodes.filter((node) => node.captureInbox === true && (node.captureContextNodeId === nodeId || node.suggestedTargetNodeId === nodeId)).map(inboxDTO)),
|
||||
CaptureTextWithContext: async (text, source, contextJSON) => {
|
||||
const ctx = parseCaptureContext(contextJSON);
|
||||
const node = { id: 'node-capture-text-' + Date.now(), title: String(text || '').trim().split('\\n').find(Boolean) || 'Captured text', type: 'note', section: '', captureInbox: true, captureKind: 'text', sourceKind: 'text', captureSource: source || 'paste', captureStatus: 'unresolved', createdAt: now, capturedAt: now, has_children: false, children: [], ...ctx };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
return clone(inboxDTO(node));
|
||||
},
|
||||
CaptureURL: async (url, title) => {
|
||||
const node = { id: 'node-capture-url-' + Date.now(), title: title || url, type: 'note', section: '', captureInbox: true, captureKind: 'url', captureSource: 'clipboard', createdAt: now, has_children: false, children: [] };
|
||||
CaptureText: async (text) => App.CaptureTextWithContext(text, 'clipboard', '{}'),
|
||||
CaptureURLWithContext: async (url, title, source, contextJSON) => {
|
||||
const ctx = parseCaptureContext(contextJSON);
|
||||
const hostname = hostnameForURL(url);
|
||||
const node = { id: 'node-capture-url-' + Date.now(), title: title || hostname || url, type: 'link', section: '', captureInbox: true, captureKind: 'url', sourceKind: 'url', captureSource: source || 'paste', captureStatus: 'unresolved', url, hostname, createdAt: now, capturedAt: now, has_children: false, children: [], ...ctx };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
return clone(inboxDTO(node));
|
||||
},
|
||||
CapturePath: async (sourcePath) => {
|
||||
CaptureURL: async (url, title) => App.CaptureURLWithContext(url, title, 'clipboard', '{}'),
|
||||
CapturePathWithContext: async (sourcePath, source, contextJSON) => {
|
||||
const ctx = parseCaptureContext(contextJSON);
|
||||
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, captureSource: 'drop', createdAt: now, has_children: false, children: [] };
|
||||
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 };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
return clone(inboxDTO(node));
|
||||
},
|
||||
CaptureFileData: async (filename) => {
|
||||
const node = { id: 'node-capture-data-' + Date.now(), title: filename, type: 'file', section: '', captureInbox: true, captureKind: filename.endsWith('.png') ? 'image' : 'file', captureSource: 'clipboard', createdAt: now, has_children: false, children: [] };
|
||||
CapturePath: async (sourcePath) => App.CapturePathWithContext(sourcePath, 'drop', '{}'),
|
||||
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.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
return clone(inboxDTO(node));
|
||||
},
|
||||
AssignInboxNode: async (nodeId, targetParentId) => {
|
||||
CaptureFileData: async (filename, dataBase64) => App.CaptureFileDataWithContext(filename, dataBase64, 'clipboard', '{}'),
|
||||
ReadClipboardText: async () => window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ || '',
|
||||
CaptureClipboardTextWithContext: async (contextJSON) => {
|
||||
const text = String(window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ || '').trim();
|
||||
if (!text) throw new Error('clipboard is empty');
|
||||
if (/^https?:\\/\\//.test(text)) return App.CaptureURLWithContext(text, '', 'clipboard_button', contextJSON);
|
||||
return App.CaptureTextWithContext(text, 'clipboard_button', contextJSON);
|
||||
},
|
||||
ResolveInboxNode: async (nodeId, targetParentId) => {
|
||||
const node = detachNode(nodeId);
|
||||
const parent = findNode(targetParentId);
|
||||
if (!node || !parent) throw new Error('assign target not found');
|
||||
if (node.sourceKind === 'url' || node.captureKind === 'url') {
|
||||
state.links[targetParentId] = state.links[targetParentId] || [];
|
||||
state.links[targetParentId].push({ id: 'link-' + Date.now(), nodeId: targetParentId, title: node.title, url: node.url, hostname: node.hostname || hostnameForURL(node.url), note: '', source: node.captureSource, capturedAt: node.capturedAt || now, createdAt: now, updatedAt: now });
|
||||
return clone(parent);
|
||||
}
|
||||
node.captureInbox = false;
|
||||
node.captureKind = '';
|
||||
node.captureSource = '';
|
||||
@@ -819,11 +905,32 @@ function wailsMockSource() {
|
||||
parent.has_children = true;
|
||||
return clone(node);
|
||||
},
|
||||
ResolveInboxNodeHere: async (nodeId) => {
|
||||
const node = findNode(nodeId);
|
||||
if (!node?.suggestedTargetNodeId) throw new Error('suggested target is required');
|
||||
return App.ResolveInboxNode(nodeId, node.suggestedTargetNodeId);
|
||||
},
|
||||
AssignInboxNode: async (nodeId, targetParentId) => App.ResolveInboxNode(nodeId, targetParentId),
|
||||
DeleteInboxNode: async (nodeId) => {
|
||||
const node = detachNode(nodeId);
|
||||
if (!node) throw new Error('inbox node not found');
|
||||
return true;
|
||||
},
|
||||
ListLinks: async (nodeId) => clone(state.links[nodeId] || []),
|
||||
UpdateLink: async (id, title, url, note) => {
|
||||
const list = Object.values(state.links).flat();
|
||||
const link = list.find((item) => item.id === id);
|
||||
if (!link) throw new Error('link not found');
|
||||
Object.assign(link, { title, url, note, hostname: hostnameForURL(url), updatedAt: now });
|
||||
return clone(link);
|
||||
},
|
||||
DeleteLink: async (id) => {
|
||||
for (const key of Object.keys(state.links)) {
|
||||
state.links[key] = state.links[key].filter((link) => link.id !== id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
OpenLink: async () => 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 }],
|
||||
|
||||
Reference in New Issue
Block a user