@marimo-team/islands 0.23.2-dev40 → 0.23.2-dev42
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/main.js +2 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +1 -0
- package/src/components/editor/file-tree/__tests__/requesting-tree.test.ts +33 -0
- package/src/components/editor/file-tree/file-explorer.tsx +7 -33
- package/src/components/editor/file-tree/requesting-tree.tsx +41 -0
- package/src/components/editor/file-tree/state.tsx +1 -0
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -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 +11 -0
- package/src/core/wasm/worker/types.ts +3 -0
- package/src/core/wasm/worker/worker.ts +1 -0
package/dist/main.js
CHANGED
|
@@ -69021,7 +69021,7 @@ ${c}
|
|
|
69021
69021
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
69022
69022
|
}
|
|
69023
69023
|
}
|
|
69024
|
-
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.2-
|
|
69024
|
+
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.2-dev42"), showCodeInRunModeAtom = atom(true);
|
|
69025
69025
|
atom(null);
|
|
69026
69026
|
var VIRTUAL_FILE_REGEX = /\/@file\/([^\s"&'/]+)\.([\dA-Za-z]+)/g, VirtualFileTracker = class e {
|
|
69027
69027
|
constructor() {
|
|
@@ -70412,6 +70412,7 @@ ${r}
|
|
|
70412
70412
|
__publicField(this, "sendPdb", throwNotImplemented);
|
|
70413
70413
|
__publicField(this, "sendCreateFileOrFolder", throwNotImplemented);
|
|
70414
70414
|
__publicField(this, "sendDeleteFileOrFolder", throwNotImplemented);
|
|
70415
|
+
__publicField(this, "sendCopyFileOrFolder", throwNotImplemented);
|
|
70415
70416
|
__publicField(this, "sendRenameFileOrFolder", throwNotImplemented);
|
|
70416
70417
|
__publicField(this, "sendUpdateFile", throwNotImplemented);
|
|
70417
70418
|
__publicField(this, "sendFileDetails", throwNotImplemented);
|
package/package.json
CHANGED
|
@@ -51,6 +51,7 @@ export const MockRequestClient = {
|
|
|
51
51
|
.mockResolvedValue({ files: [], query: "", total_found: 0 }),
|
|
52
52
|
sendCreateFileOrFolder: vi.fn().mockResolvedValue({}),
|
|
53
53
|
sendDeleteFileOrFolder: vi.fn().mockResolvedValue({}),
|
|
54
|
+
sendCopyFileOrFolder: vi.fn().mockResolvedValue({}),
|
|
54
55
|
sendRenameFileOrFolder: vi.fn().mockResolvedValue({}),
|
|
55
56
|
sendUpdateFile: vi.fn().mockResolvedValue({}),
|
|
56
57
|
sendFileDetails: vi.fn().mockResolvedValue({}),
|
|
@@ -8,6 +8,7 @@ import { RequestingTree } from "../requesting-tree";
|
|
|
8
8
|
const sendListFiles = vi.fn();
|
|
9
9
|
const sendCreateFileOrFolder = vi.fn();
|
|
10
10
|
const sendDeleteFileOrFolder = vi.fn();
|
|
11
|
+
const sendCopyFileOrFolder = vi.fn();
|
|
11
12
|
const sendRenameFileOrFolder = vi.fn();
|
|
12
13
|
|
|
13
14
|
vi.mock("@/components/ui/use-toast", () => MockModules.toast());
|
|
@@ -21,6 +22,7 @@ describe("RequestingTree", () => {
|
|
|
21
22
|
listFiles: sendListFiles,
|
|
22
23
|
createFileOrFolder: sendCreateFileOrFolder,
|
|
23
24
|
deleteFileOrFolder: sendDeleteFileOrFolder,
|
|
25
|
+
copyFileOrFolder: sendCopyFileOrFolder,
|
|
24
26
|
renameFileOrFolder: sendRenameFileOrFolder,
|
|
25
27
|
});
|
|
26
28
|
sendListFiles.mockResolvedValue({
|
|
@@ -169,6 +171,17 @@ describe("RequestingTree", () => {
|
|
|
169
171
|
`);
|
|
170
172
|
});
|
|
171
173
|
|
|
174
|
+
test("copy should duplicate a file", async () => {
|
|
175
|
+
sendCopyFileOrFolder.mockResolvedValue({ success: true });
|
|
176
|
+
|
|
177
|
+
await requestingTree.copy("1.1", "file1_copy");
|
|
178
|
+
expect(sendCopyFileOrFolder).toHaveBeenCalledWith({
|
|
179
|
+
path: "/root/file1",
|
|
180
|
+
newPath: "/root/file1_copy",
|
|
181
|
+
});
|
|
182
|
+
expect(mockOnChange).toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
172
185
|
test("createFile should create a new file", async () => {
|
|
173
186
|
sendCreateFileOrFolder.mockResolvedValue({ success: true });
|
|
174
187
|
|
|
@@ -236,6 +249,7 @@ describe("RequestingTree", () => {
|
|
|
236
249
|
listFiles: sendListFiles,
|
|
237
250
|
createFileOrFolder: sendCreateFileOrFolder,
|
|
238
251
|
deleteFileOrFolder: sendDeleteFileOrFolder,
|
|
252
|
+
copyFileOrFolder: sendCopyFileOrFolder,
|
|
239
253
|
renameFileOrFolder: sendRenameFileOrFolder,
|
|
240
254
|
});
|
|
241
255
|
sendListFiles.mockRejectedValue(new Error("Network error"));
|
|
@@ -263,6 +277,23 @@ describe("RequestingTree", () => {
|
|
|
263
277
|
});
|
|
264
278
|
});
|
|
265
279
|
|
|
280
|
+
test("copy should handle API failure", async () => {
|
|
281
|
+
sendCopyFileOrFolder.mockResolvedValue({
|
|
282
|
+
success: false,
|
|
283
|
+
message: "Error duplicating",
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await requestingTree.copy("1.1", "file1_copy");
|
|
287
|
+
expect(sendCopyFileOrFolder).toHaveBeenCalledWith({
|
|
288
|
+
path: "/root/file1",
|
|
289
|
+
newPath: "/root/file1_copy",
|
|
290
|
+
});
|
|
291
|
+
expect(toast).toHaveBeenCalledWith({
|
|
292
|
+
title: "Failed",
|
|
293
|
+
description: "Error duplicating",
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
266
297
|
test("move should handle missing parent node gracefully", async () => {
|
|
267
298
|
await requestingTree.move(["1.x"], "2");
|
|
268
299
|
expect(sendRenameFileOrFolder).not.toHaveBeenCalled();
|
|
@@ -284,6 +315,7 @@ describe("RequestingTree", () => {
|
|
|
284
315
|
listFiles: sendListFiles,
|
|
285
316
|
createFileOrFolder: sendCreateFileOrFolder,
|
|
286
317
|
deleteFileOrFolder: sendDeleteFileOrFolder,
|
|
318
|
+
copyFileOrFolder: sendCopyFileOrFolder,
|
|
287
319
|
renameFileOrFolder: sendRenameFileOrFolder,
|
|
288
320
|
});
|
|
289
321
|
|
|
@@ -305,6 +337,7 @@ describe("RequestingTree", () => {
|
|
|
305
337
|
listFiles: sendListFiles,
|
|
306
338
|
createFileOrFolder: sendCreateFileOrFolder,
|
|
307
339
|
deleteFileOrFolder: sendDeleteFileOrFolder,
|
|
340
|
+
copyFileOrFolder: sendCopyFileOrFolder,
|
|
308
341
|
renameFileOrFolder: sendRenameFileOrFolder,
|
|
309
342
|
});
|
|
310
343
|
|
|
@@ -419,8 +419,7 @@ const Edit = ({ node }: { node: NodeApi<FileInfo> }) => {
|
|
|
419
419
|
};
|
|
420
420
|
|
|
421
421
|
const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
422
|
-
const { openFile,
|
|
423
|
-
useRequestClient();
|
|
422
|
+
const { openFile, sendFileDetails } = useRequestClient();
|
|
424
423
|
const disableFileDownloads = useAtomValue(disableFileDownloadsAtom);
|
|
425
424
|
|
|
426
425
|
const fileType: FileIconType = node.data.isDirectory
|
|
@@ -502,37 +501,14 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
502
501
|
});
|
|
503
502
|
|
|
504
503
|
const handleDuplicate = useEvent(async () => {
|
|
505
|
-
if (!tree
|
|
504
|
+
if (!tree) {
|
|
506
505
|
return;
|
|
507
506
|
}
|
|
508
507
|
|
|
509
508
|
const [name, extension] = fileSplit(node.data.name);
|
|
510
509
|
const duplicateName = `${name}_copy${extension}`;
|
|
511
510
|
|
|
512
|
-
|
|
513
|
-
// First get the file contents
|
|
514
|
-
const details = await sendFileDetails({ path: node.data.path });
|
|
515
|
-
|
|
516
|
-
// Get the parent directory path
|
|
517
|
-
const parentPath = node.parent?.data.path || "";
|
|
518
|
-
|
|
519
|
-
// Create the duplicate file by creating a new file with the same contents
|
|
520
|
-
await sendCreateFileOrFolder({
|
|
521
|
-
path: parentPath,
|
|
522
|
-
type: "file",
|
|
523
|
-
name: duplicateName,
|
|
524
|
-
contents: details.contents ? btoa(details.contents) : undefined,
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
// Refresh the parent folder to show the new file
|
|
528
|
-
await tree.refreshAll([parentPath]);
|
|
529
|
-
} catch {
|
|
530
|
-
toast({
|
|
531
|
-
title: "Failed to duplicate file",
|
|
532
|
-
description: "Unable to create a duplicate of the file",
|
|
533
|
-
variant: "danger",
|
|
534
|
-
});
|
|
535
|
-
}
|
|
511
|
+
await tree.copy(node.id, duplicateName);
|
|
536
512
|
});
|
|
537
513
|
|
|
538
514
|
const renderActions = () => {
|
|
@@ -581,12 +557,10 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
581
557
|
<Edit3Icon className={ic} />
|
|
582
558
|
Rename
|
|
583
559
|
</DropdownMenuItem>
|
|
584
|
-
{
|
|
585
|
-
<
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
</DropdownMenuItem>
|
|
589
|
-
)}
|
|
560
|
+
<DropdownMenuItem onSelect={handleDuplicate}>
|
|
561
|
+
<CopyIcon className={ic} />
|
|
562
|
+
Duplicate
|
|
563
|
+
</DropdownMenuItem>
|
|
590
564
|
<DropdownMenuItem
|
|
591
565
|
onSelect={async () => {
|
|
592
566
|
await copyToClipboard(node.data.path);
|
|
@@ -17,6 +17,7 @@ export class RequestingTree {
|
|
|
17
17
|
listFiles: EditRequests["sendListFiles"];
|
|
18
18
|
createFileOrFolder: EditRequests["sendCreateFileOrFolder"];
|
|
19
19
|
deleteFileOrFolder: EditRequests["sendDeleteFileOrFolder"];
|
|
20
|
+
copyFileOrFolder: EditRequests["sendCopyFileOrFolder"];
|
|
20
21
|
renameFileOrFolder: EditRequests["sendRenameFileOrFolder"];
|
|
21
22
|
};
|
|
22
23
|
|
|
@@ -24,6 +25,7 @@ export class RequestingTree {
|
|
|
24
25
|
listFiles: EditRequests["sendListFiles"];
|
|
25
26
|
createFileOrFolder: EditRequests["sendCreateFileOrFolder"];
|
|
26
27
|
deleteFileOrFolder: EditRequests["sendDeleteFileOrFolder"];
|
|
28
|
+
copyFileOrFolder: EditRequests["sendCopyFileOrFolder"];
|
|
27
29
|
renameFileOrFolder: EditRequests["sendRenameFileOrFolder"];
|
|
28
30
|
}) {
|
|
29
31
|
this.callbacks = callbacks;
|
|
@@ -74,9 +76,44 @@ export class RequestingTree {
|
|
|
74
76
|
return true;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
async copy(id: string, newName: string): Promise<void> {
|
|
80
|
+
const node = this.delegate.find(id);
|
|
81
|
+
if (!node) {
|
|
82
|
+
toast({
|
|
83
|
+
title: "Failed",
|
|
84
|
+
description: `Node with id ${id} not found in the tree`,
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const currentPath = node.data.path as FilePath;
|
|
89
|
+
const parentPath = this.path.dirname(currentPath);
|
|
90
|
+
const newPath = this.path.join(parentPath, newName);
|
|
91
|
+
const newFile = await this.callbacks
|
|
92
|
+
.copyFileOrFolder({
|
|
93
|
+
path: currentPath,
|
|
94
|
+
newPath: newPath,
|
|
95
|
+
})
|
|
96
|
+
.then(this.handleResponse);
|
|
97
|
+
if (!newFile?.info) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.delegate.create({
|
|
101
|
+
parentId: node.parent?.id ?? null,
|
|
102
|
+
index: 0,
|
|
103
|
+
data: newFile.info,
|
|
104
|
+
});
|
|
105
|
+
this.onChange(this.delegate.data);
|
|
106
|
+
// Refresh the parent folder
|
|
107
|
+
await this.refreshAll([parentPath]);
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
async rename(id: string, name: string): Promise<void> {
|
|
78
111
|
const node = this.delegate.find(id);
|
|
79
112
|
if (!node) {
|
|
113
|
+
toast({
|
|
114
|
+
title: "Failed",
|
|
115
|
+
description: `Node with id ${id} not found in the tree`,
|
|
116
|
+
});
|
|
80
117
|
return;
|
|
81
118
|
}
|
|
82
119
|
const currentPath = node.data.path as FilePath;
|
|
@@ -172,6 +209,10 @@ export class RequestingTree {
|
|
|
172
209
|
async delete(id: string): Promise<void> {
|
|
173
210
|
const node = this.delegate.find(id);
|
|
174
211
|
if (!node) {
|
|
212
|
+
toast({
|
|
213
|
+
title: "Failed",
|
|
214
|
+
description: `Node with id ${id} not found in the tree`,
|
|
215
|
+
});
|
|
175
216
|
return;
|
|
176
217
|
}
|
|
177
218
|
|
|
@@ -15,6 +15,7 @@ export const treeAtom = atom<RequestingTree>((get) => {
|
|
|
15
15
|
listFiles: client.sendListFiles,
|
|
16
16
|
createFileOrFolder: client.sendCreateFileOrFolder,
|
|
17
17
|
deleteFileOrFolder: client.sendDeleteFileOrFolder,
|
|
18
|
+
copyFileOrFolder: client.sendCopyFileOrFolder,
|
|
18
19
|
renameFileOrFolder: client.sendRenameFileOrFolder,
|
|
19
20
|
});
|
|
20
21
|
});
|
|
@@ -237,6 +237,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
237
237
|
sendPdb = throwNotImplemented;
|
|
238
238
|
sendCreateFileOrFolder = throwNotImplemented;
|
|
239
239
|
sendDeleteFileOrFolder = throwNotImplemented;
|
|
240
|
+
sendCopyFileOrFolder = throwNotImplemented;
|
|
240
241
|
sendRenameFileOrFolder = throwNotImplemented;
|
|
241
242
|
sendUpdateFile = throwNotImplemented;
|
|
242
243
|
sendFileDetails = throwNotImplemented;
|
|
@@ -90,6 +90,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
90
90
|
sendSearchFiles: "startConnection",
|
|
91
91
|
sendCreateFileOrFolder: "throwError",
|
|
92
92
|
sendDeleteFileOrFolder: "throwError",
|
|
93
|
+
sendCopyFileOrFolder: "throwError",
|
|
93
94
|
sendRenameFileOrFolder: "throwError",
|
|
94
95
|
sendUpdateFile: "throwError",
|
|
95
96
|
sendFileDetails: "throwError",
|
|
@@ -312,6 +312,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
312
312
|
})
|
|
313
313
|
.then(handleResponse);
|
|
314
314
|
},
|
|
315
|
+
sendCopyFileOrFolder: async (request) => {
|
|
316
|
+
await waitForConnectionOpen();
|
|
317
|
+
return getClient()
|
|
318
|
+
.POST("/api/files/copy", {
|
|
319
|
+
body: request,
|
|
320
|
+
})
|
|
321
|
+
.then(handleResponse);
|
|
322
|
+
},
|
|
315
323
|
sendRenameFileOrFolder: async (request) => {
|
|
316
324
|
await waitForConnectionOpen();
|
|
317
325
|
return getClient()
|
|
@@ -66,6 +66,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
66
66
|
sendPdb: throwNotInEditMode,
|
|
67
67
|
sendCreateFileOrFolder: throwNotInEditMode,
|
|
68
68
|
sendDeleteFileOrFolder: throwNotInEditMode,
|
|
69
|
+
sendCopyFileOrFolder: throwNotInEditMode,
|
|
69
70
|
sendRenameFileOrFolder: throwNotInEditMode,
|
|
70
71
|
sendUpdateFile: throwNotInEditMode,
|
|
71
72
|
sendFileDetails: throwNotInEditMode,
|
|
@@ -51,6 +51,7 @@ export function createErrorToastingRequests(
|
|
|
51
51
|
sendPdb: "Failed to start debug session",
|
|
52
52
|
sendCreateFileOrFolder: "Failed to create file or folder",
|
|
53
53
|
sendDeleteFileOrFolder: "Failed to delete file or folder",
|
|
54
|
+
sendCopyFileOrFolder: "Failed to duplicate file or folder",
|
|
54
55
|
sendRenameFileOrFolder: "Failed to rename file or folder",
|
|
55
56
|
sendUpdateFile: "Failed to update file",
|
|
56
57
|
sendFileDetails: "Failed to get file details",
|
|
@@ -23,6 +23,8 @@ export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
|
|
|
23
23
|
export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
|
|
24
24
|
export type ExportAsPDFRequest = schemas["ExportAsPDFRequest"];
|
|
25
25
|
export type UpdateCellOutputsRequest = schemas["UpdateCellOutputsRequest"];
|
|
26
|
+
export type FileCopyRequest = schemas["FileCopyRequest"];
|
|
27
|
+
export type FileCopyResponse = schemas["FileCopyResponse"];
|
|
26
28
|
export type FileCreateRequest = schemas["FileCreateRequest"];
|
|
27
29
|
export type FileCreateResponse = schemas["FileCreateResponse"];
|
|
28
30
|
export type FileDeleteRequest = schemas["FileDeleteRequest"];
|
|
@@ -168,6 +170,7 @@ export interface EditRequests {
|
|
|
168
170
|
sendDeleteFileOrFolder: (
|
|
169
171
|
request: FileDeleteRequest,
|
|
170
172
|
) => Promise<FileDeleteResponse>;
|
|
173
|
+
sendCopyFileOrFolder: (request: FileCopyRequest) => Promise<FileCopyResponse>;
|
|
171
174
|
sendRenameFileOrFolder: (
|
|
172
175
|
request: FileMoveRequest,
|
|
173
176
|
) => Promise<FileMoveResponse>;
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
EditRequests,
|
|
18
18
|
ExportAsHTMLRequest,
|
|
19
19
|
ExportAsMarkdownRequest,
|
|
20
|
+
FileCopyResponse,
|
|
20
21
|
FileCreateResponse,
|
|
21
22
|
FileDeleteResponse,
|
|
22
23
|
FileDetailsResponse,
|
|
@@ -443,6 +444,16 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
443
444
|
return response as FileDeleteResponse;
|
|
444
445
|
};
|
|
445
446
|
|
|
447
|
+
sendCopyFileOrFolder: EditRequests["sendCopyFileOrFolder"] = async (
|
|
448
|
+
request,
|
|
449
|
+
) => {
|
|
450
|
+
const response = await this.rpc.proxy.request.bridge({
|
|
451
|
+
functionName: "copy_file_or_directory",
|
|
452
|
+
payload: request,
|
|
453
|
+
});
|
|
454
|
+
return response as FileCopyResponse;
|
|
455
|
+
};
|
|
456
|
+
|
|
446
457
|
sendRenameFileOrFolder: EditRequests["sendRenameFileOrFolder"] = async (
|
|
447
458
|
request,
|
|
448
459
|
) => {
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
CopyNotebookRequest,
|
|
12
12
|
ExportAsHTMLRequest,
|
|
13
13
|
ExportAsMarkdownRequest,
|
|
14
|
+
FileCopyRequest,
|
|
15
|
+
FileCopyResponse,
|
|
14
16
|
FileCreateRequest,
|
|
15
17
|
FileCreateResponse,
|
|
16
18
|
FileDeleteRequest,
|
|
@@ -86,6 +88,7 @@ export interface RawBridge {
|
|
|
86
88
|
delete_file_or_directory(
|
|
87
89
|
request: FileDeleteRequest,
|
|
88
90
|
): Promise<FileDeleteResponse>;
|
|
91
|
+
copy_file_or_directory(request: FileCopyRequest): Promise<FileCopyResponse>;
|
|
89
92
|
move_file_or_directory(request: FileMoveRequest): Promise<FileMoveResponse>;
|
|
90
93
|
update_file(request: FileUpdateRequest): Promise<FileUpdateResponse>;
|
|
91
94
|
load_packages(request: string): Promise<string>;
|