feat: bridge permanent trash deletion to plugins
This commit is contained in:
parent
8445ea11db
commit
3f27ba79a4
|
|
@ -85,7 +85,7 @@ Frontend bundles are mounted with a plugin-scoped API created by
|
||||||
- `capabilities.list/get/has`
|
- `capabilities.list/get/has`
|
||||||
- `commands.register/execute` for handlers declared in `contributes.commands`
|
- `commands.register/execute` for handlers declared in `contributes.commands`
|
||||||
- `events.publish/subscribe` using the bundled frontend event bus
|
- `events.publish/subscribe` using the bundled frontend event bus
|
||||||
- `files.list/metadata/readText/readBytes/writeText/createFolder/move/trash/openExternal/showInFolder`
|
- `files.list/metadata/readText/readBytes/writeText/createFolder/move/trash/listTrash/restoreTrash/deleteTrash/openExternal/showInFolder`
|
||||||
for canonical vault-relative slash paths guarded by `files.read`,
|
for canonical vault-relative slash paths guarded by `files.read`,
|
||||||
`files.write`, `files.delete`, and `files.openExternal`. Backslashes,
|
`files.write`, `files.delete`, and `files.openExternal`. Backslashes,
|
||||||
Windows absolute paths, UNC paths, traversal, `.verstak` variants, and
|
Windows absolute paths, UNC paths, traversal, `.verstak` variants, and
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,29 @@ test.describe('D: Plugin API bridge', () => {
|
||||||
expect(result.disconnected.statusLabel).toBe('disabled');
|
expect(result.disconnected.statusLabel).toBe('disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('files API permanently deletes an existing trash entry through the bridge', async ({ page }) => {
|
||||||
|
const result = await page.evaluate(async () => {
|
||||||
|
const api = window.createPluginAPI('verstak.files');
|
||||||
|
await api.files.writeText('Project/bridge-trash.txt', 'remove me', { createIfMissing: true });
|
||||||
|
const trash = await api.files.trash('Project/bridge-trash.txt');
|
||||||
|
const before = await api.files.listTrash();
|
||||||
|
await api.files.deleteTrash(trash.trashId);
|
||||||
|
const after = await api.files.listTrash();
|
||||||
|
let readError = '';
|
||||||
|
try {
|
||||||
|
await api.files.readText('Project/bridge-trash.txt');
|
||||||
|
} catch (error) {
|
||||||
|
readError = String(error && error.message || error);
|
||||||
|
}
|
||||||
|
api.dispose();
|
||||||
|
return { before, after, readError };
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.before).toHaveLength(1);
|
||||||
|
expect(result.after).toEqual([]);
|
||||||
|
expect(result.readError).toContain('not-found: Project/bridge-trash.txt');
|
||||||
|
});
|
||||||
|
|
||||||
test('backend plugin events are dispatched to subscribed frontend handlers', async ({ page }) => {
|
test('backend plugin events are dispatched to subscribed frontend handlers', async ({ page }) => {
|
||||||
const result = await page.evaluate(async () => {
|
const result = await page.evaluate(async () => {
|
||||||
const api = window.createPluginAPI('verstak.platform-test');
|
const api = window.createPluginAPI('verstak.platform-test');
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,12 @@ export function createPluginAPI(pluginId) {
|
||||||
return App.RestoreVaultTrash(pluginId, trashId, options || {});
|
return App.RestoreVaultTrash(pluginId, trashId, options || {});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
deleteTrash: function(trashId) {
|
||||||
|
assertActive('files.deleteTrash(' + trashId + ')');
|
||||||
|
return callBackendErrorString(pluginId, 'files.deleteTrash(' + trashId + ')', function() {
|
||||||
|
return App.DeleteVaultTrash(pluginId, trashId);
|
||||||
|
});
|
||||||
|
},
|
||||||
openExternal: function(relativePath) {
|
openExternal: function(relativePath) {
|
||||||
assertActive('files.openExternal(' + relativePath + ')');
|
assertActive('files.openExternal(' + relativePath + ')');
|
||||||
return callBackendErrorString(pluginId, 'files.openExternal(' + relativePath + ')', function() {
|
return callBackendErrorString(pluginId, 'files.openExternal(' + relativePath + ')', function() {
|
||||||
|
|
|
||||||
|
|
@ -2608,13 +2608,15 @@
|
||||||
if (!vaultFiles[norm.path]) return Promise.resolve([{}, 'not-found: ' + norm.path]);
|
if (!vaultFiles[norm.path]) return Promise.resolve([{}, 'not-found: ' + norm.path]);
|
||||||
var trashId = 'mock-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
var trashId = 'mock-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
||||||
var trashPath = '.verstak/trash/files/' + trashId + '/' + baseName(norm.path);
|
var trashPath = '.verstak/trash/files/' + trashId + '/' + baseName(norm.path);
|
||||||
var originalType = vaultFiles[norm.path].type || 'file';
|
var originalNode = vaultFiles[norm.path];
|
||||||
|
var originalType = originalNode.type || 'file';
|
||||||
|
var originalSize = originalType === 'file' ? String(originalNode.content || '').length : 0;
|
||||||
var moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; });
|
var moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; });
|
||||||
trashPayloads[trashId] = moving.map(function (path) {
|
trashPayloads[trashId] = moving.map(function (path) {
|
||||||
return { suffix: path.slice(norm.path.length), entry: Object.assign({}, vaultFiles[path]) };
|
return { suffix: path.slice(norm.path.length), entry: Object.assign({}, vaultFiles[path]) };
|
||||||
});
|
});
|
||||||
moving.forEach(function (path) { delete vaultFiles[path]; });
|
moving.forEach(function (path) { delete vaultFiles[path]; });
|
||||||
var entry = { originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString(), originalType: originalType, basename: baseName(norm.path) };
|
var entry = { originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString(), originalType: originalType, basename: baseName(norm.path), size: originalSize };
|
||||||
trashEntries.unshift(entry);
|
trashEntries.unshift(entry);
|
||||||
return Promise.resolve([entry, '']);
|
return Promise.resolve([entry, '']);
|
||||||
},
|
},
|
||||||
|
|
@ -2646,6 +2648,15 @@
|
||||||
trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; });
|
trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; });
|
||||||
return Promise.resolve([target.path, '']);
|
return Promise.resolve([target.path, '']);
|
||||||
},
|
},
|
||||||
|
DeleteVaultTrash: function (pluginId, trashId) {
|
||||||
|
var err = requirePluginPermission(pluginId, 'files.delete');
|
||||||
|
if (err) return Promise.resolve(err);
|
||||||
|
var entry = trashEntries.find(function (item) { return item.trashId === trashId; });
|
||||||
|
if (!entry) return Promise.resolve('not-found: trash entry ' + trashId);
|
||||||
|
delete trashPayloads[trashId];
|
||||||
|
trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; });
|
||||||
|
return Promise.resolve('');
|
||||||
|
},
|
||||||
OpenVaultPathExternal: function (pluginId, relativePath) {
|
OpenVaultPathExternal: function (pluginId, relativePath) {
|
||||||
var err = requirePluginPermission(pluginId, 'files.openExternal');
|
var err = requirePluginPermission(pluginId, 'files.openExternal');
|
||||||
if (err) return Promise.resolve(err);
|
if (err) return Promise.resolve(err);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export function CreateWorkspace(arg1:string,arg2:string):Promise<workspace.Works
|
||||||
|
|
||||||
export function CreateWorkspaceNode(arg1:string,arg2:string,arg3:string):Promise<Record<string, any>>;
|
export function CreateWorkspaceNode(arg1:string,arg2:string,arg3:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function DeleteVaultTrash(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function DisablePlugin(arg1:string):Promise<string>;
|
export function DisablePlugin(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
||||||
|
|
@ -80,6 +82,10 @@ export function OpenVaultPathExternal(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function OpenWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
export function OpenWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
||||||
|
|
||||||
|
export function PluginBrowserReceiverPairing(arg1:string):Promise<Record<string, string>|string>;
|
||||||
|
|
||||||
|
export function PluginRotateBrowserReceiverToken(arg1:string):Promise<Record<string, string>|string>;
|
||||||
|
|
||||||
export function PluginSecretsCopyLink(arg1:string,arg2:string):Promise<string|string>;
|
export function PluginSecretsCopyLink(arg1:string,arg2:string):Promise<string|string>;
|
||||||
|
|
||||||
export function PluginSecretsDelete(arg1:string,arg2:string):Promise<string>;
|
export function PluginSecretsDelete(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
@ -94,10 +100,6 @@ export function PluginSecretsUnlock(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function PluginSecretsWrite(arg1:string,arg2:Record<string, any>):Promise<Record<string, any>|string>;
|
export function PluginSecretsWrite(arg1:string,arg2:Record<string, any>):Promise<Record<string, any>|string>;
|
||||||
|
|
||||||
export function PluginBrowserReceiverPairing(arg1:string):Promise<Record<string, string>|string>;
|
|
||||||
|
|
||||||
export function PluginRotateBrowserReceiverToken(arg1:string):Promise<Record<string, string>|string>;
|
|
||||||
|
|
||||||
export function PluginSyncConfigure(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>;
|
export function PluginSyncConfigure(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>;
|
||||||
|
|
||||||
export function PluginSyncDisconnect(arg1:string):Promise<string>;
|
export function PluginSyncDisconnect(arg1:string):Promise<string>;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ export function CreateWorkspaceNode(arg1, arg2, arg3) {
|
||||||
return window['go']['api']['App']['CreateWorkspaceNode'](arg1, arg2, arg3);
|
return window['go']['api']['App']['CreateWorkspaceNode'](arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DeleteVaultTrash(arg1, arg2) {
|
||||||
|
return window['go']['api']['App']['DeleteVaultTrash'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
export function DisablePlugin(arg1) {
|
export function DisablePlugin(arg1) {
|
||||||
return window['go']['api']['App']['DisablePlugin'](arg1);
|
return window['go']['api']['App']['DisablePlugin'](arg1);
|
||||||
}
|
}
|
||||||
|
|
@ -146,6 +150,14 @@ export function OpenWorkbenchResource(arg1, arg2) {
|
||||||
return window['go']['api']['App']['OpenWorkbenchResource'](arg1, arg2);
|
return window['go']['api']['App']['OpenWorkbenchResource'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PluginBrowserReceiverPairing(arg1) {
|
||||||
|
return window['go']['api']['App']['PluginBrowserReceiverPairing'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PluginRotateBrowserReceiverToken(arg1) {
|
||||||
|
return window['go']['api']['App']['PluginRotateBrowserReceiverToken'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function PluginSecretsCopyLink(arg1, arg2) {
|
export function PluginSecretsCopyLink(arg1, arg2) {
|
||||||
return window['go']['api']['App']['PluginSecretsCopyLink'](arg1, arg2);
|
return window['go']['api']['App']['PluginSecretsCopyLink'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
@ -174,14 +186,6 @@ export function PluginSecretsWrite(arg1, arg2) {
|
||||||
return window['go']['api']['App']['PluginSecretsWrite'](arg1, arg2);
|
return window['go']['api']['App']['PluginSecretsWrite'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PluginBrowserReceiverPairing(arg1) {
|
|
||||||
return window['go']['api']['App']['PluginBrowserReceiverPairing'](arg1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PluginRotateBrowserReceiverToken(arg1) {
|
|
||||||
return window['go']['api']['App']['PluginRotateBrowserReceiverToken'](arg1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PluginSyncConfigure(arg1, arg2, arg3, arg4) {
|
export function PluginSyncConfigure(arg1, arg2, arg3, arg4) {
|
||||||
return window['go']['api']['App']['PluginSyncConfigure'](arg1, arg2, arg3, arg4);
|
return window['go']['api']['App']['PluginSyncConfigure'](arg1, arg2, arg3, arg4);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -484,6 +484,7 @@ export namespace files {
|
||||||
deletedAt: string;
|
deletedAt: string;
|
||||||
originalType: string;
|
originalType: string;
|
||||||
basename: string;
|
basename: string;
|
||||||
|
size: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new TrashEntry(source);
|
return new TrashEntry(source);
|
||||||
|
|
@ -497,6 +498,7 @@ export namespace files {
|
||||||
this.deletedAt = source["deletedAt"];
|
this.deletedAt = source["deletedAt"];
|
||||||
this.originalType = source["originalType"];
|
this.originalType = source["originalType"];
|
||||||
this.basename = source["basename"];
|
this.basename = source["basename"];
|
||||||
|
this.size = source["size"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class TrashResult {
|
export class TrashResult {
|
||||||
|
|
@ -504,6 +506,7 @@ export namespace files {
|
||||||
trashPath: string;
|
trashPath: string;
|
||||||
trashId: string;
|
trashId: string;
|
||||||
deletedAt: string;
|
deletedAt: string;
|
||||||
|
size: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new TrashResult(source);
|
return new TrashResult(source);
|
||||||
|
|
@ -515,6 +518,7 @@ export namespace files {
|
||||||
this.trashPath = source["trashPath"];
|
this.trashPath = source["trashPath"];
|
||||||
this.trashId = source["trashId"];
|
this.trashId = source["trashId"];
|
||||||
this.deletedAt = source["deletedAt"];
|
this.deletedAt = source["deletedAt"];
|
||||||
|
this.size = source["size"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class WriteOptions {
|
export class WriteOptions {
|
||||||
|
|
@ -1315,4 +1319,3 @@ export namespace workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue