feat: restore files from trash
This commit is contained in:
@@ -272,6 +272,12 @@ export function createPluginAPI(pluginId) {
|
||||
return App.ListVaultTrash(pluginId);
|
||||
});
|
||||
},
|
||||
restoreTrash: function(trashId, options) {
|
||||
assertActive('files.restoreTrash(' + trashId + ')');
|
||||
return callBackend(pluginId, 'files.restoreTrash(' + trashId + ')', function() {
|
||||
return App.RestoreVaultTrash(pluginId, trashId, options || {});
|
||||
});
|
||||
},
|
||||
openExternal: function(relativePath) {
|
||||
assertActive('files.openExternal(' + relativePath + ')');
|
||||
return callBackendErrorString(pluginId, 'files.openExternal(' + relativePath + ')', function() {
|
||||
|
||||
@@ -219,6 +219,7 @@
|
||||
var vaultFiles = makeDefaultVaultFiles();
|
||||
var externalOpens = [];
|
||||
var trashEntries = [];
|
||||
var trashPayloads = {};
|
||||
window.__wailsMockExternalOpens = [];
|
||||
var workspaceTree = makeDefaultWorkspaceTree();
|
||||
var reloadResponseMode = 'tuple';
|
||||
@@ -1345,6 +1346,9 @@
|
||||
var trashPath = '.verstak/trash/files/' + trashId + '/' + baseName(norm.path);
|
||||
var originalType = vaultFiles[norm.path].type || 'file';
|
||||
var moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; });
|
||||
trashPayloads[trashId] = moving.map(function (path) {
|
||||
return { suffix: path.slice(norm.path.length), entry: Object.assign({}, 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) };
|
||||
trashEntries.unshift(entry);
|
||||
@@ -1355,6 +1359,29 @@
|
||||
if (err) return Promise.resolve([[], err]);
|
||||
return Promise.resolve([trashEntries.slice(), '']);
|
||||
},
|
||||
RestoreVaultTrash: function (pluginId, trashId, options) {
|
||||
var deleteErr = requirePluginPermission(pluginId, 'files.delete');
|
||||
if (deleteErr) return Promise.resolve(['', deleteErr]);
|
||||
var writeErr = requirePluginPermission(pluginId, 'files.write');
|
||||
if (writeErr) return Promise.resolve(['', writeErr]);
|
||||
options = options || {};
|
||||
var entry = trashEntries.find(function (item) { return item.trashId === trashId; });
|
||||
if (!entry) return Promise.resolve(['', 'not-found: trash entry ' + trashId]);
|
||||
var target = normalizeVaultPath(options.targetPath || entry.originalPath, false);
|
||||
if (target.error) return Promise.resolve(['', target.error]);
|
||||
if (vaultFiles[target.path] && !options.overwrite) return Promise.resolve(['', 'conflict: ' + target.path]);
|
||||
var parent = parentPath(target.path);
|
||||
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve(['', 'parent-not-found: ' + parent]);
|
||||
if (options.overwrite) {
|
||||
Object.keys(vaultFiles).filter(function (path) { return path === target.path || path.indexOf(target.path + '/') === 0; }).forEach(function (path) { delete vaultFiles[path]; });
|
||||
}
|
||||
(trashPayloads[trashId] || []).forEach(function (item) {
|
||||
vaultFiles[target.path + item.suffix] = Object.assign({}, item.entry, { modifiedAt: new Date().toISOString() });
|
||||
});
|
||||
delete trashPayloads[trashId];
|
||||
trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; });
|
||||
return Promise.resolve([target.path, '']);
|
||||
},
|
||||
OpenVaultPathExternal: function (pluginId, relativePath) {
|
||||
var err = requirePluginPermission(pluginId, 'files.openExternal');
|
||||
if (err) return Promise.resolve(err);
|
||||
@@ -1722,6 +1749,8 @@
|
||||
pluginSettings = { 'verstak.platform-test': { savedText: 'initial value' } };
|
||||
vaultFiles = makeDefaultVaultFiles();
|
||||
externalOpens = [];
|
||||
trashEntries = [];
|
||||
trashPayloads = {};
|
||||
window.__wailsMockExternalOpens = [];
|
||||
workspaceTree = makeDefaultWorkspaceTree();
|
||||
reloadResponseMode = 'tuple';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const calls = [];
|
||||
|
||||
globalThis.window = {
|
||||
__VERSTAK_PLUGIN_REGISTRY__: {},
|
||||
__VERSTAK_EVENT_HANDLERS__: {},
|
||||
__VERSTAK_COMMAND_HANDLERS__: {},
|
||||
go: {
|
||||
api: {
|
||||
App: {
|
||||
RestoreVaultTrash: (pluginId, trashId, options) => {
|
||||
calls.push({ pluginId, trashId, options });
|
||||
return Promise.resolve(['Docs/restored.txt', '']);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
globalThis.__mockApp = window.go.api.App;
|
||||
|
||||
const sourcePath = path.resolve('frontend/src/lib/plugin-host/VerstakPluginAPI.js');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8')
|
||||
.replace("import * as App from '../../../wailsjs/go/api/App';", 'const App = globalThis.__mockApp;');
|
||||
const tempPath = path.resolve('/tmp/verstak-plugin-api-files-test.mjs');
|
||||
fs.writeFileSync(tempPath, source);
|
||||
|
||||
const apiModule = await import(pathToFileURL(tempPath).href + '?t=' + Date.now());
|
||||
const api = apiModule.createPluginAPI('verstak.files');
|
||||
|
||||
if (!api.files || typeof api.files.restoreTrash !== 'function') {
|
||||
throw new Error('api.files.restoreTrash is missing');
|
||||
}
|
||||
|
||||
const restored = await api.files.restoreTrash('trash-1', { overwrite: true });
|
||||
if (restored !== 'Docs/restored.txt') {
|
||||
throw new Error(`unexpected restore result: ${JSON.stringify(restored)}`);
|
||||
}
|
||||
if (calls.length !== 1 || calls[0].pluginId !== 'verstak.files' || calls[0].trashId !== 'trash-1' || calls[0].options.overwrite !== true) {
|
||||
throw new Error(`unexpected RestoreVaultTrash call: ${JSON.stringify(calls)}`);
|
||||
}
|
||||
|
||||
console.log('plugin api files smoke passed');
|
||||
Vendored
+4
-2
@@ -66,6 +66,8 @@ export function ListPluginCapabilities(arg1:string):Promise<Array<capability.Ent
|
||||
|
||||
export function ListVaultFiles(arg1:string,arg2:string):Promise<Array<files.FileEntry>|string>;
|
||||
|
||||
export function ListVaultTrash(arg1:string):Promise<Array<files.TrashEntry>|string>;
|
||||
|
||||
export function ListWorkspaces():Promise<Array<workspace.Workspace>|string>;
|
||||
|
||||
export function MoveVaultPath(arg1:string,arg2:string,arg3:string,arg4:files.MoveOptions):Promise<string>;
|
||||
@@ -106,6 +108,8 @@ export function RecordDesiredPlugin(arg1:string,arg2:string,arg3:string):Promise
|
||||
|
||||
export function ReloadPlugins():Promise<number|string>;
|
||||
|
||||
export function RestoreVaultTrash(arg1:string,arg2:string,arg3:files.RestoreOptions):Promise<string|string>;
|
||||
|
||||
export function RenameWorkspace(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function RenameWorkspaceNode(arg1:string,arg2:string):Promise<string>;
|
||||
@@ -126,8 +130,6 @@ export function SubscribePluginEvent(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function TrashVaultPath(arg1:string,arg2:string):Promise<files.TrashResult|string>;
|
||||
|
||||
export function ListVaultTrash(arg1:string):Promise<any[]|string>;
|
||||
|
||||
export function TrashWorkspace(arg1:string):Promise<workspace.TrashResult|string>;
|
||||
|
||||
export function UpdateAppSettings(arg1:Record<string, any>):Promise<string>;
|
||||
|
||||
@@ -118,6 +118,10 @@ export function ListVaultFiles(arg1, arg2) {
|
||||
return window['go']['api']['App']['ListVaultFiles'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ListVaultTrash(arg1) {
|
||||
return window['go']['api']['App']['ListVaultTrash'](arg1);
|
||||
}
|
||||
|
||||
export function ListWorkspaces() {
|
||||
return window['go']['api']['App']['ListWorkspaces']();
|
||||
}
|
||||
@@ -198,6 +202,10 @@ export function ReloadPlugins() {
|
||||
return window['go']['api']['App']['ReloadPlugins']();
|
||||
}
|
||||
|
||||
export function RestoreVaultTrash(arg1, arg2, arg3) {
|
||||
return window['go']['api']['App']['RestoreVaultTrash'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function RenameWorkspace(arg1, arg2) {
|
||||
return window['go']['api']['App']['RenameWorkspace'](arg1, arg2);
|
||||
}
|
||||
@@ -238,10 +246,6 @@ export function TrashVaultPath(arg1, arg2) {
|
||||
return window['go']['api']['App']['TrashVaultPath'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ListVaultTrash(arg1) {
|
||||
return window['go']['api']['App']['ListVaultTrash'](arg1);
|
||||
}
|
||||
|
||||
export function TrashWorkspace(arg1) {
|
||||
return window['go']['api']['App']['TrashWorkspace'](arg1);
|
||||
}
|
||||
|
||||
+109
-18
@@ -1,5 +1,51 @@
|
||||
export namespace api {
|
||||
|
||||
export class FlatContextMenuEntry {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
label: string;
|
||||
context: string;
|
||||
group?: string;
|
||||
capability?: string;
|
||||
handler?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FlatContextMenuEntry(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.pluginId = source["pluginId"];
|
||||
this.id = source["id"];
|
||||
this.label = source["label"];
|
||||
this.context = source["context"];
|
||||
this.group = source["group"];
|
||||
this.capability = source["capability"];
|
||||
this.handler = source["handler"];
|
||||
}
|
||||
}
|
||||
export class FlatAction {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
capability?: string;
|
||||
handler?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FlatAction(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.pluginId = source["pluginId"];
|
||||
this.id = source["id"];
|
||||
this.label = source["label"];
|
||||
this.icon = source["icon"];
|
||||
this.capability = source["capability"];
|
||||
this.handler = source["handler"];
|
||||
}
|
||||
}
|
||||
export class FlatWorkspaceItem {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
@@ -80,24 +126,6 @@ export namespace api {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class FlatSearchProvider {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
label: string;
|
||||
handler: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FlatSearchProvider(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.pluginId = source["pluginId"];
|
||||
this.id = source["id"];
|
||||
this.label = source["label"];
|
||||
this.handler = source["handler"];
|
||||
}
|
||||
}
|
||||
export class FlatStatusBarItem {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
@@ -160,6 +188,24 @@ export namespace api {
|
||||
this.component = source["component"];
|
||||
}
|
||||
}
|
||||
export class FlatSearchProvider {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
label: string;
|
||||
handler: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FlatSearchProvider(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.pluginId = source["pluginId"];
|
||||
this.id = source["id"];
|
||||
this.label = source["label"];
|
||||
this.handler = source["handler"];
|
||||
}
|
||||
}
|
||||
export class FlatCommand {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
@@ -209,6 +255,9 @@ export namespace api {
|
||||
statusBarItems: FlatStatusBarItem[];
|
||||
openProviders: FlatOpenProvider[];
|
||||
workspaceItems: FlatWorkspaceItem[];
|
||||
fileActions: FlatAction[];
|
||||
noteActions: FlatAction[];
|
||||
contextMenuEntries: FlatContextMenuEntry[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ContributionSummary(source);
|
||||
@@ -224,6 +273,9 @@ export namespace api {
|
||||
this.statusBarItems = this.convertValues(source["statusBarItems"], FlatStatusBarItem);
|
||||
this.openProviders = this.convertValues(source["openProviders"], FlatOpenProvider);
|
||||
this.workspaceItems = this.convertValues(source["workspaceItems"], FlatWorkspaceItem);
|
||||
this.fileActions = this.convertValues(source["fileActions"], FlatAction);
|
||||
this.noteActions = this.convertValues(source["noteActions"], FlatAction);
|
||||
this.contextMenuEntries = this.convertValues(source["contextMenuEntries"], FlatContextMenuEntry);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -252,6 +304,9 @@ export namespace api {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export class SyncStatusDTO {
|
||||
configured: boolean;
|
||||
serverUrl: string;
|
||||
@@ -390,6 +445,42 @@ export namespace files {
|
||||
this.overwrite = source["overwrite"];
|
||||
}
|
||||
}
|
||||
export class RestoreOptions {
|
||||
targetPath?: string;
|
||||
overwrite: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new RestoreOptions(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.targetPath = source["targetPath"];
|
||||
this.overwrite = source["overwrite"];
|
||||
}
|
||||
}
|
||||
export class TrashEntry {
|
||||
originalPath: string;
|
||||
trashPath: string;
|
||||
trashId: string;
|
||||
deletedAt: string;
|
||||
originalType: string;
|
||||
basename: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new TrashEntry(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.originalPath = source["originalPath"];
|
||||
this.trashPath = source["trashPath"];
|
||||
this.trashId = source["trashId"];
|
||||
this.deletedAt = source["deletedAt"];
|
||||
this.originalType = source["originalType"];
|
||||
this.basename = source["basename"];
|
||||
}
|
||||
}
|
||||
export class TrashResult {
|
||||
originalPath: string;
|
||||
trashPath: string;
|
||||
|
||||
Reference in New Issue
Block a user