feat: model inbox capture artifacts

This commit is contained in:
mirivlad 2026-06-05 01:40:08 +08:00
parent 2e86229350
commit d6ef3a973a
10 changed files with 108 additions and 17 deletions

View File

@ -1,6 +1,12 @@
package main package main
func (a *App) ListInboxNodes() ([]NodeDTO, error) { type InboxNodeDTO struct {
NodeDTO
CaptureKind string `json:"captureKind"`
CaptureSource string `json:"captureSource"`
}
func (a *App) ListInboxNodes() ([]InboxNodeDTO, error) {
if err := a.requireVault(); err != nil { if err := a.requireVault(); err != nil {
return nil, err return nil, err
} }
@ -8,7 +14,17 @@ func (a *App) ListInboxNodes() ([]NodeDTO, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
dtos := toNodeDTOs(list) dtos := make([]InboxNodeDTO, 0, len(list))
for _, n := range list {
dto := InboxNodeDTO{NodeDTO: toNodeDTO(&n)}
if kind, ok, err := a.nodes.MetaGet(n.ID, "capture.kind"); err == nil && ok {
dto.CaptureKind = kind
}
if source, ok, err := a.nodes.MetaGet(n.ID, "capture.source"); err == nil && ok {
dto.CaptureSource = source
}
dtos = append(dtos, dto)
}
for i := range dtos { for i := range dtos {
n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe") n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe")
if err != nil { if err != nil {
@ -18,3 +34,18 @@ func (a *App) ListInboxNodes() ([]NodeDTO, error) {
} }
return dtos, nil return dtos, nil
} }
func (a *App) filterInboxCaptureNodes(list []NodeDTO) []NodeDTO {
out := make([]NodeDTO, 0, len(list))
for _, item := range list {
if !a.isInboxCaptureNode(item.ID) {
out = append(out, item)
}
}
return out
}
func (a *App) isInboxCaptureNode(nodeID string) bool {
v, ok, err := a.nodes.MetaGet(nodeID, "capture.inbox")
return err == nil && ok && v == "true"
}

View File

@ -22,7 +22,7 @@ func (a *App) ListWorkspaceTree() ([]NodeDTO, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
dtos := filterContainers(toNodeDTOs(list)) dtos := filterContainers(a.filterInboxCaptureNodes(toNodeDTOs(list)))
for i := range dtos { for i := range dtos {
n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe") n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe")
if err != nil { if err != nil {
@ -41,7 +41,7 @@ func (a *App) ListWorkspaceChildren(parentID string) ([]NodeDTO, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
dtos := filterContainers(toNodeDTOs(list)) dtos := filterContainers(a.filterInboxCaptureNodes(toNodeDTOs(list)))
for i := range dtos { for i := range dtos {
n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe") n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe")
if err != nil { if err != nil {
@ -778,11 +778,11 @@ func (a *App) moveFolderNode(nodeID string, node *nodes.Node, parent *nodes.Node
func (a *App) moveNoteFileNode(nodeID string, node *nodes.Node, parent *nodes.Node, newParentID, nodeTitle string, titleChanged bool) error { func (a *App) moveNoteFileNode(nodeID string, node *nodes.Node, parent *nodes.Node, newParentID, nodeTitle string, titleChanged bool) error {
// Collect file records before any mutations. // Collect file records before any mutations.
type fileMove struct { type fileMove struct {
id string id string
oldPath string oldPath string
oldAbs string oldAbs string
newRelPath string newRelPath string
newAbs string newAbs string
} }
var fileMoves []fileMove var fileMoves []fileMove
frows, ferr := a.db.Query(`SELECT id, path FROM files WHERE node_id=?`, nodeID) frows, ferr := a.db.Query(`SELECT id, path FROM files WHERE node_id=?`, nodeID)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@
background: #13131f; background: #13131f;
} }
</style> </style>
<script type="module" crossorigin src="/assets/main-O6a-DCWF.js"></script> <script type="module" crossorigin src="/assets/main-meJOE1Ze.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-es_E5H-H.css"> <link rel="stylesheet" crossorigin href="/assets/main-es_E5H-H.css">
</head> </head>
<body> <body>

View File

@ -27,6 +27,12 @@ func TestListInboxNodesReturnsOnlyCapturedArtifacts(t *testing.T) {
if err := app.nodes.MetaSet(captured.ID, "capture.inbox", "true"); err != nil { if err := app.nodes.MetaSet(captured.ID, "capture.inbox", "true"); err != nil {
t.Fatalf("mark captured: %v", err) t.Fatalf("mark captured: %v", err)
} }
if err := app.nodes.MetaSet(captured.ID, "capture.kind", "text"); err != nil {
t.Fatalf("mark capture kind: %v", err)
}
if err := app.nodes.MetaSet(captured.ID, "capture.source", "clipboard"); err != nil {
t.Fatalf("mark capture source: %v", err)
}
list, err := app.ListInboxNodes() list, err := app.ListInboxNodes()
if err != nil { if err != nil {
@ -40,6 +46,16 @@ func TestListInboxNodesReturnsOnlyCapturedArtifacts(t *testing.T) {
if !got[captured.ID] { if !got[captured.ID] {
t.Fatal("captured artifact missing from inbox") t.Fatal("captured artifact missing from inbox")
} }
for _, item := range list {
if item.ID == captured.ID {
if item.CaptureKind != "text" {
t.Fatalf("CaptureKind = %q, want text", item.CaptureKind)
}
if item.CaptureSource != "clipboard" {
t.Fatalf("CaptureSource = %q, want clipboard", item.CaptureSource)
}
}
}
if got[manual.ID] { if got[manual.ID] {
t.Fatal("manual root should not be in inbox") t.Fatal("manual root should not be in inbox")
} }
@ -49,4 +65,14 @@ func TestListInboxNodesReturnsOnlyCapturedArtifacts(t *testing.T) {
if got[child.ID] { if got[child.ID] {
t.Fatal("nested child should not be in inbox") t.Fatal("nested child should not be in inbox")
} }
workspace, err := app.ListWorkspaceTree()
if err != nil {
t.Fatalf("ListWorkspaceTree: %v", err)
}
for _, item := range workspace {
if item.ID == captured.ID {
t.Fatal("captured artifact should not be shown in workspace tree")
}
}
} }

View File

@ -1374,6 +1374,14 @@
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') }
return labels[kind] || kind || t('kind.case') return labels[kind] || kind || t('kind.case')
} }
function captureKindLabel(kind) {
if (!kind) return ''
return t('capture.kind.' + kind)
}
function captureSourceLabel(source) {
if (!source) return ''
return t('capture.source.' + source)
}
function pluralize(n, one, few, many) { function pluralize(n, one, few, many) {
n = Math.abs(n) % 100 n = Math.abs(n) % 100
if (n >= 5 && n <= 20) return many if (n >= 5 && n <= 20) return many
@ -2015,7 +2023,11 @@
<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={() => openNodeById(item.id)} on:keydown={(e) => e.key === 'Enter' && openNodeById(item.id)}>
<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">{nodeKindLabel(item.type)} · {formatDate(item.createdAt)}</span> <span class="inbox-item-meta">
{#if item.captureKind}{captureKindLabel(item.captureKind)} · {/if}
{#if item.captureSource}{captureSourceLabel(item.captureSource)} · {/if}
{formatDate(item.createdAt)}
</span>
</div> </div>
<div class="inbox-item-actions"> <div class="inbox-item-actions">
<button class="btn btn-sm" on:click|stopPropagation={() => openNodeById(item.id)}>{t('common.open')}</button> <button class="btn btn-sm" on:click|stopPropagation={() => openNodeById(item.id)}>{t('common.open')}</button>

View File

@ -26,6 +26,15 @@ export default {
'nav.moveToRoot': 'Move to root', 'nav.moveToRoot': 'Move to root',
'inbox.subtitle': 'Captured materials that still need to be assigned to cases', 'inbox.subtitle': 'Captured materials that still need to be assigned to cases',
'inbox.empty': 'No unprocessed items', 'inbox.empty': 'No unprocessed items',
'capture.kind.text': 'Text',
'capture.kind.url': 'Link',
'capture.kind.file': 'File',
'capture.kind.folder': 'Folder',
'capture.kind.image': 'Image',
'capture.source.clipboard': 'Clipboard',
'capture.source.drop': 'Drop',
'capture.source.browser': 'Browser',
'capture.source.manual': 'Manual',
'trash.openFolder': 'Open trash folder', 'trash.openFolder': 'Open trash folder',
'trash.empty': 'Trash is empty', 'trash.empty': 'Trash is empty',
'trash.deletedNodes': 'Deleted items', 'trash.deletedNodes': 'Deleted items',

View File

@ -28,6 +28,16 @@ export default {
'inbox.subtitle': 'Захваченные материалы, которые нужно разложить по делам', 'inbox.subtitle': 'Захваченные материалы, которые нужно разложить по делам',
'inbox.empty': 'Неразобранных элементов нет', 'inbox.empty': 'Неразобранных элементов нет',
'capture.kind.text': 'Текст',
'capture.kind.url': 'Ссылка',
'capture.kind.file': 'Файл',
'capture.kind.folder': 'Папка',
'capture.kind.image': 'Изображение',
'capture.source.clipboard': 'Буфер обмена',
'capture.source.drop': 'Перетаскивание',
'capture.source.browser': 'Браузер',
'capture.source.manual': 'Вручную',
'trash.openFolder': 'Открыть папку корзины', 'trash.openFolder': 'Открыть папку корзины',
'trash.empty': 'Корзина пуста', 'trash.empty': 'Корзина пуста',
'trash.deletedNodes': 'Удаленные элементы', 'trash.deletedNodes': 'Удаленные элементы',

View File

@ -132,10 +132,13 @@ async function runReadyScenario(cdp, url) {
await screenshot(cdp, 'settings.png') await screenshot(cdp, 'settings.png')
await click(cdp, '.close-btn') await click(cdp, '.close-btn')
await waitForGone(cdp, '.settings-window') await waitForGone(cdp, '.settings-window')
await assertEval(cdp, `![...document.querySelectorAll('.tree-label')].some((el) => el.innerText.includes('Inbox Smoke Item'))`, 'workspace: captured item hidden from tree')
await clickText(cdp, '.nav-item', 'Неразобранное') await clickText(cdp, '.nav-item', 'Неразобранное')
await assertText(cdp, 'Неразобранное', 'inbox: system view opens') await assertText(cdp, 'Неразобранное', 'inbox: system view opens')
await assertText(cdp, 'Inbox Smoke Item', 'inbox: captured item visible') await assertText(cdp, 'Inbox Smoke Item', 'inbox: captured item visible')
await assertText(cdp, 'Текст', 'inbox: capture kind visible')
await assertText(cdp, 'Буфер обмена', 'inbox: capture source visible')
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('Manual Root Item')`, 'inbox: manual root is hidden') await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('Manual Root Item')`, 'inbox: manual root is hidden')
await screenshot(cdp, 'inbox.png') await screenshot(cdp, 'inbox.png')
await clickText(cdp, '.inbox-item-actions .btn', 'Открыть') await clickText(cdp, '.inbox-item-actions .btn', 'Открыть')
@ -578,7 +581,7 @@ function wailsMockSource() {
], ],
}, },
{ id: 'node-client', title: 'Smoke Client', type: 'client', section: 'clients', createdAt: now, has_children: false, children: [] }, { id: 'node-client', title: 'Smoke Client', type: 'client', section: 'clients', createdAt: now, has_children: false, children: [] },
{ id: 'node-inbox', title: 'Inbox Smoke Item', type: 'folder', section: '', captureInbox: true, createdAt: now, has_children: false, children: [] }, { id: 'node-inbox', title: 'Inbox Smoke Item', type: 'folder', section: '', captureInbox: true, captureKind: 'text', captureSource: 'clipboard', createdAt: now, has_children: false, children: [] },
{ id: 'node-manual-root', title: 'Manual Root Item', type: 'folder', section: '', createdAt: now, has_children: false, children: [] }, { id: 'node-manual-root', title: 'Manual Root Item', type: 'folder', section: '', createdAt: now, has_children: false, children: [] },
], ],
notes: { notes: {
@ -685,9 +688,9 @@ function wailsMockSource() {
{ id: 'activity', label: 'Активность' }, { id: 'activity', label: 'Активность' },
{ id: 'journal', label: 'Журнал' }, { id: 'journal', label: 'Журнал' },
], ],
ListWorkspaceTree: async () => clone(state.nodes), ListWorkspaceTree: async () => clone(state.nodes.filter((node) => node.captureInbox !== true)),
ListWorkspaceChildren: async (id) => clone(childrenOf(id)), ListWorkspaceChildren: async (id) => clone(childrenOf(id)),
ListInboxNodes: async () => clone(state.nodes.filter((node) => !node.parent_id && node.captureInbox === true)), ListInboxNodes: async () => clone(state.nodes.filter((node) => !node.parent_id && node.captureInbox === true).map((node) => ({ ...node, captureKind: node.captureKind || '', captureSource: node.captureSource || '' }))),
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 }],