@marimo-team/frontend 0.25.2-dev56 → 0.25.2-dev62

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 (36) hide show
  1. package/dist/assets/ConnectedDataExplorerComponent-C5UW0p9I.js +1 -0
  2. package/dist/assets/JsonOutput-nqqzL-n0.js +11 -0
  3. package/dist/assets/{agent-panel-9FeJ9hAg.js → agent-panel-Bkcf1sED.js} +1 -1
  4. package/dist/assets/{cell-editor-BNIogYsY.js → cell-editor-Cmqe3QmM.js} +1 -1
  5. package/dist/assets/{column-preview-D2Um0Qe3.js → column-preview-DtrgJzVI.js} +1 -1
  6. package/dist/assets/{command-palette-ChCapZF9.js → command-palette-DUPyVGsq.js} +1 -1
  7. package/dist/assets/{edit-page-DIBiWEli.js → edit-page-DMsRAPsr.js} +4 -4
  8. package/dist/assets/{file-explorer-panel-CxBxvZDk.js → file-explorer-panel-BQi88S1o.js} +1 -1
  9. package/dist/assets/{hooks-5owWOOnf.js → hooks-BHoInJWN.js} +1 -1
  10. package/dist/assets/{index-D9fmFCuc.js → index-C968xQuz.js} +7 -7
  11. package/dist/assets/index-CF8Gfz1R.css +2 -0
  12. package/dist/assets/{layout-giNu7tsO.js → layout-CzNq5YtR.js} +3 -3
  13. package/dist/assets/{panels-DftjTXK1.js → panels-Dsf6yEsA.js} +1 -1
  14. package/dist/assets/{reveal-component-C72HbbmJ.js → reveal-component-CJxdWpbj.js} +1 -1
  15. package/dist/assets/{run-page-BOSkpSu_.js → run-page-DCu4YGwW.js} +1 -1
  16. package/dist/assets/{scratchpad-panel-C3gu3_1_.js → scratchpad-panel-BSlnM0TK.js} +1 -1
  17. package/dist/assets/{session-panel-CNK8FDu3.js → session-panel-BdN43Lzi.js} +1 -1
  18. package/dist/assets/spec-D9d9nT0z.js +1 -0
  19. package/dist/assets/{list-filter-CTg19675.js → table-B7KYyTxw.js} +1 -1
  20. package/dist/assets/{useNotebookActions-ByyEgzc-.js → useNotebookActions-BDD321Hs.js} +1 -1
  21. package/dist/index.html +6 -6
  22. package/package.json +1 -1
  23. package/src/components/data-table/TableTopBar.tsx +4 -0
  24. package/src/components/data-table/__tests__/export-actions.test.tsx +1111 -14
  25. package/src/components/data-table/__tests__/schemas.test.ts +57 -0
  26. package/src/components/data-table/data-table.tsx +3 -0
  27. package/src/components/data-table/export-actions.tsx +365 -81
  28. package/src/components/data-table/schemas.ts +90 -14
  29. package/src/plugins/impl/DataTablePlugin.tsx +23 -2
  30. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +65 -8
  31. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +15 -3
  32. package/src/stories/dataframe.stories.tsx +7 -0
  33. package/dist/assets/ConnectedDataExplorerComponent-Jv_dljLQ.js +0 -1
  34. package/dist/assets/JsonOutput-Dmq_45PS.js +0 -11
  35. package/dist/assets/index-BGdpHNhZ.css +0 -2
  36. package/dist/assets/spec-SOFn9xEK.js +0 -1
@@ -3,7 +3,7 @@
3
3
  import z from "zod";
4
4
  import { rpc } from "@/plugins/core/rpc";
5
5
 
6
- export type DownloadFormat = "csv" | "json" | "parquet" | "tsv";
6
+ export type DownloadFormat = "csv" | "json" | "parquet" | "tsv" | "geojson";
7
7
 
8
8
  /**
9
9
  * Per-request export options. Each field is optional. A missing field keeps
@@ -18,6 +18,7 @@ export interface DownloadAsOptions {
18
18
  export interface DownloadAsRequest {
19
19
  format: DownloadFormat;
20
20
  options?: DownloadAsOptions;
21
+ geometry_column?: string | null;
21
22
  }
22
23
 
23
24
  export type DownloadAsArgs = (req: DownloadAsRequest) => Promise<{
@@ -25,26 +26,101 @@ export type DownloadAsArgs = (req: DownloadAsRequest) => Promise<{
25
26
  filename: string;
26
27
  error?: string | null;
27
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;
28
40
  }>;
29
41
 
42
+ const DownloadRequestSchema = z.object({
43
+ geometry_column: z.string().nullish(),
44
+ options: z
45
+ .object({
46
+ separator: z.string().optional(),
47
+ encoding: z.string().optional(),
48
+ ensure_ascii: z.boolean().optional(),
49
+ })
50
+ .optional(),
51
+ });
52
+
53
+ const DownloadResponseSchema = z.object({
54
+ url: z.string(),
55
+ filename: z.string(),
56
+ error: z.string().nullish(),
57
+ missing_packages: z.array(z.string()).nullish(),
58
+ code: z
59
+ .enum([
60
+ "geometry_required",
61
+ "invalid_geometry",
62
+ "invalid_metadata",
63
+ "unsupported_representation",
64
+ "unsupported_version",
65
+ "missing_packages",
66
+ "conversion_failed",
67
+ ])
68
+ .nullish(),
69
+ column: z.string().nullish(),
70
+ });
71
+
30
72
  export const DownloadAsSchema = rpc
31
73
  .input(
32
- z.object({
74
+ DownloadRequestSchema.extend({
33
75
  format: z.enum(["csv", "json", "parquet", "tsv"]),
34
- options: z
35
- .object({
36
- separator: z.string().optional(),
37
- encoding: z.string().optional(),
38
- ensure_ascii: z.boolean().optional(),
39
- })
40
- .optional(),
41
76
  }),
42
77
  )
78
+ .output(DownloadResponseSchema);
79
+
80
+ export const DownloadGeoJSONSchema = rpc
81
+ .input(DownloadRequestSchema.extend({ format: z.literal("geojson") }))
43
82
  .output(
44
- z.object({
45
- url: z.string(),
46
- filename: z.string(),
47
- error: z.string().nullish(),
48
- missing_packages: z.array(z.string()).nullish(),
83
+ DownloadResponseSchema.extend({
84
+ code: z.union([
85
+ DownloadResponseSchema.shape.code,
86
+ z.literal("missing_crs"),
87
+ ]),
49
88
  }),
50
89
  );
90
+
91
+ export type DownloadAsFunction = (
92
+ req: z.infer<typeof DownloadAsSchema.input>,
93
+ ) => Promise<z.infer<typeof DownloadAsSchema.output>>;
94
+
95
+ export type DownloadGeoJSON = (
96
+ req: z.infer<typeof DownloadGeoJSONSchema.input>,
97
+ ) => Promise<z.infer<typeof DownloadGeoJSONSchema.output>>;
98
+
99
+ const ExportMetadataSchema = z.object({
100
+ geometry_columns: z.array(
101
+ z.object({
102
+ name: z.string(),
103
+ encoding: z.enum(["objects", "wkb", "wkt", "other"]),
104
+ crs: z.union([z.string(), z.record(z.string(), z.unknown())]).nullable(),
105
+ }),
106
+ ),
107
+ primary_geometry_column: z.string().nullable(),
108
+ default_geometry_column: z.string().nullable(),
109
+ formats: z.record(
110
+ z.string(),
111
+ z.object({
112
+ available: z.boolean(),
113
+ reason: z.string().nullable(),
114
+ missing_packages: z.array(z.string()),
115
+ }),
116
+ ),
117
+ });
118
+
119
+ export type ExportMetadata = z.infer<typeof ExportMetadataSchema>;
120
+ export type GetExportMetadata = (
121
+ opts: Record<string, never>,
122
+ ) => Promise<ExportMetadata>;
123
+
124
+ export const GetExportMetadataSchema = rpc
125
+ .input(z.object({}))
126
+ .output(ExportMetadataSchema);
@@ -36,7 +36,12 @@ import { usePanelOwnership } from "@/components/data-table/hooks/use-panel-owner
36
36
  import { LoadingTable } from "@/components/data-table/loading-table";
37
37
  import {
38
38
  type DownloadAsArgs,
39
+ type DownloadAsFunction,
39
40
  DownloadAsSchema,
41
+ type DownloadGeoJSON,
42
+ DownloadGeoJSONSchema,
43
+ type GetExportMetadata,
44
+ GetExportMetadataSchema,
40
45
  } from "@/components/data-table/schemas";
41
46
  import { TableExplorerPanel } from "@/components/data-table/table-explorer-panel/table-explorer-panel";
42
47
  import {
@@ -209,7 +214,9 @@ interface Data<T> {
209
214
 
210
215
  // oxlint-disable-next-line typescript/consistent-type-definitions
211
216
  type DataTableFunctions = {
212
- download_as: DownloadAsArgs;
217
+ download_as: DownloadAsFunction;
218
+ download_geojson: DownloadGeoJSON;
219
+ get_export_metadata: GetExportMetadata;
213
220
  get_column_summaries: <T>(opts: {}) => Promise<ColumnSummaries<T>>;
214
221
  search: <T>(req: {
215
222
  sort?: {
@@ -302,6 +309,8 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
302
309
  )
303
310
  .withFunctions<DataTableFunctions>({
304
311
  download_as: DownloadAsSchema,
312
+ download_geojson: DownloadGeoJSONSchema,
313
+ get_export_metadata: GetExportMetadataSchema,
305
314
  get_column_summaries: rpc.input(z.looseObject({})).output(
306
315
  z.object({
307
316
  data: z.union([z.string(), z.array(z.looseObject({}))]).nullable(),
@@ -839,7 +848,9 @@ const DataTableComponent = ({
839
848
  fieldTypes,
840
849
  paginationState,
841
850
  setPaginationState,
842
- download_as: downloadAs,
851
+ download_as,
852
+ download_geojson,
853
+ get_export_metadata: getExportMetadata,
843
854
  columnSummaries,
844
855
  className,
845
856
  setValue,
@@ -880,6 +891,15 @@ const DataTableComponent = ({
880
891
  sizeBytesIsLoading?: boolean;
881
892
  }): JSX.Element => {
882
893
  const id = useId();
894
+ const downloadAs = useCallback<DownloadAsArgs>(
895
+ (request) => {
896
+ const { format, ...options } = request;
897
+ return format === "geojson"
898
+ ? download_geojson({ format, ...options })
899
+ : download_as({ format, ...options });
900
+ },
901
+ [download_as, download_geojson],
902
+ );
883
903
  const { locale } = useLocale();
884
904
  const [viewedRowIdx, setViewedRowIdx] = useState(0);
885
905
  const { isPanelOpen, isAnyPanelOpen, togglePanel, panelType, setPanelType } =
@@ -1168,6 +1188,7 @@ const DataTableComponent = ({
1168
1188
  hoverTemplate={hoverTemplate}
1169
1189
  cellHoverTexts={cellHoverTexts}
1170
1190
  downloadAs={showDownload ? downloadAs : undefined}
1191
+ getExportMetadata={showDownload ? getExportMetadata : undefined}
1171
1192
  showSearch={showSearch}
1172
1193
  searchQuery={searchQuery}
1173
1194
  onSearchQueryChange={setSearchQuery}
@@ -4,11 +4,17 @@ import { Tooltip } from "radix-ui";
4
4
 
5
5
  const TooltipProvider = Tooltip.Provider;
6
6
 
7
- import { act, render, screen, waitFor } from "@testing-library/react";
7
+ import {
8
+ act,
9
+ fireEvent,
10
+ render,
11
+ screen,
12
+ waitFor,
13
+ } from "@testing-library/react";
8
14
  import { Provider } from "jotai";
9
15
  import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
10
16
  import { SetupMocks } from "@/__mocks__/common";
11
- import type { DownloadAsArgs } from "@/components/data-table/schemas";
17
+ import type { DownloadAsFunction } from "@/components/data-table/schemas";
12
18
  import type { FieldTypesWithExternalType } from "@/components/data-table/types";
13
19
  import { store } from "@/core/state/jotai";
14
20
  import {
@@ -19,6 +25,10 @@ import {
19
25
 
20
26
  // Default to normal (non-static) mode; individual tests flip this on.
21
27
  const mockIsStatic = vi.fn().mockReturnValue(false);
28
+ vi.mock("@/utils/download", () => ({
29
+ downloadByURL: vi.fn(),
30
+ withLoadingToast: vi.fn(async (_title, callback) => callback()),
31
+ }));
22
32
  vi.mock("@/core/static/static-state", async (importOriginal) => {
23
33
  const actual =
24
34
  await importOriginal<typeof import("@/core/static/static-state")>();
@@ -30,9 +40,15 @@ beforeAll(() => {
30
40
  });
31
41
 
32
42
  describe("LoadingDataTableComponent", () => {
33
- it("keeps data columns when inferred types include a row header", async () => {
43
+ it("keeps data columns and routes each export to its RPC", async () => {
34
44
  const host = document.createElement("div");
35
45
  const data = JSON.stringify([{ index: "first", value: 1 }]);
46
+ const downloadAs = vi
47
+ .fn()
48
+ .mockResolvedValue({ url: "export", filename: "table.csv" });
49
+ const downloadGeoJSON = vi
50
+ .fn()
51
+ .mockResolvedValue({ url: "export", filename: "table.geojson" });
36
52
 
37
53
  render(
38
54
  <Provider store={store}>
@@ -43,7 +59,7 @@ describe("LoadingDataTableComponent", () => {
43
59
  pagination={false}
44
60
  pageSize={10}
45
61
  selection={null}
46
- showDownload={false}
62
+ showDownload={true}
47
63
  showFilters={false}
48
64
  showColumnSummaries={false}
49
65
  showDataTypes={false}
@@ -68,7 +84,22 @@ describe("LoadingDataTableComponent", () => {
68
84
  cell_styles: null,
69
85
  cell_hover_texts: null,
70
86
  })}
71
- download_as={vi.fn() as DownloadAsArgs}
87
+ download_as={downloadAs}
88
+ download_geojson={downloadGeoJSON}
89
+ get_export_metadata={vi.fn().mockResolvedValue({
90
+ geometry_columns: [
91
+ { name: "value", encoding: "objects", crs: "EPSG:4326" },
92
+ ],
93
+ primary_geometry_column: "value",
94
+ default_geometry_column: "value",
95
+ formats: {
96
+ geojson: {
97
+ available: true,
98
+ reason: null,
99
+ missing_packages: [],
100
+ },
101
+ },
102
+ })}
72
103
  get_column_summaries={vi.fn()}
73
104
  get_data_url={vi.fn() as GetDataUrl}
74
105
  get_row_ids={vi.fn() as GetRowIds}
@@ -83,6 +114,26 @@ describe("LoadingDataTableComponent", () => {
83
114
  screen.getByRole("columnheader", { name: "value" }),
84
115
  ).toBeInTheDocument();
85
116
  });
117
+ fireEvent.click(screen.getByTestId("export-button"));
118
+ await screen.findByRole("dialog", { name: "Export table" });
119
+ await waitFor(() =>
120
+ expect(
121
+ screen.getByRole("button", { name: "Download GeoJSON" }),
122
+ ).toBeEnabled(),
123
+ );
124
+ fireEvent.click(screen.getByRole("button", { name: "Download GeoJSON" }));
125
+ await waitFor(() =>
126
+ expect(downloadGeoJSON).toHaveBeenCalledWith({
127
+ format: "geojson",
128
+ geometry_column: "value",
129
+ }),
130
+ );
131
+ expect(downloadAs).not.toHaveBeenCalled();
132
+ fireEvent.click(screen.getByRole("button", { name: "Download CSV" }));
133
+ await waitFor(() =>
134
+ expect(downloadAs).toHaveBeenCalledWith({ format: "csv" }),
135
+ );
136
+ expect(downloadGeoJSON).toHaveBeenCalledTimes(1);
86
137
  });
87
138
 
88
139
  it("does not log duplicate keys when a pivot becomes empty", async () => {
@@ -122,7 +173,9 @@ describe("LoadingDataTableComponent", () => {
122
173
  showSearch: false,
123
174
  value: [] as (number | string | { rowId: string; columnName?: string })[],
124
175
  setValue: vi.fn(),
125
- download_as: vi.fn() as DownloadAsArgs,
176
+ download_as: vi.fn() as DownloadAsFunction,
177
+ download_geojson: vi.fn(),
178
+ get_export_metadata: vi.fn(),
126
179
  get_column_summaries: vi.fn(),
127
180
  get_data_url: vi.fn() as GetDataUrl,
128
181
  get_row_ids: vi.fn() as GetRowIds,
@@ -263,7 +316,9 @@ describe("LoadingDataTableComponent", () => {
263
316
  showSearch: true,
264
317
  value: [] as (number | string | { rowId: string; columnName?: string })[],
265
318
  setValue,
266
- download_as: vi.fn() as DownloadAsArgs,
319
+ download_as: vi.fn() as DownloadAsFunction,
320
+ download_geojson: vi.fn(),
321
+ get_export_metadata: vi.fn(),
267
322
  get_column_summaries: vi.fn().mockResolvedValue({
268
323
  data: null,
269
324
  stats: {},
@@ -379,7 +434,9 @@ describe("static notebook control suppression", () => {
379
434
  cell_styles: null,
380
435
  cell_hover_texts: null,
381
436
  }),
382
- download_as: vi.fn() as DownloadAsArgs,
437
+ download_as: vi.fn() as DownloadAsFunction,
438
+ download_geojson: vi.fn(),
439
+ get_export_metadata: vi.fn(),
383
440
  get_column_summaries: vi.fn().mockResolvedValue({
384
441
  data: null,
385
442
  stats: {},
@@ -12,8 +12,12 @@ import {
12
12
  } from "react";
13
13
  import { z } from "zod";
14
14
  import {
15
- type DownloadAsArgs,
15
+ type DownloadAsFunction,
16
16
  DownloadAsSchema,
17
+ type DownloadGeoJSON,
18
+ DownloadGeoJSONSchema,
19
+ type GetExportMetadata,
20
+ GetExportMetadataSchema,
17
21
  } from "@/components/data-table/schemas";
18
22
  import type { FieldTypesWithExternalType } from "@/components/data-table/types";
19
23
  import { ReadonlyCode } from "@/components/editor/code/readonly-python-code";
@@ -82,7 +86,9 @@ type PluginFunctions = {
82
86
  data: TableData<T>;
83
87
  total_rows: number;
84
88
  }>;
85
- download_as: DownloadAsArgs;
89
+ download_as: DownloadAsFunction;
90
+ download_geojson: DownloadGeoJSON;
91
+ get_export_metadata: GetExportMetadata;
86
92
  get_size_bytes: (opts: Record<string, never>) => Promise<{
87
93
  size_bytes?: number | null;
88
94
  }>;
@@ -153,6 +159,8 @@ export const DataFramePlugin = createPlugin<S>("marimo-dataframe")
153
159
  }),
154
160
  ),
155
161
  download_as: DownloadAsSchema,
162
+ download_geojson: DownloadGeoJSONSchema,
163
+ get_export_metadata: GetExportMetadataSchema,
156
164
  get_size_bytes: rpc
157
165
  .input(z.object({}))
158
166
  .output(z.object({ size_bytes: z.number().nullish() })),
@@ -174,7 +182,7 @@ interface DataTableProps extends Data, PluginFunctions {
174
182
  setValue: (value: S) => void;
175
183
  host: HTMLElement;
176
184
  showDownload: boolean;
177
- download_as: DownloadAsArgs;
185
+ download_as: DownloadAsFunction;
178
186
  }
179
187
 
180
188
  const EMPTY: Transformations = {
@@ -194,6 +202,8 @@ export const DataFrameComponent = memo(
194
202
  get_column_values,
195
203
  search,
196
204
  download_as,
205
+ download_geojson,
206
+ get_export_metadata,
197
207
  get_size_bytes,
198
208
  host,
199
209
  }: DataTableProps): JSX.Element => {
@@ -337,6 +347,8 @@ export const DataFrameComponent = memo(
337
347
  rowHeaders={row_headers || Arrays.EMPTY}
338
348
  showDownload={showDownload}
339
349
  download_as={download_as}
350
+ download_geojson={download_geojson}
351
+ get_export_metadata={get_export_metadata}
340
352
  get_size_bytes={get_size_bytes}
341
353
  showSearch={false}
342
354
  showFilters={false}
@@ -42,6 +42,13 @@ export const DataFrame: StoryObj = {
42
42
  host={document.body}
43
43
  showDownload={false}
44
44
  download_as={async () => ({ url: "", filename: "" })}
45
+ download_geojson={async () => ({ url: "", filename: "" })}
46
+ get_export_metadata={async () => ({
47
+ geometry_columns: [],
48
+ primary_geometry_column: null,
49
+ default_geometry_column: null,
50
+ formats: {},
51
+ })}
45
52
  get_size_bytes={async () => ({ size_bytes: null })}
46
53
  lazy={false}
47
54
  />