feat: restore files from trash view
This commit is contained in:
parent
4e6188e7cf
commit
914c8a7204
|
|
@ -112,7 +112,8 @@
|
||||||
explorer: 'M3 5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v1H3V5Zm0 6h18v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7Z',
|
explorer: 'M3 5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v1H3V5Zm0 6h18v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7Z',
|
||||||
cut: 'M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3L9.64 7.64zM6 8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0 12c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm6-8.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM19 3l-6 6 2 2 7-8h-3z',
|
cut: 'M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3L9.64 7.64zM6 8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0 12c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm6-8.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM19 3l-6 6 2 2 7-8h-3z',
|
||||||
copy: 'M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z',
|
copy: 'M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z',
|
||||||
paste: 'M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z'
|
paste: 'M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z',
|
||||||
|
restore: 'M13 3a9 9 0 1 1-8.95 8H2l3-3 3 3H6.06A7 7 0 1 0 13 5V3zm-1 5h2v5h4v2h-6V8z'
|
||||||
};
|
};
|
||||||
|
|
||||||
function iconButton(action, title, iconKey, onClick, extraClass) {
|
function iconButton(action, title, iconKey, onClick, extraClass) {
|
||||||
|
|
@ -589,8 +590,8 @@
|
||||||
el('span', {}, ['Original path']),
|
el('span', {}, ['Original path']),
|
||||||
el('span', {}, ['Type']),
|
el('span', {}, ['Type']),
|
||||||
el('span', {}, ['Deleted']),
|
el('span', {}, ['Deleted']),
|
||||||
el('span', {}, ['Trash ID']),
|
el('span', {}, ['Trash path']),
|
||||||
el('span', {}, ['Trash path'])
|
el('span', {}, ['Actions'])
|
||||||
]);
|
]);
|
||||||
listContainer.appendChild(header);
|
listContainer.appendChild(header);
|
||||||
|
|
||||||
|
|
@ -608,8 +609,18 @@
|
||||||
el('span', { className: 'files-item-name', textContent: entry.originalPath || entry.basename || '', title: entry.originalPath || '' }),
|
el('span', { className: 'files-item-name', textContent: entry.originalPath || entry.basename || '', title: entry.originalPath || '' }),
|
||||||
el('span', { className: 'files-item-meta' }, [entry.originalType || '']),
|
el('span', { className: 'files-item-meta' }, [entry.originalType || '']),
|
||||||
el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.deletedAt)]),
|
el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.deletedAt)]),
|
||||||
el('span', { className: 'files-item-meta hide-narrow' }, [entry.trashId || '']),
|
el('span', { className: 'files-item-meta hide-narrow', title: entry.trashId || '' }, [entry.trashPath || '']),
|
||||||
el('span', { className: 'files-item-meta' }, [entry.trashPath || ''])
|
el('div', { className: 'files-row-actions' }, [
|
||||||
|
el('button', {
|
||||||
|
className: 'files-row-btn',
|
||||||
|
'data-files-action': 'restore-trash',
|
||||||
|
'data-files-restore-trash': entry.trashId || '',
|
||||||
|
title: 'Restore',
|
||||||
|
'aria-label': 'Restore',
|
||||||
|
innerHTML: svgIcon(ACTION_ICONS.restore),
|
||||||
|
onClick: function (event) { event.stopPropagation(); restoreTrashEntry(entry); }
|
||||||
|
})
|
||||||
|
])
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
updateButtons();
|
updateButtons();
|
||||||
|
|
@ -867,6 +878,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreTrashEntry(entry) {
|
||||||
|
if (!entry || !entry.trashId) return;
|
||||||
|
if (!api.files || typeof api.files.restoreTrash !== 'function') {
|
||||||
|
window.alert('Restore is unavailable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api.files.restoreTrash(entry.trashId, { overwrite: false }).then(function () {
|
||||||
|
loadEntries();
|
||||||
|
}).catch(function (err) {
|
||||||
|
window.alert((err && err.message) ? err.message : String(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); });
|
filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); });
|
||||||
sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); });
|
sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); });
|
||||||
createConfirm.addEventListener('click', confirmCreate);
|
createConfirm.addEventListener('click', confirmCreate);
|
||||||
|
|
|
||||||
|
|
@ -198,34 +198,53 @@ function loadFilesComponent(document) {
|
||||||
function makeApi() {
|
function makeApi() {
|
||||||
const externalCalls = [];
|
const externalCalls = [];
|
||||||
const contributionCalls = [];
|
const contributionCalls = [];
|
||||||
|
let restored = false;
|
||||||
|
let trashEntries = [{
|
||||||
|
originalPath: 'Docs/deleted.md',
|
||||||
|
trashPath: '.verstak/trash/files/mock/deleted.md',
|
||||||
|
trashId: 'mock-trash',
|
||||||
|
deletedAt: '2026-06-27T01:02:03Z',
|
||||||
|
originalType: 'file',
|
||||||
|
basename: 'deleted.md',
|
||||||
|
}];
|
||||||
return {
|
return {
|
||||||
externalCalls,
|
externalCalls,
|
||||||
contributionCalls,
|
contributionCalls,
|
||||||
files: {
|
files: {
|
||||||
list: async () => [
|
list: async () => {
|
||||||
{
|
const entries = [{
|
||||||
name: 'readme.md',
|
name: 'readme.md',
|
||||||
relativePath: 'Docs/readme.md',
|
relativePath: 'Docs/readme.md',
|
||||||
type: 'file',
|
type: 'file',
|
||||||
extension: 'md',
|
extension: 'md',
|
||||||
size: 12,
|
size: 12,
|
||||||
modifiedAt: '2026-06-27T00:00:00Z',
|
modifiedAt: '2026-06-27T00:00:00Z',
|
||||||
},
|
}];
|
||||||
],
|
if (restored) {
|
||||||
|
entries.push({
|
||||||
|
name: 'deleted.md',
|
||||||
|
relativePath: 'Docs/deleted.md',
|
||||||
|
type: 'file',
|
||||||
|
extension: 'md',
|
||||||
|
size: 8,
|
||||||
|
modifiedAt: '2026-06-27T01:03:00Z',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
},
|
||||||
metadata: async () => { throw new Error('not-found'); },
|
metadata: async () => { throw new Error('not-found'); },
|
||||||
readText: async () => '# Readme\n',
|
readText: async () => '# Readme\n',
|
||||||
writeText: async () => undefined,
|
writeText: async () => undefined,
|
||||||
createFolder: async () => undefined,
|
createFolder: async () => undefined,
|
||||||
move: async () => undefined,
|
move: async () => undefined,
|
||||||
trash: async () => undefined,
|
trash: async () => undefined,
|
||||||
listTrash: async () => [{
|
listTrash: async () => trashEntries.slice(),
|
||||||
originalPath: 'Docs/deleted.md',
|
restoreTrash: async (trashId) => {
|
||||||
trashPath: '.verstak/trash/files/mock/deleted.md',
|
if (trashId !== 'mock-trash') throw new Error(`unexpected restore trash id: ${trashId}`);
|
||||||
trashId: 'mock-trash',
|
restored = true;
|
||||||
deletedAt: '2026-06-27T01:02:03Z',
|
trashEntries = [];
|
||||||
originalType: 'file',
|
return 'Docs/deleted.md';
|
||||||
basename: 'deleted.md',
|
},
|
||||||
}],
|
|
||||||
openExternal: async (relativePath) => { externalCalls.push({ action: 'open', path: relativePath }); },
|
openExternal: async (relativePath) => { externalCalls.push({ action: 'open', path: relativePath }); },
|
||||||
showInFolder: async (relativePath) => { externalCalls.push({ action: 'show', path: relativePath }); },
|
showInFolder: async (relativePath) => { externalCalls.push({ action: 'show', path: relativePath }); },
|
||||||
},
|
},
|
||||||
|
|
@ -324,6 +343,14 @@ async function flush() {
|
||||||
if (!trashRow || !trashRow.textContent.includes('Docs/deleted.md')) {
|
if (!trashRow || !trashRow.textContent.includes('Docs/deleted.md')) {
|
||||||
throw new Error(`trash metadata row not rendered: ${container.textContent}`);
|
throw new Error(`trash metadata row not rendered: ${container.textContent}`);
|
||||||
}
|
}
|
||||||
|
const restoreTrash = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-restore-trash') === 'mock-trash');
|
||||||
|
if (!restoreTrash) throw new Error('restore trash button not found');
|
||||||
|
restoreTrash.click();
|
||||||
|
await flush();
|
||||||
|
const restoredRow = walk(container, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/deleted.md');
|
||||||
|
if (!restoredRow) {
|
||||||
|
throw new Error(`restored file row not rendered after restore: ${container.textContent}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('files frontend smoke passed');
|
console.log('files frontend smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue