@marimo-team/islands 0.23.2-dev62 → 0.23.2-dev65

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.
@@ -10596,7 +10596,9 @@ let __tla = (async () => {
10596
10596
  switch (i) {
10597
10597
  case "insert": {
10598
10598
  if (!o) return Logger.error("newName is required for insert"), t;
10599
- let e2 = Object.entries(t), i2 = [
10599
+ let e2 = [
10600
+ ...t.entries()
10601
+ ], i2 = [
10600
10602
  ...e2.slice(0, r),
10601
10603
  [
10602
10604
  o,
@@ -10604,21 +10606,25 @@ let __tla = (async () => {
10604
10606
  ],
10605
10607
  ...e2.slice(r)
10606
10608
  ];
10607
- return Object.fromEntries(i2);
10609
+ return new Map(i2);
10608
10610
  }
10609
10611
  case "remove": {
10610
- if (r < 0 || r >= Object.keys(t).length) return t;
10611
- let e2 = (_a = Object.entries(t)[r]) == null ? void 0 : _a[0];
10612
+ if (r < 0 || r >= t.size) return t;
10613
+ let e2 = (_a = [
10614
+ ...t.entries()
10615
+ ][r]) == null ? void 0 : _a[0];
10612
10616
  if (e2) {
10613
- let { [e2]: n, ...r2 } = t;
10614
- return r2;
10617
+ let n = new Map(t);
10618
+ return n.delete(e2), n;
10615
10619
  }
10616
10620
  return t;
10617
10621
  }
10618
10622
  case "rename": {
10619
10623
  if (!o) return Logger.error("newName is required for rename"), t;
10620
- if (r < 0 || r >= Object.keys(t).length) return t;
10621
- let e2 = Object.entries(t), i2 = [
10624
+ if (r < 0 || r >= t.size) return t;
10625
+ let e2 = [
10626
+ ...t.entries()
10627
+ ], i2 = [
10622
10628
  ...e2.slice(0, r),
10623
10629
  [
10624
10630
  o,
@@ -10626,7 +10632,7 @@ let __tla = (async () => {
10626
10632
  ],
10627
10633
  ...e2.slice(r + 1)
10628
10634
  ];
10629
- return Object.fromEntries(i2);
10635
+ return new Map(i2);
10630
10636
  }
10631
10637
  }
10632
10638
  }
@@ -10642,7 +10648,7 @@ let __tla = (async () => {
10642
10648
  let [H, U] = (0, import_react.useState)(Wr), Gr = useNonce(), W = (0, import_react.useRef)(false), G;
10643
10649
  if (t[2] !== o || t[3] !== H || t[4] !== f || t[5] !== k) {
10644
10650
  G = [];
10645
- for (let [e2, t2] of Object.entries(o)) {
10651
+ for (let [e2, t2] of o) {
10646
10652
  let n2 = f === "all" || f.includes(e2);
10647
10653
  G.push({
10648
10654
  id: e2,
@@ -10765,8 +10771,8 @@ let __tla = (async () => {
10765
10771
  }, t[24] = Qr) : Qr = t[24];
10766
10772
  let $r = Qr, ei;
10767
10773
  t[25] !== o || t[26] !== K ? (ei = (e2, t2, n2) => {
10768
- let [r] = e2, i = o[K[r].title];
10769
- bb134: switch (i) {
10774
+ let [r] = e2, i = K[r].title, a2 = o.get(i);
10775
+ bb134: switch (a2) {
10770
10776
  case "number":
10771
10777
  case "integer":
10772
10778
  if (Number.isNaN(Number(t2.data))) return false;
@@ -10854,7 +10860,7 @@ let __tla = (async () => {
10854
10860
  t[48] !== o || t[49] !== K || t[50] !== N || t[51] !== x || t[52] !== u || t[53] !== a ? (pi = (e2) => {
10855
10861
  if (N) {
10856
10862
  let t2 = K[N.col].title;
10857
- if (o[e2]) {
10863
+ if (o.has(e2)) {
10858
10864
  fi(e2);
10859
10865
  return;
10860
10866
  }
@@ -10881,7 +10887,7 @@ let __tla = (async () => {
10881
10887
  let { direction: t2, columnName: n2, dataType: r } = e2;
10882
10888
  if (N) {
10883
10889
  let e3 = N.col + (t2 === "left" ? 0 : 1), i = Math.max(0, Math.min(e3, K.length));
10884
- if (o[n2]) {
10890
+ if (o.has(n2)) {
10885
10891
  fi(n2);
10886
10892
  return;
10887
10893
  }
package/dist/main.js CHANGED
@@ -26,7 +26,7 @@ import { An as FileText, B as safeExtractSetUIElementMessageBuffers, C as Accord
26
26
  import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-CO1e63h_.js";
27
27
  import { o as useSize, s as Root$3, u as createLucideIcon } from "./dist-ESg7xyoD.js";
28
28
  import { A as SquareFunction, C as DEFAULT_COLOR_SCHEME, D as SCALE_TYPE_DESCRIPTIONS, E as EMPTY_VALUE$1, O as TIME_UNIT_DESCRIPTIONS, S as DEFAULT_AGGREGATION, T as DEFAULT_TIME_UNIT, _ as AGGREGATION_TYPE_DESCRIPTIONS, a as AGGREGATION_FNS$1, b as COLOR_SCHEMES, c as COLOR_BY_FIELDS, d as NONE_VALUE, f as SELECTABLE_DATA_TYPES, g as TIME_UNITS, h as STRING_AGGREGATION_FNS, i as convertDataTypeToSelectable, j as ChartColumn, k as escapeFieldName, l as COMBINED_TIME_UNITS, m as SORT_TYPES, n as createSpecWithoutData, o as BIN_AGGREGATION, p as SINGLE_TIME_UNITS, r as isFieldSet, s as CHART_TYPES, t as augmentSpecWithData, u as ChartType, v as AGGREGATION_TYPE_ICON, w as DEFAULT_MAX_BINS_FACET, x as COUNT_FIELD, y as CHART_TYPE_ICON } from "./spec-BKWq0wn2.js";
29
- import { $ as CommandItem, A as TableHeader, B as usePrevious$1, C as Toggle, Ct as Download, D as TableBody, Dt as ChevronsDownUp, E as Table, Et as ChevronsLeft, F as renderCellValue, G as SELECT_COLUMN_ID, H as loadTableAndRawData, I as ColumnChartContext, J as getMimeValues, K as TOO_MANY_ROWS, L as ColumnChartSpecModel, M as NAMELESS_COLUMN_PREFIX, N as generateColumns, O as TableCell, Ot as ChevronLeft, P as inferFieldTypes, Q as CommandInput, R as DelayMount, S as slotsController, St as Ellipsis, T as Provider$1, Tt as ChevronsRight, U as loadTableData, V as getPageIndexForRow, W as INDEX_COLUMN_NAME, X as Command, Y as filtersToFilterGroup, Z as CommandEmpty, _ as contextAwarePanelOpen, _t as isStaticNotebook, a as DataTable, at as TabsList, b as isCellAwareAtom, bt as Funnel, c as downloadBlob, ct as ChartInfoState, d as Filenames, dt as useOverflowDetection, et as CommandList, f as prettifyRowColumnCount, ft as RenderTextWithLinks, g as PANEL_TYPES, gt as getStaticVirtualFiles, h as ContextAwarePanelItem, ht as EmotionCacheProvider, i as OutputRenderer, it as TabsContent, j as TableRow, k as TableHead, kt as ArrowDownWideNarrow, l as downloadByURL, lt as ChartLoadingState, m as useInternalStateWithSync, mt as HtmlOutput, n as JsonOutput, nt as Maps, o as InstallPackageButton, ot as TabsTrigger, p as prettifyRowCount, pt as Kbd, q as toFieldTypes, r as OutputArea, rt as Tabs, s as ADD_PRINTING_CLASS, st as ChartErrorState, t as Slide, tt as CommandSeparator, u as downloadHTMLAsImage, ut as LazyVegaEmbed, v as contextAwarePanelOwner, vt as TextWrap, w as Fill, wt as ChevronsUpDown, x as SlotNames, y as contextAwarePanelType, yt as GripHorizontal, z as useIntersectionObserver, __tla as __tla_2 } from "./slide-BiyYep36.js";
29
+ import { $ as CommandItem, A as TableHeader, B as usePrevious$1, C as Toggle, Ct as Download, D as TableBody, Dt as ChevronsDownUp, E as Table, Et as ChevronsLeft, F as renderCellValue, G as SELECT_COLUMN_ID, H as loadTableAndRawData, I as ColumnChartContext, J as getMimeValues, K as TOO_MANY_ROWS, L as ColumnChartSpecModel, M as NAMELESS_COLUMN_PREFIX, N as generateColumns, O as TableCell, Ot as ChevronLeft, P as inferFieldTypes, Q as CommandInput, R as DelayMount, S as slotsController, St as Ellipsis, T as Provider$1, Tt as ChevronsRight, U as loadTableData, V as getPageIndexForRow, W as INDEX_COLUMN_NAME, X as Command, Y as filtersToFilterGroup, Z as CommandEmpty, _ as contextAwarePanelOpen, _t as isStaticNotebook, a as DataTable, at as TabsList, b as isCellAwareAtom, bt as Funnel, c as downloadBlob, ct as ChartInfoState, d as Filenames, dt as useOverflowDetection, et as CommandList, f as prettifyRowColumnCount, ft as RenderTextWithLinks, g as PANEL_TYPES, gt as getStaticVirtualFiles, h as ContextAwarePanelItem, ht as EmotionCacheProvider, i as OutputRenderer, it as TabsContent, j as TableRow, k as TableHead, kt as ArrowDownWideNarrow, l as downloadByURL, lt as ChartLoadingState, m as useInternalStateWithSync, mt as HtmlOutput, n as JsonOutput, nt as Maps, o as InstallPackageButton, ot as TabsTrigger, p as prettifyRowCount, pt as Kbd, q as toFieldTypes, r as OutputArea, rt as Tabs, s as ADD_PRINTING_CLASS, st as ChartErrorState, t as Slide, tt as CommandSeparator, u as downloadHTMLAsImage, ut as LazyVegaEmbed, v as contextAwarePanelOwner, vt as TextWrap, w as Fill, wt as ChevronsUpDown, x as SlotNames, y as contextAwarePanelType, yt as GripHorizontal, z as useIntersectionObserver, __tla as __tla_2 } from "./slide-Du4Jfizg.js";
30
30
  import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, r as Badge, t as useOnMount } from "./useLifecycle-smVfjLNI.js";
31
31
  import { n as $fb18d541ea1ad717$export$ad991b66133851cf, r as $5a387cc49350e6db$export$722debc0e56fea39, t as $896ba0a80a8f4d36$export$85fd5fdf27bacc79 } from "./useDateFormatter-B3mCQMP3.js";
32
32
  import { t as Check } from "./check-CFM2mVDr.js";
@@ -14012,7 +14012,7 @@ Defaulting to \`null\`.`;
14012
14012
  "time",
14013
14013
  "unknown"
14014
14014
  ];
14015
- var import_compiler_runtime$88 = require_compiler_runtime(), LazyDataEditor = import_react.lazy(() => import("./glide-data-editor-CVtY_KYw.js").then(async (m) => {
14015
+ var import_compiler_runtime$88 = require_compiler_runtime(), LazyDataEditor = import_react.lazy(() => import("./glide-data-editor-DXti2axL.js").then(async (m) => {
14016
14016
  await m.__tla;
14017
14017
  return m;
14018
14018
  }));
@@ -14060,12 +14060,12 @@ Defaulting to \`null\`.`;
14060
14060
  let r = (0, import_compiler_runtime$88.c)(31), c;
14061
14061
  r[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (c = [], r[0] = c) : c = r[0];
14062
14062
  let [l, u] = (0, import_react.useState)(c), d;
14063
- r[1] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (d = {}, r[1] = d) : d = r[1];
14063
+ r[1] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (d = /* @__PURE__ */ new Map(), r[1] = d) : d = r[1];
14064
14064
  let [f, p] = (0, import_react.useState)(d), m, h;
14065
14065
  r[2] !== e.data || r[3] !== e.fieldTypes ? (m = async () => {
14066
14066
  let r2 = toFieldTypes(e.fieldTypes ?? []), c2 = Array.isArray(e.data) ? e.data : await vegaLoadData(e.data, {
14067
14067
  type: "csv",
14068
- parse: getVegaFieldTypes(r2)
14068
+ parse: getVegaFieldTypes(Object.fromEntries(r2))
14069
14069
  }, {
14070
14070
  handleBigIntAndNumberLike: true
14071
14071
  });
@@ -44992,7 +44992,7 @@ ${c}
44992
44992
  function asCellId(e) {
44993
44993
  return typeof e == "string" ? e : null;
44994
44994
  }
44995
- var import_compiler_runtime$10 = require_compiler_runtime(), LazySlidesComponent = import_react.lazy(() => import("./reveal-component-CA7oaSt2.js"));
44995
+ var import_compiler_runtime$10 = require_compiler_runtime(), LazySlidesComponent = import_react.lazy(() => import("./reveal-component-CATTJBrD.js"));
44996
44996
  const SlidesLayoutRenderer = (e) => {
44997
44997
  var _a3;
44998
44998
  let r = (0, import_compiler_runtime$10.c)(20), { cells: c, mode: l } = e, u = l === "read", d = useAtomValue(numColumnsAtom) > 1, [f, p] = (0, import_react.useState)(null), m = (0, import_react.useRef)(null), h, g;
@@ -45525,7 +45525,7 @@ ${c}
45525
45525
  return Logger.warn("Failed to get version from mount config"), null;
45526
45526
  }
45527
45527
  }
45528
- const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.2-dev62"), showCodeInRunModeAtom = atom(true);
45528
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.2-dev65"), showCodeInRunModeAtom = atom(true);
45529
45529
  atom(null);
45530
45530
  var VIRTUAL_FILE_REGEX = /\/@file\/([^\s"&'/]+)\.([\dA-Za-z]+)/g, VirtualFileTracker = class e {
45531
45531
  constructor() {
@@ -8,7 +8,7 @@ import { t as require_react } from "./react-DA-nE2FX.js";
8
8
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
9
  import "./html-to-image-BdsDysfl.js";
10
10
  import "./chunk-5FQGJX7Z-CO1e63h_.js";
11
- import { t as Slide, xt as Expand } from "./slide-BiyYep36.js";
11
+ import { t as Slide, xt as Expand } from "./slide-Du4Jfizg.js";
12
12
  import "./input-Drx1pguW.js";
13
13
  import "./toDate-yqOcZ_tY.js";
14
14
  import "./react-dom-BWRJ_g_k.js";
@@ -15548,7 +15548,10 @@ Database schema: ${n}`), (_a3 = t2.aiFix) == null ? void 0 : _a3.setAiCompletion
15548
15548
  if (Array.isArray(e) && e.every(isMimeValue)) return e.map((e2) => e2);
15549
15549
  };
15550
15550
  toFieldTypes = function(e) {
15551
- return Objects.collect(e, ([e2]) => e2, ([, [e2]]) => e2);
15551
+ return new Map(e.map(([e2, [t]]) => [
15552
+ e2,
15553
+ t
15554
+ ]));
15552
15555
  };
15553
15556
  SELECT_COLUMN_ID = "__select__";
15554
15557
  INDEX_COLUMN_NAME = "_marimo_row_id";
@@ -16818,7 +16821,7 @@ Database schema: ${n}`), (_a3 = t2.aiFix) == null ? void 0 : _a3.setAiCompletion
16818
16821
  getHeaderSummary(e) {
16819
16822
  return {
16820
16823
  stats: this.columnStats.get(e),
16821
- type: this.fieldTypes[e],
16824
+ type: this.fieldTypes.get(e),
16822
16825
  spec: this.opts.includeCharts ? this.getVegaSpec(e) : void 0
16823
16826
  };
16824
16827
  }
@@ -16850,7 +16853,8 @@ Database schema: ${n}`), (_a3 = t2.aiFix) == null ? void 0 : _a3.setAiCompletion
16850
16853
  values: t,
16851
16854
  name: "bin_values"
16852
16855
  });
16853
- let s = this.createBase(a), c = this.fieldTypes[e];
16856
+ let s = this.createBase(a), c = this.fieldTypes.get(e);
16857
+ if (c === void 0) return null;
16854
16858
  switch (e = e.replaceAll(".", "\\."), e = e.replaceAll("[", "\\[").replaceAll("]", "\\]"), e = e.replaceAll(":", "\\:"), c) {
16855
16859
  case "date":
16856
16860
  case "datetime":
@@ -17427,7 +17431,7 @@ Database schema: ${n}`), (_a3 = t2.aiFix) == null ? void 0 : _a3.setAiCompletion
17427
17431
  return logNever(c), null;
17428
17432
  }
17429
17433
  }
17430
- }, __publicField(_a, "EMPTY", new _a([], {}, {}, {}, {}, {
17434
+ }, __publicField(_a, "EMPTY", new _a([], /* @__PURE__ */ new Map(), {}, {}, {}, {
17431
17435
  includeCharts: false
17432
17436
  })), _a);
17433
17437
  import_compiler_runtime$20 = require_compiler_runtime();
@@ -17484,7 +17488,7 @@ Database schema: ${n}`), (_a3 = t2.aiFix) == null ? void 0 : _a3.setAiCompletion
17484
17488
  }), t[15] = i, t[16] = v);
17485
17489
  let y = v;
17486
17490
  s = "flex flex-col items-center text-xs text-muted-foreground align-end", c = _, d = (() => {
17487
- if (!h) return null;
17491
+ if (!h || f2 === void 0) return null;
17488
17492
  switch (f2) {
17489
17493
  case "date":
17490
17494
  case "datetime":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.2-dev62",
3
+ "version": "0.23.2-dev65",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -22,14 +22,14 @@ vi.mock("@/core/runtime/config", () => ({
22
22
 
23
23
  describe("ColumnChartSpecModel", () => {
24
24
  const mockData = "http://example.com/data.json";
25
- const mockFieldTypes: FieldTypes = {
26
- date: "date",
27
- number: "number",
28
- integer: "integer",
29
- boolean: "boolean",
30
- string: "string",
31
- datetime: "datetime",
32
- };
25
+ const mockFieldTypes: FieldTypes = new Map([
26
+ ["date", "date"],
27
+ ["number", "number"],
28
+ ["integer", "integer"],
29
+ ["boolean", "boolean"],
30
+ ["string", "string"],
31
+ ["datetime", "datetime"],
32
+ ]);
33
33
  const mockStats: Record<ColumnName, Partial<ColumnHeaderStats>> = {
34
34
  date: { min: "2023-01-01", max: "2023-12-31" },
35
35
  number: { min: 0, max: 100 },
@@ -120,9 +120,9 @@ describe("ColumnChartSpecModel", () => {
120
120
  });
121
121
 
122
122
  it("should handle special characters in column names", () => {
123
- const specialFieldTypes: FieldTypes = {
124
- "column.with[special:chars]": "time",
125
- };
123
+ const specialFieldTypes: FieldTypes = new Map([
124
+ ["column.with[special:chars]", "time"],
125
+ ]);
126
126
  const specialStats: Record<ColumnName, Partial<ColumnHeaderStats>> = {
127
127
  "column.with[special:chars]": { min: "2023-01-01", max: "2023-12-31" },
128
128
  };
@@ -267,10 +267,10 @@ describe("ColumnChartSpecModel", () => {
267
267
  });
268
268
 
269
269
  describe("snapshot with legacy data spec", () => {
270
- const fieldTypes: FieldTypes = {
270
+ const fieldTypes: FieldTypes = new Map([
271
271
  ...mockFieldTypes,
272
- a: "number",
273
- };
272
+ ["a", "number"],
273
+ ]);
274
274
 
275
275
  it("url data", () => {
276
276
  const model = new ColumnChartSpecModel(
@@ -1,6 +1,39 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { describe, expect, it } from "vitest";
3
- import { extractTimezone } from "../types";
3
+ import {
4
+ extractTimezone,
5
+ type FieldTypesWithExternalType,
6
+ toFieldTypes,
7
+ } from "../types";
8
+
9
+ describe("toFieldTypes", () => {
10
+ // Regression: https://github.com/marimo-team/marimo/issues/9269.
11
+ // When `FieldTypes` was a plain `Record<string, DataType>`, column
12
+ // names that look like non-negative integers (e.g. "2000", "2010")
13
+ // were hoisted to the front in numeric order by ECMAScript's
14
+ // `OrdinaryOwnPropertyKeys` algorithm. `Map` preserves insertion
15
+ // order for all keys.
16
+ it("preserves insertion order for digit-string column names", () => {
17
+ const input: FieldTypesWithExternalType = [
18
+ ["here", ["string", ""]],
19
+ ["is", ["string", ""]],
20
+ ["a", ["string", ""]],
21
+ ["2010", ["number", ""]],
22
+ ["column", ["string", ""]],
23
+ ["2000", ["number", ""]],
24
+ ["set", ["string", ""]],
25
+ ];
26
+ expect([...toFieldTypes(input).keys()]).toEqual([
27
+ "here",
28
+ "is",
29
+ "a",
30
+ "2010",
31
+ "column",
32
+ "2000",
33
+ "set",
34
+ ]);
35
+ });
36
+ });
4
37
 
5
38
  describe("extractTimezone", () => {
6
39
  it("should return undefined when dtype is undefined", () => {
@@ -41,7 +41,7 @@ export class ColumnChartSpecModel<T> {
41
41
 
42
42
  public static readonly EMPTY = new ColumnChartSpecModel(
43
43
  [],
44
- {},
44
+ new Map(),
45
45
  {},
46
46
  {},
47
47
  {},
@@ -97,7 +97,7 @@ export class ColumnChartSpecModel<T> {
97
97
  public getHeaderSummary(column: string) {
98
98
  return {
99
99
  stats: this.columnStats.get(column),
100
- type: this.fieldTypes[column],
100
+ type: this.fieldTypes.get(column),
101
101
  spec: this.opts.includeCharts ? this.getVegaSpec(column) : undefined,
102
102
  };
103
103
  }
@@ -141,7 +141,10 @@ export class ColumnChartSpecModel<T> {
141
141
  }
142
142
 
143
143
  const base = this.createBase(data);
144
- const type = this.fieldTypes[column];
144
+ const type = this.fieldTypes.get(column);
145
+ if (type === undefined) {
146
+ return null;
147
+ }
145
148
 
146
149
  // https://github.com/vega/altair/blob/32990a597af7c09586904f40b3f5e6787f752fa5/doc/user_guide/encodings/index.rst#escaping-special-characters-in-column-names
147
150
  // escape periods in column names
@@ -75,7 +75,7 @@ export const TableColumnSummary = <TData, TValue>({
75
75
  };
76
76
 
77
77
  const renderStats = () => {
78
- if (!stats) {
78
+ if (!stats || type === undefined) {
79
79
  return null;
80
80
  }
81
81
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  import type { RowData } from "@tanstack/react-table";
4
4
  import type { DataType } from "@/core/kernel/messages";
5
- import { Objects } from "@/utils/objects";
6
5
 
7
6
  declare module "@tanstack/react-table" {
8
7
  interface TableMeta<TData extends RowData> {
@@ -56,16 +55,19 @@ export type FieldTypesWithExternalType = [
56
55
  columnName: string,
57
56
  [dataType: DataType, externalType: string],
58
57
  ][];
59
- export type FieldTypes = Record<string, DataType>;
58
+ // Ordered map of column name -> data type.
59
+ //
60
+ // `Map` (not `Record`) because JS objects reorder integer-string keys
61
+ // to the front in numeric order per `OrdinaryOwnPropertyKeys`; a
62
+ // DataFrame with a `"2010"` column alongside `"here"` would otherwise
63
+ // lose its column order on iteration (#9269). `Map` preserves insertion
64
+ // order for all keys.
65
+ export type FieldTypes = Map<string, DataType>;
60
66
 
61
67
  export function toFieldTypes(
62
68
  fieldTypes: FieldTypesWithExternalType,
63
69
  ): FieldTypes {
64
- return Objects.collect(
65
- fieldTypes,
66
- ([columnName]) => columnName,
67
- ([, [type]]) => type,
68
- );
70
+ return new Map(fieldTypes.map(([columnName, [type]]) => [columnName, type]));
69
71
  }
70
72
 
71
73
  interface BinValue {
@@ -83,19 +83,23 @@ interface Props extends Omit<
83
83
 
84
84
  const LoadingDataEditor = (props: Props) => {
85
85
  const [data, setData] = useState<unknown[]>([]);
86
- const [columnFields, setColumnFields] = useState<FieldTypes>({});
86
+ const [columnFields, setColumnFields] = useState<FieldTypes>(new Map());
87
87
 
88
88
  // Load the data
89
89
  const { error } = useAsyncData(async () => {
90
90
  const withoutExternalTypes = toFieldTypes(props.fieldTypes ?? []);
91
91
 
92
92
  // If we already have the data, return it
93
- // Otherwise, load the data from the URL
93
+ // Otherwise, load the data from the URL. Vega's CSV parser takes a
94
+ // plain `Record`; column order doesn't matter for parsing.
94
95
  const localData = Array.isArray(props.data)
95
96
  ? props.data
96
97
  : await vegaLoadData(
97
98
  props.data,
98
- { type: "csv", parse: getVegaFieldTypes(withoutExternalTypes) },
99
+ {
100
+ type: "csv",
101
+ parse: getVegaFieldTypes(Object.fromEntries(withoutExternalTypes)),
102
+ },
99
103
  { handleBigIntAndNumberLike: true },
100
104
  );
101
105
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { describe, expect, it } from "vitest";
4
4
  import type { FieldTypes } from "@/components/data-table/types";
5
+ import type { DataType } from "@/core/kernel/messages";
5
6
  import {
6
7
  insertColumn,
7
8
  modifyColumnFields,
@@ -9,6 +10,11 @@ import {
9
10
  renameColumn,
10
11
  } from "../data-utils";
11
12
 
13
+ // Fixtures are written as object literals for readability; `FieldTypes`
14
+ // is a `Map` (#9269).
15
+ const asFieldTypes = (obj: Record<string, DataType>): FieldTypes =>
16
+ new Map(Object.entries(obj));
17
+
12
18
  describe("removeColumn", () => {
13
19
  const testData = [
14
20
  { int: 1, string: "a", bool: "True", datetime: "2025-07-12 00:07:13" },
@@ -481,12 +487,12 @@ describe("renameColumn", () => {
481
487
  });
482
488
 
483
489
  describe("modifyColumnFields", () => {
484
- const testFieldTypes: FieldTypes = {
490
+ const testFieldTypes: FieldTypes = asFieldTypes({
485
491
  int: "integer",
486
492
  string: "string",
487
493
  bool: "boolean",
488
494
  datetime: "datetime",
489
- };
495
+ });
490
496
 
491
497
  it("should insert a new column at index 0", () => {
492
498
  const result = modifyColumnFields({
@@ -496,16 +502,15 @@ describe("modifyColumnFields", () => {
496
502
  newColumnName: "newColumn",
497
503
  });
498
504
 
499
- const expected = {
500
- newColumn: "string",
501
- int: "integer",
502
- string: "string",
503
- bool: "boolean",
504
- datetime: "datetime",
505
- };
506
-
507
- expect(Object.keys(result)).toEqual(Object.keys(expected));
508
- expect(result).toEqual(expected);
505
+ expect(result).toMatchInlineSnapshot(`
506
+ Map {
507
+ "newColumn" => "string",
508
+ "int" => "integer",
509
+ "string" => "string",
510
+ "bool" => "boolean",
511
+ "datetime" => "datetime",
512
+ }
513
+ `);
509
514
  });
510
515
 
511
516
  it("should insert a new column at index 1", () => {
@@ -516,16 +521,15 @@ describe("modifyColumnFields", () => {
516
521
  newColumnName: "newColumn",
517
522
  });
518
523
 
519
- const expected = {
520
- int: "integer",
521
- newColumn: "string",
522
- string: "string",
523
- bool: "boolean",
524
- datetime: "datetime",
525
- };
526
-
527
- expect(Object.keys(result)).toEqual(Object.keys(expected));
528
- expect(result).toEqual(expected);
524
+ expect(result).toMatchInlineSnapshot(`
525
+ Map {
526
+ "int" => "integer",
527
+ "newColumn" => "string",
528
+ "string" => "string",
529
+ "bool" => "boolean",
530
+ "datetime" => "datetime",
531
+ }
532
+ `);
529
533
  });
530
534
 
531
535
  it("should insert a new column at index 2", () => {
@@ -536,16 +540,15 @@ describe("modifyColumnFields", () => {
536
540
  newColumnName: "newColumn",
537
541
  });
538
542
 
539
- const expected = {
540
- int: "integer",
541
- string: "string",
542
- newColumn: "string",
543
- bool: "boolean",
544
- datetime: "datetime",
545
- };
546
-
547
- expect(Object.keys(result)).toEqual(Object.keys(expected));
548
- expect(result).toEqual(expected);
543
+ expect(result).toMatchInlineSnapshot(`
544
+ Map {
545
+ "int" => "integer",
546
+ "string" => "string",
547
+ "newColumn" => "string",
548
+ "bool" => "boolean",
549
+ "datetime" => "datetime",
550
+ }
551
+ `);
549
552
  });
550
553
 
551
554
  it("should insert a new column at the end", () => {
@@ -557,16 +560,15 @@ describe("modifyColumnFields", () => {
557
560
  dataType: "datetime",
558
561
  });
559
562
 
560
- const expected = {
561
- int: "integer",
562
- string: "string",
563
- bool: "boolean",
564
- datetime: "datetime",
565
- newColumn: "datetime", // Set to the same type as the data type
566
- };
567
-
568
- expect(Object.keys(result)).toEqual(Object.keys(expected));
569
- expect(result).toEqual(expected);
563
+ expect(result).toMatchInlineSnapshot(`
564
+ Map {
565
+ "int" => "integer",
566
+ "string" => "string",
567
+ "bool" => "boolean",
568
+ "datetime" => "datetime",
569
+ "newColumn" => "datetime",
570
+ }
571
+ `);
570
572
  });
571
573
 
572
574
  it("should insert a new column beyond the array length", () => {
@@ -577,16 +579,15 @@ describe("modifyColumnFields", () => {
577
579
  newColumnName: "newColumn",
578
580
  });
579
581
 
580
- const expected = {
581
- int: "integer",
582
- string: "string",
583
- bool: "boolean",
584
- datetime: "datetime",
585
- newColumn: "string",
586
- };
587
-
588
- expect(Object.keys(result)).toEqual(Object.keys(expected));
589
- expect(result).toEqual(expected);
582
+ expect(result).toMatchInlineSnapshot(`
583
+ Map {
584
+ "int" => "integer",
585
+ "string" => "string",
586
+ "bool" => "boolean",
587
+ "datetime" => "datetime",
588
+ "newColumn" => "string",
589
+ }
590
+ `);
590
591
  });
591
592
 
592
593
  it("should remove column at index 0", () => {
@@ -596,14 +597,13 @@ describe("modifyColumnFields", () => {
596
597
  type: "remove",
597
598
  });
598
599
 
599
- const expected = {
600
- string: "string",
601
- bool: "boolean",
602
- datetime: "datetime",
603
- };
604
-
605
- expect(Object.keys(result)).toEqual(Object.keys(expected));
606
- expect(result).toEqual(expected);
600
+ expect(result).toMatchInlineSnapshot(`
601
+ Map {
602
+ "string" => "string",
603
+ "bool" => "boolean",
604
+ "datetime" => "datetime",
605
+ }
606
+ `);
607
607
  });
608
608
 
609
609
  it("should remove column at index 1", () => {
@@ -613,14 +613,13 @@ describe("modifyColumnFields", () => {
613
613
  type: "remove",
614
614
  });
615
615
 
616
- const expected = {
617
- int: "integer",
618
- bool: "boolean",
619
- datetime: "datetime",
620
- };
621
-
622
- expect(Object.keys(result)).toEqual(Object.keys(expected));
623
- expect(result).toEqual(expected);
616
+ expect(result).toMatchInlineSnapshot(`
617
+ Map {
618
+ "int" => "integer",
619
+ "bool" => "boolean",
620
+ "datetime" => "datetime",
621
+ }
622
+ `);
624
623
  });
625
624
 
626
625
  it("should remove column at index 2", () => {
@@ -630,14 +629,13 @@ describe("modifyColumnFields", () => {
630
629
  type: "remove",
631
630
  });
632
631
 
633
- const expected = {
634
- int: "integer",
635
- string: "string",
636
- datetime: "datetime",
637
- };
638
-
639
- expect(Object.keys(result)).toEqual(Object.keys(expected));
640
- expect(result).toEqual(expected);
632
+ expect(result).toMatchInlineSnapshot(`
633
+ Map {
634
+ "int" => "integer",
635
+ "string" => "string",
636
+ "datetime" => "datetime",
637
+ }
638
+ `);
641
639
  });
642
640
 
643
641
  it("should remove column at index 3", () => {
@@ -647,14 +645,13 @@ describe("modifyColumnFields", () => {
647
645
  type: "remove",
648
646
  });
649
647
 
650
- const expected = {
651
- int: "integer",
652
- string: "string",
653
- bool: "boolean",
654
- };
655
-
656
- expect(Object.keys(result)).toEqual(Object.keys(expected));
657
- expect(result).toEqual(expected);
648
+ expect(result).toMatchInlineSnapshot(`
649
+ Map {
650
+ "int" => "integer",
651
+ "string" => "string",
652
+ "bool" => "boolean",
653
+ }
654
+ `);
658
655
  });
659
656
 
660
657
  it("should handle removing non-existent column index", () => {
@@ -685,15 +682,14 @@ describe("modifyColumnFields", () => {
685
682
  newColumnName: "number",
686
683
  });
687
684
 
688
- const expected = {
689
- number: "string", // Defaults to string type for renamed columns
690
- string: "string",
691
- bool: "boolean",
692
- datetime: "datetime",
693
- };
694
-
695
- expect(Object.keys(result)).toEqual(Object.keys(expected));
696
- expect(result).toEqual(expected);
685
+ expect(result).toMatchInlineSnapshot(`
686
+ Map {
687
+ "number" => "string",
688
+ "string" => "string",
689
+ "bool" => "boolean",
690
+ "datetime" => "datetime",
691
+ }
692
+ `);
697
693
  });
698
694
 
699
695
  it("should rename column at index 1", () => {
@@ -704,15 +700,14 @@ describe("modifyColumnFields", () => {
704
700
  newColumnName: "text",
705
701
  });
706
702
 
707
- const expected = {
708
- int: "integer",
709
- text: "string", // Defaults to string type for renamed columns
710
- bool: "boolean",
711
- datetime: "datetime",
712
- };
713
-
714
- expect(Object.keys(result)).toEqual(Object.keys(expected));
715
- expect(result).toEqual(expected);
703
+ expect(result).toMatchInlineSnapshot(`
704
+ Map {
705
+ "int" => "integer",
706
+ "text" => "string",
707
+ "bool" => "boolean",
708
+ "datetime" => "datetime",
709
+ }
710
+ `);
716
711
  });
717
712
 
718
713
  it("should rename column at index 3", () => {
@@ -724,15 +719,14 @@ describe("modifyColumnFields", () => {
724
719
  newColumnName: "timestamp",
725
720
  });
726
721
 
727
- const expected = {
728
- int: "integer",
729
- string: "string",
730
- bool: "boolean",
731
- timestamp: "datetime",
732
- };
733
-
734
- expect(Object.keys(result)).toEqual(Object.keys(expected));
735
- expect(result).toEqual(expected);
722
+ expect(result).toMatchInlineSnapshot(`
723
+ Map {
724
+ "int" => "integer",
725
+ "string" => "string",
726
+ "bool" => "boolean",
727
+ "timestamp" => "datetime",
728
+ }
729
+ `);
736
730
  });
737
731
 
738
732
  it("should handle renaming non-existent column index", () => {
@@ -758,7 +752,7 @@ describe("modifyColumnFields", () => {
758
752
  });
759
753
 
760
754
  it("should preserve original field types structure", () => {
761
- const originalFieldTypes = { ...testFieldTypes };
755
+ const originalFieldTypes = new Map(testFieldTypes);
762
756
  modifyColumnFields({
763
757
  columnFields: testFieldTypes,
764
758
  columnIdx: 1,
@@ -769,7 +763,7 @@ describe("modifyColumnFields", () => {
769
763
  });
770
764
 
771
765
  it("should handle empty field types object", () => {
772
- const emptyFieldTypes: FieldTypes = {};
766
+ const emptyFieldTypes: FieldTypes = new Map();
773
767
 
774
768
  // Insert
775
769
  const insertResult = modifyColumnFields({
@@ -778,7 +772,11 @@ describe("modifyColumnFields", () => {
778
772
  type: "insert",
779
773
  newColumnName: "newColumn",
780
774
  });
781
- expect(insertResult).toEqual({ newColumn: "string" });
775
+ expect(insertResult).toMatchInlineSnapshot(`
776
+ Map {
777
+ "newColumn" => "string",
778
+ }
779
+ `);
782
780
 
783
781
  // Remove
784
782
  const removeResult = modifyColumnFields({
@@ -786,16 +784,16 @@ describe("modifyColumnFields", () => {
786
784
  columnIdx: 0,
787
785
  type: "remove",
788
786
  });
789
- expect(removeResult).toEqual({});
787
+ expect(removeResult).toMatchInlineSnapshot(`Map {}`);
790
788
  });
791
789
 
792
790
  it("should handle field types with special characters in column names", () => {
793
- const specialFieldTypes: FieldTypes = {
791
+ const specialFieldTypes: FieldTypes = asFieldTypes({
794
792
  "column-with-dash": "integer",
795
793
  column_with_underscore: "string",
796
794
  "column.with.dot": "boolean",
797
795
  "column with space": "datetime",
798
- };
796
+ });
799
797
 
800
798
  // Insert
801
799
  const insertResult = modifyColumnFields({
@@ -804,15 +802,15 @@ describe("modifyColumnFields", () => {
804
802
  type: "insert",
805
803
  newColumnName: "new-column",
806
804
  });
807
- const insertExpected = {
808
- "column-with-dash": "integer",
809
- "new-column": "string",
810
- column_with_underscore: "string",
811
- "column.with.dot": "boolean",
812
- "column with space": "datetime",
813
- };
814
- expect(Object.keys(insertResult)).toEqual(Object.keys(insertExpected));
815
- expect(insertResult).toEqual(insertExpected);
805
+ expect(insertResult).toMatchInlineSnapshot(`
806
+ Map {
807
+ "column-with-dash" => "integer",
808
+ "new-column" => "string",
809
+ "column_with_underscore" => "string",
810
+ "column.with.dot" => "boolean",
811
+ "column with space" => "datetime",
812
+ }
813
+ `);
816
814
 
817
815
  // Remove
818
816
  const removeResult = modifyColumnFields({
@@ -820,13 +818,13 @@ describe("modifyColumnFields", () => {
820
818
  columnIdx: 1,
821
819
  type: "remove",
822
820
  });
823
- const removeExpected = {
824
- "column-with-dash": "integer",
825
- "column.with.dot": "boolean",
826
- "column with space": "datetime",
827
- };
828
- expect(Object.keys(removeResult)).toEqual(Object.keys(removeExpected));
829
- expect(removeResult).toEqual(removeExpected);
821
+ expect(removeResult).toMatchInlineSnapshot(`
822
+ Map {
823
+ "column-with-dash" => "integer",
824
+ "column.with.dot" => "boolean",
825
+ "column with space" => "datetime",
826
+ }
827
+ `);
830
828
 
831
829
  // Rename
832
830
  const renameResult = modifyColumnFields({
@@ -835,14 +833,14 @@ describe("modifyColumnFields", () => {
835
833
  type: "rename",
836
834
  newColumnName: "renamed-column",
837
835
  });
838
- const renameExpected = {
839
- "column-with-dash": "integer",
840
- "renamed-column": "string",
841
- "column.with.dot": "boolean",
842
- "column with space": "datetime",
843
- };
844
- expect(Object.keys(renameResult)).toEqual(Object.keys(renameExpected));
845
- expect(renameResult).toEqual(renameExpected);
836
+ expect(renameResult).toMatchInlineSnapshot(`
837
+ Map {
838
+ "column-with-dash" => "integer",
839
+ "renamed-column" => "string",
840
+ "column.with.dot" => "boolean",
841
+ "column with space" => "datetime",
842
+ }
843
+ `);
846
844
  });
847
845
 
848
846
  it("should handle multiple operations in sequence", () => {
@@ -864,13 +862,13 @@ describe("modifyColumnFields", () => {
864
862
  newColumnName: "renamed",
865
863
  });
866
864
 
867
- const expected = {
868
- renamed: "string",
869
- newColumn: "string",
870
- bool: "boolean",
871
- datetime: "datetime",
872
- };
873
- expect(Object.keys(result)).toEqual(Object.keys(expected));
874
- expect(result).toEqual(expected);
865
+ expect(result).toMatchInlineSnapshot(`
866
+ Map {
867
+ "renamed" => "string",
868
+ "newColumn" => "string",
869
+ "bool" => "boolean",
870
+ "datetime" => "datetime",
871
+ }
872
+ `);
875
873
  });
876
874
  });
@@ -79,24 +79,25 @@ export function modifyColumnFields(opts: {
79
79
  return columnFields;
80
80
  }
81
81
 
82
- const entries = Object.entries(columnFields);
83
- const newEntries = [
82
+ const entries = [...columnFields.entries()];
83
+ const newEntries: Array<[string, DataType]> = [
84
84
  ...entries.slice(0, columnIdx),
85
85
  [newColumnName, dataType || "string"],
86
86
  ...entries.slice(columnIdx),
87
87
  ];
88
- return Object.fromEntries(newEntries);
88
+ return new Map(newEntries);
89
89
  }
90
90
  case "remove": {
91
- if (columnIdx < 0 || columnIdx >= Object.keys(columnFields).length) {
91
+ if (columnIdx < 0 || columnIdx >= columnFields.size) {
92
92
  return columnFields;
93
93
  }
94
94
 
95
- const entries = Object.entries(columnFields);
95
+ const entries = [...columnFields.entries()];
96
96
  const columnName = entries[columnIdx]?.[0];
97
97
  if (columnName) {
98
- const { [columnName]: _, ...rest } = columnFields;
99
- return rest;
98
+ const next = new Map(columnFields);
99
+ next.delete(columnName);
100
+ return next;
100
101
  }
101
102
  return columnFields;
102
103
  }
@@ -106,18 +107,18 @@ export function modifyColumnFields(opts: {
106
107
  return columnFields;
107
108
  }
108
109
 
109
- if (columnIdx < 0 || columnIdx >= Object.keys(columnFields).length) {
110
+ if (columnIdx < 0 || columnIdx >= columnFields.size) {
110
111
  return columnFields;
111
112
  }
112
113
 
113
114
  // Rename at the right index
114
- const entries = Object.entries(columnFields);
115
- const newEntries = [
115
+ const entries = [...columnFields.entries()];
116
+ const newEntries: Array<[string, DataType]> = [
116
117
  ...entries.slice(0, columnIdx),
117
118
  [newColumnName, dataType || "string"],
118
119
  ...entries.slice(columnIdx + 1),
119
120
  ];
120
- return Object.fromEntries(newEntries);
121
+ return new Map(newEntries);
121
122
  }
122
123
  }
123
124
  }
@@ -102,7 +102,7 @@ export const GlideDataEditor = <T,>({
102
102
 
103
103
  const columns: ModifiedGridColumn[] = useMemo(() => {
104
104
  const columns: ModifiedGridColumn[] = [];
105
- for (const [columnName, fieldType] of Object.entries(columnFields)) {
105
+ for (const [columnName, fieldType] of columnFields) {
106
106
  const editable =
107
107
  editableColumns === "all" || editableColumns.includes(columnName);
108
108
 
@@ -311,7 +311,7 @@ export const GlideDataEditor = <T,>({
311
311
  const [col, _row] = cell;
312
312
  const key = columns[col].title;
313
313
 
314
- const columnType = columnFields[key];
314
+ const columnType = columnFields.get(key);
315
315
  // Verify the new value is of the correct type
316
316
  switch (columnType) {
317
317
  case "number":
@@ -445,7 +445,7 @@ export const GlideDataEditor = <T,>({
445
445
  const oldColumnName = columns[menu.col].title;
446
446
 
447
447
  // Validate the new column name
448
- if (columnFields[newName]) {
448
+ if (columnFields.has(newName)) {
449
449
  toastColumnExists(newName);
450
450
  return;
451
451
  }
@@ -498,7 +498,7 @@ export const GlideDataEditor = <T,>({
498
498
  const clampedColumnIdx = Math.max(0, Math.min(columnIdx, columns.length));
499
499
 
500
500
  // Validate the new column name
501
- if (columnFields[columnName]) {
501
+ if (columnFields.has(columnName)) {
502
502
  toastColumnExists(columnName);
503
503
  return;
504
504
  }