fix: remove flatname workaround, store Files as real FileNode #379
This commit is contained in:
+108
-164
@@ -98,44 +98,21 @@ interface FileState {
|
||||
|
||||
const DIRECTORY_TYPES = new Set(['d', 'application/x-directory', 'text/directory', 'httpd/unix-directory', 'inode/directory']);
|
||||
|
||||
// Stalwart rejects "/" in file names, so we use Unicode DIVISION SLASH as the
|
||||
// path separator when encoding folder hierarchy into flat file names.
|
||||
const PATH_SEP = '\u2215'; // ∕
|
||||
|
||||
function isDirectoryType(type: string | undefined): boolean {
|
||||
if (!type) return false;
|
||||
return DIRECTORY_TYPES.has(type) || type.includes('directory');
|
||||
}
|
||||
|
||||
// Convert currentPath to a server-side name prefix for filtering
|
||||
// "/" -> "", "/test" -> "test∕", "/test/sub" -> "test∕sub∕"
|
||||
function getPathPrefix(currentPath: string): string {
|
||||
if (currentPath === '/') return '';
|
||||
return currentPath.slice(1).replace(/\//g, PATH_SEP) + PATH_SEP;
|
||||
// Direct children of a given parent in the FileNode hierarchy.
|
||||
function childrenOf(nodes: FileNode[], parentId: string | null): FileNode[] {
|
||||
return nodes.filter(n => (n.parentId ?? null) === parentId);
|
||||
}
|
||||
|
||||
// Filter nodes to only direct children of a path prefix
|
||||
function filterNodesByPrefix(nodes: FileNode[], prefix: string): FileNode[] {
|
||||
if (prefix === '') {
|
||||
// Root: nodes whose names have no PATH_SEP
|
||||
return nodes.filter(n => !n.name.includes(PATH_SEP));
|
||||
}
|
||||
// Subfolder: nodes starting with prefix, with no additional PATH_SEP after the prefix
|
||||
return nodes.filter(n => {
|
||||
if (!n.name.startsWith(prefix)) return false;
|
||||
const remaining = n.name.slice(prefix.length);
|
||||
return remaining.length > 0 && !remaining.includes(PATH_SEP);
|
||||
});
|
||||
}
|
||||
|
||||
function nodeToResource(node: FileNode, pathPrefix: string = ''): FileResource {
|
||||
const displayName = pathPrefix && node.name.startsWith(pathPrefix)
|
||||
? node.name.slice(pathPrefix.length)
|
||||
: node.name;
|
||||
function nodeToResource(node: FileNode): FileResource {
|
||||
const isDir = isDirectoryType(node.type);
|
||||
return {
|
||||
id: node.id,
|
||||
name: displayName,
|
||||
name: node.name,
|
||||
serverName: node.name,
|
||||
isDirectory: isDir,
|
||||
contentType: isDir ? '' : node.type,
|
||||
@@ -146,6 +123,29 @@ function nodeToResource(node: FileNode, pathPrefix: string = ''): FileResource {
|
||||
};
|
||||
}
|
||||
|
||||
function sortResources(resources: FileResource[]): FileResource[] {
|
||||
// Directories first, then alphabetically.
|
||||
return resources.sort((a, b) => {
|
||||
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve a display path (e.g. "/Documents/Notes") to a FileNode id by walking
|
||||
// the hierarchy from the root. Returns null for the root, or undefined if any
|
||||
// segment can't be found.
|
||||
function resolvePathToId(nodes: FileNode[], path: string): string | null | undefined {
|
||||
if (path === '/' || path === '') return null;
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
let parentId: string | null = null;
|
||||
for (const segment of segments) {
|
||||
const match: FileNode | undefined = childrenOf(nodes, parentId).find(n => n.name === segment && isDirectoryType(n.type));
|
||||
if (!match) return undefined;
|
||||
parentId = match.id;
|
||||
}
|
||||
return parentId;
|
||||
}
|
||||
|
||||
function getUniqueName(name: string, existingNames: Set<string>): string {
|
||||
if (!existingNames.has(name)) return name;
|
||||
const dotIndex = name.lastIndexOf('.');
|
||||
@@ -246,16 +246,11 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
try { localStorage.setItem('files-path-stack', JSON.stringify(newStack)); } catch { /* ignore */ }
|
||||
|
||||
try {
|
||||
// Always fetch all nodes from root - Stalwart doesn't support parentId nesting
|
||||
const allNodes = await client.listFileNodes(null);
|
||||
const prefix = getPathPrefix(newPath);
|
||||
const filteredNodes = filterNodesByPrefix(allNodes, prefix);
|
||||
const resources = filteredNodes.map(n => nodeToResource(n, prefix));
|
||||
// Sort: directories first, then alphabetically
|
||||
resources.sort((a, b) => {
|
||||
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
// Fetch the whole tree once and select the current parent's direct
|
||||
// children locally. Hierarchy is derived from parentId links, exactly as
|
||||
// the JMAP FileNode spec intends (issue #379).
|
||||
const allNodes = await client.listAllFileNodes();
|
||||
const resources = sortResources(childrenOf(allNodes, parentId).map(nodeToResource));
|
||||
|
||||
// Prune recent files whose backing node no longer exists on the server
|
||||
const { recentFiles } = get();
|
||||
@@ -295,7 +290,18 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallback: if we can't resolve, stay at current location
|
||||
// Fallback: resolve the path against the live hierarchy (covers favorites
|
||||
// and recent paths outside the current breadcrumb stack).
|
||||
const { client } = get();
|
||||
if (client) {
|
||||
try {
|
||||
const allNodes = await client.listAllFileNodes();
|
||||
const id = resolvePathToId(allNodes, path);
|
||||
if (id !== undefined) {
|
||||
await navigate(id, segments[segments.length - 1]);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
},
|
||||
|
||||
navigateUp: async () => {
|
||||
@@ -312,21 +318,17 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
},
|
||||
|
||||
createDirectory: async (name: string) => {
|
||||
const { client, currentPath, refresh } = get();
|
||||
const { client, currentParentId, refresh } = get();
|
||||
if (!client) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
const fullName = prefix + name;
|
||||
await client.createFileDirectory(fullName, null);
|
||||
await client.createFileDirectory(name, currentParentId);
|
||||
await refresh();
|
||||
},
|
||||
|
||||
uploadFile: async (file: File) => {
|
||||
const { client, currentPath } = get();
|
||||
const { client, currentParentId } = get();
|
||||
if (!client) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
const fullName = prefix + file.name;
|
||||
const abortController = new AbortController();
|
||||
set({ uploadAbortController: abortController });
|
||||
set({ uploadProgress: { name: file.name, loaded: 0, total: file.size, current: 1, totalFiles: 1 } });
|
||||
@@ -341,17 +343,16 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
});
|
||||
if (abortController.signal.aborted) return;
|
||||
set({ uploadProgress: { name: file.name, loaded: file.size, total: file.size, current: 1, totalFiles: 1 } });
|
||||
await client.createFileNode(fullName, blobId, type || file.type || 'application/octet-stream', file.size, null);
|
||||
await client.createFileNode(file.name, blobId, type || file.type || 'application/octet-stream', file.size, currentParentId);
|
||||
} finally {
|
||||
set({ uploadProgress: null, uploadAbortController: null });
|
||||
}
|
||||
},
|
||||
|
||||
uploadFiles: async (files: File[]) => {
|
||||
const { client, currentPath, resources } = get();
|
||||
const { client, currentParentId, resources } = get();
|
||||
if (!client) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
const abortController = new AbortController();
|
||||
set({ uploadAbortController: abortController });
|
||||
const totalFiles = files.length;
|
||||
@@ -362,7 +363,6 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
const file = files[i];
|
||||
const uniqueName = getUniqueName(file.name, existingNames);
|
||||
existingNames.add(uniqueName);
|
||||
const fullName = prefix + uniqueName;
|
||||
set({ uploadProgress: { name: file.name, loaded: 0, total: file.size, current: i + 1, totalFiles } });
|
||||
|
||||
try {
|
||||
@@ -375,7 +375,7 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
});
|
||||
if (abortController.signal.aborted) break;
|
||||
set({ uploadProgress: { name: file.name, loaded: file.size, total: file.size, current: i + 1, totalFiles } });
|
||||
await client.createFileNode(fullName, blobId, type || file.type || 'application/octet-stream', file.size, null);
|
||||
await client.createFileNode(uniqueName, blobId, type || file.type || 'application/octet-stream', file.size, currentParentId);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') break;
|
||||
set({ uploadProgress: null, uploadAbortController: null });
|
||||
@@ -395,15 +395,15 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
},
|
||||
|
||||
uploadFolder: async (files: File[]) => {
|
||||
const { client, currentPath } = get();
|
||||
const { client, currentParentId } = get();
|
||||
if (!client || files.length === 0) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
const abortController = new AbortController();
|
||||
set({ uploadAbortController: abortController });
|
||||
const totalFiles = files.length;
|
||||
|
||||
// Collect unique directory paths from the uploaded folder structure
|
||||
// Collect unique directory paths (relative to the dropped folder) and
|
||||
// create them as real nested directories, mapping each path to its node id.
|
||||
const dirs = new Set<string>();
|
||||
for (const file of files) {
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name;
|
||||
@@ -413,25 +413,35 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
}
|
||||
}
|
||||
|
||||
// Create directories as flat entries with prefixed names (no parentId nesting)
|
||||
// Convert "/" separators from webkitRelativePath to PATH_SEP (∕) for server names
|
||||
// Map a directory path to its created node id. Root ('') maps to the
|
||||
// current folder we are uploading into.
|
||||
const dirIds = new Map<string, string | null>();
|
||||
dirIds.set('', currentParentId);
|
||||
|
||||
const sortedDirs = [...dirs].sort((a, b) => a.split('/').length - b.split('/').length);
|
||||
for (const dir of sortedDirs) {
|
||||
if (abortController.signal.aborted) break;
|
||||
const fullDirName = prefix + dir.replace(/\//g, PATH_SEP);
|
||||
const slash = dir.lastIndexOf('/');
|
||||
const parentPath = slash >= 0 ? dir.slice(0, slash) : '';
|
||||
const dirName = slash >= 0 ? dir.slice(slash + 1) : dir;
|
||||
const parentId = dirIds.get(parentPath) ?? currentParentId;
|
||||
try {
|
||||
await client.createFileDirectory(fullDirName, null);
|
||||
const created = await client.createFileDirectory(dirName, parentId);
|
||||
dirIds.set(dir, created.id);
|
||||
} catch {
|
||||
// Directory may already exist - ignore
|
||||
// Directory may already exist - leave it unmapped; files fall back to
|
||||
// the closest known parent below.
|
||||
}
|
||||
}
|
||||
|
||||
// Upload files with full prefixed paths
|
||||
// Upload files into their containing directory.
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (abortController.signal.aborted) break;
|
||||
const file = files[i];
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name;
|
||||
const fullName = prefix + relativePath.replace(/\//g, PATH_SEP);
|
||||
const slash = relativePath.lastIndexOf('/');
|
||||
const dirPath = slash >= 0 ? relativePath.slice(0, slash) : '';
|
||||
const parentId = dirIds.get(dirPath) ?? currentParentId;
|
||||
|
||||
set({ uploadProgress: { name: relativePath, loaded: 0, total: file.size, current: i + 1, totalFiles } });
|
||||
|
||||
@@ -439,7 +449,7 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
const { blobId, type } = await client.uploadBlob(file);
|
||||
if (abortController.signal.aborted) break;
|
||||
set({ uploadProgress: { name: relativePath, loaded: file.size, total: file.size, current: i + 1, totalFiles } });
|
||||
await client.createFileNode(fullName, blobId, type || file.type || 'application/octet-stream', file.size, null);
|
||||
await client.createFileNode(file.name, blobId, type || file.type || 'application/octet-stream', file.size, parentId);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') break;
|
||||
set({ uploadProgress: null, uploadAbortController: null });
|
||||
@@ -457,22 +467,9 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
const resource = resources.find(r => r.name === name);
|
||||
if (!resource) return;
|
||||
|
||||
const idsToDelete = [resource.id];
|
||||
|
||||
// If deleting a folder, also delete all files inside it
|
||||
if (resource.isDirectory) {
|
||||
const allNodes = await client.listFileNodes(null);
|
||||
const folderPrefix = resource.serverName + PATH_SEP;
|
||||
for (const node of allNodes) {
|
||||
if (node.name.startsWith(folderPrefix)) {
|
||||
idsToDelete.push(node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.destroyFileNodes(idsToDelete);
|
||||
const deletedIdSet = new Set(idsToDelete);
|
||||
const nextRecentFiles = recentFiles.filter(r => !deletedIdSet.has(r.id));
|
||||
// The server removes descendant nodes (onDestroyRemoveChildren).
|
||||
await client.destroyFileNodes([resource.id]);
|
||||
const nextRecentFiles = recentFiles.filter(r => r.id !== resource.id);
|
||||
set({ recentFiles: nextRecentFiles });
|
||||
try { localStorage.setItem('files-recent-files', JSON.stringify(nextRecentFiles)); } catch { /* ignore */ }
|
||||
await refresh();
|
||||
@@ -483,26 +480,14 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
if (!client) return;
|
||||
|
||||
const idsToDelete: string[] = [];
|
||||
let allNodes: FileNode[] | null = null;
|
||||
|
||||
for (const name of names) {
|
||||
const resource = resources.find(r => r.name === name);
|
||||
if (!resource) continue;
|
||||
idsToDelete.push(resource.id);
|
||||
|
||||
if (resource.isDirectory) {
|
||||
if (!allNodes) allNodes = await client.listFileNodes(null);
|
||||
const folderPrefix = resource.serverName + PATH_SEP;
|
||||
for (const node of allNodes) {
|
||||
if (node.name.startsWith(folderPrefix)) {
|
||||
idsToDelete.push(node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource) idsToDelete.push(resource.id);
|
||||
}
|
||||
|
||||
if (idsToDelete.length === 0) return;
|
||||
|
||||
// The server removes descendant nodes (onDestroyRemoveChildren).
|
||||
await client.destroyFileNodes(idsToDelete);
|
||||
const deletedIdSet = new Set(idsToDelete);
|
||||
const nextRecentFiles = recentFiles.filter(r => !deletedIdSet.has(r.id));
|
||||
@@ -513,35 +498,18 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
},
|
||||
|
||||
renameResource: async (oldName: string, newName: string) => {
|
||||
const { client, resources, currentPath, refresh } = get();
|
||||
const { client, resources, refresh } = get();
|
||||
if (!client) return;
|
||||
|
||||
const resource = resources.find(r => r.name === oldName);
|
||||
if (!resource) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
const oldServerName = resource.serverName;
|
||||
const newServerName = prefix + newName;
|
||||
|
||||
await client.updateFileNode(resource.id, { name: newServerName });
|
||||
|
||||
// If renaming a folder, also rename all files inside it
|
||||
if (resource.isDirectory) {
|
||||
const allNodes = await client.listFileNodes(null);
|
||||
const oldFolderPrefix = oldServerName + PATH_SEP;
|
||||
const newFolderPrefix = newServerName + PATH_SEP;
|
||||
for (const node of allNodes) {
|
||||
if (node.name.startsWith(oldFolderPrefix)) {
|
||||
const newNodeName = newFolderPrefix + node.name.slice(oldFolderPrefix.length);
|
||||
await client.updateFileNode(node.id, { name: newNodeName });
|
||||
}
|
||||
}
|
||||
}
|
||||
await client.updateFileNode(resource.id, { name: newName });
|
||||
|
||||
set({
|
||||
lastAction: {
|
||||
type: 'rename',
|
||||
entries: [{ id: resource.id, from: { name: oldServerName }, to: { name: newServerName } }],
|
||||
entries: [{ id: resource.id, from: { name: oldName }, to: { name: newName } }],
|
||||
sourceParentId: null,
|
||||
},
|
||||
});
|
||||
@@ -592,32 +560,28 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
},
|
||||
|
||||
createTextFile: async (name: string) => {
|
||||
const { client, currentPath, refresh } = get();
|
||||
const { client, currentParentId, refresh } = get();
|
||||
if (!client) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
const fullName = prefix + name;
|
||||
const emptyBlob = new File([''], name, { type: 'text/plain' });
|
||||
const { blobId } = await client.uploadBlob(emptyBlob);
|
||||
await client.createFileNode(fullName, blobId, 'text/plain', 0, null);
|
||||
await client.createFileNode(name, blobId, 'text/plain', 0, currentParentId);
|
||||
await refresh();
|
||||
},
|
||||
|
||||
duplicateResource: async (name: string) => {
|
||||
const { client, resources, currentPath, refresh } = get();
|
||||
const { client, resources, currentParentId, refresh } = get();
|
||||
if (!client) return;
|
||||
|
||||
const resource = resources.find(r => r.name === name);
|
||||
if (!resource) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
const dotIdx = name.lastIndexOf('.');
|
||||
const copyName = dotIdx > 0
|
||||
? `${name.substring(0, dotIdx)} (copy)${name.substring(dotIdx)}`
|
||||
: `${name} (copy)`;
|
||||
const fullCopyName = prefix + copyName;
|
||||
|
||||
await client.copyFileNode(resource.id, fullCopyName, null);
|
||||
await client.copyFileNode(resource.id, copyName, currentParentId);
|
||||
await refresh();
|
||||
},
|
||||
|
||||
@@ -631,10 +595,9 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
const entries: UndoAction['entries'] = [];
|
||||
for (const name of names) {
|
||||
const resource = resources.find(r => r.name === name);
|
||||
if (!resource) continue;
|
||||
const newServerName = targetResource.serverName + PATH_SEP + resource.name;
|
||||
await client.updateFileNode(resource.id, { name: newServerName });
|
||||
entries.push({ id: resource.id, from: { name: resource.serverName }, to: { name: newServerName } });
|
||||
if (!resource || resource.id === targetResource.id) continue;
|
||||
await client.updateFileNode(resource.id, { parentId: targetResource.id });
|
||||
entries.push({ id: resource.id, from: { parentId: resource.parentId }, to: { parentId: targetResource.id } });
|
||||
}
|
||||
set({
|
||||
selectedResources: new Set(),
|
||||
@@ -644,21 +607,19 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
},
|
||||
|
||||
moveToParent: async (names: string[]) => {
|
||||
const { client, resources, currentPath, refresh } = get();
|
||||
if (!client || currentPath === '/') return;
|
||||
const { client, resources, pathStack, refresh } = get();
|
||||
if (!client || pathStack.length <= 1) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
// Parent prefix: strip the last segment from the current prefix
|
||||
// e.g. "folder∕sub∕" → "folder∕", "folder∕" → ""
|
||||
const parentPrefix = prefix.slice(0, prefix.lastIndexOf(PATH_SEP, prefix.length - 2) + 1);
|
||||
// Move into the grandparent of the current folder's contents, i.e. the
|
||||
// entry one level up in the breadcrumb stack.
|
||||
const newParentId = pathStack[pathStack.length - 2].id;
|
||||
|
||||
const entries: UndoAction['entries'] = [];
|
||||
for (const name of names) {
|
||||
const resource = resources.find(r => r.name === name);
|
||||
if (!resource) continue;
|
||||
const newServerName = parentPrefix + resource.name;
|
||||
await client.updateFileNode(resource.id, { name: newServerName });
|
||||
entries.push({ id: resource.id, from: { name: resource.serverName }, to: { name: newServerName } });
|
||||
await client.updateFileNode(resource.id, { parentId: newParentId });
|
||||
entries.push({ id: resource.id, from: { parentId: resource.parentId }, to: { parentId: newParentId } });
|
||||
}
|
||||
set({
|
||||
selectedResources: new Set(),
|
||||
@@ -668,38 +629,34 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
},
|
||||
|
||||
cutResources: (names: string[]) => {
|
||||
const { currentPath, resources } = get();
|
||||
const { currentPath, currentParentId, resources } = get();
|
||||
const ids = names.map(n => resources.find(r => r.name === n)?.id).filter(Boolean) as string[];
|
||||
const serverNames = names.map(n => resources.find(r => r.name === n)?.serverName).filter(Boolean) as string[];
|
||||
set({ clipboard: { mode: 'cut', ids, names, serverNames, sourceParentId: null, sourcePath: currentPath } });
|
||||
set({ clipboard: { mode: 'cut', ids, names, serverNames, sourceParentId: currentParentId, sourcePath: currentPath } });
|
||||
},
|
||||
|
||||
copyResources: (names: string[]) => {
|
||||
const { currentPath, resources } = get();
|
||||
const { currentPath, currentParentId, resources } = get();
|
||||
const ids = names.map(n => resources.find(r => r.name === n)?.id).filter(Boolean) as string[];
|
||||
const serverNames = names.map(n => resources.find(r => r.name === n)?.serverName).filter(Boolean) as string[];
|
||||
set({ clipboard: { mode: 'copy', ids, names, serverNames, sourceParentId: null, sourcePath: currentPath } });
|
||||
set({ clipboard: { mode: 'copy', ids, names, serverNames, sourceParentId: currentParentId, sourcePath: currentPath } });
|
||||
},
|
||||
|
||||
pasteResources: async () => {
|
||||
const { client, currentPath, clipboard, refresh } = get();
|
||||
const { client, currentParentId, clipboard, refresh } = get();
|
||||
if (!client || !clipboard) return;
|
||||
|
||||
const prefix = getPathPrefix(currentPath);
|
||||
const entries: UndoAction['entries'] = [];
|
||||
|
||||
for (let i = 0; i < clipboard.ids.length; i++) {
|
||||
const id = clipboard.ids[i];
|
||||
const displayName = clipboard.names[i];
|
||||
const oldServerName = clipboard.serverNames?.[i];
|
||||
|
||||
if (clipboard.mode === 'cut') {
|
||||
const newServerName = prefix + displayName;
|
||||
await client.updateFileNode(id, { name: newServerName });
|
||||
entries.push({ id, from: { name: oldServerName }, to: { name: newServerName } });
|
||||
await client.updateFileNode(id, { parentId: currentParentId });
|
||||
entries.push({ id, from: { parentId: clipboard.sourceParentId }, to: { parentId: currentParentId } });
|
||||
} else {
|
||||
const fullName = prefix + displayName;
|
||||
await client.copyFileNode(id, fullName, null);
|
||||
await client.copyFileNode(id, displayName, currentParentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,10 +702,10 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
if (!client) return [];
|
||||
|
||||
try {
|
||||
const allNodes = await client.listFileNodes(null);
|
||||
const prefix = getPathPrefix(path);
|
||||
const filtered = filterNodesByPrefix(allNodes, prefix);
|
||||
return filtered.map(n => nodeToResource(n, prefix));
|
||||
const allNodes = await client.listAllFileNodes();
|
||||
const parentId = resolvePathToId(allNodes, path);
|
||||
if (parentId === undefined) return [];
|
||||
return sortResources(childrenOf(allNodes, parentId).map(nodeToResource));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -758,21 +715,8 @@ export const useFileStore = create<FileState>((set, get) => ({
|
||||
const { client } = get();
|
||||
if (!client) return [];
|
||||
try {
|
||||
const allNodes = await client.listFileNodes(null);
|
||||
|
||||
if (parentId === null) {
|
||||
// Root level: nodes with simple names (no "/")
|
||||
const rootNodes = allNodes.filter(n => !n.name.includes('/'));
|
||||
return rootNodes.map(n => nodeToResource(n));
|
||||
}
|
||||
|
||||
// Find the folder node to get its server name
|
||||
const folder = allNodes.find(n => n.id === parentId);
|
||||
if (!folder) return [];
|
||||
|
||||
const prefix = folder.name + PATH_SEP;
|
||||
const filtered = filterNodesByPrefix(allNodes, prefix);
|
||||
return filtered.map(n => nodeToResource(n, prefix));
|
||||
const allNodes = await client.listAllFileNodes();
|
||||
return sortResources(childrenOf(allNodes, parentId).map(nodeToResource));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user