@marimo-team/islands 0.23.3-dev71 → 0.23.3-dev8
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/{chat-ui-CTt4WX0V.js → chat-ui-BLFhPclV.js} +2 -2
- package/dist/{html-to-image-BdsDysfl.js → html-to-image-XYwXqg2E.js} +2107 -2107
- package/dist/main.js +6 -6
- package/dist/{process-output-COL2Pf5I.js → process-output-BDVjDpbu.js} +1 -1
- package/dist/{reveal-component-Cd5Y35Ny.js → reveal-component-CrnLosc4.js} +2 -2
- package/dist/{slide-BEerfanN.js → slide-Dl7Rf496.js} +1 -1
- package/package.json +2 -2
- package/src/components/editor/file-tree/__tests__/requesting-tree.test.ts +84 -2
- package/src/components/editor/file-tree/file-explorer.tsx +142 -203
- package/src/components/editor/file-tree/file-name-input.tsx +41 -0
- package/src/components/editor/file-tree/file-operations.tsx +266 -0
- package/src/components/editor/file-tree/requesting-tree.tsx +68 -49
- package/src/components/home/state.ts +13 -1
- package/src/components/pages/home-page.tsx +116 -10
- package/src/core/network/requests-network.ts +0 -3
- package/src/utils/__tests__/path.test.ts +20 -0
- package/src/utils/pathUtils.test.ts +141 -1
- package/src/utils/pathUtils.ts +46 -0
- package/src/utils/paths.ts +9 -1
|
@@ -6,21 +6,18 @@ import {
|
|
|
6
6
|
ArrowLeftIcon,
|
|
7
7
|
BetweenHorizontalStartIcon,
|
|
8
8
|
BracesIcon,
|
|
9
|
-
CopyIcon,
|
|
10
9
|
CopyMinusIcon,
|
|
11
10
|
DownloadIcon,
|
|
12
|
-
Edit3Icon,
|
|
13
11
|
ExternalLinkIcon,
|
|
14
12
|
EyeOffIcon,
|
|
15
13
|
FilePlus2Icon,
|
|
16
14
|
FolderPlusIcon,
|
|
17
15
|
ListTreeIcon,
|
|
18
16
|
PlaySquareIcon,
|
|
19
|
-
Trash2Icon,
|
|
20
17
|
UploadIcon,
|
|
21
18
|
ViewIcon,
|
|
22
19
|
} from "lucide-react";
|
|
23
|
-
import React, { Suspense, use,
|
|
20
|
+
import React, { Suspense, use, useRef, useState } from "react";
|
|
24
21
|
import {
|
|
25
22
|
type NodeApi,
|
|
26
23
|
type NodeRendererProps,
|
|
@@ -34,9 +31,15 @@ import {
|
|
|
34
31
|
type FileIconType,
|
|
35
32
|
guessFileIconType,
|
|
36
33
|
} from "@/components/editor/file-tree/file-icons";
|
|
34
|
+
import {
|
|
35
|
+
DeleteMenuItem,
|
|
36
|
+
DuplicateMenuItem,
|
|
37
|
+
FileActionsDropdown,
|
|
38
|
+
RenameMenuItem,
|
|
39
|
+
} from "@/components/editor/file-tree/file-operations";
|
|
40
|
+
import { FileNameInput } from "@/components/editor/file-tree/file-name-input";
|
|
37
41
|
import {
|
|
38
42
|
MENU_ITEM_ICON_CLASS,
|
|
39
|
-
MoreActionsButton,
|
|
40
43
|
RefreshIconButton,
|
|
41
44
|
TreeChevron,
|
|
42
45
|
} from "@/components/editor/file-tree/tree-actions";
|
|
@@ -46,11 +49,8 @@ import { useImperativeModal } from "@/components/modal/ImperativeModal";
|
|
|
46
49
|
import { AlertDialogDestructiveAction } from "@/components/ui/alert-dialog";
|
|
47
50
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
48
51
|
import {
|
|
49
|
-
DropdownMenu,
|
|
50
|
-
DropdownMenuContent,
|
|
51
52
|
DropdownMenuItem,
|
|
52
53
|
DropdownMenuSeparator,
|
|
53
|
-
DropdownMenuTrigger,
|
|
54
54
|
} from "@/components/ui/dropdown-menu";
|
|
55
55
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
56
56
|
import { toast } from "@/components/ui/use-toast";
|
|
@@ -69,7 +69,7 @@ import { downloadBlob } from "@/utils/download";
|
|
|
69
69
|
import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
|
|
70
70
|
import { openNotebook } from "@/utils/links";
|
|
71
71
|
import type { FilePath } from "@/utils/paths";
|
|
72
|
-
import {
|
|
72
|
+
import { makeDuplicateName } from "@/utils/pathUtils";
|
|
73
73
|
import { jotaiJsonStorage } from "@/utils/storage/jotai";
|
|
74
74
|
import { useTreeDndManager } from "./dnd-wrapper";
|
|
75
75
|
import { FileViewer } from "./file-viewer";
|
|
@@ -131,7 +131,7 @@ export const FileExplorer: React.FC<{
|
|
|
131
131
|
openPrompt({
|
|
132
132
|
title: "File name",
|
|
133
133
|
onConfirm: async (name) => {
|
|
134
|
-
tree.createFile(name, null);
|
|
134
|
+
tree.createFile({ name, parentId: null });
|
|
135
135
|
},
|
|
136
136
|
});
|
|
137
137
|
});
|
|
@@ -140,7 +140,7 @@ export const FileExplorer: React.FC<{
|
|
|
140
140
|
openPrompt({
|
|
141
141
|
title: "Notebook name",
|
|
142
142
|
onConfirm: async (name) => {
|
|
143
|
-
tree.createFile(name, null, "notebook");
|
|
143
|
+
tree.createFile({ name, parentId: null, type: "notebook" });
|
|
144
144
|
},
|
|
145
145
|
});
|
|
146
146
|
});
|
|
@@ -392,33 +392,6 @@ const Show = ({
|
|
|
392
392
|
);
|
|
393
393
|
};
|
|
394
394
|
|
|
395
|
-
const Edit = ({ node }: { node: NodeApi<FileInfo> }) => {
|
|
396
|
-
const ref = useRef<HTMLInputElement>(null);
|
|
397
|
-
useEffect(() => {
|
|
398
|
-
ref.current?.focus();
|
|
399
|
-
// Select everything, but the extension
|
|
400
|
-
ref.current?.setSelectionRange(0, node.data.name.lastIndexOf("."));
|
|
401
|
-
}, [node.data.name]);
|
|
402
|
-
|
|
403
|
-
return (
|
|
404
|
-
<input
|
|
405
|
-
ref={ref}
|
|
406
|
-
className="flex-1 bg-transparent border border-border text-muted-foreground"
|
|
407
|
-
defaultValue={node.data.name}
|
|
408
|
-
onClick={(e) => e.stopPropagation()}
|
|
409
|
-
onBlur={() => node.reset()}
|
|
410
|
-
onKeyDown={(e) => {
|
|
411
|
-
if (e.key === "Escape") {
|
|
412
|
-
node.reset();
|
|
413
|
-
}
|
|
414
|
-
if (e.key === "Enter") {
|
|
415
|
-
node.submit(e.currentTarget.value);
|
|
416
|
-
}
|
|
417
|
-
}}
|
|
418
|
-
/>
|
|
419
|
-
);
|
|
420
|
-
};
|
|
421
|
-
|
|
422
395
|
const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
423
396
|
const { openFile, sendFileDetails } = useRequestClient();
|
|
424
397
|
const disableFileDownloads = useAtomValue(disableFileDownloadsAtom);
|
|
@@ -486,7 +459,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
486
459
|
openPrompt({
|
|
487
460
|
title: "File name",
|
|
488
461
|
onConfirm: async (name) => {
|
|
489
|
-
tree?.createFile(name, node.id);
|
|
462
|
+
tree?.createFile({ name, parentId: node.id });
|
|
490
463
|
},
|
|
491
464
|
});
|
|
492
465
|
});
|
|
@@ -496,7 +469,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
496
469
|
openPrompt({
|
|
497
470
|
title: "Notebook name",
|
|
498
471
|
onConfirm: async (name) => {
|
|
499
|
-
tree?.createFile(name, node.id, "notebook");
|
|
472
|
+
tree?.createFile({ name, parentId: node.id, type: "notebook" });
|
|
500
473
|
},
|
|
501
474
|
});
|
|
502
475
|
});
|
|
@@ -505,158 +478,9 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
505
478
|
if (!tree) {
|
|
506
479
|
return;
|
|
507
480
|
}
|
|
508
|
-
|
|
509
|
-
const [name, extension] = fileSplit(node.data.name);
|
|
510
|
-
const duplicateName = `${name}_copy${extension}`;
|
|
511
|
-
|
|
512
|
-
await tree.copy(node.id, duplicateName);
|
|
481
|
+
await tree.copy(node.id, makeDuplicateName(node.data.name));
|
|
513
482
|
});
|
|
514
483
|
|
|
515
|
-
const renderActions = () => {
|
|
516
|
-
const ic = MENU_ITEM_ICON_CLASS;
|
|
517
|
-
return (
|
|
518
|
-
<DropdownMenuContent
|
|
519
|
-
align="end"
|
|
520
|
-
className="print:hidden w-[220px]"
|
|
521
|
-
onClick={(e) => e.stopPropagation()}
|
|
522
|
-
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
523
|
-
>
|
|
524
|
-
{!node.data.isDirectory && (
|
|
525
|
-
<DropdownMenuItem onSelect={() => node.select()}>
|
|
526
|
-
<ViewIcon className={ic} />
|
|
527
|
-
Open file
|
|
528
|
-
</DropdownMenuItem>
|
|
529
|
-
)}
|
|
530
|
-
{!node.data.isDirectory && !isWasm() && (
|
|
531
|
-
<DropdownMenuItem
|
|
532
|
-
onSelect={() => {
|
|
533
|
-
openFile({ path: node.data.path });
|
|
534
|
-
}}
|
|
535
|
-
>
|
|
536
|
-
<ExternalLinkIcon className={ic} />
|
|
537
|
-
Open file in external editor
|
|
538
|
-
</DropdownMenuItem>
|
|
539
|
-
)}
|
|
540
|
-
{node.data.isDirectory && (
|
|
541
|
-
<>
|
|
542
|
-
<DropdownMenuItem onSelect={() => handleCreateNotebook()}>
|
|
543
|
-
<MarimoPlusIcon className={ic} />
|
|
544
|
-
Create notebook
|
|
545
|
-
</DropdownMenuItem>
|
|
546
|
-
<DropdownMenuItem onSelect={() => handleCreateFile()}>
|
|
547
|
-
<FilePlus2Icon className={ic} />
|
|
548
|
-
Create file
|
|
549
|
-
</DropdownMenuItem>
|
|
550
|
-
<DropdownMenuItem onSelect={() => handleCreateFolder()}>
|
|
551
|
-
<FolderPlusIcon className={ic} />
|
|
552
|
-
Create folder
|
|
553
|
-
</DropdownMenuItem>
|
|
554
|
-
<DropdownMenuSeparator />
|
|
555
|
-
</>
|
|
556
|
-
)}
|
|
557
|
-
<DropdownMenuItem onSelect={() => node.edit()}>
|
|
558
|
-
<Edit3Icon className={ic} />
|
|
559
|
-
Rename
|
|
560
|
-
</DropdownMenuItem>
|
|
561
|
-
<DropdownMenuItem onSelect={handleDuplicate}>
|
|
562
|
-
<CopyIcon className={ic} />
|
|
563
|
-
Duplicate
|
|
564
|
-
</DropdownMenuItem>
|
|
565
|
-
<DropdownMenuItem
|
|
566
|
-
onSelect={async () => {
|
|
567
|
-
await copyToClipboard(node.data.path);
|
|
568
|
-
toast({ title: "Copied to clipboard" });
|
|
569
|
-
}}
|
|
570
|
-
>
|
|
571
|
-
<ListTreeIcon className={ic} />
|
|
572
|
-
Copy path
|
|
573
|
-
</DropdownMenuItem>
|
|
574
|
-
{tree && (
|
|
575
|
-
<DropdownMenuItem
|
|
576
|
-
onSelect={async () => {
|
|
577
|
-
await copyToClipboard(
|
|
578
|
-
tree.relativeFromRoot(node.data.path as FilePath),
|
|
579
|
-
);
|
|
580
|
-
toast({ title: "Copied to clipboard" });
|
|
581
|
-
}}
|
|
582
|
-
>
|
|
583
|
-
<ListTreeIcon className={ic} />
|
|
584
|
-
Copy relative path
|
|
585
|
-
</DropdownMenuItem>
|
|
586
|
-
)}
|
|
587
|
-
<DropdownMenuSeparator />
|
|
588
|
-
|
|
589
|
-
<DropdownMenuItem
|
|
590
|
-
onSelect={() => {
|
|
591
|
-
const { path } = node.data;
|
|
592
|
-
const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
|
|
593
|
-
handleInsertCode(pythonCode);
|
|
594
|
-
}}
|
|
595
|
-
>
|
|
596
|
-
<BetweenHorizontalStartIcon className={ic} />
|
|
597
|
-
Insert snippet for reading file
|
|
598
|
-
</DropdownMenuItem>
|
|
599
|
-
<DropdownMenuItem
|
|
600
|
-
onSelect={async () => {
|
|
601
|
-
toast({
|
|
602
|
-
title: "Copied to clipboard",
|
|
603
|
-
description:
|
|
604
|
-
"Code to open the file has been copied to your clipboard. You can also drag and drop this file into the editor",
|
|
605
|
-
});
|
|
606
|
-
const { path } = node.data;
|
|
607
|
-
const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
|
|
608
|
-
await copyToClipboard(pythonCode);
|
|
609
|
-
}}
|
|
610
|
-
>
|
|
611
|
-
<BracesIcon className={ic} />
|
|
612
|
-
Copy snippet for reading file
|
|
613
|
-
</DropdownMenuItem>
|
|
614
|
-
{/* Not shown in WASM */}
|
|
615
|
-
{node.data.isMarimoFile && !isWasm() && (
|
|
616
|
-
<>
|
|
617
|
-
<DropdownMenuSeparator />
|
|
618
|
-
<DropdownMenuItem onSelect={handleOpenMarimoFile}>
|
|
619
|
-
<PlaySquareIcon className={ic} />
|
|
620
|
-
Open notebook
|
|
621
|
-
</DropdownMenuItem>
|
|
622
|
-
</>
|
|
623
|
-
)}
|
|
624
|
-
<DropdownMenuSeparator />
|
|
625
|
-
{!node.data.isDirectory && !disableFileDownloads && (
|
|
626
|
-
<>
|
|
627
|
-
<DropdownMenuItem
|
|
628
|
-
onSelect={async () => {
|
|
629
|
-
const details = await sendFileDetails({ path: node.data.path });
|
|
630
|
-
if (details.isBase64 && details.contents) {
|
|
631
|
-
const blob = deserializeBlob(
|
|
632
|
-
base64ToDataURL(
|
|
633
|
-
details.contents as Base64String,
|
|
634
|
-
details.mimeType || "application/octet-stream",
|
|
635
|
-
),
|
|
636
|
-
);
|
|
637
|
-
downloadBlob(blob, node.data.name);
|
|
638
|
-
} else {
|
|
639
|
-
downloadBlob(
|
|
640
|
-
new Blob([details.contents || ""]),
|
|
641
|
-
node.data.name,
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
}}
|
|
645
|
-
>
|
|
646
|
-
<DownloadIcon className={ic} />
|
|
647
|
-
Download
|
|
648
|
-
</DropdownMenuItem>
|
|
649
|
-
<DropdownMenuSeparator />
|
|
650
|
-
</>
|
|
651
|
-
)}
|
|
652
|
-
<DropdownMenuItem onSelect={handleDeleteFile} variant="danger">
|
|
653
|
-
<Trash2Icon className={ic} />
|
|
654
|
-
Delete
|
|
655
|
-
</DropdownMenuItem>
|
|
656
|
-
</DropdownMenuContent>
|
|
657
|
-
);
|
|
658
|
-
};
|
|
659
|
-
|
|
660
484
|
return (
|
|
661
485
|
<div
|
|
662
486
|
style={style}
|
|
@@ -690,23 +514,138 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
690
514
|
/>
|
|
691
515
|
)}
|
|
692
516
|
{node.isEditing ? (
|
|
693
|
-
<
|
|
517
|
+
<FileNameInput node={node} />
|
|
694
518
|
) : (
|
|
695
519
|
<Show node={node} onOpenMarimoFile={handleOpenMarimoFile} />
|
|
696
520
|
)}
|
|
697
|
-
<
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
521
|
+
<FileActionsDropdown
|
|
522
|
+
testId="file-explorer-more-button"
|
|
523
|
+
iconClassName="w-5 h-5"
|
|
524
|
+
>
|
|
525
|
+
{!node.data.isDirectory && (
|
|
526
|
+
<DropdownMenuItem onSelect={() => node.select()}>
|
|
527
|
+
<ViewIcon className={MENU_ITEM_ICON_CLASS} />
|
|
528
|
+
Open file
|
|
529
|
+
</DropdownMenuItem>
|
|
530
|
+
)}
|
|
531
|
+
{!node.data.isDirectory && !isWasm() && (
|
|
532
|
+
<DropdownMenuItem
|
|
533
|
+
onSelect={() => {
|
|
534
|
+
openFile({ path: node.data.path });
|
|
535
|
+
}}
|
|
536
|
+
>
|
|
537
|
+
<ExternalLinkIcon className={MENU_ITEM_ICON_CLASS} />
|
|
538
|
+
Open file in external editor
|
|
539
|
+
</DropdownMenuItem>
|
|
540
|
+
)}
|
|
541
|
+
{node.data.isDirectory && (
|
|
542
|
+
<>
|
|
543
|
+
<DropdownMenuItem onSelect={() => handleCreateNotebook()}>
|
|
544
|
+
<MarimoPlusIcon className={MENU_ITEM_ICON_CLASS} />
|
|
545
|
+
Create notebook
|
|
546
|
+
</DropdownMenuItem>
|
|
547
|
+
<DropdownMenuItem onSelect={() => handleCreateFile()}>
|
|
548
|
+
<FilePlus2Icon className={MENU_ITEM_ICON_CLASS} />
|
|
549
|
+
Create file
|
|
550
|
+
</DropdownMenuItem>
|
|
551
|
+
<DropdownMenuItem onSelect={() => handleCreateFolder()}>
|
|
552
|
+
<FolderPlusIcon className={MENU_ITEM_ICON_CLASS} />
|
|
553
|
+
Create folder
|
|
554
|
+
</DropdownMenuItem>
|
|
555
|
+
<DropdownMenuSeparator />
|
|
556
|
+
</>
|
|
557
|
+
)}
|
|
558
|
+
<RenameMenuItem onSelect={() => node.edit()} />
|
|
559
|
+
<DuplicateMenuItem onSelect={handleDuplicate} />
|
|
560
|
+
<DropdownMenuItem
|
|
561
|
+
onSelect={async () => {
|
|
562
|
+
await copyToClipboard(node.data.path);
|
|
563
|
+
toast({ title: "Copied to clipboard" });
|
|
564
|
+
}}
|
|
702
565
|
>
|
|
703
|
-
<
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
566
|
+
<ListTreeIcon className={MENU_ITEM_ICON_CLASS} />
|
|
567
|
+
Copy path
|
|
568
|
+
</DropdownMenuItem>
|
|
569
|
+
{tree && (
|
|
570
|
+
<DropdownMenuItem
|
|
571
|
+
onSelect={async () => {
|
|
572
|
+
await copyToClipboard(
|
|
573
|
+
tree.relativeFromRoot(node.data.path as FilePath),
|
|
574
|
+
);
|
|
575
|
+
toast({ title: "Copied to clipboard" });
|
|
576
|
+
}}
|
|
577
|
+
>
|
|
578
|
+
<ListTreeIcon className={MENU_ITEM_ICON_CLASS} />
|
|
579
|
+
Copy relative path
|
|
580
|
+
</DropdownMenuItem>
|
|
581
|
+
)}
|
|
582
|
+
<DropdownMenuSeparator />
|
|
583
|
+
<DropdownMenuItem
|
|
584
|
+
onSelect={() => {
|
|
585
|
+
const { path } = node.data;
|
|
586
|
+
const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
|
|
587
|
+
handleInsertCode(pythonCode);
|
|
588
|
+
}}
|
|
589
|
+
>
|
|
590
|
+
<BetweenHorizontalStartIcon className={MENU_ITEM_ICON_CLASS} />
|
|
591
|
+
Insert snippet for reading file
|
|
592
|
+
</DropdownMenuItem>
|
|
593
|
+
<DropdownMenuItem
|
|
594
|
+
onSelect={async () => {
|
|
595
|
+
toast({
|
|
596
|
+
title: "Copied to clipboard",
|
|
597
|
+
description:
|
|
598
|
+
"Code to open the file has been copied to your clipboard. You can also drag and drop this file into the editor",
|
|
599
|
+
});
|
|
600
|
+
const { path } = node.data;
|
|
601
|
+
const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
|
|
602
|
+
await copyToClipboard(pythonCode);
|
|
603
|
+
}}
|
|
604
|
+
>
|
|
605
|
+
<BracesIcon className={MENU_ITEM_ICON_CLASS} />
|
|
606
|
+
Copy snippet for reading file
|
|
607
|
+
</DropdownMenuItem>
|
|
608
|
+
{node.data.isMarimoFile && !isWasm() && (
|
|
609
|
+
<>
|
|
610
|
+
<DropdownMenuSeparator />
|
|
611
|
+
<DropdownMenuItem onSelect={handleOpenMarimoFile}>
|
|
612
|
+
<PlaySquareIcon className={MENU_ITEM_ICON_CLASS} />
|
|
613
|
+
Open notebook
|
|
614
|
+
</DropdownMenuItem>
|
|
615
|
+
</>
|
|
616
|
+
)}
|
|
617
|
+
<DropdownMenuSeparator />
|
|
618
|
+
{!node.data.isDirectory && !disableFileDownloads && (
|
|
619
|
+
<>
|
|
620
|
+
<DropdownMenuItem
|
|
621
|
+
onSelect={async () => {
|
|
622
|
+
const details = await sendFileDetails({
|
|
623
|
+
path: node.data.path,
|
|
624
|
+
});
|
|
625
|
+
if (details.isBase64 && details.contents) {
|
|
626
|
+
const blob = deserializeBlob(
|
|
627
|
+
base64ToDataURL(
|
|
628
|
+
details.contents as Base64String,
|
|
629
|
+
details.mimeType || "application/octet-stream",
|
|
630
|
+
),
|
|
631
|
+
);
|
|
632
|
+
downloadBlob(blob, node.data.name);
|
|
633
|
+
} else {
|
|
634
|
+
downloadBlob(
|
|
635
|
+
new Blob([details.contents || ""]),
|
|
636
|
+
node.data.name,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}}
|
|
640
|
+
>
|
|
641
|
+
<DownloadIcon className={MENU_ITEM_ICON_CLASS} />
|
|
642
|
+
Download
|
|
643
|
+
</DropdownMenuItem>
|
|
644
|
+
<DropdownMenuSeparator />
|
|
645
|
+
</>
|
|
646
|
+
)}
|
|
647
|
+
<DeleteMenuItem onSelect={handleDeleteFile} />
|
|
648
|
+
</FileActionsDropdown>
|
|
710
649
|
</span>
|
|
711
650
|
</div>
|
|
712
651
|
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { NodeApi } from "react-arborist";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import type { FileInfo } from "@/core/network/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Inline rename input used by `react-arborist` nodes when `node.isEditing`
|
|
9
|
+
* is true. Auto-focuses and selects everything except the extension so a
|
|
10
|
+
* user can type straight into the name.
|
|
11
|
+
*/
|
|
12
|
+
export const FileNameInput = ({ node }: { node: NodeApi<FileInfo> }) => {
|
|
13
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
ref.current?.focus();
|
|
16
|
+
// Select everything but the extension. For extensionless names
|
|
17
|
+
// (`README`) and dotfiles (`.env`), select the full name.
|
|
18
|
+
const name = node.data.name;
|
|
19
|
+
const dotIndex = name.lastIndexOf(".");
|
|
20
|
+
const end = dotIndex > 0 ? dotIndex : name.length;
|
|
21
|
+
ref.current?.setSelectionRange(0, end);
|
|
22
|
+
}, [node.data.name]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<input
|
|
26
|
+
ref={ref}
|
|
27
|
+
className="flex-1 bg-transparent border border-border text-muted-foreground"
|
|
28
|
+
defaultValue={node.data.name}
|
|
29
|
+
onClick={(e) => e.stopPropagation()}
|
|
30
|
+
onBlur={() => node.reset()}
|
|
31
|
+
onKeyDown={(e) => {
|
|
32
|
+
if (e.key === "Escape") {
|
|
33
|
+
node.reset();
|
|
34
|
+
}
|
|
35
|
+
if (e.key === "Enter") {
|
|
36
|
+
node.submit(e.currentTarget.value);
|
|
37
|
+
}
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|