feat: add trash restore API
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user