fix: restrict inbox to captured artifacts

This commit is contained in:
mirivlad 2026-06-05 01:35:27 +08:00
parent 58a74acbf6
commit 2e86229350
9 changed files with 34 additions and 30 deletions

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-4y6wyoK9.js"></script> <script type="module" crossorigin src="/assets/main-O6a-DCWF.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

@ -2,30 +2,30 @@ package main
import "testing" import "testing"
func TestListInboxNodesReturnsOnlyUnassignedRoots(t *testing.T) { func TestListInboxNodesReturnsOnlyCapturedArtifacts(t *testing.T) {
app, _ := setupTestApp(t) app, _ := setupTestApp(t)
unassigned, err := app.CreateNodeFromTemplate("", "Unassigned Root", "folder.default") manual, err := app.CreateNodeFromTemplate("", "Manual Root", "folder.default")
if err != nil { if err != nil {
t.Fatalf("create unassigned root: %v", err) t.Fatalf("create manual root: %v", err)
} }
inbox, err := app.CreateNodeFromTemplate("", "Inbox Root", "folder.default") legacyInbox, err := app.CreateNodeFromTemplate("", "Legacy Inbox Root", "folder.default")
if err != nil { if err != nil {
t.Fatalf("create inbox root: %v", err) t.Fatalf("create legacy inbox root: %v", err)
} }
assigned, err := app.CreateNodeFromTemplate("", "Assigned Root", "folder.default") captured, err := app.CreateNodeFromTemplate("", "Captured Artifact", "folder.default")
if err != nil { if err != nil {
t.Fatalf("create assigned root: %v", err) t.Fatalf("create captured artifact: %v", err)
} }
child, err := app.CreateNodeFromTemplate(unassigned.ID, "Nested Child", "folder.default") child, err := app.CreateNodeFromTemplate(captured.ID, "Nested Child", "folder.default")
if err != nil { if err != nil {
t.Fatalf("create child: %v", err) t.Fatalf("create child: %v", err)
} }
if _, err := app.db.Exec(`UPDATE nodes SET section = 'inbox' WHERE id = ?`, inbox.ID); err != nil { if _, err := app.db.Exec(`UPDATE nodes SET section = 'inbox' WHERE id = ?`, legacyInbox.ID); err != nil {
t.Fatalf("mark inbox: %v", err) t.Fatalf("mark legacy inbox: %v", err)
} }
if _, err := app.db.Exec(`UPDATE nodes SET section = 'projects' WHERE id = ?`, assigned.ID); err != nil { if err := app.nodes.MetaSet(captured.ID, "capture.inbox", "true"); err != nil {
t.Fatalf("mark assigned: %v", err) t.Fatalf("mark captured: %v", err)
} }
list, err := app.ListInboxNodes() list, err := app.ListInboxNodes()
@ -37,14 +37,14 @@ func TestListInboxNodesReturnsOnlyUnassignedRoots(t *testing.T) {
for _, item := range list { for _, item := range list {
got[item.ID] = true got[item.ID] = true
} }
if !got[unassigned.ID] { if !got[captured.ID] {
t.Fatal("unassigned root missing from inbox") t.Fatal("captured artifact missing from inbox")
} }
if !got[inbox.ID] { if got[manual.ID] {
t.Fatal("section=inbox root missing from inbox") t.Fatal("manual root should not be in inbox")
} }
if got[assigned.ID] { if got[legacyInbox.ID] {
t.Fatal("assigned root should not be in inbox") t.Fatal("section=inbox root without capture metadata should not be in inbox")
} }
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")

View File

@ -2004,7 +2004,6 @@
<h2>{t('nav.inbox')}</h2> <h2>{t('nav.inbox')}</h2>
<p>{t('inbox.subtitle')}</p> <p>{t('inbox.subtitle')}</p>
</div> </div>
<button class="btn btn-primary btn-sm" on:click={openCreateRoot}>+ {t('nav.createNode')}</button>
</div> </div>
{#if inboxNodes.length === 0} {#if inboxNodes.length === 0}
<div class="empty-state"> <div class="empty-state">

View File

@ -24,7 +24,7 @@ export default {
'nav.createInside': 'Create inside', 'nav.createInside': 'Create inside',
'nav.createNode': 'Create element', 'nav.createNode': 'Create element',
'nav.moveToRoot': 'Move to root', 'nav.moveToRoot': 'Move to root',
'inbox.subtitle': 'Root items without an assigned section', 'inbox.subtitle': 'Captured materials that still need to be assigned to cases',
'inbox.empty': 'No unprocessed items', 'inbox.empty': 'No unprocessed items',
'trash.openFolder': 'Open trash folder', 'trash.openFolder': 'Open trash folder',
'trash.empty': 'Trash is empty', 'trash.empty': 'Trash is empty',

View File

@ -25,7 +25,7 @@ export default {
'nav.createNode': 'Создать элемент', 'nav.createNode': 'Создать элемент',
'nav.moveToRoot': 'Переместить в корень', 'nav.moveToRoot': 'Переместить в корень',
'inbox.subtitle': 'Корневые элементы без назначенного раздела', 'inbox.subtitle': 'Захваченные материалы, которые нужно разложить по делам',
'inbox.empty': 'Неразобранных элементов нет', 'inbox.empty': 'Неразобранных элементов нет',
'trash.openFolder': 'Открыть папку корзины', 'trash.openFolder': 'Открыть папку корзины',

View File

@ -169,10 +169,13 @@ func (r *Repository) ListRoots(includeDeleted bool) ([]Node, error) {
return scanNodes(rows) return scanNodes(rows)
} }
// ListInboxRoots returns active root nodes that are not assigned to a specific section. // ListInboxRoots returns active root capture artifacts explicitly marked for inbox.
func (r *Repository) ListInboxRoots(includeDeleted bool) ([]Node, error) { func (r *Repository) ListInboxRoots(includeDeleted bool) ([]Node, error) {
q := `SELECT ` + nodeColumns + ` FROM nodes q := `SELECT ` + nodeColumns + ` FROM nodes
WHERE parent_id IS NULL AND COALESCE(section, '') IN ('', 'inbox')` WHERE parent_id IS NULL
AND id IN (
SELECT node_id FROM node_meta WHERE key = 'capture.inbox' AND value = 'true'
)`
if !includeDeleted { if !includeDeleted {
q += " AND deleted_at IS NULL" q += " AND deleted_at IS NULL"
} }

View File

@ -135,7 +135,8 @@ async function runReadyScenario(cdp, url) {
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: unassigned item visible') await assertText(cdp, 'Inbox Smoke Item', 'inbox: captured item visible')
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', 'Открыть')
await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list') await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list')
@ -577,7 +578,8 @@ 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: '', 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-manual-root', title: 'Manual Root Item', type: 'folder', section: '', createdAt: now, has_children: false, children: [] },
], ],
notes: { notes: {
'node-project': [{ id: 'note-1', title: 'Smoke note', createdAt: now }], 'node-project': [{ id: 'note-1', title: 'Smoke note', createdAt: now }],
@ -685,7 +687,7 @@ function wailsMockSource() {
], ],
ListWorkspaceTree: async () => clone(state.nodes), ListWorkspaceTree: async () => clone(state.nodes),
ListWorkspaceChildren: async (id) => clone(childrenOf(id)), ListWorkspaceChildren: async (id) => clone(childrenOf(id)),
ListInboxNodes: async () => clone(state.nodes.filter((node) => !node.parent_id && (!node.section || node.section === 'inbox'))), ListInboxNodes: async () => clone(state.nodes.filter((node) => !node.parent_id && node.captureInbox === 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 }],