@marimo-team/islands 0.23.2-dev41 → 0.23.2-dev44

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 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-dev41"), showCodeInRunModeAtom = atom(true);
69024
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.2-dev44"), 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.2-dev41",
3
+ "version": "0.23.2-dev44",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -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
 
@@ -381,6 +381,7 @@ const Show = ({
381
381
  {node.data.name}
382
382
  {node.data.isMarimoFile && !isWasm() && (
383
383
  <span
384
+ data-testid="file-explorer-open-marimo-button"
384
385
  className="shrink-0 ml-2 text-sm hidden group-hover:inline hover:underline"
385
386
  onClick={onOpenMarimoFile}
386
387
  >
@@ -419,8 +420,7 @@ const Edit = ({ node }: { node: NodeApi<FileInfo> }) => {
419
420
  };
420
421
 
421
422
  const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
422
- const { openFile, sendCreateFileOrFolder, sendFileDetails } =
423
- useRequestClient();
423
+ const { openFile, sendFileDetails } = useRequestClient();
424
424
  const disableFileDownloads = useAtomValue(disableFileDownloadsAtom);
425
425
 
426
426
  const fileType: FileIconType = node.data.isDirectory
@@ -502,37 +502,14 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
502
502
  });
503
503
 
504
504
  const handleDuplicate = useEvent(async () => {
505
- if (!tree || node.data.isDirectory) {
505
+ if (!tree) {
506
506
  return;
507
507
  }
508
508
 
509
509
  const [name, extension] = fileSplit(node.data.name);
510
510
  const duplicateName = `${name}_copy${extension}`;
511
511
 
512
- try {
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
- }
512
+ await tree.copy(node.id, duplicateName);
536
513
  });
537
514
 
538
515
  const renderActions = () => {
@@ -581,12 +558,10 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
581
558
  <Edit3Icon className={ic} />
582
559
  Rename
583
560
  </DropdownMenuItem>
584
- {!node.data.isDirectory && (
585
- <DropdownMenuItem onSelect={handleDuplicate}>
586
- <CopyIcon className={ic} />
587
- Duplicate
588
- </DropdownMenuItem>
589
- )}
561
+ <DropdownMenuItem onSelect={handleDuplicate}>
562
+ <CopyIcon className={ic} />
563
+ Duplicate
564
+ </DropdownMenuItem>
590
565
  <DropdownMenuItem
591
566
  onSelect={async () => {
592
567
  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>;
@@ -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>;
@@ -369,6 +369,7 @@ const namesThatRequireSync = new Set<keyof RawBridge>([
369
369
  "rename_file",
370
370
  "create_file_or_directory",
371
371
  "delete_file_or_directory",
372
+ "copy_file_or_directory",
372
373
  "move_file_or_directory",
373
374
  "update_file",
374
375
  ]);