@marimo-team/frontend 0.21.2-dev27 → 0.21.2-dev28

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 (57) hide show
  1. package/dist/assets/JsonOutput-BKsCyU13.js +46 -0
  2. package/dist/assets/{LazyAnyLanguageCodeMirror-m8w66E4s.js → LazyAnyLanguageCodeMirror-Dmh6kfiy.js} +2 -2
  3. package/dist/assets/{RenderHTML-C_fyTOvC.js → RenderHTML-D_zT74Lq.js} +1 -1
  4. package/dist/assets/{add-cell-with-ai-BRtYorDJ.js → add-cell-with-ai-mTm-dtan.js} +1 -1
  5. package/dist/assets/{agent-panel-DGJVT16i.js → agent-panel-B33uC899.js} +4 -4
  6. package/dist/assets/{ai-model-dropdown-DudGwo8M.js → ai-model-dropdown-aWQwR5QF.js} +1 -1
  7. package/dist/assets/{any-language-editor-DwQMnAM3.js → any-language-editor-C4OP3txB.js} +1 -1
  8. package/dist/assets/{app-config-button-DpzUMkw9.js → app-config-button-BPvtPnFO.js} +1 -1
  9. package/dist/assets/{cell-editor-2jUky7HX.js → cell-editor-CphJ_7RI.js} +1 -1
  10. package/dist/assets/{chat-display-CUgnBuwx.js → chat-display-B6a8fK7j.js} +1 -1
  11. package/dist/assets/{chat-panel-BOonHbD0.js → chat-panel-B247iLHO.js} +1 -1
  12. package/dist/assets/{column-preview-DxF69H1r.js → column-preview-CtAbPvyW.js} +1 -1
  13. package/dist/assets/{command-palette-DOuVk9HJ.js → command-palette-Ba8b6UXV.js} +1 -1
  14. package/dist/assets/{common-lw1c9Wgv.js → common-DrrPhv3x.js} +1 -1
  15. package/dist/assets/{context-aware-panel-BvlpddI6.js → context-aware-panel-91O6JrAw.js} +3 -3
  16. package/dist/assets/copy-CA4fP28I.js +1 -0
  17. package/dist/assets/{copy-icon-BgmMM9Zg.js → copy-icon-C6n2vWSW.js} +1 -1
  18. package/dist/assets/{dependency-graph-panel-8tHjw4xN.js → dependency-graph-panel-BHHmugpa.js} +1 -1
  19. package/dist/assets/{documentation-panel-CiqkAFTI.js → documentation-panel-C0LXfz0P.js} +1 -1
  20. package/dist/assets/{edit-page-BVY1l3nw.js → edit-page-DrMpMRcp.js} +3 -3
  21. package/dist/assets/{file-explorer-panel-_OxGV-EN.js → file-explorer-panel-BS6bXHdR.js} +1 -1
  22. package/dist/assets/{glide-data-editor-xt5xNZeV.js → glide-data-editor-CoE8cs_o.js} +1 -1
  23. package/dist/assets/{home-page-h9VVoEDu.js → home-page-D7Z_Tlyv.js} +1 -1
  24. package/dist/assets/{hooks-Dy_0RQ1Z.js → hooks-3nahTPnh.js} +1 -1
  25. package/dist/assets/index-CIk_2P5O.js +38 -0
  26. package/dist/assets/{layout-BC0sIu2P.js → layout-fT20kG4_.js} +3 -3
  27. package/dist/assets/{markdown-renderer-Cj9ft7VI.js → markdown-renderer-A3Ed0cjI.js} +1 -1
  28. package/dist/assets/{packages-panel-B1mp4FDH.js → packages-panel-BU9XDexQ.js} +1 -1
  29. package/dist/assets/{panels-DQcWYSGG.js → panels-BQFJvWY0.js} +1 -1
  30. package/dist/assets/{readonly-python-code-CKdreYF8.js → readonly-python-code-i_LeNopW.js} +1 -1
  31. package/dist/assets/{run-page-BVFSk_-Z.js → run-page-DRNJCJFp.js} +1 -1
  32. package/dist/assets/{scratchpad-panel-DKj6qzI8.js → scratchpad-panel-DlQ1e62a.js} +1 -1
  33. package/dist/assets/{secrets-panel-B1Z-6dmz.js → secrets-panel-CfWyTaso.js} +1 -1
  34. package/dist/assets/{session-panel-C4ctrCWJ.js → session-panel-B1yjHqHu.js} +1 -1
  35. package/dist/assets/{snippets-panel-DXimlux2.js → snippets-panel-Dk7Ka2Gd.js} +1 -1
  36. package/dist/assets/{terminal-Cr7wbEjz.js → terminal-BZZqbl0D.js} +1 -1
  37. package/dist/assets/{useBoolean-3u0CYJkt.js → useBoolean-BYlCXj8U.js} +1 -1
  38. package/dist/assets/{useCellActionButton-CL2hZzP1.js → useCellActionButton-D-FPtHfb.js} +1 -1
  39. package/dist/assets/{useDependencyPanelTab-uG7OU1qF.js → useDependencyPanelTab-CYwz2tNi.js} +1 -1
  40. package/dist/assets/{useNotebookActions-DkhkeIz3.js → useNotebookActions-BeK3aC7s.js} +1 -1
  41. package/dist/index.html +14 -14
  42. package/package.json +1 -1
  43. package/src/components/data-table/__tests__/utils.test.ts +138 -1
  44. package/src/components/data-table/context-menu.tsx +9 -5
  45. package/src/components/data-table/data-table.tsx +3 -0
  46. package/src/components/data-table/range-focus/__tests__/atoms.test.ts +8 -2
  47. package/src/components/data-table/range-focus/__tests__/test-utils.ts +2 -0
  48. package/src/components/data-table/range-focus/__tests__/utils.test.ts +82 -8
  49. package/src/components/data-table/range-focus/atoms.ts +2 -2
  50. package/src/components/data-table/range-focus/utils.ts +50 -12
  51. package/src/components/data-table/types.ts +7 -0
  52. package/src/components/data-table/utils.ts +87 -0
  53. package/src/plugins/impl/DataTablePlugin.tsx +23 -2
  54. package/src/utils/copy.ts +18 -5
  55. package/dist/assets/JsonOutput-BRPU4yqw.js +0 -46
  56. package/dist/assets/copy-YwM0Pd7v.js +0 -1
  57. package/dist/assets/index-D3_EYEUd.js +0 -38
@@ -20,7 +20,8 @@ describe("getCellValues", () => {
20
20
  it("should return empty string for empty selection", () => {
21
21
  const mockTable = createMockTable([], []);
22
22
  const result = getCellValues(mockTable, new Set());
23
- expect(result).toBe("");
23
+ expect(result.text).toBe("");
24
+ expect(result.html).toBeUndefined();
24
25
  });
25
26
 
26
27
  it("should ignore select checkbox in tables", () => {
@@ -29,7 +30,8 @@ describe("getCellValues", () => {
29
30
  const table = createMockTable([row], []);
30
31
 
31
32
  const result = getCellValues(table, new Set());
32
- expect(result).toBe("");
33
+ expect(result.text).toBe("");
34
+ expect(result.html).toBeUndefined();
33
35
  });
34
36
 
35
37
  it("should return single cell value", () => {
@@ -38,7 +40,8 @@ describe("getCellValues", () => {
38
40
  const table = createMockTable([row], []);
39
41
 
40
42
  const result = getCellValues(table, new Set(["0_0"]));
41
- expect(result).toBe("test");
43
+ expect(result.text).toBe("test");
44
+ expect(result.html).toBeUndefined();
42
45
  });
43
46
 
44
47
  it("should return multiple cells from same row separated by tabs", () => {
@@ -48,7 +51,8 @@ describe("getCellValues", () => {
48
51
  const table = createMockTable([row], []);
49
52
 
50
53
  const result = getCellValues(table, new Set(["0_0", "0_1"]));
51
- expect(result).toBe("value1\tvalue2");
54
+ expect(result.text).toBe("value1\tvalue2");
55
+ expect(result.html).toBeUndefined();
52
56
  });
53
57
 
54
58
  it("should return multiple rows separated by newlines", () => {
@@ -59,7 +63,8 @@ describe("getCellValues", () => {
59
63
  const table = createMockTable([row1, row2], []);
60
64
 
61
65
  const result = getCellValues(table, new Set(["0_0", "1_0"]));
62
- expect(result).toBe("row1\nrow2");
66
+ expect(result.text).toBe("row1\nrow2");
67
+ expect(result.html).toBeUndefined();
63
68
  });
64
69
 
65
70
  it("should handle missing rows gracefully", () => {
@@ -69,7 +74,7 @@ describe("getCellValues", () => {
69
74
 
70
75
  // Row "999" doesn't exist and is skipped
71
76
  const result = getCellValues(table, new Set(["0_0", "999_0"]));
72
- expect(result).toBe("test");
77
+ expect(result.text).toBe("test");
73
78
  });
74
79
 
75
80
  it("should include undefined for missing columns on existing rows", () => {
@@ -80,7 +85,7 @@ describe("getCellValues", () => {
80
85
 
81
86
  // Column "999" doesn't exist but row.getValue() returns undefined
82
87
  const result = getCellValues(table, new Set(["0_0", "0_1", "0_999"]));
83
- expect(result).toBe("test1\ttest2\tundefined");
88
+ expect(result.text).toBe("test1\ttest2\tundefined");
84
89
  });
85
90
 
86
91
  it("should handle complex data types", () => {
@@ -91,7 +96,76 @@ describe("getCellValues", () => {
91
96
  const table = createMockTable([row], []);
92
97
 
93
98
  const result = getCellValues(table, new Set(["0_0", "0_1", "0_2"]));
94
- expect(result).toBe('{"name":"test"}\tnull\tundefined');
99
+ expect(result.text).toBe('{"name":"test"}\tnull\tundefined');
100
+ expect(result.html).toBeUndefined();
101
+ });
102
+
103
+ it("should use raw values when rawData is available", () => {
104
+ const mimeBundle = {
105
+ _serialized_mime_bundle: {
106
+ mimetype: "text/html",
107
+ data: "<b>formatted_42</b>",
108
+ },
109
+ };
110
+ const cell1 = createMockCell("0_a", mimeBundle);
111
+ const cell2 = createMockCell("0_b", "displayed_b");
112
+ const row = createMockRow("0", [cell1, cell2]);
113
+ const table = createMockTable([row], [], {
114
+ rawData: [{ a: 42, b: "raw_b" }],
115
+ });
116
+
117
+ const result = getCellValues(table, new Set(["0_a", "0_b"]));
118
+ expect(result.text).toBe("42\traw_b");
119
+ expect(result.html).toBe(
120
+ "<table><tr><td><b>formatted_42</b></td><td>raw_b</td></tr></table>",
121
+ );
122
+ });
123
+
124
+ it("should fall back to displayed value when rawData is not set", () => {
125
+ const cell = createMockCell("0_name", "displayed");
126
+ const row = createMockRow("0", [cell]);
127
+ const table = createMockTable([row], []);
128
+
129
+ const result = getCellValues(table, new Set(["0_name"]));
130
+ expect(result.text).toBe("displayed");
131
+ expect(result.html).toBeUndefined();
132
+ });
133
+
134
+ it("should return html table when cells contain mime bundles", () => {
135
+ const mimeBundle = {
136
+ _serialized_mime_bundle: {
137
+ mimetype: "text/html",
138
+ data: '<a href="https://example.com">link</a>',
139
+ },
140
+ };
141
+ const cell1 = createMockCell("0_url", mimeBundle);
142
+ const cell2 = createMockCell("0_name", "plain text");
143
+ const row = createMockRow("0", [cell1, cell2]);
144
+ const table = createMockTable([row], []);
145
+
146
+ const result = getCellValues(table, new Set(["0_url", "0_name"]));
147
+ expect(result.text).toBe("link\tplain text");
148
+ expect(result.html).toBe(
149
+ '<table><tr><td><a href="https://example.com">link</a></td><td>plain text</td></tr></table>',
150
+ );
151
+ });
152
+
153
+ it("should escape html entities in non-mime cells within html table", () => {
154
+ const mimeBundle = {
155
+ _serialized_mime_bundle: {
156
+ mimetype: "text/html",
157
+ data: "<b>bold</b>",
158
+ },
159
+ };
160
+ const cell1 = createMockCell("0_html", mimeBundle);
161
+ const cell2 = createMockCell("0_text", "a < b & c > d");
162
+ const row = createMockRow("0", [cell1, cell2]);
163
+ const table = createMockTable([row], []);
164
+
165
+ const result = getCellValues(table, new Set(["0_html", "0_text"]));
166
+ expect(result.html).toBe(
167
+ "<table><tr><td><b>bold</b></td><td>a &lt; b &amp; c &gt; d</td></tr></table>",
168
+ );
95
169
  });
96
170
  });
97
171
 
@@ -167,8 +167,8 @@ const {
167
167
  return state;
168
168
  }
169
169
 
170
- const text = getCellValues(table, state.selectedCells);
171
- copyToClipboard(text);
170
+ const { text, html } = getCellValues(table, state.selectedCells);
171
+ copyToClipboard(text, html);
172
172
  onCopyComplete();
173
173
 
174
174
  return {
@@ -2,17 +2,25 @@
2
2
 
3
3
  import type { Table } from "@tanstack/react-table";
4
4
  import { SELECT_COLUMN_ID } from "../types";
5
- import { stringifyUnknownValue } from "../utils";
5
+ import { getClipboardContent, getRawValue } from "../utils";
6
6
  import type { SelectedCell } from "./atoms";
7
7
 
8
+ export interface CellValuesResult {
9
+ text: string;
10
+ html: string | undefined;
11
+ }
12
+
8
13
  /**
9
- * Get the values of the selected cells.
14
+ * Get the values of the selected cells, preferring raw (unformatted) values
15
+ * for plain text. If any cell contains HTML (e.g. a hyperlink), also builds
16
+ * an HTML table so rich content can be preserved on paste.
10
17
  */
11
18
  export function getCellValues<TData>(
12
19
  table: Table<TData>,
13
20
  selectedCellIds: Set<string>,
14
- ): string {
15
- const rowValues = new Map<string, string[]>();
21
+ ): CellValuesResult {
22
+ const rows = new Map<string, { text: string; html?: string }[]>();
23
+ let hasHtml = false;
16
24
 
17
25
  for (const cellId of selectedCellIds) {
18
26
  if (cellId.includes(SELECT_COLUMN_ID)) {
@@ -26,17 +34,46 @@ export function getCellValues<TData>(
26
34
  continue;
27
35
  }
28
36
 
29
- const cellValue = row.getValue(columnId);
30
- const values = rowValues.get(rowId) ?? [];
31
- values.push(stringifyUnknownValue({ value: cellValue }));
32
- rowValues.set(rowId, values);
37
+ const rawValue = getRawValue(table, row.index, columnId);
38
+ const { text, html } = getClipboardContent(
39
+ rawValue,
40
+ row.getValue(columnId),
41
+ );
42
+
43
+ if (html) {
44
+ hasHtml = true;
45
+ }
46
+ const cells = rows.get(rowId) ?? [];
47
+ cells.push({ text, html });
48
+ rows.set(rowId, cells);
49
+ }
50
+
51
+ const rowValues = [...rows.values()];
52
+ const tabSeparatedText = rowValues
53
+ .map((cells) => cells.map((c) => c.text).join("\t"))
54
+ .join("\n");
55
+
56
+ let htmlTable: string | undefined;
57
+ if (hasHtml) {
58
+ const htmlTableRows = rowValues
59
+ .map(
60
+ (cells) =>
61
+ `<tr>${cells.map((c) => `<td>${c.html ?? escapeHtml(c.text)}</td>`).join("")}</tr>`,
62
+ )
63
+ .join("");
64
+ htmlTable = `<table>${htmlTableRows}</table>`;
33
65
  }
34
66
 
35
- return getTabSeparatedValues([...rowValues.values()]);
67
+ return {
68
+ text: tabSeparatedText,
69
+ html: htmlTable,
70
+ };
36
71
  }
37
72
 
38
- export function getTabSeparatedValues(values: string[][]) {
39
- return values.map((row) => row.join("\t")).join("\n");
73
+ function escapeHtml(str: string): string {
74
+ const div = document.createElement("div");
75
+ div.textContent = str;
76
+ return div.innerHTML;
40
77
  }
41
78
 
42
79
  /**
@@ -74,7 +111,8 @@ export function getNumericValuesFromSelectedCells<TData>(
74
111
  continue;
75
112
  }
76
113
 
77
- const value = row.getValue(columnId);
114
+ const value =
115
+ getRawValue(table, row.index, columnId) ?? row.getValue(columnId);
78
116
 
79
117
  // Only accept numbers and strings
80
118
  // Skip booleans, null, etc.
@@ -1,8 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { RowData } from "@tanstack/react-table";
3
4
  import type { DataType } from "@/core/kernel/messages";
4
5
  import { Objects } from "@/utils/objects";
5
6
 
7
+ declare module "@tanstack/react-table" {
8
+ interface TableMeta<TData extends RowData> {
9
+ rawData?: TData[]; // raw data for filtering/copying (present only if format_mapping is provided)
10
+ }
11
+ }
12
+
6
13
  export type ColumnName = string;
7
14
 
8
15
  export const ColumnHeaderStatsKeys = [
@@ -1,8 +1,10 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { Table } from "@tanstack/react-table";
3
4
  import type { TableData } from "@/plugins/impl/DataTablePlugin";
4
5
  import { vegaLoadData } from "@/plugins/impl/vega/loader";
5
6
  import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
7
+ import { getMimeValues } from "./mime-cell";
6
8
  import { INDEX_COLUMN_NAME } from "./types";
7
9
 
8
10
  /**
@@ -32,6 +34,21 @@ export async function loadTableData<T = object>(
32
34
  return tableData;
33
35
  }
34
36
 
37
+ /**
38
+ * Load both table and raw table data. Raw table data is typically when
39
+ * there is formatting applied to the table data.
40
+ */
41
+ export async function loadTableAndRawData<T>(
42
+ tableData: TableData<T>,
43
+ rawTableData?: TableData<T> | null,
44
+ ): Promise<[T[], T[] | undefined]> {
45
+ if (rawTableData) {
46
+ return Promise.all([loadTableData(tableData), loadTableData(rawTableData)]);
47
+ }
48
+
49
+ return [await loadTableData(tableData), undefined];
50
+ }
51
+
35
52
  /**
36
53
  * Get the stable row ID for a row.
37
54
  *
@@ -87,3 +104,73 @@ export function stringifyUnknownValue(opts: {
87
104
  }
88
105
  return String(value);
89
106
  }
107
+
108
+ function stripHtml(html: string): string {
109
+ const div = document.createElement("div");
110
+ div.innerHTML = html;
111
+ const text = (div.textContent || div.innerText || "").trim();
112
+ return text || html;
113
+ }
114
+
115
+ const HTML_MIMETYPES = new Set(["text/html", "text/markdown"]);
116
+
117
+ function isRecord(value: unknown): value is Record<string, unknown> {
118
+ return value !== null && typeof value === "object" && !Array.isArray(value);
119
+ }
120
+
121
+ /**
122
+ * Get clipboard-ready text and optional HTML for a cell.
123
+ *
124
+ * @param rawValue - The raw (unformatted) value, or undefined if not available.
125
+ * @param displayedValue - The displayed value (may be a mime bundle).
126
+ */
127
+ export function getClipboardContent(
128
+ rawValue: unknown,
129
+ displayedValue: unknown,
130
+ ): { text: string; html?: string } {
131
+ const mimeValues =
132
+ typeof displayedValue === "object" && displayedValue !== null
133
+ ? getMimeValues(displayedValue)
134
+ : undefined;
135
+
136
+ let html: string | undefined;
137
+ if (mimeValues) {
138
+ // text/markdown from mo.md() contains rendered HTML
139
+ const htmlParts = mimeValues
140
+ .filter((v) => HTML_MIMETYPES.has(v.mimetype))
141
+ .map((v) => v.data);
142
+ html = htmlParts.length > 0 ? htmlParts.join("") : undefined;
143
+ }
144
+
145
+ let text: string;
146
+ if (rawValue !== undefined && rawValue !== displayedValue) {
147
+ text = stringifyUnknownValue({ value: rawValue });
148
+ } else if (mimeValues) {
149
+ text = mimeValues
150
+ .map((v) => (HTML_MIMETYPES.has(v.mimetype) ? stripHtml(v.data) : v.data))
151
+ .join(", ");
152
+ } else {
153
+ text = stringifyUnknownValue({ value: displayedValue });
154
+ }
155
+
156
+ return { text, html };
157
+ }
158
+
159
+ /**
160
+ * Get the raw (unformatted) value for a row/column from the table,
161
+ * or undefined if raw data is not available.
162
+ */
163
+ export function getRawValue<TData>(
164
+ table: Table<TData>,
165
+ rowIndex: number,
166
+ columnId: string,
167
+ ): unknown {
168
+ const rawData = table.options.meta?.rawData;
169
+ if (rawData) {
170
+ const rawRow = rawData[rowIndex];
171
+ if (isRecord(rawRow)) {
172
+ return rawRow[columnId];
173
+ }
174
+ }
175
+ return undefined;
176
+ }
@@ -53,6 +53,7 @@ import {
53
53
  } from "@/components/data-table/types";
54
54
  import {
55
55
  getPageIndexForRow,
56
+ loadTableAndRawData,
56
57
  loadTableData,
57
58
  } from "@/components/data-table/utils";
58
59
  import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
@@ -174,6 +175,7 @@ const valueCounts: z.ZodType<ValueCounts> = z.array(
174
175
  interface Data<T> {
175
176
  label: string | null;
176
177
  data: TableData<T>;
178
+ rawData?: TableData<T> | null;
177
179
  totalRows: number | TooManyRows;
178
180
  pagination: boolean;
179
181
  pageSize: number;
@@ -221,6 +223,7 @@ type DataTableFunctions = {
221
223
  total_rows: number | TooManyRows;
222
224
  cell_styles?: CellStyleState | null;
223
225
  cell_hover_texts?: Record<string, Record<string, string | null>> | null;
226
+ raw_data?: TableData<T> | null;
224
227
  }>;
225
228
  get_data_url?: GetDataUrl;
226
229
  get_row_ids?: GetRowIds;
@@ -243,6 +246,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
243
246
  ]),
244
247
  label: z.string().nullable(),
245
248
  data: z.union([z.string(), z.array(z.object({}).passthrough())]),
249
+ rawData: z.union([z.string(), z.array(z.looseObject({}))]).nullish(),
246
250
  totalRows: z.union([z.number(), z.literal(TOO_MANY_ROWS)]),
247
251
  pagination: z.boolean().default(false),
248
252
  pageSize: z.number().default(10),
@@ -327,6 +331,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
327
331
  )
328
332
  .nullable(),
329
333
  cell_hover_texts: cellHoverTextSchema.nullable(),
334
+ raw_data: z.union([z.string(), z.array(z.looseObject({}))]).nullish(),
330
335
  }),
331
336
  ),
332
337
  get_row_ids: rpc.input(z.object({}).passthrough()).output(
@@ -529,17 +534,23 @@ export const LoadingDataTableComponent = memo(
529
534
  // Data loading
530
535
  const { data, error, isPending, isFetching } = useAsyncData<{
531
536
  rows: T[];
537
+ rawRows?: T[];
532
538
  totalRows: number | TooManyRows;
533
539
  cellStyles: CellStyleState | undefined | null;
534
540
  cellHoverTexts?: Record<string, Record<string, string | null>> | null;
535
541
  }>(async () => {
536
542
  // If there is no data, return an empty array
537
543
  if (props.totalRows === 0) {
538
- return { rows: Arrays.EMPTY, totalRows: 0, cellStyles: {} };
544
+ return {
545
+ rows: Arrays.EMPTY,
546
+ totalRows: 0,
547
+ cellStyles: {},
548
+ };
539
549
  }
540
550
 
541
551
  // Table data is a url string or an array of objects
542
552
  let tableData = props.data;
553
+ let rawTableData: TableData<T> | undefined | null = props.rawData;
543
554
  let totalRows = props.totalRows;
544
555
  let cellStyles = props.cellStyles;
545
556
  let cellHoverTexts = props.cellHoverTexts;
@@ -587,13 +598,19 @@ export const LoadingDataTableComponent = memo(
587
598
  } else {
588
599
  const searchResults = await searchResultsPromise;
589
600
  tableData = searchResults.data;
601
+ rawTableData = searchResults.raw_data;
590
602
  totalRows = searchResults.total_rows;
591
603
  cellStyles = searchResults.cell_styles || {};
592
604
  cellHoverTexts = searchResults.cell_hover_texts || {};
593
605
  }
594
- tableData = await loadTableData(tableData);
606
+ const [data, rawData] = await loadTableAndRawData(
607
+ tableData,
608
+ rawTableData,
609
+ );
610
+ tableData = data;
595
611
  return {
596
612
  rows: tableData,
613
+ rawRows: rawData,
597
614
  totalRows: totalRows,
598
615
  cellStyles,
599
616
  cellHoverTexts,
@@ -715,6 +732,7 @@ export const LoadingDataTableComponent = memo(
715
732
  <DataTableComponent
716
733
  {...props}
717
734
  data={data?.rows ?? Arrays.EMPTY}
735
+ rawData={data?.rawRows}
718
736
  columnSummaries={columnSummaries}
719
737
  sorting={sorting}
720
738
  setSorting={setSorting}
@@ -766,6 +784,7 @@ LoadingDataTableComponent.displayName = "LoadingDataTableComponent";
766
784
  const DataTableComponent = ({
767
785
  label,
768
786
  data,
787
+ rawData,
769
788
  totalRows,
770
789
  maxColumns,
771
790
  pagination,
@@ -814,6 +833,7 @@ const DataTableComponent = ({
814
833
  }: DataTableProps<unknown> &
815
834
  DataTableSearchProps & {
816
835
  data: unknown[];
836
+ rawData?: unknown[];
817
837
  columnSummaries?: ColumnSummaries;
818
838
  getRow: (rowIdx: number) => Promise<GetRowResult>;
819
839
  }): JSX.Element => {
@@ -1015,6 +1035,7 @@ const DataTableComponent = ({
1015
1035
  <Labeled label={label} align="top" fullWidth={true}>
1016
1036
  <DataTable
1017
1037
  data={data}
1038
+ rawData={rawData}
1018
1039
  columns={columns}
1019
1040
  className={className}
1020
1041
  maxHeight={maxHeight}
package/src/utils/copy.ts CHANGED
@@ -2,21 +2,34 @@
2
2
  import { Logger } from "./Logger";
3
3
 
4
4
  /**
5
- * Tries to copy text to the clipboard using the navigator.clipboard API.
6
- * If that fails, it falls back to prompting the user to copy.
5
+ * Copy text to the clipboard. When `html` is provided, writes both
6
+ * text/html and text/plain so rich content (e.g. hyperlinks) is
7
+ * preserved when pasting into apps like Excel or Google Sheets.
7
8
  *
8
9
  * As of 2024-10-29, Safari does not support navigator.clipboard.writeText
9
10
  * when running localhost http.
10
11
  */
11
- export async function copyToClipboard(text: string) {
12
+ export async function copyToClipboard(text: string, html?: string) {
12
13
  if (navigator.clipboard === undefined) {
13
14
  Logger.warn("navigator.clipboard is not supported");
14
15
  window.prompt("Copy to clipboard: Ctrl+C, Enter", text);
15
16
  return;
16
17
  }
17
18
 
18
- await navigator.clipboard.writeText(text).catch(async () => {
19
- // Fallback to prompt
19
+ if (html && navigator.clipboard.write) {
20
+ try {
21
+ const item = new ClipboardItem({
22
+ "text/html": new Blob([html], { type: "text/html" }),
23
+ "text/plain": new Blob([text], { type: "text/plain" }),
24
+ });
25
+ await navigator.clipboard.write([item]);
26
+ return;
27
+ } catch {
28
+ Logger.warn("Failed to write rich text, falling back to plain text");
29
+ }
30
+ }
31
+
32
+ await navigator.clipboard.writeText(text).catch(() => {
20
33
  Logger.warn("Failed to copy to clipboard using navigator.clipboard");
21
34
  window.prompt("Copy to clipboard: Ctrl+C, Enter", text);
22
35
  });