feat: complete trash restore and batch actions
This commit is contained in:
+103
-7
@@ -102,6 +102,9 @@
|
||||
let linkNote = ''
|
||||
let linkStatus = ''
|
||||
let trashInfo = null
|
||||
let trashCount = 0
|
||||
let trashSelectedIds = []
|
||||
let trashFolderId = ''
|
||||
let showCreateNode = false
|
||||
let newNodeTitle = ''
|
||||
let createInNode = null
|
||||
@@ -237,6 +240,7 @@
|
||||
|
||||
loading = false
|
||||
loadSyncStatus()
|
||||
refreshTrashCount()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -278,7 +282,7 @@
|
||||
} else if (id === 'inbox') {
|
||||
inboxNodes = await wailsCall('ListInboxNodes') || []
|
||||
} else if (id === 'trash') {
|
||||
trashInfo = await wailsCall('ListTrash') || { nodes: [], entries: [], trashPath: '' }
|
||||
await refreshTrash()
|
||||
} else if (id === 'journal') {
|
||||
await loadJournal()
|
||||
} else if (id === 'activity') {
|
||||
@@ -882,6 +886,7 @@
|
||||
try {
|
||||
await wailsCall('DeleteNode', node.id)
|
||||
await reloadTreePreservingExpanded()
|
||||
await refreshTrashCount()
|
||||
if (selectedNode && selectedNode.id === node.id) {
|
||||
selectedNode = null
|
||||
}
|
||||
@@ -1168,6 +1173,73 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTrash() {
|
||||
trashInfo = await wailsCall('ListTrash') || { nodes: [], entries: [], trashPath: '', count: 0 }
|
||||
trashCount = trashInfo.count || 0
|
||||
trashSelectedIds = trashSelectedIds.filter(id => (trashInfo.nodes || []).some(node => node.id === id))
|
||||
if (trashFolderId && !(trashInfo.nodes || []).some(node => node.id === trashFolderId)) {
|
||||
trashFolderId = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTrashCount() {
|
||||
try { trashCount = await wailsCall('TrashCount') || 0 } catch (e) { trashCount = 0 }
|
||||
}
|
||||
|
||||
function trashVisibleNodes() {
|
||||
const nodes = trashInfo?.nodes || []
|
||||
if (!trashFolderId) return nodes.filter(node => !node.parentId || !nodes.some(other => other.id === node.parentId))
|
||||
return nodes.filter(node => node.parentId === trashFolderId)
|
||||
}
|
||||
|
||||
function toggleTrashSelection(id) {
|
||||
trashSelectedIds = trashSelectedIds.includes(id)
|
||||
? trashSelectedIds.filter(existing => existing !== id)
|
||||
: [...trashSelectedIds, id]
|
||||
}
|
||||
|
||||
function trashSelectionOr(id) {
|
||||
return trashSelectedIds.length > 0 ? trashSelectedIds : [id]
|
||||
}
|
||||
|
||||
async function restoreTrash(ids) {
|
||||
try {
|
||||
await wailsCall('RestoreTrashNodesJSON', JSON.stringify(ids))
|
||||
await reloadTreePreservingExpanded()
|
||||
await refreshTrash()
|
||||
} catch (e) { error = String(e) }
|
||||
}
|
||||
|
||||
async function purgeTrash(ids) {
|
||||
openConfirm({
|
||||
title: t('delete.confirmTitle'),
|
||||
message: t('delete.confirmMessage') + ' ' + ids.length + '?',
|
||||
confirmText: t('common.delete'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await wailsCall('PurgeTrashNodesJSON', JSON.stringify(ids))
|
||||
await refreshTrash()
|
||||
} catch (e) { error = String(e) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function emptyTrash() {
|
||||
openConfirm({
|
||||
title: t('delete.confirmTitle'),
|
||||
message: t('trash.empty') + '?',
|
||||
confirmText: t('common.delete'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await wailsCall('EmptyTrash')
|
||||
await refreshTrash()
|
||||
} catch (e) { error = String(e) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openSuggestionWorklogModal(s) {
|
||||
acceptingSuggestion = s
|
||||
editingWorklogEntry = null
|
||||
@@ -2116,6 +2188,9 @@
|
||||
{#if view.id === 'journal' && suggestionCount > 0}
|
||||
<span class="nav-badge">{suggestionCount}</span>
|
||||
{/if}
|
||||
{#if view.id === 'trash' && trashCount > 0}
|
||||
<span class="nav-badge">{trashCount}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -2654,7 +2729,14 @@
|
||||
<h2>{t('nav.trash')}</h2>
|
||||
<p>{trashInfo?.trashPath || ''}</p>
|
||||
</div>
|
||||
<button class="btn btn-sm" on:click={() => wailsCall('OpenTrashFolder')}>{t('trash.openFolder')}</button>
|
||||
<div class="trash-actions">
|
||||
{#if trashSelectedIds.length > 0}
|
||||
<button class="btn btn-sm btn-primary" on:click={() => restoreTrash(trashSelectedIds)}>{t('trash.restore')} ({trashSelectedIds.length})</button>
|
||||
<button class="btn btn-sm btn-danger" on:click={() => purgeTrash(trashSelectedIds)}>{t('common.delete')} ({trashSelectedIds.length})</button>
|
||||
{/if}
|
||||
<button class="btn btn-sm btn-danger" on:click={emptyTrash}>{t('trash.emptyTrash')}</button>
|
||||
<button class="btn btn-sm" on:click={() => wailsCall('OpenTrashFolder')}>{t('trash.openFolder')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if !trashInfo || ((trashInfo.nodes || []).length === 0 && (trashInfo.entries || []).length === 0)}
|
||||
<div class="empty-state">
|
||||
@@ -2667,13 +2749,24 @@
|
||||
{#if (trashInfo.nodes || []).length === 0}
|
||||
<p class="trash-empty-line">{t('common.empty')}</p>
|
||||
{:else}
|
||||
{#each trashInfo.nodes as node}
|
||||
<div class="trash-row">
|
||||
{#if trashFolderId}
|
||||
<button class="btn btn-sm back-btn" on:click={() => { trashFolderId = ''; trashSelectedIds = [] }}>{t('common.backLabel')}</button>
|
||||
{/if}
|
||||
{#each trashVisibleNodes() as node}
|
||||
<div class="trash-row" class:selected={trashSelectedIds.includes(node.id)}>
|
||||
<input type="checkbox" checked={trashSelectedIds.includes(node.id)} on:change={() => toggleTrashSelection(node.id)} />
|
||||
<div class="trash-row-main">
|
||||
<span class="trash-row-title">{node.title}</span>
|
||||
<span class="trash-row-meta">{nodeKindLabel(node.type)} · {formatDate(node.deletedAt)}</span>
|
||||
<span class="trash-row-meta">{node.nodePath || nodeKindLabel(node.type)} · {formatDate(node.deletedAt)}</span>
|
||||
</div>
|
||||
{#if node.fsPath}<span class="trash-row-path">{node.fsPath}</span>{/if}
|
||||
<div class="trash-row-actions">
|
||||
{#if node.type !== 'file' && node.type !== 'note'}
|
||||
<button class="btn btn-sm" on:click={() => { trashFolderId = node.id; trashSelectedIds = [] }}>{t('common.open')}</button>
|
||||
{/if}
|
||||
<button class="btn btn-sm btn-primary" on:click={() => restoreTrash(trashSelectionOr(node.id))}>{t('trash.restore')}</button>
|
||||
<button class="btn btn-sm btn-danger" on:click={() => purgeTrash(trashSelectionOr(node.id))}>{t('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -3527,14 +3620,17 @@
|
||||
.trash-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
|
||||
.trash-header h2 { margin: 0 0 6px; }
|
||||
.trash-header p { margin: 0; color: #8888a0; font-size: 12px; }
|
||||
.trash-actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
|
||||
.trash-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
|
||||
.trash-section { min-width: 0; }
|
||||
.trash-section h3 { margin: 0 0 10px; font-size: 13px; color: #a5b4fc; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
.trash-row { padding: 10px 12px; border: 1px solid #2a2a3c; border-radius: 8px; background: #1a1a28; margin-bottom: 8px; }
|
||||
.trash-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid #2a2a3c; border-radius: 8px; background: #1a1a28; margin-bottom: 8px; }
|
||||
.trash-row.selected { border-color: #6366f1; background: #20203a; }
|
||||
.trash-row-main { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; }
|
||||
.trash-row-title { min-width: 0; color: #e4e4ef; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.trash-row-meta { flex-shrink: 0; color: #8888a0; font-size: 12px; }
|
||||
.trash-row-path { display: block; margin-top: 4px; color: #707088; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.trash-row-path { grid-column: 2 / -1; display: block; color: #707088; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.trash-row-actions { display: flex; gap: 6px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
|
||||
.trash-empty-line { color: #8888a0; font-size: 13px; margin: 0; }
|
||||
|
||||
/* Journal screen */
|
||||
|
||||
@@ -55,6 +55,8 @@ export default {
|
||||
'capture.dropOverlayGlobal': 'Will be added to global Inbox',
|
||||
'trash.openFolder': 'Open trash folder',
|
||||
'trash.empty': 'Trash is empty',
|
||||
'trash.emptyTrash': 'Empty trash',
|
||||
'trash.restore': 'Restore',
|
||||
'trash.deletedNodes': 'Deleted items',
|
||||
'trash.physicalEntries': 'Files in .verstak/trash',
|
||||
'tab.overview': 'Overview',
|
||||
|
||||
@@ -58,6 +58,8 @@ export default {
|
||||
|
||||
'trash.openFolder': 'Открыть папку корзины',
|
||||
'trash.empty': 'Корзина пуста',
|
||||
'trash.emptyTrash': 'Очистить корзину',
|
||||
'trash.restore': 'Восстановить',
|
||||
'trash.deletedNodes': 'Удаленные элементы',
|
||||
'trash.physicalEntries': 'Файлы в .verstak/trash',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user