feat: add trash restore API

This commit is contained in:
2026-06-28 22:22:47 +08:00
parent 144d844794
commit 4e62e7e019
13 changed files with 89 additions and 9 deletions
+6
View File
@@ -26,6 +26,7 @@ describe('VerstakPluginAPI contract', () => {
expect(typeof api.files.move).toBe('function');
expect(typeof api.files.trash).toBe('function');
expect(typeof api.files.listTrash).toBe('function');
expect(typeof api.files.restoreTrash).toBe('function');
expect(typeof api.files.openExternal).toBe('function');
expect(typeof api.files.showInFolder).toBe('function');
expect(typeof api.workbench.openResource).toBe('function');
@@ -251,6 +252,11 @@ describe('VerstakPluginAPI contract', () => {
await expect(api.files.listTrash()).resolves.toEqual([
expect.objectContaining({ originalPath: 'PlatformTest/two.txt', trashId: trash.trashId }),
]);
await expect(api.files.restoreTrash(trash.trashId)).resolves.toBe('PlatformTest/two.txt');
await expect(api.files.list('PlatformTest')).resolves.toEqual([
expect.objectContaining({ relativePath: 'PlatformTest/two.txt', type: 'file' }),
]);
await expect(api.files.listTrash()).resolves.toEqual([]);
});
test('files mock rejects non-canonical and reserved paths', async () => {
+2
View File
@@ -14,6 +14,7 @@ import type {
OpenResourceResult,
PluginSettings,
RegisteredContributionPoints,
RestoreTrashOptions,
TrashEntry,
TrashResult,
WriteTextOptions,
@@ -122,6 +123,7 @@ export interface VerstakPluginAPI {
move(fromRelativePath: string, toRelativePath: string, options?: MovePathOptions): Promise<void>;
trash(relativePath: string): Promise<TrashResult>;
listTrash(): Promise<TrashEntry[]>;
restoreTrash(trashId: string, options?: RestoreTrashOptions): Promise<string>;
openExternal(relativePath: string): Promise<void>;
showInFolder(relativePath: string): Promise<void>;
};
+28 -1
View File
@@ -56,6 +56,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
const eventHandlers = new Map<string, Array<(event: any) => void>>();
const files = new Map<string, { type: 'file' | 'folder'; content?: string; modifiedAt: string }>();
const trashEntries: Array<{ originalPath: string; trashPath: string; trashId: string; deletedAt: string; originalType: 'file' | 'folder'; basename: string }> = [];
const trashPayloads = new Map<string, Array<{ suffix: string; node: { type: 'file' | 'folder'; content?: string; modifiedAt: string } }>>();
files.set('', { type: 'folder', modifiedAt: new Date().toISOString() });
function normalizePath(path: string, allowRoot = false): string {
@@ -237,11 +238,37 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
originalType: node.type,
basename: baseName(path),
};
files.delete(path);
const moving = Array.from(files.entries()).filter(([candidate]) => candidate === path || candidate.startsWith(`${path}/`));
trashPayloads.set(trashId, moving.map(([candidate, movingNode]) => ({
suffix: candidate.slice(path.length),
node: { ...movingNode },
})));
moving.forEach(([candidate]) => files.delete(candidate));
trashEntries.unshift(entry);
return entry;
}),
listTrash: vi.fn(async () => trashEntries.slice()),
restoreTrash: vi.fn(async (trashId: string, options = {}) => {
const entry = trashEntries.find((item) => item.trashId === trashId);
if (!entry) throw new Error(`not-found: trash entry ${trashId}`);
const target = normalizePath((options as { targetPath?: string }).targetPath || entry.originalPath);
const overwrite = !!(options as { overwrite?: boolean }).overwrite;
if (files.has(target) && !overwrite) throw new Error(`conflict: ${target}`);
const parent = parentPath(target);
if (!files.get(parent) || files.get(parent)?.type !== 'folder') throw new Error(`parent-not-found: ${parent}`);
if (overwrite) {
Array.from(files.keys())
.filter((candidate) => candidate === target || candidate.startsWith(`${target}/`))
.forEach((candidate) => files.delete(candidate));
}
(trashPayloads.get(trashId) || []).forEach(({ suffix, node }) => {
files.set(`${target}${suffix}`, { ...node, modifiedAt: new Date().toISOString() });
});
trashPayloads.delete(trashId);
const index = trashEntries.findIndex((item) => item.trashId === trashId);
if (index >= 0) trashEntries.splice(index, 1);
return target;
}),
openExternal: vi.fn(async (relativePath: string) => {
const path = normalizePath(relativePath);
if (!files.has(path)) throw new Error(`not-found: ${path}`);
+7
View File
@@ -134,6 +134,13 @@ export interface MovePathOptions {
overwrite?: boolean;
}
export interface RestoreTrashOptions {
/** Restore to another vault-relative path instead of the original path. */
targetPath?: string;
/** Replace an existing target path. Hosts reject conflicts by default. */
overwrite?: boolean;
}
export interface TrashResult {
originalPath: string;
trashPath: string;