feat: cover global trash workflow in desktop

This commit is contained in:
mirivlad 2026-07-10 09:00:14 +08:00
parent 3f27ba79a4
commit 1d1eaa6bc7
5 changed files with 388 additions and 73 deletions

View File

@ -464,6 +464,9 @@ contributions summary.
Conflicts are rejected unless `options.overwrite` is true.
- `files.deleteTrash(trashId)` — permanently removes a file/folder already in
internal trash. This operation cannot be undone.
- UI policy: the workspace `Files` plugin displays only live files and folders.
The global official `verstak.trash` plugin owns listing, restoring, filtering,
and permanently deleting deleted items across the vault.
- `files.openExternal(relativePath)` — opens a vault-relative file/folder in
the OS default application.
- `files.showInFolder(relativePath)` — reveals a vault-relative file/folder in

View File

@ -218,7 +218,7 @@ test.describe('G: Files Plugin', () => {
await expect(page.locator('[data-file-name="project-only.txt"]')).toBeVisible();
});
test('files explorer restores an item from trash metadata', async ({ page }) => {
test('files explorer keeps removed items out of live files without Trash metadata', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
@ -232,14 +232,8 @@ test.describe('G: Files Plugin', () => {
await page.locator('[data-files-action="trash"]').click();
await page.locator('.files-modal-btn.confirm').click();
await expect(page.locator('[data-file-name="RestoreMe.txt"]')).toHaveCount(0);
await page.locator('[data-files-action="trash-view"]').click();
await expect(page.locator('.files-breadcrumb')).toContainText('Trash metadata');
await expect(page.locator('[data-files-trash-id]').filter({ hasText: 'Project/RestoreMe.txt' })).toBeVisible();
await page.locator('[data-files-restore-trash]').first().click();
await expect(page.locator('.files-breadcrumb')).toContainText('Project');
await expect(page.locator('[data-file-name="RestoreMe.txt"]')).toBeVisible();
await expect(page.locator('[data-files-action="trash-view"]')).toHaveCount(0);
await expect(page.locator('[data-files-restore-trash]')).toHaveCount(0);
});
test('files explorer uses labeled controls and no row New Here action', async ({ page }) => {
@ -258,7 +252,6 @@ test.describe('G: Files Plugin', () => {
open: 'open',
rename: 'rename',
trash: 'trash',
'trash-view': 'trashView',
cut: 'cut',
copy: 'copy',
paste: 'paste',

View File

@ -78,13 +78,13 @@ test.describe('E: Plugin Manager layout', () => {
const health = page.locator('[data-plugin-manager-summary="health"]');
await expect(health).toBeVisible();
await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('9');
await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('10');
await expect(health.locator('[data-plugin-status-summary="failed"]')).toContainText('0');
await expect(health.locator('[data-plugin-status-summary="disabled"]')).toContainText('0');
const risk = page.locator('[data-plugin-manager-summary="risk"]');
await expect(risk).toBeVisible();
await expect(risk.locator('[data-plugin-risk-summary="elevated-permissions"]')).toContainText('5');
await expect(risk.locator('[data-plugin-risk-summary="elevated-permissions"]')).toContainText('6');
await expect(risk).toContainText('elevated permissions');
});

View File

@ -0,0 +1,121 @@
import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
async function openTrash(page) {
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Trash' }).click();
await expect(page.locator('.trash-root')).toBeVisible({ timeout: 10000 });
}
async function createTrashEntry(page, path, content, deletedAt) {
return page.evaluate(async ({ path, content, deletedAt }) => {
const api = window.createPluginAPI('verstak.files');
await api.files.writeText(path, content, { createIfMissing: true });
const entry = await api.files.trash(path);
window.__wailsMock.setTrashDeletedAt(entry.trashId, deletedAt);
api.dispose();
return entry;
}, { path, content, deletedAt });
}
test.describe('L: Global Trash Plugin', () => {
let consoleCollector;
test.beforeEach(async ({ page }) => {
consoleCollector = setupConsoleCollector(page);
await resetMockState(page);
await page.goto('/');
await waitForAppReady(page);
});
test.afterEach(async () => {
consoleCollector.assertNoErrors();
});
test('moves a Files item into global Trash without exposing Trash metadata in Files', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
await page.getByRole('tab', { name: 'Files' }).click();
await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
await page.locator('[data-files-action="new-text"]').click();
await page.locator('[data-files-create-input]').fill('GlobalTrash.txt');
await page.locator('[data-files-create-confirm]').click();
await expect(page.locator('[data-file-name="GlobalTrash.txt"]')).toBeVisible();
await page.locator('[data-file-name="GlobalTrash.txt"]').click();
await page.locator('[data-files-action="trash"]').click();
await page.locator('.files-modal-btn.confirm').click();
await expect(page.locator('[data-file-name="GlobalTrash.txt"]')).toHaveCount(0);
await expect(page.locator('[data-files-action="trash-view"]')).toHaveCount(0);
await openTrash(page);
const row = page.locator('[data-trash-row]').filter({ hasText: 'GlobalTrash.txt' });
await expect(row).toBeVisible();
await expect(row).toContainText('Project');
await expect(row).toContainText('Project/GlobalTrash.txt');
});
test('filters and sorts global Trash and restores without overwriting a conflict', async ({ page }) => {
const older = await createTrashEntry(page, 'Project/Older.txt', 'older original', '2026-06-20T08:00:00.000Z');
const newest = await createTrashEntry(page, 'Test/Newest.txt', 'newest original', '2026-06-29T08:00:00.000Z');
const restore = await createTrashEntry(page, 'Project/Restore.txt', 'restore original', '2026-06-25T08:00:00.000Z');
const conflict = await page.evaluate(async () => {
const api = window.createPluginAPI('verstak.files');
await api.files.writeText('Project/Conflict.txt', 'trashed content', { createIfMissing: true });
const entry = await api.files.trash('Project/Conflict.txt');
await api.files.writeText('Project/Conflict.txt', 'existing content', { createIfMissing: true });
window.__wailsMock.setTrashDeletedAt(entry.trashId, '2026-06-24T08:00:00.000Z');
api.dispose();
return entry;
});
await openTrash(page);
await page.locator('[data-trash-filter-workspace]').selectOption('Project');
await expect(page.locator(`[data-trash-row="${older.trashId}"]`)).toBeVisible();
await expect(page.locator(`[data-trash-row="${newest.trashId}"]`)).toHaveCount(0);
await page.locator('[data-trash-filter-workspace]').selectOption('');
await page.locator('[data-trash-filter-search]').fill('Newest');
await expect(page.locator(`[data-trash-row="${newest.trashId}"]`)).toBeVisible();
await expect(page.locator(`[data-trash-row="${older.trashId}"]`)).toHaveCount(0);
await page.locator('[data-trash-filter-search]').fill('');
await page.locator('[data-trash-sort]').selectOption('date-asc');
await expect(page.locator('[data-trash-row]').first()).toHaveAttribute('data-trash-row', older.trashId);
await page.locator(`[data-trash-restore="${restore.trashId}"]`).click();
await expect(page.locator(`[data-trash-row="${restore.trashId}"]`)).toHaveCount(0);
await expect.poll(() => page.evaluate(async () => {
const api = window.createPluginAPI('verstak.files');
const content = await api.files.readText('Project/Restore.txt');
api.dispose();
return content;
})).toBe('restore original');
await page.locator(`[data-trash-restore="${conflict.trashId}"]`).click();
await expect(page.locator('[data-trash-status]')).toContainText('Restore blocked');
await expect(page.locator(`[data-trash-row="${conflict.trashId}"]`)).toBeVisible();
await expect.poll(() => page.evaluate(async () => {
const api = window.createPluginAPI('verstak.files');
const content = await api.files.readText('Project/Conflict.txt');
api.dispose();
return content;
})).toBe('existing content');
});
test('requires confirmation before permanently deleting a Trash item', async ({ page }) => {
const entry = await createTrashEntry(page, 'Test/Permanent.txt', 'remove forever', '2026-06-26T08:00:00.000Z');
await openTrash(page);
await page.locator(`[data-trash-delete="${entry.trashId}"]`).click();
await expect(page.locator(`[data-trash-confirm="${entry.trashId}"]`)).toBeVisible();
await expect(page.locator(`[data-trash-row="${entry.trashId}"]`)).toBeVisible();
await page.locator(`[data-trash-confirm-delete="${entry.trashId}"]`).click();
await expect(page.locator(`[data-trash-row="${entry.trashId}"]`)).toHaveCount(0);
await expect.poll(() => page.evaluate(async (trashId) => {
const api = window.createPluginAPI('verstak.files');
const entries = await api.files.listTrash();
api.dispose();
return entries.some((item) => item.trashId === trashId);
}, entry.trashId)).toBe(false);
});
});

View File

@ -134,6 +134,7 @@
rootPath: '/tmp/verstak-test/plugins/files',
error: ''
},
'verstak.trash': makeTrashPluginState(),
'verstak.notes': {
status: 'loaded',
enabled: true,
@ -282,6 +283,8 @@
var vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
var vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.notes', 'verstak.sync', 'verstak.activity', 'verstak.journal', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.notes', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.journal', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
vaultPluginState.enabledPlugins.push('verstak.trash');
vaultPluginState.desiredPlugins.push({ id: 'verstak.trash', version: '0.1.0', source: 'official' });
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
var workbenchPreferences = {};
var openedResources = [];
@ -345,6 +348,33 @@
};
}
function makeTrashPluginState() {
return {
status: 'loaded',
enabled: true,
manifest: {
schemaVersion: 1,
id: 'verstak.trash',
name: 'Trash',
version: '0.1.0',
apiVersion: '0.1.0',
description: 'Global vault trash manager for deleted files and folders.',
source: 'official',
icon: 'trash',
provides: ['trash.management'],
requires: ['verstak/core/files/v1'],
permissions: ['files.delete', 'files.write', 'ui.register'],
frontend: { entry: 'frontend/dist/index.js' },
contributes: {
views: [{ id: 'verstak.trash.view', title: 'Trash', icon: 'trash', component: 'TrashView' }],
sidebarItems: [{ id: 'verstak.trash.sidebar', title: 'Trash', icon: 'trash', view: 'verstak.trash.view', position: 40 }]
}
},
rootPath: '/tmp/verstak-test/plugins/trash',
error: ''
};
}
function makeDefaultSyncState() {
return {
configured: false,
@ -840,7 +870,6 @@
var sort = 'folder-name';
var createMode = '';
var renaming = null;
var showingTrash = false;
function scoped(local) { local = clean(local); return root ? (local ? root + '/' + local : root) : local; }
function local(full) { full = clean(full); return root && full.indexOf(root + '/') === 0 ? full.slice(root.length + 1) : full === root ? '' : full; }
function saveHistory() { window.__filesHistoryByWorkspace[historyKey] = { stack: history.slice(), index: historyIndex, currentPath: current }; }
@ -899,19 +928,18 @@
var backButton = btn('Back', 'back', goBack, 'back');
var forwardButton = btn('Forward', 'forward', goForward, 'forward');
var upButton = btn('Up', 'up', function () { if (current) nav(parent(current)); }, 'up');
var refreshButton = btn('Refresh', 'refresh', function () { if (showingTrash) loadTrash(); else load(); }, 'refresh');
var refreshButton = btn('Refresh', 'refresh', load, 'refresh');
var newFolderButton = btn('New folder', 'new-folder', function () { startCreate('folder'); }, 'folderAdd');
var newMarkdownButton = btn('New markdown file', 'new-markdown', function () { startCreate('markdown'); }, 'markdownAdd');
var newTextButton = btn('New text file', 'new-text', function () { startCreate('text'); }, 'textAdd');
var openButton = btn('Open', 'open', function () { open(firstSelected()); }, 'open');
var renameButton = btn('Rename', 'rename', function () { startRename(firstSelected()); }, 'rename');
var trashButton = btn('Move to trash', 'trash', function () { trashSelection(); }, 'trash');
var trashViewButton = btn('Trash metadata', 'trash-view', loadTrash, 'trashView');
var cutButton = btn('Cut', 'cut', cutSelection, 'cut');
var copyButton = btn('Copy', 'copy', copySelection, 'copy');
var pasteButton = btn('Paste', 'paste', paste, 'paste');
toolbar.appendChild(breadcrumb);
[backButton, forwardButton, upButton, refreshButton, newFolderButton, newMarkdownButton, newTextButton, openButton, renameButton, trashButton, trashViewButton, cutButton, copyButton, pasteButton].forEach(function (button) { toolbar.appendChild(button); });
[backButton, forwardButton, upButton, refreshButton, newFolderButton, newMarkdownButton, newTextButton, openButton, renameButton, trashButton, cutButton, copyButton, pasteButton].forEach(function (button) { toolbar.appendChild(button); });
var filterInput = e('input', { className: 'files-filter', 'data-files-filter': '', placeholder: 'Filter current folder' }, []);
filterInput.addEventListener('input', function () { filter = filterInput.value.toLowerCase(); render(); });
toolbar.appendChild(filterInput);
@ -967,18 +995,6 @@
}, [part]));
});
}
function setToolbarTrashMode(isTrash) {
[upButton, newFolderButton, newMarkdownButton, newTextButton, openButton, renameButton, trashButton, cutButton, copyButton, pasteButton, filterInput, sortSelect].forEach(function (node) {
node.disabled = !!isTrash;
if (isTrash) {
node.style.setProperty('opacity', '.45', 'important');
node.style.setProperty('cursor', 'default', 'important');
} else {
node.style.removeProperty('opacity');
node.style.removeProperty('cursor');
}
});
}
function visible() {
return entries.filter(function (item) { return !item.isHidden && !item.isReserved && (!filter || item.name.toLowerCase().indexOf(filter) !== -1); }).sort(function (a, b) {
if (sort === 'folder-name') { if (a.type === 'folder' && b.type !== 'folder') return -1; if (a.type !== 'folder' && b.type === 'folder') return 1; }
@ -989,8 +1005,6 @@
});
}
function render() {
showingTrash = false;
setToolbarTrashMode(false);
updateBreadcrumb();
list.innerHTML = '';
list.appendChild(e('div', { className: 'files-header' }, [e('span', {}, ['Name']), e('span', {}, ['Type']), e('span', {}, ['Size']), e('span', {}, ['Modified']), e('span', {}, ['Actions'])]));
@ -1024,38 +1038,6 @@
list.appendChild(row);
});
}
function renderTrash(items) {
showingTrash = true;
setToolbarTrashMode(true);
selected = {};
lastClicked = '';
breadcrumb.innerHTML = '';
breadcrumb.appendChild(e('span', { className: 'files-breadcrumb-current' }, ['Trash metadata']));
list.innerHTML = '';
list.appendChild(e('div', { className: 'files-header' }, [e('span', {}, ['Original path']), e('span', {}, ['Type']), e('span', {}, ['Deleted']), e('span', {}, ['Trash path']), e('span', {}, ['Actions'])]));
if (!items.length) {
list.appendChild(e('div', { className: 'files-empty' }, ['Trash is empty']));
return;
}
items.forEach(function (item) {
list.appendChild(e('div', { className: 'files-item', 'data-files-trash-id': item.trashId || '' }, [
e('span', { className: 'files-item-name', title: item.originalPath || '' }, [item.originalPath || item.basename || '']),
e('span', { className: 'files-item-meta' }, [item.originalType || '']),
e('span', { className: 'files-item-meta' }, [formatDate(item.deletedAt)]),
e('span', { className: 'files-item-meta', title: item.trashPath || '' }, [item.trashPath || '']),
e('span', { className: 'files-row-actions' }, [e('button', {
className: 'files-row-btn',
'data-files-action': 'restore-trash',
'data-files-icon': 'restore',
'data-files-restore-trash': item.trashId || '',
title: 'Restore',
'aria-label': 'Restore',
innerHTML: actionSvg('restore'),
onClick: function (ev) { ev.stopPropagation(); restoreTrash(item); }
}, [])])
]));
});
}
function select(item, ev) {
if (ev && (ev.ctrlKey || ev.metaKey)) {
if (selected[item.relativePath]) delete selected[item.relativePath]; else selected[item.relativePath] = true;
@ -1074,17 +1056,6 @@
render();
}
function load() { selected = {}; api.files.list(scoped(current)).then(function (result) { entries = result || []; render(); }).catch(function (err) { list.textContent = 'Error: ' + (err.message || err); }); }
function loadTrash() {
if (!api.files || typeof api.files.listTrash !== 'function') {
list.innerHTML = 'Trash metadata is unavailable';
return;
}
api.files.listTrash().then(function (result) { renderTrash(result || []); }).catch(function (err) { list.textContent = 'Error: ' + (err.message || err); });
}
function restoreTrash(item) {
if (!item || !item.trashId || !api.files || typeof api.files.restoreTrash !== 'function') return;
api.files.restoreTrash(item.trashId, { overwrite: false }).then(load).catch(function (err) { list.textContent = 'Error: ' + (err.message || err); });
}
function nav(path, push) {
current = clean(path);
if (push !== false) {
@ -1428,6 +1399,222 @@
}.toString() + ')();';
}
function trashPluginBundle() {
return '(' + function () {
var PLUGIN_ID = 'verstak.trash';
function el(tag, attrs, children) {
var node = document.createElement(tag);
attrs = attrs || {};
Object.keys(attrs).forEach(function (key) {
var value = attrs[key];
if (value == null) return;
if (key === 'className') node.className = value;
else if (key === 'textContent') node.textContent = value;
else if (key.indexOf('on') === 0) node.addEventListener(key.slice(2).toLowerCase(), value);
else if (key === 'value') node.value = value;
else if (key === 'disabled') node.disabled = !!value;
else node.setAttribute(key, value);
});
(children || []).forEach(function (child) {
if (child == null) return;
node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
});
return node;
}
function text(value) { return String(value == null ? '' : value); }
function cleanPath(value) { return text(value).split('/').filter(Boolean).join('/'); }
function nameFor(entry) {
var path = cleanPath(entry && entry.originalPath);
return text(entry && entry.basename).trim() || path.split('/').pop() || 'Untitled item';
}
function workspaceFor(entry) { return cleanPath(entry && entry.originalPath).split('/')[0] || 'Vault root'; }
function typeFor(entry) { return entry && entry.originalType === 'folder' ? 'Folder' : 'File'; }
function dateFor(entry) { return text(entry && entry.deletedAt); }
function errorText(error) { return error && error.message ? error.message : text(error); }
function isConflict(error) { return /conflict:/i.test(errorText(error)); }
var TrashView = {
mount: function (containerEl, props, api) {
var state = {
entries: [], workspace: '', query: '', sort: 'date-desc', loading: true,
busyId: '', confirmingId: '', status: '', statusError: false, disposed: false
};
function workspaces() {
var values = {};
state.entries.forEach(function (entry) { values[workspaceFor(entry)] = true; });
return Object.keys(values).sort();
}
function visible() {
var query = state.query.toLowerCase();
return state.entries.filter(function (entry) {
if (state.workspace && workspaceFor(entry) !== state.workspace) return false;
return !query || (nameFor(entry) + ' ' + text(entry.originalPath) + ' ' + workspaceFor(entry)).toLowerCase().indexOf(query) !== -1;
}).sort(function (left, right) {
if (state.sort === 'date-asc') return dateFor(left).localeCompare(dateFor(right));
if (state.sort === 'name-asc') return nameFor(left).localeCompare(nameFor(right));
return dateFor(right).localeCompare(dateFor(left));
});
}
function render() {
var rows = visible();
containerEl.innerHTML = '';
containerEl.className = 'trash-root';
containerEl.setAttribute('data-plugin-id', PLUGIN_ID);
var workspaceSelect = el('select', {
'data-trash-filter-workspace': '', value: state.workspace,
onChange: function (event) { state.workspace = event.target.value; render(); }
}, [el('option', { value: '' }, ['All workspaces'])]);
workspaces().forEach(function (workspace) {
workspaceSelect.appendChild(el('option', { value: workspace }, [workspace]));
});
var search = el('input', {
type: 'search', value: state.query, placeholder: 'Filter name or path',
'data-trash-filter-search': '',
onInput: function (event) { state.query = event.target.value; render(); }
}, []);
var sort = el('select', {
'data-trash-sort': '', value: state.sort,
onChange: function (event) { state.sort = event.target.value; render(); }
}, [
el('option', { value: 'date-desc' }, ['Deleted: newest']),
el('option', { value: 'date-asc' }, ['Deleted: oldest']),
el('option', { value: 'name-asc' }, ['Name'])
]);
containerEl.appendChild(el('div', { className: 'trash-toolbar' }, [
el('strong', {}, ['Trash']), search, workspaceSelect, sort,
el('button', { type: 'button', onClick: load }, ['Refresh'])
]));
containerEl.appendChild(el('div', {
className: 'trash-status' + (state.statusError ? ' error' : ''),
'data-trash-status': ''
}, [state.loading ? 'Loading deleted items...' : (state.status || rows.length + ' deleted items')]));
var list = el('div', { className: 'trash-list', 'data-trash-list': '' }, []);
if (state.loading) {
list.appendChild(el('div', { className: 'trash-empty' }, ['Loading deleted items...']));
} else if (!rows.length) {
list.appendChild(el('div', { className: 'trash-empty' }, [state.entries.length ? 'No deleted items match the current filters.' : 'Trash is empty.']));
} else {
rows.forEach(function (entry) {
list.appendChild(el('div', {
className: 'trash-row', 'data-trash-row': entry.trashId, 'data-trash-workspace': workspaceFor(entry)
}, [
el('span', { className: 'trash-name' }, [nameFor(entry)]),
el('span', { className: 'trash-workspace' }, [workspaceFor(entry)]),
el('span', { className: 'trash-path' }, [entry.originalPath || '']),
el('span', { className: 'trash-meta' }, [dateFor(entry)]),
el('span', { className: 'trash-meta' }, [typeFor(entry)]),
el('button', {
type: 'button', disabled: state.busyId === entry.trashId,
'data-trash-restore': entry.trashId,
onClick: function () { restore(entry); }
}, [state.busyId === entry.trashId ? 'Restoring...' : 'Restore']),
el('button', {
type: 'button', disabled: state.busyId === entry.trashId,
'data-trash-delete': entry.trashId,
onClick: function () { state.confirmingId = entry.trashId; render(); }
}, ['Delete permanently'])
]));
});
}
containerEl.appendChild(list);
var entry = state.entries.find(function (item) { return item.trashId === state.confirmingId; });
if (entry) {
containerEl.appendChild(el('div', { className: 'trash-confirm', 'data-trash-confirm': entry.trashId }, [
el('p', {}, ['Delete permanently?']),
el('span', {}, [entry.originalPath || nameFor(entry)]),
el('button', {
type: 'button', 'data-trash-confirm-cancel': entry.trashId,
onClick: function () { state.confirmingId = ''; render(); }
}, ['Cancel']),
el('button', {
type: 'button', disabled: state.busyId === entry.trashId,
'data-trash-confirm-delete': entry.trashId,
onClick: function () { deletePermanently(entry); }
}, ['Delete permanently'])
]));
}
}
function restore(entry) {
state.busyId = entry.trashId;
state.status = '';
state.statusError = false;
render();
api.files.restoreTrash(entry.trashId, { overwrite: false }).then(function () {
if (state.disposed) return;
state.entries = state.entries.filter(function (item) { return item.trashId !== entry.trashId; });
state.busyId = '';
state.status = 'Restored ' + nameFor(entry) + '.';
render();
}).catch(function (error) {
if (state.disposed) return;
state.busyId = '';
state.statusError = true;
state.status = isConflict(error)
? 'Restore blocked: an item already exists at the original path. Nothing was overwritten.'
: 'Restore failed: ' + errorText(error);
render();
});
}
function deletePermanently(entry) {
state.busyId = entry.trashId;
state.status = '';
state.statusError = false;
render();
api.files.deleteTrash(entry.trashId).then(function () {
if (state.disposed) return;
state.entries = state.entries.filter(function (item) { return item.trashId !== entry.trashId; });
state.busyId = '';
state.confirmingId = '';
state.status = 'Permanently deleted ' + nameFor(entry) + '.';
render();
}).catch(function (error) {
if (state.disposed) return;
state.busyId = '';
state.statusError = true;
state.status = 'Permanent delete failed: ' + errorText(error);
render();
});
}
function load() {
state.loading = true;
state.status = '';
state.statusError = false;
render();
api.files.listTrash().then(function (entries) {
if (state.disposed) return;
state.entries = Array.isArray(entries) ? entries : [];
state.loading = false;
render();
}).catch(function (error) {
if (state.disposed) return;
state.entries = [];
state.loading = false;
state.statusError = true;
state.status = 'Could not load Trash: ' + errorText(error);
render();
});
}
containerEl.__trashCleanup = function () { state.disposed = true; containerEl.innerHTML = ''; };
load();
},
unmount: function (containerEl) { if (containerEl.__trashCleanup) containerEl.__trashCleanup(); }
};
window.VerstakPluginRegister(PLUGIN_ID, { components: { TrashView: TrashView } });
}.toString() + ')();';
}
function simplePluginBundle(pluginId, componentName, rootClass, title) {
var markup = '<div class="' + rootClass + '"><h2>' + title + '</h2></div>';
return '(function(){var Component={mount:function(containerEl){containerEl.innerHTML=' + JSON.stringify(markup) + ';},unmount:function(containerEl){containerEl.innerHTML="";}};window.VerstakPluginRegister(' + JSON.stringify(pluginId) + ',{components:{' + componentName + ':Component}});})();';
@ -2442,6 +2629,9 @@
if (pluginId === 'verstak.files' && assetPath === 'frontend/dist/index.js') {
return Promise.resolve(filesPluginBundle());
}
if (pluginId === 'verstak.trash' && assetPath === 'frontend/dist/index.js') {
return Promise.resolve(trashPluginBundle());
}
if (pluginId === 'verstak.notes' && assetPath === 'frontend/dist/index.js') {
return Promise.resolve(simplePluginBundle('verstak.notes', 'NotesView', 'notes-root', 'Notes'));
}
@ -2943,6 +3133,7 @@
rootPath: '/tmp/verstak-test/plugins/files',
error: ''
},
'verstak.trash': makeTrashPluginState(),
'verstak.notes': {
status: 'loaded',
enabled: true,
@ -3090,6 +3281,8 @@
};
vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.notes', 'verstak.sync', 'verstak.activity', 'verstak.journal', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.notes', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.journal', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
vaultPluginState.enabledPlugins.push('verstak.trash');
vaultPluginState.desiredPlugins.push({ id: 'verstak.trash', version: '0.1.0', source: 'official' });
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
workbenchPreferences = {};
openedResources = [];
@ -3152,6 +3345,11 @@
},
setVaultStatus: function (status) { vaultStatus = status; },
setVaultPluginState: function (state) { vaultPluginState = state; },
setTrashDeletedAt: function (trashId, deletedAt) {
trashEntries.forEach(function (entry) {
if (entry.trashId === trashId) entry.deletedAt = deletedAt;
});
},
setReloadResponseMode: function (mode) { reloadResponseMode = mode || 'tuple'; }
};