@marimo-team/islands 0.23.7-dev9 → 0.23.7-dev90
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/{ConnectedDataExplorerComponent-DnRhpPMJ.js → ConnectedDataExplorerComponent-2lBNiUv6.js} +13 -13
- package/dist/{ErrorBoundary-Da4UeYxT.js → ErrorBoundary-D3wrPNma.js} +1 -1
- package/dist/{any-language-editor-DDubl8YH.js → any-language-editor-VWs_7v27.js} +5 -5
- package/dist/assets/__vite-browser-external-CAdMKBac.js +1 -0
- package/dist/assets/worker-CpBbwbQo.js +73 -0
- package/dist/{button-CA5pI2YF.js → button-Dj4BTre0.js} +5 -0
- package/dist/{capabilities-6laDasij.js → capabilities-C9rrYCzf.js} +1 -1
- package/dist/{chat-ui-BmWZZ3mE.js → chat-ui-D3XBept8.js} +625 -233
- package/dist/{check-CFM2mVDr.js → check-BcUIXnUT.js} +1 -1
- package/dist/{code-visibility-CRHzv49w.js → code-visibility-C5NrPsUC.js} +11480 -1992
- package/dist/{copy-TGGAUEWp.js → copy-DLf4aN7I.js} +2 -2
- package/dist/{dist-ESg7xyoD.js → dist-D3ZI9nhS.js} +2 -2
- package/dist/{error-banner-DnBPzEWg.js → error-banner-CVkfBUT3.js} +2 -2
- package/dist/{esm-Dd1z1auZ.js → esm-CWp0KQeK.js} +1 -1
- package/dist/{extends-CzJgxo2J.js → extends-vAi97cpa.js} +4 -4
- package/dist/{formats-CgaK7Gmx.js → formats-Dsy9kkZu.js} +3 -3
- package/dist/{glide-data-editor-B-3A3G02.js → glide-data-editor-DucgdjRo.js} +9 -9
- package/dist/{html-to-image-BwZL1Pkk.js → html-to-image-CpggM7u1.js} +2667 -2408
- package/dist/{input-BAOe64zx.js → input-D4kjoQUB.js} +8 -6
- package/dist/{label-BCWi-Oqu.js → label-BLqV33b1.js} +2 -2
- package/dist/{loader-BvW0-YWZ.js → loader-Dr8Qem8p.js} +1 -1
- package/dist/main.js +1697 -10282
- package/dist/{mermaid-cXSZ1pfD.js → mermaid-DO-Daq7u.js} +5 -5
- package/dist/{process-output-lpVrk7d5.js → process-output-X8TR20AK.js} +3 -3
- package/dist/reveal-component-kMIwe09M.js +7447 -0
- package/dist/{spec-DSIuqd3f.js → spec-hVaaZsY5.js} +4 -4
- package/dist/{strings-B_FOH6eV.js → strings-BiIhGaI8.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-BHs0PWwp.js → swiper-component-DlD2GU2g.js} +2 -2
- package/dist/{toDate-CHtl9vts.js → toDate-CIpC_34u.js} +33 -20
- package/dist/{tooltip-B0mtKTXm.js → tooltip-DRaMBu06.js} +3 -3
- package/dist/{types-DBtDeUKD.js → types-Dzuoc3LN.js} +1 -1
- package/dist/{useAsyncData-B6hCGywC.js → useAsyncData-C56Khv_R.js} +1 -1
- package/dist/{useDateFormatter-B3mCQMP3.js → useDateFormatter-B_9k85Ex.js} +2 -2
- package/dist/{useDeepCompareMemoize-CmwDuYUH.js → useDeepCompareMemoize-Dt98v2ua.js} +1 -1
- package/dist/{useIframeCapabilities-DbdLoEDm.js → useIframeCapabilities-BkYHTrss.js} +1 -1
- package/dist/{useLifecycle-CjMjllqy.js → useLifecycle-BF6-z62y.js} +3 -3
- package/dist/{useTheme-CByZUW0p.js → useTheme-DykuNHR2.js} +2 -2
- package/dist/{vega-component-C2BYPkfd.js → vega-component-cSdqoAxe.js} +10 -10
- package/dist/{zod-BxdsqRPd.js → zod-BWkcDORu.js} +1 -1
- package/package.json +3 -3
- package/src/components/chat/chat-components.tsx +47 -0
- package/src/components/chat/chat-display.tsx +41 -7
- package/src/components/chat/chat-panel.tsx +37 -10
- package/src/components/chat/chat-utils.ts +42 -20
- package/src/components/chat/reasoning-accordion.tsx +14 -3
- package/src/components/chat/tool-call/shared.ts +13 -0
- package/src/components/chat/tool-call/tool-approval-card.tsx +62 -0
- package/src/components/chat/tool-call/tool-args.tsx +26 -0
- package/src/components/chat/tool-call/tool-call-view.tsx +99 -0
- package/src/components/chat/tool-call/tool-error-card.tsx +81 -0
- package/src/components/chat/tool-call/tool-history-row.tsx +153 -0
- package/src/components/chat/tool-call/tool-result.tsx +101 -0
- package/src/components/data-table/__tests__/column-header.test.ts +3 -1
- package/src/components/data-table/__tests__/column-header.test.tsx +308 -0
- package/src/components/data-table/__tests__/filter-by-values-picker.test.tsx +112 -0
- package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +261 -0
- package/src/components/data-table/__tests__/filters.test.ts +196 -49
- package/src/components/data-table/charts/components/form-fields.tsx +1 -0
- package/src/components/data-table/column-header.tsx +349 -170
- package/src/components/data-table/date-filter-inputs.tsx +325 -0
- package/src/components/data-table/filter-by-values-picker.tsx +70 -9
- package/src/components/data-table/filter-pill-editor.tsx +410 -156
- package/src/components/data-table/filter-pills.tsx +69 -54
- package/src/components/data-table/filters.ts +218 -101
- package/src/components/data-table/header-items.tsx +8 -1
- package/src/components/data-table/operator-labels.ts +25 -0
- package/src/components/data-table/regex-input.tsx +61 -0
- package/src/components/dependency-graph/minimap-content.tsx +14 -3
- package/src/components/editor/actions/pair-with-agent-modal.tsx +140 -49
- package/src/components/editor/actions/useNotebookActions.tsx +3 -1
- package/src/components/editor/app-container.tsx +7 -1
- package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +10 -2
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +1 -0
- package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
- package/src/components/editor/chrome/wrapper/footer.tsx +4 -1
- package/src/components/editor/chrome/wrapper/panels.tsx +4 -1
- package/src/components/editor/chrome/wrapper/sidebar.tsx +4 -1
- package/src/components/editor/controls/Controls.tsx +11 -3
- package/src/components/editor/file-tree/file-explorer.tsx +12 -2
- package/src/components/editor/header/__tests__/status.test.tsx +108 -0
- package/src/components/editor/header/status.tsx +44 -10
- package/src/components/editor/navigation/__tests__/clipboard.test.ts +106 -0
- package/src/components/editor/navigation/__tests__/navigation.test.ts +70 -0
- package/src/components/editor/navigation/clipboard.ts +99 -25
- package/src/components/editor/navigation/navigation.ts +15 -1
- package/src/components/editor/notebook-cell.tsx +5 -0
- package/src/components/editor/output/console/ConsoleOutput.tsx +23 -5
- package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +114 -0
- package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +5 -4
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +55 -15
- package/src/components/editor/renderers/slides-layout/plugin.tsx +8 -25
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +19 -6
- package/src/components/editor/renderers/slides-layout/types.ts +40 -31
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -0
- package/src/components/home/components.tsx +6 -0
- package/src/components/pages/run-page.tsx +4 -1
- package/src/components/scratchpad/scratchpad.tsx +1 -0
- package/src/components/slides/__tests__/slide-notes.test.ts +131 -0
- package/src/components/slides/reveal-component.tsx +252 -147
- package/src/components/slides/slide-notes-editor.tsx +127 -0
- package/src/components/slides/slide-notes.ts +64 -0
- package/src/components/slides/slides.css +14 -0
- package/src/components/ui/combobox.tsx +24 -5
- package/src/components/ui/number-field.tsx +2 -0
- package/src/core/ai/tools/__tests__/registry.test.ts +10 -12
- package/src/core/ai/tools/registry.ts +9 -5
- package/src/core/cells/__tests__/cells.test.ts +187 -0
- package/src/core/cells/__tests__/pending-cut-service.test.tsx +123 -0
- package/src/core/cells/cells.ts +102 -17
- package/src/core/cells/document-changes.ts +6 -1
- package/src/core/cells/pending-cut-service.ts +55 -0
- package/src/core/cells/utils.ts +11 -0
- package/src/core/codemirror/cells/extensions.ts +10 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +152 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +99 -0
- package/src/core/codemirror/go-to-definition/commands.ts +382 -22
- package/src/core/codemirror/go-to-definition/utils.ts +23 -5
- package/src/core/edit-app.tsx +3 -2
- package/src/core/hotkeys/hotkeys.ts +5 -0
- package/src/core/islands/worker/worker.tsx +3 -2
- package/src/core/run-app.tsx +2 -1
- package/src/core/runtime/__tests__/runtime.test.ts +38 -17
- package/src/core/runtime/runtime.ts +57 -34
- package/src/core/wasm/__tests__/utils.test.ts +34 -0
- package/src/core/wasm/utils.ts +14 -0
- package/src/core/wasm/worker/bootstrap.ts +3 -2
- package/src/core/wasm/worker/worker.ts +3 -2
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +156 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +101 -0
- package/src/core/websocket/transports/__tests__/ws.test.ts +125 -0
- package/src/core/websocket/transports/basic.ts +1 -1
- package/src/core/websocket/transports/ws.ts +96 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +133 -54
- package/src/core/websocket/useWebSocket.tsx +3 -15
- package/src/css/app/Cell.css +10 -0
- package/src/plugins/core/__test__/sanitize.test.ts +30 -0
- package/src/plugins/impl/DropdownPlugin.tsx +12 -1
- package/src/plugins/impl/MultiselectPlugin.tsx +4 -0
- package/src/plugins/impl/SearchableSelect.tsx +11 -1
- package/src/plugins/impl/TabsPlugin.tsx +35 -7
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +56 -0
- package/src/plugins/impl/__tests__/TabsPlugin.test.tsx +154 -0
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +48 -36
- package/src/plugins/impl/data-frames/schema.ts +4 -1
- package/src/plugins/layout/DownloadPlugin.tsx +9 -7
- package/src/utils/__tests__/id-tree.test.ts +71 -0
- package/src/utils/download.ts +4 -2
- package/src/utils/id-tree.tsx +89 -0
- package/dist/assets/__vite-browser-external-rrUYDKRl.js +0 -1
- package/dist/assets/worker-Bfy15ViQ.js +0 -73
- package/dist/reveal-component-C97Ceb7e.js +0 -4863
- package/src/components/chat/tool-call-accordion.tsx +0 -247
|
@@ -31,6 +31,20 @@
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* Resize handle for the notes panel below the deck. Self-contained so the
|
|
35
|
+
slides layout doesn't have to inherit styles from app-chrome.css. */
|
|
36
|
+
.mo-slides-notes-resize {
|
|
37
|
+
height: 4px;
|
|
38
|
+
background-color: transparent;
|
|
39
|
+
transition: background-color 200ms linear;
|
|
40
|
+
outline: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.mo-slides-notes-resize:hover,
|
|
44
|
+
.mo-slides-notes-resize[data-resize-handle-active] {
|
|
45
|
+
background-color: var(--slate-11);
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
/* helpful for debugging */
|
|
35
49
|
/* .mo-slide-content {
|
|
36
50
|
background-color: red;
|
|
@@ -39,6 +39,7 @@ interface ComboboxCommonProps<TValue> {
|
|
|
39
39
|
className?: string;
|
|
40
40
|
id?: string;
|
|
41
41
|
keepPopoverOpenOnSelect?: boolean;
|
|
42
|
+
disabled?: boolean;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
type ComboboxFilterProps =
|
|
@@ -95,6 +96,7 @@ export const Combobox = <TValue,>({
|
|
|
95
96
|
chipsClassName,
|
|
96
97
|
keepPopoverOpenOnSelect,
|
|
97
98
|
id,
|
|
99
|
+
disabled = false,
|
|
98
100
|
...rest
|
|
99
101
|
}: ComboboxProps<TValue>) => {
|
|
100
102
|
const [open = false, setOpen] = useControllableState({
|
|
@@ -168,19 +170,30 @@ export const Combobox = <TValue,>({
|
|
|
168
170
|
|
|
169
171
|
return (
|
|
170
172
|
<div className={cn("relative")} {...rest}>
|
|
171
|
-
<Popover
|
|
173
|
+
<Popover
|
|
174
|
+
open={open}
|
|
175
|
+
onOpenChange={(v) => {
|
|
176
|
+
if (disabled && v) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
setOpen(v);
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
172
182
|
<PopoverTrigger asChild={true}>
|
|
173
|
-
<
|
|
183
|
+
<button
|
|
174
184
|
id={id}
|
|
185
|
+
type="button"
|
|
175
186
|
className={cn(
|
|
176
|
-
"flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid
|
|
187
|
+
"flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid",
|
|
188
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
177
189
|
className,
|
|
178
190
|
)}
|
|
179
191
|
aria-expanded={open}
|
|
192
|
+
aria-disabled={disabled}
|
|
180
193
|
>
|
|
181
194
|
<span className="truncate flex-1 min-w-0">{renderValue()}</span>
|
|
182
195
|
<ChevronDownIcon className="ml-3 w-4 h-4 opacity-50 shrink-0" />
|
|
183
|
-
</
|
|
196
|
+
</button>
|
|
184
197
|
</PopoverTrigger>
|
|
185
198
|
<PopoverContent
|
|
186
199
|
className="w-full min-w-(--radix-popover-trigger-width) p-0"
|
|
@@ -215,9 +228,15 @@ export const Combobox = <TValue,>({
|
|
|
215
228
|
{displayValue?.(val) ?? String(val)}
|
|
216
229
|
<XCircle
|
|
217
230
|
onClick={() => {
|
|
231
|
+
if (disabled) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
218
234
|
handleSelect(val);
|
|
219
235
|
}}
|
|
220
|
-
className=
|
|
236
|
+
className={cn(
|
|
237
|
+
"w-3 h-3 opacity-50 hover:opacity-100 ml-1 cursor-pointer",
|
|
238
|
+
disabled && "pointer-events-none",
|
|
239
|
+
)}
|
|
221
240
|
/>
|
|
222
241
|
</Badge>
|
|
223
242
|
);
|
|
@@ -62,6 +62,7 @@ export const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
62
62
|
slot="increment"
|
|
63
63
|
isDisabled={props.isDisabled}
|
|
64
64
|
variant={variant}
|
|
65
|
+
excludeFromTabOrder={true}
|
|
65
66
|
>
|
|
66
67
|
<ChevronUp
|
|
67
68
|
aria-hidden={true}
|
|
@@ -73,6 +74,7 @@ export const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
73
74
|
slot="decrement"
|
|
74
75
|
isDisabled={props.isDisabled}
|
|
75
76
|
variant={variant}
|
|
77
|
+
excludeFromTabOrder={true}
|
|
76
78
|
>
|
|
77
79
|
<ChevronDown
|
|
78
80
|
aria-hidden={true}
|
|
@@ -18,13 +18,11 @@ describe("FrontendToolRegistry", () => {
|
|
|
18
18
|
|
|
19
19
|
it("invokes a tool with valid args and validates input/output", async () => {
|
|
20
20
|
const registry = new FrontendToolRegistry([new TestFrontendTool()]);
|
|
21
|
-
const response = await registry.invoke(
|
|
22
|
-
"test_frontend_tool",
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{} as never,
|
|
27
|
-
);
|
|
21
|
+
const response = await registry.invoke({
|
|
22
|
+
toolName: "test_frontend_tool",
|
|
23
|
+
rawArgs: { name: "Alice" },
|
|
24
|
+
toolContext: {} as never,
|
|
25
|
+
});
|
|
28
26
|
|
|
29
27
|
// Check InvokeResult wrapper
|
|
30
28
|
expect(response.tool_name).toBe("test_frontend_tool");
|
|
@@ -47,11 +45,11 @@ describe("FrontendToolRegistry", () => {
|
|
|
47
45
|
|
|
48
46
|
it("returns a structured error on invalid args", async () => {
|
|
49
47
|
const registry = new FrontendToolRegistry([new TestFrontendTool()]);
|
|
50
|
-
const response = await registry.invoke(
|
|
51
|
-
"test_frontend_tool",
|
|
52
|
-
{},
|
|
53
|
-
{} as never,
|
|
54
|
-
);
|
|
48
|
+
const response = await registry.invoke({
|
|
49
|
+
toolName: "test_frontend_tool",
|
|
50
|
+
rawArgs: {},
|
|
51
|
+
toolContext: {} as never,
|
|
52
|
+
});
|
|
55
53
|
|
|
56
54
|
// Check InvokeResult wrapper
|
|
57
55
|
expect(response.tool_name).toBe("test_frontend_tool");
|
|
@@ -52,11 +52,15 @@ export class FrontendToolRegistry {
|
|
|
52
52
|
return tool;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
async invoke<TName extends string>(
|
|
56
|
-
toolName
|
|
57
|
-
rawArgs
|
|
58
|
-
toolContext
|
|
59
|
-
|
|
55
|
+
async invoke<TName extends string>({
|
|
56
|
+
toolName,
|
|
57
|
+
rawArgs,
|
|
58
|
+
toolContext,
|
|
59
|
+
}: {
|
|
60
|
+
toolName: TName;
|
|
61
|
+
rawArgs: unknown;
|
|
62
|
+
toolContext: ToolNotebookContext;
|
|
63
|
+
}): Promise<InvokeResult<TName>> {
|
|
60
64
|
const tool = this.getToolOrThrow(toolName);
|
|
61
65
|
const handler = tool.handler;
|
|
62
66
|
const inputSchema = tool.schema;
|
|
@@ -212,6 +212,20 @@ describe("cell reducer", () => {
|
|
|
212
212
|
`);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
it("can add a cell with name and config", () => {
|
|
216
|
+
actions.createNewCell({
|
|
217
|
+
cellId: firstCellId,
|
|
218
|
+
before: false,
|
|
219
|
+
code: "x = 1",
|
|
220
|
+
name: "My Cell",
|
|
221
|
+
config: { hide_code: true, disabled: false },
|
|
222
|
+
});
|
|
223
|
+
const newCellId = state.cellIds.inOrderIds[1];
|
|
224
|
+
expect(state.cellData[newCellId].name).toBe("My Cell");
|
|
225
|
+
expect(state.cellData[newCellId].config.hide_code).toBe(true);
|
|
226
|
+
expect(state.cellData[newCellId].config.disabled).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
215
229
|
it("can delete a Python cell and undo delete", () => {
|
|
216
230
|
actions.createNewCell({
|
|
217
231
|
cellId: firstCellId,
|
|
@@ -602,6 +616,179 @@ describe("cell reducer", () => {
|
|
|
602
616
|
expect(formatCells(state)).toBe(before);
|
|
603
617
|
});
|
|
604
618
|
|
|
619
|
+
it("can move multiple cells relative to target", () => {
|
|
620
|
+
actions.createNewCell({
|
|
621
|
+
cellId: firstCellId,
|
|
622
|
+
before: false,
|
|
623
|
+
});
|
|
624
|
+
actions.createNewCell({
|
|
625
|
+
cellId: cellId("1"),
|
|
626
|
+
before: false,
|
|
627
|
+
});
|
|
628
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
629
|
+
"
|
|
630
|
+
[0] ''
|
|
631
|
+
|
|
632
|
+
[1] ''
|
|
633
|
+
|
|
634
|
+
[2] ''
|
|
635
|
+
"
|
|
636
|
+
`);
|
|
637
|
+
|
|
638
|
+
// Move first two cells after the third
|
|
639
|
+
actions.moveCellsRelativeTo({
|
|
640
|
+
cellIds: [firstCellId, cellId("1")],
|
|
641
|
+
targetCellId: cellId("2"),
|
|
642
|
+
position: "after",
|
|
643
|
+
});
|
|
644
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
645
|
+
"
|
|
646
|
+
[2] ''
|
|
647
|
+
|
|
648
|
+
[0] ''
|
|
649
|
+
|
|
650
|
+
[1] ''
|
|
651
|
+
"
|
|
652
|
+
`);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("can undo cut-paste (move with previousPlacements)", () => {
|
|
656
|
+
actions.createNewCell({
|
|
657
|
+
cellId: firstCellId,
|
|
658
|
+
before: false,
|
|
659
|
+
});
|
|
660
|
+
actions.createNewCell({
|
|
661
|
+
cellId: cellId("1"),
|
|
662
|
+
before: false,
|
|
663
|
+
});
|
|
664
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
665
|
+
"
|
|
666
|
+
[0] ''
|
|
667
|
+
|
|
668
|
+
[1] ''
|
|
669
|
+
|
|
670
|
+
[2] ''
|
|
671
|
+
"
|
|
672
|
+
`);
|
|
673
|
+
|
|
674
|
+
const col = state.cellIds.findWithId(firstCellId);
|
|
675
|
+
const previousPlacements = [
|
|
676
|
+
{
|
|
677
|
+
columnId: col.id,
|
|
678
|
+
index: col.indexOfOrThrow(
|
|
679
|
+
firstCellId,
|
|
680
|
+
) as import("@/utils/id-tree").CellIndex,
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
columnId: col.id,
|
|
684
|
+
index: col.indexOfOrThrow(
|
|
685
|
+
cellId("1"),
|
|
686
|
+
) as import("@/utils/id-tree").CellIndex,
|
|
687
|
+
},
|
|
688
|
+
];
|
|
689
|
+
|
|
690
|
+
actions.moveCellsRelativeTo({
|
|
691
|
+
cellIds: [firstCellId, cellId("1")],
|
|
692
|
+
targetCellId: cellId("2"),
|
|
693
|
+
position: "after",
|
|
694
|
+
previousPlacements,
|
|
695
|
+
});
|
|
696
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
697
|
+
"
|
|
698
|
+
[2] ''
|
|
699
|
+
|
|
700
|
+
[0] ''
|
|
701
|
+
|
|
702
|
+
[1] ''
|
|
703
|
+
"
|
|
704
|
+
`);
|
|
705
|
+
|
|
706
|
+
actions.undoDeleteCell();
|
|
707
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
708
|
+
"
|
|
709
|
+
[0] ''
|
|
710
|
+
|
|
711
|
+
[1] ''
|
|
712
|
+
|
|
713
|
+
[2] ''
|
|
714
|
+
"
|
|
715
|
+
`);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("undo order: cut-paste then delete — first undo restores delete, second undo undoes move", () => {
|
|
719
|
+
actions.createNewCell({
|
|
720
|
+
cellId: firstCellId,
|
|
721
|
+
before: false,
|
|
722
|
+
});
|
|
723
|
+
actions.createNewCell({
|
|
724
|
+
cellId: cellId("1"),
|
|
725
|
+
before: false,
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const col = state.cellIds.findWithId(firstCellId);
|
|
729
|
+
const previousPlacements = [
|
|
730
|
+
{
|
|
731
|
+
columnId: col.id,
|
|
732
|
+
index: col.indexOfOrThrow(
|
|
733
|
+
firstCellId,
|
|
734
|
+
) as import("@/utils/id-tree").CellIndex,
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
columnId: col.id,
|
|
738
|
+
index: col.indexOfOrThrow(
|
|
739
|
+
cellId("1"),
|
|
740
|
+
) as import("@/utils/id-tree").CellIndex,
|
|
741
|
+
},
|
|
742
|
+
];
|
|
743
|
+
|
|
744
|
+
actions.moveCellsRelativeTo({
|
|
745
|
+
cellIds: [firstCellId, cellId("1")],
|
|
746
|
+
targetCellId: cellId("2"),
|
|
747
|
+
position: "after",
|
|
748
|
+
previousPlacements,
|
|
749
|
+
});
|
|
750
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
751
|
+
"
|
|
752
|
+
[2] ''
|
|
753
|
+
|
|
754
|
+
[0] ''
|
|
755
|
+
|
|
756
|
+
[1] ''
|
|
757
|
+
"
|
|
758
|
+
`);
|
|
759
|
+
|
|
760
|
+
actions.deleteCell({ cellId: cellId("2") });
|
|
761
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
762
|
+
"
|
|
763
|
+
[0] ''
|
|
764
|
+
|
|
765
|
+
[1] ''
|
|
766
|
+
"
|
|
767
|
+
`);
|
|
768
|
+
|
|
769
|
+
actions.undoDeleteCell();
|
|
770
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
771
|
+
"
|
|
772
|
+
[3] ''
|
|
773
|
+
|
|
774
|
+
[0] ''
|
|
775
|
+
|
|
776
|
+
[1] ''
|
|
777
|
+
"
|
|
778
|
+
`);
|
|
779
|
+
|
|
780
|
+
actions.undoDeleteCell();
|
|
781
|
+
expect(formatCells(state)).toMatchInlineSnapshot(`
|
|
782
|
+
"
|
|
783
|
+
[0] ''
|
|
784
|
+
|
|
785
|
+
[1] ''
|
|
786
|
+
|
|
787
|
+
[3] ''
|
|
788
|
+
"
|
|
789
|
+
`);
|
|
790
|
+
});
|
|
791
|
+
|
|
605
792
|
it("can run cell and receive cell messages", () => {
|
|
606
793
|
// HAPPY PATH
|
|
607
794
|
/////////////////
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { createStore, Provider } from "jotai";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { cellId } from "@/__tests__/branded";
|
|
7
|
+
import type { CellId } from "@/core/cells/ids";
|
|
8
|
+
import {
|
|
9
|
+
pendingCutStateAtom,
|
|
10
|
+
useHasPendingCut,
|
|
11
|
+
useIsPendingCut,
|
|
12
|
+
usePendingCutActions,
|
|
13
|
+
usePendingCutState,
|
|
14
|
+
} from "../pending-cut-service";
|
|
15
|
+
|
|
16
|
+
function createTestWrapper() {
|
|
17
|
+
const store = createStore();
|
|
18
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
19
|
+
<Provider store={store}>{children}</Provider>
|
|
20
|
+
);
|
|
21
|
+
return { wrapper, store };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("pending-cut-service", () => {
|
|
25
|
+
it("markForCut sets cellIds", () => {
|
|
26
|
+
const { wrapper, store } = createTestWrapper();
|
|
27
|
+
const cellIds: CellId[] = [cellId("cell-1"), cellId("cell-2")];
|
|
28
|
+
|
|
29
|
+
const { result } = renderHook(
|
|
30
|
+
() => ({
|
|
31
|
+
actions: usePendingCutActions(),
|
|
32
|
+
state: usePendingCutState(),
|
|
33
|
+
}),
|
|
34
|
+
{ wrapper },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
act(() => {
|
|
38
|
+
result.current.actions.markForCut({ cellIds });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const state = store.get(pendingCutStateAtom);
|
|
42
|
+
expect(state.cellIds).toEqual(new Set(cellIds));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("clear resets to initial state", () => {
|
|
46
|
+
const { wrapper, store } = createTestWrapper();
|
|
47
|
+
const cellIds: CellId[] = [cellId("cell-1")];
|
|
48
|
+
|
|
49
|
+
const { result } = renderHook(
|
|
50
|
+
() => ({
|
|
51
|
+
actions: usePendingCutActions(),
|
|
52
|
+
state: usePendingCutState(),
|
|
53
|
+
}),
|
|
54
|
+
{ wrapper },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
act(() => {
|
|
58
|
+
result.current.actions.markForCut({ cellIds });
|
|
59
|
+
});
|
|
60
|
+
expect(store.get(pendingCutStateAtom).cellIds.size).toBe(1);
|
|
61
|
+
|
|
62
|
+
act(() => {
|
|
63
|
+
result.current.actions.clear();
|
|
64
|
+
});
|
|
65
|
+
const state = store.get(pendingCutStateAtom);
|
|
66
|
+
expect(state.cellIds.size).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("useIsPendingCut returns true when cellId is marked for cut", () => {
|
|
70
|
+
const { wrapper } = createTestWrapper();
|
|
71
|
+
const targetCellId = cellId("cell-1");
|
|
72
|
+
|
|
73
|
+
const { result: actionsResult } = renderHook(() => usePendingCutActions(), {
|
|
74
|
+
wrapper,
|
|
75
|
+
});
|
|
76
|
+
const { result: isPendingResult } = renderHook(
|
|
77
|
+
() => useIsPendingCut(targetCellId),
|
|
78
|
+
{ wrapper },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(isPendingResult.current).toBe(false);
|
|
82
|
+
|
|
83
|
+
act(() => {
|
|
84
|
+
actionsResult.current.markForCut({ cellIds: [targetCellId] });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(isPendingResult.current).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("useIsPendingCut returns false when cellId is not marked for cut", () => {
|
|
91
|
+
const { wrapper } = createTestWrapper();
|
|
92
|
+
const { result } = renderHook(() => useIsPendingCut(cellId("other-cell")), {
|
|
93
|
+
wrapper,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { result: actionsResult } = renderHook(() => usePendingCutActions(), {
|
|
97
|
+
wrapper,
|
|
98
|
+
});
|
|
99
|
+
act(() => {
|
|
100
|
+
actionsResult.current.markForCut({ cellIds: [cellId("cell-1")] });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result.current).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("useHasPendingCut returns true when any cells are marked for cut", () => {
|
|
107
|
+
const { wrapper } = createTestWrapper();
|
|
108
|
+
const { result: hasPendingResult } = renderHook(() => useHasPendingCut(), {
|
|
109
|
+
wrapper,
|
|
110
|
+
});
|
|
111
|
+
const { result: actionsResult } = renderHook(() => usePendingCutActions(), {
|
|
112
|
+
wrapper,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(hasPendingResult.current).toBe(false);
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
actionsResult.current.markForCut({ cellIds: [cellId("cell-1")] });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(hasPendingResult.current).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/core/cells/cells.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
splitEditor,
|
|
24
24
|
updateEditorCodeFromPython,
|
|
25
25
|
} from "../codemirror/language/utils";
|
|
26
|
+
import type { SerializedEditorState } from "../codemirror/types";
|
|
26
27
|
import { findCollapseRange, mergeOutlines } from "../dom/outline";
|
|
27
28
|
import type { CellMessage } from "../kernel/messages";
|
|
28
29
|
import { isErrorMime } from "../mime";
|
|
@@ -50,11 +51,36 @@ import {
|
|
|
50
51
|
canUndoDeletes,
|
|
51
52
|
disabledCellIds,
|
|
52
53
|
enabledCellIds,
|
|
54
|
+
getUndoLabel,
|
|
53
55
|
notebookIsRunning,
|
|
54
56
|
notebookNeedsRun,
|
|
55
57
|
notebookQueueOrRunningCount,
|
|
56
58
|
} from "./utils";
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* History entry for undoing a cell deletion.
|
|
62
|
+
*/
|
|
63
|
+
export interface UndoDeleteEntry {
|
|
64
|
+
type: "delete";
|
|
65
|
+
name: string;
|
|
66
|
+
serializedEditorState: SerializedEditorState;
|
|
67
|
+
column: CellColumnId;
|
|
68
|
+
index: CellIndex;
|
|
69
|
+
isSetupCell: boolean;
|
|
70
|
+
config: CellConfig;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* History entry for undoing a cut-paste (move).
|
|
75
|
+
*/
|
|
76
|
+
export interface UndoMoveEntry {
|
|
77
|
+
type: "move";
|
|
78
|
+
cellIds: CellId[];
|
|
79
|
+
placements: Array<{ columnId: CellColumnId; index: CellIndex }>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type HistoryEntry = UndoDeleteEntry | UndoMoveEntry;
|
|
83
|
+
|
|
58
84
|
/**
|
|
59
85
|
* The state of the notebook.
|
|
60
86
|
*/
|
|
@@ -76,19 +102,9 @@ export interface NotebookState {
|
|
|
76
102
|
*/
|
|
77
103
|
cellHandles: Record<CellId, React.RefObject<CellHandle | null>>;
|
|
78
104
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* (CodeMirror types the serialized config as any.)
|
|
105
|
+
* Undo stack: deleted cells and cut-paste moves, in chronological order.
|
|
82
106
|
*/
|
|
83
|
-
history:
|
|
84
|
-
name: string;
|
|
85
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
86
|
-
serializedEditorState: any;
|
|
87
|
-
column: CellColumnId;
|
|
88
|
-
index: CellIndex;
|
|
89
|
-
isSetupCell: boolean;
|
|
90
|
-
config: CellConfig;
|
|
91
|
-
}[];
|
|
107
|
+
history: HistoryEntry[];
|
|
92
108
|
/**
|
|
93
109
|
* Key of cell to scroll to; typically set by actions that re-order the cell
|
|
94
110
|
* array. Call the SCROLL_TO_TARGET action to scroll to the specified cell
|
|
@@ -158,6 +174,10 @@ export interface CreateNewCellAction {
|
|
|
158
174
|
before: boolean;
|
|
159
175
|
/** Initial code content for the new cell */
|
|
160
176
|
code?: string;
|
|
177
|
+
/** Optional name for the new cell */
|
|
178
|
+
name?: string;
|
|
179
|
+
/** Optional cell configuration */
|
|
180
|
+
config?: CellConfig;
|
|
161
181
|
/** The last executed code for the new cell */
|
|
162
182
|
lastCodeRun?: string;
|
|
163
183
|
/** Timestamp of the last execution */
|
|
@@ -187,11 +207,13 @@ const {
|
|
|
187
207
|
cellId,
|
|
188
208
|
before,
|
|
189
209
|
code,
|
|
210
|
+
name,
|
|
211
|
+
config,
|
|
190
212
|
lastCodeRun = null,
|
|
191
213
|
lastExecutionTime = null,
|
|
192
214
|
autoFocus = true,
|
|
193
215
|
skipIfCodeExists = false,
|
|
194
|
-
hideCode =
|
|
216
|
+
hideCode = undefined,
|
|
195
217
|
} = action;
|
|
196
218
|
|
|
197
219
|
let columnId: CellColumnId;
|
|
@@ -234,8 +256,12 @@ const {
|
|
|
234
256
|
[newCellId]: createCell({
|
|
235
257
|
id: newCellId,
|
|
236
258
|
code,
|
|
259
|
+
name,
|
|
237
260
|
lastCodeRun,
|
|
238
|
-
config: createCellConfig({
|
|
261
|
+
config: createCellConfig({
|
|
262
|
+
...config,
|
|
263
|
+
...(hideCode != null && { hide_code: hideCode }),
|
|
264
|
+
}),
|
|
239
265
|
lastExecutionTime,
|
|
240
266
|
edited: Boolean(code) && code !== lastCodeRun,
|
|
241
267
|
}),
|
|
@@ -417,6 +443,40 @@ const {
|
|
|
417
443
|
scrollKey: null,
|
|
418
444
|
};
|
|
419
445
|
},
|
|
446
|
+
moveCellsRelativeTo: (
|
|
447
|
+
state,
|
|
448
|
+
action: {
|
|
449
|
+
cellIds: CellId[];
|
|
450
|
+
targetCellId: CellId;
|
|
451
|
+
position: "before" | "after";
|
|
452
|
+
previousPlacements?: Array<{ columnId: CellColumnId; index: CellIndex }>;
|
|
453
|
+
},
|
|
454
|
+
) => {
|
|
455
|
+
const { cellIds, targetCellId, position, previousPlacements } = action;
|
|
456
|
+
if (cellIds.length === 0) {
|
|
457
|
+
return state;
|
|
458
|
+
}
|
|
459
|
+
const newCellIds = state.cellIds.moveCellsRelativeTo(
|
|
460
|
+
cellIds,
|
|
461
|
+
targetCellId,
|
|
462
|
+
position,
|
|
463
|
+
);
|
|
464
|
+
// Only record undo when caller provided full before-state
|
|
465
|
+
const canUndoMove =
|
|
466
|
+
previousPlacements && previousPlacements.length === cellIds.length;
|
|
467
|
+
const history = canUndoMove
|
|
468
|
+
? [
|
|
469
|
+
...state.history,
|
|
470
|
+
{ type: "move" as const, cellIds, placements: previousPlacements },
|
|
471
|
+
]
|
|
472
|
+
: state.history;
|
|
473
|
+
return {
|
|
474
|
+
...state,
|
|
475
|
+
cellIds: newCellIds,
|
|
476
|
+
history,
|
|
477
|
+
scrollKey: null,
|
|
478
|
+
};
|
|
479
|
+
},
|
|
420
480
|
dropCellOverColumn: (
|
|
421
481
|
state,
|
|
422
482
|
action: { cellId: CellId; columnId: CellColumnId },
|
|
@@ -659,6 +719,7 @@ const {
|
|
|
659
719
|
history: [
|
|
660
720
|
...state.history,
|
|
661
721
|
{
|
|
722
|
+
type: "delete",
|
|
662
723
|
name: prevData.name,
|
|
663
724
|
serializedEditorState: serializedEditorState,
|
|
664
725
|
column: column.id,
|
|
@@ -675,7 +736,29 @@ const {
|
|
|
675
736
|
return state;
|
|
676
737
|
}
|
|
677
738
|
|
|
678
|
-
const
|
|
739
|
+
const last = state.history[state.history.length - 1];
|
|
740
|
+
|
|
741
|
+
if (last.type === "move") {
|
|
742
|
+
const { cellIds, placements } = last;
|
|
743
|
+
if (
|
|
744
|
+
cellIds.length === 0 ||
|
|
745
|
+
placements.length !== cellIds.length ||
|
|
746
|
+
cellIds.some((id) => !state.cellData[id])
|
|
747
|
+
) {
|
|
748
|
+
return { ...state, history: state.history.slice(0, -1) };
|
|
749
|
+
}
|
|
750
|
+
const toRestore = cellIds.map((id, i) => ({
|
|
751
|
+
id,
|
|
752
|
+
columnId: placements[i].columnId,
|
|
753
|
+
index: placements[i].index,
|
|
754
|
+
}));
|
|
755
|
+
return {
|
|
756
|
+
...state,
|
|
757
|
+
cellIds: state.cellIds.placeCells(toRestore),
|
|
758
|
+
history: state.history.slice(0, -1),
|
|
759
|
+
scrollKey: cellIds[0] ?? null,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
679
762
|
|
|
680
763
|
const {
|
|
681
764
|
name,
|
|
@@ -684,7 +767,7 @@ const {
|
|
|
684
767
|
index,
|
|
685
768
|
isSetupCell,
|
|
686
769
|
config,
|
|
687
|
-
} =
|
|
770
|
+
} = last;
|
|
688
771
|
|
|
689
772
|
const cellId = isSetupCell ? SETUP_CELL_ID : CellId.create();
|
|
690
773
|
const undoCell = createCell({
|
|
@@ -790,7 +873,7 @@ const {
|
|
|
790
873
|
cellReducer: (cell) => {
|
|
791
874
|
return {
|
|
792
875
|
...cell,
|
|
793
|
-
config: { ...cell.config, ...config },
|
|
876
|
+
config: createCellConfig({ ...cell.config, ...config }),
|
|
794
877
|
};
|
|
795
878
|
},
|
|
796
879
|
});
|
|
@@ -1633,6 +1716,8 @@ export const canUndoDeletesAtom = atom((get) =>
|
|
|
1633
1716
|
canUndoDeletes(get(notebookAtom)),
|
|
1634
1717
|
);
|
|
1635
1718
|
|
|
1719
|
+
export const undoLabelAtom = atom((get) => getUndoLabel(get(notebookAtom)));
|
|
1720
|
+
|
|
1636
1721
|
export const needsRunAtom = atom((get) => notebookNeedsRun(get(notebookAtom)));
|
|
1637
1722
|
|
|
1638
1723
|
export const cellErrorsAtom = atom((get) => {
|