feat: assign and delete inbox artifacts
This commit is contained in:
parent
a96a316883
commit
6eaa4cda49
|
|
@ -1,5 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
type InboxNodeDTO struct {
|
type InboxNodeDTO struct {
|
||||||
NodeDTO
|
NodeDTO
|
||||||
CaptureKind string `json:"captureKind"`
|
CaptureKind string `json:"captureKind"`
|
||||||
|
|
@ -32,6 +34,42 @@ func (a *App) ListInboxNodes() ([]InboxNodeDTO, error) {
|
||||||
return dtos, nil
|
return dtos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) AssignInboxNode(nodeID, targetParentID string) (*NodeDTO, error) {
|
||||||
|
if err := a.requireVault(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !a.isInboxCaptureNode(nodeID) {
|
||||||
|
return nil, fmt.Errorf("node is not an inbox artifact")
|
||||||
|
}
|
||||||
|
if targetParentID == "" {
|
||||||
|
return nil, fmt.Errorf("target parent is required")
|
||||||
|
}
|
||||||
|
if err := a.MoveNode(nodeID, targetParentID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := a.clearCaptureMeta(nodeID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dto, err := a.GetNodeDetail(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) DeleteInboxNode(nodeID string) error {
|
||||||
|
if err := a.requireVault(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !a.isInboxCaptureNode(nodeID) {
|
||||||
|
return fmt.Errorf("node is not an inbox artifact")
|
||||||
|
}
|
||||||
|
if err := a.DeleteNode(nodeID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return a.clearCaptureMeta(nodeID)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) filterInboxCaptureNodes(list []NodeDTO) []NodeDTO {
|
func (a *App) filterInboxCaptureNodes(list []NodeDTO) []NodeDTO {
|
||||||
out := make([]NodeDTO, 0, len(list))
|
out := make([]NodeDTO, 0, len(list))
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
|
|
@ -46,3 +84,8 @@ func (a *App) isInboxCaptureNode(nodeID string) bool {
|
||||||
v, ok, err := a.nodes.MetaGet(nodeID, "capture.inbox")
|
v, ok, err := a.nodes.MetaGet(nodeID, "capture.inbox")
|
||||||
return err == nil && ok && v == "true"
|
return err == nil && ok && v == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) clearCaptureMeta(nodeID string) error {
|
||||||
|
_, err := a.db.Exec(`DELETE FROM node_meta WHERE node_id = ? AND key LIKE 'capture.%'`, nodeID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -16,8 +16,8 @@
|
||||||
background: #13131f;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-D-CBbIRx.js"></script>
|
<script type="module" crossorigin src="/assets/main-B7pwitR5.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-BgORPqqS.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-CnGHmBj4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"verstak/internal/core/nodes"
|
||||||
|
)
|
||||||
|
|
||||||
func TestListInboxNodesReturnsOnlyCapturedArtifacts(t *testing.T) {
|
func TestListInboxNodesReturnsOnlyCapturedArtifacts(t *testing.T) {
|
||||||
app, _ := setupTestApp(t)
|
app, _ := setupTestApp(t)
|
||||||
|
|
@ -76,3 +81,72 @@ func TestListInboxNodesReturnsOnlyCapturedArtifacts(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAssignInboxNodeMovesArtifactIntoCase(t *testing.T) {
|
||||||
|
app, _ := setupTestApp(t)
|
||||||
|
|
||||||
|
parent, err := app.CreateNodeFromTemplate("", "Target Case", "folder.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create parent: %v", err)
|
||||||
|
}
|
||||||
|
captured, err := app.CaptureText("Captured task material")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CaptureText: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
moved, err := app.AssignInboxNode(captured.ID, parent.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssignInboxNode: %v", err)
|
||||||
|
}
|
||||||
|
if moved.ParentID == nil || *moved.ParentID != parent.ID {
|
||||||
|
t.Fatalf("ParentID = %v, want %q", moved.ParentID, parent.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
inbox, err := app.ListInboxNodes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListInboxNodes: %v", err)
|
||||||
|
}
|
||||||
|
for _, item := range inbox {
|
||||||
|
if item.ID == captured.ID {
|
||||||
|
t.Fatal("assigned artifact should leave inbox")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notes, err := app.ListNotes(parent.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListNotes: %v", err)
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for _, note := range notes {
|
||||||
|
if note.ID == captured.ID {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("assigned text artifact missing from target case notes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteInboxNodeRemovesArtifactFromInbox(t *testing.T) {
|
||||||
|
app, _ := setupTestApp(t)
|
||||||
|
|
||||||
|
captured, err := app.CaptureText("Delete this captured material")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CaptureText: %v", err)
|
||||||
|
}
|
||||||
|
if err := app.DeleteInboxNode(captured.ID); err != nil {
|
||||||
|
t.Fatalf("DeleteInboxNode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inbox, err := app.ListInboxNodes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListInboxNodes: %v", err)
|
||||||
|
}
|
||||||
|
for _, item := range inbox {
|
||||||
|
if item.ID == captured.ID {
|
||||||
|
t.Fatal("deleted artifact should leave inbox")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := app.nodes.GetActive(captured.ID); !errors.Is(err, nodes.ErrNotFound) {
|
||||||
|
t.Fatalf("GetActive err = %v, want ErrNotFound", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,12 @@
|
||||||
let renameId = ''
|
let renameId = ''
|
||||||
let renameValue = ''
|
let renameValue = ''
|
||||||
let renameError = ''
|
let renameError = ''
|
||||||
|
let assignInboxItem = null
|
||||||
|
let inboxAssignQuery = ''
|
||||||
|
let inboxAssignResults = []
|
||||||
|
let inboxAssignTarget = null
|
||||||
|
let inboxAssignSearching = false
|
||||||
|
let inboxAssignTimer
|
||||||
|
|
||||||
// ===== Sync state =====
|
// ===== Sync state =====
|
||||||
let syncStatus = null
|
let syncStatus = null
|
||||||
|
|
@ -1504,6 +1510,76 @@
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
if (paths.length > 0) await captureDroppedPaths(paths)
|
if (paths.length > 0) await captureDroppedPaths(paths)
|
||||||
}
|
}
|
||||||
|
function openAssignInbox(item) {
|
||||||
|
assignInboxItem = item
|
||||||
|
inboxAssignQuery = ''
|
||||||
|
inboxAssignResults = []
|
||||||
|
inboxAssignTarget = null
|
||||||
|
}
|
||||||
|
function closeAssignInbox() {
|
||||||
|
assignInboxItem = null
|
||||||
|
inboxAssignQuery = ''
|
||||||
|
inboxAssignResults = []
|
||||||
|
inboxAssignTarget = null
|
||||||
|
inboxAssignSearching = false
|
||||||
|
}
|
||||||
|
function isAssignTarget(result) {
|
||||||
|
return ['case', 'client', 'project', 'folder', 'document', 'recipe'].includes(result.type)
|
||||||
|
}
|
||||||
|
async function searchInboxAssignTargets() {
|
||||||
|
const q = inboxAssignQuery.trim()
|
||||||
|
if (!q || q.length < 2) {
|
||||||
|
inboxAssignResults = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inboxAssignSearching = true
|
||||||
|
try {
|
||||||
|
const results = await wailsCall('SearchNodes', q) || []
|
||||||
|
inboxAssignResults = results.filter(result => isAssignTarget(result) && result.id !== assignInboxItem?.id)
|
||||||
|
} catch (e) {
|
||||||
|
inboxAssignResults = []
|
||||||
|
} finally {
|
||||||
|
inboxAssignSearching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onInboxAssignSearchInput(e) {
|
||||||
|
inboxAssignQuery = e.target.value
|
||||||
|
inboxAssignTarget = null
|
||||||
|
clearTimeout(inboxAssignTimer)
|
||||||
|
inboxAssignTimer = setTimeout(searchInboxAssignTargets, 200)
|
||||||
|
}
|
||||||
|
function selectInboxAssignTarget(result) {
|
||||||
|
inboxAssignTarget = result
|
||||||
|
inboxAssignQuery = result.path || result.title
|
||||||
|
inboxAssignResults = []
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
closeAssignInbox()
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function confirmDeleteInbox(item) {
|
||||||
|
openConfirm({
|
||||||
|
title: t('inbox.deleteTitle'),
|
||||||
|
message: t('inbox.deleteConfirm', { title: item.title }),
|
||||||
|
confirmText: t('common.delete'),
|
||||||
|
danger: true,
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await wailsCall('DeleteInboxNode', item.id)
|
||||||
|
inboxNodes = inboxNodes.filter(existing => existing.id !== item.id)
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
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
|
||||||
|
|
@ -2166,8 +2242,10 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="inbox-item-actions">
|
<div class="inbox-item-actions">
|
||||||
|
<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={() => openNodeById(item.id)}>{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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -2783,6 +2861,39 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if assignInboxItem}
|
||||||
|
<div class="modal-overlay" role="button" tabindex="0" on:click|self={closeAssignInbox} on:keydown={onKeyActivate(closeAssignInbox)}>
|
||||||
|
<div class="modal">
|
||||||
|
<h3>{t('inbox.assignTitle')}</h3>
|
||||||
|
<div class="create-context">{assignInboxItem.title}</div>
|
||||||
|
<div class="form-group assign-search">
|
||||||
|
<label><span class="label-text">{t('inbox.assignTarget')}</span>
|
||||||
|
<input type="text" placeholder={t('inbox.assignSearchPlaceholder')} bind:value={inboxAssignQuery}
|
||||||
|
on:input={onInboxAssignSearchInput}
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && inboxAssignTarget && submitAssignInbox()} />
|
||||||
|
</label>
|
||||||
|
{#if inboxAssignResults.length > 0}
|
||||||
|
<div class="assign-search-results">
|
||||||
|
{#each inboxAssignResults as result}
|
||||||
|
<button class="assign-search-result" on:click={() => selectInboxAssignTarget(result)}>
|
||||||
|
<span>{result.path || result.title}</span>
|
||||||
|
<span>{nodeKindLabel(result.type)}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if inboxAssignSearching}
|
||||||
|
<div class="assign-status">{t('common.loading')}</div>
|
||||||
|
{/if}
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-primary" on:click={submitAssignInbox} disabled={!inboxAssignTarget}>{t('inbox.assign')}</button>
|
||||||
|
<button class="btn" on:click={closeAssignInbox}>{t('common.cancel')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showConfirm}
|
{#if showConfirm}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
title={confirmTitle}
|
title={confirmTitle}
|
||||||
|
|
@ -3058,6 +3169,12 @@
|
||||||
.form-group select { appearance: none; -webkit-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M2 4l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 32px; }
|
.form-group select { appearance: none; -webkit-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M2 4l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 32px; }
|
||||||
.form-group input:focus, .form-group select:focus { outline: none; border-color: #6366f1; }
|
.form-group input:focus, .form-group select:focus { outline: none; border-color: #6366f1; }
|
||||||
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||||
|
.assign-search { position: relative; }
|
||||||
|
.assign-search-results { position: absolute; left: 0; right: 0; top: 100%; z-index: 110; margin-top: 4px; background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 6px; max-height: 220px; overflow-y: auto; }
|
||||||
|
.assign-search-result { width: 100%; display: flex; justify-content: space-between; gap: 12px; padding: 8px 10px; border: 0; border-bottom: 1px solid #24243a; background: transparent; color: #e4e4ef; text-align: left; cursor: pointer; font-family: inherit; }
|
||||||
|
.assign-search-result:hover { background: #222238; }
|
||||||
|
.assign-search-result span:last-child { color: #8888a0; font-size: 12px; flex-shrink: 0; }
|
||||||
|
.assign-status { color: #8888a0; font-size: 12px; }
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; }
|
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; }
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ export default {
|
||||||
'inbox.captured': 'Added',
|
'inbox.captured': 'Added',
|
||||||
'inbox.clipboardEmpty': 'Clipboard is empty',
|
'inbox.clipboardEmpty': 'Clipboard is empty',
|
||||||
'inbox.clipboardUnavailable': 'Clipboard is unavailable',
|
'inbox.clipboardUnavailable': 'Clipboard is unavailable',
|
||||||
|
'inbox.assign': 'Assign',
|
||||||
|
'inbox.assignTitle': 'Assign material',
|
||||||
|
'inbox.assignTarget': 'Case',
|
||||||
|
'inbox.assignSearchPlaceholder': 'Find case',
|
||||||
|
'inbox.deleteTitle': 'Delete material',
|
||||||
|
'inbox.deleteConfirm': 'Delete "{title}" from inbox?',
|
||||||
'capture.kind.text': 'Text',
|
'capture.kind.text': 'Text',
|
||||||
'capture.kind.url': 'Link',
|
'capture.kind.url': 'Link',
|
||||||
'capture.kind.file': 'File',
|
'capture.kind.file': 'File',
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ export default {
|
||||||
'inbox.captured': 'Добавлено',
|
'inbox.captured': 'Добавлено',
|
||||||
'inbox.clipboardEmpty': 'Буфер обмена пуст',
|
'inbox.clipboardEmpty': 'Буфер обмена пуст',
|
||||||
'inbox.clipboardUnavailable': 'Буфер обмена недоступен',
|
'inbox.clipboardUnavailable': 'Буфер обмена недоступен',
|
||||||
|
'inbox.assign': 'Разложить',
|
||||||
|
'inbox.assignTitle': 'Разложить материал',
|
||||||
|
'inbox.assignTarget': 'Дело',
|
||||||
|
'inbox.assignSearchPlaceholder': 'Найти дело',
|
||||||
|
'inbox.deleteTitle': 'Удалить материал',
|
||||||
|
'inbox.deleteConfirm': 'Удалить «{title}» из неразобранного?',
|
||||||
|
|
||||||
'capture.kind.text': 'Текст',
|
'capture.kind.text': 'Текст',
|
||||||
'capture.kind.url': 'Ссылка',
|
'capture.kind.url': 'Ссылка',
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,14 @@ export function CaptureFileData(arg1, arg2) {
|
||||||
return window['go']['main']['App']['CaptureFileData'](arg1, arg2);
|
return window['go']['main']['App']['CaptureFileData'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AssignInboxNode(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['AssignInboxNode'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteInboxNode(arg1) {
|
||||||
|
return window['go']['main']['App']['DeleteInboxNode'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function ListTrash() {
|
export function ListTrash() {
|
||||||
return window['go']['main']['App']['ListTrash']();
|
return window['go']['main']['App']['ListTrash']();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,18 @@ async function runReadyScenario(cdp, url) {
|
||||||
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
|
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
|
||||||
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')
|
||||||
|
await clickInboxItemButton(cdp, 'smoke-drop-folder', 'Разложить')
|
||||||
|
await waitForSelector(cdp, '.modal input[type="text"]')
|
||||||
|
await setInputValue(cdp, '.modal input[type="text"]', 'Smoke')
|
||||||
|
await waitForSelector(cdp, '.assign-search-result')
|
||||||
|
await assertEval(cdp, `document.querySelector('.assign-search-result')?.innerText.includes('Smoke Project')`, 'inbox: assign target search works')
|
||||||
|
await clickText(cdp, '.assign-search-result', 'Smoke Project')
|
||||||
|
await clickText(cdp, '.modal-actions .btn', 'Разложить')
|
||||||
|
await waitForGone(cdp, '.modal-overlay')
|
||||||
|
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('smoke-drop-folder')`, 'inbox: assigned item leaves inbox')
|
||||||
|
await clickInboxItemButton(cdp, 'pasted-smoke.png', 'Удалить')
|
||||||
|
await clickText(cdp, '.overlay .btn', 'Удалить')
|
||||||
|
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')
|
||||||
|
|
@ -392,6 +404,24 @@ async function clickFolderOpenButton(cdp, name) {
|
||||||
await sleep(300)
|
await sleep(300)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clickInboxItemButton(cdp, title, buttonText) {
|
||||||
|
const ok = await evalValue(cdp, `
|
||||||
|
(() => {
|
||||||
|
const norm = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
||||||
|
const row = [...document.querySelectorAll('.inbox-item')]
|
||||||
|
.find((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(title)}));
|
||||||
|
if (!row) return false;
|
||||||
|
const btn = [...row.querySelectorAll('button')]
|
||||||
|
.find((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(buttonText)}));
|
||||||
|
if (!btn) return false;
|
||||||
|
btn.click();
|
||||||
|
return true;
|
||||||
|
})()
|
||||||
|
`)
|
||||||
|
if (!ok) throw new Error(`Inbox item button not found: ${title} -> ${buttonText}`)
|
||||||
|
await sleep(250)
|
||||||
|
}
|
||||||
|
|
||||||
async function evalValue(cdp, expression) {
|
async function evalValue(cdp, expression) {
|
||||||
const result = await cdp.send('Runtime.evaluate', {
|
const result = await cdp.send('Runtime.evaluate', {
|
||||||
expression,
|
expression,
|
||||||
|
|
@ -688,6 +718,20 @@ function wailsMockSource() {
|
||||||
return node?.children || [];
|
return node?.children || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detachNode(id, items = state.nodes) {
|
||||||
|
const idx = items.findIndex((node) => node.id === id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const [node] = items.splice(idx, 1);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.children) continue;
|
||||||
|
const found = detachNode(id, item.children);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function readyStatus() {
|
function readyStatus() {
|
||||||
return {
|
return {
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
|
|
@ -762,6 +806,24 @@ function wailsMockSource() {
|
||||||
state.nodes.push(node);
|
state.nodes.push(node);
|
||||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||||
},
|
},
|
||||||
|
AssignInboxNode: async (nodeId, targetParentId) => {
|
||||||
|
const node = detachNode(nodeId);
|
||||||
|
const parent = findNode(targetParentId);
|
||||||
|
if (!node || !parent) throw new Error('assign target not found');
|
||||||
|
node.captureInbox = false;
|
||||||
|
node.captureKind = '';
|
||||||
|
node.captureSource = '';
|
||||||
|
node.parent_id = targetParentId;
|
||||||
|
parent.children = parent.children || [];
|
||||||
|
parent.children.push(node);
|
||||||
|
parent.has_children = true;
|
||||||
|
return clone(node);
|
||||||
|
},
|
||||||
|
DeleteInboxNode: async (nodeId) => {
|
||||||
|
const node = detachNode(nodeId);
|
||||||
|
if (!node) throw new Error('inbox node not found');
|
||||||
|
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 }],
|
||||||
|
|
@ -773,7 +835,7 @@ function wailsMockSource() {
|
||||||
SetTemplateEnabled: async () => true,
|
SetTemplateEnabled: async () => true,
|
||||||
GetNodeDetail: async (id) => clone(findNode(id) || fileNodeDetails[id] || null),
|
GetNodeDetail: async (id) => clone(findNode(id) || fileNodeDetails[id] || null),
|
||||||
GetNodeTitle: async (id) => findNode(id)?.title || '',
|
GetNodeTitle: async (id) => findNode(id)?.title || '',
|
||||||
SearchNodes: async (query) => allNodes().filter((node) => node.title.toLowerCase().includes(String(query || '').toLowerCase())).map((node) => ({ id: node.id, title: node.title, path: '/Smoke/' + node.title })),
|
SearchNodes: async (query) => allNodes().filter((node) => node.title.toLowerCase().includes(String(query || '').toLowerCase())).map((node) => ({ id: node.id, title: node.title, path: '/Smoke/' + node.title, type: node.type })),
|
||||||
CreateNodeFromTemplate: async (parentId, title, templateId) => {
|
CreateNodeFromTemplate: async (parentId, title, templateId) => {
|
||||||
const id = 'node-created-' + Date.now();
|
const id = 'node-created-' + Date.now();
|
||||||
const parent = parentId ? findNode(parentId) : null;
|
const parent = parentId ? findNode(parentId) : null;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue