@marimo-team/islands 0.25.3-dev5 → 0.25.3-dev51
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-BiZWOrv4.js → ErrorBoundary-CbxCC2kk.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-C1Gjgokv.js} +1103 -1103
- 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-CW5_8ZOI.js → code-visibility-DoZ9lxzx.js} +6 -6
- package/dist/{common-mzWw4FdI.js → common-D1PacyXO.js} +3112 -3015
- 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-CJmehSdc.js → data-grid-overlay-editor-B50UpkXd.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-MG5hiCIa.js} +76 -76
- 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-BcT1FBud.js → feedback-button-DqPqwryB.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-XckatD0j.js → glide-data-editor-BxMhUd3K.js} +370 -368
- package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
- package/dist/{globals-jRrLCkrP.js → globals-DTJa_doJ.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-By72ALyI.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 +2358 -2221
- 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-pZx8dUDV.js → number-overlay-editor-Z_H6x5b-.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-D-OMW5sV.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-79ez8-D5.js → reveal-component-tAB5K3QE.js} +10 -10
- 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__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
- package/src/__tests__/host-imports.test.ts +1 -0
- package/src/__tests__/mount.test.ts +8 -8
- package/src/components/app-config/ai-config.tsx +5 -1
- 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 +417 -6
- 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 +263 -161
- 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/Output.tsx +5 -1
- package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
- package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
- package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
- 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/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/chrome/wrapper/__tests__/attachment-status.test.tsx +277 -78
- package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +196 -41
- 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 +139 -36
- package/src/components/editor/errors/send-error-report-button.tsx +10 -7
- package/src/components/editor/file-tree/file-viewer.tsx +4 -2
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -22
- package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
- 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/__tests__/toast.test.tsx +58 -0
- package/src/core/attachments/state.ts +5 -8
- package/src/core/attachments/toast.ts +15 -0
- package/src/core/cells/__tests__/error-report.test.ts +53 -3
- package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
- package/src/core/cells/__tests__/session.test.ts +18 -3
- package/src/core/cells/cells.ts +2 -1
- package/src/core/cells/error-report.ts +14 -6
- package/src/core/cells/pending-delete-service.ts +2 -6
- package/src/core/cells/session.ts +19 -1
- package/src/core/config/pair.ts +5 -3
- 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/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/useAsyncData.ts +3 -3
- package/src/hooks/useDataSourceDiscovery.ts +6 -4
- package/src/hooks/useElapsedTime.ts +3 -3
- package/src/hooks/useInputHistory.ts +4 -7
- package/src/hooks/useResizeObserver.ts +4 -7
- package/src/mount.tsx +3 -3
- package/src/plugins/core/registerReactComponent.tsx +1 -1
- package/src/plugins/impl/DropdownPlugin.tsx +2 -6
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
- 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/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
|
@@ -54,6 +54,7 @@ function renderExportActions(
|
|
|
54
54
|
props: Partial<ExportActionProps> = {},
|
|
55
55
|
sizeLimit: { limitBytes: number; unavailableMessage: string } | null = null,
|
|
56
56
|
mode: AppMode = "edit",
|
|
57
|
+
strictMode = false,
|
|
57
58
|
) {
|
|
58
59
|
const store = createStore();
|
|
59
60
|
store.set(downloadSizeLimitAtom, sizeLimit);
|
|
@@ -66,7 +67,7 @@ function renderExportActions(
|
|
|
66
67
|
</TooltipProvider>
|
|
67
68
|
</Provider>
|
|
68
69
|
);
|
|
69
|
-
const result = render(view(props));
|
|
70
|
+
const result = render(view(props), { reactStrictMode: strictMode });
|
|
70
71
|
return {
|
|
71
72
|
...result,
|
|
72
73
|
rerenderExportActions: (nextProps: Partial<ExportActionProps>) =>
|
|
@@ -284,7 +285,7 @@ describe("ExportActions dialog", () => {
|
|
|
284
285
|
await waitFor(() => {
|
|
285
286
|
expect(downloadByURL).toHaveBeenCalled();
|
|
286
287
|
});
|
|
287
|
-
expect(getExportMetadata).toHaveBeenCalledTimes(
|
|
288
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(3);
|
|
288
289
|
expect(
|
|
289
290
|
screen.queryByText("GeoParquet export requires pyarrow."),
|
|
290
291
|
).not.toBeInTheDocument();
|
|
@@ -310,10 +311,10 @@ describe("ExportActions dialog", () => {
|
|
|
310
311
|
mocks.handleInstallPackages.mock.calls[0][1]();
|
|
311
312
|
});
|
|
312
313
|
await waitFor(() => {
|
|
313
|
-
expect(getExportMetadata).toHaveBeenCalledTimes(
|
|
314
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(3);
|
|
314
315
|
});
|
|
315
316
|
expect(
|
|
316
|
-
screen.
|
|
317
|
+
await screen.findByRole("button", { name: "Install pyarrow" }),
|
|
317
318
|
).toBeInTheDocument();
|
|
318
319
|
expect(
|
|
319
320
|
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
@@ -530,6 +531,328 @@ describe("ExportActions dialog", () => {
|
|
|
530
531
|
expect(downloadByURL).not.toHaveBeenCalled();
|
|
531
532
|
});
|
|
532
533
|
|
|
534
|
+
async function startGeoParquetInstall(strictMode = false) {
|
|
535
|
+
const getExportMetadata = vi.fn().mockResolvedValue(geometryMetadata);
|
|
536
|
+
downloadAs.mockResolvedValueOnce({
|
|
537
|
+
url: "",
|
|
538
|
+
filename: "",
|
|
539
|
+
error: "GeoParquet export requires pyarrow.",
|
|
540
|
+
missing_packages: ["pyarrow"],
|
|
541
|
+
});
|
|
542
|
+
const { rerenderExportActions, unmount } = renderExportActions(
|
|
543
|
+
{ getExportMetadata, metadataSource: "first" },
|
|
544
|
+
null,
|
|
545
|
+
"edit",
|
|
546
|
+
strictMode,
|
|
547
|
+
);
|
|
548
|
+
const dialog = await openDialog();
|
|
549
|
+
fireEvent.click(
|
|
550
|
+
await screen.findByRole("button", { name: "Download GeoParquet" }),
|
|
551
|
+
);
|
|
552
|
+
fireEvent.click(
|
|
553
|
+
await screen.findByRole("button", { name: "Install pyarrow" }),
|
|
554
|
+
);
|
|
555
|
+
return { dialog, getExportMetadata, rerenderExportActions, unmount };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
it("does not retry with a geometry choice after the final geometry disappears", async () => {
|
|
559
|
+
const { getExportMetadata } = await startGeoParquetInstall();
|
|
560
|
+
getExportMetadata.mockResolvedValue({
|
|
561
|
+
geometry_columns: [],
|
|
562
|
+
primary_geometry_column: null,
|
|
563
|
+
default_geometry_column: null,
|
|
564
|
+
formats: {},
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
568
|
+
|
|
569
|
+
expect(downloadAs).toHaveBeenCalledTimes(1);
|
|
570
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
571
|
+
expect(
|
|
572
|
+
await screen.findByText(
|
|
573
|
+
"The selected geometry is no longer available. Choose another geometry to export.",
|
|
574
|
+
),
|
|
575
|
+
).toBeInTheDocument();
|
|
576
|
+
fireEvent.click(screen.getByRole("button", { name: "Download Parquet" }));
|
|
577
|
+
await waitFor(() => expect(downloadByURL).toHaveBeenCalledTimes(1));
|
|
578
|
+
expect(downloadAs).toHaveBeenLastCalledWith({ format: "parquet" });
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("does not export when installation finishes after the widget unmounts", async () => {
|
|
582
|
+
const { getExportMetadata, unmount } = await startGeoParquetInstall();
|
|
583
|
+
unmount();
|
|
584
|
+
|
|
585
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
586
|
+
|
|
587
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(1);
|
|
588
|
+
expect(downloadAs).toHaveBeenCalledTimes(1);
|
|
589
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("does not export when post-install metadata resolves after unmount", async () => {
|
|
593
|
+
const { getExportMetadata, unmount } = await startGeoParquetInstall();
|
|
594
|
+
let resolveCheck: (value: ExportMetadata) => void = () => undefined;
|
|
595
|
+
getExportMetadata.mockImplementationOnce(
|
|
596
|
+
() =>
|
|
597
|
+
new Promise<ExportMetadata>((resolve) => {
|
|
598
|
+
resolveCheck = resolve;
|
|
599
|
+
}),
|
|
600
|
+
);
|
|
601
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
602
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
603
|
+
unmount();
|
|
604
|
+
|
|
605
|
+
await act(async () => resolveCheck(geometryMetadata));
|
|
606
|
+
|
|
607
|
+
expect(downloadAs).toHaveBeenCalledTimes(1);
|
|
608
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("does not deliver a retried export that resolves after unmount", async () => {
|
|
612
|
+
const { unmount } = await startGeoParquetInstall();
|
|
613
|
+
let resolveDownload: (value: {
|
|
614
|
+
url: string;
|
|
615
|
+
filename: string;
|
|
616
|
+
}) => void = () => undefined;
|
|
617
|
+
downloadAs.mockImplementationOnce(
|
|
618
|
+
() =>
|
|
619
|
+
new Promise((resolve) => {
|
|
620
|
+
resolveDownload = resolve;
|
|
621
|
+
}),
|
|
622
|
+
);
|
|
623
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
624
|
+
expect(downloadAs).toHaveBeenCalledTimes(2);
|
|
625
|
+
unmount();
|
|
626
|
+
|
|
627
|
+
await act(async () =>
|
|
628
|
+
resolveDownload({
|
|
629
|
+
url: "https://example.test/export",
|
|
630
|
+
filename: "table",
|
|
631
|
+
}),
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("retries after installation under StrictMode effect replay", async () => {
|
|
638
|
+
await startGeoParquetInstall(true);
|
|
639
|
+
|
|
640
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
641
|
+
|
|
642
|
+
await waitFor(() => expect(downloadByURL).toHaveBeenCalledTimes(1));
|
|
643
|
+
expect(downloadAs).toHaveBeenLastCalledWith({
|
|
644
|
+
format: "parquet",
|
|
645
|
+
geometry_column: "location",
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("does not export when installation finishes after the dialog closes", async () => {
|
|
650
|
+
const { dialog, getExportMetadata } = await startGeoParquetInstall();
|
|
651
|
+
fireEvent.click(within(dialog).getByRole("button", { name: "Close" }));
|
|
652
|
+
await waitFor(() => {
|
|
653
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
657
|
+
|
|
658
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(1);
|
|
659
|
+
expect(downloadAs).toHaveBeenCalledTimes(1);
|
|
660
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("does not export when installation finishes after the source changes", async () => {
|
|
664
|
+
const { getExportMetadata, rerenderExportActions } =
|
|
665
|
+
await startGeoParquetInstall();
|
|
666
|
+
rerenderExportActions({ getExportMetadata, metadataSource: "second" });
|
|
667
|
+
await waitFor(() => {
|
|
668
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
672
|
+
|
|
673
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
674
|
+
expect(downloadAs).toHaveBeenCalledTimes(1);
|
|
675
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("does not export a non-geographic retry after the dialog closes", async () => {
|
|
679
|
+
downloadAs.mockResolvedValueOnce({
|
|
680
|
+
url: "",
|
|
681
|
+
filename: "",
|
|
682
|
+
error: "Parquet export requires pyarrow.",
|
|
683
|
+
missing_packages: ["pyarrow"],
|
|
684
|
+
});
|
|
685
|
+
renderExportActions();
|
|
686
|
+
const dialog = await openDialog();
|
|
687
|
+
fireEvent.click(screen.getByRole("button", { name: "Download Parquet" }));
|
|
688
|
+
fireEvent.click(
|
|
689
|
+
await screen.findByRole("button", { name: "Install pyarrow" }),
|
|
690
|
+
);
|
|
691
|
+
fireEvent.click(within(dialog).getByRole("button", { name: "Close" }));
|
|
692
|
+
|
|
693
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
694
|
+
|
|
695
|
+
expect(downloadAs).toHaveBeenCalledTimes(1);
|
|
696
|
+
expect(downloadByURL).not.toHaveBeenCalled();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("retries after installation when an earlier export succeeded", async () => {
|
|
700
|
+
const getExportMetadata = vi.fn().mockResolvedValue({
|
|
701
|
+
...geometryMetadata,
|
|
702
|
+
formats: {
|
|
703
|
+
parquet: {
|
|
704
|
+
available: false,
|
|
705
|
+
reason: "GeoParquet export requires pyarrow.",
|
|
706
|
+
missing_packages: ["pyarrow"],
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
renderExportActions({ getExportMetadata });
|
|
711
|
+
await openDialog();
|
|
712
|
+
fireEvent.click(screen.getByRole("button", { name: "Download CSV" }));
|
|
713
|
+
await waitFor(() => expect(downloadByURL).toHaveBeenCalledTimes(1));
|
|
714
|
+
fireEvent.click(
|
|
715
|
+
await screen.findByRole("button", { name: "Install pyarrow" }),
|
|
716
|
+
);
|
|
717
|
+
getExportMetadata.mockResolvedValue(geometryMetadata);
|
|
718
|
+
|
|
719
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
720
|
+
|
|
721
|
+
await waitFor(() => expect(downloadByURL).toHaveBeenCalledTimes(2));
|
|
722
|
+
expect(downloadAs).toHaveBeenLastCalledWith({
|
|
723
|
+
format: "parquet",
|
|
724
|
+
geometry_column: "location",
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it("waits for the post-install metadata check before exporting", async () => {
|
|
729
|
+
let resolveCheck: (value: ExportMetadata) => void = () => undefined;
|
|
730
|
+
const getExportMetadata = vi.fn().mockResolvedValue({
|
|
731
|
+
...geometryMetadata,
|
|
732
|
+
formats: {
|
|
733
|
+
parquet: {
|
|
734
|
+
available: false,
|
|
735
|
+
reason: "GeoParquet export requires pyarrow.",
|
|
736
|
+
missing_packages: ["pyarrow"],
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
renderExportActions({ getExportMetadata });
|
|
741
|
+
await openDialog();
|
|
742
|
+
fireEvent.click(
|
|
743
|
+
await screen.findByRole("button", { name: "Install pyarrow" }),
|
|
744
|
+
);
|
|
745
|
+
getExportMetadata.mockImplementationOnce(
|
|
746
|
+
() =>
|
|
747
|
+
new Promise((resolve) => {
|
|
748
|
+
resolveCheck = resolve;
|
|
749
|
+
}),
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
753
|
+
expect(downloadAs).not.toHaveBeenCalled();
|
|
754
|
+
|
|
755
|
+
getExportMetadata.mockResolvedValue(geometryMetadata);
|
|
756
|
+
await act(async () => resolveCheck(geometryMetadata));
|
|
757
|
+
await waitFor(() => expect(downloadByURL).toHaveBeenCalled());
|
|
758
|
+
expect(downloadAs).toHaveBeenCalledTimes(1);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("uses the new source eligibility after a settled package failure", async () => {
|
|
762
|
+
const getExportMetadata = vi.fn().mockResolvedValue(geometryMetadata);
|
|
763
|
+
downloadAs.mockResolvedValueOnce({
|
|
764
|
+
url: "",
|
|
765
|
+
filename: "",
|
|
766
|
+
error: "GeoParquet export requires pyarrow.",
|
|
767
|
+
missing_packages: ["pyarrow"],
|
|
768
|
+
});
|
|
769
|
+
const { rerenderExportActions } = renderExportActions({
|
|
770
|
+
getExportMetadata,
|
|
771
|
+
metadataSource: "first",
|
|
772
|
+
});
|
|
773
|
+
await openDialog();
|
|
774
|
+
fireEvent.click(
|
|
775
|
+
await screen.findByRole("button", { name: "Download GeoParquet" }),
|
|
776
|
+
);
|
|
777
|
+
await screen.findByRole("button", { name: "Install pyarrow" });
|
|
778
|
+
|
|
779
|
+
rerenderExportActions({ getExportMetadata, metadataSource: "second" });
|
|
780
|
+
|
|
781
|
+
await waitFor(() =>
|
|
782
|
+
expect(
|
|
783
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
784
|
+
).toBeEnabled(),
|
|
785
|
+
);
|
|
786
|
+
expect(
|
|
787
|
+
screen.queryByRole("button", { name: "Install pyarrow" }),
|
|
788
|
+
).not.toBeInTheDocument();
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("ignores metadata for an earlier source that resolves late", async () => {
|
|
792
|
+
let resolveFirst: (value: ExportMetadata) => void = () => undefined;
|
|
793
|
+
const first = vi.fn(
|
|
794
|
+
() =>
|
|
795
|
+
new Promise<ExportMetadata>((resolve) => {
|
|
796
|
+
resolveFirst = resolve;
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
799
|
+
const second = vi.fn().mockResolvedValue({
|
|
800
|
+
...geometryMetadata,
|
|
801
|
+
geometry_columns: [
|
|
802
|
+
{ name: "region", encoding: "objects", crs: "EPSG:3857" },
|
|
803
|
+
],
|
|
804
|
+
primary_geometry_column: "region",
|
|
805
|
+
default_geometry_column: "region",
|
|
806
|
+
});
|
|
807
|
+
const { rerenderExportActions } = renderExportActions({
|
|
808
|
+
getExportMetadata: first,
|
|
809
|
+
metadataSource: "first",
|
|
810
|
+
});
|
|
811
|
+
await openDialog();
|
|
812
|
+
rerenderExportActions({
|
|
813
|
+
getExportMetadata: second,
|
|
814
|
+
metadataSource: "second",
|
|
815
|
+
});
|
|
816
|
+
await waitFor(() =>
|
|
817
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
818
|
+
"region",
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
await act(async () => resolveFirst(geometryMetadata));
|
|
823
|
+
|
|
824
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
825
|
+
"region",
|
|
826
|
+
);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it("does not use earlier metadata when the new source request fails", async () => {
|
|
830
|
+
const getExportMetadata = vi.fn().mockResolvedValue(geometryMetadata);
|
|
831
|
+
const { rerenderExportActions } = renderExportActions({
|
|
832
|
+
getExportMetadata,
|
|
833
|
+
metadataSource: "first",
|
|
834
|
+
});
|
|
835
|
+
await openDialog();
|
|
836
|
+
await waitFor(() =>
|
|
837
|
+
expect(
|
|
838
|
+
screen.getByRole("button", { name: "Download GeoParquet" }),
|
|
839
|
+
).toBeEnabled(),
|
|
840
|
+
);
|
|
841
|
+
getExportMetadata.mockRejectedValueOnce(new Error("Connection lost"));
|
|
842
|
+
|
|
843
|
+
rerenderExportActions({ getExportMetadata, metadataSource: "second" });
|
|
844
|
+
|
|
845
|
+
expect(
|
|
846
|
+
await screen.findByText(
|
|
847
|
+
"Could not load geometry options: Connection lost",
|
|
848
|
+
),
|
|
849
|
+
).toBeInTheDocument();
|
|
850
|
+
expect(
|
|
851
|
+
screen.getByRole("button", { name: "Download Parquet" }),
|
|
852
|
+
).toBeDisabled();
|
|
853
|
+
expect(screen.getByRole("button", { name: "Download CSV" })).toBeEnabled();
|
|
854
|
+
});
|
|
855
|
+
|
|
533
856
|
it("shows formats in the approved order with an options toggle where supported", async () => {
|
|
534
857
|
renderExportActions();
|
|
535
858
|
const dialog = await openDialog();
|
|
@@ -591,6 +914,94 @@ describe("ExportActions dialog", () => {
|
|
|
591
914
|
expect(within(dialog).queryByRole("group")).not.toBeInTheDocument();
|
|
592
915
|
});
|
|
593
916
|
|
|
917
|
+
it("describes format options with their description and current settings", async () => {
|
|
918
|
+
renderExportActions({
|
|
919
|
+
getExportMetadata: vi.fn().mockResolvedValue(geometryMetadata),
|
|
920
|
+
});
|
|
921
|
+
await openDialog();
|
|
922
|
+
await screen.findByRole("button", { name: "GeoParquet options" });
|
|
923
|
+
|
|
924
|
+
for (const [label, description] of [
|
|
925
|
+
["CSV", "Comma-separated values Comma · UTF-8"],
|
|
926
|
+
["TSV", "Best for Excel and Google Sheets UTF-8"],
|
|
927
|
+
["JSON", "Raw JSON data Escapes non-ASCII"],
|
|
928
|
+
["GeoParquet", "Parquet with geometry and CRS metadata location"],
|
|
929
|
+
["GeoJSON", "Geometry in longitude and latitude location"],
|
|
930
|
+
]) {
|
|
931
|
+
expect(
|
|
932
|
+
screen.getByRole("button", { name: `${label} options` }),
|
|
933
|
+
).toHaveAccessibleDescription(description);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const csvOptions = screen.getByRole("button", { name: "CSV options" });
|
|
937
|
+
fireEvent.click(csvOptions);
|
|
938
|
+
fireEvent.change(screen.getByRole("combobox", { name: "Delimiter" }), {
|
|
939
|
+
target: { value: ";" },
|
|
940
|
+
});
|
|
941
|
+
expect(csvOptions).toHaveAccessibleDescription(
|
|
942
|
+
"Comma-separated values Semicolon · UTF-8",
|
|
943
|
+
);
|
|
944
|
+
fireEvent.click(csvOptions);
|
|
945
|
+
expect(csvOptions).toHaveAccessibleDescription(
|
|
946
|
+
"Comma-separated values Semicolon · UTF-8",
|
|
947
|
+
);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it.each([
|
|
951
|
+
["CSV", "Comma-separated values"],
|
|
952
|
+
["TSV", "Best for Excel and Google Sheets"],
|
|
953
|
+
["JSON", "Raw JSON data"],
|
|
954
|
+
["GeoParquet", "Parquet with geometry and CRS metadata"],
|
|
955
|
+
["GeoJSON", "Geometry in longitude and latitude"],
|
|
956
|
+
])(
|
|
957
|
+
"toggles %s options from the row label and description",
|
|
958
|
+
async (label, description) => {
|
|
959
|
+
renderExportActions({
|
|
960
|
+
getExportMetadata: vi.fn().mockResolvedValue(geometryMetadata),
|
|
961
|
+
});
|
|
962
|
+
await openDialog();
|
|
963
|
+
const toggle = await screen.findByRole("button", {
|
|
964
|
+
name: `${label} options`,
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
fireEvent.click(screen.getByText(label));
|
|
968
|
+
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
|
969
|
+
expect(
|
|
970
|
+
screen.getByRole("group", { name: `${label} options` }),
|
|
971
|
+
).toBeInTheDocument();
|
|
972
|
+
|
|
973
|
+
fireEvent.click(screen.getByText(description));
|
|
974
|
+
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
975
|
+
expect(
|
|
976
|
+
screen.queryByRole("group", { name: `${label} options` }),
|
|
977
|
+
).not.toBeInTheDocument();
|
|
978
|
+
},
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
it("keeps download and copy independent of row expansion", async () => {
|
|
982
|
+
vi.stubGlobal(
|
|
983
|
+
"fetch",
|
|
984
|
+
vi.fn().mockResolvedValue({
|
|
985
|
+
ok: true,
|
|
986
|
+
text: vi.fn().mockResolvedValue("name,value\n"),
|
|
987
|
+
}),
|
|
988
|
+
);
|
|
989
|
+
renderExportActions();
|
|
990
|
+
await openDialog();
|
|
991
|
+
const toggle = screen.getByRole("button", { name: "CSV options" });
|
|
992
|
+
|
|
993
|
+
fireEvent.click(screen.getByRole("button", { name: "Download CSV" }));
|
|
994
|
+
await waitFor(() => expect(downloadByURL).toHaveBeenCalled());
|
|
995
|
+
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
996
|
+
|
|
997
|
+
fireEvent.click(screen.getByText("CSV"));
|
|
998
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy CSV" }));
|
|
999
|
+
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
|
1000
|
+
await waitFor(() => {
|
|
1001
|
+
expect(copyToClipboard).toHaveBeenCalledWith("name,value\n");
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
|
|
594
1005
|
it("sends only changed CSV options with a download", async () => {
|
|
595
1006
|
renderExportActions();
|
|
596
1007
|
await openDialog();
|
|
@@ -1412,7 +1823,7 @@ describe("ExportActions GeoJSON", () => {
|
|
|
1412
1823
|
);
|
|
1413
1824
|
await act(async () => mocks.handleInstallPackages.mock.calls[0][1]());
|
|
1414
1825
|
await waitFor(() => expect(downloadByURL).toHaveBeenCalled());
|
|
1415
|
-
expect(getExportMetadata).toHaveBeenCalledTimes(
|
|
1826
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(3);
|
|
1416
1827
|
expect(downloadAs).toHaveBeenLastCalledWith({
|
|
1417
1828
|
format: "geojson",
|
|
1418
1829
|
geometry_column: "boundary",
|
|
@@ -1685,7 +2096,7 @@ describe("ExportActions GeoJSON clipboard", () => {
|
|
|
1685
2096
|
format: "geojson",
|
|
1686
2097
|
geometry_column: "boundary",
|
|
1687
2098
|
});
|
|
1688
|
-
expect(getExportMetadata).toHaveBeenCalledTimes(
|
|
2099
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(3);
|
|
1689
2100
|
expect(downloadByURL).not.toHaveBeenCalled();
|
|
1690
2101
|
});
|
|
1691
2102
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Table } from "@tanstack/react-table";
|
|
8
8
|
import { Columns3Icon, EyeIcon, EyeOffIcon, ScanEyeIcon } from "lucide-react";
|
|
9
|
-
import React
|
|
9
|
+
import React from "react";
|
|
10
10
|
import { ColumnName } from "@/components/datasources/components";
|
|
11
11
|
import { Button } from "@/components/ui/button";
|
|
12
12
|
import {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "@/components/ui/popover";
|
|
25
25
|
import { type BulkAction, useSelectList } from "@/components/ui/select-core";
|
|
26
26
|
import type { DataType } from "@/core/kernel/messages";
|
|
27
|
+
import { Arrays } from "@/utils/arrays";
|
|
27
28
|
import { cn } from "@/utils/cn";
|
|
28
29
|
import { Events } from "@/utils/events";
|
|
29
30
|
import { smartMatchFilter } from "@/utils/smartMatch";
|
|
@@ -67,9 +68,8 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
67
68
|
const hideableIds = userColumns
|
|
68
69
|
.filter((column) => column.getCanHide())
|
|
69
70
|
.map((column) => column.id);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
).length;
|
|
71
|
+
const visibleHideableColumnIds = Arrays.difference(hideableIds, hiddenIds);
|
|
72
|
+
const visibleHideableCount = visibleHideableColumnIds.length;
|
|
73
73
|
|
|
74
74
|
const applyHidden = (next: string[] | string | null) => {
|
|
75
75
|
const hidden = new Set(Array.isArray(next) ? next : []);
|
|
@@ -106,15 +106,10 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
106
106
|
.filter((option) => !option.disabled)
|
|
107
107
|
.map((option) => option.value)
|
|
108
108
|
: [];
|
|
109
|
-
const showOnlyColumnState =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
),
|
|
114
|
-
hideableColumnIdSet: new Set(hideableIds),
|
|
115
|
-
}),
|
|
116
|
-
[hideableIds, hiddenIds],
|
|
117
|
-
);
|
|
109
|
+
const showOnlyColumnState = {
|
|
110
|
+
visibleHideableColumnIds,
|
|
111
|
+
hideableColumnIdSet: new Set(hideableIds),
|
|
112
|
+
};
|
|
118
113
|
const hasSearchBulkActions =
|
|
119
114
|
showOnlyMatchIds.length > 0 || matchingActions.length > 0;
|
|
120
115
|
|
|
@@ -135,6 +135,7 @@ export function generateColumns<T>({
|
|
|
135
135
|
}): ColumnDef<T>[] {
|
|
136
136
|
// Row-headers are typically index columns
|
|
137
137
|
const rowHeadersSet = new Set(rowHeaders.map(([columnName]) => columnName));
|
|
138
|
+
const wrappedColumnsSet = new Set(wrappedColumns);
|
|
138
139
|
|
|
139
140
|
const typesByColumn = Maps.keyBy(fieldTypes, (entry) => entry[0]);
|
|
140
141
|
|
|
@@ -287,7 +288,7 @@ export function generateColumns<T>({
|
|
|
287
288
|
}
|
|
288
289
|
|
|
289
290
|
const justify = getJustify(key);
|
|
290
|
-
const wrapped =
|
|
291
|
+
const wrapped = wrappedColumnsSet.has(key);
|
|
291
292
|
const isCellSelected = cell?.getIsSelected?.() || false;
|
|
292
293
|
const canSelectCell =
|
|
293
294
|
(selection === "single-cell" || selection === "multi-cell") &&
|
|
@@ -12,7 +12,7 @@ import { Logger } from "@/utils/Logger";
|
|
|
12
12
|
import type { Cell } from "@tanstack/react-table";
|
|
13
13
|
import { useAtomValue } from "jotai";
|
|
14
14
|
import { CopyIcon, FilterIcon, SquareStack } from "lucide-react";
|
|
15
|
-
import type
|
|
15
|
+
import { type RefObject, useRef, useState } from "react";
|
|
16
16
|
import useEvent from "react-use-event-hook";
|
|
17
17
|
import {
|
|
18
18
|
ContextMenu,
|
|
@@ -123,8 +123,21 @@ export const DataTableContextMenu = <TData,>({
|
|
|
123
123
|
tableRef: RefObject<HTMLTableSectionElement | null>;
|
|
124
124
|
copyAllCells: () => void;
|
|
125
125
|
}) => {
|
|
126
|
+
const [activeCell, setActiveCell] = useState<Cell<TData, unknown> | null>(
|
|
127
|
+
null,
|
|
128
|
+
);
|
|
129
|
+
const highlightedCell = useRef<Element | null>(null);
|
|
126
130
|
const handleContextMenuChange = useEvent((open: boolean) => {
|
|
131
|
+
highlightedCell.current?.classList.remove("bg-(--green-4)");
|
|
132
|
+
highlightedCell.current = null;
|
|
133
|
+
if (!open) {
|
|
134
|
+
// Forget the cell so a later right-click outside any cell can't reuse it
|
|
135
|
+
contextMenuRef.current = null;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
127
139
|
const cell = contextMenuRef.current;
|
|
140
|
+
setActiveCell(cell);
|
|
128
141
|
if (!cell) {
|
|
129
142
|
return;
|
|
130
143
|
}
|
|
@@ -138,42 +151,45 @@ export const DataTableContextMenu = <TData,>({
|
|
|
138
151
|
return;
|
|
139
152
|
}
|
|
140
153
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} else {
|
|
144
|
-
cellElement.classList.remove("bg-(--green-4)");
|
|
145
|
-
}
|
|
154
|
+
cellElement.classList.add("bg-(--green-4)");
|
|
155
|
+
highlightedCell.current = cellElement;
|
|
146
156
|
});
|
|
147
157
|
|
|
148
158
|
return (
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
// Open via the trigger so a right-click while already open retargets the
|
|
160
|
+
// menu; Radix only reports open changes, so use it for closing.
|
|
161
|
+
<ContextMenu
|
|
162
|
+
onOpenChange={(open) => {
|
|
163
|
+
if (!open) {
|
|
164
|
+
handleContextMenuChange(false);
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<ContextMenuTrigger
|
|
169
|
+
asChild={true}
|
|
170
|
+
onContextMenu={() => handleContextMenuChange(true)}
|
|
171
|
+
>
|
|
172
|
+
{tableBody}
|
|
173
|
+
</ContextMenuTrigger>
|
|
151
174
|
<ContextMenuPortal>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
/>
|
|
175
|
+
{activeCell && (
|
|
176
|
+
<CellContextMenu cell={activeCell} copySelectedCells={copyAllCells} />
|
|
177
|
+
)}
|
|
156
178
|
</ContextMenuPortal>
|
|
157
179
|
</ContextMenu>
|
|
158
180
|
);
|
|
159
181
|
};
|
|
160
182
|
|
|
161
183
|
export const CellContextMenu = <TData,>({
|
|
162
|
-
|
|
184
|
+
cell,
|
|
163
185
|
copySelectedCells,
|
|
164
186
|
}: {
|
|
165
|
-
|
|
187
|
+
cell: Cell<TData, unknown>;
|
|
166
188
|
copySelectedCells: () => void;
|
|
167
189
|
}) => {
|
|
168
190
|
const selectedCells = useAtomValue(selectedCellsAtom);
|
|
169
191
|
const multipleSelectedCells = selectedCells.size > 1;
|
|
170
192
|
|
|
171
|
-
const cell = cellRef.current;
|
|
172
|
-
if (!cell) {
|
|
173
|
-
Logger.error("No cell found in context menu");
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
193
|
const table = cell.getContext().table;
|
|
178
194
|
const displayedValue = cell.getValue();
|
|
179
195
|
const rawValue =
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import React from "react";
|
|
2
|
+
import React, { useMemo } from "react";
|
|
3
3
|
import { useDateFormatter, useLocale } from "react-aria";
|
|
4
4
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
5
5
|
|
|
@@ -45,6 +45,7 @@ export const DatePopover: React.FC<DatePopoverProps> = ({
|
|
|
45
45
|
|
|
46
46
|
const TimezoneDisplay = ({ date }: { date: Date }) => {
|
|
47
47
|
const { locale } = useLocale();
|
|
48
|
+
// The system timezone can change without the locale changing.
|
|
48
49
|
const localTimezone = new Intl.DateTimeFormat(locale).resolvedOptions()
|
|
49
50
|
.timeZone;
|
|
50
51
|
const hasSubSeconds = date.getUTCMilliseconds() !== 0;
|
|
@@ -116,9 +117,10 @@ const RelativeTime = ({ date }: { date: Date }) => {
|
|
|
116
117
|
const { locale } = useLocale();
|
|
117
118
|
|
|
118
119
|
// Initialize relative time formatter with current locale and "auto" numeric style
|
|
119
|
-
const relativeTimeFormatter =
|
|
120
|
-
numeric: "auto",
|
|
121
|
-
|
|
120
|
+
const relativeTimeFormatter = useMemo(
|
|
121
|
+
() => new Intl.RelativeTimeFormat(locale, { numeric: "auto" }),
|
|
122
|
+
[locale],
|
|
123
|
+
);
|
|
122
124
|
|
|
123
125
|
const currentTime = new Date();
|
|
124
126
|
// Get difference in seconds between now and the input date
|