diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index ff3fce6..51e3541 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -92,8 +92,15 @@ let suggestions = [] let suggestionCount = 0 let inboxNodes = [] + let localInboxNodes = [] let inboxCaptureBusy = false let inboxCaptureStatus = '' + let links = [] + let editingLink = null + let linkTitle = '' + let linkURL = '' + let linkNote = '' + let linkStatus = '' let trashInfo = null let showCreateNode = false let newNodeTitle = '' @@ -170,6 +177,8 @@ { id: 'overview', label: t('tab.overview') }, { id: 'notes', label: t('tab.notes') }, { id: 'files', label: t('tab.files') }, + { id: 'inbox', label: t('tab.inbox') }, + { id: 'links', label: t('tab.links') }, { id: 'actions', label: t('tab.actions') }, { id: 'worklog', label: t('tab.worklog') }, { id: 'activity', label: t('tab.activity') }, @@ -248,6 +257,8 @@ files = [] actions = [] worklog = [] + localInboxNodes = [] + links = [] suggestions = [] inboxNodes = [] trashInfo = null @@ -312,6 +323,8 @@ try { files = await wailsCall('ListFiles', nodeID) || [] } catch(e) {} try { actions = await wailsCall('ListActions', nodeID) || [] } catch(e) {} try { worklog = initWorklogEntries(await wailsCall('ListWorklog', nodeID)) } catch(e) {} + try { localInboxNodes = await wailsCall('ListInboxNodesForTarget', nodeID) || [] } catch(e) { localInboxNodes = [] } + try { links = await wailsCall('ListLinks', nodeID) || [] } catch(e) { links = [] } try { suggestions = await wailsCall('GetSuggestions') || [] suggestionCount = suggestions.length @@ -1395,7 +1408,7 @@ try { return new Date(str).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }) } catch (e) { return str } } function nodeKindLabel(kind) { - const labels = { 'project': t('kind.project'), 'client': t('kind.client'), 'document': t('kind.document'), 'recipe': t('kind.recipe'), 'folder': t('kind.folder'), 'note': t('kind.note'), 'file': t('kind.file'), 'archive': t('kind.archive'), 'case': t('kind.case') } + const labels = { 'project': t('kind.project'), 'client': t('kind.client'), 'document': t('kind.document'), 'recipe': t('kind.recipe'), 'folder': t('kind.folder'), 'note': t('kind.note'), 'file': t('kind.file'), 'archive': t('kind.archive'), 'case': t('kind.case'), 'link': t('kind.link') } return labels[kind] || kind || t('kind.case') } function captureKindLabel(kind) { @@ -1409,6 +1422,9 @@ function addInboxCapture(item) { if (!item || !item.id) return inboxNodes = [item, ...inboxNodes.filter(existing => existing.id !== item.id)] + if (selectedNode && (item.captureContextNodeId === selectedNode.id || item.suggestedTargetNodeId === selectedNode.id)) { + localInboxNodes = [item, ...localInboxNodes.filter(existing => existing.id !== item.id)] + } } function looksLikeURL(value) { try { @@ -1689,14 +1705,31 @@ async function submitAssignInbox() { if (!assignInboxItem || !inboxAssignTarget) return try { - await wailsCall('AssignInboxNode', assignInboxItem.id, inboxAssignTarget.id) - inboxNodes = inboxNodes.filter(item => item.id !== assignInboxItem.id) - await reloadTreePreservingExpanded() + await resolveInboxToTarget(assignInboxItem, inboxAssignTarget.id) closeAssignInbox() } catch (e) { error = String(e) } } + async function resolveInboxToTarget(item, targetNodeId) { + if (!item || !targetNodeId) return + await wailsCall('ResolveInboxNode', item.id, targetNodeId) + inboxNodes = inboxNodes.filter(existing => existing.id !== item.id) + localInboxNodes = localInboxNodes.filter(existing => existing.id !== item.id) + await reloadTreePreservingExpanded() + if (selectedNode) { + await loadTabData(selectedNode.id) + } + } + async function resolveInboxHere(item) { + const targetId = item?.suggestedTargetNodeId || selectedNode?.id || '' + if (!targetId) return + try { + await resolveInboxToTarget(item, targetId) + } catch (e) { + error = String(e) + } + } function confirmDeleteInbox(item) { openConfirm({ title: t('inbox.deleteTitle'), @@ -1707,12 +1740,68 @@ try { await wailsCall('DeleteInboxNode', item.id) inboxNodes = inboxNodes.filter(existing => existing.id !== item.id) + localInboxNodes = localInboxNodes.filter(existing => existing.id !== item.id) } catch (e) { error = String(e) } } }) } + function openEditLink(link) { + editingLink = link + linkTitle = link.title || '' + linkURL = link.url || '' + linkNote = link.note || '' + linkStatus = '' + } + function closeEditLink() { + editingLink = null + linkTitle = '' + linkURL = '' + linkNote = '' + linkStatus = '' + } + async function submitEditLink() { + if (!editingLink || !linkURL.trim()) return + try { + const updated = await wailsCall('UpdateLink', editingLink.id, linkTitle.trim(), linkURL.trim(), linkNote) + links = links.map(link => link.id === updated.id ? updated : link) + closeEditLink() + } catch (e) { + linkStatus = String(e) + } + } + async function deleteLink(link) { + openConfirm({ + title: t('links.deleteTitle'), + message: t('links.deleteConfirm', { title: link.title }), + confirmText: t('common.delete'), + danger: true, + onConfirm: async () => { + try { + await wailsCall('DeleteLink', link.id) + links = links.filter(item => item.id !== link.id) + } catch (e) { + error = String(e) + } + } + }) + } + async function openLink(link) { + try { + await wailsCall('OpenLink', link.id) + } catch (e) { + error = String(e) + } + } + async function copyLinkURL(link) { + try { + await navigator.clipboard?.writeText?.(link.url) + linkStatus = t('links.copied') + } catch (e) { + linkStatus = t('links.copyUnavailable') + } + } function pluralize(n, one, few, many) { n = Math.abs(n) % 100 if (n >= 5 && n <= 20) return many @@ -2188,6 +2277,63 @@ /> {/if} + {:else if activeTab === 'inbox'} +
+ {#if localInboxNodes.length === 0} +

{t('inbox.localEmpty')}

+ {:else} +
+ {#each localInboxNodes as item} +
openNodeById(item.id)} on:keydown={(e) => e.key === 'Enter' && openNodeById(item.id)}> +
+ {item.title} + + {#if item.captureKind}{captureKindLabel(item.captureKind)} · {/if} + {#if item.captureSource}{captureSourceLabel(item.captureSource)} · {/if} + {#if item.captureContextLabel}{t('inbox.capturedIn')}: {item.captureContextLabel} · {/if} + {formatDate(item.capturedAt || item.createdAt)} + +
+
+ + + +
+
+ {/each} +
+ {/if} +
+ + {:else if activeTab === 'links'} + + {:else if activeTab === 'actions'}
@@ -2380,6 +2526,9 @@
+ {#if item.suggestedTargetNodeId} + + {/if} @@ -3032,6 +3181,36 @@
{/if} + {#if editingLink} + + {/if} + {#if showConfirm}