@marimo-team/islands 0.21.2-dev6 → 0.21.2-dev61
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-D0GoOd_c.js → ConnectedDataExplorerComponent-DrWDbHRV.js} +1 -1
- package/dist/{any-language-editor-DlsjUw_l.js → any-language-editor-BRpxklRq.js} +1 -1
- package/dist/{copy-DIK6DiIA.js → copy-BjkXCUxP.js} +12 -2
- package/dist/{esm-BLobyqMs.js → esm-No_6eSQS.js} +1 -1
- package/dist/{glide-data-editor-pZyd9UJ_.js → glide-data-editor-858wsVkd.js} +1 -1
- package/dist/main.js +821 -417
- package/dist/{spec-Bfvf9Hre.js → spec-oVDndBz4.js} +25 -16
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/__mocks__/notebook.ts +9 -9
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/branded.ts +20 -0
- package/src/components/app-config/user-config-form.tsx +5 -4
- package/src/components/data-table/__tests__/utils.test.ts +138 -1
- package/src/components/data-table/charts/__tests__/storage.test.ts +7 -7
- package/src/components/data-table/context-menu.tsx +9 -5
- package/src/components/data-table/data-table.tsx +3 -0
- package/src/components/data-table/range-focus/__tests__/atoms.test.ts +8 -2
- package/src/components/data-table/range-focus/__tests__/test-utils.ts +2 -0
- package/src/components/data-table/range-focus/__tests__/utils.test.ts +82 -8
- package/src/components/data-table/range-focus/atoms.ts +2 -2
- package/src/components/data-table/range-focus/utils.ts +50 -12
- package/src/components/data-table/types.ts +7 -0
- package/src/components/data-table/utils.ts +87 -0
- package/src/components/editor/__tests__/data-attributes.test.tsx +8 -8
- package/src/components/editor/ai/__tests__/completion-utils.test.ts +15 -15
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +2 -2
- package/src/components/editor/connections/storage/as-code.ts +2 -2
- package/src/components/editor/file-tree/file-explorer.tsx +16 -2
- package/src/components/editor/file-tree/file-viewer.tsx +17 -3
- package/src/components/editor/navigation/__tests__/clipboard.test.ts +2 -2
- package/src/components/editor/navigation/__tests__/selection.test.ts +7 -6
- package/src/components/editor/navigation/__tests__/state.test.ts +8 -7
- package/src/components/editor/output/MarimoErrorOutput.tsx +7 -7
- package/src/components/editor/output/__tests__/traceback.test.tsx +4 -4
- package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +4 -4
- package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +8 -1
- package/src/components/storage/storage-file-viewer.tsx +35 -1
- package/src/components/storage/storage-inspector.tsx +9 -4
- package/src/components/storage/storage-snippets.ts +3 -3
- package/src/components/tracing/tracing.tsx +3 -1
- package/src/components/ui/range-slider.tsx +108 -1
- package/src/core/ai/__tests__/staged-cells.test.ts +9 -8
- package/src/core/ai/context/providers/__tests__/cell-output.test.ts +31 -31
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -3
- package/src/core/ai/context/providers/__tests__/tables.test.ts +3 -2
- package/src/core/ai/context/providers/__tests__/variable.test.ts +84 -63
- package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +10 -9
- package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +6 -6
- package/src/core/ai/tools/edit-notebook-tool.ts +3 -3
- package/src/core/cells/__tests__/add-missing-import.test.ts +3 -3
- package/src/core/cells/__tests__/apply-transaction.test.ts +279 -0
- package/src/core/cells/__tests__/cells.test.ts +198 -135
- package/src/core/cells/__tests__/document-changes.test.ts +575 -0
- package/src/core/cells/__tests__/document-roundtrip.test.ts +376 -0
- package/src/core/cells/__tests__/focus.test.ts +5 -4
- package/src/core/cells/__tests__/logs.test.ts +13 -12
- package/src/core/cells/__tests__/pending-delete-service.test.tsx +3 -3
- package/src/core/cells/__tests__/runs.test.ts +22 -21
- package/src/core/cells/__tests__/scrollCellIntoView.test.ts +8 -7
- package/src/core/cells/__tests__/session.test.ts +23 -22
- package/src/core/cells/cells.ts +29 -4
- package/src/core/cells/document-changes.ts +644 -0
- package/src/core/cells/ids.ts +5 -5
- package/src/core/cells/logs.ts +2 -2
- package/src/core/cells/runs.ts +6 -8
- package/src/core/codemirror/__tests__/format.test.ts +34 -36
- package/src/core/codemirror/__tests__/setup.test.ts +2 -2
- package/src/core/codemirror/cells/__tests__/extensions.test.ts +114 -0
- package/src/core/codemirror/cells/__tests__/traceback-decorations.test.ts +33 -32
- package/src/core/codemirror/cells/extensions.ts +66 -23
- package/src/core/codemirror/completion/__tests__/keymap.test.ts +15 -35
- package/src/core/codemirror/completion/keymap.ts +14 -4
- package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +12 -13
- package/src/core/codemirror/language/__tests__/utils.test.ts +3 -3
- package/src/core/codemirror/language/embedded/__tests__/embedded-python.test.ts +7 -8
- package/src/core/codemirror/language/languages/python.ts +4 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +4 -3
- package/src/core/codemirror/lsp/notebook-lsp.ts +28 -2
- package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +7 -6
- package/src/core/codemirror/reactive-references/analyzer.ts +2 -2
- package/src/core/codemirror/rtc/loro/__tests__/sync.test.ts +52 -0
- package/src/core/codemirror/rtc/loro/sync.ts +1 -0
- package/src/core/datasets/__tests__/data-source.test.ts +5 -6
- package/src/core/datasets/state.ts +1 -1
- package/src/core/errors/__tests__/errors.test.ts +2 -1
- package/src/core/export/__tests__/hooks.test.ts +37 -36
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/islands/main.ts +4 -7
- package/src/core/kernel/__tests__/handlers.test.ts +5 -4
- package/src/core/kernel/handlers.ts +7 -4
- package/src/core/network/DeferredRequestRegistry.ts +2 -2
- package/src/core/network/__tests__/CachingRequestRegistry.test.ts +9 -10
- package/src/core/network/__tests__/DeferredRequestRegistry.test.ts +4 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +9 -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 +5 -0
- package/src/core/static/__tests__/virtual-file-tracker.test.ts +8 -8
- package/src/core/static/virtual-file-tracker.ts +1 -1
- package/src/core/storage/__tests__/state.test.ts +31 -21
- package/src/core/storage/state.ts +1 -1
- package/src/core/variables/__tests__/state.test.ts +6 -6
- package/src/core/variables/types.ts +2 -2
- package/src/core/wasm/__tests__/state.test.ts +8 -8
- package/src/core/wasm/bridge.ts +1 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +31 -16
- package/src/css/app/fonts.css +6 -6
- package/src/css/md-tooltip.css +4 -39
- package/src/css/md.css +7 -0
- package/src/fonts/Fira_Mono/FiraMono-Bold.woff2 +0 -0
- package/src/fonts/Fira_Mono/FiraMono-Medium.woff2 +0 -0
- package/src/fonts/Fira_Mono/FiraMono-Regular.woff2 +0 -0
- package/src/fonts/Lora/Lora-VariableFont_wght.woff2 +0 -0
- package/src/fonts/PT_Sans/PTSans-Bold.woff2 +0 -0
- package/src/fonts/PT_Sans/PTSans-Regular.woff2 +0 -0
- package/src/plugins/core/RenderHTML.tsx +17 -0
- package/src/plugins/core/__test__/RenderHTML.test.ts +45 -0
- package/src/plugins/core/sanitize-html.ts +25 -18
- package/src/plugins/impl/DataTablePlugin.tsx +23 -2
- package/src/plugins/impl/SliderPlugin.tsx +1 -3
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +120 -0
- package/src/plugins/impl/anywidget/model.ts +1 -2
- package/src/stories/cell.stories.tsx +8 -8
- package/src/stories/layout/vertical/one-column.stories.tsx +9 -8
- package/src/stories/log-viewer.stories.tsx +8 -8
- package/src/stories/variables.stories.tsx +2 -2
- package/src/utils/__tests__/download.test.tsx +21 -20
- package/src/utils/copy.ts +18 -5
- package/src/utils/createReducer.ts +26 -11
- package/src/utils/download.ts +4 -3
- package/src/utils/html-to-image.ts +6 -0
- package/src/utils/json/base64.ts +3 -3
- package/src/utils/traceback.ts +5 -3
- package/src/fonts/Fira_Mono/FiraMono-Bold.ttf +0 -0
- package/src/fonts/Fira_Mono/FiraMono-Medium.ttf +0 -0
- package/src/fonts/Fira_Mono/FiraMono-Regular.ttf +0 -0
- package/src/fonts/Lora/Lora-Italic-VariableFont_wght.ttf +0 -0
- package/src/fonts/Lora/Lora-VariableFont_wght.ttf +0 -0
- package/src/fonts/Lora/static/Lora-Bold.ttf +0 -0
- package/src/fonts/Lora/static/Lora-BoldItalic.ttf +0 -0
- package/src/fonts/Lora/static/Lora-Italic.ttf +0 -0
- package/src/fonts/Lora/static/Lora-Medium.ttf +0 -0
- package/src/fonts/Lora/static/Lora-MediumItalic.ttf +0 -0
- package/src/fonts/Lora/static/Lora-Regular.ttf +0 -0
- package/src/fonts/Lora/static/Lora-SemiBold.ttf +0 -0
- package/src/fonts/Lora/static/Lora-SemiBoldItalic.ttf +0 -0
- package/src/fonts/PT_Sans/PTSans-Bold.ttf +0 -0
- package/src/fonts/PT_Sans/PTSans-BoldItalic.ttf +0 -0
- package/src/fonts/PT_Sans/PTSans-Italic.ttf +0 -0
- package/src/fonts/PT_Sans/PTSans-Regular.ttf +0 -0
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
} from "@/components/data-table/types";
|
|
54
54
|
import {
|
|
55
55
|
getPageIndexForRow,
|
|
56
|
+
loadTableAndRawData,
|
|
56
57
|
loadTableData,
|
|
57
58
|
} from "@/components/data-table/utils";
|
|
58
59
|
import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
|
|
@@ -174,6 +175,7 @@ const valueCounts: z.ZodType<ValueCounts> = z.array(
|
|
|
174
175
|
interface Data<T> {
|
|
175
176
|
label: string | null;
|
|
176
177
|
data: TableData<T>;
|
|
178
|
+
rawData?: TableData<T> | null;
|
|
177
179
|
totalRows: number | TooManyRows;
|
|
178
180
|
pagination: boolean;
|
|
179
181
|
pageSize: number;
|
|
@@ -221,6 +223,7 @@ type DataTableFunctions = {
|
|
|
221
223
|
total_rows: number | TooManyRows;
|
|
222
224
|
cell_styles?: CellStyleState | null;
|
|
223
225
|
cell_hover_texts?: Record<string, Record<string, string | null>> | null;
|
|
226
|
+
raw_data?: TableData<T> | null;
|
|
224
227
|
}>;
|
|
225
228
|
get_data_url?: GetDataUrl;
|
|
226
229
|
get_row_ids?: GetRowIds;
|
|
@@ -243,6 +246,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
|
|
|
243
246
|
]),
|
|
244
247
|
label: z.string().nullable(),
|
|
245
248
|
data: z.union([z.string(), z.array(z.object({}).passthrough())]),
|
|
249
|
+
rawData: z.union([z.string(), z.array(z.looseObject({}))]).nullish(),
|
|
246
250
|
totalRows: z.union([z.number(), z.literal(TOO_MANY_ROWS)]),
|
|
247
251
|
pagination: z.boolean().default(false),
|
|
248
252
|
pageSize: z.number().default(10),
|
|
@@ -327,6 +331,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
|
|
|
327
331
|
)
|
|
328
332
|
.nullable(),
|
|
329
333
|
cell_hover_texts: cellHoverTextSchema.nullable(),
|
|
334
|
+
raw_data: z.union([z.string(), z.array(z.looseObject({}))]).nullish(),
|
|
330
335
|
}),
|
|
331
336
|
),
|
|
332
337
|
get_row_ids: rpc.input(z.object({}).passthrough()).output(
|
|
@@ -529,17 +534,23 @@ export const LoadingDataTableComponent = memo(
|
|
|
529
534
|
// Data loading
|
|
530
535
|
const { data, error, isPending, isFetching } = useAsyncData<{
|
|
531
536
|
rows: T[];
|
|
537
|
+
rawRows?: T[];
|
|
532
538
|
totalRows: number | TooManyRows;
|
|
533
539
|
cellStyles: CellStyleState | undefined | null;
|
|
534
540
|
cellHoverTexts?: Record<string, Record<string, string | null>> | null;
|
|
535
541
|
}>(async () => {
|
|
536
542
|
// If there is no data, return an empty array
|
|
537
543
|
if (props.totalRows === 0) {
|
|
538
|
-
return {
|
|
544
|
+
return {
|
|
545
|
+
rows: Arrays.EMPTY,
|
|
546
|
+
totalRows: 0,
|
|
547
|
+
cellStyles: {},
|
|
548
|
+
};
|
|
539
549
|
}
|
|
540
550
|
|
|
541
551
|
// Table data is a url string or an array of objects
|
|
542
552
|
let tableData = props.data;
|
|
553
|
+
let rawTableData: TableData<T> | undefined | null = props.rawData;
|
|
543
554
|
let totalRows = props.totalRows;
|
|
544
555
|
let cellStyles = props.cellStyles;
|
|
545
556
|
let cellHoverTexts = props.cellHoverTexts;
|
|
@@ -587,13 +598,19 @@ export const LoadingDataTableComponent = memo(
|
|
|
587
598
|
} else {
|
|
588
599
|
const searchResults = await searchResultsPromise;
|
|
589
600
|
tableData = searchResults.data;
|
|
601
|
+
rawTableData = searchResults.raw_data;
|
|
590
602
|
totalRows = searchResults.total_rows;
|
|
591
603
|
cellStyles = searchResults.cell_styles || {};
|
|
592
604
|
cellHoverTexts = searchResults.cell_hover_texts || {};
|
|
593
605
|
}
|
|
594
|
-
|
|
606
|
+
const [data, rawData] = await loadTableAndRawData(
|
|
607
|
+
tableData,
|
|
608
|
+
rawTableData,
|
|
609
|
+
);
|
|
610
|
+
tableData = data;
|
|
595
611
|
return {
|
|
596
612
|
rows: tableData,
|
|
613
|
+
rawRows: rawData,
|
|
597
614
|
totalRows: totalRows,
|
|
598
615
|
cellStyles,
|
|
599
616
|
cellHoverTexts,
|
|
@@ -715,6 +732,7 @@ export const LoadingDataTableComponent = memo(
|
|
|
715
732
|
<DataTableComponent
|
|
716
733
|
{...props}
|
|
717
734
|
data={data?.rows ?? Arrays.EMPTY}
|
|
735
|
+
rawData={data?.rawRows}
|
|
718
736
|
columnSummaries={columnSummaries}
|
|
719
737
|
sorting={sorting}
|
|
720
738
|
setSorting={setSorting}
|
|
@@ -766,6 +784,7 @@ LoadingDataTableComponent.displayName = "LoadingDataTableComponent";
|
|
|
766
784
|
const DataTableComponent = ({
|
|
767
785
|
label,
|
|
768
786
|
data,
|
|
787
|
+
rawData,
|
|
769
788
|
totalRows,
|
|
770
789
|
maxColumns,
|
|
771
790
|
pagination,
|
|
@@ -814,6 +833,7 @@ const DataTableComponent = ({
|
|
|
814
833
|
}: DataTableProps<unknown> &
|
|
815
834
|
DataTableSearchProps & {
|
|
816
835
|
data: unknown[];
|
|
836
|
+
rawData?: unknown[];
|
|
817
837
|
columnSummaries?: ColumnSummaries;
|
|
818
838
|
getRow: (rowIdx: number) => Promise<GetRowResult>;
|
|
819
839
|
}): JSX.Element => {
|
|
@@ -1015,6 +1035,7 @@ const DataTableComponent = ({
|
|
|
1015
1035
|
<Labeled label={label} align="top" fullWidth={true}>
|
|
1016
1036
|
<DataTable
|
|
1017
1037
|
data={data}
|
|
1038
|
+
rawData={rawData}
|
|
1018
1039
|
columns={columns}
|
|
1019
1040
|
className={className}
|
|
1020
1041
|
maxHeight={maxHeight}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, fireEvent, render } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { z } from "zod";
|
|
6
|
+
import { SetupMocks } from "@/__mocks__/common";
|
|
7
|
+
import { initialModeAtom } from "@/core/mode";
|
|
8
|
+
import { store } from "@/core/state/jotai";
|
|
9
|
+
import type { IPluginProps } from "../../types";
|
|
10
|
+
import { SliderPlugin } from "../SliderPlugin";
|
|
11
|
+
|
|
12
|
+
SetupMocks.resizeObserver();
|
|
13
|
+
|
|
14
|
+
describe("SliderPlugin", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.useFakeTimers();
|
|
17
|
+
store.set(initialModeAtom, "edit");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const createProps = (
|
|
25
|
+
debounce: boolean,
|
|
26
|
+
includeInput: boolean,
|
|
27
|
+
setValue: ReturnType<typeof vi.fn>,
|
|
28
|
+
): IPluginProps<number, z.infer<typeof SliderPlugin.prototype.validator>> => {
|
|
29
|
+
return {
|
|
30
|
+
host: document.createElement("div"),
|
|
31
|
+
value: 5,
|
|
32
|
+
setValue,
|
|
33
|
+
data: {
|
|
34
|
+
initialValue: 5,
|
|
35
|
+
start: 0,
|
|
36
|
+
stop: 10,
|
|
37
|
+
step: 1,
|
|
38
|
+
label: "Test Slider",
|
|
39
|
+
debounce,
|
|
40
|
+
orientation: "horizontal" as const,
|
|
41
|
+
showValue: false,
|
|
42
|
+
fullWidth: false,
|
|
43
|
+
includeInput,
|
|
44
|
+
steps: null,
|
|
45
|
+
},
|
|
46
|
+
functions: {},
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
it("slider triggers setValue immediately when debounce is false", () => {
|
|
51
|
+
const plugin = new SliderPlugin();
|
|
52
|
+
const setValue = vi.fn();
|
|
53
|
+
const props = createProps(false, false, setValue);
|
|
54
|
+
const { container } = render(plugin.render(props));
|
|
55
|
+
|
|
56
|
+
act(() => {
|
|
57
|
+
vi.advanceTimersByTime(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const thumb = container.querySelector('[role="slider"]');
|
|
61
|
+
expect(thumb).toBeTruthy();
|
|
62
|
+
|
|
63
|
+
// Radix UI Slider updates on keyboard ArrowRight/ArrowLeft
|
|
64
|
+
act(() => {
|
|
65
|
+
(thumb as HTMLElement)?.focus();
|
|
66
|
+
fireEvent.keyDown(thumb!, { key: "ArrowRight" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(setValue).toHaveBeenCalledWith(6);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("slider does not trigger setValue immediately when debounce is true", () => {
|
|
73
|
+
const plugin = new SliderPlugin();
|
|
74
|
+
const setValue = vi.fn();
|
|
75
|
+
const props = createProps(true, false, setValue);
|
|
76
|
+
const { container } = render(plugin.render(props));
|
|
77
|
+
|
|
78
|
+
act(() => {
|
|
79
|
+
vi.advanceTimersByTime(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const thumb = container.querySelector('[role="slider"]');
|
|
83
|
+
|
|
84
|
+
act(() => {
|
|
85
|
+
(thumb as HTMLElement)?.focus();
|
|
86
|
+
// Simulate just a programmatic change that Radix would trigger via pointer move
|
|
87
|
+
// which fires onValueChange but not onValueCommit yet
|
|
88
|
+
// Because we can't easily separated Radix's internal pointer events in jsdom, we
|
|
89
|
+
// test the main issue: editable input. We can trust Radix's onValueChange vs onValueCommit.
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// We verified above that NumberField works when debounce=true
|
|
93
|
+
expect(setValue).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("editable input triggers setValue immediately even when slider debounce is true", () => {
|
|
97
|
+
const plugin = new SliderPlugin();
|
|
98
|
+
const setValue = vi.fn();
|
|
99
|
+
const props = createProps(true, true, setValue);
|
|
100
|
+
const { getByRole } = render(plugin.render(props));
|
|
101
|
+
|
|
102
|
+
act(() => {
|
|
103
|
+
vi.advanceTimersByTime(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// The react-aria NumberField renders an input textbox.
|
|
107
|
+
const numericInput = getByRole("textbox");
|
|
108
|
+
|
|
109
|
+
act(() => {
|
|
110
|
+
// Simulate typing a new value and pressing enter
|
|
111
|
+
// With React-Aria NumberField, onChange fires on blur or enter
|
|
112
|
+
fireEvent.change(numericInput, { target: { value: "9" } });
|
|
113
|
+
fireEvent.blur(numericInput);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Because the user explicitly typed 9 in the editable input,
|
|
117
|
+
// setValue should be called immediately regardless of debounce=true.
|
|
118
|
+
expect(setValue).toHaveBeenCalledWith(9);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -323,8 +323,7 @@ export async function handleWidgetMessage(
|
|
|
323
323
|
const msg = notification.message;
|
|
324
324
|
|
|
325
325
|
// Decode base64 buffers to DataViews (present in open/update/custom messages)
|
|
326
|
-
const base64Buffers: Base64String[] =
|
|
327
|
-
"buffers" in msg ? (msg.buffers as Base64String[]) : [];
|
|
326
|
+
const base64Buffers: Base64String[] = "buffers" in msg ? msg.buffers : [];
|
|
328
327
|
const buffers = base64Buffers.map(base64ToDataView);
|
|
329
328
|
|
|
330
329
|
switch (msg.method) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
4
4
|
import { createStore, Provider } from "jotai";
|
|
5
5
|
import { createRef } from "react";
|
|
6
|
+
import { cellId } from "@/__tests__/branded";
|
|
6
7
|
import { type NotebookState, notebookAtom } from "@/core/cells/cells";
|
|
7
8
|
import {
|
|
8
9
|
type CellRuntimeState,
|
|
@@ -18,7 +19,6 @@ import { MultiColumn } from "@/utils/id-tree";
|
|
|
18
19
|
import type { Milliseconds, Seconds } from "@/utils/time";
|
|
19
20
|
import { Cell as EditorCell } from "../components/editor/notebook-cell";
|
|
20
21
|
import { TooltipProvider } from "../components/ui/tooltip";
|
|
21
|
-
import type { CellId } from "../core/cells/ids";
|
|
22
22
|
|
|
23
23
|
type Story = StoryObj<typeof Cell>;
|
|
24
24
|
|
|
@@ -34,11 +34,11 @@ const Cell: React.FC<{
|
|
|
34
34
|
config?: CellConfig;
|
|
35
35
|
};
|
|
36
36
|
}> = ({ overrides = {} }) => {
|
|
37
|
-
const
|
|
37
|
+
const cid = cellId("1");
|
|
38
38
|
const notebook: NotebookState = {
|
|
39
39
|
cellData: {
|
|
40
|
-
[
|
|
41
|
-
id:
|
|
40
|
+
[cid]: {
|
|
41
|
+
id: cid,
|
|
42
42
|
name: "cell_1",
|
|
43
43
|
code: "import marimo as mo",
|
|
44
44
|
edited: overrides.edited ?? false,
|
|
@@ -52,9 +52,9 @@ const Cell: React.FC<{
|
|
|
52
52
|
lastExecutionTime: null,
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
|
-
cellIds: MultiColumn.from([[
|
|
55
|
+
cellIds: MultiColumn.from([[cid]]),
|
|
56
56
|
cellRuntime: {
|
|
57
|
-
[
|
|
57
|
+
[cid]: createCellRuntimeState({
|
|
58
58
|
output: overrides.output ?? null,
|
|
59
59
|
runElapsedTimeMs: overrides.runElapsedTimeMs ?? (10 as Milliseconds),
|
|
60
60
|
status: overrides.status ?? "idle",
|
|
@@ -70,7 +70,7 @@ const Cell: React.FC<{
|
|
|
70
70
|
}),
|
|
71
71
|
},
|
|
72
72
|
cellHandles: {
|
|
73
|
-
[
|
|
73
|
+
[cid]: createRef(),
|
|
74
74
|
},
|
|
75
75
|
cellLogs: [],
|
|
76
76
|
history: [],
|
|
@@ -86,7 +86,7 @@ const Cell: React.FC<{
|
|
|
86
86
|
<Provider store={store}>
|
|
87
87
|
<TooltipProvider>
|
|
88
88
|
<EditorCell
|
|
89
|
-
cellId={
|
|
89
|
+
cellId={cid}
|
|
90
90
|
theme={"light"}
|
|
91
91
|
showPlaceholder={false}
|
|
92
92
|
mode={"edit"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Meta } from "@storybook/react-vite";
|
|
4
4
|
import { createStore, Provider } from "jotai";
|
|
5
5
|
import { createRef } from "react";
|
|
6
|
+
import { cellId } from "@/__tests__/branded";
|
|
6
7
|
import { type NotebookState, notebookAtom } from "@/core/cells/cells";
|
|
7
8
|
import { createCellRuntimeState } from "@/core/cells/types";
|
|
8
9
|
import { defaultUserConfig, parseAppConfig } from "@/core/config/config-schema";
|
|
@@ -99,8 +100,8 @@ export default {
|
|
|
99
100
|
type W = Window & { __MARIMO_STATIC__?: { files: Record<string, unknown> } };
|
|
100
101
|
|
|
101
102
|
const EditModeCodeShown = () => {
|
|
102
|
-
const
|
|
103
|
-
const notebook = createLongReprNotebook(
|
|
103
|
+
const cid = cellId("Hbol");
|
|
104
|
+
const notebook = createLongReprNotebook(cid);
|
|
104
105
|
|
|
105
106
|
const store = createStore();
|
|
106
107
|
store.set(notebookAtom, notebook);
|
|
@@ -124,8 +125,8 @@ const EditModeCodeShown = () => {
|
|
|
124
125
|
};
|
|
125
126
|
|
|
126
127
|
const EditModeCodeHidden = () => {
|
|
127
|
-
const
|
|
128
|
-
const notebook = createLongReprNotebook(
|
|
128
|
+
const cid = cellId("Hbol");
|
|
129
|
+
const notebook = createLongReprNotebook(cid, true);
|
|
129
130
|
|
|
130
131
|
const store = createStore();
|
|
131
132
|
store.set(notebookAtom, notebook);
|
|
@@ -149,8 +150,8 @@ const EditModeCodeHidden = () => {
|
|
|
149
150
|
};
|
|
150
151
|
|
|
151
152
|
const ReadModeCodeShown = () => {
|
|
152
|
-
const
|
|
153
|
-
const notebook = createLongReprNotebook(
|
|
153
|
+
const cid = cellId("Hbol");
|
|
154
|
+
const notebook = createLongReprNotebook(cid);
|
|
154
155
|
|
|
155
156
|
const store = createStore();
|
|
156
157
|
store.set(notebookAtom, notebook);
|
|
@@ -168,8 +169,8 @@ const ReadModeCodeShown = () => {
|
|
|
168
169
|
};
|
|
169
170
|
|
|
170
171
|
const ReadModeCodeHidden = () => {
|
|
171
|
-
const
|
|
172
|
-
const notebook = createLongReprNotebook(
|
|
172
|
+
const cid = cellId("Hbol");
|
|
173
|
+
const notebook = createLongReprNotebook(cid);
|
|
173
174
|
|
|
174
175
|
const store = createStore();
|
|
175
176
|
store.set(notebookAtom, notebook);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { cellId } from "@/__tests__/branded";
|
|
3
4
|
import { LogViewer } from "@/components/editor/chrome/panels/logs-panel";
|
|
4
|
-
import type { CellId } from "@/core/cells/ids";
|
|
5
5
|
import { Dialog } from "../components/ui/dialog";
|
|
6
6
|
import { TooltipProvider } from "../components/ui/tooltip";
|
|
7
7
|
|
|
@@ -23,37 +23,37 @@ export const Primary: Story = {
|
|
|
23
23
|
{
|
|
24
24
|
timestamp: Date.now(),
|
|
25
25
|
level: "stdout",
|
|
26
|
-
cellId: "cell1"
|
|
26
|
+
cellId: cellId("cell1"),
|
|
27
27
|
message: "Hello world!",
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
timestamp: Date.now(),
|
|
31
31
|
level: "stdout",
|
|
32
|
-
cellId: "cell1"
|
|
32
|
+
cellId: cellId("cell1"),
|
|
33
33
|
message: "Running cell...",
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
timestamp: Date.now(),
|
|
37
37
|
level: "stdout",
|
|
38
|
-
cellId: "cell1"
|
|
38
|
+
cellId: cellId("cell1"),
|
|
39
39
|
message: "Done!",
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
timestamp: Date.now(),
|
|
43
43
|
level: "stderr",
|
|
44
|
-
cellId: "cell2"
|
|
44
|
+
cellId: cellId("cell2"),
|
|
45
45
|
message: "Output is too large!",
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
timestamp: Date.now(),
|
|
49
49
|
level: "stderr",
|
|
50
|
-
cellId: "cell2"
|
|
50
|
+
cellId: cellId("cell2"),
|
|
51
51
|
message: "String length is too short.".repeat(100),
|
|
52
52
|
},
|
|
53
|
-
...Array.from({ length: 100 }).map((
|
|
53
|
+
...Array.from({ length: 100 }).map(() => ({
|
|
54
54
|
timestamp: Date.now(),
|
|
55
55
|
level: "stdout" as const,
|
|
56
|
-
cellId: "cell1"
|
|
56
|
+
cellId: cellId("cell1"),
|
|
57
57
|
message: "Running cell...",
|
|
58
58
|
})),
|
|
59
59
|
]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { cellId } from "@/__tests__/branded";
|
|
3
4
|
import { VariableTable } from "@/components/variables/variables-table";
|
|
4
|
-
import type { CellId } from "@/core/cells/ids";
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof VariableTable> = {
|
|
7
7
|
title: "VariableTable",
|
|
@@ -51,7 +51,7 @@ export const Primary: Story = {
|
|
|
51
51
|
<div className="max-w-4xl">
|
|
52
52
|
<VariableTable
|
|
53
53
|
variables={variables}
|
|
54
|
-
cellIds={["2", "1", "3"]
|
|
54
|
+
cellIds={[cellId("2"), cellId("1"), cellId("3")]}
|
|
55
55
|
/>
|
|
56
56
|
</div>
|
|
57
57
|
),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
4
|
import { Mocks } from "@/__mocks__/common";
|
|
4
|
-
import
|
|
5
|
+
import { cellId } from "@/__tests__/branded";
|
|
5
6
|
import { CellOutputId } from "@/core/cells/ids";
|
|
6
7
|
import {
|
|
7
8
|
downloadAsPDF,
|
|
@@ -218,7 +219,7 @@ describe("getImageDataUrlForCell", () => {
|
|
|
218
219
|
beforeEach(() => {
|
|
219
220
|
vi.clearAllMocks();
|
|
220
221
|
mockElement = document.createElement("div");
|
|
221
|
-
mockElement.id = CellOutputId.create("cell-1"
|
|
222
|
+
mockElement.id = CellOutputId.create(cellId("cell-1"));
|
|
222
223
|
document.body.append(mockElement);
|
|
223
224
|
});
|
|
224
225
|
|
|
@@ -227,7 +228,7 @@ describe("getImageDataUrlForCell", () => {
|
|
|
227
228
|
});
|
|
228
229
|
|
|
229
230
|
it("should return undefined if element is not found", async () => {
|
|
230
|
-
const result = await getImageDataUrlForCell("nonexistent"
|
|
231
|
+
const result = await getImageDataUrlForCell(cellId("nonexistent"));
|
|
231
232
|
|
|
232
233
|
expect(result).toBeUndefined();
|
|
233
234
|
expect(Logger.error).toHaveBeenCalledWith(
|
|
@@ -238,7 +239,7 @@ describe("getImageDataUrlForCell", () => {
|
|
|
238
239
|
it("should capture screenshot and return data URL", async () => {
|
|
239
240
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
240
241
|
|
|
241
|
-
const result = await getImageDataUrlForCell("cell-1"
|
|
242
|
+
const result = await getImageDataUrlForCell(cellId("cell-1"));
|
|
242
243
|
|
|
243
244
|
expect(result).toBe(mockDataUrl);
|
|
244
245
|
expect(toPng).toHaveBeenCalledWith(
|
|
@@ -253,7 +254,7 @@ describe("getImageDataUrlForCell", () => {
|
|
|
253
254
|
it("should pass style options to prevent clipping", async () => {
|
|
254
255
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
255
256
|
|
|
256
|
-
await getImageDataUrlForCell("cell-1"
|
|
257
|
+
await getImageDataUrlForCell(cellId("cell-1"));
|
|
257
258
|
|
|
258
259
|
expect(toPng).toHaveBeenCalledWith(
|
|
259
260
|
mockElement,
|
|
@@ -274,7 +275,7 @@ describe("getImageDataUrlForCell", () => {
|
|
|
274
275
|
});
|
|
275
276
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
276
277
|
|
|
277
|
-
await getImageDataUrlForCell("cell-1"
|
|
278
|
+
await getImageDataUrlForCell(cellId("cell-1"));
|
|
278
279
|
|
|
279
280
|
expect(toPng).toHaveBeenCalledWith(
|
|
280
281
|
mockElement,
|
|
@@ -287,7 +288,7 @@ describe("getImageDataUrlForCell", () => {
|
|
|
287
288
|
it("should pass scrollbar hiding styles via extraStyleContent", async () => {
|
|
288
289
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
289
290
|
|
|
290
|
-
await getImageDataUrlForCell("cell-1"
|
|
291
|
+
await getImageDataUrlForCell(cellId("cell-1"));
|
|
291
292
|
|
|
292
293
|
expect(toPng).toHaveBeenCalledWith(
|
|
293
294
|
mockElement,
|
|
@@ -302,7 +303,7 @@ describe("getImageDataUrlForCell", () => {
|
|
|
302
303
|
mockElement.style.maxHeight = "100px";
|
|
303
304
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
304
305
|
|
|
305
|
-
await getImageDataUrlForCell("cell-1"
|
|
306
|
+
await getImageDataUrlForCell(cellId("cell-1"));
|
|
306
307
|
|
|
307
308
|
// DOM should remain unchanged
|
|
308
309
|
expect(mockElement.style.overflow).toBe("hidden");
|
|
@@ -312,7 +313,7 @@ describe("getImageDataUrlForCell", () => {
|
|
|
312
313
|
it("should throw error on failure", async () => {
|
|
313
314
|
vi.mocked(toPng).mockRejectedValue(new Error("Capture failed"));
|
|
314
315
|
|
|
315
|
-
await expect(getImageDataUrlForCell("cell-1"
|
|
316
|
+
await expect(getImageDataUrlForCell(cellId("cell-1"))).rejects.toThrow(
|
|
316
317
|
"Capture failed",
|
|
317
318
|
);
|
|
318
319
|
});
|
|
@@ -320,13 +321,13 @@ describe("getImageDataUrlForCell", () => {
|
|
|
320
321
|
it("should handle concurrent captures correctly", async () => {
|
|
321
322
|
// Create a second element
|
|
322
323
|
const mockElement2 = document.createElement("div");
|
|
323
|
-
mockElement2.id = CellOutputId.create("cell-2"
|
|
324
|
+
mockElement2.id = CellOutputId.create(cellId("cell-2"));
|
|
324
325
|
document.body.append(mockElement2);
|
|
325
326
|
|
|
326
327
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
327
328
|
|
|
328
|
-
const capture1 = getImageDataUrlForCell("cell-1"
|
|
329
|
-
const capture2 = getImageDataUrlForCell("cell-2"
|
|
329
|
+
const capture1 = getImageDataUrlForCell(cellId("cell-1"));
|
|
330
|
+
const capture2 = getImageDataUrlForCell(cellId("cell-2"));
|
|
330
331
|
|
|
331
332
|
await Promise.all([capture1, capture2]);
|
|
332
333
|
|
|
@@ -437,8 +438,8 @@ describe("downloadHTMLAsImage", () => {
|
|
|
437
438
|
await downloadHTMLAsImage({ element: mockElement, filename: "test" });
|
|
438
439
|
|
|
439
440
|
expect(toast).toHaveBeenCalledWith({
|
|
440
|
-
title: "
|
|
441
|
-
description: "Failed
|
|
441
|
+
title: "Failed to download as PNG",
|
|
442
|
+
description: "Failed",
|
|
442
443
|
variant: "danger",
|
|
443
444
|
});
|
|
444
445
|
});
|
|
@@ -461,7 +462,7 @@ describe("downloadCellOutputAsImage", () => {
|
|
|
461
462
|
beforeEach(() => {
|
|
462
463
|
vi.clearAllMocks();
|
|
463
464
|
mockElement = document.createElement("div");
|
|
464
|
-
mockElement.id = CellOutputId.create("cell-1"
|
|
465
|
+
mockElement.id = CellOutputId.create(cellId("cell-1"));
|
|
465
466
|
mockAppEl = document.createElement("div");
|
|
466
467
|
mockAppEl.id = "App";
|
|
467
468
|
// Mock scrollTo since jsdom doesn't implement it
|
|
@@ -486,7 +487,7 @@ describe("downloadCellOutputAsImage", () => {
|
|
|
486
487
|
});
|
|
487
488
|
|
|
488
489
|
it("should show error toast if element not found", async () => {
|
|
489
|
-
await downloadCellOutputAsImage("nonexistent"
|
|
490
|
+
await downloadCellOutputAsImage(cellId("nonexistent"), "test");
|
|
490
491
|
|
|
491
492
|
expect(toPng).not.toHaveBeenCalled();
|
|
492
493
|
expect(Logger.error).toHaveBeenCalledWith(
|
|
@@ -502,7 +503,7 @@ describe("downloadCellOutputAsImage", () => {
|
|
|
502
503
|
it("should show error toast if toPng fails", async () => {
|
|
503
504
|
vi.mocked(toPng).mockRejectedValue(new Error("Screenshot failed"));
|
|
504
505
|
|
|
505
|
-
await downloadCellOutputAsImage("cell-1"
|
|
506
|
+
await downloadCellOutputAsImage(cellId("cell-1"), "result");
|
|
506
507
|
|
|
507
508
|
expect(toast).toHaveBeenCalledWith({
|
|
508
509
|
title: "Failed to download PNG",
|
|
@@ -514,7 +515,7 @@ describe("downloadCellOutputAsImage", () => {
|
|
|
514
515
|
it("should download cell output as image", async () => {
|
|
515
516
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
516
517
|
|
|
517
|
-
await downloadCellOutputAsImage("cell-1"
|
|
518
|
+
await downloadCellOutputAsImage(cellId("cell-1"), "result");
|
|
518
519
|
|
|
519
520
|
expect(toPng).toHaveBeenCalledWith(
|
|
520
521
|
mockElement,
|
|
@@ -529,7 +530,7 @@ describe("downloadCellOutputAsImage", () => {
|
|
|
529
530
|
it("should pass style options to toPng for full content capture", async () => {
|
|
530
531
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
531
532
|
|
|
532
|
-
await downloadCellOutputAsImage("cell-1"
|
|
533
|
+
await downloadCellOutputAsImage(cellId("cell-1"), "result");
|
|
533
534
|
|
|
534
535
|
expect(toPng).toHaveBeenCalledWith(
|
|
535
536
|
mockElement,
|
|
@@ -547,7 +548,7 @@ describe("downloadCellOutputAsImage", () => {
|
|
|
547
548
|
mockElement.style.maxHeight = "100px";
|
|
548
549
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
549
550
|
|
|
550
|
-
await downloadCellOutputAsImage("cell-1"
|
|
551
|
+
await downloadCellOutputAsImage(cellId("cell-1"), "result");
|
|
551
552
|
|
|
552
553
|
// DOM should remain unchanged
|
|
553
554
|
expect(mockElement.style.overflow).toBe("hidden");
|
package/src/utils/copy.ts
CHANGED
|
@@ -2,21 +2,34 @@
|
|
|
2
2
|
import { Logger } from "./Logger";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Copy text to the clipboard. When `html` is provided, writes both
|
|
6
|
+
* text/html and text/plain so rich content (e.g. hyperlinks) is
|
|
7
|
+
* preserved when pasting into apps like Excel or Google Sheets.
|
|
7
8
|
*
|
|
8
9
|
* As of 2024-10-29, Safari does not support navigator.clipboard.writeText
|
|
9
10
|
* when running localhost http.
|
|
10
11
|
*/
|
|
11
|
-
export async function copyToClipboard(text: string) {
|
|
12
|
+
export async function copyToClipboard(text: string, html?: string) {
|
|
12
13
|
if (navigator.clipboard === undefined) {
|
|
13
14
|
Logger.warn("navigator.clipboard is not supported");
|
|
14
15
|
window.prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
if (html && navigator.clipboard.write) {
|
|
20
|
+
try {
|
|
21
|
+
const item = new ClipboardItem({
|
|
22
|
+
"text/html": new Blob([html], { type: "text/html" }),
|
|
23
|
+
"text/plain": new Blob([text], { type: "text/plain" }),
|
|
24
|
+
});
|
|
25
|
+
await navigator.clipboard.write([item]);
|
|
26
|
+
return;
|
|
27
|
+
} catch {
|
|
28
|
+
Logger.warn("Failed to write rich text, falling back to plain text");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await navigator.clipboard.writeText(text).catch(() => {
|
|
20
33
|
Logger.warn("Failed to copy to clipboard using navigator.clipboard");
|
|
21
34
|
window.prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
|
22
35
|
});
|