@marimo-team/islands 0.25.3-dev6 → 0.25.3-dev60
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-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
- package/dist/{ErrorBoundary-Ca-Sa492.js → ErrorBoundary-BmHVnL-A.js} +8 -8
- package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
- package/dist/any-language-editor-k2B8YI3Y.js +75 -0
- package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
- package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
- package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
- package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
- package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C0_lCzql.js} +1139 -1140
- package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
- package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
- package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
- package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
- package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
- package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
- package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
- package/dist/{code-visibility-NGRjhbtA.js → code-visibility-BWERlt58.js} +6 -6
- package/dist/{common-B7_ZSg0P.js → common-E2zdIyjs.js} +4960 -4835
- package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
- package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
- package/dist/{data-grid-overlay-editor-B_cmrkBm.js → data-grid-overlay-editor-CHUwPg7P.js} +5 -5
- package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
- package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
- package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
- package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
- package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
- package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-B3eUiVJB.js} +177 -169
- package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
- package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
- package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
- package/dist/{feedback-button-CTF2Nj9s.js → feedback-button-DwOUm3jt.js} +29 -29
- package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
- package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
- package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
- package/dist/{glide-data-editor-BQHF9puC.js → glide-data-editor-C9WGlA66.js} +370 -368
- package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
- package/dist/{globals-Cj8iqMEp.js → globals-BwFL--QV.js} +3 -3
- package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
- package/dist/{html-to-image-B8s5SgGm.js → html-to-image-6xcoTKhC.js} +29 -29
- package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
- package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
- package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
- package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
- package/dist/main.js +3177 -2980
- package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
- package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
- package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
- package/dist/{number-overlay-editor-DBcnm0SY.js → number-overlay-editor-Ci9h4-h1.js} +3 -3
- package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
- package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
- package/dist/precisionRound-DhAzeRcm.js +49 -0
- package/dist/{process-output-CKRXyszb.js → process-output-BJRJFobf.js} +4 -4
- package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
- package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
- package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
- package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
- package/dist/{reveal-component-DTBSuGkC.js → reveal-component-Ay-npdvI.js} +19 -19
- package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
- package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
- package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
- package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
- package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
- package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
- package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
- package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
- package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
- package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
- package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
- package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
- package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
- package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
- package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
- package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
- package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
- package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
- package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
- package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
- package/package.json +4 -4
- package/src/__tests__/host-imports.test.ts +1 -0
- package/src/components/app-config/ai-config.tsx +6 -2
- package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
- package/src/components/chat/chat-panel.tsx +3 -7
- package/src/components/chat/chat-utils.ts +4 -8
- package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
- package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
- package/src/components/data-table/__tests__/export-actions.test.tsx +434 -6
- package/src/components/data-table/charts/charts.tsx +14 -19
- package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
- package/src/components/data-table/columns.tsx +2 -1
- package/src/components/data-table/context-menu.tsx +36 -20
- package/src/components/data-table/date-popover.tsx +6 -4
- package/src/components/data-table/export-actions.tsx +267 -163
- package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
- package/src/components/data-table/schemas.ts +14 -36
- package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
- package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
- package/src/components/editor/cell/CreateCellButton.tsx +8 -10
- package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
- package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
- package/src/components/editor/cell/code/cell-editor.tsx +1 -1
- package/src/components/editor/cell/code/language-toggle.tsx +8 -7
- package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
- package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +12 -18
- package/src/components/editor/connections/components.tsx +1 -1
- package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
- package/src/components/editor/controls/command-palette.tsx +22 -10
- package/src/components/editor/dynamic-favicon.tsx +21 -40
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +12 -23
- package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
- package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
- package/src/components/editor/file-tree/file-explorer.tsx +10 -5
- package/src/components/editor/file-tree/file-viewer.tsx +4 -2
- package/src/components/editor/file-tree/use-file-search.ts +35 -21
- package/src/components/editor/output/MarimoTracebackOutput.tsx +1 -1
- package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
- package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
- package/src/components/icons/large-spinner.tsx +3 -14
- package/src/components/pages/home-page.tsx +4 -4
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/tracing/tracing.tsx +2 -2
- package/src/core/attachments/state.ts +5 -8
- package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
- package/src/core/cells/cells.ts +2 -1
- package/src/core/cells/pending-delete-service.ts +2 -6
- package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
- package/src/core/datasets/data-source-discovery.ts +4 -6
- package/src/core/edit-app.tsx +2 -2
- package/src/core/host/AGENTS.md +11 -2
- package/src/core/host/__tests__/attachments.test.ts +56 -0
- package/src/core/host/__tests__/document.test.ts +126 -0
- package/src/core/host/__tests__/events.test.ts +49 -0
- package/src/core/host/__tests__/fixtures.ts +41 -0
- package/src/core/host/__tests__/notebook-client.test.ts +496 -0
- package/src/core/host/connection.ts +81 -0
- package/src/core/host/events.ts +68 -0
- package/src/core/host/notebook-client.ts +316 -0
- package/src/core/host/resources/attachments.ts +19 -0
- package/src/core/host/resources/document.ts +101 -0
- package/src/core/hotkeys/actions.ts +2 -4
- package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/network/connection.ts +4 -2
- package/src/core/network/useConnectionNotice.ts +8 -2
- package/src/core/run-app.tsx +2 -2
- package/src/core/runtime/__tests__/runtime.test.ts +5 -12
- package/src/core/runtime/runtime.ts +5 -5
- package/src/core/static/files.ts +2 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
- package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
- package/src/core/websocket/connection-state.ts +70 -0
- package/src/core/websocket/state.ts +7 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
- package/src/core/websocket/transports/create.ts +31 -0
- package/src/core/websocket/transports/sse.ts +14 -2
- package/src/core/websocket/transports/transport.ts +12 -23
- package/src/core/websocket/types.ts +9 -39
- package/src/core/websocket/useNotebookClient.ts +78 -0
- package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
- package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
- package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
- package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
- package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
- package/src/hooks/__tests__/useScript.test.tsx +77 -0
- package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
- package/src/hooks/useAsyncData.ts +3 -3
- package/src/hooks/useDataSourceDiscovery.ts +6 -4
- package/src/hooks/useDebounce.ts +5 -8
- package/src/hooks/useDeepCompareMemoize.ts +1 -1
- package/src/hooks/useDelayElapsed.ts +14 -10
- package/src/hooks/useElapsedTime.ts +3 -3
- package/src/hooks/useInputHistory.ts +4 -7
- package/src/hooks/useInternalStateWithSync.ts +6 -6
- package/src/hooks/useResizeObserver.ts +4 -7
- package/src/hooks/useScript.ts +67 -70
- package/src/hooks/useStateForKey.ts +93 -0
- package/src/plugins/core/registerReactComponent.tsx +1 -1
- package/src/plugins/impl/DataTablePlugin.tsx +14 -15
- package/src/plugins/impl/DropdownPlugin.tsx +2 -6
- package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/SliderPlugin.tsx +6 -8
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +201 -1
- package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
- package/src/plugins/impl/anywidget/runtime.ts +21 -9
- package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
- package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
- package/src/plugins/impl/chat/chat-ui.tsx +10 -9
- package/src/plugins/impl/code/any-language-editor.tsx +2 -2
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
- package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
- package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
- package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
- package/src/plugins/impl/panel/utils.ts +17 -0
- package/src/plugins/impl/vega/make-selectable.ts +3 -6
- package/src/stories/__tests__/editor-story.test.tsx +33 -0
- package/src/stories/editor.stories.tsx +24 -17
- package/src/utils/__tests__/arrays.test.ts +35 -0
- package/src/utils/__tests__/numbers.test.ts +48 -1
- package/src/utils/__tests__/objects.test.ts +21 -0
- package/src/utils/arrays.ts +10 -0
- package/src/utils/numbers.ts +28 -17
- package/src/utils/objects.ts +3 -7
- package/src/utils/once.ts +19 -19
- package/dist/any-language-editor-DZ_YTVus.js +0 -76
- package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
- package/dist/precisionRound-DMIVQrt2.js +0 -97
- package/src/core/host/__tests__/protocol.test.ts +0 -214
- package/src/core/host/protocol.ts +0 -1943
- package/src/core/websocket/useWebSocket.tsx +0 -88
|
@@ -34,6 +34,7 @@ import { DelayMount } from "@/components/utils/delay-mount";
|
|
|
34
34
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
35
35
|
import { Banner, ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
36
36
|
import type { GetRowResult } from "@/plugins/impl/DataTablePlugin";
|
|
37
|
+
import { Objects } from "@/utils/objects";
|
|
37
38
|
import { NAMELESS_COLUMN_PREFIX, renderCellValue } from "../columns";
|
|
38
39
|
import { prettifyRowCount } from "../pagination";
|
|
39
40
|
import {
|
|
@@ -88,8 +89,7 @@ export const RowViewerPanel: React.FC<RowViewerPanelProps> = ({
|
|
|
88
89
|
handleRowSelectionChange?.((prev) => {
|
|
89
90
|
if (isRowSelected) {
|
|
90
91
|
// Remove this row from selection
|
|
91
|
-
|
|
92
|
-
return rest;
|
|
92
|
+
return Objects.filter(prev, (_, key) => key !== String(rowIdx));
|
|
93
93
|
}
|
|
94
94
|
// Add this row to selection
|
|
95
95
|
return { ...prev, [rowIdx]: true };
|
|
@@ -3,42 +3,6 @@
|
|
|
3
3
|
import z from "zod";
|
|
4
4
|
import { rpc } from "@/plugins/core/rpc";
|
|
5
5
|
|
|
6
|
-
export type DownloadFormat = "csv" | "json" | "parquet" | "tsv" | "geojson";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Per-request export options. Each field is optional. A missing field keeps
|
|
10
|
-
* the widget's default for that setting.
|
|
11
|
-
*/
|
|
12
|
-
export interface DownloadAsOptions {
|
|
13
|
-
separator?: string;
|
|
14
|
-
encoding?: string;
|
|
15
|
-
ensure_ascii?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface DownloadAsRequest {
|
|
19
|
-
format: DownloadFormat;
|
|
20
|
-
options?: DownloadAsOptions;
|
|
21
|
-
geometry_column?: string | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type DownloadAsArgs = (req: DownloadAsRequest) => Promise<{
|
|
25
|
-
url: string;
|
|
26
|
-
filename: string;
|
|
27
|
-
error?: string | null;
|
|
28
|
-
missing_packages?: string[] | null;
|
|
29
|
-
code?:
|
|
30
|
-
| "geometry_required"
|
|
31
|
-
| "invalid_geometry"
|
|
32
|
-
| "invalid_metadata"
|
|
33
|
-
| "unsupported_representation"
|
|
34
|
-
| "unsupported_version"
|
|
35
|
-
| "missing_packages"
|
|
36
|
-
| "missing_crs"
|
|
37
|
-
| "conversion_failed"
|
|
38
|
-
| null;
|
|
39
|
-
column?: string | null;
|
|
40
|
-
}>;
|
|
41
|
-
|
|
42
6
|
const DownloadRequestSchema = z.object({
|
|
43
7
|
geometry_column: z.string().nullish(),
|
|
44
8
|
options: z
|
|
@@ -88,6 +52,20 @@ export const DownloadGeoJSONSchema = rpc
|
|
|
88
52
|
}),
|
|
89
53
|
);
|
|
90
54
|
|
|
55
|
+
export type DownloadAsRequest =
|
|
56
|
+
| z.infer<typeof DownloadAsSchema.input>
|
|
57
|
+
| z.infer<typeof DownloadGeoJSONSchema.input>;
|
|
58
|
+
|
|
59
|
+
type DownloadResponse =
|
|
60
|
+
| z.infer<typeof DownloadAsSchema.output>
|
|
61
|
+
| z.infer<typeof DownloadGeoJSONSchema.output>;
|
|
62
|
+
|
|
63
|
+
export type DownloadFormat = DownloadAsRequest["format"];
|
|
64
|
+
export type DownloadAsOptions = NonNullable<DownloadAsRequest["options"]>;
|
|
65
|
+
export type DownloadAsArgs = (
|
|
66
|
+
req: DownloadAsRequest,
|
|
67
|
+
) => Promise<DownloadResponse>;
|
|
68
|
+
|
|
91
69
|
export type DownloadAsFunction = (
|
|
92
70
|
req: z.infer<typeof DownloadAsSchema.input>,
|
|
93
71
|
) => Promise<z.infer<typeof DownloadAsSchema.output>>;
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
render as renderComponent,
|
|
6
|
+
waitFor,
|
|
7
|
+
} from "@testing-library/react";
|
|
4
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import {
|
|
9
|
+
import { Provider, createStore } from "jotai";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
import { MockNotebook } from "@/__mocks__/notebook";
|
|
12
|
+
import { cellId } from "@/__tests__/branded";
|
|
13
|
+
import { notebookAtom } from "@/core/cells/cells";
|
|
6
14
|
import { DynamicFavicon } from "../dynamic-favicon";
|
|
7
15
|
|
|
8
|
-
// Mock useCellErrors hook
|
|
9
|
-
vi.mock("@/core/cells/cells", () => ({
|
|
10
|
-
useCellErrors: vi.fn(),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
16
|
describe("DynamicFavicon", () => {
|
|
14
17
|
let favicon: HTMLLinkElement;
|
|
18
|
+
let store: ReturnType<typeof createStore>;
|
|
19
|
+
const id = cellId("cell");
|
|
20
|
+
|
|
21
|
+
function render(ui: ReactNode) {
|
|
22
|
+
return renderComponent(ui, {
|
|
23
|
+
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setErrors() {
|
|
28
|
+
store.set(
|
|
29
|
+
notebookAtom,
|
|
30
|
+
MockNotebook.notebookStateWithErrors([
|
|
31
|
+
{
|
|
32
|
+
cellId: id,
|
|
33
|
+
cellName: "cell",
|
|
34
|
+
errorData: [MockNotebook.errors.syntax("Invalid syntax")],
|
|
35
|
+
},
|
|
36
|
+
]),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
15
39
|
|
|
16
40
|
beforeEach(() => {
|
|
17
41
|
document
|
|
@@ -27,15 +51,20 @@ describe("DynamicFavicon", () => {
|
|
|
27
51
|
// Mock document.hasFocus
|
|
28
52
|
vi.spyOn(document, "hasFocus").mockReturnValue(true);
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
store = createStore();
|
|
55
|
+
store.set(
|
|
56
|
+
notebookAtom,
|
|
57
|
+
MockNotebook.notebookState({
|
|
58
|
+
cellData: { [id]: { code: "x = 1" } },
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
32
61
|
});
|
|
33
62
|
|
|
34
63
|
afterEach(() => {
|
|
35
64
|
document
|
|
36
65
|
.querySelectorAll("link[rel~='icon']")
|
|
37
66
|
.forEach((element) => element.remove());
|
|
38
|
-
vi.
|
|
67
|
+
vi.restoreAllMocks();
|
|
39
68
|
vi.useRealTimers();
|
|
40
69
|
});
|
|
41
70
|
|
|
@@ -67,9 +96,7 @@ describe("DynamicFavicon", () => {
|
|
|
67
96
|
});
|
|
68
97
|
|
|
69
98
|
it("should show error favicon when run completes with errors", async () => {
|
|
70
|
-
(
|
|
71
|
-
{ error: "mock error" },
|
|
72
|
-
]);
|
|
99
|
+
setErrors();
|
|
73
100
|
|
|
74
101
|
const { rerender } = render(<DynamicFavicon isRunning={true} />);
|
|
75
102
|
|
|
@@ -147,6 +174,72 @@ describe("DynamicFavicon", () => {
|
|
|
147
174
|
});
|
|
148
175
|
});
|
|
149
176
|
|
|
177
|
+
it("does not look up or rewrite the favicon when code changes", async () => {
|
|
178
|
+
const { rerender } = render(<DynamicFavicon isRunning={true} />);
|
|
179
|
+
await waitFor(() => expect(favicon.href).toContain("circle-play.ico"));
|
|
180
|
+
rerender(<DynamicFavicon isRunning={false} />);
|
|
181
|
+
await waitFor(() => expect(favicon.href).toContain("circle-check.ico"));
|
|
182
|
+
const lookup = vi.spyOn(document, "querySelector");
|
|
183
|
+
const write = vi.spyOn(favicon, "href", "set");
|
|
184
|
+
|
|
185
|
+
await act(async () => {
|
|
186
|
+
store.set(notebookAtom, (state) => ({
|
|
187
|
+
...state,
|
|
188
|
+
cellData: {
|
|
189
|
+
...state.cellData,
|
|
190
|
+
[id]: { ...state.cellData[id], code: "x = 2" },
|
|
191
|
+
},
|
|
192
|
+
}));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(
|
|
196
|
+
lookup.mock.calls.filter(
|
|
197
|
+
([selector]) => selector === "link[rel~='icon']",
|
|
198
|
+
),
|
|
199
|
+
).toEqual([]);
|
|
200
|
+
expect(write).not.toHaveBeenCalled();
|
|
201
|
+
lookup.mockRestore();
|
|
202
|
+
write.mockRestore();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("updates the favicon when the error count changes", async () => {
|
|
206
|
+
const { rerender } = render(<DynamicFavicon isRunning={true} />);
|
|
207
|
+
await waitFor(() => expect(favicon.href).toContain("circle-play.ico"));
|
|
208
|
+
rerender(<DynamicFavicon isRunning={false} />);
|
|
209
|
+
await waitFor(() => expect(favicon.href).toContain("circle-check.ico"));
|
|
210
|
+
|
|
211
|
+
act(() => setErrors());
|
|
212
|
+
await waitFor(() => expect(favicon.href).toContain("circle-x.ico"));
|
|
213
|
+
|
|
214
|
+
act(() =>
|
|
215
|
+
store.set(
|
|
216
|
+
notebookAtom,
|
|
217
|
+
MockNotebook.notebookState({ cellData: { [id]: {} } }),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
await waitFor(() => expect(favicon.href).toContain("circle-check.ico"));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("keeps the completion icon until its own reset timer expires", async () => {
|
|
224
|
+
vi.useFakeTimers();
|
|
225
|
+
const { rerender } = render(<DynamicFavicon isRunning={true} />);
|
|
226
|
+
await act(() => vi.advanceTimersByTimeAsync(1000));
|
|
227
|
+
rerender(<DynamicFavicon isRunning={false} />);
|
|
228
|
+
await act(() => vi.advanceTimersByTimeAsync(2000));
|
|
229
|
+
expect(favicon.href).toContain("circle-check.ico");
|
|
230
|
+
await act(() => vi.advanceTimersByTimeAsync(1000));
|
|
231
|
+
expect(favicon.href).toContain("favicon.ico");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("cancels the reset timer when unmounted", async () => {
|
|
235
|
+
vi.useFakeTimers();
|
|
236
|
+
const { unmount } = render(<DynamicFavicon isRunning={true} />);
|
|
237
|
+
await act(() => vi.advanceTimersByTimeAsync(0));
|
|
238
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
239
|
+
unmount();
|
|
240
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
150
243
|
describe("notifications", () => {
|
|
151
244
|
beforeEach(() => {
|
|
152
245
|
vi.spyOn(document, "visibilityState", "get").mockReturnValue("hidden");
|
|
@@ -156,7 +249,7 @@ describe("DynamicFavicon", () => {
|
|
|
156
249
|
global.Notification.permission = "granted";
|
|
157
250
|
});
|
|
158
251
|
|
|
159
|
-
it("should send success notification when run completes without errors", () => {
|
|
252
|
+
it("should send success notification when run completes without errors", async () => {
|
|
160
253
|
// @ts-expect-error ok in tests
|
|
161
254
|
global.Notification = vi
|
|
162
255
|
.fn()
|
|
@@ -172,12 +265,11 @@ describe("DynamicFavicon", () => {
|
|
|
172
265
|
|
|
173
266
|
const { rerender } = render(<DynamicFavicon isRunning={true} />);
|
|
174
267
|
rerender(<DynamicFavicon isRunning={false} />);
|
|
268
|
+
await waitFor(() => expect(Notification).toHaveBeenCalledOnce());
|
|
175
269
|
});
|
|
176
270
|
|
|
177
|
-
it("should send error notification when run completes with errors", () => {
|
|
178
|
-
(
|
|
179
|
-
{ error: "mock error" },
|
|
180
|
-
]);
|
|
271
|
+
it("should send error notification when run completes with errors", async () => {
|
|
272
|
+
setErrors();
|
|
181
273
|
|
|
182
274
|
// @ts-expect-error ok in tests
|
|
183
275
|
global.Notification = vi
|
|
@@ -194,6 +286,7 @@ describe("DynamicFavicon", () => {
|
|
|
194
286
|
|
|
195
287
|
const { rerender } = render(<DynamicFavicon isRunning={true} />);
|
|
196
288
|
rerender(<DynamicFavicon isRunning={false} />);
|
|
289
|
+
await waitFor(() => expect(Notification).toHaveBeenCalledOnce());
|
|
197
290
|
});
|
|
198
291
|
|
|
199
292
|
it("should not send notification when document is visible", () => {
|
|
@@ -314,6 +314,30 @@ describe("PairWithAgentModal", () => {
|
|
|
314
314
|
expect(screen.getByRole("tabpanel")).not.toHaveTextContent("--notebook");
|
|
315
315
|
});
|
|
316
316
|
|
|
317
|
+
it("reverifies the target when host mode is enabled again for the same Session", async () => {
|
|
318
|
+
await act(async () => {
|
|
319
|
+
render(<PairWithAgentModal onClose={vi.fn()} />, { wrapper });
|
|
320
|
+
});
|
|
321
|
+
expect(screen.getByRole("tabpanel")).toHaveTextContent(NOTEBOOK_ID);
|
|
322
|
+
act(() =>
|
|
323
|
+
store.set(pairConfigAtom, { ...PAIR_CONFIG, hostEnabled: false }),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const pending = new Deferred<Response>();
|
|
327
|
+
vi.mocked(fetch).mockImplementation((url) =>
|
|
328
|
+
String(url).endsWith("/api/sessions")
|
|
329
|
+
? pending.promise
|
|
330
|
+
: responseFor(url),
|
|
331
|
+
);
|
|
332
|
+
act(() => store.set(pairConfigAtom, PAIR_CONFIG));
|
|
333
|
+
expect(screen.getByRole("button", { name: "Copy command" })).toBeDisabled();
|
|
334
|
+
expect(screen.getByRole("tabpanel")).not.toHaveTextContent(NOTEBOOK_ID);
|
|
335
|
+
|
|
336
|
+
await act(async () => pending.resolve(await responseFor("/api/sessions")));
|
|
337
|
+
expect(screen.getByRole("button", { name: "Copy command" })).toBeEnabled();
|
|
338
|
+
expect(screen.getByRole("tabpanel")).toHaveTextContent(NOTEBOOK_ID);
|
|
339
|
+
});
|
|
340
|
+
|
|
317
341
|
it("keeps copying disabled when Notebook verification fails", async () => {
|
|
318
342
|
vi.mocked(fetch).mockImplementation((url) =>
|
|
319
343
|
String(url).endsWith("/api/sessions")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
4
|
-
import React, { useEffect, useState } from "react";
|
|
4
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
5
5
|
import { useAtomValue } from "jotai";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import {
|
|
@@ -53,21 +53,20 @@ export const PairWithAgentModal: React.FC<{
|
|
|
53
53
|
const config = useAtomValue(pairConfigAtom);
|
|
54
54
|
const hostEnabled = config?.hostEnabled ?? false;
|
|
55
55
|
const stableSessionId = useAtomValue(stableSessionIdAtom);
|
|
56
|
+
const lookupIdentity = useMemo(
|
|
57
|
+
() => ({ hostEnabled, stableSessionId }),
|
|
58
|
+
[hostEnabled, stableSessionId],
|
|
59
|
+
);
|
|
56
60
|
const [lookup, setLookup] = useState<
|
|
57
61
|
{
|
|
58
|
-
|
|
59
|
-
} & (
|
|
60
|
-
| { status: "ready"; notebookId: string | null }
|
|
61
|
-
| { status: "loading" | "error" }
|
|
62
|
-
)
|
|
62
|
+
identity: typeof lookupIdentity;
|
|
63
|
+
} & ({ status: "ready"; notebookId: string | null } | { status: "error" })
|
|
63
64
|
>();
|
|
64
65
|
useEffect(() => {
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const { hostEnabled, stableSessionId: sessionId } = lookupIdentity;
|
|
67
|
+
if (!hostEnabled || !sessionId) {
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
|
-
const sessionId = stableSessionId;
|
|
70
|
-
setLookup({ sessionId, status: "loading" });
|
|
71
70
|
let cancelled = false;
|
|
72
71
|
API.get<Record<string, { session_id: string; notebook_id: string | null }>>(
|
|
73
72
|
"/sessions",
|
|
@@ -79,22 +78,26 @@ export const PairWithAgentModal: React.FC<{
|
|
|
79
78
|
);
|
|
80
79
|
setLookup(
|
|
81
80
|
session
|
|
82
|
-
? {
|
|
83
|
-
|
|
81
|
+
? {
|
|
82
|
+
identity: lookupIdentity,
|
|
83
|
+
status: "ready",
|
|
84
|
+
notebookId: session.notebook_id,
|
|
85
|
+
}
|
|
86
|
+
: { identity: lookupIdentity, status: "error" },
|
|
84
87
|
);
|
|
85
88
|
}
|
|
86
89
|
})
|
|
87
90
|
.catch(() => {
|
|
88
91
|
if (!cancelled) {
|
|
89
|
-
setLookup({
|
|
92
|
+
setLookup({ identity: lookupIdentity, status: "error" });
|
|
90
93
|
}
|
|
91
94
|
});
|
|
92
95
|
return () => {
|
|
93
96
|
cancelled = true;
|
|
94
97
|
};
|
|
95
|
-
}, [
|
|
98
|
+
}, [lookupIdentity]);
|
|
96
99
|
const currentLookup =
|
|
97
|
-
lookup?.
|
|
100
|
+
lookup?.identity === lookupIdentity ? lookup : undefined;
|
|
98
101
|
const verified = !hostEnabled || currentLookup?.status === "ready";
|
|
99
102
|
const notebookId =
|
|
100
103
|
hostEnabled && currentLookup?.status === "ready"
|
|
@@ -106,9 +106,9 @@ export const AddCellWithAI: React.FC<{
|
|
|
106
106
|
const runtimeManager = useRuntimeManager();
|
|
107
107
|
|
|
108
108
|
const inputRef = useRef<ReactCodeMirrorRef>(null);
|
|
109
|
-
const submissionController =
|
|
110
|
-
new StagedCellSubmissionController(),
|
|
111
|
-
)
|
|
109
|
+
const [submissionController] = useState(
|
|
110
|
+
() => new StagedCellSubmissionController(),
|
|
111
|
+
);
|
|
112
112
|
|
|
113
113
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
114
114
|
const { files, addFiles, removeFile } = useFileState();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { DatabaseIcon, DiamondPlusIcon, PlusIcon } from "lucide-react";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
4
|
import { Button } from "@/components/editor/inputs/Inputs";
|
|
5
5
|
import { MinimalHotkeys } from "@/components/shortcuts/renderShortcut";
|
|
6
6
|
import {
|
|
@@ -36,7 +36,8 @@ export const CreateCellButton = ({
|
|
|
36
36
|
const { createNewCell, addSetupCellIfDoesntExist } = useCellActions();
|
|
37
37
|
const shortcut = `${oneClickShortcut}-Click`;
|
|
38
38
|
const [open, setOpen] = useState(false);
|
|
39
|
-
|
|
39
|
+
// Time the dropdown last opened, to ignore the click that opened it
|
|
40
|
+
const openedAt = useRef(0);
|
|
40
41
|
|
|
41
42
|
const baseTooltipContent =
|
|
42
43
|
getConnectionTooltip(connectionState) || tooltipContent;
|
|
@@ -83,9 +84,7 @@ export const CreateCellButton = ({
|
|
|
83
84
|
|
|
84
85
|
const openDropdown = () => {
|
|
85
86
|
setOpen(true);
|
|
86
|
-
|
|
87
|
-
// Allow interactions after a brief delay to prevent the dropdown items immediately being clicked
|
|
88
|
-
setTimeout(() => setJustOpened(false), 200);
|
|
87
|
+
openedAt.current = Date.now();
|
|
89
88
|
};
|
|
90
89
|
|
|
91
90
|
// We use onPointerDownCapture (not onPointerDown) to intercept events in
|
|
@@ -121,15 +120,14 @@ export const CreateCellButton = ({
|
|
|
121
120
|
const handleOpenChange = (isOpen: boolean) => {
|
|
122
121
|
setOpen(isOpen);
|
|
123
122
|
if (isOpen) {
|
|
124
|
-
|
|
125
|
-
// Allow interactions after a brief delay
|
|
126
|
-
setTimeout(() => setJustOpened(false), 200);
|
|
123
|
+
openedAt.current = Date.now();
|
|
127
124
|
}
|
|
128
125
|
};
|
|
129
126
|
|
|
130
127
|
const handleFirstItemClick = (e: React.MouseEvent) => {
|
|
131
|
-
// Hack to prevent the first item from being clicked when the dropdown is
|
|
132
|
-
|
|
128
|
+
// Hack to prevent the first item from being clicked when the dropdown is
|
|
129
|
+
// opened: allow interactions only after a brief delay
|
|
130
|
+
if (Date.now() - openedAt.current < 200) {
|
|
133
131
|
e.preventDefault();
|
|
134
132
|
e.stopPropagation();
|
|
135
133
|
return;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { useShouldShowInterrupt } from "../useShouldShowInterrupt";
|
|
6
|
+
|
|
7
|
+
describe("useShouldShowInterrupt", () => {
|
|
8
|
+
beforeEach(() => vi.useFakeTimers());
|
|
9
|
+
afterEach(() => vi.useRealTimers());
|
|
10
|
+
|
|
11
|
+
it("waits for each run and hides immediately when it stops", () => {
|
|
12
|
+
const { result, rerender } = renderHook(useShouldShowInterrupt, {
|
|
13
|
+
initialProps: false,
|
|
14
|
+
});
|
|
15
|
+
expect(result.current).toBe(false);
|
|
16
|
+
rerender(true);
|
|
17
|
+
expect(result.current).toBe(false);
|
|
18
|
+
act(() => vi.advanceTimersByTime(200));
|
|
19
|
+
expect(result.current).toBe(true);
|
|
20
|
+
rerender(false);
|
|
21
|
+
expect(result.current).toBe(false);
|
|
22
|
+
rerender(true);
|
|
23
|
+
expect(result.current).toBe(false);
|
|
24
|
+
act(() => vi.advanceTimersByTime(199));
|
|
25
|
+
expect(result.current).toBe(false);
|
|
26
|
+
act(() => vi.advanceTimersByTime(1));
|
|
27
|
+
expect(result.current).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("cancels the delay for a short run", () => {
|
|
31
|
+
const { result, rerender } = renderHook(useShouldShowInterrupt, {
|
|
32
|
+
initialProps: true,
|
|
33
|
+
});
|
|
34
|
+
act(() => vi.advanceTimersByTime(100));
|
|
35
|
+
rerender(false);
|
|
36
|
+
act(() => vi.advanceTimersByTime(200));
|
|
37
|
+
expect(result.current).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { EditorState } from "@codemirror/state";
|
|
4
|
+
import { EditorView } from "@codemirror/view";
|
|
5
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
6
|
+
import { createRef } from "react";
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
9
|
+
import { switchLanguage } from "@/core/codemirror/language/extension";
|
|
10
|
+
import { LanguageToggle } from "../language-toggle";
|
|
11
|
+
|
|
12
|
+
vi.mock("@/core/codemirror/language/extension", () => ({
|
|
13
|
+
switchLanguage: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("LanguageToggle", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.mocked(switchLanguage).mockClear();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("uses the editor mounted after the button rendered", () => {
|
|
22
|
+
const editorViewRef = createRef<EditorView>();
|
|
23
|
+
const onAfterToggle = vi.fn();
|
|
24
|
+
render(
|
|
25
|
+
<TooltipProvider>
|
|
26
|
+
<LanguageToggle
|
|
27
|
+
editorViewRef={editorViewRef}
|
|
28
|
+
currentLanguageAdapter="python"
|
|
29
|
+
canSwitchToLanguage={true}
|
|
30
|
+
toType="markdown"
|
|
31
|
+
displayName="Markdown"
|
|
32
|
+
icon="Markdown"
|
|
33
|
+
onAfterToggle={onAfterToggle}
|
|
34
|
+
/>
|
|
35
|
+
</TooltipProvider>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
fireEvent.click(screen.getByRole("button", { name: "Markdown" }));
|
|
39
|
+
expect(switchLanguage).not.toHaveBeenCalled();
|
|
40
|
+
expect(onAfterToggle).not.toHaveBeenCalled();
|
|
41
|
+
|
|
42
|
+
const view = new EditorView({ state: EditorState.create() });
|
|
43
|
+
editorViewRef.current = view;
|
|
44
|
+
try {
|
|
45
|
+
fireEvent.click(screen.getByRole("button", { name: "Markdown" }));
|
|
46
|
+
expect(switchLanguage).toHaveBeenCalledWith(view, {
|
|
47
|
+
language: "markdown",
|
|
48
|
+
});
|
|
49
|
+
expect(onAfterToggle).toHaveBeenCalledOnce();
|
|
50
|
+
} finally {
|
|
51
|
+
view.destroy();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -572,7 +572,7 @@ const CellEditorInternal = ({
|
|
|
572
572
|
<div className="absolute top-1 right-5">
|
|
573
573
|
<LanguageToggles
|
|
574
574
|
code={code}
|
|
575
|
-
|
|
575
|
+
editorViewRef={editorViewRef}
|
|
576
576
|
currentLanguageAdapter={languageAdapter}
|
|
577
577
|
onAfterToggle={afterToggleMarkdown}
|
|
578
578
|
/>
|
|
@@ -13,7 +13,7 @@ import { Functions } from "@/utils/functions";
|
|
|
13
13
|
import { MarkdownIcon, PythonIcon } from "./icons";
|
|
14
14
|
|
|
15
15
|
interface LanguageTogglesProps {
|
|
16
|
-
|
|
16
|
+
editorViewRef: React.RefObject<EditorView | null>;
|
|
17
17
|
code: string;
|
|
18
18
|
currentLanguageAdapter: LanguageAdapter["type"] | undefined;
|
|
19
19
|
onAfterToggle: () => void;
|
|
@@ -25,7 +25,7 @@ interface LanguageTogglesProps {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
28
|
-
|
|
28
|
+
editorViewRef,
|
|
29
29
|
code,
|
|
30
30
|
currentLanguageAdapter,
|
|
31
31
|
onAfterToggle,
|
|
@@ -43,7 +43,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
|
43
43
|
return (
|
|
44
44
|
<div className={className}>
|
|
45
45
|
<LanguageToggle
|
|
46
|
-
|
|
46
|
+
editorViewRef={editorViewRef}
|
|
47
47
|
currentLanguageAdapter={currentLanguageAdapter}
|
|
48
48
|
canSwitchToLanguage={canUseSQL && currentLanguageAdapter === "python"}
|
|
49
49
|
icon={
|
|
@@ -58,7 +58,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
|
58
58
|
onAfterToggle={onAfterToggle}
|
|
59
59
|
/>
|
|
60
60
|
<LanguageToggle
|
|
61
|
-
|
|
61
|
+
editorViewRef={editorViewRef}
|
|
62
62
|
currentLanguageAdapter={currentLanguageAdapter}
|
|
63
63
|
canSwitchToLanguage={
|
|
64
64
|
canUseMarkdown && currentLanguageAdapter === "python"
|
|
@@ -75,7 +75,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
|
75
75
|
onAfterToggle={onAfterToggle}
|
|
76
76
|
/>
|
|
77
77
|
<LanguageToggle
|
|
78
|
-
|
|
78
|
+
editorViewRef={editorViewRef}
|
|
79
79
|
currentLanguageAdapter={currentLanguageAdapter}
|
|
80
80
|
canSwitchToLanguage={true}
|
|
81
81
|
icon={
|
|
@@ -95,7 +95,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
|
95
95
|
|
|
96
96
|
interface Props {
|
|
97
97
|
className?: string;
|
|
98
|
-
|
|
98
|
+
editorViewRef: React.RefObject<EditorView | null>;
|
|
99
99
|
canSwitchToLanguage: boolean;
|
|
100
100
|
currentLanguageAdapter: LanguageAdapter["type"] | undefined;
|
|
101
101
|
toType: LanguageAdapter["type"];
|
|
@@ -105,7 +105,7 @@ interface Props {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export const LanguageToggle: React.FC<Props> = ({
|
|
108
|
-
|
|
108
|
+
editorViewRef,
|
|
109
109
|
currentLanguageAdapter,
|
|
110
110
|
canSwitchToLanguage,
|
|
111
111
|
icon,
|
|
@@ -114,6 +114,7 @@ export const LanguageToggle: React.FC<Props> = ({
|
|
|
114
114
|
onAfterToggle,
|
|
115
115
|
}) => {
|
|
116
116
|
const handleClick = () => {
|
|
117
|
+
const editorView = editorViewRef.current;
|
|
117
118
|
if (!editorView) {
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import {
|
|
2
|
+
import { useDelayElapsed } from "@/hooks/useDelayElapsed";
|
|
3
3
|
|
|
4
4
|
const DELAY_MS = 200;
|
|
5
5
|
|
|
@@ -10,17 +10,7 @@ const DELAY_MS = 200;
|
|
|
10
10
|
export function useShouldShowInterrupt(running: boolean) {
|
|
11
11
|
// Start a timer when the run starts.
|
|
12
12
|
// After 200ms, show the interrupt button to avoid flickering.
|
|
13
|
-
const
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (!running) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
setHasRunLongEnough(false);
|
|
19
|
-
const timeout = setTimeout(() => {
|
|
20
|
-
setHasRunLongEnough(true);
|
|
21
|
-
}, DELAY_MS);
|
|
22
|
-
return () => clearTimeout(timeout);
|
|
23
|
-
}, [running]);
|
|
13
|
+
const hasRunLongEnough = useDelayElapsed(running ? DELAY_MS : 0);
|
|
24
14
|
|
|
25
15
|
return running && hasRunLongEnough;
|
|
26
16
|
}
|
|
@@ -18,7 +18,10 @@ import type {
|
|
|
18
18
|
DependencyTreeNode,
|
|
19
19
|
DependencyTreeResponse,
|
|
20
20
|
} from "@/core/network/types";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
invalidatePackageData,
|
|
23
|
+
withPackageInvalidation,
|
|
24
|
+
} from "@/core/packages/package-data";
|
|
22
25
|
import { useInstallPackages } from "@/core/packages/useInstallPackage";
|
|
23
26
|
import { sandboxAtom, sandboxSyncAtom } from "@/core/packages/sandbox-state";
|
|
24
27
|
import { store } from "@/core/state/jotai";
|
|
@@ -143,6 +146,32 @@ describe("PackagesPanel", () => {
|
|
|
143
146
|
expect(screen.queryByText("cycle")).not.toBeInTheDocument();
|
|
144
147
|
});
|
|
145
148
|
|
|
149
|
+
it("collapses expanded nodes when the dependency tree is refreshed", async () => {
|
|
150
|
+
const { client } = renderPanel(
|
|
151
|
+
{ kind: "sandbox", backend: "pixi" },
|
|
152
|
+
populatedTree,
|
|
153
|
+
);
|
|
154
|
+
fireEvent.click(await screen.findByRole("treeitem", { name: /polars/ }));
|
|
155
|
+
expect(screen.getByText("already in tree")).toBeInTheDocument();
|
|
156
|
+
|
|
157
|
+
vi.mocked(client.getDependencyTree).mockResolvedValue({
|
|
158
|
+
tree: structuredClone(populatedTree),
|
|
159
|
+
context: { kind: "sandbox", backend: "pixi" },
|
|
160
|
+
});
|
|
161
|
+
act(() => invalidatePackageData());
|
|
162
|
+
|
|
163
|
+
await waitFor(() =>
|
|
164
|
+
expect(client.getDependencyTree).toHaveBeenCalledTimes(2),
|
|
165
|
+
);
|
|
166
|
+
await waitFor(() =>
|
|
167
|
+
expect(screen.queryByText("already in tree")).not.toBeInTheDocument(),
|
|
168
|
+
);
|
|
169
|
+
expect(screen.getByRole("treeitem", { name: /polars/ })).toHaveAttribute(
|
|
170
|
+
"aria-expanded",
|
|
171
|
+
"false",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
146
175
|
it("keeps the configured package manager changeable outside a sandbox", async () => {
|
|
147
176
|
const { getPackageList } = renderPanel({
|
|
148
177
|
kind: "package-manager",
|