@marimo-team/frontend 0.23.16-dev3 → 0.23.16-dev31
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/assets/{CellStatus-BalYv0pK.js → CellStatus-CmojVYzm.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DI7mV-Gw.js → ConnectedDataExplorerComponent-CMaZapRd.js} +1 -1
- package/dist/assets/{ErrorBoundary-C7_5jjWi.js → ErrorBoundary-DzTD8LVq.js} +1 -1
- package/dist/assets/{ImperativeModal-BIavOiUv.js → ImperativeModal-fY-ZfT0b.js} +1 -1
- package/dist/assets/{JsonOutput-CkCD-FgM.js → JsonOutput-BwwRpByA.js} +12 -12
- package/dist/assets/{LazyAnyLanguageCodeMirror-sVm_hz8A.js → LazyAnyLanguageCodeMirror-B2JvSXP-.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DO2k2fA5.js → MarimoErrorOutput-BWte8vjk.js} +4 -4
- package/dist/assets/{RSPContexts-CgC81f0I.js → RSPContexts-WiCgGm__.js} +1 -1
- package/dist/assets/RenderHTML-DRltV71H.js +1 -0
- package/dist/assets/{RunButton-BKRupqr7.js → RunButton-DIegd2mN.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CEZtaZCK.js → add-cell-with-ai-BBFD-M9V.js} +17 -17
- package/dist/assets/add-connection-dialog-CECJ8507.js +170 -0
- package/dist/assets/{agent-panel-CHB6t7Zy.js → agent-panel-YaS0WXP1.js} +5 -5
- package/dist/assets/ai-model-dropdown-BpVBUZnz.js +5 -0
- package/dist/assets/{alert-dialog-BXegNC0I.js → alert-dialog-Bb8Lz8ko.js} +1 -1
- package/dist/assets/{any-language-editor-DCpZRrpx.js → any-language-editor--tRHs8C_.js} +1 -1
- package/dist/assets/{app-config-button-NrM9ywXA.js → app-config-button-BW-w01aF.js} +1 -1
- package/dist/assets/{button-eIihcbUw.js → button-DDGiD1NW.js} +1 -1
- package/dist/assets/{cache-panel-wllSTlbM.js → cache-panel-DcQUeUpg.js} +1 -1
- package/dist/assets/{capabilities-B09j1Eor.js → capabilities-DoEg0XwL.js} +1 -1
- package/dist/assets/{cell-editor-B5JVq7_q.js → cell-editor-EV-1IccQ.js} +10 -10
- package/dist/assets/cell-link-C5gpAn3R.js +1 -0
- package/dist/assets/cells-6yd8nOAu.js +231 -0
- package/dist/assets/{chat-display-BG08ltQ3.js → chat-display-8BbC5B-D.js} +1 -1
- package/dist/assets/{chat-panel-P2CzCtXJ.js → chat-panel-00V3uFDx.js} +3 -3
- package/dist/assets/{chat-ui-fAJZAoTD.js → chat-ui-DWhJ2Ozy.js} +1 -1
- package/dist/assets/{column-preview-Dne1dIt0.js → column-preview-DeXESYpM.js} +1 -1
- package/dist/assets/{command-CAJLPEBq.js → command-Dqxie4gq.js} +1 -1
- package/dist/assets/{command-palette-iOCqUIs1.js → command-palette-DZ2rNzzf.js} +1 -1
- package/dist/assets/{common-CD6Mj8n6.js → common-XVELED_O.js} +1 -1
- package/dist/assets/components-BLHpmhXx.js +1 -0
- package/dist/assets/config-B-Q7J_6d.js +1 -0
- package/dist/assets/{config-COUOIPEq.js → config-BJVx3eBW.js} +1 -1
- package/dist/assets/{copy-BtTOqPTS.js → copy-Bxb1_Gtw.js} +1 -1
- package/dist/assets/{copy-icon-C2pG0ZNK.js → copy-icon-neDQUo4Q.js} +1 -1
- package/dist/assets/{createReducer-C7kZ8zEe.js → createReducer-DGx5Af-C.js} +1 -1
- package/dist/assets/{datasource-ySAiC9gr.js → datasource-CTqa8bf-.js} +2 -2
- package/dist/assets/{dates-D-kIKDYA.js → dates-C2LWzsCZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BF23EwcD.js → dependency-graph-panel-C4Nzcddp.js} +4 -4
- package/dist/assets/{dialog-D1DEsRZ2.js → dialog-BCVepWLJ.js} +1 -1
- package/dist/assets/{dist-ByVeMWkr.js → dist-BLN8PNJV.js} +1 -1
- package/dist/assets/dist-CiVShZKM.js +2 -0
- package/dist/assets/{documentation-panel-e9Fi9R7e.js → documentation-panel-Bym4G7IS.js} +1 -1
- package/dist/assets/download-HKgqJX4l.js +9 -0
- package/dist/assets/{dropdown-menu-BFZLnWeO.js → dropdown-menu-dHmZR8bI.js} +1 -1
- package/dist/assets/{edit-page-DSEkwJ4O.js → edit-page-D80ascX2.js} +6 -6
- package/dist/assets/{error-banner-B6eIlxzg.js → error-banner-BSxEOIn2.js} +1 -1
- package/dist/assets/{error-entries-Birw-8VX.js → error-entries-CxyvaxEM.js} +2 -2
- package/dist/assets/{error-panel-DdsUL6b7.js → error-panel-CfSy7uUr.js} +1 -1
- package/dist/assets/{es-1D83cgiu.js → es-gYgfF8y5.js} +1 -1
- package/dist/assets/{esm-BL7jQmzn.js → esm-B30N1GS0.js} +1 -1
- package/dist/assets/{field-BeZbVzXA.js → field-BsxD35VE.js} +1 -1
- package/dist/assets/file-explorer-panel-Dd5Xn2TK.js +26 -0
- package/dist/assets/{file-icons-Cb54mIL8.js → file-icons-CSjJ3JC6.js} +1 -1
- package/dist/assets/{file-name-input-BWFcK1ey.js → file-name-input-CrKtrXMl.js} +1 -1
- package/dist/assets/floating-outline-ByQ8vvif.js +1 -0
- package/dist/assets/focus-CVONl-Dy.js +1 -0
- package/dist/assets/{form-B6BkSCJb.js → form-Ca3yd_Od.js} +1 -1
- package/dist/assets/{formats-ClEFGPy-.js → formats-DzmLpc9A.js} +1 -1
- package/dist/assets/{formatting-B_41gSF5.js → formatting-CDf6w1Kt.js} +1 -1
- package/dist/assets/{fullscreen-dUjPYOmm.js → fullscreen-CTDqNQr6.js} +1 -1
- package/dist/assets/{gallery-page-CWQQAYpa.js → gallery-page-CJ3ms1Hn.js} +1 -1
- package/dist/assets/{glide-data-editor-lMARFWHS.js → glide-data-editor-j2igs9Y_.js} +1 -1
- package/dist/assets/home-page-FpAawj9s.js +4 -0
- package/dist/assets/{html-to-image-CwmX28T9.js → html-to-image-C-4ysbGe.js} +2 -2
- package/dist/assets/index-4k5eCUzO.js +38 -0
- package/dist/assets/index-xs-zSSrX.css +2 -0
- package/dist/assets/{input-CqrUjynb.js → input-JxFwu-3G.js} +1 -1
- package/dist/assets/kiosk-mode-BumOFEtY.js +1 -0
- package/dist/assets/{label-BElwWzf0.js → label-CqjrpwRC.js} +1 -1
- package/dist/assets/{layout-CUlZijwG.js → layout-Dtql_o_T.js} +5 -5
- package/dist/assets/{links-CToBjIIj.js → links-CZr5bjvo.js} +1 -1
- package/dist/assets/{logs-panel-iFV4ZJ9b.js → logs-panel-5u1wUBn1.js} +1 -1
- package/dist/assets/{markdown-renderer-QvXG0CRk.js → markdown-renderer-dABDqZDw.js} +2 -2
- package/dist/assets/{menu-items-DbSLBfXR.js → menu-items-CCi6aOPm.js} +1 -1
- package/dist/assets/{mermaid-Bh0flEIb.js → mermaid-CYN0wOyG.js} +1 -1
- package/dist/assets/name-cell-input-Y4knmEIU.js +1 -0
- package/dist/assets/node-sql-parser-Si4oZ2P-.js +68 -0
- package/dist/assets/{numbers-CeeaFKqu.js → numbers-rmawRqQP.js} +1 -1
- package/dist/assets/outline-panel-CQ_KWLip.js +1 -0
- package/dist/assets/{packages-panel-BzQF60rG.js → packages-panel-Ijt7d51P.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-CKOYwOYw.js → pair-with-agent-modal-BUtnUuiE.js} +1 -1
- package/dist/assets/panels-DfU1Oikj.js +4 -0
- package/dist/assets/{popover-DrHQ3KEt.js → popover-Bcz1Zikb.js} +1 -1
- package/dist/assets/process-output-Dd11YI04.js +1 -0
- package/dist/assets/{radio-group-CLBjGkt5.js → radio-group-22eWJ9fv.js} +1 -1
- package/dist/assets/readonly-code-display-BjK-TEri.js +1 -0
- package/dist/assets/{readonly-python-code-CW_G5e4E.js → readonly-python-code-n_TrLmsR.js} +1 -1
- package/dist/assets/{renderShortcut-BZLr8D7H.js → renderShortcut-Bl8_FVn8.js} +1 -1
- package/dist/assets/{reveal-component-BLMTmd8T.js → reveal-component-CCLTV1q6.js} +60 -60
- package/dist/assets/{run-page-B6pgDYEJ.js → run-page-DbezK7Qs.js} +1 -1
- package/dist/assets/{runs-D51Xwrsk.js → runs-DBTXqJHS.js} +1 -1
- package/dist/assets/scratchpad-panel-C2wAEv5n.js +1 -0
- package/dist/assets/{scroll-to-bottom-button-Cgr10kaJ.js → scroll-to-bottom-button-C8yfT4wq.js} +1 -1
- package/dist/assets/{secrets-panel-Cc5KDBk6.js → secrets-panel-6J6wcuVR.js} +1 -1
- package/dist/assets/{select-B5TFNjmU.js → select-BP75_5jJ.js} +1 -1
- package/dist/assets/{session-CEFnN9Cq.js → session-BmukRjSi.js} +1 -1
- package/dist/assets/{session-panel-Y0Oon0sD.js → session-panel-BZGNcZmp.js} +1 -1
- package/dist/assets/{share-CYpcX4N4.js → share-852X_cqy.js} +1 -1
- package/dist/assets/{skeleton-D3QPxqWC.js → skeleton-BzlKcq21.js} +1 -1
- package/dist/assets/slides-DkEgzTJB.css +1 -0
- package/dist/assets/{snippets-panel-CXpGflMO.js → snippets-panel-DNaNtkR0.js} +1 -1
- package/dist/assets/{spec-CzqcmYQk.js → spec-C7opZZRp.js} +1 -1
- package/dist/assets/{state-BvdK8eiw.js → state-D10U2MwA.js} +1 -1
- package/dist/assets/{state-WBgc7MiA.js → state-DDag0B_B.js} +1 -1
- package/dist/assets/state-DFPAfLw7.js +3 -0
- package/dist/assets/{state-DBzEQtWz.js → state-jCdh8Dcx.js} +1 -1
- package/dist/assets/{strings-Djnbwmfl.js → strings--BknHX9C.js} +1 -1
- package/dist/assets/{swiper-component-DInvCxhI.js → swiper-component-g8EXE911.js} +1 -1
- package/dist/assets/switch-OVajJBPR.js +1 -0
- package/dist/assets/{terminal-D3IQBBYo.js → terminal-217O8hlZ.js} +1 -1
- package/dist/assets/{textarea-CpFfEUet.js → textarea-DuEtSOCl.js} +1 -1
- package/dist/assets/{tooltip-CFX4fYHU.js → tooltip-4bZlU7jl.js} +1 -1
- package/dist/assets/{tooltip-HLb6RxVp.js → tooltip-B7U4xkIN.js} +1 -1
- package/dist/assets/{tracing-panel-BU-o_LCd.js → tracing-panel-rVAa3En0.js} +2 -2
- package/dist/assets/{tracing-CKn3gWHw.js → tracing-ttUd3J7U.js} +1 -1
- package/dist/assets/{tree-actions-CsC2XmGo.js → tree-actions-UCr1tMHw.js} +1 -1
- package/dist/assets/{useBoolean-D89u30n-.js → useBoolean-CFj86iac.js} +1 -1
- package/dist/assets/useCellActionButton-CJknR0XY.js +1 -0
- package/dist/assets/{useDeleteCell-EPH4kcG7.js → useDeleteCell-PrgejW8A.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CZnK7N2L.js → useDependencyPanelTab-5jc-_-hy.js} +1 -1
- package/dist/assets/useEventListener-C0195JsF.js +1 -0
- package/dist/assets/{useHotkey-DIuRH8Wu.js → useHotkey-DLZvrRZT.js} +1 -1
- package/dist/assets/{useIframeCapabilities-Ccb4fvKc.js → useIframeCapabilities-j6fR_dBA.js} +1 -1
- package/dist/assets/{useInstallPackage-BRbUAUFn.js → useInstallPackage-WiYBwhUO.js} +1 -1
- package/dist/assets/{useInterval-CC0vKpWm.js → useInterval-CWBTUy5W.js} +1 -1
- package/dist/assets/useNotebookActions-DG8f4m8p.js +12 -0
- package/dist/assets/useRunCells-DHsd7HGL.js +1 -0
- package/dist/assets/{useSplitCell-9Qz93yPZ.js → useSplitCell-MZpER0Bh.js} +1 -1
- package/dist/assets/{useTheme-KXqAyDPC.js → useTheme-Bxv9B31g.js} +1 -1
- package/dist/assets/{utils-Z80SV4_d.js → utils-BWBBpCkT.js} +1 -1
- package/dist/assets/{vega-component-Cie3ag_T.js → vega-component-Db8KjZJG.js} +1 -1
- package/dist/assets/{write-secret-modal-DEEn7sZV.js → write-secret-modal-3-v-dHVj.js} +1 -1
- package/dist/index.html +68 -68
- package/package.json +2 -2
- package/src/__mocks__/requests.ts +45 -4
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/types.ts +1 -1
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +0 -4
- package/src/components/editor/actions/pdf-export.ts +1 -4
- package/src/components/editor/actions/useNotebookActions.tsx +7 -13
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
- package/src/components/editor/connections/storage/as-code.ts +40 -21
- package/src/components/editor/connections/storage/schemas.ts +83 -42
- package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
- package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
- package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
- package/src/components/editor/file-tree/file-render-mode.ts +32 -0
- package/src/components/editor/file-tree/file-viewer.tsx +76 -50
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +25 -19
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +3 -0
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bridge.ts +16 -0
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
- package/src/core/network/__tests__/requests-network.test.ts +21 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +14 -8
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +25 -7
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/bridge.ts +4 -2
- package/src/core/wasm/worker/types.ts +5 -2
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/utils/__tests__/download.test.tsx +24 -1
- package/src/utils/download.ts +11 -6
- package/dist/assets/RenderHTML-BCvU9dYz.js +0 -1
- package/dist/assets/add-connection-dialog-B-jCRNTN.js +0 -170
- package/dist/assets/ai-model-dropdown-DXgLphFa.js +0 -5
- package/dist/assets/cell-link-DqBkyAFe.js +0 -1
- package/dist/assets/cells-Cf7bHHq3.js +0 -229
- package/dist/assets/components-BH6gDVv6.js +0 -1
- package/dist/assets/config-BbOTTCYY.js +0 -1
- package/dist/assets/dist-BCMBu0rO.js +0 -2
- package/dist/assets/download-BomYt3nl.js +0 -9
- package/dist/assets/file-explorer-panel-BfXM2BOd.js +0 -26
- package/dist/assets/floating-outline-wz-WSRgP.js +0 -1
- package/dist/assets/focus-DiEyfiAG.js +0 -1
- package/dist/assets/home-page-DJ4jJsZj.js +0 -4
- package/dist/assets/index-A71LAt6h.css +0 -2
- package/dist/assets/index-Dbc5mg7a.js +0 -38
- package/dist/assets/kiosk-mode-ABm3oYgq.js +0 -1
- package/dist/assets/name-cell-input-C6cukQ9k.js +0 -1
- package/dist/assets/node-sql-parser-BWgUjcfV.js +0 -68
- package/dist/assets/outline-panel-BPlg9_CE.js +0 -1
- package/dist/assets/panels-Dj45fW-v.js +0 -4
- package/dist/assets/process-output-BEhIGfkO.js +0 -1
- package/dist/assets/readonly-code-display-D63oRtBP.js +0 -1
- package/dist/assets/scratchpad-panel-B0mEqBNk.js +0 -1
- package/dist/assets/slides-MCbMDtlY.css +0 -1
- package/dist/assets/state-BZQc7xxL.js +0 -3
- package/dist/assets/switch-CvweaCXq.js +0 -1
- package/dist/assets/useCellActionButton-CRfIEVB5.js +0 -1
- package/dist/assets/useEventListener-B_w3A283.js +0 -1
- package/dist/assets/useNotebookActions-DTXAoukG.js +0 -12
- package/dist/assets/useRunCells-CSSF_mJ0.js +0 -1
|
@@ -22,6 +22,7 @@ vi.mock("../api", async () => {
|
|
|
22
22
|
return {
|
|
23
23
|
...actual,
|
|
24
24
|
API: {
|
|
25
|
+
handleExportResponse: vi.fn((response) => response.data),
|
|
25
26
|
handleResponse: vi.fn((response) => response.data),
|
|
26
27
|
handleResponseReturnNull: vi.fn(() => null),
|
|
27
28
|
},
|
|
@@ -32,6 +33,7 @@ vi.mock("../api", async () => {
|
|
|
32
33
|
vi.mock("../connection", () => ({
|
|
33
34
|
isConnectedAtom: { read: vi.fn(() => true) },
|
|
34
35
|
waitForConnectionOpen: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
waitForConnectionOpenIfNotebook: vi.fn().mockResolvedValue(undefined),
|
|
35
37
|
}));
|
|
36
38
|
|
|
37
39
|
describe("createNetworkRequests", () => {
|
|
@@ -121,6 +123,25 @@ describe("createNetworkRequests", () => {
|
|
|
121
123
|
expect(mockClient.GET).toHaveBeenCalledWith("/api/environment");
|
|
122
124
|
});
|
|
123
125
|
|
|
126
|
+
it("getExportAvailability should GET /api/export/availability", async () => {
|
|
127
|
+
const requests = createNetworkRequests();
|
|
128
|
+
await requests.getExportAvailability();
|
|
129
|
+
|
|
130
|
+
expect(mockClient.GET).toHaveBeenCalledWith("/api/export/availability");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("getPackageList should not require a kernel connection", async () => {
|
|
134
|
+
const { waitForConnectionOpen, waitForConnectionOpenIfNotebook } =
|
|
135
|
+
await import("../connection");
|
|
136
|
+
|
|
137
|
+
const requests = createNetworkRequests();
|
|
138
|
+
await requests.getPackageList();
|
|
139
|
+
|
|
140
|
+
expect(mockClient.GET).toHaveBeenCalledWith("/api/packages/list");
|
|
141
|
+
expect(waitForConnectionOpenIfNotebook).toHaveBeenCalled();
|
|
142
|
+
expect(waitForConnectionOpen).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
124
145
|
it("exportAsIPYNB should call the new endpoint as text", async () => {
|
|
125
146
|
const requests = createNetworkRequests();
|
|
126
147
|
await requests.exportAsIPYNB({
|
package/src/core/network/api.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Logger } from "../../utils/Logger";
|
|
|
6
6
|
import { Strings } from "../../utils/strings";
|
|
7
7
|
import { getRuntimeManager } from "../runtime/config";
|
|
8
8
|
import type { RuntimeManager } from "../runtime/runtime";
|
|
9
|
+
import type { ExportedFile } from "./types";
|
|
9
10
|
|
|
10
11
|
function getBaseUriWithoutQueryParams(): string {
|
|
11
12
|
// Remove query params and hash
|
|
@@ -15,6 +16,15 @@ function getBaseUriWithoutQueryParams(): string {
|
|
|
15
16
|
return url.toString();
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function getExportFilename(response: Response): string {
|
|
20
|
+
const contentDisposition = response.headers.get("Content-Disposition");
|
|
21
|
+
const match = contentDisposition?.match(/filename\*=UTF-8''([^;]+)/i);
|
|
22
|
+
if (!match) {
|
|
23
|
+
throw new Error("Export response is missing a filename");
|
|
24
|
+
}
|
|
25
|
+
return decodeURIComponent(match[1]);
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
/**
|
|
19
29
|
* Wrapper around fetch that adds XSRF token and session ID to the request and
|
|
20
30
|
* strong types.
|
|
@@ -119,6 +129,23 @@ export const API = {
|
|
|
119
129
|
}
|
|
120
130
|
return Promise.resolve(response.data as T);
|
|
121
131
|
},
|
|
132
|
+
handleExportResponse: async <T extends BlobPart>(response: {
|
|
133
|
+
data?: T | undefined;
|
|
134
|
+
error?: Record<string, unknown>;
|
|
135
|
+
response: Response;
|
|
136
|
+
}): Promise<ExportedFile<T>> => {
|
|
137
|
+
const contents = await API.handleResponse<T>(response);
|
|
138
|
+
const mediaType = response.response.headers.get("Content-Type");
|
|
139
|
+
if (!mediaType) {
|
|
140
|
+
throw new Error("Export response is missing a media type");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
contents,
|
|
145
|
+
filename: getExportFilename(response.response),
|
|
146
|
+
mediaType,
|
|
147
|
+
};
|
|
148
|
+
},
|
|
122
149
|
handleResponseReturnNull: (response: {
|
|
123
150
|
error?: Record<string, unknown>;
|
|
124
151
|
response: Response;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { atom } from "jotai";
|
|
3
|
+
import { isNotebookPage } from "../mode";
|
|
3
4
|
import { waitFor } from "../state/jotai";
|
|
4
5
|
import { type ConnectionStatus, WebSocketState } from "../websocket/types";
|
|
5
6
|
|
|
@@ -17,6 +18,20 @@ export function waitForConnectionOpen() {
|
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Waits for the kernel connection, but only on pages that open one. Non-notebook
|
|
23
|
+
* pages (home, gallery) never connect, so waiting there would hang forever.
|
|
24
|
+
*
|
|
25
|
+
* Use for requests that the marimo server can serve without a session, but that
|
|
26
|
+
* should still wait for the kernel when a notebook is open.
|
|
27
|
+
*/
|
|
28
|
+
export function waitForConnectionOpenIfNotebook() {
|
|
29
|
+
if (!isNotebookPage()) {
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
return waitForConnectionOpen();
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
export const isConnectingAtom = atom((get) => {
|
|
21
36
|
const connection = get(connectionAtom);
|
|
22
37
|
return connection.state === WebSocketState.CONNECTING;
|
|
@@ -103,6 +103,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
103
103
|
|
|
104
104
|
// Served by the marimo server without a kernel session
|
|
105
105
|
getEnvironmentInfo: "serverOnly",
|
|
106
|
+
getExportAvailability: "serverOnly",
|
|
106
107
|
|
|
107
108
|
// Home operations throw errors
|
|
108
109
|
getRecentFiles: "startConnection",
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import { once } from "@/utils/once";
|
|
4
4
|
import { getRuntimeManager } from "../runtime/config";
|
|
5
5
|
import { API, createClientWithRuntimeManager } from "./api";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
waitForConnectionOpen,
|
|
8
|
+
waitForConnectionOpenIfNotebook,
|
|
9
|
+
} from "./connection";
|
|
7
10
|
import type { EditRequests, RunRequests } from "./types";
|
|
8
11
|
|
|
9
12
|
/**
|
|
@@ -19,7 +22,7 @@ function multipartInit(formData: FormData) {
|
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
const { handleResponse, handleResponseReturnNull } = API;
|
|
25
|
+
const { handleExportResponse, handleResponse, handleResponseReturnNull } = API;
|
|
23
26
|
|
|
24
27
|
export function createNetworkRequests(): EditRequests & RunRequests {
|
|
25
28
|
const getClient = once(() => {
|
|
@@ -399,6 +402,9 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
399
402
|
})
|
|
400
403
|
.then(handleResponse);
|
|
401
404
|
},
|
|
405
|
+
getExportAvailability: () => {
|
|
406
|
+
return getClient().GET("/api/export/availability").then(handleResponse);
|
|
407
|
+
},
|
|
402
408
|
exportAsHTML: async (request) => {
|
|
403
409
|
if (
|
|
404
410
|
process.env.NODE_ENV === "development" ||
|
|
@@ -412,7 +418,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
412
418
|
parseAs: "text",
|
|
413
419
|
params: getParams(),
|
|
414
420
|
})
|
|
415
|
-
.then(
|
|
421
|
+
.then(handleExportResponse);
|
|
416
422
|
},
|
|
417
423
|
exportAsMarkdown: async (request) => {
|
|
418
424
|
return getClient()
|
|
@@ -421,7 +427,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
421
427
|
parseAs: "text",
|
|
422
428
|
params: getParams(),
|
|
423
429
|
})
|
|
424
|
-
.then(
|
|
430
|
+
.then(handleExportResponse);
|
|
425
431
|
},
|
|
426
432
|
exportAsIPYNB: async (request) => {
|
|
427
433
|
return getClient()
|
|
@@ -430,7 +436,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
430
436
|
parseAs: "text",
|
|
431
437
|
params: getParams(),
|
|
432
438
|
})
|
|
433
|
-
.then(
|
|
439
|
+
.then(handleExportResponse);
|
|
434
440
|
},
|
|
435
441
|
exportAsPDF: async (request) => {
|
|
436
442
|
return getClient()
|
|
@@ -439,7 +445,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
439
445
|
parseAs: "blob",
|
|
440
446
|
params: getParams(),
|
|
441
447
|
})
|
|
442
|
-
.then(
|
|
448
|
+
.then(handleExportResponse);
|
|
443
449
|
},
|
|
444
450
|
autoExportAsHTML: async (request) => {
|
|
445
451
|
return getClient()
|
|
@@ -489,12 +495,12 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
489
495
|
},
|
|
490
496
|
getPackageList: async () => {
|
|
491
497
|
// If the sidebar is already open, it may try to load before the session has been initialized
|
|
492
|
-
await
|
|
498
|
+
await waitForConnectionOpenIfNotebook();
|
|
493
499
|
return getClient().GET("/api/packages/list").then(handleResponse);
|
|
494
500
|
},
|
|
495
501
|
getDependencyTree: async () => {
|
|
496
502
|
// If the sidebar is already open, it may try to load before the session has been initialized
|
|
497
|
-
await
|
|
503
|
+
await waitForConnectionOpenIfNotebook();
|
|
498
504
|
return getClient().GET("/api/packages/tree").then(handleResponse);
|
|
499
505
|
},
|
|
500
506
|
listSecretKeys: async (request) => {
|
|
@@ -82,6 +82,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
82
82
|
getWorkspaceFiles: throwNotInEditMode,
|
|
83
83
|
getRunningNotebooks: throwNotInEditMode,
|
|
84
84
|
shutdownSession: throwNotInEditMode,
|
|
85
|
+
getExportAvailability: throwNotInEditMode,
|
|
85
86
|
exportAsHTML: throwNotInEditMode,
|
|
86
87
|
exportAsIPYNB: throwNotInEditMode,
|
|
87
88
|
exportAsMarkdown: throwNotInEditMode,
|
|
@@ -63,6 +63,7 @@ export function createErrorToastingRequests(
|
|
|
63
63
|
getWorkspaceFiles: "Failed to get workspace files",
|
|
64
64
|
getRunningNotebooks: "Failed to get running notebooks",
|
|
65
65
|
shutdownSession: "Failed to shutdown session",
|
|
66
|
+
getExportAvailability: "", // No toast
|
|
66
67
|
exportAsHTML: "Failed to export HTML",
|
|
67
68
|
exportAsIPYNB: "Failed to export ipynb",
|
|
68
69
|
exportAsMarkdown: "Failed to export Markdown",
|
|
@@ -17,12 +17,23 @@ export type CellConfig = schemas["CellConfig"];
|
|
|
17
17
|
export type RuntimeState = schemas["CellNotification"]["status"];
|
|
18
18
|
export type CodeCompletionRequest = schemas["CodeCompletionRequest"];
|
|
19
19
|
export type DeleteCellRequest = schemas["DeleteCellRequest"];
|
|
20
|
+
export type AutoExportAsIPYNBRequest = schemas["AutoExportAsIPYNBRequest"];
|
|
21
|
+
export type AutoExportAsMarkdownRequest =
|
|
22
|
+
schemas["AutoExportAsMarkdownRequest"];
|
|
20
23
|
export type ExportAsHTMLRequest = schemas["ExportAsHTMLRequest"];
|
|
21
24
|
export type ExportAsMarkdownRequest = schemas["ExportAsMarkdownRequest"];
|
|
22
25
|
export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
|
|
23
26
|
export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
|
|
24
27
|
export type ExportAsPDFRequest = schemas["ExportAsPDFRequest"];
|
|
28
|
+
export type ExportAvailabilityResponse = schemas["ExportAvailabilityResponse"];
|
|
25
29
|
export type UpdateCellOutputsRequest = schemas["UpdateCellOutputsRequest"];
|
|
30
|
+
|
|
31
|
+
export interface ExportedFile<T extends BlobPart = BlobPart> {
|
|
32
|
+
contents: T;
|
|
33
|
+
filename: string;
|
|
34
|
+
mediaType: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export type FileCopyRequest = schemas["FileCopyRequest"];
|
|
27
38
|
export type FileCopyResponse = schemas["FileCopyResponse"];
|
|
28
39
|
export type FileCreateRequest = schemas["FileCreateRequest"];
|
|
@@ -191,7 +202,9 @@ export interface EditRequests {
|
|
|
191
202
|
request: FileMoveRequest,
|
|
192
203
|
) => Promise<FileMoveResponse>;
|
|
193
204
|
sendUpdateFile: (request: FileUpdateRequest) => Promise<FileUpdateResponse>;
|
|
194
|
-
sendFileDetails: (
|
|
205
|
+
sendFileDetails: (
|
|
206
|
+
request: FileDetailsRequest,
|
|
207
|
+
) => Promise<FileDetailsResponse>;
|
|
195
208
|
// Homepage requests
|
|
196
209
|
openTutorial: (request: OpenTutorialRequest) => Promise<MarimoFile>;
|
|
197
210
|
getRecentFiles: () => Promise<RecentFilesResponse>;
|
|
@@ -203,13 +216,18 @@ export interface EditRequests {
|
|
|
203
216
|
request: ShutdownSessionRequest,
|
|
204
217
|
) => Promise<RunningNotebooksResponse>;
|
|
205
218
|
// Export requests
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
getExportAvailability: () => Promise<ExportAvailabilityResponse>;
|
|
220
|
+
exportAsHTML: (request: ExportAsHTMLRequest) => Promise<ExportedFile<string>>;
|
|
221
|
+
exportAsIPYNB: (
|
|
222
|
+
request: ExportAsIPYNBRequest,
|
|
223
|
+
) => Promise<ExportedFile<string>>;
|
|
224
|
+
exportAsMarkdown: (
|
|
225
|
+
request: ExportAsMarkdownRequest,
|
|
226
|
+
) => Promise<ExportedFile<string>>;
|
|
227
|
+
exportAsPDF: (request: ExportAsPDFRequest) => Promise<ExportedFile<Blob>>;
|
|
210
228
|
autoExportAsHTML: (request: ExportAsHTMLRequest) => Promise<null>;
|
|
211
|
-
autoExportAsMarkdown: (request:
|
|
212
|
-
autoExportAsIPYNB: (request:
|
|
229
|
+
autoExportAsMarkdown: (request: AutoExportAsMarkdownRequest) => Promise<null>;
|
|
230
|
+
autoExportAsIPYNB: (request: AutoExportAsIPYNBRequest) => Promise<null>;
|
|
213
231
|
updateCellOutputs: (request: UpdateCellOutputsRequest) => Promise<null>;
|
|
214
232
|
// Package requests
|
|
215
233
|
getPackageList: () => Promise<ListPackagesResponse>;
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { Filenames } from "@/utils/filenames";
|
|
5
|
-
import { Paths } from "@/utils/paths";
|
|
3
|
+
import { downloadExportedFile } from "@/utils/download";
|
|
6
4
|
import { getRequestClient } from "../network/requests";
|
|
7
5
|
import { VirtualFileTracker } from "./virtual-file-tracker";
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
8
|
* Downloads the current notebook as an HTML file.
|
|
11
9
|
*/
|
|
12
|
-
export async function downloadAsHTML(opts: {
|
|
13
|
-
filename: string;
|
|
14
|
-
includeCode: boolean;
|
|
15
|
-
}) {
|
|
10
|
+
export async function downloadAsHTML(opts: { includeCode: boolean }) {
|
|
16
11
|
const client = getRequestClient();
|
|
17
|
-
const {
|
|
18
|
-
const
|
|
12
|
+
const { includeCode } = opts;
|
|
13
|
+
const exportedFile = await client.exportAsHTML({
|
|
19
14
|
download: true,
|
|
20
15
|
includeCode: includeCode,
|
|
21
16
|
files: VirtualFileTracker.INSTANCE.filenames(),
|
|
22
17
|
});
|
|
23
|
-
const filenameWithoutPath = Paths.basename(filename) ?? "notebook.py";
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
new Blob([html], { type: "text/html" }),
|
|
27
|
-
Filenames.toHTML(filenameWithoutPath),
|
|
28
|
-
);
|
|
19
|
+
downloadExportedFile(exportedFile);
|
|
29
20
|
}
|
|
30
21
|
|
|
31
22
|
function updateAssetUrl(existingUrl: string, assetBaseUrl: string) {
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
EnvironmentInfo,
|
|
20
20
|
ExportAsHTMLRequest,
|
|
21
21
|
ExportAsMarkdownRequest,
|
|
22
|
+
ExportedFile,
|
|
22
23
|
FileCopyResponse,
|
|
23
24
|
FileCreateResponse,
|
|
24
25
|
FileDeleteResponse,
|
|
@@ -520,7 +521,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
520
521
|
functionName: "export_html",
|
|
521
522
|
payload: request,
|
|
522
523
|
});
|
|
523
|
-
return response as string
|
|
524
|
+
return response as ExportedFile<string>;
|
|
524
525
|
};
|
|
525
526
|
|
|
526
527
|
exportAsMarkdown: EditRequests["exportAsMarkdown"] = async (
|
|
@@ -530,7 +531,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
530
531
|
functionName: "export_markdown",
|
|
531
532
|
payload: request,
|
|
532
533
|
});
|
|
533
|
-
return response as string
|
|
534
|
+
return response as ExportedFile<string>;
|
|
534
535
|
};
|
|
535
536
|
|
|
536
537
|
previewDatasetColumn: EditRequests["previewDatasetColumn"] = async (
|
|
@@ -642,6 +643,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
642
643
|
getWorkspaceFiles = throwNotImplemented;
|
|
643
644
|
getRunningNotebooks = throwNotImplemented;
|
|
644
645
|
shutdownSession = throwNotImplemented;
|
|
646
|
+
getExportAvailability = throwNotImplemented;
|
|
645
647
|
exportAsIPYNB = throwNotImplemented;
|
|
646
648
|
exportAsPDF = throwNotImplemented;
|
|
647
649
|
autoExportAsHTML = throwNotImplemented;
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
EnvironmentInfo,
|
|
13
13
|
ExportAsHTMLRequest,
|
|
14
14
|
ExportAsMarkdownRequest,
|
|
15
|
+
ExportedFile,
|
|
15
16
|
FileCopyRequest,
|
|
16
17
|
FileCopyResponse,
|
|
17
18
|
FileCreateRequest,
|
|
@@ -96,8 +97,10 @@ export interface RawBridge {
|
|
|
96
97
|
load_packages(request: string): Promise<string>;
|
|
97
98
|
read_file(request: string): Promise<string>;
|
|
98
99
|
set_interrupt_buffer(request: Uint8Array): Promise<string>;
|
|
99
|
-
export_html(request: ExportAsHTMLRequest): Promise<string
|
|
100
|
-
export_markdown(
|
|
100
|
+
export_html(request: ExportAsHTMLRequest): Promise<ExportedFile<string>>;
|
|
101
|
+
export_markdown(
|
|
102
|
+
request: ExportAsMarkdownRequest,
|
|
103
|
+
): Promise<ExportedFile<string>>;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
export type BridgePayload<T extends keyof RawBridge> = T extends keyof RawBridge
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { FormWrapper } from "../FormPlugin";
|
|
6
|
+
|
|
7
|
+
describe("FormPlugin", () => {
|
|
8
|
+
it("submits the form on Ctrl+Enter and calls setValue", async () => {
|
|
9
|
+
const setValue = vi.fn();
|
|
10
|
+
const validate = vi.fn().mockResolvedValue(null);
|
|
11
|
+
|
|
12
|
+
render(
|
|
13
|
+
<FormWrapper
|
|
14
|
+
currentValue="old-val"
|
|
15
|
+
newValue="new-val"
|
|
16
|
+
setValue={setValue}
|
|
17
|
+
validate={validate}
|
|
18
|
+
shouldValidate={false}
|
|
19
|
+
clearOnSubmit={false}
|
|
20
|
+
submitButtonLabel="Submit"
|
|
21
|
+
bordered={true}
|
|
22
|
+
loading={false}
|
|
23
|
+
submitButtonDisabled={false}
|
|
24
|
+
showClearButton={false}
|
|
25
|
+
clearButtonLabel="Clear"
|
|
26
|
+
label={null}
|
|
27
|
+
>
|
|
28
|
+
<input data-testid="input" />
|
|
29
|
+
</FormWrapper>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const input = screen.getByTestId("input");
|
|
33
|
+
|
|
34
|
+
// Simulate Ctrl+Enter on input
|
|
35
|
+
fireEvent.keyDown(input, { key: "Enter", ctrlKey: true });
|
|
36
|
+
|
|
37
|
+
await waitFor(() => {
|
|
38
|
+
expect(setValue).toHaveBeenCalledWith("new-val");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("submits the form on Ctrl+Enter and runs validation when shouldValidate is true", async () => {
|
|
43
|
+
const setValue = vi.fn();
|
|
44
|
+
const validate = vi.fn().mockResolvedValue("Validation failed message");
|
|
45
|
+
|
|
46
|
+
render(
|
|
47
|
+
<FormWrapper
|
|
48
|
+
currentValue="old-val"
|
|
49
|
+
newValue="new-val"
|
|
50
|
+
setValue={setValue}
|
|
51
|
+
validate={validate}
|
|
52
|
+
shouldValidate={true}
|
|
53
|
+
clearOnSubmit={false}
|
|
54
|
+
submitButtonLabel="Submit"
|
|
55
|
+
bordered={true}
|
|
56
|
+
loading={false}
|
|
57
|
+
submitButtonDisabled={false}
|
|
58
|
+
showClearButton={false}
|
|
59
|
+
clearButtonLabel="Clear"
|
|
60
|
+
label={null}
|
|
61
|
+
>
|
|
62
|
+
<input data-testid="input" />
|
|
63
|
+
</FormWrapper>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const input = screen.getByTestId("input");
|
|
67
|
+
|
|
68
|
+
// Simulate Ctrl+Enter on input
|
|
69
|
+
fireEvent.keyDown(input, { key: "Enter", ctrlKey: true });
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(validate).toHaveBeenCalledWith({ value: "new-val" });
|
|
73
|
+
// should not submit (setValue not called) due to validation failure
|
|
74
|
+
expect(setValue).not.toHaveBeenCalled();
|
|
75
|
+
// error message displayed
|
|
76
|
+
expect(screen.getByText("Validation failed message")).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("clears inputs on submission when clearOnSubmit is true", async () => {
|
|
81
|
+
const setValue = vi.fn();
|
|
82
|
+
const validate = vi.fn().mockResolvedValue(null);
|
|
83
|
+
|
|
84
|
+
let mockUIElement: (HTMLElement & { reset?: () => void }) | null = null;
|
|
85
|
+
const mockReset = vi.fn();
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<FormWrapper
|
|
89
|
+
currentValue="old-val"
|
|
90
|
+
newValue="new-val"
|
|
91
|
+
setValue={setValue}
|
|
92
|
+
validate={validate}
|
|
93
|
+
shouldValidate={false}
|
|
94
|
+
clearOnSubmit={true}
|
|
95
|
+
submitButtonLabel="Submit"
|
|
96
|
+
bordered={true}
|
|
97
|
+
loading={false}
|
|
98
|
+
submitButtonDisabled={false}
|
|
99
|
+
showClearButton={false}
|
|
100
|
+
clearButtonLabel="Clear"
|
|
101
|
+
label={null}
|
|
102
|
+
>
|
|
103
|
+
<div
|
|
104
|
+
ref={(el) => {
|
|
105
|
+
if (el && !mockUIElement) {
|
|
106
|
+
mockUIElement = document.createElement("marimo-ui-element");
|
|
107
|
+
mockUIElement.setAttribute("data-testid", "mock-ui-element");
|
|
108
|
+
mockUIElement.reset = mockReset;
|
|
109
|
+
const inputEl = document.createElement("input");
|
|
110
|
+
inputEl.type = "text";
|
|
111
|
+
mockUIElement.appendChild(inputEl);
|
|
112
|
+
el.appendChild(mockUIElement);
|
|
113
|
+
}
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
</FormWrapper>,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const input = screen.getByRole("textbox");
|
|
120
|
+
|
|
121
|
+
// Simulate Ctrl+Enter on input
|
|
122
|
+
fireEvent.keyDown(input, { key: "Enter", ctrlKey: true });
|
|
123
|
+
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(setValue).toHaveBeenCalledWith("new-val");
|
|
126
|
+
expect(mockReset).toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("does not submit on Shift+Enter", async () => {
|
|
131
|
+
const setValue = vi.fn();
|
|
132
|
+
const validate = vi.fn().mockResolvedValue(null);
|
|
133
|
+
|
|
134
|
+
render(
|
|
135
|
+
<FormWrapper
|
|
136
|
+
currentValue="old-val"
|
|
137
|
+
newValue="new-val"
|
|
138
|
+
setValue={setValue}
|
|
139
|
+
validate={validate}
|
|
140
|
+
shouldValidate={false}
|
|
141
|
+
clearOnSubmit={false}
|
|
142
|
+
submitButtonLabel="Submit"
|
|
143
|
+
bordered={true}
|
|
144
|
+
loading={false}
|
|
145
|
+
submitButtonDisabled={false}
|
|
146
|
+
showClearButton={false}
|
|
147
|
+
clearButtonLabel="Clear"
|
|
148
|
+
label={null}
|
|
149
|
+
>
|
|
150
|
+
<input data-testid="input" />
|
|
151
|
+
</FormWrapper>,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const input = screen.getByTestId("input");
|
|
155
|
+
|
|
156
|
+
// Simulate Shift+Enter on input
|
|
157
|
+
fireEvent.keyDown(input, { key: "Enter", shiftKey: true });
|
|
158
|
+
|
|
159
|
+
expect(setValue).not.toHaveBeenCalled();
|
|
160
|
+
expect(validate).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
downloadAsPDF,
|
|
9
9
|
downloadByURL,
|
|
10
10
|
downloadCellOutputAsImage,
|
|
11
|
+
downloadExportedFile,
|
|
11
12
|
downloadHTMLAsImage,
|
|
12
13
|
getImageDataUrlForCell,
|
|
13
14
|
withLoadingToast,
|
|
@@ -195,7 +196,6 @@ describe("downloadAsPDF", () => {
|
|
|
195
196
|
|
|
196
197
|
await expect(
|
|
197
198
|
downloadAsPDF({
|
|
198
|
-
filename: "path/to/notebook.py",
|
|
199
199
|
webpdf: false,
|
|
200
200
|
preset: "slides",
|
|
201
201
|
}),
|
|
@@ -572,6 +572,7 @@ describe("downloadByURL", () => {
|
|
|
572
572
|
|
|
573
573
|
afterEach(() => {
|
|
574
574
|
vi.restoreAllMocks();
|
|
575
|
+
vi.unstubAllGlobals();
|
|
575
576
|
});
|
|
576
577
|
|
|
577
578
|
it("should create anchor, set attributes, click, and remove", () => {
|
|
@@ -583,4 +584,26 @@ describe("downloadByURL", () => {
|
|
|
583
584
|
expect(mockAnchor.click).toHaveBeenCalled();
|
|
584
585
|
expect(mockAnchor.remove).toHaveBeenCalled();
|
|
585
586
|
});
|
|
587
|
+
|
|
588
|
+
it("downloads an exported file with its returned metadata", async () => {
|
|
589
|
+
let downloadedBlob: Blob | undefined;
|
|
590
|
+
vi.stubGlobal("URL", {
|
|
591
|
+
createObjectURL: vi.fn((blob: Blob) => {
|
|
592
|
+
downloadedBlob = blob;
|
|
593
|
+
return "blob:export";
|
|
594
|
+
}),
|
|
595
|
+
revokeObjectURL: vi.fn(),
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
downloadExportedFile({
|
|
599
|
+
contents: "notebook",
|
|
600
|
+
filename: "résumé.qmd",
|
|
601
|
+
mediaType: "text/plain; charset=utf-8",
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
expect(downloadedBlob?.type).toBe("text/plain; charset=utf-8");
|
|
605
|
+
expect(await downloadedBlob?.text()).toBe("notebook");
|
|
606
|
+
expect(mockAnchor.download).toBe("résumé.qmd");
|
|
607
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:export");
|
|
608
|
+
});
|
|
586
609
|
});
|
package/src/utils/download.ts
CHANGED
|
@@ -4,8 +4,8 @@ import React from "react";
|
|
|
4
4
|
import { toast } from "@/components/ui/use-toast";
|
|
5
5
|
import { type CellId, CellOutputId } from "@/core/cells/ids";
|
|
6
6
|
import { getRequestClient } from "@/core/network/requests";
|
|
7
|
+
import type { ExportedFile } from "@/core/network/types";
|
|
7
8
|
import { Filenames } from "@/utils/filenames";
|
|
8
|
-
import { Paths } from "@/utils/paths";
|
|
9
9
|
import { prettyError } from "./errors";
|
|
10
10
|
import { toPng } from "./html-to-image";
|
|
11
11
|
import { captureExternalIframes } from "./iframe";
|
|
@@ -191,6 +191,14 @@ export function downloadBlob(blob: Blob, filename: string) {
|
|
|
191
191
|
URL.revokeObjectURL(url);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
export function downloadExportedFile({
|
|
195
|
+
contents,
|
|
196
|
+
filename,
|
|
197
|
+
mediaType,
|
|
198
|
+
}: ExportedFile) {
|
|
199
|
+
downloadBlob(new Blob([contents], { type: mediaType }), filename);
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
export type PDFExportPreset = "document" | "slides";
|
|
195
203
|
|
|
196
204
|
/**
|
|
@@ -200,7 +208,6 @@ export type PDFExportPreset = "document" | "slides";
|
|
|
200
208
|
* Standard PDF requires Pandoc & TeX (~few GBs) but is of higher quality.
|
|
201
209
|
*/
|
|
202
210
|
export async function downloadAsPDF(opts: {
|
|
203
|
-
filename: string;
|
|
204
211
|
webpdf: boolean;
|
|
205
212
|
preset?: PDFExportPreset;
|
|
206
213
|
includeInputs?: boolean;
|
|
@@ -210,7 +217,6 @@ export async function downloadAsPDF(opts: {
|
|
|
210
217
|
}) {
|
|
211
218
|
const client = getRequestClient();
|
|
212
219
|
const {
|
|
213
|
-
filename,
|
|
214
220
|
webpdf,
|
|
215
221
|
preset = "document",
|
|
216
222
|
includeInputs = true,
|
|
@@ -220,7 +226,7 @@ export async function downloadAsPDF(opts: {
|
|
|
220
226
|
} = opts;
|
|
221
227
|
|
|
222
228
|
try {
|
|
223
|
-
const
|
|
229
|
+
const exportedFile = await client.exportAsPDF({
|
|
224
230
|
webpdf,
|
|
225
231
|
preset,
|
|
226
232
|
includeInputs,
|
|
@@ -229,8 +235,7 @@ export async function downloadAsPDF(opts: {
|
|
|
229
235
|
rasterServer,
|
|
230
236
|
});
|
|
231
237
|
|
|
232
|
-
|
|
233
|
-
downloadBlob(pdfBlob, Filenames.toPDF(filenameWithoutPath));
|
|
238
|
+
downloadExportedFile(exportedFile);
|
|
234
239
|
} catch (error) {
|
|
235
240
|
toast({
|
|
236
241
|
title: "Failed to download",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as x}from"./chunk-LvLJmgfZ.js";import{i as E,p as T,u as M}from"./useEvent-D91BmmQi.js";import{t as C}from"./react-Bj1aDYRI.js";import{Mi as S,Ni as f,Tn as H,at as j,kn as N,p as I,vt as L}from"./cells-Cf7bHHq3.js";import{t as g}from"./compiler-runtime-B3qBwwSJ.js";import{g as w}from"./useEventListener-B_w3A283.js";import{c as k}from"./utils-Z80SV4_d.js";import{t as V}from"./jsx-runtime-Blw4afVn.js";import{t as $}from"./tooltip-CFX4fYHU.js";import{t as A}from"./copy-icon-C2pG0ZNK.js";import{t as F}from"./usePress-5hBv4sjD.js";import{n as O}from"./useDebounce-JDKr83KO.js";import{t as _}from"./useRunCells-CSSF_mJ0.js";var W=g(),m=x(C(),1),l=x(V(),1);const q=e=>{let t=(0,W.c)(16),r,n,i;t[0]===e?(r=t[1],n=t[2],i=t[3]):({href:n,children:r,...i}=e,t[0]=e,t[1]=r,t[2]=n,t[3]=i);let a=(0,m.useRef)(null),o;t[4]===n?o=t[5]:(o=()=>{let h=new URL(globalThis.location.href);h.hash=n,globalThis.history.pushState({},"",h.toString()),globalThis.dispatchEvent(new HashChangeEvent("hashchange"));let R=n.slice(1),v=document.getElementById(R);v&&v.scrollIntoView({behavior:"smooth",block:"start"})},t[4]=n,t[5]=o);let s=o,c;t[6]===s?c=t[7]:(c={onPress:()=>{s()}},t[6]=s,t[7]=c);let{pressProps:d}=F(c),u;t[8]===s?u=t[9]:(u=h=>{h.preventDefault(),s()},t[8]=s,t[9]=u);let b=u,p;return t[10]!==r||t[11]!==b||t[12]!==n||t[13]!==d||t[14]!==i?(p=(0,l.jsx)("a",{ref:a,href:n,...d,onClick:b,...i,children:r}),t[10]=r,t[11]=b,t[12]=n,t[13]=d,t[14]=i,t[15]=p):p=t[15],p};async function z(e){let t=I().inOrderIds.at(0);if(t)try{let r=await H.request({document:e,cellId:t});if(!r||r.options.length===0)return;let n=e.split(".").pop()??e,i=r.options[0],a=r.options.find(o=>o.name===n)??i;a!=null&&a.completion_info&&E.set(N,{documentation:a.completion_info})}catch(r){w.debug(`Doc lookup failed for "${e}"`,r)}}var P=g();const D=e=>{let t=(0,P.c)(8),{qualifiedName:r,children:n}=e,i;t[0]===r?i=t[1]:(i=()=>{z(r)},t[0]=r,t[1]=i);let a=O(i,100),o;t[2]===a?o=t[3]:(o=()=>a.cancel(),t[2]=a,t[3]=o);let s;return t[4]!==n||t[5]!==a||t[6]!==o?(s=(0,l.jsx)("span",{onMouseEnter:a,onMouseLeave:o,children:n}),t[4]=n,t[5]=a,t[6]=o,t[7]=s):s=t[7],s};function B(e){if(typeof e!="object"||!e)return!1;let t=e;return!(t.trusted!==!0||t.notebookCode!==void 0&&typeof t.notebookCode!="string")}function U(){let e=window==null?void 0:window.__MARIMO_EXPORT_CONTEXT__;return B(e)?e:void 0}function y(){var e;return((e=U())==null?void 0:e.trusted)===!0}function X(){if(E.get(_)||y()||E.get(k))return!0;try{if(j()==="read")return!0}catch{}return!1}var G=T(e=>{let t=e(_),r=e(k);if(t||r||y())return!1;let n=!0;try{n=j()==="read"}catch{return!0}return!n});function J(){return M(G)}var K=g(),Y=e=>{if(e instanceof f.Element&&!/^[A-Za-z][\w-]*$/.test(e.name))return m.createElement(m.Fragment)},Z=(e,t)=>{if(t instanceof f.Element&&t.name==="body"){if((0,m.isValidElement)(e)&&"props"in e){let r=e.props.children;return(0,l.jsx)(l.Fragment,{children:r})}return}},Q=(e,t)=>{if(t instanceof f.Element&&t.name==="html"){if((0,m.isValidElement)(e)&&"props"in e){let r=e.props.children;return(0,l.jsx)(l.Fragment,{children:r})}return}},tt=e=>{if(e instanceof f.Element&&e.attribs&&e.name==="iframe"){let t=document.createElement("iframe");return Object.entries(e.attribs).forEach(([r,n])=>{r.startsWith('"')&&r.endsWith('"')&&(r=r.slice(1,-1)),t.setAttribute(r,n)}),(0,l.jsx)("div",{dangerouslySetInnerHTML:{__html:t.outerHTML}})}},et=e=>{if(e instanceof f.Element&&e.name==="script"){let t=e.attribs.src;if(!t)return;if(!X())return w.warn(`[RenderHTML] refusing <script src> in untrusted context: ${t}`),(0,l.jsx)(l.Fragment,{});if(![...document.querySelectorAll("script[src]")].some(r=>r.getAttribute("src")===t)){let r=document.createElement("script");r.src=t,document.head.append(r)}return(0,l.jsx)(l.Fragment,{})}},rt=(e,t)=>{if(t instanceof f.Element&&t.name==="a"){let r=t.attribs.href;if(r!=null&&r.startsWith("#")&&!r.startsWith("#code/")){let n=null;return(0,m.isValidElement)(e)&&"props"in e&&(n=e.props.children),(0,l.jsx)(q,{href:r,...t.attribs,children:n})}}},nt=(e,t,r)=>{var n,i;if(t instanceof f.Element&&t.name==="div"&&((i=(n=t.attribs)==null?void 0:n.class)!=null&&i.includes("codehilite")))return(0,l.jsx)(lt,{children:e},r)},it=(e,t,r)=>{var i;if(!(t instanceof f.Element)||t.name!=="img")return;let n=(i=t.attribs)==null?void 0:i.src;if(!(!n||!(0,m.isValidElement)(e))&&!/^data:/i.test(n))return(0,m.cloneElement)(e,{key:`${n}-${r}`})},at=(e,t,r)=>{var n;if(!(!(t instanceof f.Element)||t.name!=="input")&&((n=t.attribs)==null?void 0:n.type)==="checkbox"&&(0,m.isValidElement)(e))return(0,m.cloneElement)(e,{key:`checkbox-${r}-${"checked"in t.attribs}`})},ot=(e,t)=>{var r;if(t instanceof f.Element&&((r=t.attribs)!=null&&r["data-marimo-doc"])){let n=t.attribs["data-marimo-doc"];return(0,l.jsx)(D,{qualifiedName:n,children:e})}},st=(e,t)=>{var r,n;if(t instanceof f.Element&&((r=t.attribs)!=null&&r["data-tooltip"])){if((((n=t.name)==null?void 0:n.toLowerCase())??"").startsWith("marimo-"))return;let i=t.attribs["data-tooltip"];return(0,l.jsx)($,{content:i,children:e})}},lt=e=>{let t=(0,K.c)(3),{children:r}=e,n=(0,m.useRef)(null),i;t[0]===Symbol.for("react.memo_cache_sentinel")?(i=(0,l.jsx)("div",{className:"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity",children:(0,l.jsx)(A,{tooltip:!1,className:"p-1",value:()=>{var s;let o=(s=n.current)==null?void 0:s.firstChild;return o&&o.textContent||""}})}),t[0]=i):i=t[0];let a;return t[1]===r?a=t[2]:(a=(0,l.jsxs)("div",{className:"relative group codehilite-wrapper",ref:n,children:[r,i]}),t[1]=r,t[2]=a),a};const ct=({html:e,additionalReplacements:t=[],alwaysSanitizeHtml:r=!0})=>(0,l.jsx)(mt,{html:e,alwaysSanitizeHtml:r,additionalReplacements:t});var mt=({html:e,additionalReplacements:t=[],alwaysSanitizeHtml:r})=>{let n=J();return ft({html:(0,m.useMemo)(()=>r||n?L(e):e,[e,r,n]),additionalReplacements:t})};function ft({html:e,additionalReplacements:t=[]}){let r=[Y,tt,et,...t],n=[nt,rt,ot,st,Z,Q],i=[it,at];return S(e,{replace:(a,o)=>{for(let s of r){let c=s(a,o);if(c)return c}return a},transform:(a,o,s)=>{let c=a;for(let d of n){let u=d(c,o,s);if(u){c=u;break}}for(let d of i){let u=d(c,o,s);u&&(c=u)}return c}})}export{y as n,ct as t};
|