@marimo-team/frontend 0.24.1-dev83 → 0.24.1-dev85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{CellStatus-GcHBZKLm.js → CellStatus-D40JXMpi.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-BU1NZpRe.js → ConnectedDataExplorerComponent-Dkqk66Mv.js} +1 -1
- package/dist/assets/{JsonOutput-CUJlEdsN.js → JsonOutput-DeeAtdky.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-DCAezqcJ.js → MarimoErrorOutput-Dh-877oV.js} +1 -1
- package/dist/assets/{RenderHTML-D9-75ltP.js → RenderHTML-C7JgAfHC.js} +1 -1
- package/dist/assets/{RunButton-plSsbubG.js → RunButton-C9d_w3qn.js} +1 -1
- package/dist/assets/{add-cell-with-ai-Cfby28hZ.js → add-cell-with-ai-zrX9HOPy.js} +1 -1
- package/dist/assets/{add-connection-dialog-By0i0Fq5.js → add-connection-dialog-OQxtKyCV.js} +1 -1
- package/dist/assets/{agent-panel-Cf482jaA.js → agent-panel-DF7_IEEH.js} +1 -1
- package/dist/assets/{ai-model-dropdown-DMfaGQ8o.js → ai-model-dropdown-DIItpu01.js} +1 -1
- package/dist/assets/{app-config-button-93unbEYx.js → app-config-button-CeDIYZUc.js} +1 -1
- package/dist/assets/{blob-yxfhyyvD.js → blob-iewSQmkZ.js} +1 -1
- package/dist/assets/{cell-editor-CZmqG6aB.js → cell-editor-BkHTcrF-.js} +1 -1
- package/dist/assets/{cell-link-hHrWcozB.js → cell-link-ZLZTXwR8.js} +1 -1
- package/dist/assets/{cells-yuYDzbAy.js → cells-DyE9dSiM.js} +1 -1
- package/dist/assets/{chat-display-CnhvADgL.js → chat-display-B9XOYJaA.js} +1 -1
- package/dist/assets/{chat-panel-CdE97AxM.js → chat-panel-Drx8ToF5.js} +1 -1
- package/dist/assets/{chat-ui-BD3rpULw.js → chat-ui-CJ1VFz1m.js} +1 -1
- package/dist/assets/{column-preview-BEmcYaKl.js → column-preview-Ls8UnJOt.js} +1 -1
- package/dist/assets/{command-palette-BGRRw0sn.js → command-palette-DWyHSatV.js} +1 -1
- package/dist/assets/{common-Bs2kaOs2.js → common-03vbDqrN.js} +1 -1
- package/dist/assets/{components-ib4asSo7.js → components-CBlGIIAX.js} +1 -1
- package/dist/assets/{components-ByyAzQxP.js → components-J_vBLfsO.js} +1 -1
- package/dist/assets/{config-DaNyNpZL.js → config-D4frVGQ4.js} +1 -1
- package/dist/assets/{config-Cl3eMWxb.js → config-ycxJQP2b.js} +1 -1
- package/dist/assets/{datasource-DeZYq5UY.js → datasource-B4VfwXg_.js} +1 -1
- package/dist/assets/{dependency-graph-panel-g10osAFc.js → dependency-graph-panel-B24NhlCS.js} +1 -1
- package/dist/assets/{documentation-panel-CZ1mKkDh.js → documentation-panel-C4y_ufGj.js} +1 -1
- package/dist/assets/{download-D1crMjD8.js → download-Kg8_rqQH.js} +1 -1
- package/dist/assets/{edit-page-Bk1sT7Wb.js → edit-page-DL_yo5rn.js} +4 -4
- package/dist/assets/{error-entries-B6XTLRyE.js → error-entries-Cy1y8FaI.js} +1 -1
- package/dist/assets/{error-panel-lexB3L6f.js → error-panel-D_pwhWfh.js} +1 -1
- package/dist/assets/file-explorer-panel-QoJu0RbE.js +1 -0
- package/dist/assets/{file-icons-Dx3v6lMu.js → file-icons-BN7fZ7-c.js} +1 -1
- package/dist/assets/file-name-input-qU-dl8Og.js +3 -0
- package/dist/assets/{floating-outline-3rQEbxxk.js → floating-outline-Coq72JrA.js} +1 -1
- package/dist/assets/{glide-data-editor-DZ2Minh8.js → glide-data-editor-DcY3nXI8.js} +1 -1
- package/dist/assets/{home-page-Dnz4tfZB.js → home-page-C6NwS9L9.js} +1 -1
- package/dist/assets/{index-BID9w87A.js → index-DMvKFHTV.js} +5 -5
- package/dist/assets/{kiosk-mode-CjT3sqy8.js → kiosk-mode-CzGt3mc2.js} +1 -1
- package/dist/assets/{layout-qHVpnnQT.js → layout-ByAlvlct.js} +3 -3
- package/dist/assets/{logs-panel-CSbUhhRo.js → logs-panel-zYelRd3A.js} +1 -1
- package/dist/assets/{markdown-renderer-BgUTGtM0.js → markdown-renderer-BqMslrdb.js} +1 -1
- package/dist/assets/{mermaid-Nzih1WTG.js → mermaid-CeseGGnS.js} +1 -1
- package/dist/assets/{name-cell-input-pEE4FGAL.js → name-cell-input-jrtswt5e.js} +1 -1
- package/dist/assets/{outline-panel-BgnjEQWg.js → outline-panel-B0P8XNnQ.js} +1 -1
- package/dist/assets/{packages-panel-BprfUOzZ.js → packages-panel-C08E4Wjm.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-Gg8MZHWW.js → pair-with-agent-modal-BZBengJw.js} +1 -1
- package/dist/assets/{panel-accordion-state-vpqEc3Kd.js → panel-accordion-state-Cyy4afrx.js} +1 -1
- package/dist/assets/{panels-3h7Cf8xJ.js → panels-DRtPsJ1u.js} +1 -1
- package/dist/assets/{process-output-siRm_lza.js → process-output-BiA5MKpM.js} +1 -1
- package/dist/assets/{readonly-code-display-CLWV76FT.js → readonly-code-display-B45Jy4vH.js} +1 -1
- package/dist/assets/{readonly-python-code-Bor9b9pV.js → readonly-python-code-BQ5Ti6Hw.js} +1 -1
- package/dist/assets/{renderShortcut-CS4ccH3x.js → renderShortcut-DK_OucLT.js} +1 -1
- package/dist/assets/{reveal-component-DNPksvzT.js → reveal-component-Bna3jncc.js} +1 -1
- package/dist/assets/{run-page-C4aONLoP.js → run-page-CFC5C_It.js} +1 -1
- package/dist/assets/{scratchpad-panel-BQSoRQvQ.js → scratchpad-panel-m2LRBFI5.js} +1 -1
- package/dist/assets/{session-panel-DB5qEYqt.js → session-panel-BV8Whoy8.js} +1 -1
- package/dist/assets/{skeleton-BSxwEE0h.js → skeleton-CWFWp9eU.js} +1 -1
- package/dist/assets/{snippets-panel-B8Yi81tz.js → snippets-panel-DBzdOGwQ.js} +1 -1
- package/dist/assets/state-DPS9Q717.js +3 -0
- package/dist/assets/{state-O8w98G11.js → state-rP7TRCla.js} +1 -1
- package/dist/assets/{switch-Gl6cozkm.js → switch-CtH5Ct94.js} +1 -1
- package/dist/assets/{terminal-DOqqA9Ha.js → terminal-C73iz1fU.js} +1 -1
- package/dist/assets/{tracing-DuLkr7ts.js → tracing-BP1J14GT.js} +1 -1
- package/dist/assets/{tracing-panel-CeLKVG8M.js → tracing-panel-CQasWudc.js} +2 -2
- package/dist/assets/{tree-actions-Bh-Nnaub.js → tree-actions-CrB9REp3.js} +1 -1
- package/dist/assets/{useAddCell-_JRYhAFd.js → useAddCell-DHYPnl7W.js} +1 -1
- package/dist/assets/{useBoolean-Cu7Eado1.js → useBoolean-ZU5StAbr.js} +1 -1
- package/dist/assets/{useCellActionButton-Bi8Lnv3F.js → useCellActionButton-C7bEu9tJ.js} +1 -1
- package/dist/assets/{useDeleteCell-CBQiavzs.js → useDeleteCell-CIzxZKUw.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-BOn3UJ07.js → useDependencyPanelTab-CLmRGbOD.js} +1 -1
- package/dist/assets/{useNotebookActions--pblpIWF.js → useNotebookActions-Q97EgZxR.js} +1 -1
- package/dist/assets/{useRunCells-BFQfLlUl.js → useRunCells-Csh5VYKu.js} +1 -1
- package/dist/assets/{useSplitCell-Ig9P6lAn.js → useSplitCell-BfT7UcKM.js} +1 -1
- package/dist/assets/{useTheme-D3zvxdF3.js → useTheme-CF-zG9YH.js} +1 -1
- package/dist/assets/{utils-CUZQNAWJ.js → utils-CWSZQEjr.js} +1 -1
- package/dist/assets/{vega-component-B0MZ_r-y.js → vega-component-Bel2PDO-.js} +1 -1
- package/dist/index.html +26 -26
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +1 -0
- package/src/components/editor/chrome/panels/file-explorer-panel.tsx +3 -2
- package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +31 -18
- package/src/components/editor/file-tree/__tests__/file-explorer.test.ts +18 -12
- package/src/components/editor/file-tree/__tests__/requesting-tree.test.ts +364 -387
- package/src/components/editor/file-tree/file-explorer.tsx +137 -97
- package/src/components/editor/file-tree/file-name-input.tsx +5 -1
- package/src/components/editor/file-tree/file-viewer.tsx +2 -2
- package/src/components/editor/file-tree/requesting-tree.tsx +291 -171
- package/src/components/editor/file-tree/state.tsx +1 -0
- package/src/core/config/__tests__/config-schema.test.ts +15 -0
- package/src/core/config/config-schema.ts +15 -0
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/network/__tests__/requests-network.test.ts +7 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +10 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +4 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +3 -0
- package/src/core/wasm/bridge.ts +9 -0
- package/src/core/wasm/worker/types.ts +2 -0
- package/dist/assets/file-explorer-panel-DHRH2J-3.js +0 -1
- package/dist/assets/file-name-input-DzL8D_7x.js +0 -3
- package/dist/assets/state-D1N9_41P.js +0 -3
|
@@ -5,15 +5,24 @@ import { toast } from "@/components/ui/use-toast";
|
|
|
5
5
|
import type {
|
|
6
6
|
EditRequests,
|
|
7
7
|
FileInfo,
|
|
8
|
+
FileRoot,
|
|
8
9
|
FileUpdateResponse,
|
|
9
10
|
} from "@/core/network/types";
|
|
10
11
|
import { prettyError } from "@/utils/errors";
|
|
11
12
|
import { Functions } from "@/utils/functions";
|
|
12
13
|
import { type FilePath, PathBuilder } from "@/utils/paths";
|
|
13
|
-
import { resolvePaths } from "@/utils/pathUtils";
|
|
14
14
|
import { mapWithConcurrency } from "@/utils/semaphore";
|
|
15
15
|
|
|
16
16
|
const FILE_OP_CONCURRENCY = 5;
|
|
17
|
+
const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[/\\]/;
|
|
18
|
+
const WINDOWS_UNC_PATH = /^\\\\/;
|
|
19
|
+
|
|
20
|
+
export type FileTreeNode = Omit<FileInfo, "children"> & {
|
|
21
|
+
children: FileTreeNode[];
|
|
22
|
+
isRoot: boolean;
|
|
23
|
+
rootPath: string;
|
|
24
|
+
isPrimaryRoot: boolean;
|
|
25
|
+
};
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
28
|
* Normalized result of a file mutation: the server response when successful,
|
|
@@ -35,8 +44,9 @@ export function handleFileResponse(
|
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export class RequestingTree {
|
|
38
|
-
private delegate = new SimpleTree<
|
|
39
|
-
private callbacks: {
|
|
47
|
+
private delegate = new SimpleTree<FileTreeNode>([]);
|
|
48
|
+
private readonly callbacks: {
|
|
49
|
+
getRoots: EditRequests["getFileRoots"];
|
|
40
50
|
listFiles: EditRequests["sendListFiles"];
|
|
41
51
|
createFileOrFolder: EditRequests["sendCreateFileOrFolder"];
|
|
42
52
|
deleteFileOrFolder: EditRequests["sendDeleteFileOrFolder"];
|
|
@@ -45,6 +55,7 @@ export class RequestingTree {
|
|
|
45
55
|
};
|
|
46
56
|
|
|
47
57
|
constructor(callbacks: {
|
|
58
|
+
getRoots: EditRequests["getFileRoots"];
|
|
48
59
|
listFiles: EditRequests["sendListFiles"];
|
|
49
60
|
createFileOrFolder: EditRequests["sendCreateFileOrFolder"];
|
|
50
61
|
deleteFileOrFolder: EditRequests["sendDeleteFileOrFolder"];
|
|
@@ -54,18 +65,35 @@ export class RequestingTree {
|
|
|
54
65
|
this.callbacks = callbacks;
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
private
|
|
58
|
-
private onChange: (data:
|
|
68
|
+
private roots: FileRoot[] = [];
|
|
69
|
+
private onChange: (data: FileTreeNode[]) => void = Functions.NOOP;
|
|
59
70
|
private path = new PathBuilder("/");
|
|
60
71
|
|
|
61
|
-
initialize = async (
|
|
72
|
+
initialize = async (
|
|
73
|
+
onChange: (data: FileTreeNode[]) => void,
|
|
74
|
+
): Promise<void> => {
|
|
62
75
|
this.onChange = onChange;
|
|
63
76
|
if (this.delegate.data.length === 0) {
|
|
64
77
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
const { roots } = await this.callbacks.getRoots();
|
|
79
|
+
const primaryRoot = roots.find((root) => root.isPrimary);
|
|
80
|
+
if (!primaryRoot) {
|
|
81
|
+
throw new Error("File browser response is missing a primary root");
|
|
82
|
+
}
|
|
83
|
+
this.roots = roots;
|
|
84
|
+
this.path = PathBuilder.guessDeliminator(primaryRoot.path);
|
|
85
|
+
this.delegate = new SimpleTree(roots.map(toRootNode));
|
|
86
|
+
if (roots.length === 1) {
|
|
87
|
+
const data = await this.callbacks.listFiles({
|
|
88
|
+
path: primaryRoot.path,
|
|
89
|
+
});
|
|
90
|
+
this.delegate.update({
|
|
91
|
+
id: this.getPrimaryRootId(),
|
|
92
|
+
changes: {
|
|
93
|
+
children: annotateFiles(data.files, primaryRoot),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
69
97
|
} catch (error) {
|
|
70
98
|
toast({
|
|
71
99
|
title: "Failed",
|
|
@@ -74,119 +102,91 @@ export class RequestingTree {
|
|
|
74
102
|
}
|
|
75
103
|
}
|
|
76
104
|
|
|
77
|
-
this.
|
|
105
|
+
this.emitChange();
|
|
78
106
|
};
|
|
79
107
|
|
|
80
108
|
async expand(id: string): Promise<boolean> {
|
|
81
109
|
const node = this.delegate.find(id);
|
|
82
|
-
if (!node) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
if (!node.data.isDirectory) {
|
|
110
|
+
if (!node?.data.isDirectory) {
|
|
86
111
|
return false;
|
|
87
112
|
}
|
|
88
113
|
|
|
89
|
-
// We may attempt to load empty directories multiple times
|
|
90
|
-
// but that is fine
|
|
91
114
|
if (node.children && node.children.length > 0) {
|
|
92
|
-
// Already loaded
|
|
93
115
|
return true;
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
const data = await this.callbacks.listFiles({ path: node.data.path });
|
|
97
|
-
this.delegate.update({
|
|
98
|
-
|
|
119
|
+
this.delegate.update({
|
|
120
|
+
id,
|
|
121
|
+
changes: {
|
|
122
|
+
children: annotateFiles(data.files, this.getRootForNode(node.data)),
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
this.emitChange();
|
|
99
126
|
return true;
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
async copy(id: string, newName: string): Promise<void> {
|
|
103
|
-
const node = this.
|
|
130
|
+
const node = this.getMutableNode(id);
|
|
104
131
|
if (!node) {
|
|
105
|
-
toast({
|
|
106
|
-
title: "Failed",
|
|
107
|
-
description: `Node with id ${id} not found in the tree`,
|
|
108
|
-
});
|
|
109
132
|
return;
|
|
110
133
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
const parentPath = this.path.dirname(path);
|
|
117
|
-
const newFile = await this.callbacks
|
|
134
|
+
const path = node.data.path as FilePath;
|
|
135
|
+
const parentPath = this.getParentPath(node.data);
|
|
136
|
+
const newPath = joinPath(parentPath, newName);
|
|
137
|
+
const result = await this.callbacks
|
|
118
138
|
.copyFileOrFolder({ path, newPath })
|
|
119
139
|
.then(handleFileResponse);
|
|
120
|
-
if (
|
|
121
|
-
|
|
140
|
+
if (result) {
|
|
141
|
+
await this.refreshPath(parentPath);
|
|
122
142
|
}
|
|
123
|
-
this.delegate.create({
|
|
124
|
-
parentId: node.parent?.id ?? null,
|
|
125
|
-
index: 0,
|
|
126
|
-
data: newFile.info,
|
|
127
|
-
});
|
|
128
|
-
this.onChange(this.delegate.data);
|
|
129
|
-
// Refresh the parent folder
|
|
130
|
-
await this.refreshAll([parentPath]);
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
async rename(id: string, name: string): Promise<void> {
|
|
134
|
-
const node = this.
|
|
146
|
+
const node = this.getMutableNode(id);
|
|
135
147
|
if (!node) {
|
|
136
|
-
toast({
|
|
137
|
-
title: "Failed",
|
|
138
|
-
description: `Node with id ${id} not found in the tree`,
|
|
139
|
-
});
|
|
140
148
|
return;
|
|
141
149
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
root: this.rootPath,
|
|
146
|
-
});
|
|
150
|
+
const path = node.data.path as FilePath;
|
|
151
|
+
const parentPath = this.getParentPath(node.data);
|
|
152
|
+
const newPath = joinPath(parentPath, name);
|
|
147
153
|
const result = await this.callbacks
|
|
148
154
|
.renameFileOrFolder({ path, newPath })
|
|
149
155
|
.then(handleFileResponse);
|
|
150
|
-
if (
|
|
151
|
-
|
|
156
|
+
if (result) {
|
|
157
|
+
await this.refreshPath(parentPath);
|
|
152
158
|
}
|
|
153
|
-
|
|
154
|
-
this.delegate.update({ id, changes: { name, path: newPath } });
|
|
155
|
-
this.onChange(this.delegate.data);
|
|
156
|
-
// Rename all of its children
|
|
157
|
-
await this.refreshAll([newPath]);
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
async move(fromIds: string[], parentId: string | null): Promise<void> {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
const targetParentId = parentId ?? this.getPrimaryRootId();
|
|
163
|
+
const parent = this.delegate.find(targetParentId);
|
|
164
|
+
if (!parent?.data.isDirectory) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
164
167
|
|
|
168
|
+
const refreshPaths = new Set<string>();
|
|
165
169
|
await mapWithConcurrency(fromIds, FILE_OP_CONCURRENCY, async (id) => {
|
|
166
|
-
const node = this.
|
|
170
|
+
const node = this.getMutableNode(id, false);
|
|
167
171
|
if (!node) {
|
|
168
172
|
return;
|
|
169
173
|
}
|
|
170
174
|
const originalPath = node.data.path;
|
|
171
|
-
const
|
|
172
|
-
|
|
175
|
+
const sourceParentPath = this.getParentPath(node.data);
|
|
176
|
+
const newPath = joinPath(
|
|
177
|
+
parent.data.path,
|
|
173
178
|
this.path.basename(originalPath as FilePath),
|
|
174
179
|
);
|
|
175
180
|
const result = await this.callbacks
|
|
176
181
|
.renameFileOrFolder({ path: originalPath, newPath })
|
|
177
182
|
.then(handleFileResponse);
|
|
178
|
-
if (
|
|
179
|
-
|
|
183
|
+
if (result) {
|
|
184
|
+
refreshPaths.add(parent.data.path);
|
|
185
|
+
refreshPaths.add(sourceParentPath);
|
|
180
186
|
}
|
|
181
|
-
|
|
182
|
-
this.delegate.move({ id, parentId, index: 0 });
|
|
183
|
-
this.delegate.update({ id, changes: { path: newPath } });
|
|
184
187
|
});
|
|
185
188
|
|
|
186
|
-
this.
|
|
187
|
-
|
|
188
|
-
// Refresh the parent folder
|
|
189
|
-
await this.refreshAll([parentPath]);
|
|
189
|
+
await this.refreshPaths([...refreshPaths]);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
async createFile({
|
|
@@ -198,140 +198,260 @@ export class RequestingTree {
|
|
|
198
198
|
parentId: string | null;
|
|
199
199
|
type?: "file" | "notebook";
|
|
200
200
|
}): Promise<void> {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
: this.rootPath;
|
|
204
|
-
const newFile = await this.callbacks
|
|
205
|
-
.createFileOrFolder({ path: parentPath, type: type, name: name })
|
|
206
|
-
.then(handleFileResponse);
|
|
207
|
-
if (!newFile?.info) {
|
|
201
|
+
const parent = this.getParentNode(parentId);
|
|
202
|
+
if (!parent) {
|
|
208
203
|
return;
|
|
209
204
|
}
|
|
210
|
-
this.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
205
|
+
const result = await this.callbacks
|
|
206
|
+
.createFileOrFolder({
|
|
207
|
+
path: parent.data.path,
|
|
208
|
+
type,
|
|
209
|
+
name,
|
|
210
|
+
})
|
|
211
|
+
.then(handleFileResponse);
|
|
212
|
+
if (result) {
|
|
213
|
+
await this.refreshPath(parent.data.path as FilePath);
|
|
214
|
+
}
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
async createFolder(name: string, parentId: string | null): Promise<void> {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
: this.rootPath;
|
|
224
|
-
const newFolder = await this.callbacks
|
|
225
|
-
.createFileOrFolder({ path: parentPath, type: "directory", name: name })
|
|
226
|
-
.then(handleFileResponse);
|
|
227
|
-
if (!newFolder?.info) {
|
|
218
|
+
const parent = this.getParentNode(parentId);
|
|
219
|
+
if (!parent) {
|
|
228
220
|
return;
|
|
229
221
|
}
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
222
|
+
const result = await this.callbacks
|
|
223
|
+
.createFileOrFolder({
|
|
224
|
+
path: parent.data.path,
|
|
225
|
+
type: "directory",
|
|
226
|
+
name,
|
|
227
|
+
})
|
|
228
|
+
.then(handleFileResponse);
|
|
229
|
+
if (result) {
|
|
230
|
+
await this.refreshPath(parent.data.path as FilePath);
|
|
231
|
+
}
|
|
238
232
|
}
|
|
239
233
|
|
|
240
234
|
async delete(id: string): Promise<void> {
|
|
241
|
-
const node = this.
|
|
235
|
+
const node = this.getMutableNode(id);
|
|
242
236
|
if (!node) {
|
|
243
|
-
toast({
|
|
244
|
-
title: "Failed",
|
|
245
|
-
description: `Node with id ${id} not found in the tree`,
|
|
246
|
-
});
|
|
247
237
|
return;
|
|
248
238
|
}
|
|
249
|
-
|
|
239
|
+
const parentPath = this.getParentPath(node.data);
|
|
250
240
|
const result = await this.callbacks
|
|
251
241
|
.deleteFileOrFolder({ path: node.data.path })
|
|
252
242
|
.then(handleFileResponse);
|
|
253
|
-
if (
|
|
254
|
-
|
|
243
|
+
if (result) {
|
|
244
|
+
await this.refreshPath(parentPath);
|
|
255
245
|
}
|
|
256
|
-
this.delegate.drop({ id });
|
|
257
|
-
this.onChange(this.delegate.data);
|
|
258
246
|
}
|
|
259
247
|
|
|
260
248
|
refreshAll = async (ids: string[]): Promise<void> => {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
249
|
+
const paths = [
|
|
250
|
+
...this.roots.map((root) => root.path),
|
|
251
|
+
...ids
|
|
252
|
+
.map((id) => this.delegate.find(id)?.data.path)
|
|
253
|
+
.filter((path): path is string => Boolean(path)),
|
|
254
|
+
];
|
|
255
|
+
await this.refreshPaths(paths);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
refreshPath = async (path: FilePath): Promise<void> => {
|
|
259
|
+
await this.refreshPaths([path]);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
public getPrimaryRelativePath = (path: FilePath): FilePath | null => {
|
|
263
|
+
const primaryRoot = this.getPrimaryRoot();
|
|
264
|
+
if (!primaryRoot) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return relativePath(path, primaryRoot.path);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
public getPrimaryRootPath = (): FilePath => {
|
|
271
|
+
return (this.getPrimaryRoot()?.path ?? "") as FilePath;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
public getPrimaryRootId = (): string => {
|
|
275
|
+
const path = this.getPrimaryRoot()?.path ?? "";
|
|
276
|
+
return fileTreeNodeId(path, path);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
public isPrimaryNode = (node: FileTreeNode): boolean => {
|
|
280
|
+
return node.isPrimaryRoot;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
public getDisplayPath = (path: FilePath): string => {
|
|
284
|
+
const root = this.getRootForPath(path);
|
|
285
|
+
if (!root) {
|
|
286
|
+
return path;
|
|
287
|
+
}
|
|
288
|
+
const relative = relativePath(path, root.path);
|
|
289
|
+
if (!relative) {
|
|
290
|
+
return root.name;
|
|
291
|
+
}
|
|
292
|
+
return root.isPrimary ? relative : `${root.name}/${relative}`;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
public isRootPath = (path: FilePath): boolean => {
|
|
296
|
+
return this.roots.some((root) => pathsEqual(path, root.path));
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
private getPrimaryRoot(): FileRoot | undefined {
|
|
300
|
+
return this.roots.find((root) => root.isPrimary);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private getRootForNode(node: FileTreeNode): FileRoot {
|
|
304
|
+
return (
|
|
305
|
+
this.roots.find((root) => pathsEqual(root.path, node.rootPath)) ??
|
|
306
|
+
this.getPrimaryRoot() ?? {
|
|
307
|
+
path: node.rootPath,
|
|
308
|
+
name: node.rootPath,
|
|
309
|
+
isPrimary: node.isPrimaryRoot,
|
|
310
|
+
}
|
|
272
311
|
);
|
|
312
|
+
}
|
|
273
313
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
314
|
+
private getRootForPath(path: FilePath): FileRoot | undefined {
|
|
315
|
+
return this.roots
|
|
316
|
+
.filter((root) => relativePath(path, root.path) !== null)
|
|
317
|
+
.toSorted((left, right) => right.path.length - left.path.length)[0];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private getParentNode(parentId: string | null) {
|
|
321
|
+
const resolvedId = parentId ?? this.getPrimaryRootId();
|
|
322
|
+
const node = this.delegate.find(resolvedId);
|
|
323
|
+
return node?.data.isDirectory ? node : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private getParentPath(node: FileTreeNode): FilePath {
|
|
327
|
+
const parent = this.delegate.find(node.id)?.parent;
|
|
328
|
+
return (parent?.data.path ?? node.rootPath) as FilePath;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private getMutableNode(id: string, showError = true) {
|
|
332
|
+
const node = this.delegate.find(id);
|
|
333
|
+
if (!node || node.data.isRoot) {
|
|
334
|
+
if (showError) {
|
|
335
|
+
toast({
|
|
336
|
+
title: "Failed",
|
|
337
|
+
description: node
|
|
338
|
+
? "File browser roots cannot be modified"
|
|
339
|
+
: `Node with id ${id} not found in the tree`,
|
|
282
340
|
});
|
|
283
341
|
}
|
|
342
|
+
return null;
|
|
284
343
|
}
|
|
344
|
+
return node;
|
|
345
|
+
}
|
|
285
346
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
refreshPath = async (path: FilePath): Promise<void> => {
|
|
290
|
-
const data = await this.callbacks.listFiles({ path }).catch(() => null);
|
|
291
|
-
if (!data) {
|
|
347
|
+
private refreshPaths = async (paths: string[]): Promise<void> => {
|
|
348
|
+
const uniquePaths = [...new Set(paths)];
|
|
349
|
+
if (uniquePaths.length === 0) {
|
|
292
350
|
return;
|
|
293
351
|
}
|
|
352
|
+
const results = await mapWithConcurrency(
|
|
353
|
+
uniquePaths,
|
|
354
|
+
FILE_OP_CONCURRENCY,
|
|
355
|
+
(path) => this.callbacks.listFiles({ path }).catch(() => null),
|
|
356
|
+
);
|
|
294
357
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
358
|
+
for (const [index, path] of uniquePaths.entries()) {
|
|
359
|
+
const result = results[index];
|
|
360
|
+
if (!result) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
// The same absolute path may appear below multiple overlapping roots.
|
|
364
|
+
// Refresh every occurrence, while keeping each occurrence in its own ID
|
|
365
|
+
// namespace and preserving its root metadata.
|
|
366
|
+
for (const root of this.roots) {
|
|
367
|
+
const node = this.delegate.find(fileTreeNodeId(root.path, path));
|
|
368
|
+
if (!node?.data.isDirectory) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
this.delegate.update({
|
|
372
|
+
id: node.id,
|
|
373
|
+
changes: {
|
|
374
|
+
children: annotateFiles(result.files, root),
|
|
375
|
+
},
|
|
376
|
+
});
|
|
301
377
|
}
|
|
302
|
-
this.delegate.update({ id: item.id, changes: { children: data.files } });
|
|
303
378
|
}
|
|
304
|
-
this.
|
|
379
|
+
this.emitChange();
|
|
305
380
|
};
|
|
306
381
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
382
|
+
private emitChange(): void {
|
|
383
|
+
const data = this.delegate.data;
|
|
384
|
+
this.onChange(this.roots.length === 1 ? (data[0]?.children ?? []) : data);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
312
387
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
388
|
+
function toRootNode(root: FileRoot): FileTreeNode {
|
|
389
|
+
return {
|
|
390
|
+
id: fileTreeNodeId(root.path, root.path),
|
|
391
|
+
path: root.path,
|
|
392
|
+
name: root.name,
|
|
393
|
+
isDirectory: true,
|
|
394
|
+
isMarimoFile: false,
|
|
395
|
+
children: [],
|
|
396
|
+
isRoot: true,
|
|
397
|
+
rootPath: root.path,
|
|
398
|
+
isPrimaryRoot: root.isPrimary,
|
|
317
399
|
};
|
|
400
|
+
}
|
|
318
401
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
402
|
+
function annotateFiles(files: FileInfo[], root: FileRoot): FileTreeNode[] {
|
|
403
|
+
return files.map((file) => ({
|
|
404
|
+
...file,
|
|
405
|
+
id: fileTreeNodeId(root.path, file.path),
|
|
406
|
+
children: annotateFiles(file.children ?? [], root),
|
|
407
|
+
isRoot: false,
|
|
408
|
+
rootPath: root.path,
|
|
409
|
+
isPrimaryRoot: root.isPrimary,
|
|
410
|
+
}));
|
|
322
411
|
}
|
|
323
412
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
413
|
+
/** A stable, globally unique tree ID for a path as viewed from one root. */
|
|
414
|
+
export function fileTreeNodeId(rootPath: string, path: string): string {
|
|
415
|
+
return `${encodeURIComponent(rootPath)}:${encodeURIComponent(path)}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function relativePath(path: string, root: string): FilePath | null {
|
|
419
|
+
const windowsPath = isWindowsPath(root);
|
|
420
|
+
const normalizeCase = (value: string) =>
|
|
421
|
+
windowsPath ? value.toLowerCase() : value;
|
|
422
|
+
const comparedPath = normalizeCase(path);
|
|
423
|
+
const comparedRoot = normalizeCase(root);
|
|
424
|
+
if (comparedPath === comparedRoot) {
|
|
425
|
+
return "" as FilePath;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const delimiter = pathDelimiter(root);
|
|
429
|
+
const rootWithDelimiter = root.endsWith(delimiter)
|
|
430
|
+
? root
|
|
431
|
+
: `${root}${delimiter}`;
|
|
432
|
+
const comparedPrefix = normalizeCase(rootWithDelimiter);
|
|
433
|
+
if (!comparedPath.startsWith(comparedPrefix)) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
return path.slice(rootWithDelimiter.length) as FilePath;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function pathsEqual(left: string, right: string): boolean {
|
|
440
|
+
return relativePath(left, right) === ("" as FilePath);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function joinPath(parent: string, name: string): FilePath {
|
|
444
|
+
const delimiter = pathDelimiter(parent);
|
|
445
|
+
return `${parent}${parent.endsWith(delimiter) ? "" : delimiter}${name}` as FilePath;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function isWindowsPath(path: string): boolean {
|
|
449
|
+
return WINDOWS_DRIVE_PATH.test(path) || WINDOWS_UNC_PATH.test(path);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function pathDelimiter(path: string): "/" | "\\" {
|
|
453
|
+
if (WINDOWS_UNC_PATH.test(path) || /^[A-Za-z]:\\/.test(path)) {
|
|
454
|
+
return "\\";
|
|
335
455
|
}
|
|
336
|
-
return
|
|
456
|
+
return "/";
|
|
337
457
|
}
|
|
@@ -11,6 +11,7 @@ export const treeAtom = atom<RequestingTree>((get) => {
|
|
|
11
11
|
const client = get(requestClientAtom);
|
|
12
12
|
invariant(client, "no requestClientAtom set");
|
|
13
13
|
return new RequestingTree({
|
|
14
|
+
getRoots: client.getFileRoots,
|
|
14
15
|
listFiles: client.sendListFiles,
|
|
15
16
|
createFileOrFolder: client.sendCreateFileOrFolder,
|
|
16
17
|
deleteFileOrFolder: client.sendDeleteFileOrFolder,
|
|
@@ -77,6 +77,7 @@ test("default UserConfig - empty", () => {
|
|
|
77
77
|
"theme": "light",
|
|
78
78
|
},
|
|
79
79
|
"experimental": {},
|
|
80
|
+
"file_browser": {},
|
|
80
81
|
"formatting": {
|
|
81
82
|
"line_length": 79,
|
|
82
83
|
},
|
|
@@ -152,6 +153,7 @@ test("default UserConfig - one level", () => {
|
|
|
152
153
|
"theme": "light",
|
|
153
154
|
},
|
|
154
155
|
"experimental": {},
|
|
156
|
+
"file_browser": {},
|
|
155
157
|
"formatting": {
|
|
156
158
|
"line_length": 79,
|
|
157
159
|
},
|
|
@@ -212,6 +214,19 @@ test("default UserConfig with additional information", () => {
|
|
|
212
214
|
);
|
|
213
215
|
});
|
|
214
216
|
|
|
217
|
+
test("UserConfig with file browser folders", () => {
|
|
218
|
+
const config = UserConfigSchema.parse({
|
|
219
|
+
file_browser: {
|
|
220
|
+
folders: [{ path: "/data", name: "Data" }, { path: "/shared" }],
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(config.file_browser?.folders).toEqual([
|
|
225
|
+
{ path: "/data", name: "Data" },
|
|
226
|
+
{ path: "/shared" },
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
|
|
215
230
|
test("UserConfig with custom_providers", () => {
|
|
216
231
|
const config = UserConfigSchema.parse({
|
|
217
232
|
ai: {
|
|
@@ -214,6 +214,19 @@ export const UserConfigSchema = z
|
|
|
214
214
|
sql_linter: z.boolean().optional(),
|
|
215
215
|
})
|
|
216
216
|
.prefault(() => ({})),
|
|
217
|
+
file_browser: z
|
|
218
|
+
.looseObject({
|
|
219
|
+
folders: z
|
|
220
|
+
.array(
|
|
221
|
+
z.object({
|
|
222
|
+
path: z.string(),
|
|
223
|
+
name: z.string().optional(),
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
.optional(),
|
|
227
|
+
})
|
|
228
|
+
.optional()
|
|
229
|
+
.prefault({}),
|
|
217
230
|
sharing: z
|
|
218
231
|
.looseObject({
|
|
219
232
|
html: z.boolean().optional(),
|
|
@@ -237,6 +250,7 @@ export const UserConfigSchema = z
|
|
|
237
250
|
runtime: {},
|
|
238
251
|
display: {},
|
|
239
252
|
diagnostics: {},
|
|
253
|
+
file_browser: {},
|
|
240
254
|
experimental: {},
|
|
241
255
|
server: {},
|
|
242
256
|
ai: {},
|
|
@@ -341,6 +355,7 @@ export function defaultUserConfig(): UserConfig {
|
|
|
341
355
|
runtime: {},
|
|
342
356
|
display: {},
|
|
343
357
|
diagnostics: {},
|
|
358
|
+
file_browser: {},
|
|
344
359
|
experimental: {},
|
|
345
360
|
server: {},
|
|
346
361
|
ai: {},
|