@marimo-team/frontend 0.24.1-dev116 → 0.24.1-dev119
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-BnX3kFgK.js → CellStatus-LoTwk7fR.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-Slfh2UI3.js → ConnectedDataExplorerComponent-DW6cJ-Lr.js} +1 -1
- package/dist/assets/{JsonOutput-i6wa-ZZg.js → JsonOutput-Bgxc9G7d.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-CIYHG601.js → MarimoErrorOutput-DqpoYOvw.js} +1 -1
- package/dist/assets/{RenderHTML-BIg2a2gf.js → RenderHTML-DwhMAd9l.js} +1 -1
- package/dist/assets/{RunButton-z5x8Nygi.js → RunButton-BlpbweyH.js} +1 -1
- package/dist/assets/{VisuallyHidden-K_kroCIL.js → VisuallyHidden-CBMDmozt.js} +1 -1
- package/dist/assets/{add-cell-with-ai-P7MBEjnv.js → add-cell-with-ai-R-3DS79K.js} +1 -1
- package/dist/assets/{add-connection-dialog-B7yGNrvC.js → add-connection-dialog-BGfpw5lC.js} +1 -1
- package/dist/assets/{agent-panel-DUUWyz59.js → agent-panel-B03P2qSx.js} +1 -1
- package/dist/assets/ai-model-dropdown-DCmiuk7x.js +3 -0
- package/dist/assets/{app-config-button-vxdvoC0x.js → app-config-button-DUJC6b6x.js} +1 -1
- package/dist/assets/{blob-DDxD75AS.js → blob-B2d_dHwI.js} +1 -1
- package/dist/assets/{cell-editor-D6Cyy4UF.js → cell-editor-QmA8u1Ay.js} +1 -1
- package/dist/assets/{cell-link-B7DFi5Hf.js → cell-link-6burT2XF.js} +1 -1
- package/dist/assets/{cells-CS_eEQ19.js → cells-Cp-G34Rz.js} +1 -1
- package/dist/assets/{chat-display-CtnwRDHx.js → chat-display-Bu405iyc.js} +1 -1
- package/dist/assets/{chat-panel-DScfSsWl.js → chat-panel-BlUwNY5j.js} +1 -1
- package/dist/assets/{chat-ui-9G115u_7.js → chat-ui-DGiVDzNh.js} +1 -1
- package/dist/assets/{column-preview-BVtft2uw.js → column-preview-DHe0-7z4.js} +1 -1
- package/dist/assets/{command-palette-uGrOQhzq.js → command-palette-D-kPO0tm.js} +1 -1
- package/dist/assets/{common-CQLiJ8JV.js → common-CHR7w7aQ.js} +1 -1
- package/dist/assets/{components-B5BY9RR4.js → components-C7mWrmXr.js} +1 -1
- package/dist/assets/{components-DAhqHptJ.js → components-DUYCXngU.js} +1 -1
- package/dist/assets/{config-CJhe6_P2.js → config-B-r81uZ6.js} +1 -1
- package/dist/assets/{config-BRRSKtP3.js → config-BlNM0caL.js} +1 -1
- package/dist/assets/{datasource-Br61QFhd.js → datasource-DrODSIs0.js} +1 -1
- package/dist/assets/{dependency-graph-panel-DoAAA7Y1.js → dependency-graph-panel-AY5BEVH2.js} +1 -1
- package/dist/assets/{documentation-panel-CYUIPHeO.js → documentation-panel-CaJF7e-j.js} +1 -1
- package/dist/assets/{download-D2Ha98tp.js → download-DMxOzI7b.js} +1 -1
- package/dist/assets/{edit-page-ClGPnVOK.js → edit-page-CdCPUPqZ.js} +4 -4
- package/dist/assets/{error-entries-ooCnKQtX.js → error-entries-Cv2m1yhc.js} +1 -1
- package/dist/assets/{error-panel-CRtClD-k.js → error-panel-Bhlvt6_H.js} +1 -1
- package/dist/assets/file-explorer-panel-DMKcHVIX.js +1 -0
- package/dist/assets/{file-icons-D0QvDCq_.js → file-icons-DdGYuADJ.js} +1 -1
- package/dist/assets/file-name-input-K4MKESI_.js +3 -0
- package/dist/assets/{floating-outline-FQva8xB9.js → floating-outline-DaUHnl_e.js} +1 -1
- package/dist/assets/{gallery-page-LX7vPBcZ.js → gallery-page-C3pBpHlY.js} +1 -1
- package/dist/assets/{glide-data-editor-DgWXD84U.js → glide-data-editor-sICUMEhk.js} +1 -1
- package/dist/assets/{home-page-DtRwYsH-.js → home-page-Cir7n_EF.js} +1 -1
- package/dist/assets/index-BGT7have.css +2 -0
- package/dist/assets/{index-BphlL86Q.js → index-ClZGC-u9.js} +6 -6
- package/dist/assets/{input-C-RazPBA.js → input-DEbY281O.js} +1 -1
- package/dist/assets/{kiosk-mode-BqYH0GIF.js → kiosk-mode-mxgb5lWf.js} +1 -1
- package/dist/assets/{layout-C4U2fp5O.js → layout-CC2AHMrW.js} +3 -3
- package/dist/assets/{logs-panel-BPfZpDxG.js → logs-panel-BGS2v3ba.js} +1 -1
- package/dist/assets/{markdown-renderer-CbdXI4uO.js → markdown-renderer-BZKlbm2Q.js} +1 -1
- package/dist/assets/{mermaid-BJLFqJtJ.js → mermaid-l6lQkCib.js} +1 -1
- package/dist/assets/{name-cell-input-CObHxs3L.js → name-cell-input-BMCdK-el.js} +1 -1
- package/dist/assets/{outline-panel-Dyr_2yws.js → outline-panel-Dr5u73V6.js} +1 -1
- package/dist/assets/{packages-panel-Cato7lRU.js → packages-panel-BdpRg9tX.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-bYFRKgrw.js → pair-with-agent-modal-CbUFKJHP.js} +1 -1
- package/dist/assets/{panel-accordion-state-CB4RQUbp.js → panel-accordion-state-DjUTN7i5.js} +1 -1
- package/dist/assets/{panels-CaAkyFQ2.js → panels-BsXw4GTa.js} +1 -1
- package/dist/assets/{process-output-BvjsWKqP.js → process-output-Ck7-P_xp.js} +1 -1
- package/dist/assets/{readonly-code-display-D0VodUl8.js → readonly-code-display--S2daDa6.js} +1 -1
- package/dist/assets/{readonly-python-code-DlajH1tG.js → readonly-python-code-DpIyjHqY.js} +1 -1
- package/dist/assets/{renderShortcut-DCEcfEd4.js → renderShortcut-X-CJUSM8.js} +1 -1
- package/dist/assets/{reveal-component-NBGQ6pCq.js → reveal-component-BuI7t0JO.js} +1 -1
- package/dist/assets/{run-page-TDSGBzJ2.js → run-page-DZNsqz9H.js} +1 -1
- package/dist/assets/{scratchpad-panel-smtKLHdh.js → scratchpad-panel-CARmXIqQ.js} +1 -1
- package/dist/assets/{secrets-panel-CWX3By7x.js → secrets-panel-D3BpDIgg.js} +1 -1
- package/dist/assets/{session-panel-BEpBnPuv.js → session-panel-CB5EvmZs.js} +1 -1
- package/dist/assets/{skeleton-BVafJh4G.js → skeleton-idYPIMw_.js} +1 -1
- package/dist/assets/{snippets-panel-D5Fdmjit.js → snippets-panel-D5yy7kj0.js} +1 -1
- package/dist/assets/{state-D2Q_Pzdk.js → state-B6AklCzD.js} +1 -1
- package/dist/assets/{state-CbrP-RfI.js → state-Di9TbuV9.js} +1 -1
- package/dist/assets/{switch-BzJYj8tz.js → switch-DjXr9Y9D.js} +1 -1
- package/dist/assets/{terminal-CWTEGSbX.js → terminal-CQAFGBfN.js} +1 -1
- package/dist/assets/{tracing-htk2wAUH.js → tracing-DNk3gmWv.js} +1 -1
- package/dist/assets/{tracing-panel-D_FtT8xh.js → tracing-panel-Yqazr6w_.js} +2 -2
- package/dist/assets/{tree-actions-CIKsGmOE.js → tree-actions-CG5g7aUQ.js} +1 -1
- package/dist/assets/{useAddCell-B9V9LPof.js → useAddCell-DdB_uBif.js} +1 -1
- package/dist/assets/{useBoolean-Dh6ALPsT.js → useBoolean-BXC4CItZ.js} +1 -1
- package/dist/assets/{useCellActionButton-Byz1dxEY.js → useCellActionButton-B31neI3m.js} +1 -1
- package/dist/assets/{useDebounce-Dulm61-t.js → useDebounce-DgpSQhrP.js} +1 -1
- package/dist/assets/{useDeleteCell-xIU8YxOm.js → useDeleteCell-Bz49_cFj.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CTX4KBOX.js → useDependencyPanelTab-C_ps0r--.js} +1 -1
- package/dist/assets/{useNotebookActions-CEWtEB4v.js → useNotebookActions-Br4AVVRg.js} +1 -1
- package/dist/assets/{useRunCells-DpLEou4d.js → useRunCells-CX7oW5zy.js} +1 -1
- package/dist/assets/{useSplitCell-DIMMMQzM.js → useSplitCell-Dy2_DmJR.js} +1 -1
- package/dist/assets/{useTheme-OSyXt1jZ.js → useTheme-C-ri8BtY.js} +1 -1
- package/dist/assets/utils-D5Ywm7-q.js +1 -0
- package/dist/assets/{vega-component-D23FlAOS.js → vega-component-C7TCzyaU.js} +1 -1
- package/dist/assets/{write-secret-modal-C2V1d4p-.js → write-secret-modal-D6zoZgLY.js} +1 -1
- package/dist/index.html +31 -31
- package/package.json +1 -1
- package/src/components/ai/ai-provider-icon.tsx +2 -2
- package/src/components/ai/ai-utils.ts +1 -1
- package/src/components/app-config/ai-config.tsx +33 -37
- package/src/components/app-config/user-config-form.tsx +1 -1
- package/src/components/data-table/renderers.tsx +2 -6
- package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +832 -0
- package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +27 -0
- package/src/components/editor/file-tree/__tests__/requesting-tree.test.ts +130 -0
- package/src/components/editor/file-tree/file-browser-row.tsx +82 -0
- package/src/components/editor/file-tree/file-explorer.tsx +377 -159
- package/src/components/editor/file-tree/file-operations.tsx +1 -1
- package/src/components/editor/file-tree/file-search-results.tsx +142 -0
- package/src/components/editor/file-tree/file-tree-row.tsx +40 -0
- package/src/components/editor/file-tree/file-viewer.tsx +2 -1
- package/src/components/editor/file-tree/requesting-tree.tsx +162 -32
- package/src/components/editor/file-tree/use-file-search.ts +142 -0
- package/src/components/editor/file-tree/use-hover-expand.ts +34 -0
- package/src/components/ui/links.tsx +2 -1
- package/src/core/ai/ids/__tests__/ids.test.ts +0 -8
- package/src/core/ai/ids/ids.ts +1 -1
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +14 -0
- package/src/core/network/requests-toasting.tsx +1 -1
- package/dist/assets/ai-model-dropdown-BN9nyr0_.js +0 -3
- package/dist/assets/file-explorer-panel-DB17Zw1x.js +0 -1
- package/dist/assets/file-name-input-DsaNomb7.js +0 -3
- package/dist/assets/index-D1E_LKIW.css +0 -2
- package/dist/assets/utils-B-_Wem_I.js +0 -1
|
@@ -16,14 +16,17 @@ import {
|
|
|
16
16
|
PlaySquareIcon,
|
|
17
17
|
UploadIcon,
|
|
18
18
|
ViewIcon,
|
|
19
|
+
XIcon,
|
|
19
20
|
} from "lucide-react";
|
|
20
21
|
import React, { Suspense, use, useRef, useState } from "react";
|
|
21
22
|
import {
|
|
23
|
+
type DragPreviewProps,
|
|
22
24
|
type NodeApi,
|
|
23
25
|
type NodeRendererProps,
|
|
24
26
|
Tree,
|
|
25
27
|
type TreeApi,
|
|
26
28
|
} from "react-arborist";
|
|
29
|
+
import { createPortal } from "react-dom";
|
|
27
30
|
import useEvent from "react-use-event-hook";
|
|
28
31
|
import {
|
|
29
32
|
FILE_ICON,
|
|
@@ -54,6 +57,7 @@ import {
|
|
|
54
57
|
DropdownMenuSeparator,
|
|
55
58
|
} from "@/components/ui/dropdown-menu";
|
|
56
59
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
60
|
+
import { SearchInput } from "@/components/ui/input";
|
|
57
61
|
import { toast } from "@/components/ui/use-toast";
|
|
58
62
|
import { useCellActions } from "@/core/cells/cells";
|
|
59
63
|
import { useLastFocusedCellId } from "@/core/cells/focus";
|
|
@@ -61,6 +65,8 @@ import { disableFileDownloadsAtom } from "@/core/config/config";
|
|
|
61
65
|
import { useRequestClient } from "@/core/network/requests";
|
|
62
66
|
import { isWasm } from "@/core/wasm/utils";
|
|
63
67
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
68
|
+
import { useFileSearch } from "./use-file-search";
|
|
69
|
+
import { useHoverExpand } from "./use-hover-expand";
|
|
64
70
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
65
71
|
import { cn } from "@/utils/cn";
|
|
66
72
|
import { copyToClipboard } from "@/utils/copy";
|
|
@@ -71,6 +77,8 @@ import { jotaiJsonStorage } from "@/utils/storage/jotai";
|
|
|
71
77
|
import { useTreeDndManager } from "./dnd-wrapper";
|
|
72
78
|
import { downloadFile } from "./download";
|
|
73
79
|
import { FileViewer } from "./file-viewer";
|
|
80
|
+
import { FileSearchResults } from "./file-search-results";
|
|
81
|
+
import { FileBrowserRow } from "./file-browser-row";
|
|
74
82
|
import type { FileTreeNode, RequestingTree } from "./requesting-tree";
|
|
75
83
|
import { openStateAtom, treeAtom } from "./state";
|
|
76
84
|
import { PYTHON_CODE_FOR_FILE_TYPE } from "./types";
|
|
@@ -102,6 +110,10 @@ export const FileExplorer: React.FC<{
|
|
|
102
110
|
externalDropDestinationPath?: FilePath | null;
|
|
103
111
|
}> = ({ height, externalDropDestinationPath = null }) => {
|
|
104
112
|
const treeRef = useRef<TreeApi<FileTreeNode>>(null);
|
|
113
|
+
const searchTreeRef = useRef<TreeApi<FileTreeNode>>(null);
|
|
114
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
115
|
+
const [query, setQuery] = useState("");
|
|
116
|
+
const [revealId, setRevealId] = useState<string | null>(null);
|
|
105
117
|
const dndManager = useTreeDndManager();
|
|
106
118
|
const [tree] = useAtom(treeAtom);
|
|
107
119
|
const [data, setData] = useState<FileTreeNode[]>([]);
|
|
@@ -112,6 +124,7 @@ export const FileExplorer: React.FC<{
|
|
|
112
124
|
} | null>(null);
|
|
113
125
|
const [showHiddenFiles, setShowHiddenFiles] =
|
|
114
126
|
useAtom<boolean>(hiddenFilesState);
|
|
127
|
+
const search = useFileSearch({ query: query.trim(), tree, showHiddenFiles });
|
|
115
128
|
// Keep external state to remember which folders are open when this
|
|
116
129
|
// component is unmounted.
|
|
117
130
|
const [openState, setOpenState] = useAtom(openStateAtom);
|
|
@@ -135,14 +148,12 @@ export const FileExplorer: React.FC<{
|
|
|
135
148
|
openUploadPicker();
|
|
136
149
|
});
|
|
137
150
|
|
|
138
|
-
const { openPrompt } = useImperativeModal();
|
|
151
|
+
const { openPrompt, openConfirm } = useImperativeModal();
|
|
139
152
|
const { isPending, error } = useAsyncData(() => tree.initialize(setData), []);
|
|
140
153
|
|
|
141
|
-
const handleRefresh = useEvent(() => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Object.keys(openState).filter((id) => openState[id]),
|
|
145
|
-
);
|
|
154
|
+
const handleRefresh = useEvent(async () => {
|
|
155
|
+
await tree.refreshAll(Object.keys(openState).filter((id) => openState[id]));
|
|
156
|
+
search.refetch();
|
|
146
157
|
});
|
|
147
158
|
|
|
148
159
|
const handleHiddenFilesToggle = useEvent(() => {
|
|
@@ -204,6 +215,77 @@ export const FileExplorer: React.FC<{
|
|
|
204
215
|
[tree, handleUploadFiles, externalDropDestinationPath],
|
|
205
216
|
);
|
|
206
217
|
|
|
218
|
+
React.useEffect(() => {
|
|
219
|
+
if (revealId && !openFile) {
|
|
220
|
+
const activeTree = query.trim() ? searchTreeRef.current : treeRef.current;
|
|
221
|
+
activeTree?.openParents(revealId);
|
|
222
|
+
activeTree?.select(revealId);
|
|
223
|
+
const node = activeTree?.get(revealId);
|
|
224
|
+
if (node?.data.isDirectory) {
|
|
225
|
+
node.open();
|
|
226
|
+
}
|
|
227
|
+
setRevealId(null);
|
|
228
|
+
}
|
|
229
|
+
}, [revealId, query, openFile, data]);
|
|
230
|
+
|
|
231
|
+
const handleReveal = async (node: FileTreeNode) => {
|
|
232
|
+
if (!node.isDirectory) {
|
|
233
|
+
setOpenFile(node);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (await tree.reveal(node)) {
|
|
237
|
+
setQuery("");
|
|
238
|
+
setRevealId(node.id);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const handleTreeKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
243
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "f") {
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
event.stopPropagation();
|
|
246
|
+
searchRef.current?.focus();
|
|
247
|
+
searchRef.current?.select();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (event.key === "Escape" && query) {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
event.stopPropagation();
|
|
253
|
+
setQuery("");
|
|
254
|
+
searchRef.current?.focus();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const role =
|
|
258
|
+
event.target instanceof Element
|
|
259
|
+
? event.target.getAttribute("role")
|
|
260
|
+
: null;
|
|
261
|
+
if (query.trim() || (role !== "treeitem" && role !== "tree")) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const node = treeRef.current?.focusedNode;
|
|
265
|
+
if (!node || treeRef.current?.isEditing) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (event.key === "Enter") {
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
event.stopPropagation();
|
|
271
|
+
if (node.data.isDirectory) {
|
|
272
|
+
node.toggle();
|
|
273
|
+
} else {
|
|
274
|
+
node.activate();
|
|
275
|
+
}
|
|
276
|
+
} else if (event.key === "F2" && !node.data.isRoot) {
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
event.stopPropagation();
|
|
279
|
+
node.edit();
|
|
280
|
+
} else if (event.key === "Delete" || event.key === "Backspace") {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
event.stopPropagation();
|
|
283
|
+
if (!node.data.isRoot) {
|
|
284
|
+
void node.tree.delete(node.id);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
207
289
|
if (isPending) {
|
|
208
290
|
return <Spinner size="medium" centered={true} />;
|
|
209
291
|
}
|
|
@@ -212,128 +294,230 @@ export const FileExplorer: React.FC<{
|
|
|
212
294
|
return <ErrorBanner error={error} />;
|
|
213
295
|
}
|
|
214
296
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
297
|
+
const preview = openFile ? (
|
|
298
|
+
<>
|
|
299
|
+
<div className="flex items-center pl-1 pr-3 shrink-0 border-b justify-between">
|
|
300
|
+
<Button
|
|
301
|
+
autoFocus={true}
|
|
302
|
+
aria-label="Back to files"
|
|
303
|
+
onClick={() => {
|
|
304
|
+
setRevealId(openFile.id);
|
|
305
|
+
setOpenFile(null);
|
|
306
|
+
}}
|
|
307
|
+
data-testid="file-explorer-back-button"
|
|
308
|
+
variant="text"
|
|
309
|
+
size="xs"
|
|
310
|
+
className="mb-0"
|
|
311
|
+
>
|
|
312
|
+
<ArrowLeftIcon size={16} />
|
|
313
|
+
</Button>
|
|
314
|
+
<span className="font-bold">{openFile.name}</span>
|
|
315
|
+
</div>
|
|
316
|
+
<Suspense>
|
|
317
|
+
<FileViewer
|
|
318
|
+
onOpenNotebook={
|
|
319
|
+
tree.isPrimaryNode(openFile)
|
|
320
|
+
? (evt) => {
|
|
321
|
+
const path = tree.getPrimaryRelativePath(
|
|
322
|
+
openFile.path as FilePath,
|
|
323
|
+
);
|
|
324
|
+
if (path !== null) {
|
|
325
|
+
openMarimoNotebook(evt, path);
|
|
241
326
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
327
|
+
}
|
|
328
|
+
: undefined
|
|
329
|
+
}
|
|
330
|
+
file={openFile}
|
|
331
|
+
/>
|
|
332
|
+
</Suspense>
|
|
333
|
+
</>
|
|
334
|
+
) : null;
|
|
250
335
|
|
|
251
336
|
return (
|
|
252
337
|
<>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
selectedFolder?.path ?? tree.getPrimaryRootPath(),
|
|
271
|
-
)}
|
|
272
|
-
onUpload={handleUploadFiles}
|
|
273
|
-
/>
|
|
274
|
-
<RequestingTreeContext value={contextValue}>
|
|
275
|
-
<Tree<FileTreeNode>
|
|
276
|
-
width="100%"
|
|
277
|
-
ref={treeRef}
|
|
278
|
-
height={height - 33}
|
|
279
|
-
className="h-full"
|
|
280
|
-
data={visibleData}
|
|
281
|
-
initialOpenState={openState}
|
|
282
|
-
openByDefault={false}
|
|
283
|
-
// Use shared DnD manager to prevent "Cannot have two HTML5 backends" error
|
|
284
|
-
dndManager={dndManager}
|
|
285
|
-
// Hide the drop cursor
|
|
286
|
-
renderCursor={() => null}
|
|
287
|
-
// Disable dropping files into files
|
|
288
|
-
disableDrag={(node) => node.isRoot}
|
|
289
|
-
disableDrop={({ parentNode, dragNodes }) =>
|
|
290
|
-
dragNodes.some((node) => node.data.isRoot) ||
|
|
291
|
-
(parentNode ? !parentNode.data.isDirectory : false)
|
|
292
|
-
}
|
|
293
|
-
onDelete={async ({ ids }) => {
|
|
294
|
-
for (const id of ids) {
|
|
295
|
-
await tree.delete(id);
|
|
338
|
+
{preview}
|
|
339
|
+
<div hidden={!!openFile} onKeyDownCapture={handleTreeKeyDown}>
|
|
340
|
+
<input
|
|
341
|
+
data-testid="file-explorer-upload-input"
|
|
342
|
+
{...getUploadInputProps()}
|
|
343
|
+
/>
|
|
344
|
+
<div>
|
|
345
|
+
<Toolbar
|
|
346
|
+
onRefresh={handleRefresh}
|
|
347
|
+
onHidden={handleHiddenFilesToggle}
|
|
348
|
+
showHiddenFiles={showHiddenFiles}
|
|
349
|
+
onCreateFile={handleCreateFile}
|
|
350
|
+
onCreateNotebook={handleCreateNotebook}
|
|
351
|
+
onCreateFolder={handleCreateFolder}
|
|
352
|
+
onCollapseAll={handleCollapseAll}
|
|
353
|
+
uploadDestinationPath={
|
|
354
|
+
selectedFolder?.path ?? tree.getPrimaryRootPath()
|
|
296
355
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
356
|
+
uploadDestinationLabel={getUploadDestinationLabel(
|
|
357
|
+
tree,
|
|
358
|
+
selectedFolder?.path ?? tree.getPrimaryRootPath(),
|
|
359
|
+
)}
|
|
360
|
+
onUpload={handleUploadFiles}
|
|
361
|
+
/>
|
|
362
|
+
<div className="relative -mt-1">
|
|
363
|
+
<SearchInput
|
|
364
|
+
ref={searchRef}
|
|
365
|
+
className="rounded-none pr-6 focus-visible:outline-1 focus-visible:outline-ring/50"
|
|
366
|
+
clearable={false}
|
|
367
|
+
aria-label="Search files and folders"
|
|
368
|
+
placeholder="Search"
|
|
369
|
+
title="Search names in all configured roots, up to 20 folders deep. Generated directories such as node_modules and .git are excluded."
|
|
370
|
+
value={query}
|
|
371
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
372
|
+
onKeyDown={(event) => {
|
|
373
|
+
const activeTree = query.trim()
|
|
374
|
+
? searchTreeRef.current
|
|
375
|
+
: treeRef.current;
|
|
376
|
+
if (event.key === "ArrowDown") {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
activeTree?.focus(activeTree.firstNode);
|
|
379
|
+
} else if (event.key === "Enter" && query.trim()) {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
activeTree?.firstNode?.activate();
|
|
382
|
+
}
|
|
383
|
+
}}
|
|
384
|
+
/>
|
|
385
|
+
{query && (
|
|
386
|
+
<Button
|
|
387
|
+
variant="text"
|
|
388
|
+
size="icon"
|
|
389
|
+
className="absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2"
|
|
390
|
+
aria-label="Clear search"
|
|
391
|
+
onClick={() => {
|
|
392
|
+
setQuery("");
|
|
393
|
+
searchRef.current?.focus();
|
|
394
|
+
}}
|
|
395
|
+
>
|
|
396
|
+
<XIcon className="h-3 w-3" />
|
|
397
|
+
</Button>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
<div hidden={!query.trim()}>
|
|
402
|
+
<FileSearchResults
|
|
403
|
+
treeRef={searchTreeRef}
|
|
404
|
+
search={search}
|
|
405
|
+
height={Math.max(0, height - 63)}
|
|
406
|
+
onReveal={handleReveal}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
<div hidden={!!query.trim()} className="relative">
|
|
410
|
+
<RequestingTreeContext value={contextValue}>
|
|
411
|
+
<Tree<FileTreeNode>
|
|
412
|
+
width="100%"
|
|
413
|
+
ref={treeRef}
|
|
414
|
+
height={Math.max(0, height - 63)}
|
|
415
|
+
className="h-full"
|
|
416
|
+
data={visibleData}
|
|
417
|
+
childrenAccessor={(node) =>
|
|
418
|
+
node.isDirectory ? node.children : null
|
|
419
|
+
}
|
|
420
|
+
selectionFollowsFocus={true}
|
|
421
|
+
rowClassName="outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring"
|
|
422
|
+
renderRow={FileBrowserRow}
|
|
423
|
+
renderDragPreview={(props) => (
|
|
424
|
+
<FileDragPreview {...props} tree={treeRef.current} />
|
|
425
|
+
)}
|
|
426
|
+
disableEdit={(node) => node.isRoot}
|
|
427
|
+
initialOpenState={openState}
|
|
428
|
+
openByDefault={false}
|
|
429
|
+
// Use shared DnD manager to prevent "Cannot have two HTML5 backends" error
|
|
430
|
+
dndManager={dndManager}
|
|
431
|
+
// Hide the drop cursor
|
|
432
|
+
renderCursor={() => null}
|
|
433
|
+
// Disable dropping files into files
|
|
434
|
+
disableDrag={(node) => node.isRoot}
|
|
435
|
+
disableDrop={({ parentNode, dragNodes }) =>
|
|
436
|
+
dragNodes.some((node) => node.data.isRoot) ||
|
|
437
|
+
(parentNode ? !parentNode.data.isDirectory : false)
|
|
438
|
+
}
|
|
439
|
+
onDelete={async ({ ids }) => {
|
|
440
|
+
openConfirm({
|
|
441
|
+
title: "Delete file",
|
|
442
|
+
description:
|
|
443
|
+
"Are you sure you want to delete the selected file or folder?",
|
|
444
|
+
confirmAction: (
|
|
445
|
+
<AlertDialogDestructiveAction
|
|
446
|
+
onClick={async () => {
|
|
447
|
+
for (const id of ids) {
|
|
448
|
+
await tree.delete(id);
|
|
449
|
+
}
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
Delete
|
|
453
|
+
</AlertDialogDestructiveAction>
|
|
454
|
+
),
|
|
455
|
+
});
|
|
456
|
+
}}
|
|
457
|
+
onRename={async ({ id, name }) => {
|
|
458
|
+
await tree.rename(id, name);
|
|
459
|
+
}}
|
|
460
|
+
onMove={async ({ dragIds, parentId }) => {
|
|
461
|
+
await tree.move(dragIds, parentId);
|
|
462
|
+
}}
|
|
463
|
+
onSelect={(nodes) => {
|
|
464
|
+
const first = nodes[0];
|
|
465
|
+
if (!first) {
|
|
466
|
+
setSelectedFolder(null);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (first.data.isDirectory) {
|
|
470
|
+
setSelectedFolder({
|
|
471
|
+
id: first.id,
|
|
472
|
+
path: first.data.path as FilePath,
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
setSelectedFolder(null);
|
|
477
|
+
}}
|
|
478
|
+
onActivate={(node) => {
|
|
479
|
+
if (!node.data.isDirectory) {
|
|
480
|
+
setOpenFile(node.data);
|
|
481
|
+
}
|
|
482
|
+
}}
|
|
483
|
+
onToggle={async (id) => {
|
|
484
|
+
const isOpen =
|
|
485
|
+
treeRef.current?.isOpen(id) ?? !(openState[id] ?? false);
|
|
486
|
+
if (!isOpen) {
|
|
487
|
+
setOpenState((previous) => ({ ...previous, [id]: false }));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const loaded = await tree.expand(id);
|
|
491
|
+
if (!loaded) {
|
|
492
|
+
treeRef.current?.close(id);
|
|
493
|
+
}
|
|
494
|
+
const remainsOpen =
|
|
495
|
+
loaded && (treeRef.current?.isOpen(id) ?? false);
|
|
496
|
+
setOpenState((previous) => ({
|
|
497
|
+
...previous,
|
|
498
|
+
[id]: remainsOpen,
|
|
499
|
+
}));
|
|
500
|
+
}}
|
|
501
|
+
padding={15}
|
|
502
|
+
rowHeight={30}
|
|
503
|
+
indent={INDENT_STEP}
|
|
504
|
+
overscanCount={8}
|
|
505
|
+
// Disable multi-selection
|
|
506
|
+
disableMultiSelection={true}
|
|
507
|
+
>
|
|
508
|
+
{Node}
|
|
509
|
+
</Tree>
|
|
510
|
+
</RequestingTreeContext>
|
|
511
|
+
{!visibleData.length && (
|
|
512
|
+
<p
|
|
513
|
+
role="status"
|
|
514
|
+
className="absolute top-2 left-3 text-sm text-muted-foreground"
|
|
515
|
+
>
|
|
516
|
+
No visible files.
|
|
517
|
+
</p>
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
337
521
|
</>
|
|
338
522
|
);
|
|
339
523
|
};
|
|
@@ -368,7 +552,7 @@ const Toolbar = ({
|
|
|
368
552
|
const uploadLabel = `Upload files to ${uploadDestinationLabel}`;
|
|
369
553
|
|
|
370
554
|
return (
|
|
371
|
-
<div className="flex items-center justify-end px-2 shrink-0
|
|
555
|
+
<div className="flex items-center justify-end px-2 shrink-0">
|
|
372
556
|
<Tooltip content="Add notebook">
|
|
373
557
|
<Button
|
|
374
558
|
data-testid="file-explorer-add-notebook-button"
|
|
@@ -445,16 +629,7 @@ const Show = ({
|
|
|
445
629
|
) => void;
|
|
446
630
|
}) => {
|
|
447
631
|
const label = (
|
|
448
|
-
<span
|
|
449
|
-
className="flex-1 overflow-hidden text-ellipsis"
|
|
450
|
-
onClick={(e) => {
|
|
451
|
-
if (node.data.isDirectory) {
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
e.stopPropagation();
|
|
455
|
-
node.select();
|
|
456
|
-
}}
|
|
457
|
-
>
|
|
632
|
+
<span className="flex-1 overflow-hidden text-ellipsis">
|
|
458
633
|
{node.data.name}
|
|
459
634
|
{node.data.isMarimoFile && node.data.isPrimaryRoot && !isWasm() && (
|
|
460
635
|
<span
|
|
@@ -484,7 +659,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileTreeNode>) => {
|
|
|
484
659
|
: guessFileIconType(node.data.name);
|
|
485
660
|
|
|
486
661
|
const Icon = FILE_ICON[fileType];
|
|
487
|
-
const {
|
|
662
|
+
const { openPrompt } = useImperativeModal();
|
|
488
663
|
const { createNewCell } = useCellActions();
|
|
489
664
|
const lastFocusedCellId = useLastFocusedCellId();
|
|
490
665
|
|
|
@@ -501,6 +676,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileTreeNode>) => {
|
|
|
501
676
|
const isExternalDropTarget =
|
|
502
677
|
node.data.isDirectory &&
|
|
503
678
|
fileExplorer?.externalDropDestinationPath === node.data.path;
|
|
679
|
+
useHoverExpand(node, isExternalDropTarget);
|
|
504
680
|
|
|
505
681
|
const handleOpenMarimoFile = async (
|
|
506
682
|
evt: Pick<Event, "stopPropagation" | "preventDefault">,
|
|
@@ -514,20 +690,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileTreeNode>) => {
|
|
|
514
690
|
const handleDeleteFile = async (evt: Event) => {
|
|
515
691
|
evt.stopPropagation();
|
|
516
692
|
evt.preventDefault();
|
|
517
|
-
|
|
518
|
-
title: "Delete file",
|
|
519
|
-
description: `Are you sure you want to delete ${node.data.name}?`,
|
|
520
|
-
confirmAction: (
|
|
521
|
-
<AlertDialogDestructiveAction
|
|
522
|
-
onClick={async () => {
|
|
523
|
-
await node.tree.delete(node.id);
|
|
524
|
-
}}
|
|
525
|
-
aria-label="Confirm"
|
|
526
|
-
>
|
|
527
|
-
Delete
|
|
528
|
-
</AlertDialogDestructiveAction>
|
|
529
|
-
),
|
|
530
|
-
});
|
|
693
|
+
await node.tree.delete(node.id);
|
|
531
694
|
};
|
|
532
695
|
|
|
533
696
|
const handleCreateFolder = useEvent(async () => {
|
|
@@ -576,40 +739,53 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileTreeNode>) => {
|
|
|
576
739
|
? { [FILE_EXPLORER_DIRECTORY_PATH_ATTRIBUTE]: node.data.path }
|
|
577
740
|
: {})}
|
|
578
741
|
className={cn(
|
|
579
|
-
"flex items-center cursor-pointer ml-1 text-muted-foreground whitespace-nowrap group",
|
|
742
|
+
"relative flex h-full items-center cursor-pointer ml-1 text-muted-foreground whitespace-nowrap group",
|
|
580
743
|
)}
|
|
581
744
|
draggable={!node.data.isRoot}
|
|
582
|
-
onClick={(
|
|
583
|
-
evt.stopPropagation();
|
|
745
|
+
onClick={() => {
|
|
584
746
|
if (node.data.isDirectory) {
|
|
585
|
-
node.select();
|
|
586
747
|
node.toggle();
|
|
587
748
|
}
|
|
588
749
|
}}
|
|
750
|
+
onDoubleClick={(event) => {
|
|
751
|
+
event.stopPropagation();
|
|
752
|
+
if (!node.data.isDirectory) {
|
|
753
|
+
node.activate();
|
|
754
|
+
}
|
|
755
|
+
}}
|
|
589
756
|
>
|
|
757
|
+
{Array.from({ length: node.level }, (_, level) => (
|
|
758
|
+
<span
|
|
759
|
+
key={level}
|
|
760
|
+
aria-hidden="true"
|
|
761
|
+
className="pointer-events-none absolute inset-y-0 border-l border-border"
|
|
762
|
+
style={{ left: level * INDENT_STEP + 8 }}
|
|
763
|
+
/>
|
|
764
|
+
))}
|
|
590
765
|
<FolderArrow node={node} />
|
|
591
766
|
<span
|
|
592
767
|
className={cn(
|
|
593
768
|
"flex items-center pl-1 py-1 cursor-pointer hover:bg-accent/50 hover:text-accent-foreground rounded-l flex-1 overflow-hidden group",
|
|
594
769
|
node.willReceiveDrop &&
|
|
770
|
+
node.tree.canDrop() &&
|
|
595
771
|
node.data.isDirectory &&
|
|
596
|
-
"bg-
|
|
772
|
+
"bg-primary/15 hover:bg-primary/15 text-accent-foreground",
|
|
597
773
|
node.isSelected &&
|
|
598
774
|
"bg-accent/60 hover:bg-accent/60 text-accent-foreground",
|
|
599
775
|
isExternalDropTarget &&
|
|
600
|
-
"bg-primary/15 hover:bg-primary/15 text-accent-foreground
|
|
776
|
+
"bg-primary/15 hover:bg-primary/15 text-accent-foreground",
|
|
601
777
|
)}
|
|
602
778
|
>
|
|
603
779
|
{node.data.isRoot ? (
|
|
604
780
|
<FolderRootIcon
|
|
605
|
-
className="w-
|
|
781
|
+
className="w-4 h-4 shrink-0 mr-2 text-primary"
|
|
606
782
|
strokeWidth={1.5}
|
|
607
783
|
/>
|
|
608
784
|
) : node.data.isMarimoFile ? (
|
|
609
|
-
<MarimoIcon className="w-
|
|
785
|
+
<MarimoIcon className="w-4 h-4 shrink-0 mr-2" strokeWidth={1.5} />
|
|
610
786
|
) : (
|
|
611
787
|
<Icon
|
|
612
|
-
className={cn("w-
|
|
788
|
+
className={cn("w-4 h-4 shrink-0 mr-2", FILE_ICON_COLOR[fileType])}
|
|
613
789
|
strokeWidth={1.5}
|
|
614
790
|
/>
|
|
615
791
|
)}
|
|
@@ -624,7 +800,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileTreeNode>) => {
|
|
|
624
800
|
>
|
|
625
801
|
{!node.data.isDirectory && (
|
|
626
802
|
<DropdownMenuItem
|
|
627
|
-
onSelect={() => node.
|
|
803
|
+
onSelect={() => node.activate()}
|
|
628
804
|
data-testid="file-explorer-open-file-menu-item"
|
|
629
805
|
>
|
|
630
806
|
<ViewIcon className={MENU_ITEM_ICON_CLASS} />
|
|
@@ -794,6 +970,18 @@ const FolderArrow = ({ node }: { node: NodeApi<FileTreeNode> }) => {
|
|
|
794
970
|
return <span className="w-4 h-4 shrink-0" />;
|
|
795
971
|
}
|
|
796
972
|
|
|
973
|
+
if (node.data.loadState === "loading") {
|
|
974
|
+
return (
|
|
975
|
+
<span
|
|
976
|
+
role="status"
|
|
977
|
+
aria-label={`Loading ${node.data.name}`}
|
|
978
|
+
className="w-4 h-4 shrink-0"
|
|
979
|
+
>
|
|
980
|
+
<Spinner size="small" />
|
|
981
|
+
</span>
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
797
985
|
return <TreeChevron isExpanded={node.isOpen} className="w-4 h-4" />;
|
|
798
986
|
};
|
|
799
987
|
|
|
@@ -852,3 +1040,33 @@ function treeContainsId(list: FileTreeNode[], id: string): boolean {
|
|
|
852
1040
|
(item.children ? treeContainsId(item.children, id) : false),
|
|
853
1041
|
);
|
|
854
1042
|
}
|
|
1043
|
+
|
|
1044
|
+
function FileDragPreview({
|
|
1045
|
+
mouse,
|
|
1046
|
+
id,
|
|
1047
|
+
isDragging,
|
|
1048
|
+
tree,
|
|
1049
|
+
}: DragPreviewProps & { tree: TreeApi<FileTreeNode> | null }) {
|
|
1050
|
+
const node = tree?.get(id);
|
|
1051
|
+
if (!isDragging || !mouse || !node) {
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
const fileType = node.data.isDirectory
|
|
1055
|
+
? "directory"
|
|
1056
|
+
: guessFileIconType(node.data.name);
|
|
1057
|
+
const Icon = FILE_ICON[fileType];
|
|
1058
|
+
return createPortal(
|
|
1059
|
+
<div
|
|
1060
|
+
className="pointer-events-none fixed left-0 top-0 z-[1000] opacity-[0.85]"
|
|
1061
|
+
style={{
|
|
1062
|
+
transform: `translate3d(${mouse.x + 12}px, ${mouse.y + 12}px, 0)`,
|
|
1063
|
+
}}
|
|
1064
|
+
>
|
|
1065
|
+
<div className="flex max-w-72 items-center gap-2 rounded-md border border-border bg-popover px-3 py-2 text-sm text-popover-foreground shadow-md">
|
|
1066
|
+
<Icon className={cn("h-4 w-4 shrink-0", FILE_ICON_COLOR[fileType])} />
|
|
1067
|
+
<span className="truncate">{node.data.name}</span>
|
|
1068
|
+
</div>
|
|
1069
|
+
</div>,
|
|
1070
|
+
document.body,
|
|
1071
|
+
);
|
|
1072
|
+
}
|