@marimo-team/frontend 0.21.2-dev26 → 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.
- package/dist/assets/JsonOutput-BKsCyU13.js +46 -0
- package/dist/assets/{LazyAnyLanguageCodeMirror-m8w66E4s.js → LazyAnyLanguageCodeMirror-Dmh6kfiy.js} +2 -2
- package/dist/assets/{RenderHTML-C_fyTOvC.js → RenderHTML-D_zT74Lq.js} +1 -1
- package/dist/assets/{add-cell-with-ai-BRtYorDJ.js → add-cell-with-ai-mTm-dtan.js} +1 -1
- package/dist/assets/{agent-panel-DGJVT16i.js → agent-panel-B33uC899.js} +4 -4
- package/dist/assets/{ai-model-dropdown-DudGwo8M.js → ai-model-dropdown-aWQwR5QF.js} +1 -1
- package/dist/assets/{any-language-editor-DwQMnAM3.js → any-language-editor-C4OP3txB.js} +1 -1
- package/dist/assets/{app-config-button-DpzUMkw9.js → app-config-button-BPvtPnFO.js} +1 -1
- package/dist/assets/{cell-editor-2jUky7HX.js → cell-editor-CphJ_7RI.js} +1 -1
- package/dist/assets/{chat-display-CUgnBuwx.js → chat-display-B6a8fK7j.js} +1 -1
- package/dist/assets/{chat-panel-BOonHbD0.js → chat-panel-B247iLHO.js} +1 -1
- package/dist/assets/{column-preview-DxF69H1r.js → column-preview-CtAbPvyW.js} +1 -1
- package/dist/assets/{command-palette-DOuVk9HJ.js → command-palette-Ba8b6UXV.js} +1 -1
- package/dist/assets/{common-lw1c9Wgv.js → common-DrrPhv3x.js} +1 -1
- package/dist/assets/{context-aware-panel-BvlpddI6.js → context-aware-panel-91O6JrAw.js} +3 -3
- package/dist/assets/copy-CA4fP28I.js +1 -0
- package/dist/assets/{copy-icon-BgmMM9Zg.js → copy-icon-C6n2vWSW.js} +1 -1
- package/dist/assets/{dependency-graph-panel-8tHjw4xN.js → dependency-graph-panel-BHHmugpa.js} +1 -1
- package/dist/assets/{documentation-panel-CiqkAFTI.js → documentation-panel-C0LXfz0P.js} +1 -1
- package/dist/assets/{edit-page-BVY1l3nw.js → edit-page-DrMpMRcp.js} +3 -3
- package/dist/assets/{file-explorer-panel-_OxGV-EN.js → file-explorer-panel-BS6bXHdR.js} +1 -1
- package/dist/assets/{glide-data-editor-xt5xNZeV.js → glide-data-editor-CoE8cs_o.js} +1 -1
- package/dist/assets/{home-page-h9VVoEDu.js → home-page-D7Z_Tlyv.js} +1 -1
- package/dist/assets/{hooks-Dy_0RQ1Z.js → hooks-3nahTPnh.js} +1 -1
- package/dist/assets/index-CIk_2P5O.js +38 -0
- package/dist/assets/{layout-BC0sIu2P.js → layout-fT20kG4_.js} +3 -3
- package/dist/assets/{markdown-renderer-Cj9ft7VI.js → markdown-renderer-A3Ed0cjI.js} +1 -1
- package/dist/assets/{packages-panel-B1mp4FDH.js → packages-panel-BU9XDexQ.js} +1 -1
- package/dist/assets/{panels-DQcWYSGG.js → panels-BQFJvWY0.js} +1 -1
- package/dist/assets/{readonly-python-code-CKdreYF8.js → readonly-python-code-i_LeNopW.js} +1 -1
- package/dist/assets/{run-page-BVFSk_-Z.js → run-page-DRNJCJFp.js} +1 -1
- package/dist/assets/{scratchpad-panel-DKj6qzI8.js → scratchpad-panel-DlQ1e62a.js} +1 -1
- package/dist/assets/{secrets-panel-B1Z-6dmz.js → secrets-panel-CfWyTaso.js} +1 -1
- package/dist/assets/{session-panel-C4ctrCWJ.js → session-panel-B1yjHqHu.js} +1 -1
- package/dist/assets/{snippets-panel-DXimlux2.js → snippets-panel-Dk7Ka2Gd.js} +1 -1
- package/dist/assets/{terminal-Cr7wbEjz.js → terminal-BZZqbl0D.js} +1 -1
- package/dist/assets/{useBoolean-3u0CYJkt.js → useBoolean-BYlCXj8U.js} +1 -1
- package/dist/assets/{useCellActionButton-CL2hZzP1.js → useCellActionButton-D-FPtHfb.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-uG7OU1qF.js → useDependencyPanelTab-CYwz2tNi.js} +1 -1
- package/dist/assets/{useNotebookActions-DkhkeIz3.js → useNotebookActions-BeK3aC7s.js} +1 -1
- package/dist/index.html +14 -14
- package/package.json +1 -1
- package/src/components/data-table/__tests__/utils.test.ts +138 -1
- package/src/components/data-table/context-menu.tsx +9 -5
- package/src/components/data-table/data-table.tsx +3 -0
- package/src/components/data-table/range-focus/__tests__/atoms.test.ts +8 -2
- package/src/components/data-table/range-focus/__tests__/test-utils.ts +2 -0
- package/src/components/data-table/range-focus/__tests__/utils.test.ts +82 -8
- package/src/components/data-table/range-focus/atoms.ts +2 -2
- package/src/components/data-table/range-focus/utils.ts +50 -12
- package/src/components/data-table/types.ts +7 -0
- package/src/components/data-table/utils.ts +87 -0
- package/src/plugins/impl/DataTablePlugin.tsx +23 -2
- package/src/utils/copy.ts +18 -5
- package/dist/assets/JsonOutput-BRPU4yqw.js +0 -46
- package/dist/assets/copy-YwM0Pd7v.js +0 -1
- 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 < b & c > 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 {
|
|
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
|
-
):
|
|
15
|
-
const
|
|
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
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
67
|
+
return {
|
|
68
|
+
text: tabSeparatedText,
|
|
69
|
+
html: htmlTable,
|
|
70
|
+
};
|
|
36
71
|
}
|
|
37
72
|
|
|
38
|
-
|
|
39
|
-
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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
|
});
|