diff --git a/frontend/e2e/files-plugin.spec.js b/frontend/e2e/files-plugin.spec.js
index faf7049..8259337 100644
--- a/frontend/e2e/files-plugin.spec.js
+++ b/frontend/e2e/files-plugin.spec.js
@@ -114,12 +114,36 @@ test.describe('G: Files Plugin', () => {
await expect(page.locator('[data-file-name="bad"]')).toHaveCount(0);
});
+ test('files explorer restores an item from 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 });
+
+ await page.locator('[data-files-action="new-text"]').click();
+ await page.locator('[data-files-create-input]').fill('RestoreMe.txt');
+ await page.locator('[data-files-create-confirm]').click();
+ await expect(page.locator('[data-file-name="RestoreMe.txt"]')).toBeVisible();
+
+ await page.locator('[data-file-name="RestoreMe.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="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();
+ });
+
test('files explorer uses labeled controls and no row New Here action', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
- for (const action of ['back', 'forward', 'up', 'refresh', 'new-folder', 'new-markdown', 'new-text', 'open', 'rename', 'trash', 'cut', 'copy', 'paste']) {
+ for (const action of ['back', 'forward', 'up', 'refresh', 'new-folder', 'new-markdown', 'new-text', 'open', 'rename', 'trash', 'trash-view', 'cut', 'copy', 'paste']) {
const button = page.locator(`[data-files-action="${action}"]`);
await expect(button).toHaveAttribute('title', /.+/);
await expect(button.locator('svg')).toBeVisible();
diff --git a/frontend/src/lib/test/wails-mock.js b/frontend/src/lib/test/wails-mock.js
index 189a1d0..cd6a3d3 100644
--- a/frontend/src/lib/test/wails-mock.js
+++ b/frontend/src/lib/test/wails-mock.js
@@ -730,6 +730,7 @@
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 }; }
@@ -737,20 +738,22 @@
var breadcrumb = e('div', { className: 'files-breadcrumb' }, []);
function btn(title, action, fn) { return e('button', { className: 'files-toolbar-btn', 'data-files-action': action, title: title, 'aria-label': title, innerHTML: SVG + '' + title + '', onClick: fn }, []); }
function rowBtn(title, action, fn) { return e('button', { className: 'files-row-btn', 'data-files-action': action, title: title, 'aria-label': title, innerHTML: SVG + '' + title + '', onClick: fn }, []); }
+ var backButton = btn('Back', 'back', goBack);
+ var forwardButton = btn('Forward', 'forward', goForward);
+ var upButton = btn('Up', 'up', function () { if (current) nav(parent(current)); });
+ var refreshButton = btn('Refresh', 'refresh', function () { if (showingTrash) loadTrash(); else load(); });
+ var newFolderButton = btn('New folder', 'new-folder', function () { startCreate('folder'); });
+ var newMarkdownButton = btn('New markdown file', 'new-markdown', function () { startCreate('markdown'); });
+ var newTextButton = btn('New text file', 'new-text', function () { startCreate('text'); });
+ var openButton = btn('Open', 'open', function () { open(firstSelected()); });
+ var renameButton = btn('Rename', 'rename', function () { startRename(firstSelected()); });
+ var trashButton = btn('Move to trash', 'trash', function () { trashSelection(); });
+ var trashViewButton = btn('Trash metadata', 'trash-view', loadTrash);
+ var cutButton = btn('Cut', 'cut', cutSelection);
+ var copyButton = btn('Copy', 'copy', copySelection);
+ var pasteButton = btn('Paste', 'paste', paste);
toolbar.appendChild(breadcrumb);
- toolbar.appendChild(btn('Back', 'back', goBack));
- toolbar.appendChild(btn('Forward', 'forward', goForward));
- toolbar.appendChild(btn('Up', 'up', function () { if (current) nav(parent(current)); }));
- toolbar.appendChild(btn('Refresh', 'refresh', load));
- toolbar.appendChild(btn('New folder', 'new-folder', function () { startCreate('folder'); }));
- toolbar.appendChild(btn('New markdown file', 'new-markdown', function () { startCreate('markdown'); }));
- toolbar.appendChild(btn('New text file', 'new-text', function () { startCreate('text'); }));
- toolbar.appendChild(btn('Open', 'open', function () { open(firstSelected()); }));
- toolbar.appendChild(btn('Rename', 'rename', function () { startRename(firstSelected()); }));
- toolbar.appendChild(btn('Move to trash', 'trash', function () { trashSelection(); }));
- toolbar.appendChild(btn('Cut', 'cut', cutSelection));
- toolbar.appendChild(btn('Copy', 'copy', copySelection));
- toolbar.appendChild(btn('Paste', 'paste', paste));
+ [backButton, forwardButton, upButton, refreshButton, newFolderButton, newMarkdownButton, newTextButton, openButton, renameButton, trashButton, trashViewButton, 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);
@@ -802,6 +805,18 @@
}, [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; }
@@ -812,6 +827,8 @@
});
}
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'])]));
@@ -839,6 +856,37 @@
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-restore-trash': item.trashId || '',
+ title: 'Restore',
+ 'aria-label': 'Restore',
+ innerHTML: SVG + '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;
@@ -857,6 +905,17 @@
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) {