@marimo-team/islands 0.25.3-dev191 → 0.25.3-dev193
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-BBbOElk3.js → ConnectedDataExplorerComponent-BRuwLOIx.js} +9 -9
- package/dist/{ErrorBoundary--X0z83Yx.js → ErrorBoundary-BVu_xT-p.js} +2 -2
- package/dist/{abnfDiagram-VCTEODGH-DhMT6Uhd.js → abnfDiagram-VCTEODGH-LlJTOOU_.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-BVbEiJLq.js → architectureDiagram-5GKGNRK7-9PQtGLLZ.js} +1 -1
- package/dist/{arrays-R6JsImiT.js → arrays-DA6KkLfq.js} +6 -12
- package/dist/assets/__vite-browser-external-BCJsx8QS.js +1 -0
- package/dist/assets/{worker-C6e9ERyd.js → worker-DodjWT_Z.js} +2 -2
- package/dist/{capabilities-CI4xUPsp.js → capabilities-CvWU-ey_.js} +1 -1
- package/dist/{chat-ui-BguxTQEL.js → chat-ui-Bl2euMBD.js} +93 -93
- package/dist/{chunk-IMKFNOWR-By5hSbJJ.js → chunk-IMKFNOWR-DcXv-BD5.js} +1 -1
- package/dist/{chunk-SHT3W25Y-DFrEYG54.js → chunk-SHT3W25Y-VwnELN23.js} +1 -1
- package/dist/{chunk-SVP7TREG-DFXGc9ew.js → chunk-SVP7TREG-DgQpI5Zi.js} +1 -1
- package/dist/{chunk-TICWLB2K-DZdNIpoZ.js → chunk-TICWLB2K-CTg6VTJT.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-Cgj_eEQA.js → classDiagram-ZZMXUADV-UzmcFrnW.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-DLAUrSqj.js → classDiagram-v2-VYDZK3BY-DhjiI6IQ.js} +2 -2
- package/dist/{code-visibility-uDyLo9_i.js → code-visibility-BTVimCUw.js} +38 -42
- package/dist/{common-DWKY45Md.js → common-fTgq7gO-.js} +849 -849
- package/dist/{cynefinDiagram-5FMLGOSQ-Cikf9AKa.js → cynefinDiagram-5FMLGOSQ-O4ShIbJX.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-C5KEi5_l.js → dagre-GXQ25YYZ-dtHhhcPu.js} +1 -1
- package/dist/{data-grid-overlay-editor-CRnYWD1L.js → data-grid-overlay-editor-DAZZkLWg.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BaoBTQjR.js → diagram-S7CK7UJ4-ppPYVZ6W.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DDufBsUA.js → diagram-UQ7AKVKN-CMZDf--k.js} +1 -1
- package/dist/{diagram-VX7I27RA-CY6RALJ8.js → diagram-VX7I27RA-CFvJdx-C.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Bc-LHsD6.js → diagram-Z3DM3KII-Dt6_uFbv.js} +1 -1
- package/dist/{dropdown-menu-drDjxJyV.js → dropdown-menu-CeaOsjWZ.js} +2 -2
- package/dist/{ebnfDiagram-PWID7BFC-Cz0J-tcC.js → ebnfDiagram-PWID7BFC-DpRFrKK-.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-DTXTDgFu.js → erDiagram-RLTQ6QDP-CqAU4b0u.js} +1 -1
- package/dist/{error-entries-CZ1NQ6Kv.js → error-entries-CYGwr5-O.js} +5596 -6012
- package/dist/feedback-button-DzFFn-n4.js +374 -0
- package/dist/{flowDiagram-HODETNUW-RbwWf2zc.js → flowDiagram-HODETNUW-i8-Gw8HO.js} +2 -2
- package/dist/{formats-DbXnijtH.js → formats-BtoHHy42.js} +1 -1
- package/dist/{glide-data-editor-CvRiA49N.js → glide-data-editor-DFsqVbWR.js} +5 -5
- package/dist/{globals-BR4CmGOS.js → globals-Dr0MRANu.js} +2 -2
- package/dist/{html-to-image-D_4_hQ8U.js → html-to-image-Cy5UDpQJ.js} +56 -56
- package/dist/{infoDiagram-27XIBGKW-DlmzZjmI.js → infoDiagram-27XIBGKW-DpfOKdLL.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-EN3TYP_q.js → ishikawaDiagram-5VMMS53U-Dg7mrMG7.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-CK_-Ru7Q.js → kanban-definition-UXKFOSKX-QB6e3Jrj.js} +1 -1
- package/dist/main.js +5875 -5876
- package/dist/{mermaid-DU9ZRmkG.js → mermaid-CeWG4Egr.js} +48 -48
- package/dist/{mindmap-definition-YA3MSWOX-C-hqgUvF.js → mindmap-definition-YA3MSWOX-BTz3uTGe.js} +1 -1
- package/dist/{number-overlay-editor-DiYGbxNa.js → number-overlay-editor-DupOSYHx.js} +1 -1
- package/dist/{once-eG8Vmvjd.js → once-DWGO2mQ2.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-BiMejYPB.js → pegDiagram-XKGWAZYB-3ZIEa3mB.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-B1zCZ1eq.js → pieDiagram-E7YTZNPT-BJAlk046.js} +1 -1
- package/dist/{process-output-DxSCHBWp.js → process-output-q9MTAIMX.js} +2 -2
- package/dist/{railroadDiagram-O6MQD6OU-BLcfaASl.js → railroadDiagram-O6MQD6OU-Dn3bsSRa.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-DtBOf52-.js → requirementDiagram-BXWQKSXE-wcOmQQCL.js} +1 -1
- package/dist/{reveal-component-D3DsXPIm.js → reveal-component-Bg8rP_Uo.js} +17 -17
- package/dist/{sequenceDiagram-WJ2MYXX4-DIwF19_n.js → sequenceDiagram-WJ2MYXX4-DSh_qxoF.js} +1 -1
- package/dist/{spec-DZDnnlgT.js → spec-C7Fos-If.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-sWk1AdYL.js → stateDiagram-D77RDMKH-DH_H8NMH.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-CiNo-OI7.js → stateDiagram-v2-MP3YSRHH-D_JM7MEK.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-Bj3hiOY5.js → swimlanes-42K2YHIH-CfoTJO-9.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-BzEo2r_N.js → swimlanesDiagram-VR7AAH4N-Z_QhvK0G.js} +2 -2
- package/dist/{swiper-component-BkCm-kLg.js → swiper-component-D2geL1Ku.js} +1 -1
- package/dist/{timeline-definition-24CTP7MA-CrPQk42H.js → timeline-definition-24CTP7MA-BFtdScaL.js} +1 -1
- package/dist/{toDate-Cu2pIZcC.js → toDate-CQAFSwos.js} +24 -24
- package/dist/{useAsyncData-DEhLdRIu.js → useAsyncData-BeyZyjso.js} +1 -1
- package/dist/{useDeepCompareMemoize-RjZGVhBV.js → useDeepCompareMemoize-DOQgae0W.js} +1 -1
- package/dist/{useIframeCapabilities-CSdWn6NP.js → useIframeCapabilities-DNvaj9wM.js} +1 -1
- package/dist/{useLifecycle-DtcB2s1z.js → useLifecycle-n2lnCFS8.js} +1 -1
- package/dist/{useTheme-CoEtr4mK.js → useTheme-CY777LXN.js} +303 -99
- package/dist/{vega-component-BhjTrP9-.js → vega-component-0nQzA-I8.js} +6 -6
- package/dist/{vennDiagram-4TSXK5OY-BaYMoIpj.js → vennDiagram-4TSXK5OY-BXY8IGCA.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-DjHeuj8p.js → wardleyDiagram-VM6X3IG4-D0tFe6eC.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-Bc_hcuXS.js → xychartDiagram-S5SC5T6Z-Dx8wFM-y.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/notebook.ts +0 -1
- package/src/__mocks__/partial.ts +26 -0
- package/src/__tests__/main.test.tsx +24 -1
- package/src/components/app-config/app-config-form.tsx +5 -9
- package/src/components/editor/__tests__/data-attributes.test.tsx +0 -1
- package/src/components/editor/__tests__/output-persistence.test.tsx +0 -1
- package/src/components/editor/actions/useCellActionButton.tsx +6 -5
- package/src/components/editor/actions/useConfigActions.tsx +3 -4
- package/src/components/editor/actions/useNotebookActions.tsx +5 -4
- package/src/components/editor/cell/code/cell-editor.tsx +13 -6
- package/src/components/editor/cell/collapse.tsx +6 -1
- package/src/components/editor/chrome/components/feedback-button.tsx +1 -7
- package/src/components/editor/controls/Controls.tsx +1 -1
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +8 -6
- package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -15
- package/src/components/editor/navigation/__tests__/selection.test.ts +44 -21
- package/src/components/editor/navigation/focus-utils.ts +19 -0
- package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +6 -13
- package/src/components/editor/navigation/navigation.ts +52 -43
- package/src/components/editor/navigation/selection.ts +30 -29
- package/src/components/editor/notebook-cell.tsx +10 -18
- package/src/components/editor/renderers/cell-array.tsx +6 -5
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/pages/edit-page.tsx +0 -13
- package/src/components/scratchpad/scratchpad.tsx +0 -1
- package/src/components/slides/reveal-component.tsx +1 -1
- package/src/components/slides/slide-cell-view.tsx +0 -1
- package/src/components/sort/SortableCellsProvider.tsx +6 -5
- package/src/core/MarimoApp.tsx +1 -1
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -37
- package/src/core/cells/__tests__/actions.test.ts +190 -80
- package/src/core/cells/__tests__/arrange.test.ts +263 -0
- package/src/core/cells/__tests__/cells.test.ts +11 -1101
- package/src/core/cells/__tests__/document.test.ts +3 -32
- package/src/core/cells/__tests__/editor.test.ts +57 -0
- package/src/core/cells/__tests__/harness.ts +82 -0
- package/src/core/cells/__tests__/layout.test.ts +189 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +7 -1
- package/src/core/cells/__tests__/session.test.ts +329 -174
- package/src/core/cells/__tests__/utils.test.ts +0 -1
- package/src/core/cells/actions.ts +247 -82
- package/src/core/cells/arrange.ts +341 -0
- package/src/core/cells/cells.ts +27 -516
- package/src/core/cells/document.ts +6 -5
- package/src/core/cells/edit.ts +30 -0
- package/src/core/cells/editor.ts +82 -0
- package/src/core/cells/layout.ts +152 -0
- package/src/core/cells/session.ts +75 -52
- package/src/core/cells/types.ts +0 -5
- package/src/core/cells/utils.ts +6 -4
- package/src/core/config/__tests__/app-config.test.ts +64 -0
- package/src/core/config/__tests__/config-schema.test.ts +1 -1
- package/src/core/config/config-schema.ts +6 -2
- package/src/core/config/config.ts +26 -12
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/kernel/handlers.ts +1 -13
- package/src/core/meta/__tests__/code-visibility.test.tsx +78 -70
- package/src/core/meta/code-visibility.ts +18 -9
- package/src/core/notebook/__tests__/operations.test.ts +51 -0
- package/src/core/notebook/document/__tests__/codemirror.test.ts +15 -0
- package/src/core/notebook/index.ts +2 -1
- package/src/core/notebook/operations.ts +30 -0
- package/src/core/saving/__tests__/state.test.ts +111 -0
- package/src/core/saving/save-component.tsx +19 -33
- package/src/core/saving/state.ts +38 -39
- package/src/core/saving/useAutoSave.ts +8 -35
- package/src/core/session/__tests__/backend.test.ts +1 -0
- package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +1 -0
- package/src/core/session/active-notebook.ts +11 -0
- package/src/core/session/backend.ts +17 -19
- package/src/core/session/embedded.ts +7 -0
- package/src/core/session/useNotebookReplica.ts +3 -1
- package/src/core/session/useNotebookSession.tsx +7 -8
- package/src/core/transports/__tests__/static.test.ts +139 -8
- package/src/core/transports/static.ts +98 -16
- package/src/core/wasm/__tests__/bridge.test.ts +1 -0
- package/src/mount.tsx +19 -10
- package/src/stories/cell.stories.tsx +0 -2
- package/src/stories/layout/vertical/one-column.stories.tsx +0 -2
- package/src/utils/__tests__/id-tree.test.ts +20 -1548
- package/src/utils/id-tree.tsx +12 -537
- package/dist/assets/__vite-browser-external-C8gQho07.js +0 -1
- package/dist/feedback-button-CDf1yRm_.js +0 -379
- package/src/components/editor/navigation/__tests__/state.test.ts +0 -81
- package/src/components/editor/navigation/state.ts +0 -65
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { renderHook } from "@testing-library/react";
|
|
4
|
-
import
|
|
4
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
5
|
+
import { atom, createStore, Provider } from "jotai";
|
|
5
6
|
import type { ReactNode } from "react";
|
|
6
7
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
+
import { partial } from "@/__mocks__/partial";
|
|
7
9
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
8
10
|
import { type AppMode, kioskModeAtom, viewStateAtom } from "@/core/mode";
|
|
11
|
+
import type { Document, NotebookReplica } from "@/core/notebook/replica";
|
|
9
12
|
import {
|
|
10
13
|
remoteAdapter,
|
|
11
14
|
runtimeAdapterAtom,
|
|
12
15
|
staticAdapter,
|
|
13
16
|
type RuntimeAdapter,
|
|
14
17
|
} from "@/core/runtime/adapter";
|
|
18
|
+
import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
|
|
15
19
|
import { useNotebookCodeAvailable } from "../code-visibility";
|
|
16
20
|
|
|
17
21
|
interface StoreOpts {
|
|
@@ -19,6 +23,39 @@ interface StoreOpts {
|
|
|
19
23
|
kiosk?: boolean;
|
|
20
24
|
showInRunMode?: boolean;
|
|
21
25
|
runtime?: RuntimeAdapter;
|
|
26
|
+
/** Each cell's source as the host sent it; null is withheld. */
|
|
27
|
+
codes?: ReadonlyArray<string | null>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function document(codes: ReadonlyArray<string | null>): Document {
|
|
31
|
+
const cells = codes.map((code, index): Protocol.Cell => ({
|
|
32
|
+
id: `cell-${index}` as Protocol.CellId,
|
|
33
|
+
notebook_id: "nb",
|
|
34
|
+
code,
|
|
35
|
+
name: "_",
|
|
36
|
+
config: {
|
|
37
|
+
column: null,
|
|
38
|
+
disabled: false,
|
|
39
|
+
hide_code: false,
|
|
40
|
+
expand_output: false,
|
|
41
|
+
},
|
|
42
|
+
etag: `etag-${index}`,
|
|
43
|
+
code_hash: `hash-${index}`,
|
|
44
|
+
version: 1,
|
|
45
|
+
}));
|
|
46
|
+
return {
|
|
47
|
+
revision: {
|
|
48
|
+
notebook_id: "nb",
|
|
49
|
+
order: cells.map((cell) => cell.id),
|
|
50
|
+
app_config: {},
|
|
51
|
+
etag: "etag",
|
|
52
|
+
saved_etag: "etag",
|
|
53
|
+
order_etag: "order",
|
|
54
|
+
app_config_etag: "config",
|
|
55
|
+
version: 1,
|
|
56
|
+
},
|
|
57
|
+
cells: new Map(cells.map((cell) => [cell.id, cell])),
|
|
58
|
+
};
|
|
22
59
|
}
|
|
23
60
|
|
|
24
61
|
function makeStore({
|
|
@@ -26,23 +63,32 @@ function makeStore({
|
|
|
26
63
|
kiosk = false,
|
|
27
64
|
showInRunMode = true,
|
|
28
65
|
runtime = remoteAdapter,
|
|
66
|
+
codes = ["x = 1", ""],
|
|
29
67
|
}: StoreOpts = {}) {
|
|
30
68
|
const store = createStore();
|
|
31
69
|
store.set(viewStateAtom, { mode, cellAnchor: null });
|
|
32
70
|
store.set(kioskModeAtom, kiosk);
|
|
33
71
|
store.set(showCodeInRunModeAtom, showInRunMode);
|
|
34
72
|
store.set(runtimeAdapterAtom, runtime);
|
|
73
|
+
store.set(
|
|
74
|
+
activeNotebookReplicaAtom,
|
|
75
|
+
partial<NotebookReplica>(
|
|
76
|
+
{ documentAtom: atom(document(codes)) },
|
|
77
|
+
"replica",
|
|
78
|
+
),
|
|
79
|
+
);
|
|
35
80
|
return store;
|
|
36
81
|
}
|
|
37
82
|
|
|
38
|
-
function
|
|
39
|
-
|
|
83
|
+
function render(store: ReturnType<typeof createStore>) {
|
|
84
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
40
85
|
<Provider store={store}>{children}</Provider>
|
|
41
86
|
);
|
|
87
|
+
return renderHook(() => useNotebookCodeAvailable(), { wrapper }).result
|
|
88
|
+
.current;
|
|
42
89
|
}
|
|
43
90
|
|
|
44
|
-
const
|
|
45
|
-
const cellsWithoutCode = [{ code: "" }, { code: "" }];
|
|
91
|
+
const withheld = [null, null];
|
|
46
92
|
|
|
47
93
|
const originalHref = window.location.href;
|
|
48
94
|
|
|
@@ -60,21 +106,11 @@ describe("useNotebookCodeAvailable", () => {
|
|
|
60
106
|
});
|
|
61
107
|
|
|
62
108
|
it("returns true in edit mode regardless of cells", () => {
|
|
63
|
-
|
|
64
|
-
const { result } = renderHook(
|
|
65
|
-
() => useNotebookCodeAvailable(cellsWithoutCode),
|
|
66
|
-
{ wrapper: wrap(store) },
|
|
67
|
-
);
|
|
68
|
-
expect(result.current).toBe(true);
|
|
109
|
+
expect(render(makeStore({ mode: "edit", codes: withheld }))).toBe(true);
|
|
69
110
|
});
|
|
70
111
|
|
|
71
112
|
it("returns true in present mode (only reachable from edit)", () => {
|
|
72
|
-
|
|
73
|
-
const { result } = renderHook(
|
|
74
|
-
() => useNotebookCodeAvailable(cellsWithoutCode),
|
|
75
|
-
{ wrapper: wrap(store) },
|
|
76
|
-
);
|
|
77
|
-
expect(result.current).toBe(true);
|
|
113
|
+
expect(render(makeStore({ mode: "present", codes: withheld }))).toBe(true);
|
|
78
114
|
});
|
|
79
115
|
|
|
80
116
|
it("returns true in kiosk mode even when read mode and no code", () => {
|
|
@@ -82,88 +118,60 @@ describe("useNotebookCodeAvailable", () => {
|
|
|
82
118
|
mode: "read",
|
|
83
119
|
kiosk: true,
|
|
84
120
|
showInRunMode: false,
|
|
121
|
+
codes: withheld,
|
|
85
122
|
});
|
|
86
|
-
|
|
87
|
-
() => useNotebookCodeAvailable(cellsWithoutCode),
|
|
88
|
-
{ wrapper: wrap(store) },
|
|
89
|
-
);
|
|
90
|
-
expect(result.current).toBe(true);
|
|
123
|
+
expect(render(store)).toBe(true);
|
|
91
124
|
});
|
|
92
125
|
|
|
93
126
|
it("returns true in read mode when at least one cell has code", () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
99
|
-
|
|
127
|
+
expect(render(makeStore({ mode: "read" }))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("counts an empty cell as code the host sent", () => {
|
|
131
|
+
expect(render(makeStore({ mode: "read", codes: ["", null] }))).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns false in read mode when the host withheld every source", () => {
|
|
135
|
+
expect(render(makeStore({ mode: "read", codes: withheld }))).toBe(false);
|
|
100
136
|
});
|
|
101
137
|
|
|
102
|
-
it("returns false in read mode
|
|
138
|
+
it("returns false in read mode before a document arrives", () => {
|
|
103
139
|
const store = makeStore({ mode: "read" });
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ wrapper: wrap(store) },
|
|
107
|
-
);
|
|
108
|
-
expect(result.current).toBe(false);
|
|
140
|
+
store.set(activeNotebookReplicaAtom, null);
|
|
141
|
+
expect(render(store)).toBe(false);
|
|
109
142
|
});
|
|
110
143
|
|
|
111
144
|
it("returns false in read mode when host opts out via showAppCode", () => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
() => useNotebookCodeAvailable(cellsWithCode),
|
|
115
|
-
{ wrapper: wrap(store) },
|
|
145
|
+
expect(render(makeStore({ mode: "read", showInRunMode: false }))).toBe(
|
|
146
|
+
false,
|
|
116
147
|
);
|
|
117
|
-
expect(result.current).toBe(false);
|
|
118
148
|
});
|
|
119
149
|
|
|
120
150
|
it("returns false in read mode when ?include-code=false", () => {
|
|
121
151
|
setSearch("?include-code=false");
|
|
122
|
-
|
|
123
|
-
const { result } = renderHook(
|
|
124
|
-
() => useNotebookCodeAvailable(cellsWithCode),
|
|
125
|
-
{ wrapper: wrap(store) },
|
|
126
|
-
);
|
|
127
|
-
expect(result.current).toBe(false);
|
|
152
|
+
expect(render(makeStore({ mode: "read" }))).toBe(false);
|
|
128
153
|
});
|
|
129
154
|
|
|
130
155
|
it("ignores ?include-code=false in kiosk mode", () => {
|
|
131
156
|
setSearch("?include-code=false");
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{ wrapper: wrap(store) },
|
|
136
|
-
);
|
|
137
|
-
expect(result.current).toBe(true);
|
|
157
|
+
expect(
|
|
158
|
+
render(makeStore({ mode: "read", kiosk: true, codes: withheld })),
|
|
159
|
+
).toBe(true);
|
|
138
160
|
});
|
|
139
161
|
|
|
140
162
|
it("checks embedded code in a static kiosk", () => {
|
|
141
|
-
const
|
|
163
|
+
const options = {
|
|
142
164
|
mode: "read",
|
|
143
165
|
kiosk: true,
|
|
144
166
|
runtime: staticAdapter,
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
() => useNotebookCodeAvailable(cellsWithoutCode),
|
|
149
|
-
{ wrapper: wrap(store) },
|
|
150
|
-
);
|
|
151
|
-
const withCode = renderHook(() => useNotebookCodeAvailable(cellsWithCode), {
|
|
152
|
-
wrapper: wrap(store),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
expect(withoutCode.result.current).toBe(false);
|
|
156
|
-
expect(withCode.result.current).toBe(true);
|
|
167
|
+
} as const;
|
|
168
|
+
expect(render(makeStore({ ...options, codes: withheld }))).toBe(false);
|
|
169
|
+
expect(render(makeStore(options))).toBe(true);
|
|
157
170
|
});
|
|
158
171
|
|
|
159
172
|
it("returns false for non-edit/non-read modes (home, gallery)", () => {
|
|
160
173
|
for (const mode of ["home", "gallery"] as const) {
|
|
161
|
-
|
|
162
|
-
const { result } = renderHook(
|
|
163
|
-
() => useNotebookCodeAvailable(cellsWithCode),
|
|
164
|
-
{ wrapper: wrap(store) },
|
|
165
|
-
);
|
|
166
|
-
expect(result.current).toBe(false);
|
|
174
|
+
expect(render(makeStore({ mode }))).toBe(false);
|
|
167
175
|
}
|
|
168
176
|
});
|
|
169
177
|
});
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtomValue } from "jotai";
|
|
3
|
+
import { atom, useAtomValue } from "jotai";
|
|
4
4
|
import { KnownQueryParams } from "@/core/constants";
|
|
5
5
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
6
6
|
import { kioskModeAtom, viewStateAtom } from "@/core/mode";
|
|
7
7
|
import { runtimeAdapterAtom } from "@/core/runtime/adapter";
|
|
8
|
+
import { confirmedDocumentAtom } from "@/core/session/active-notebook";
|
|
8
9
|
import { logNever } from "@/utils/assertNever";
|
|
9
10
|
|
|
11
|
+
/** Whether any cell's source reached this client. A host withholds source as null. */
|
|
12
|
+
export const codeAvailableAtom = atom((get) => {
|
|
13
|
+
const document = get(confirmedDocumentAtom);
|
|
14
|
+
return (
|
|
15
|
+
document !== null &&
|
|
16
|
+
[...document.cells.values()].some((cell) => cell.code !== null)
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
|
-
* Whether the notebook source code
|
|
21
|
+
* Whether the notebook source code can be rendered.
|
|
12
22
|
*
|
|
13
|
-
* In `marimo run` the server
|
|
14
|
-
* set
|
|
15
|
-
*
|
|
23
|
+
* In `marimo run` the server withholds cell sources unless `--include-code`
|
|
24
|
+
* is set. Use this to gate any "show code" / "copy code" / "download .py"
|
|
25
|
+
* affordance.
|
|
16
26
|
*/
|
|
17
|
-
export function useNotebookCodeAvailable(
|
|
18
|
-
|
|
19
|
-
): boolean {
|
|
27
|
+
export function useNotebookCodeAvailable(): boolean {
|
|
28
|
+
const available = useAtomValue(codeAvailableAtom);
|
|
20
29
|
const kioskMode = useAtomValue(kioskModeAtom);
|
|
21
30
|
const { mode } = useAtomValue(viewStateAtom);
|
|
22
31
|
const showInRunMode = useAtomValue(showCodeInRunModeAtom);
|
|
@@ -41,7 +50,7 @@ export function useNotebookCodeAvailable(
|
|
|
41
50
|
if (params.get(KnownQueryParams.includeCode) === "false") {
|
|
42
51
|
return false;
|
|
43
52
|
}
|
|
44
|
-
return
|
|
53
|
+
return available;
|
|
45
54
|
}
|
|
46
55
|
default:
|
|
47
56
|
logNever(mode);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { NotebookOutput } from "@marimo-team/client/protocol";
|
|
4
|
+
import { atom, createStore } from "jotai";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { partial } from "@/__mocks__/partial";
|
|
7
|
+
import { kioskModeAtom } from "@/core/mode";
|
|
8
|
+
import type { NotebookReplica } from "@/core/notebook/replica";
|
|
9
|
+
import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
|
|
10
|
+
import { canEditAtom, notebookOperationsAtom } from "../operations";
|
|
11
|
+
|
|
12
|
+
function connect(
|
|
13
|
+
store: ReturnType<typeof createStore>,
|
|
14
|
+
operations: readonly string[] | null,
|
|
15
|
+
) {
|
|
16
|
+
const notebook =
|
|
17
|
+
operations === null
|
|
18
|
+
? null
|
|
19
|
+
: ({ id: "nb", operations } as unknown as NotebookOutput);
|
|
20
|
+
store.set(
|
|
21
|
+
activeNotebookReplicaAtom,
|
|
22
|
+
partial<NotebookReplica>({ notebookAtom: atom(notebook) }, "replica"),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("notebook operations", () => {
|
|
27
|
+
it("advertises nothing until the notebook is ready", () => {
|
|
28
|
+
const store = createStore();
|
|
29
|
+
expect(store.get(notebookOperationsAtom)).toEqual([]);
|
|
30
|
+
expect(store.get(canEditAtom)).toBe(false);
|
|
31
|
+
connect(store, null);
|
|
32
|
+
expect(store.get(canEditAtom)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("allows editing when the host lists notebook.edit", () => {
|
|
36
|
+
const store = createStore();
|
|
37
|
+
connect(store, ["notebook.edit", "runtime.run"]);
|
|
38
|
+
expect(store.get(canEditAtom)).toBe(true);
|
|
39
|
+
connect(store, ["runtime.run"]);
|
|
40
|
+
expect(store.get(canEditAtom)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("withholds editing from a kiosk connection", () => {
|
|
44
|
+
const store = createStore();
|
|
45
|
+
connect(store, ["notebook.edit"]);
|
|
46
|
+
store.set(kioskModeAtom, true);
|
|
47
|
+
expect(store.get(canEditAtom)).toBe(false);
|
|
48
|
+
store.set(kioskModeAtom, false);
|
|
49
|
+
expect(store.get(canEditAtom)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -65,6 +65,21 @@ function languages(id: CellId) {
|
|
|
65
65
|
addSetupCell: vi.fn(),
|
|
66
66
|
deleteCells: vi.fn(),
|
|
67
67
|
undoDeleteCell: vi.fn(),
|
|
68
|
+
focusCell: vi.fn(),
|
|
69
|
+
focusTopCell: vi.fn(),
|
|
70
|
+
focusBottomCell: vi.fn(),
|
|
71
|
+
collapseCell: vi.fn(),
|
|
72
|
+
expandCell: vi.fn(),
|
|
73
|
+
collapseAllCells: vi.fn(),
|
|
74
|
+
expandAllCells: vi.fn(),
|
|
75
|
+
showCellIfHidden: vi.fn(),
|
|
76
|
+
moveCell: vi.fn(),
|
|
77
|
+
moveCellToIndex: vi.fn(),
|
|
78
|
+
moveCellsRelativeTo: vi.fn(),
|
|
79
|
+
dropCellOverCell: vi.fn(),
|
|
80
|
+
dropCellOverColumn: vi.fn(),
|
|
81
|
+
sendToTop: vi.fn(),
|
|
82
|
+
sendToBottom: vi.fn(),
|
|
68
83
|
onRun: vi.fn(),
|
|
69
84
|
deleteCell: vi.fn(),
|
|
70
85
|
afterToggleMarkdown: vi.fn(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** What the editor reads about the current notebook. */
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
useNotebookAttachments,
|
|
@@ -16,3 +16,4 @@ export {
|
|
|
16
16
|
useNotebookDocumentEdit,
|
|
17
17
|
useNotebookDocumentSync,
|
|
18
18
|
} from "./document";
|
|
19
|
+
export { canEditAtom, notebookOperationsAtom } from "./operations";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/** What the host lets this client do to the notebook. */
|
|
4
|
+
|
|
5
|
+
import { atom } from "jotai";
|
|
6
|
+
import { kioskModeAtom } from "../mode";
|
|
7
|
+
import { activeNotebookReplicaAtom } from "../session/active-notebook";
|
|
8
|
+
|
|
9
|
+
const NO_OPERATIONS: readonly string[] = [];
|
|
10
|
+
|
|
11
|
+
/** Operations the host advertises for this notebook, none until its stream is ready. */
|
|
12
|
+
export const notebookOperationsAtom = atom((get) => {
|
|
13
|
+
const replica = get(activeNotebookReplicaAtom);
|
|
14
|
+
const notebook = replica === null ? null : get(replica.notebookAtom);
|
|
15
|
+
return notebook?.operations ?? NO_OPERATIONS;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether this client may edit the document.
|
|
20
|
+
*
|
|
21
|
+
* TODO(MO-8028): The host advertises the same operations to every
|
|
22
|
+
* connection of a notebook, so a kiosk connection, an interactor that may
|
|
23
|
+
* not edit, narrows it here. Once the host narrows `operations` per
|
|
24
|
+
* connection by consumer capability, the kiosk term goes.
|
|
25
|
+
*/
|
|
26
|
+
export const canEditAtom = atom(
|
|
27
|
+
(get) =>
|
|
28
|
+
!get(kioskModeAtom) &&
|
|
29
|
+
get(notebookOperationsAtom).includes("notebook.edit"),
|
|
30
|
+
);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
4
|
+
import { atom, createStore } from "jotai";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { partial } from "@/__mocks__/partial";
|
|
7
|
+
import type {
|
|
8
|
+
DocumentModel,
|
|
9
|
+
DocumentSyncStatus,
|
|
10
|
+
} from "@/core/notebook/document";
|
|
11
|
+
import type { Document, NotebookReplica } from "@/core/notebook/replica";
|
|
12
|
+
import {
|
|
13
|
+
activeDocumentModelAtom,
|
|
14
|
+
activeNotebookReplicaAtom,
|
|
15
|
+
} from "@/core/session/active-notebook";
|
|
16
|
+
import { initialLayoutState, layoutStateAtom } from "../../layout/state";
|
|
17
|
+
import { documentVersionAtom, needsSaveAtom, savedLayoutAtom } from "../state";
|
|
18
|
+
|
|
19
|
+
function revision(
|
|
20
|
+
overrides: Partial<Protocol.Document> = {},
|
|
21
|
+
): Protocol.Document {
|
|
22
|
+
return {
|
|
23
|
+
notebook_id: "nb",
|
|
24
|
+
order: [],
|
|
25
|
+
app_config: {},
|
|
26
|
+
etag: "v2",
|
|
27
|
+
saved_etag: "v2",
|
|
28
|
+
order_etag: "order",
|
|
29
|
+
app_config_etag: "config",
|
|
30
|
+
version: 2,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function connect(
|
|
36
|
+
store: ReturnType<typeof createStore>,
|
|
37
|
+
document: Document | null,
|
|
38
|
+
status: DocumentSyncStatus["status"] = "idle",
|
|
39
|
+
) {
|
|
40
|
+
store.set(
|
|
41
|
+
activeNotebookReplicaAtom,
|
|
42
|
+
partial<NotebookReplica>({ documentAtom: atom(document) }, "replica"),
|
|
43
|
+
);
|
|
44
|
+
store.set(
|
|
45
|
+
activeDocumentModelAtom,
|
|
46
|
+
partial<DocumentModel>(
|
|
47
|
+
{ syncStatusAtom: atom({ status } as DocumentSyncStatus) },
|
|
48
|
+
"model",
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("needsSaveAtom", () => {
|
|
54
|
+
it("is false without a connected document", () => {
|
|
55
|
+
const store = createStore();
|
|
56
|
+
expect(store.get(needsSaveAtom)).toBe(false);
|
|
57
|
+
expect(store.get(documentVersionAtom)).toBeNull();
|
|
58
|
+
connect(store, null);
|
|
59
|
+
expect(store.get(needsSaveAtom)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("compares the host's etag with what it last wrote", () => {
|
|
63
|
+
const store = createStore();
|
|
64
|
+
connect(store, { revision: revision(), cells: new Map() });
|
|
65
|
+
expect(store.get(needsSaveAtom)).toBe(false);
|
|
66
|
+
expect(store.get(documentVersionAtom)).toBe(2);
|
|
67
|
+
connect(store, { revision: revision({ etag: "v3" }), cells: new Map() });
|
|
68
|
+
expect(store.get(needsSaveAtom)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("treats a document never written as unsaved", () => {
|
|
72
|
+
const store = createStore();
|
|
73
|
+
connect(store, {
|
|
74
|
+
revision: revision({ saved_etag: null }),
|
|
75
|
+
cells: new Map(),
|
|
76
|
+
});
|
|
77
|
+
expect(store.get(needsSaveAtom)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it.each(["pending", "sending", "confirming", "error"] as const)(
|
|
81
|
+
"counts edits the host has not confirmed (%s)",
|
|
82
|
+
(status) => {
|
|
83
|
+
const store = createStore();
|
|
84
|
+
connect(store, { revision: revision(), cells: new Map() }, status);
|
|
85
|
+
expect(store.get(needsSaveAtom)).toBe(true);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
it("ignores a model that is not running", () => {
|
|
90
|
+
const store = createStore();
|
|
91
|
+
connect(store, { revision: revision(), cells: new Map() }, "unavailable");
|
|
92
|
+
expect(store.get(needsSaveAtom)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("compares the layout with the one last saved", () => {
|
|
96
|
+
const store = createStore();
|
|
97
|
+
connect(store, { revision: revision(), cells: new Map() });
|
|
98
|
+
store.set(layoutStateAtom, {
|
|
99
|
+
selectedLayout: "grid",
|
|
100
|
+
layoutData: { grid: null },
|
|
101
|
+
});
|
|
102
|
+
expect(store.get(needsSaveAtom)).toBe(false);
|
|
103
|
+
store.set(savedLayoutAtom, initialLayoutState());
|
|
104
|
+
expect(store.get(needsSaveAtom)).toBe(true);
|
|
105
|
+
store.set(savedLayoutAtom, {
|
|
106
|
+
selectedLayout: "grid",
|
|
107
|
+
layoutData: { grid: null },
|
|
108
|
+
});
|
|
109
|
+
expect(store.get(needsSaveAtom)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtom, useAtomValue,
|
|
3
|
+
import { useAtom, useAtomValue, useStore } from "jotai";
|
|
4
4
|
import { SaveIcon } from "lucide-react";
|
|
5
5
|
import { useState } from "react";
|
|
6
6
|
import { FilenameInput } from "@/components/editor/header/filename-input";
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { Label } from "../../components/ui/label";
|
|
21
21
|
import { useEvent } from "../../hooks/useEvent";
|
|
22
22
|
import { Logger } from "../../utils/Logger";
|
|
23
|
-
import { getCellConfigs, getNotebook
|
|
23
|
+
import { getCellConfigs, getNotebook } from "../cells/cells";
|
|
24
24
|
import { flushDocument, withDocumentSave } from "../cells/document";
|
|
25
25
|
import { notebookCells } from "../cells/utils";
|
|
26
26
|
import { formatAll } from "../codemirror/format";
|
|
@@ -28,20 +28,16 @@ import { autoSaveConfigAtom } from "../config/config";
|
|
|
28
28
|
import { useAutoExport } from "../export/hooks";
|
|
29
29
|
import { getSerializedLayout } from "../layout/layout";
|
|
30
30
|
import { layoutStateAtom } from "../layout/state";
|
|
31
|
-
import { kioskModeAtom } from "../mode";
|
|
32
31
|
import { connectionAtom } from "../network/connection";
|
|
32
|
+
import { canEditAtom } from "@/core/notebook";
|
|
33
33
|
import { useRequestClient } from "../network/requests";
|
|
34
34
|
import { ConnectionState } from "../session/types";
|
|
35
35
|
import { isWasm } from "../wasm/utils";
|
|
36
36
|
import { filenameAtom } from "./file-state";
|
|
37
37
|
import { useFilename, useUpdateFilename } from "./filename";
|
|
38
|
-
import {
|
|
38
|
+
import { documentVersionAtom, needsSaveAtom, savedLayoutAtom } from "./state";
|
|
39
39
|
import { useAutoSave } from "./useAutoSave";
|
|
40
40
|
|
|
41
|
-
interface SaveNotebookProps {
|
|
42
|
-
kioskMode: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
let notebookSaveQueue: Promise<void> = Promise.resolve();
|
|
46
42
|
|
|
47
43
|
export function enqueueNotebookSave<T>(save: () => Promise<T>): Promise<T> {
|
|
@@ -53,12 +49,12 @@ export function enqueueNotebookSave<T>(save: () => Promise<T>): Promise<T> {
|
|
|
53
49
|
return result;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
export const SaveComponent = (
|
|
52
|
+
export const SaveComponent = () => {
|
|
57
53
|
const filename = useFilename();
|
|
58
54
|
const needsSave = useAtomValue(needsSaveAtom);
|
|
59
55
|
const closed = useAtomValue(connectionAtom).state === ConnectionState.CLOSED;
|
|
60
56
|
const { saveOrNameNotebook, saveIfNotebookIsPersistent } = useSaveNotebook();
|
|
61
|
-
useAutoSaveNotebook({ onSave: saveIfNotebookIsPersistent
|
|
57
|
+
useAutoSaveNotebook({ onSave: saveIfNotebookIsPersistent });
|
|
62
58
|
|
|
63
59
|
useAutoExport();
|
|
64
60
|
|
|
@@ -105,7 +101,6 @@ export const SaveComponent = ({ kioskMode }: SaveNotebookProps) => {
|
|
|
105
101
|
export function useSaveNotebook() {
|
|
106
102
|
const { sendSave } = useRequestClient();
|
|
107
103
|
const { openModal, closeModal, openAlert } = useImperativeModal();
|
|
108
|
-
const setLastSavedNotebook = useSetAtom(lastSavedNotebookAtom);
|
|
109
104
|
const updateFilename = useUpdateFilename();
|
|
110
105
|
const store = useStore();
|
|
111
106
|
|
|
@@ -114,11 +109,6 @@ export function useSaveNotebook() {
|
|
|
114
109
|
enqueueNotebookSave(async () => {
|
|
115
110
|
const connection = store.get(connectionAtom);
|
|
116
111
|
const autoSaveConfig = store.get(autoSaveConfigAtom);
|
|
117
|
-
const kioskMode = store.get(kioskModeAtom);
|
|
118
|
-
|
|
119
|
-
if (kioskMode) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
112
|
|
|
123
113
|
// Don't save if we are not connected to a kernel
|
|
124
114
|
if (connection.state !== ConnectionState.OPEN) {
|
|
@@ -126,6 +116,11 @@ export function useSaveNotebook() {
|
|
|
126
116
|
return;
|
|
127
117
|
}
|
|
128
118
|
|
|
119
|
+
// The host may not let this consumer edit, or may not have said yet.
|
|
120
|
+
if (!store.get(canEditAtom)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
129
124
|
Logger.log("saving to ", filename);
|
|
130
125
|
|
|
131
126
|
if (userInitiated && autoSaveConfig.format_on_save) {
|
|
@@ -157,14 +152,14 @@ export function useSaveNotebook() {
|
|
|
157
152
|
layout: getSerializedLayout(),
|
|
158
153
|
persist: true,
|
|
159
154
|
});
|
|
160
|
-
return
|
|
155
|
+
return layout;
|
|
161
156
|
};
|
|
162
157
|
// TODO(MO-7955): Remove this conditional once all frontend saves use
|
|
163
158
|
// the host protocol. Host saves cannot recover edits from an editor snapshot.
|
|
164
|
-
const
|
|
159
|
+
const savedLayout = isWasm()
|
|
165
160
|
? await flushDocument().then(save)
|
|
166
161
|
: await withDocumentSave(save);
|
|
167
|
-
|
|
162
|
+
store.set(savedLayoutAtom, savedLayout);
|
|
168
163
|
}),
|
|
169
164
|
);
|
|
170
165
|
|
|
@@ -224,29 +219,20 @@ export function isNamedPersistentFile(
|
|
|
224
219
|
);
|
|
225
220
|
}
|
|
226
221
|
|
|
227
|
-
export function useAutoSaveNotebook(opts: {
|
|
228
|
-
onSave: () => void;
|
|
229
|
-
kioskMode: boolean;
|
|
230
|
-
}) {
|
|
222
|
+
export function useAutoSaveNotebook(opts: { onSave: () => void }) {
|
|
231
223
|
const autoSaveConfig = useAtomValue(autoSaveConfigAtom);
|
|
232
|
-
const
|
|
224
|
+
const canEdit = useAtomValue(canEditAtom);
|
|
233
225
|
const [connection] = useAtom(connectionAtom);
|
|
234
226
|
const needsSave = useAtomValue(needsSaveAtom);
|
|
235
|
-
|
|
236
|
-
const cells = notebookCells(notebook);
|
|
237
|
-
const codes = cells.map((cell) => cell.code);
|
|
238
|
-
const cellNames = cells.map((cell) => cell.name);
|
|
239
|
-
const configs = getCellConfigs(notebook);
|
|
227
|
+
const version = useAtomValue(documentVersionAtom);
|
|
240
228
|
|
|
241
229
|
useAutoSave({
|
|
242
230
|
onSave: opts.onSave,
|
|
243
231
|
needsSave: needsSave,
|
|
244
|
-
|
|
245
|
-
cellConfigs: configs,
|
|
246
|
-
cellNames: cellNames,
|
|
232
|
+
version,
|
|
247
233
|
connStatus: connection,
|
|
248
234
|
config: autoSaveConfig,
|
|
249
|
-
|
|
235
|
+
canEdit,
|
|
250
236
|
});
|
|
251
237
|
}
|
|
252
238
|
|