@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.
Files changed (220) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
  2. package/dist/{ErrorBoundary-BiZWOrv4.js → ErrorBoundary-CbxCC2kk.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
  4. package/dist/any-language-editor-k2B8YI3Y.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
  8. package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
  9. package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C1Gjgokv.js} +1103 -1103
  10. package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
  15. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  16. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
  17. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
  18. package/dist/{code-visibility-CW5_8ZOI.js → code-visibility-DoZ9lxzx.js} +6 -6
  19. package/dist/{common-mzWw4FdI.js → common-D1PacyXO.js} +3112 -3015
  20. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
  21. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-CJmehSdc.js → data-grid-overlay-editor-B50UpkXd.js} +5 -5
  24. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  25. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
  26. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
  27. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
  28. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
  29. package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
  30. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  31. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-MG5hiCIa.js} +76 -76
  32. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
  33. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
  34. package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
  35. package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
  36. package/dist/{feedback-button-BcT1FBud.js → feedback-button-DqPqwryB.js} +29 -29
  37. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
  38. package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
  39. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  40. package/dist/{glide-data-editor-XckatD0j.js → glide-data-editor-BxMhUd3K.js} +370 -368
  41. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
  42. package/dist/{globals-jRrLCkrP.js → globals-DTJa_doJ.js} +3 -3
  43. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  44. package/dist/{html-to-image-B8s5SgGm.js → html-to-image-By72ALyI.js} +29 -29
  45. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
  46. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
  47. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
  48. package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
  49. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  50. package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
  51. package/dist/main.js +2358 -2221
  52. package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
  53. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  54. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
  55. package/dist/{number-overlay-editor-pZx8dUDV.js → number-overlay-editor-Z_H6x5b-.js} +3 -3
  56. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  57. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  58. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
  59. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
  60. package/dist/precisionRound-DhAzeRcm.js +49 -0
  61. package/dist/{process-output-CKRXyszb.js → process-output-D-OMW5sV.js} +4 -4
  62. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  63. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
  64. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  65. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
  66. package/dist/{reveal-component-79ez8-D5.js → reveal-component-tAB5K3QE.js} +10 -10
  67. package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
  68. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
  69. package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
  70. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
  71. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
  72. package/dist/style.css +1 -1
  73. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
  74. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
  75. package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
  76. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  77. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
  78. package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
  79. package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
  80. package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
  81. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
  82. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
  83. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
  84. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
  85. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
  86. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
  87. package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
  88. package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
  89. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  90. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  91. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
  92. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
  93. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
  94. package/package.json +4 -4
  95. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  96. package/src/__tests__/host-imports.test.ts +1 -0
  97. package/src/__tests__/mount.test.ts +8 -8
  98. package/src/components/app-config/ai-config.tsx +5 -1
  99. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  100. package/src/components/chat/chat-panel.tsx +3 -7
  101. package/src/components/chat/chat-utils.ts +4 -8
  102. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  103. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  104. package/src/components/data-table/__tests__/export-actions.test.tsx +417 -6
  105. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  106. package/src/components/data-table/columns.tsx +2 -1
  107. package/src/components/data-table/context-menu.tsx +36 -20
  108. package/src/components/data-table/date-popover.tsx +6 -4
  109. package/src/components/data-table/export-actions.tsx +263 -161
  110. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  111. package/src/components/data-table/schemas.ts +14 -36
  112. package/src/components/editor/Output.tsx +5 -1
  113. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  114. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  115. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
  116. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  117. package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
  118. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  119. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  120. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  121. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  122. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  123. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +277 -78
  124. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +196 -41
  125. package/src/components/editor/connections/components.tsx +1 -1
  126. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  127. package/src/components/editor/controls/command-palette.tsx +22 -10
  128. package/src/components/editor/dynamic-favicon.tsx +21 -40
  129. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +139 -36
  130. package/src/components/editor/errors/send-error-report-button.tsx +10 -7
  131. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  132. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -22
  133. package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
  134. package/src/components/pages/home-page.tsx +4 -4
  135. package/src/components/slides/slide-cell-view.tsx +1 -1
  136. package/src/components/tracing/tracing.tsx +2 -2
  137. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  138. package/src/core/attachments/state.ts +5 -8
  139. package/src/core/attachments/toast.ts +15 -0
  140. package/src/core/cells/__tests__/error-report.test.ts +53 -3
  141. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  142. package/src/core/cells/__tests__/session.test.ts +18 -3
  143. package/src/core/cells/cells.ts +2 -1
  144. package/src/core/cells/error-report.ts +14 -6
  145. package/src/core/cells/pending-delete-service.ts +2 -6
  146. package/src/core/cells/session.ts +19 -1
  147. package/src/core/config/pair.ts +5 -3
  148. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  149. package/src/core/datasets/data-source-discovery.ts +4 -6
  150. package/src/core/edit-app.tsx +2 -2
  151. package/src/core/host/AGENTS.md +11 -2
  152. package/src/core/host/__tests__/attachments.test.ts +56 -0
  153. package/src/core/host/__tests__/document.test.ts +126 -0
  154. package/src/core/host/__tests__/events.test.ts +49 -0
  155. package/src/core/host/__tests__/fixtures.ts +41 -0
  156. package/src/core/host/__tests__/notebook-client.test.ts +496 -0
  157. package/src/core/host/connection.ts +81 -0
  158. package/src/core/host/events.ts +68 -0
  159. package/src/core/host/notebook-client.ts +316 -0
  160. package/src/core/host/resources/attachments.ts +19 -0
  161. package/src/core/host/resources/document.ts +101 -0
  162. package/src/core/hotkeys/actions.ts +2 -4
  163. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  164. package/src/core/islands/bootstrap.ts +0 -1
  165. package/src/core/network/connection.ts +4 -2
  166. package/src/core/run-app.tsx +2 -2
  167. package/src/core/runtime/__tests__/runtime.test.ts +5 -12
  168. package/src/core/runtime/runtime.ts +5 -5
  169. package/src/core/static/files.ts +2 -0
  170. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  171. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  172. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
  173. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
  174. package/src/core/websocket/connection-state.ts +70 -0
  175. package/src/core/websocket/state.ts +7 -0
  176. package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
  177. package/src/core/websocket/transports/create.ts +31 -0
  178. package/src/core/websocket/transports/sse.ts +14 -2
  179. package/src/core/websocket/transports/transport.ts +12 -23
  180. package/src/core/websocket/types.ts +9 -39
  181. package/src/core/websocket/useNotebookClient.ts +78 -0
  182. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
  183. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  184. package/src/hooks/useAsyncData.ts +3 -3
  185. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  186. package/src/hooks/useElapsedTime.ts +3 -3
  187. package/src/hooks/useInputHistory.ts +4 -7
  188. package/src/hooks/useResizeObserver.ts +4 -7
  189. package/src/mount.tsx +3 -3
  190. package/src/plugins/core/registerReactComponent.tsx +1 -1
  191. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  192. package/src/plugins/impl/FormPlugin.tsx +1 -1
  193. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
  194. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  195. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  196. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  197. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  198. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  199. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  200. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  201. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  202. package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
  203. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  204. package/src/plugins/impl/panel/utils.ts +17 -0
  205. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  206. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  207. package/src/stories/editor.stories.tsx +24 -17
  208. package/src/utils/__tests__/arrays.test.ts +35 -0
  209. package/src/utils/__tests__/numbers.test.ts +48 -1
  210. package/src/utils/__tests__/objects.test.ts +21 -0
  211. package/src/utils/arrays.ts +10 -0
  212. package/src/utils/numbers.ts +28 -17
  213. package/src/utils/objects.ts +3 -7
  214. package/src/utils/once.ts +19 -19
  215. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  216. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  217. package/dist/precisionRound-DMIVQrt2.js +0 -97
  218. package/src/core/host/__tests__/protocol.test.ts +0 -214
  219. package/src/core/host/protocol.ts +0 -1943
  220. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -14,6 +14,7 @@ import {
14
14
  TableIcon,
15
15
  } from "lucide-react";
16
16
  import React from "react";
17
+ import useEvent from "react-use-event-hook";
17
18
  import { downloadSizeLimitAtom } from "./download-policy/atoms";
18
19
  import type {
19
20
  DownloadAsArgs,
@@ -22,6 +23,7 @@ import type {
22
23
  ExportMetadata,
23
24
  GetExportMetadata,
24
25
  } from "./schemas";
26
+ import { useAsyncData } from "@/hooks/useAsyncData";
25
27
  import { logNever } from "@/utils/assertNever";
26
28
  import { cn } from "@/utils/cn";
27
29
  import { copyToClipboard } from "@/utils/copy";
@@ -274,19 +276,130 @@ const failureTitleForAction = (action: ExportAction): string =>
274
276
  const failureDescription = (error: unknown): string =>
275
277
  typeof error === "string" ? error : prettyError(error);
276
278
 
279
+ const OBSOLETE_GEOMETRY_MESSAGE =
280
+ "The selected geometry is no longer available. Choose another geometry to export.";
281
+
282
+ type GeographicBlocker =
283
+ | "format-unavailable"
284
+ | "geometry-required"
285
+ | "choose-geometry"
286
+ | "obsolete-geometry"
287
+ | "missing-crs";
288
+
289
+ /**
290
+ * Find the first reason that a geographic export cannot start with the
291
+ * selected geometry. The dialog uses it to disable a format, and the retry
292
+ * after a package installation uses it to decide whether to export.
293
+ */
294
+ function geographicBlocker(
295
+ metadata: ExportMetadata,
296
+ format: GeographicFormat,
297
+ geometryColumn: string | null,
298
+ ): GeographicBlocker | null {
299
+ if (metadata.formats[format]?.available === false) {
300
+ return "format-unavailable";
301
+ }
302
+ const selected = metadata.geometry_columns.find(
303
+ (column) => column.name === geometryColumn,
304
+ );
305
+ if (geometryColumn !== null && !selected) {
306
+ return "obsolete-geometry";
307
+ }
308
+ if (metadata.geometry_columns.length === 0) {
309
+ return format === "geojson" ? "geometry-required" : null;
310
+ }
311
+ if (!selected) {
312
+ return "choose-geometry";
313
+ }
314
+ if (format === "geojson" && !selected.crs) {
315
+ return "missing-crs";
316
+ }
317
+ return null;
318
+ }
319
+
320
+ const geometryDisplayName = (name: string | null): string | null =>
321
+ name === "" ? "(unnamed geometry)" : name;
322
+
323
+ function geographicBlockerReason(
324
+ blocker: GeographicBlocker | null,
325
+ formatLabel: string,
326
+ geometryColumn: string | null,
327
+ ): string | null {
328
+ switch (blocker) {
329
+ case "choose-geometry":
330
+ return `Choose a primary geometry column to export ${formatLabel}.`;
331
+ case "obsolete-geometry":
332
+ return OBSOLETE_GEOMETRY_MESSAGE;
333
+ case "missing-crs":
334
+ return `Geometry column ${geometryDisplayName(geometryColumn)} needs a CRS for GeoJSON export. Declare its source CRS in Python before exporting.`;
335
+ case "format-unavailable":
336
+ case "geometry-required":
337
+ case null:
338
+ return null;
339
+ default:
340
+ logNever(blocker);
341
+ return null;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * One open dialog for one table source. Closing the dialog or replacing the
347
+ * source creates a new context, and work started under an older context must
348
+ * not change the dialog or start an export.
349
+ */
350
+ interface ExportContext {
351
+ source: unknown;
352
+ }
353
+
277
354
  export const ExportActions: React.FC<ExportActionProps> = (props) => {
278
355
  const { getExportMetadata, metadataSource } = props;
279
356
  const [exportDialogOpen, setExportDialogOpen] = React.useState(false);
280
357
  const [failure, setFailure] = React.useState<ExportFailure | null>(null);
281
358
  const [settings, setSettings] =
282
359
  React.useState<ExportSettings>(DEFAULT_SETTINGS);
283
- const [metadata, setMetadata] = React.useState<ExportMetadata | null>(null);
284
- const [metadataLoading, setMetadataLoading] = React.useState(false);
285
- const [metadataError, setMetadataError] = React.useState<string | null>(null);
286
- const [metadataRefresh, setMetadataRefresh] = React.useState(0);
360
+ const [exportContext, setExportContext] = React.useState<ExportContext>(
361
+ () => ({ source: metadataSource }),
362
+ );
363
+ if (exportContext.source !== metadataSource) {
364
+ setExportContext({ source: metadataSource });
365
+ }
366
+ const mounted = React.useRef(false);
367
+ const isCurrentContext = useEvent(
368
+ (context: ExportContext) => mounted.current && context === exportContext,
369
+ );
370
+ const metadataRequest = useAsyncData(async () => {
371
+ if (!exportDialogOpen || !getExportMetadata) {
372
+ return null;
373
+ }
374
+ return { context: exportContext, metadata: await getExportMetadata({}) };
375
+ }, [exportDialogOpen, getExportMetadata, exportContext]);
376
+ // The hook keeps earlier data while it reloads and after a failure. Only a
377
+ // completed response for the current context can enable an export.
378
+ const metadata =
379
+ metadataRequest.status === "success" &&
380
+ metadataRequest.data?.context === exportContext
381
+ ? metadataRequest.data.metadata
382
+ : null;
383
+ const metadataError =
384
+ metadataRequest.status === "error"
385
+ ? failureDescription(metadataRequest.error)
386
+ : null;
287
387
  const [geometryColumn, setGeometryColumn] = React.useState<string | null>(
288
388
  null,
289
389
  );
390
+ const [reconciledMetadata, setReconciledMetadata] =
391
+ React.useState<ExportMetadata | null>(null);
392
+ if (metadata !== reconciledMetadata) {
393
+ setReconciledMetadata(metadata);
394
+ if (metadata) {
395
+ setGeometryColumn((current) =>
396
+ current !== null &&
397
+ metadata.geometry_columns.some((column) => column.name === current)
398
+ ? current
399
+ : metadata.default_geometry_column,
400
+ );
401
+ }
402
+ }
290
403
  const hasGeometry = !!metadata?.geometry_columns.length;
291
404
  const [expandedFormat, setExpandedFormat] = React.useState<
292
405
  ConfigurableFormat | GeographicFormat | null
@@ -294,43 +407,17 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
294
407
  const triggerRef = React.useRef<HTMLButtonElement>(null);
295
408
  const dialogRef = React.useRef<HTMLDivElement>(null);
296
409
  const latestActionId = React.useRef(0);
410
+ React.useLayoutEffect(() => {
411
+ mounted.current = true;
412
+ return () => {
413
+ mounted.current = false;
414
+ latestActionId.current += 1;
415
+ };
416
+ }, []);
297
417
  React.useEffect(() => {
298
418
  latestActionId.current += 1;
419
+ setFailure(null);
299
420
  }, [metadataSource]);
300
- React.useEffect(() => {
301
- if (!exportDialogOpen || !getExportMetadata) {
302
- return;
303
- }
304
- let active = true;
305
- setMetadata(null);
306
- setMetadataLoading(true);
307
- setMetadataError(null);
308
- void getExportMetadata({}).then(
309
- (result) => {
310
- if (!active) {
311
- return;
312
- }
313
- setMetadata(result);
314
- setGeometryColumn((current) =>
315
- current !== null &&
316
- result.geometry_columns.some((column) => column.name === current)
317
- ? current
318
- : result.default_geometry_column,
319
- );
320
- setMetadataLoading(false);
321
- },
322
- (error: unknown) => {
323
- if (!active) {
324
- return;
325
- }
326
- setMetadataError(failureDescription(error));
327
- setMetadataLoading(false);
328
- },
329
- );
330
- return () => {
331
- active = false;
332
- };
333
- }, [exportDialogOpen, getExportMetadata, metadataSource, metadataRefresh]);
334
421
  const policy = useAtomValue(downloadSizeLimitAtom);
335
422
  const overLimit = !!(
336
423
  policy &&
@@ -368,9 +455,8 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
368
455
  setFailure(null);
369
456
  setExpandedFormat(null);
370
457
  setSettings(DEFAULT_SETTINGS);
371
- setMetadata(null);
372
- setMetadataError(null);
373
458
  setGeometryColumn(null);
459
+ setExportContext({ source: metadataSource });
374
460
  }
375
461
  };
376
462
 
@@ -553,57 +639,67 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
553
639
  }
554
640
  };
555
641
 
556
- const retryAfterInstall = async (action: ExportAction) => {
557
- if (isGeographic(action.format) && getExportMetadata) {
642
+ // Package installation can finish after the dialog closes or the source
643
+ // changes, so the retry carries the context in which installation began.
644
+ const retryAfterInstall = useEvent(
645
+ async (action: ExportAction, context: ExportContext) => {
646
+ if (!isCurrentContext(context)) {
647
+ return;
648
+ }
649
+ if (!isGeographic(action.format) || !getExportMetadata) {
650
+ retryAction(action);
651
+ return;
652
+ }
558
653
  const actionId = latestActionId.current;
654
+ const isCurrent = () =>
655
+ isCurrentContext(context) && actionId === latestActionId.current;
559
656
  let result: ExportMetadata;
560
657
  try {
561
658
  result = await getExportMetadata({});
562
- } catch (error) {
563
- if (actionId === latestActionId.current) {
564
- setMetadataError(failureDescription(error));
659
+ } catch {
660
+ if (isCurrent()) {
661
+ metadataRequest.refetch();
565
662
  }
566
663
  return;
567
664
  }
568
- if (actionId !== latestActionId.current) {
569
- return;
570
- }
571
- setMetadata(result);
572
- setMetadataError(null);
573
- if (result.formats[action.format]?.available === false) {
574
- setFailure(null);
665
+ if (!isCurrent()) {
575
666
  return;
576
667
  }
577
- if (
578
- geometryColumn !== null &&
579
- !result.geometry_columns.some(
580
- (column) => column.name === geometryColumn,
581
- )
582
- ) {
583
- setGeometryColumn(result.default_geometry_column);
584
- setActionFailure(actionId, {
585
- kind: "error",
586
- title: failureTitleForAction(action),
587
- description:
588
- "The selected geometry is no longer available. Choose another geometry to export.",
589
- action,
590
- });
591
- return;
592
- }
593
- if (result.geometry_columns.length > 0 && geometryColumn === null) {
594
- return;
595
- }
596
- if (
597
- action.format === "geojson" &&
598
- !result.geometry_columns.find(
599
- (column) => column.name === geometryColumn,
600
- )?.crs
601
- ) {
602
- setFailure(null);
603
- return;
668
+ // Validation reads its own response, so a display refresh cannot
669
+ // start an export that this check rejected.
670
+ metadataRequest.refetch();
671
+ const blocker = geographicBlocker(result, action.format, geometryColumn);
672
+ switch (blocker) {
673
+ case null:
674
+ retryAction(action);
675
+ return;
676
+ case "obsolete-geometry":
677
+ setGeometryColumn(result.default_geometry_column);
678
+ setActionFailure(actionId, {
679
+ kind: "error",
680
+ title: failureTitleForAction(action),
681
+ description: OBSOLETE_GEOMETRY_MESSAGE,
682
+ action,
683
+ });
684
+ return;
685
+ case "choose-geometry":
686
+ return;
687
+ case "format-unavailable":
688
+ case "geometry-required":
689
+ case "missing-crs":
690
+ setFailure(null);
691
+ return;
692
+ default:
693
+ logNever(blocker);
604
694
  }
605
- }
606
- retryAction(action);
695
+ },
696
+ );
697
+
698
+ const createInstallRetry = (action: ExportAction) => {
699
+ const context = exportContext;
700
+ return () => {
701
+ void retryAfterInstall(action, context);
702
+ };
607
703
  };
608
704
 
609
705
  const toggleExpanded = (format: ExportFormat) => {
@@ -665,14 +761,15 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
665
761
  const expanded =
666
762
  configurableFormat !== null &&
667
763
  expandedFormat === configurableFormat;
764
+ const FormatHeader = configurableFormat ? "button" : "div";
668
765
  const panelId = `export-options-${option.format}`;
766
+ const descriptionId = `${panelId}-description`;
767
+ const summaryId = `${panelId}-summary`;
669
768
  const rowFailure =
670
769
  failure?.action.format === option.format ? failure : null;
671
770
  const summary =
672
771
  isGeographic(option.format) && hasGeometry
673
- ? geometryColumn === ""
674
- ? "(unnamed geometry)"
675
- : (geometryColumn ?? "Choose geometry")
772
+ ? (geometryDisplayName(geometryColumn) ?? "Choose geometry")
676
773
  : settingsSummary(settings, option.format);
677
774
  const selectedGeometryIndex =
678
775
  metadata?.geometry_columns.findIndex(
@@ -681,20 +778,14 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
681
778
  const eligibility = isGeographic(option.format)
682
779
  ? metadata?.formats[option.format]
683
780
  : undefined;
684
- const selectedGeometry =
685
- metadata?.geometry_columns[selectedGeometryIndex];
686
- const missingCrs =
687
- option.format === "geojson" &&
688
- selectedGeometry !== undefined &&
689
- !selectedGeometry.crs;
781
+ const blocker =
782
+ isGeographic(option.format) && metadata
783
+ ? geographicBlocker(metadata, option.format, geometryColumn)
784
+ : null;
690
785
  const geometryReason = metadataError
691
786
  ? `Could not load geometry options: ${metadataError}`
692
787
  : (eligibility?.reason ??
693
- (hasGeometry && geometryColumn === null
694
- ? `Choose a primary geometry column to export ${optionLabel}.`
695
- : missingCrs
696
- ? `Geometry column ${geometryColumn === "" ? "(unnamed geometry)" : geometryColumn} needs a CRS for GeoJSON export. Declare its source CRS in Python before exporting.`
697
- : null));
788
+ geographicBlockerReason(blocker, optionLabel, geometryColumn));
698
789
  const missingPackageFailure =
699
790
  rowFailure?.kind === "missing-packages" ? rowFailure : null;
700
791
  const missingPackages =
@@ -704,13 +795,7 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
704
795
  : []);
705
796
  const geographicUnavailable =
706
797
  isGeographic(option.format) &&
707
- ((!!getExportMetadata && !metadata) ||
708
- metadataLoading ||
709
- !!metadataError ||
710
- eligibility?.available === false ||
711
- missingCrs ||
712
- (!!metadata?.geometry_columns.length &&
713
- geometryColumn === null));
798
+ ((!!getExportMetadata && !metadata) || blocker !== null);
714
799
  const copyUnavailable =
715
800
  isGeographic(option.format) &&
716
801
  (geographicUnavailable || missingPackages.length > 0);
@@ -720,55 +805,78 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
720
805
  data-testid={`export-row-${option.format}`}
721
806
  className={cn(expanded && "bg-muted/40")}
722
807
  >
723
- <div className="grid min-h-[60px] grid-cols-[28px_minmax(0,1fr)_auto] items-center gap-x-1 px-2.5 py-2 hover:bg-accent/50">
724
- {configurableFormat ? (
725
- <Button
726
- type="button"
727
- variant="ghost"
728
- size="icon"
729
- aria-label={`${optionLabel} options`}
730
- aria-expanded={expanded}
731
- aria-controls={panelId}
732
- onClick={() => toggleExpanded(option.format)}
808
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center hover:bg-accent/50">
809
+ <FormatHeader
810
+ type={configurableFormat ? "button" : undefined}
811
+ className={cn(
812
+ "grid min-h-[60px] min-w-0 grid-cols-[28px_minmax(0,1fr)] items-center gap-x-1 py-2 pl-2.5 pr-1 text-left",
813
+ configurableFormat &&
814
+ "cursor-pointer focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
815
+ )}
816
+ aria-label={
817
+ configurableFormat ? `${optionLabel} options` : undefined
818
+ }
819
+ aria-describedby={
820
+ configurableFormat
821
+ ? [descriptionId, summary && summaryId]
822
+ .filter(Boolean)
823
+ .join(" ")
824
+ : undefined
825
+ }
826
+ aria-expanded={configurableFormat ? expanded : undefined}
827
+ aria-controls={configurableFormat ? panelId : undefined}
828
+ onClick={
829
+ configurableFormat
830
+ ? () => toggleExpanded(option.format)
831
+ : undefined
832
+ }
833
+ >
834
+ <span
835
+ aria-hidden={true}
836
+ className="inline-flex h-6 w-6 items-center justify-center"
733
837
  >
734
- <ChevronRightIcon
735
- className={cn(
736
- "h-4 w-4 text-muted-foreground transition-transform",
737
- expanded && "rotate-90",
738
- )}
838
+ {configurableFormat && (
839
+ <ChevronRightIcon
840
+ className={cn(
841
+ "h-4 w-4 text-muted-foreground transition-transform",
842
+ expanded && "rotate-90",
843
+ )}
844
+ />
845
+ )}
846
+ </span>
847
+ <span className="flex min-w-0 items-center gap-3 pl-1">
848
+ <option.icon
849
+ aria-hidden={true}
850
+ data-testid={`export-format-icon-${option.format}`}
851
+ className="h-4 w-4 shrink-0 text-muted-foreground"
739
852
  />
740
- </Button>
741
- ) : (
742
- <span aria-hidden={true} className="h-6 w-6" />
743
- )}
744
- <div className="flex min-w-0 items-center gap-3 pl-1">
745
- <option.icon
746
- aria-hidden={true}
747
- data-testid={`export-format-icon-${option.format}`}
748
- className="h-4 w-4 shrink-0 text-muted-foreground"
749
- />
750
- <div className="grid min-w-0 gap-1">
751
- <span className="flex min-w-0 items-baseline gap-x-2">
752
- <span className="text-sm font-medium leading-tight">
753
- {optionLabel}
754
- </span>
755
- {summary && (
756
- <span
757
- data-testid={`export-summary-${option.format}`}
758
- className="min-w-0 truncate text-xs text-muted-foreground"
759
- >
760
- {summary}
853
+ <span className="grid min-w-0 gap-1">
854
+ <span className="flex min-w-0 items-baseline gap-x-2">
855
+ <span className="text-sm font-medium leading-tight">
856
+ {optionLabel}
761
857
  </span>
762
- )}
763
- </span>
764
- <span className="text-xs text-muted-foreground leading-tight">
765
- {option.format === "parquet" && hasGeometry
766
- ? "Parquet with geometry and CRS metadata"
767
- : option.description}
858
+ {summary && (
859
+ <span
860
+ id={summaryId}
861
+ data-testid={`export-summary-${option.format}`}
862
+ className="min-w-0 truncate text-xs text-muted-foreground"
863
+ >
864
+ {summary}
865
+ </span>
866
+ )}
867
+ </span>
868
+ <span
869
+ id={descriptionId}
870
+ className="text-xs text-muted-foreground leading-tight"
871
+ >
872
+ {option.format === "parquet" && hasGeometry
873
+ ? "Parquet with geometry and CRS metadata"
874
+ : option.description}
875
+ </span>
768
876
  </span>
769
- </div>
770
- </div>
771
- <div className="flex items-center gap-0.5">
877
+ </span>
878
+ </FormatHeader>
879
+ <div className="flex items-center gap-0.5 pr-2.5">
772
880
  <Tooltip
773
881
  content={
774
882
  option.canDownload
@@ -888,9 +996,7 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
888
996
  type="button"
889
997
  size="xs"
890
998
  variant="outline"
891
- onClick={() =>
892
- setMetadataRefresh((current) => current + 1)
893
- }
999
+ onClick={metadataRequest.refetch}
894
1000
  >
895
1001
  Retry
896
1002
  </Button>
@@ -906,17 +1012,13 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
906
1012
  missingPackageFailure?.description ??
907
1013
  eligibility?.reason
908
1014
  }
909
- onInstall={() => {
910
- void retryAfterInstall(
911
- missingPackageFailure?.action ?? {
912
- destination: "download",
913
- format:
914
- option.format === "geojson"
915
- ? "geojson"
916
- : "parquet",
917
- },
918
- );
919
- }}
1015
+ onInstall={createInstallRetry(
1016
+ missingPackageFailure?.action ?? {
1017
+ destination: "download",
1018
+ format:
1019
+ option.format === "geojson" ? "geojson" : "parquet",
1020
+ },
1021
+ )}
920
1022
  className="items-start"
921
1023
  />
922
1024
  </div>
@@ -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
- const { [rowIdx]: removedRow, ...rest } = prev;
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>>;
@@ -66,7 +66,10 @@ export type OnRefactorWithAI = (opts: {
66
66
  * Renders an output based on an OutputMessage.
67
67
  */
68
68
  export const OutputRenderer: React.FC<{
69
- message: Pick<OutputMessage, "channel" | "data" | "mimetype" | "code">;
69
+ message: Pick<
70
+ OutputMessage,
71
+ "channel" | "data" | "mimetype" | "code" | "traceback_format"
72
+ >;
70
73
  cellId?: CellId;
71
74
  onRefactorWithAI?: OnRefactorWithAI;
72
75
  wrapText?: boolean;
@@ -185,6 +188,7 @@ export const OutputRenderer: React.FC<{
185
188
  <MarimoTracebackOutput
186
189
  onRefactorWithAI={onRefactorWithAI}
187
190
  traceback={data}
191
+ tracebackFormat={message.traceback_format}
188
192
  code={message.code}
189
193
  cellId={cellId}
190
194
  />