@marimo-team/frontend 0.24.1-dev2 → 0.24.1-dev21
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/{CellStatus-DqJypoFY.js → CellStatus-DsvXjUsv.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DLvmqIcV.js → ConnectedDataExplorerComponent-WU7zjB7Z.js} +1 -1
- package/dist/assets/{JsonOutput-DnrBQBRT.js → JsonOutput-D62UH4Ip.js} +7 -7
- package/dist/assets/{MarimoErrorOutput-BVdFtnJz.js → MarimoErrorOutput-B_IfCm1k.js} +7 -7
- package/dist/assets/{RenderHTML-X9kskQZ0.js → RenderHTML-DY5kbf6q.js} +1 -1
- package/dist/assets/{RunButton-D83R2wvd.js → RunButton-BfHw11b3.js} +1 -1
- package/dist/assets/{add-cell-with-ai-HD16K3PB.js → add-cell-with-ai-B25FLoLF.js} +12 -12
- package/dist/assets/{add-connection-dialog-V86wrXca.js → add-connection-dialog-B05rbnef.js} +6 -6
- package/dist/assets/{agent-panel-XdLSrKjl.js → agent-panel-CQobsqAS.js} +6 -6
- package/dist/assets/{ai-model-dropdown-BJp70TST.js → ai-model-dropdown-CmDWOIkZ.js} +4 -4
- package/dist/assets/app-config-button-BkwiW00-.js +1 -0
- package/dist/assets/{blob-BKW9TqBu.js → blob-BqDSSN5X.js} +1 -1
- package/dist/assets/{cell-editor-DQSrfeL3.js → cell-editor-CefMlZnX.js} +14 -14
- package/dist/assets/cell-link-CP4zF1rO.js +1 -0
- package/dist/assets/{cells-DTFqEQb0.js → cells-DD-zwhxi.js} +72 -72
- package/dist/assets/{chat-display-CclXFQQP.js → chat-display-59QZ49m7.js} +1 -1
- package/dist/assets/{chat-panel-ft0VIKS8.js → chat-panel-CDZbG1OG.js} +2 -2
- package/dist/assets/{chat-ui-DUYenr6D.js → chat-ui-DoBTbTFY.js} +1 -1
- package/dist/assets/{column-preview-BLap3eAk.js → column-preview-BNg0hwmL.js} +1 -1
- package/dist/assets/{command-palette-C7nz0zHt.js → command-palette-gEcd355-.js} +1 -1
- package/dist/assets/{common-DKQZgzC9.js → common-CcYiDLMe.js} +1 -1
- package/dist/assets/{components-Bf_hykBb.js → components-B4D3PZ3j.js} +1 -1
- package/dist/assets/config-DFK5eCC3.js +1 -0
- package/dist/assets/{datasource-DZgUyqAK.js → datasource-DAM_bXu_.js} +2 -2
- package/dist/assets/dependency-graph-panel-01bZMvwS.js +5 -0
- package/dist/assets/{documentation-panel-BQYA8gCW.js → documentation-panel-BhruLk_O.js} +1 -1
- package/dist/assets/{download-_oAlkvj6.js → download-DT1x-ePG.js} +1 -1
- package/dist/assets/{edit-page-DnXTBsfC.js → edit-page-C8uQVdbi.js} +6 -6
- package/dist/assets/{error-entries-uxjSsTal.js → error-entries-Dg9m5P8l.js} +3 -3
- package/dist/assets/{error-panel-Bq0TKquh.js → error-panel-By_xNe1V.js} +1 -1
- package/dist/assets/{file-explorer-panel-uhg7VNMd.js → file-explorer-panel-BzTVzCS7.js} +3 -3
- package/dist/assets/{file-icons-V3puz20c.js → file-icons-Dop5RazI.js} +1 -1
- package/dist/assets/{file-name-input-DWf6YZwM.js → file-name-input-C5_fsJ6t.js} +1 -1
- package/dist/assets/{floating-outline-Vqr5D17R.js → floating-outline-Bt2aLttK.js} +1 -1
- package/dist/assets/{focus-BTlyH5K_.js → focus-DCZbWsqm.js} +1 -1
- package/dist/assets/{form-Bb1JVuzT.js → form-Cj3PfSUI.js} +1 -1
- package/dist/assets/{form-CRSnWxLR.js → form-DHy5wB3F.js} +1 -1
- package/dist/assets/{glide-data-editor-BNZxtmKn.js → glide-data-editor-BPPCsS2s.js} +2 -2
- package/dist/assets/{home-page-RduyhdYP.js → home-page-EsENjhWM.js} +2 -2
- package/dist/assets/{html-to-image-BsXiWo0v.js → html-to-image-CajdmeRM.js} +1 -1
- package/dist/assets/index-BZ-HxYIU.js +38 -0
- package/dist/assets/index-CxZ6uZaz.css +2 -0
- package/dist/assets/{kiosk-mode-BH15-Gn-.js → kiosk-mode-B8UhlOLw.js} +1 -1
- package/dist/assets/{layout-1X5SpE18.js → layout-B73d8K81.js} +5 -5
- package/dist/assets/{logs-panel-CVxksxou.js → logs-panel-CYMHvjs-.js} +1 -1
- package/dist/assets/{markdown-renderer-BjuEVUOa.js → markdown-renderer-DM-Whfio.js} +1 -1
- package/dist/assets/{name-cell-input-CtYGGTR5.js → name-cell-input-DF8MnZDV.js} +1 -1
- package/dist/assets/{outline-panel-BhxoKAbS.js → outline-panel-Q7h5hFfo.js} +1 -1
- package/dist/assets/{packages-panel-BWP3YWNW.js → packages-panel-BW4xGU0r.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-BCMZbqSR.js → pair-with-agent-modal-DiDLAjxV.js} +1 -1
- package/dist/assets/panel-accordion-state-iff6EfKa.js +1 -0
- package/dist/assets/{panels-D40pa9F1.js → panels-BqKyRqOy.js} +2 -2
- package/dist/assets/{process-output-WaMx3G7b.js → process-output-B9grqmZC.js} +1 -1
- package/dist/assets/{radio-group-5KDs5aO1.js → radio-group-B8t5cIdU.js} +1 -1
- package/dist/assets/{readonly-code-display-DnHcjfFl.js → readonly-code-display-CY-jDwQL.js} +1 -1
- package/dist/assets/{readonly-python-code-BV8otm7e.js → readonly-python-code-DFjpK9Hc.js} +1 -1
- package/dist/assets/{reveal-component-yVci-fc9.js → reveal-component-DWjuDjk-.js} +2 -2
- package/dist/assets/{run-page-aQycuCjn.js → run-page-DNJz4YBq.js} +1 -1
- package/dist/assets/{scratchpad-panel-lsqN24Au.js → scratchpad-panel-BOQN7LjT.js} +1 -1
- package/dist/assets/{session-panel-C4JUkPt7.js → session-panel-C9zMMa1x.js} +1 -1
- package/dist/assets/{skeleton-DOZFvsSE.js → skeleton-mj_3hAv8.js} +1 -1
- package/dist/assets/{snippets-panel-QNY_Dl2J.js → snippets-panel-C-VlGXwd.js} +1 -1
- package/dist/assets/spec-C_psFYbu.js +1 -0
- package/dist/assets/state-BSVx1akM.js +3 -0
- package/dist/assets/{state-ByxLiTvP.js → state-C1e6in0R.js} +1 -1
- package/dist/assets/{switch-CDT2KH2g.js → switch-ulnbZFaI.js} +1 -1
- package/dist/assets/{terminal-UyHT9ie2.js → terminal-Bo6pnmSU.js} +1 -1
- package/dist/assets/{tracing-BiBPyXVg.js → tracing-D-L-9q2d.js} +1 -1
- package/dist/assets/{tracing-panel-D8LtcU_g.js → tracing-panel-DQzhu-hj.js} +2 -2
- package/dist/assets/{tree-actions-DUAdNn1i.js → tree-actions-BBmIijvt.js} +1 -1
- package/dist/assets/{useBoolean-CgN_4RAl.js → useBoolean-C_UV6tQ7.js} +1 -1
- package/dist/assets/{useCellActionButton-Bp2eEIzO.js → useCellActionButton-COoOWq__.js} +1 -1
- package/dist/assets/{useDeleteCell-DO-9USfB.js → useDeleteCell-C802AuEJ.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-Bi8pUBnn.js → useDependencyPanelTab-DR6NOy5O.js} +1 -1
- package/dist/assets/useNotebookActions-BXVkcfqk.js +12 -0
- package/dist/assets/{useRunCells-BpZ_0Xuz.js → useRunCells-DuxMdaXo.js} +1 -1
- package/dist/assets/{useSplitCell-C2fjcfID.js → useSplitCell-B1EAP5Nv.js} +1 -1
- package/dist/assets/{utils-Y4n-X10o.js → utils-Bp3MLwWq.js} +1 -1
- package/dist/assets/{vega-component-3G_aA1Wa.js → vega-component-dzk5T-cc.js} +1 -1
- package/dist/assets/{vega-embed-container-CH1lX0V6.js → vega-embed-container-D0V0wn9T.js} +1 -1
- package/dist/index.html +29 -29
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +4 -0
- package/src/components/app-config/optional-features.test.ts +78 -0
- package/src/components/app-config/optional-features.tsx +33 -6
- package/src/components/data-table/__tests__/column_formatting.test.ts +7 -0
- package/src/components/data-table/charts/__tests__/schemas.test.ts +17 -2
- package/src/components/data-table/charts/chart-spec/types.ts +2 -0
- package/src/components/data-table/column-formatting/feature.ts +1 -0
- package/src/components/data-table/column-formatting/types.ts +1 -0
- package/src/components/data-table/column-summary/chart-spec-model.tsx +1 -0
- package/src/components/data-table/column-summary/column-summary.tsx +1 -0
- package/src/components/data-table/columns.tsx +4 -3
- package/src/components/datasets/__tests__/icons.test.ts +10 -2
- package/src/components/datasets/icons.tsx +4 -0
- package/src/components/datasources/__tests__/components.test.tsx +14 -1
- package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +206 -34
- package/src/components/editor/actions/export-dialog/export-dialog.tsx +9 -3
- package/src/components/editor/actions/export-dialog/format-notice.tsx +69 -24
- package/src/components/editor/actions/export-dialog/use-export-dialog.ts +27 -2
- package/src/components/ui/toaster.tsx +14 -1
- package/src/core/codemirror/code-lens/__tests__/popover.test.ts +13 -0
- package/src/core/codemirror/code-lens/popover.tsx +11 -5
- package/src/core/codemirror/language/languages/sql/__tests__/renderers.test.tsx +15 -4
- package/src/core/codemirror/language/languages/sql/renderers.tsx +1 -0
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/kernel/messages.ts +1 -0
- package/src/core/mode.ts +6 -1
- package/src/core/network/__tests__/requests-lazy.test.ts +40 -0
- package/src/core/network/__tests__/requests-network.test.ts +13 -0
- package/src/core/network/requests-lazy.ts +11 -4
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +5 -0
- package/src/core/wasm/bridge.ts +1 -0
- package/src/plugins/impl/MatrixPlugin.tsx +256 -35
- package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +13 -1
- package/src/plugins/impl/__tests__/MatrixPlugin.test.tsx +540 -3
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +1 -0
- package/src/plugins/impl/data-editor/glide-utils.ts +1 -0
- package/src/plugins/impl/data-frames/__tests__/schema.test.ts +9 -1
- package/src/plugins/impl/data-frames/utils/__tests__/operators.test.ts +8 -0
- package/src/plugins/impl/data-frames/utils/operators.ts +1 -1
- package/src/plugins/impl/vega/__tests__/vega.test.ts +22 -4
- package/src/plugins/impl/vega/utils.ts +3 -0
- package/dist/assets/app-config-button-C2zJXSrK.js +0 -1
- package/dist/assets/cell-link-D26WjdN1.js +0 -1
- package/dist/assets/config-EoJoeZuK.js +0 -1
- package/dist/assets/dependency-graph-panel-CH6kIoat.js +0 -5
- package/dist/assets/index-C5WESBbZ.js +0 -38
- package/dist/assets/index-WoaGtlKd.css +0 -2
- package/dist/assets/panel-accordion-state-0CKLFeAf.js +0 -1
- package/dist/assets/spec-NeCHOv2Z.js +0 -1
- package/dist/assets/state-Ci7n55A9.js +0 -3
- package/dist/assets/useNotebookActions-Bm9VzJ_M.js +0 -12
|
@@ -9,13 +9,18 @@ import type {
|
|
|
9
9
|
} from "@/core/kernel/messages";
|
|
10
10
|
import { renderColumnInfo, renderTableInfo } from "../renderers";
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const bogusColumn = {
|
|
13
13
|
name: "geom",
|
|
14
|
-
type: "
|
|
14
|
+
type: "bogus_type" as DataType,
|
|
15
15
|
external_type: "geometry",
|
|
16
16
|
sample_values: [],
|
|
17
17
|
} as DataTableColumn;
|
|
18
18
|
|
|
19
|
+
const geometryColumn = {
|
|
20
|
+
...bogusColumn,
|
|
21
|
+
type: "geometry" as DataType,
|
|
22
|
+
} as DataTableColumn;
|
|
23
|
+
|
|
19
24
|
describe("SQL renderers", () => {
|
|
20
25
|
it("renders table information with an unrecognized column type", () => {
|
|
21
26
|
const table: DataTable = {
|
|
@@ -26,7 +31,7 @@ describe("SQL renderers", () => {
|
|
|
26
31
|
num_rows: null,
|
|
27
32
|
num_columns: 1,
|
|
28
33
|
variable_name: null,
|
|
29
|
-
columns: [
|
|
34
|
+
columns: [bogusColumn],
|
|
30
35
|
primary_keys: [],
|
|
31
36
|
indexes: [],
|
|
32
37
|
};
|
|
@@ -37,8 +42,14 @@ describe("SQL renderers", () => {
|
|
|
37
42
|
});
|
|
38
43
|
|
|
39
44
|
it("renders column information with an unrecognized column type", () => {
|
|
40
|
-
const result = render(renderColumnInfo(
|
|
45
|
+
const result = render(renderColumnInfo(bogusColumn));
|
|
41
46
|
|
|
42
47
|
expect(result.container.querySelector("svg")).not.toBeNull();
|
|
43
48
|
});
|
|
49
|
+
|
|
50
|
+
it("renders geometry column information with the map pin icon", () => {
|
|
51
|
+
const result = render(renderColumnInfo(geometryColumn));
|
|
52
|
+
|
|
53
|
+
expect(result.container.querySelector(".lucide-map-pin")).not.toBeNull();
|
|
54
|
+
});
|
|
44
55
|
});
|
|
@@ -41,6 +41,7 @@ const DATA_TYPE_COLORS: Record<DataType, string> = {
|
|
|
41
41
|
number: "bg-(--purple-4) text-(--purple-11)",
|
|
42
42
|
integer: "bg-(--purple-4) text-(--purple-11)",
|
|
43
43
|
string: "bg-(--blue-4) text-(--blue-11)",
|
|
44
|
+
geometry: "bg-(--cyan-4) text-(--cyan-11)",
|
|
44
45
|
unknown: "bg-(--slate-4) text-(--slate-11)",
|
|
45
46
|
};
|
|
46
47
|
|
|
@@ -356,6 +356,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
356
356
|
sendFileDetails = throwNotImplemented;
|
|
357
357
|
openTutorial = throwNotImplemented;
|
|
358
358
|
getExportAvailability = throwNotImplemented;
|
|
359
|
+
installExportRequirements = throwNotImplemented;
|
|
359
360
|
exportAsHTML = throwNotImplemented;
|
|
360
361
|
exportAsIPYNB = throwNotImplemented;
|
|
361
362
|
exportAsMarkdown = throwNotImplemented;
|
package/src/core/mode.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { atom, useAtomValue } from "jotai";
|
|
4
|
+
import { KnownQueryParams } from "@/core/constants";
|
|
4
5
|
import { isIslands } from "@/core/islands/utils";
|
|
5
6
|
import { assertExists } from "@/utils/assertExists";
|
|
6
7
|
import { invariant } from "@/utils/invariant";
|
|
@@ -98,5 +99,9 @@ export const kioskModeAtom = atom<boolean>(false);
|
|
|
98
99
|
*/
|
|
99
100
|
export function useInstallAllowed(): boolean {
|
|
100
101
|
const { mode } = useAtomValue(viewStateAtom);
|
|
101
|
-
|
|
102
|
+
const kioskMode = useAtomValue(kioskModeAtom);
|
|
103
|
+
const kioskRequested =
|
|
104
|
+
new URLSearchParams(window.location.search).get(KnownQueryParams.kiosk) ===
|
|
105
|
+
"true";
|
|
106
|
+
return mode !== "read" && !kioskMode && !kioskRequested;
|
|
102
107
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { cellId, requestId, uiElementId } from "@/__tests__/branded";
|
|
5
|
+
import { waitForKernelToBeInstantiated } from "../../kernel/state";
|
|
5
6
|
import type { RuntimeManager } from "../../runtime/runtime";
|
|
7
|
+
import { waitForConnectionOpen } from "../connection";
|
|
6
8
|
import { createLazyRequests } from "../requests-lazy";
|
|
7
9
|
import type {
|
|
8
10
|
EditRequests,
|
|
@@ -55,6 +57,10 @@ describe("createLazyRequests", () => {
|
|
|
55
57
|
sendInterrupt: vi.fn().mockResolvedValue({ interrupted: true }),
|
|
56
58
|
sendPdb: vi.fn().mockResolvedValue({ pdb: true }),
|
|
57
59
|
sendRunScratchpad: vi.fn().mockResolvedValue({ run: true }),
|
|
60
|
+
installExportRequirements: vi.fn().mockResolvedValue({
|
|
61
|
+
source: "server",
|
|
62
|
+
formats: [],
|
|
63
|
+
}),
|
|
58
64
|
} as unknown as EditRequests & RunRequests;
|
|
59
65
|
});
|
|
60
66
|
|
|
@@ -104,6 +110,40 @@ describe("createLazyRequests", () => {
|
|
|
104
110
|
expect(mockDelegate.getExportAvailability).toHaveBeenCalledOnce();
|
|
105
111
|
});
|
|
106
112
|
|
|
113
|
+
it("installs export requirements without waiting for the kernel", async () => {
|
|
114
|
+
const lazyRequests = createLazyRequests(
|
|
115
|
+
mockDelegate,
|
|
116
|
+
mockGetRuntimeManager,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
await lazyRequests.installExportRequirements({ format: "pdf" });
|
|
120
|
+
|
|
121
|
+
expect(mockInit).toHaveBeenCalledOnce();
|
|
122
|
+
expect(waitForConnectionOpen).toHaveBeenCalledOnce();
|
|
123
|
+
expect(waitForKernelToBeInstantiated).not.toHaveBeenCalled();
|
|
124
|
+
expect(mockDelegate.installExportRequirements).toHaveBeenCalledWith({
|
|
125
|
+
format: "pdf",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("sends instantiate without waiting for the kernel", async () => {
|
|
130
|
+
const lazyRequests = createLazyRequests(
|
|
131
|
+
mockDelegate,
|
|
132
|
+
mockGetRuntimeManager,
|
|
133
|
+
);
|
|
134
|
+
const request = {
|
|
135
|
+
objectIds: [uiElementId("obj1")],
|
|
136
|
+
values: [],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await lazyRequests.sendInstantiate(request);
|
|
140
|
+
|
|
141
|
+
expect(mockInit).toHaveBeenCalledOnce();
|
|
142
|
+
expect(waitForConnectionOpen).toHaveBeenCalledOnce();
|
|
143
|
+
expect(waitForKernelToBeInstantiated).not.toHaveBeenCalled();
|
|
144
|
+
expect(mockDelegate.sendInstantiate).toHaveBeenCalledWith(request);
|
|
145
|
+
});
|
|
146
|
+
|
|
107
147
|
it("should call init once before first request", async () => {
|
|
108
148
|
const lazyRequests = createLazyRequests(
|
|
109
149
|
mockDelegate,
|
|
@@ -143,6 +143,19 @@ describe("createNetworkRequests", () => {
|
|
|
143
143
|
expect(mockClient.GET).toHaveBeenCalledWith("/api/export/availability");
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
it("installExportRequirements should POST the export format", async () => {
|
|
147
|
+
const requests = createNetworkRequests();
|
|
148
|
+
await requests.installExportRequirements({ format: "pdf" });
|
|
149
|
+
|
|
150
|
+
expect(mockClient.POST).toHaveBeenCalledWith(
|
|
151
|
+
"/api/export/requirements/install",
|
|
152
|
+
expect.objectContaining({
|
|
153
|
+
body: { format: "pdf" },
|
|
154
|
+
params: expect.anything(),
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
146
159
|
it("discoverDataSources should POST to the discovery endpoint", async () => {
|
|
147
160
|
const requests = createNetworkRequests();
|
|
148
161
|
const request = { requestId: "discovery-request" } as any;
|
|
@@ -26,6 +26,10 @@ type AllRequests = EditRequests & RunRequests;
|
|
|
26
26
|
// executing. Use for user-initiated actions that should "just work" and
|
|
27
27
|
// kick off the kernel if needed (e.g., clicking Run).
|
|
28
28
|
//
|
|
29
|
+
// - startConnectionWithoutKernel: Initializes the runtime and waits for an open
|
|
30
|
+
// connection. Use for session-scoped server requests that do not need an
|
|
31
|
+
// instantiated kernel.
|
|
32
|
+
//
|
|
29
33
|
// - waitForConnectionOpen: Waits for an existing connection but won't start one.
|
|
30
34
|
// Use for operations that depend on a running kernel but shouldn't be the
|
|
31
35
|
// trigger to start it (e.g., saving, interrupting).
|
|
@@ -38,6 +42,7 @@ type Action =
|
|
|
38
42
|
| "throwError"
|
|
39
43
|
| "dropRequest"
|
|
40
44
|
| "startConnection"
|
|
45
|
+
| "startConnectionWithoutKernel"
|
|
41
46
|
| "waitForConnectionOpen"
|
|
42
47
|
| "serverOnly";
|
|
43
48
|
|
|
@@ -45,7 +50,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
45
50
|
// These will start a connection if not already connected and then wait until the connection is open
|
|
46
51
|
sendComponentValues: "startConnection",
|
|
47
52
|
sendModelValue: "startConnection",
|
|
48
|
-
sendInstantiate: "
|
|
53
|
+
sendInstantiate: "startConnectionWithoutKernel",
|
|
49
54
|
sendRun: "startConnection",
|
|
50
55
|
sendDeleteCell: "startConnection",
|
|
51
56
|
sendRunScratchpad: "startConnection",
|
|
@@ -107,6 +112,9 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
107
112
|
getEnvironmentInfo: "serverOnly",
|
|
108
113
|
getExportAvailability: "serverOnly",
|
|
109
114
|
|
|
115
|
+
// Session-scoped server operations
|
|
116
|
+
installExportRequirements: "startConnectionWithoutKernel",
|
|
117
|
+
|
|
110
118
|
// Home operations throw errors
|
|
111
119
|
getRecentFiles: "startConnection",
|
|
112
120
|
getWorkspaceFiles: "startConnection",
|
|
@@ -186,13 +194,12 @@ export function createLazyRequests(
|
|
|
186
194
|
await waitForKernelToBeInstantiated();
|
|
187
195
|
return request(...args);
|
|
188
196
|
|
|
197
|
+
case "startConnectionWithoutKernel":
|
|
189
198
|
case "startConnection":
|
|
190
199
|
// Start connection and wait for it to be open
|
|
191
200
|
await initOnce(runtimeManager);
|
|
192
201
|
await waitForConnectionOpen();
|
|
193
|
-
if (
|
|
194
|
-
// We don't need to wait for kernel to be instantiated if we are sending an instantiate request
|
|
195
|
-
// otherwise we will wait forever
|
|
202
|
+
if (action === "startConnection") {
|
|
196
203
|
await waitForKernelToBeInstantiated();
|
|
197
204
|
}
|
|
198
205
|
return request(...args);
|
|
@@ -417,6 +417,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
417
417
|
getExportAvailability: () => {
|
|
418
418
|
return getClient().GET("/api/export/availability").then(handleResponse);
|
|
419
419
|
},
|
|
420
|
+
installExportRequirements: (request) => {
|
|
421
|
+
return getClient()
|
|
422
|
+
.POST("/api/export/requirements/install", {
|
|
423
|
+
body: request,
|
|
424
|
+
params: getParams(),
|
|
425
|
+
})
|
|
426
|
+
.then(handleResponse);
|
|
427
|
+
},
|
|
420
428
|
exportAsHTML: async (request) => {
|
|
421
429
|
if (
|
|
422
430
|
process.env.NODE_ENV === "development" ||
|
|
@@ -84,6 +84,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
84
84
|
getRunningNotebooks: throwNotInEditMode,
|
|
85
85
|
shutdownSession: throwNotInEditMode,
|
|
86
86
|
getExportAvailability: throwNotInEditMode,
|
|
87
|
+
installExportRequirements: throwNotInEditMode,
|
|
87
88
|
exportAsHTML: throwNotInEditMode,
|
|
88
89
|
exportAsIPYNB: throwNotInEditMode,
|
|
89
90
|
exportAsMarkdown: throwNotInEditMode,
|
|
@@ -65,6 +65,7 @@ export function createErrorToastingRequests(
|
|
|
65
65
|
getRunningNotebooks: "Failed to get running notebooks",
|
|
66
66
|
shutdownSession: "Failed to shutdown session",
|
|
67
67
|
getExportAvailability: "", // No toast
|
|
68
|
+
installExportRequirements: "Failed to install export requirements",
|
|
68
69
|
exportAsHTML: "Failed to export HTML",
|
|
69
70
|
exportAsIPYNB: "Failed to export ipynb",
|
|
70
71
|
exportAsMarkdown: "Failed to export Markdown",
|
|
@@ -29,6 +29,8 @@ export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
|
|
|
29
29
|
export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
|
|
30
30
|
export type ExportAsPDFRequest = schemas["ExportAsPDFRequest"];
|
|
31
31
|
export type ExportAvailabilityResponse = schemas["ExportAvailabilityResponse"];
|
|
32
|
+
export type InstallExportRequirementsRequest =
|
|
33
|
+
schemas["InstallExportRequirementsRequest"];
|
|
32
34
|
export type UpdateCellOutputsRequest = schemas["UpdateCellOutputsRequest"];
|
|
33
35
|
|
|
34
36
|
export interface ExportedFile<T extends BlobPart = BlobPart> {
|
|
@@ -223,6 +225,9 @@ export interface EditRequests {
|
|
|
223
225
|
) => Promise<RunningNotebooksResponse>;
|
|
224
226
|
// Export requests
|
|
225
227
|
getExportAvailability: () => Promise<ExportAvailabilityResponse>;
|
|
228
|
+
installExportRequirements: (
|
|
229
|
+
request: InstallExportRequirementsRequest,
|
|
230
|
+
) => Promise<ExportAvailabilityResponse>;
|
|
226
231
|
exportAsHTML: (request: ExportAsHTMLRequest) => Promise<ExportedFile<string>>;
|
|
227
232
|
exportAsIPYNB: (
|
|
228
233
|
request: ExportAsIPYNBRequest,
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -675,6 +675,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
675
675
|
getRunningNotebooks = throwNotImplemented;
|
|
676
676
|
shutdownSession = throwNotImplemented;
|
|
677
677
|
getExportAvailability = throwNotImplemented;
|
|
678
|
+
installExportRequirements = throwNotImplemented;
|
|
678
679
|
exportAsIPYNB = throwNotImplemented;
|
|
679
680
|
exportAsPDF = throwNotImplemented;
|
|
680
681
|
autoExportAsHTML = throwNotImplemented;
|
|
@@ -54,6 +54,40 @@ export class MatrixPlugin implements IPlugin<T, Data> {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const PIXELS_PER_STEP = 10;
|
|
57
|
+
const COARSE_MULTIPLIER = 10;
|
|
58
|
+
const FINE_MULTIPLIER = 0.1;
|
|
59
|
+
|
|
60
|
+
/** Step multiplier from modifier keys: shift = coarse, alt = fine. */
|
|
61
|
+
const stepMultiplier = (e: { shiftKey: boolean; altKey: boolean }): number => {
|
|
62
|
+
if (e.shiftKey) {
|
|
63
|
+
return COARSE_MULTIPLIER;
|
|
64
|
+
}
|
|
65
|
+
if (e.altKey) {
|
|
66
|
+
return FINE_MULTIPLIER;
|
|
67
|
+
}
|
|
68
|
+
return 1;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Strip floating-point noise from step arithmetic (e.g. 3 * 0.1).
|
|
73
|
+
* The noise tolerance scales with the step so that legitimately tiny
|
|
74
|
+
* results (e.g. from a 2e-14 step) are preserved, while noise left over
|
|
75
|
+
* from arithmetic on ordinary magnitudes (e.g. the 5.55e-17 residue of
|
|
76
|
+
* 0.3 - 3 * 0.1) still snaps away.
|
|
77
|
+
*/
|
|
78
|
+
const cleanFloat = (x: number, step: number): number => {
|
|
79
|
+
const rounded = Number(x.toFixed(12));
|
|
80
|
+
const tolerance =
|
|
81
|
+
Number.EPSILON * Math.max(Math.abs(x), Math.abs(step)) * 100;
|
|
82
|
+
return Math.abs(x - rounded) <= tolerance ? rounded : x;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
interface EditState {
|
|
86
|
+
row: number;
|
|
87
|
+
col: number;
|
|
88
|
+
text: string;
|
|
89
|
+
selectAll: boolean;
|
|
90
|
+
}
|
|
57
91
|
|
|
58
92
|
interface MatrixComponentProps extends Data {
|
|
59
93
|
value: T;
|
|
@@ -80,6 +114,7 @@ const MatrixComponent = ({
|
|
|
80
114
|
col: number;
|
|
81
115
|
startX: number;
|
|
82
116
|
startValue: number;
|
|
117
|
+
multiplier: number;
|
|
83
118
|
} | null>(null);
|
|
84
119
|
const [activeCell, setActiveCell] = useState<{
|
|
85
120
|
row: number;
|
|
@@ -95,6 +130,16 @@ const MatrixComponent = ({
|
|
|
95
130
|
}, [value]);
|
|
96
131
|
const displayValue = activeCell == null ? value : draft;
|
|
97
132
|
|
|
133
|
+
// Editing state is mirrored in a ref so that the blur fired while
|
|
134
|
+
// committing (refocusing the cell unmounts the input) can't commit twice.
|
|
135
|
+
const [editing, setEditingState] = useState<EditState | null>(null);
|
|
136
|
+
const editingRef = useRef<EditState | null>(null);
|
|
137
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
138
|
+
const setEditing = useCallback((edit: EditState | null) => {
|
|
139
|
+
editingRef.current = edit;
|
|
140
|
+
setEditingState(edit);
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
98
143
|
const formatValue = (val: number) =>
|
|
99
144
|
scientific ? val.toExponential(precision) : val.toFixed(precision);
|
|
100
145
|
|
|
@@ -112,22 +157,131 @@ const MatrixComponent = ({
|
|
|
112
157
|
[minValue, maxValue],
|
|
113
158
|
);
|
|
114
159
|
|
|
160
|
+
// The single mutation chokepoint for every edit path (drag, arrow, type).
|
|
161
|
+
// Bounds are enforced here so no path can escape them. In symmetric mode
|
|
162
|
+
// the pair must stay equal *and* respect both cells' bounds, so the shared
|
|
163
|
+
// value is clamped to the intersection of the two ranges. Returns null
|
|
164
|
+
// when the clamped value is already in place, so callers can skip
|
|
165
|
+
// redundant updates.
|
|
166
|
+
const withCellValue = useCallback(
|
|
167
|
+
(base: T, row: number, col: number, newValue: number): T | null => {
|
|
168
|
+
const mirrored = symmetric && row !== col;
|
|
169
|
+
let clamped = clampValue(newValue, row, col);
|
|
170
|
+
if (mirrored) {
|
|
171
|
+
clamped = clampValue(clamped, col, row);
|
|
172
|
+
}
|
|
173
|
+
if (clamped === base[row][col]) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const copy = base.map((r) => [...r]);
|
|
177
|
+
copy[row][col] = clamped;
|
|
178
|
+
if (mirrored) {
|
|
179
|
+
copy[col][row] = clamped;
|
|
180
|
+
}
|
|
181
|
+
return copy;
|
|
182
|
+
},
|
|
183
|
+
[symmetric, clampValue],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const startEditing = useCallback(
|
|
187
|
+
(row: number, col: number, seed?: string) => {
|
|
188
|
+
if (disabled[row][col]) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
dragState.current = null;
|
|
192
|
+
setActiveCell(null);
|
|
193
|
+
setEditing({
|
|
194
|
+
row,
|
|
195
|
+
col,
|
|
196
|
+
text: seed ?? String(value[row][col]),
|
|
197
|
+
selectAll: seed == null,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
[disabled, value, setEditing],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Callback ref: focus the input when it mounts. When editing starts from
|
|
204
|
+
// a typed character, place the caret after the seed instead of selecting.
|
|
205
|
+
const focusInput = useCallback((el: HTMLInputElement | null) => {
|
|
206
|
+
inputRef.current = el;
|
|
207
|
+
if (el) {
|
|
208
|
+
el.focus();
|
|
209
|
+
if (editingRef.current?.selectAll) {
|
|
210
|
+
el.select();
|
|
211
|
+
} else {
|
|
212
|
+
el.setSelectionRange(el.value.length, el.value.length);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
const commitEdit = useCallback(
|
|
218
|
+
(refocusCell: boolean) => {
|
|
219
|
+
const edit = editingRef.current;
|
|
220
|
+
if (!edit) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const cell = refocusCell ? inputRef.current?.closest("td") : null;
|
|
224
|
+
setEditing(null);
|
|
225
|
+
const text = edit.text.trim();
|
|
226
|
+
const parsed = Number(text);
|
|
227
|
+
// Typed values are clamped to bounds but not snapped to `step`,
|
|
228
|
+
// so exact values like 2.32e7 survive.
|
|
229
|
+
if (text !== "" && Number.isFinite(parsed)) {
|
|
230
|
+
const copy = withCellValue(value, edit.row, edit.col, parsed);
|
|
231
|
+
if (copy) {
|
|
232
|
+
setDraft(copy);
|
|
233
|
+
setValue(copy);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
cell?.focus();
|
|
237
|
+
},
|
|
238
|
+
[value, withCellValue, setValue, setEditing],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const cancelEdit = useCallback(() => {
|
|
242
|
+
const cell = inputRef.current?.closest("td");
|
|
243
|
+
setEditing(null);
|
|
244
|
+
cell?.focus();
|
|
245
|
+
}, [setEditing]);
|
|
246
|
+
|
|
247
|
+
const handleInputKeyDown = useCallback(
|
|
248
|
+
(e: React.KeyboardEvent) => {
|
|
249
|
+
e.stopPropagation();
|
|
250
|
+
if (e.key === "Enter") {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
commitEdit(true);
|
|
253
|
+
} else if (e.key === "Escape") {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
cancelEdit();
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
[commitEdit, cancelEdit],
|
|
259
|
+
);
|
|
260
|
+
|
|
115
261
|
const handlePointerDown = useCallback(
|
|
116
|
-
(e: React.PointerEvent
|
|
117
|
-
if (
|
|
262
|
+
(e: React.PointerEvent<HTMLTableCellElement>, row: number, col: number) => {
|
|
263
|
+
if (
|
|
264
|
+
disabled[row][col] ||
|
|
265
|
+
editing != null ||
|
|
266
|
+
!(e.target instanceof Element)
|
|
267
|
+
) {
|
|
118
268
|
return;
|
|
119
269
|
}
|
|
120
270
|
e.preventDefault();
|
|
271
|
+
// preventDefault also suppresses the browser's click-to-focus, so
|
|
272
|
+
// focus the cell explicitly to keep keyboard stepping reachable.
|
|
273
|
+
e.currentTarget.focus();
|
|
121
274
|
e.target.setPointerCapture(e.pointerId);
|
|
122
275
|
dragState.current = {
|
|
123
276
|
row,
|
|
124
277
|
col,
|
|
125
278
|
startX: e.clientX,
|
|
126
279
|
startValue: displayValue[row][col],
|
|
280
|
+
multiplier: stepMultiplier(e),
|
|
127
281
|
};
|
|
128
282
|
setActiveCell({ row, col });
|
|
129
283
|
},
|
|
130
|
-
[disabled, displayValue],
|
|
284
|
+
[disabled, editing, displayValue],
|
|
131
285
|
);
|
|
132
286
|
|
|
133
287
|
const handlePointerMove = useCallback(
|
|
@@ -136,26 +290,32 @@ const MatrixComponent = ({
|
|
|
136
290
|
if (!state) {
|
|
137
291
|
return;
|
|
138
292
|
}
|
|
293
|
+
const multiplier = stepMultiplier(e);
|
|
294
|
+
if (multiplier !== state.multiplier) {
|
|
295
|
+
// Rebase so toggling a modifier mid-drag rescales future movement
|
|
296
|
+
// instead of jumping the value.
|
|
297
|
+
state.startX = e.clientX;
|
|
298
|
+
state.startValue = displayValue[state.row][state.col];
|
|
299
|
+
state.multiplier = multiplier;
|
|
300
|
+
}
|
|
139
301
|
const { row, col, startX, startValue } = state;
|
|
140
302
|
const dx = e.clientX - startX;
|
|
141
|
-
const cellStep = step[row][col];
|
|
303
|
+
const cellStep = step[row][col] * multiplier;
|
|
142
304
|
const steps = Math.round(dx / PIXELS_PER_STEP);
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
copy[col][row] = newValue;
|
|
151
|
-
}
|
|
305
|
+
const copy = withCellValue(
|
|
306
|
+
displayValue,
|
|
307
|
+
row,
|
|
308
|
+
col,
|
|
309
|
+
cleanFloat(startValue + steps * cellStep, cellStep),
|
|
310
|
+
);
|
|
311
|
+
if (copy) {
|
|
152
312
|
setDraft(copy);
|
|
153
313
|
if (!debounce) {
|
|
154
314
|
setValue(copy);
|
|
155
315
|
}
|
|
156
316
|
}
|
|
157
317
|
},
|
|
158
|
-
[step,
|
|
318
|
+
[step, displayValue, withCellValue, debounce, setValue],
|
|
159
319
|
);
|
|
160
320
|
|
|
161
321
|
const handlePointerUp = useCallback(() => {
|
|
@@ -168,27 +328,61 @@ const MatrixComponent = ({
|
|
|
168
328
|
|
|
169
329
|
const handleKeyDown = useCallback(
|
|
170
330
|
(e: React.KeyboardEvent, row: number, col: number) => {
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
331
|
+
if (disabled[row][col]) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (e.key === "Enter" || e.key === "F2") {
|
|
175
335
|
e.preventDefault();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
336
|
+
startEditing(row, col);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Typing a number starts editing, seeded with the typed character.
|
|
340
|
+
if (
|
|
341
|
+
e.key.length === 1 &&
|
|
342
|
+
/[\d.+-]/.test(e.key) &&
|
|
343
|
+
!e.ctrlKey &&
|
|
344
|
+
!e.metaKey &&
|
|
345
|
+
!e.altKey
|
|
346
|
+
) {
|
|
347
|
+
e.preventDefault();
|
|
348
|
+
startEditing(row, col, e.key);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const cellStep = step[row][col];
|
|
352
|
+
let delta: number;
|
|
353
|
+
switch (e.key) {
|
|
354
|
+
// Right/left mirror up/down, matching the horizontal drag direction
|
|
355
|
+
// (and the native slider convention).
|
|
356
|
+
case "ArrowUp":
|
|
357
|
+
case "ArrowRight":
|
|
358
|
+
delta = cellStep * stepMultiplier(e);
|
|
359
|
+
break;
|
|
360
|
+
case "ArrowDown":
|
|
361
|
+
case "ArrowLeft":
|
|
362
|
+
delta = -cellStep * stepMultiplier(e);
|
|
363
|
+
break;
|
|
364
|
+
case "PageUp":
|
|
365
|
+
delta = cellStep * COARSE_MULTIPLIER;
|
|
366
|
+
break;
|
|
367
|
+
case "PageDown":
|
|
368
|
+
delta = -cellStep * COARSE_MULTIPLIER;
|
|
369
|
+
break;
|
|
370
|
+
default:
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
e.preventDefault();
|
|
374
|
+
const copy = withCellValue(
|
|
375
|
+
displayValue,
|
|
376
|
+
row,
|
|
377
|
+
col,
|
|
378
|
+
cleanFloat(displayValue[row][col] + delta, delta),
|
|
379
|
+
);
|
|
380
|
+
if (copy) {
|
|
381
|
+
setDraft(copy);
|
|
382
|
+
setValue(copy);
|
|
189
383
|
}
|
|
190
384
|
},
|
|
191
|
-
[disabled, step, displayValue,
|
|
385
|
+
[disabled, startEditing, step, displayValue, withCellValue, setValue],
|
|
192
386
|
);
|
|
193
387
|
|
|
194
388
|
const hasRowLabels = rowLabels != null && rowLabels.length > 0;
|
|
@@ -238,6 +432,10 @@ const MatrixComponent = ({
|
|
|
238
432
|
const isDisabled = disabled[i][j];
|
|
239
433
|
const isActive =
|
|
240
434
|
activeCell?.row === i && activeCell?.col === j;
|
|
435
|
+
const cellEdit =
|
|
436
|
+
editing != null && editing.row === i && editing.col === j
|
|
437
|
+
? editing
|
|
438
|
+
: null;
|
|
241
439
|
const rowLabel = rowLabels?.[i] ?? `Row ${i + 1}`;
|
|
242
440
|
const colLabel = columnLabels?.[j] ?? `Column ${j + 1}`;
|
|
243
441
|
return (
|
|
@@ -247,24 +445,47 @@ const MatrixComponent = ({
|
|
|
247
445
|
"relative text-center min-w-14 h-8 px-2 transition-colors touch-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-hidden",
|
|
248
446
|
isDisabled
|
|
249
447
|
? "cursor-default text-muted-foreground"
|
|
250
|
-
:
|
|
251
|
-
|
|
448
|
+
: cellEdit
|
|
449
|
+
? "cursor-text"
|
|
450
|
+
: "cursor-ew-resize text-link hover:bg-accent",
|
|
451
|
+
(isActive || cellEdit != null) && "bg-accent",
|
|
252
452
|
j === 0 && "bracket-l",
|
|
253
453
|
j === numCols - 1 && "bracket-r",
|
|
254
454
|
i === 0 && "bracket-t",
|
|
255
455
|
i === numRows - 1 && "bracket-b",
|
|
256
456
|
)}
|
|
257
457
|
tabIndex={isDisabled ? -1 : 0}
|
|
458
|
+
title={String(cellValue)}
|
|
258
459
|
aria-label={`${rowLabel}, ${colLabel}`}
|
|
259
460
|
aria-valuenow={cellValue}
|
|
260
461
|
aria-valuemin={minValue?.[i]?.[j]}
|
|
261
462
|
aria-valuemax={maxValue?.[i]?.[j]}
|
|
262
463
|
aria-disabled={isDisabled || undefined}
|
|
263
464
|
onPointerDown={(e) => handlePointerDown(e, i, j)}
|
|
465
|
+
onDoubleClick={() => startEditing(i, j)}
|
|
264
466
|
onKeyDown={(e) => handleKeyDown(e, i, j)}
|
|
265
467
|
data-testid={`matrix-cell-${i}-${j}`}
|
|
266
468
|
>
|
|
267
|
-
{
|
|
469
|
+
{cellEdit ? (
|
|
470
|
+
<input
|
|
471
|
+
ref={focusInput}
|
|
472
|
+
className="bg-transparent text-center font-mono text-sm text-foreground outline-none select-text"
|
|
473
|
+
style={{
|
|
474
|
+
width: `${Math.max(cellEdit.text.length + 2, 6)}ch`,
|
|
475
|
+
}}
|
|
476
|
+
value={cellEdit.text}
|
|
477
|
+
onChange={(e) =>
|
|
478
|
+
setEditing({ ...cellEdit, text: e.target.value })
|
|
479
|
+
}
|
|
480
|
+
onKeyDown={handleInputKeyDown}
|
|
481
|
+
onBlur={() => commitEdit(false)}
|
|
482
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
483
|
+
aria-label={`Edit ${rowLabel}, ${colLabel}`}
|
|
484
|
+
data-testid={`matrix-input-${i}-${j}`}
|
|
485
|
+
/>
|
|
486
|
+
) : (
|
|
487
|
+
formatValue(cellValue)
|
|
488
|
+
)}
|
|
268
489
|
</td>
|
|
269
490
|
);
|
|
270
491
|
})}
|