@marimo-team/islands 0.25.3-dev322 → 0.25.3-dev348
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/{ConnectedDataExplorerComponent-DkXBUBf3.js → ConnectedDataExplorerComponent-xliyyXNO.js} +16 -16
- package/dist/{ErrorBoundary-CYa6lyKz.js → ErrorBoundary-CIimOpqs.js} +7 -7
- package/dist/{abnfDiagram-VCTEODGH-BPS-pTU2.js → abnfDiagram-VCTEODGH-Dl9Nrtbf.js} +2 -2
- package/dist/{any-language-editor-DIS7IdV1.js → any-language-editor-CBHhdCC0.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-DB2MHOb7.js → architectureDiagram-5GKGNRK7-DTg032fQ.js} +1 -1
- package/dist/{assertNever-HbUt8DyH.js → assertNever-ie77PjMn.js} +13 -13
- package/dist/{chat-ui-85k50jaj.js → chat-ui-BV0qyfru.js} +51 -51
- package/dist/{check-CApDh1-n.js → check-DTIv5ZZ3.js} +1 -1
- package/dist/{chunk-IMKFNOWR-CjFCedjx.js → chunk-IMKFNOWR-CsFB-2LW.js} +1 -1
- package/dist/{chunk-SHT3W25Y-Dptltg0_.js → chunk-SHT3W25Y-ByuJHeBL.js} +1 -1
- package/dist/{chunk-SVP7TREG-2jBN760m.js → chunk-SVP7TREG-9oBZhbrr.js} +1 -1
- package/dist/{chunk-TICWLB2K-CS8BE-b9.js → chunk-TICWLB2K-Bx4m0cJ1.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-HtQWp-Cm.js → classDiagram-ZZMXUADV-Cms37-C9.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-DA9LETN-.js → classDiagram-v2-VYDZK3BY-qAom4Bi7.js} +2 -2
- package/dist/{code-visibility-CGcIJMGj.js → code-visibility-_-8-dvb4.js} +20 -20
- package/dist/{common-B-Ng3J3K.js → common-CNXuhphD.js} +620 -620
- package/dist/{config-B171m9zn.js → config-CvBnR945.js} +3 -3
- package/dist/{copy-icon-k2_Fx5Qv.js → copy-icon-DmtNidSa.js} +2 -2
- package/dist/{createReducer-BPPI11pP.js → createReducer-BxBCTvKn.js} +5 -5
- package/dist/{cynefinDiagram-5FMLGOSQ-eyocrg_E.js → cynefinDiagram-5FMLGOSQ-BjJZwMEz.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-Dtm96Nvx.js → dagre-GXQ25YYZ-Bohu7_09.js} +1 -1
- package/dist/{data-grid-overlay-editor-CUD6F0Vj.js → data-grid-overlay-editor--4Gk_rwA.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-C09TrdNO.js → diagram-S7CK7UJ4-Chl0n835.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-vHUsSML6.js → diagram-UQ7AKVKN-DHx8nzy7.js} +1 -1
- package/dist/{diagram-VX7I27RA-Ch4mRgz8.js → diagram-VX7I27RA-CKdpv1U4.js} +1 -1
- package/dist/{diagram-Z3DM3KII-DyFQNdAL.js → diagram-Z3DM3KII-sY-sI--2.js} +1 -1
- package/dist/{dialog-C2Muh059.js → dialog-Cqu5Hp5U.js} +21 -21
- package/dist/{dropdown-menu-BGbNnuFq.js → dropdown-menu-DXEYk99S.js} +707 -707
- package/dist/{ebnfDiagram-PWID7BFC-N3SvlrCs.js → ebnfDiagram-PWID7BFC-CD0P2xeH.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-RR2q2xtd.js → erDiagram-RLTQ6QDP-BaKQuYxC.js} +1 -1
- package/dist/{error-banner-3_4Lc93Y.js → error-banner-DUB_qUeL.js} +58 -58
- package/dist/{error-entries-BhNAZWU1.js → error-entries-CTb_cxXY.js} +310 -310
- package/dist/{feedback-button-DK6zUzAk.js → feedback-button-BaA4r2dl.js} +7 -7
- package/dist/{flowDiagram-HODETNUW-nxd7WhlW.js → flowDiagram-HODETNUW-BcCpHh7-.js} +2 -2
- package/dist/{formats-aUcw6MrS.js → formats-hF6fETr6.js} +1 -1
- package/dist/{glide-data-editor-O-94UN5R.js → glide-data-editor-CRn3KXyR.js} +3129 -3119
- package/dist/{glide-utils-D4UP8TGv.js → glide-utils-Bnd_k48H.js} +2 -2
- package/dist/{globals-BVjYePCn.js → globals-_FmD97Ui.js} +3 -3
- package/dist/{html-to-image-DOOtP-4-.js → html-to-image-Bbl0PeC7.js} +478 -478
- package/dist/{infoDiagram-27XIBGKW-CUYXQKGf.js → infoDiagram-27XIBGKW-DWltseIa.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-v6gsCrlK.js → ishikawaDiagram-5VMMS53U-DHgepBPo.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-z236-0Ok.js → kanban-definition-UXKFOSKX-B2_BIjBq.js} +1 -1
- package/dist/main.js +6712 -6590
- package/dist/{mermaid-C7lQ_2LJ.js → mermaid--2K0cmt9.js} +34 -34
- package/dist/{mindmap-definition-YA3MSWOX--pZF7OCY.js → mindmap-definition-YA3MSWOX-DNebycIP.js} +1 -1
- package/dist/{number-overlay-editor-jkzzCN4e.js → number-overlay-editor-Bcg25qrp.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-Bgw0Zz_6.js → pegDiagram-XKGWAZYB-CH4U3II-.js} +2 -2
- package/dist/{pencil-RXFY-Ek8.js → pencil-MMnw1YQh.js} +1 -1
- package/dist/{pieDiagram-E7YTZNPT-B27tDYvK.js → pieDiagram-E7YTZNPT-pfcrdUCa.js} +1 -1
- package/dist/{process-output-CiX_Dl4L.js → process-output-Dw_sI4Xp.js} +4 -4
- package/dist/{railroadDiagram-O6MQD6OU-CBjPhGpj.js → railroadDiagram-O6MQD6OU-BZKpAbNG.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-DDDrLEw9.js → requirementDiagram-BXWQKSXE-DnyfIHe3.js} +1 -1
- package/dist/{reveal-component-CvPmNI5C.js → reveal-component-BVC491am.js} +18 -18
- package/dist/{select-Cv6lR39D.js → select-suaAxFuo.js} +286 -286
- package/dist/{sequenceDiagram-WJ2MYXX4-DQPaMygQ.js → sequenceDiagram-WJ2MYXX4-BeYuWX7N.js} +1 -1
- package/dist/{spec-BoCLWa-M.js → spec-BQNx2-D7.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-BDoDVVnq.js → stateDiagram-D77RDMKH-B8FxwAM2.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-KNko46uJ.js → stateDiagram-v2-MP3YSRHH-CzWLBqm1.js} +2 -2
- package/dist/{static-state-DgdOadIH.js → static-state-Al_hmf1n.js} +6 -6
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-boFjOuiI.js → swimlanes-42K2YHIH-CzKmGMts.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-BC4FLNwY.js → swimlanesDiagram-VR7AAH4N-DnWIUN_9.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-CLVPYA-v.js → timeline-definition-24CTP7MA-Czf2QN2R.js} +1 -1
- package/dist/{toast-CUuX7uXg.js → toast-CsJa1nGj.js} +66 -66
- package/dist/{tooltip-DBv8QUvS.js → tooltip-BVbJPkcl.js} +49 -49
- package/dist/{use-restore-focus-BxJh5Zfa.js → use-restore-focus-tPmARo1Q.js} +1035 -1022
- package/dist/{useDateFormatter-2k4mHRBq.js → useDateFormatter-B4ygYuUU.js} +2 -2
- package/dist/{useDeepCompareMemoize-Cb2GRQIS.js → useDeepCompareMemoize-Bcu0DkCS.js} +1 -1
- package/dist/{useLifecycle-B2fV3c8N.js → useLifecycle-MTdtnR3-.js} +1 -1
- package/dist/{vega-component-do3stf0K.js → vega-component-BhBbP4S7.js} +6 -6
- package/dist/{vennDiagram-4TSXK5OY-BLq7sNjb.js → vennDiagram-4TSXK5OY-BrHqWB1U.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-oyJEWWtb.js → wardleyDiagram-VM6X3IG4-Cms1K_3v.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-DMjWgQPm.js → xychartDiagram-S5SC5T6Z-_kUiODzS.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +0 -2
- package/src/components/chat/__tests__/notebook-context.test.ts +199 -0
- package/src/components/chat/chat-panel.tsx +33 -15
- package/src/components/chat/notebook-context.ts +80 -0
- package/src/components/editor/cell/StopButton.tsx +3 -3
- package/src/components/editor/cell/cell-context-menu.tsx +18 -2
- package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +38 -1
- package/src/components/editor/cell/code/cell-editor.tsx +2 -1
- package/src/components/editor/cell/code/language-toggle.tsx +6 -4
- package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +9 -6
- package/src/components/slides/slide-cell-view.tsx +2 -1
- package/src/components/ui/__tests__/fullscreen.test.tsx +56 -1
- package/src/components/ui/fullscreen.tsx +18 -2
- package/src/components/ui/slider.tsx +6 -2
- package/src/core/codemirror/copilot/getCodes.ts +1 -1
- package/src/core/edit-app.tsx +5 -4
- package/src/core/executions/__tests__/input.test.ts +1 -1
- package/src/core/executions/__tests__/interrupt.test.ts +200 -0
- package/src/core/executions/__tests__/run.test.ts +3 -3
- package/src/core/executions/index.ts +1 -0
- package/src/core/executions/interrupt.ts +49 -0
- package/src/core/islands/bridge.ts +0 -2
- package/src/core/network/requests-network.ts +0 -15
- package/src/core/network/requests-static.ts +0 -2
- package/src/core/network/requests-toasting.tsx +0 -2
- package/src/core/network/types.ts +0 -3
- package/src/core/notebook/document/__tests__/model.test.ts +4 -4
- package/src/core/notebook/document/__tests__/test-host.test.ts +6 -6
- package/src/core/notebook/document/__tests__/test-host.ts +42 -46
- package/src/core/notebook/document/__tests__/test-notebook.ts +5 -4
- package/src/core/notebook/document/sync.ts +12 -5
- package/src/core/notebook/replica/__tests__/fixtures.ts +8 -0
- package/src/core/notebook/replica/backend.ts +1 -1
- package/src/core/saving/__tests__/document.test.tsx +1 -1
- package/src/core/transports/__tests__/static.test.ts +1 -1
- package/src/core/transports/__tests__/wasm.test.ts +1 -1
- package/src/core/transports/static.ts +8 -2
- package/src/core/transports/wasm.ts +2 -4
- package/src/core/wasm/__tests__/bridge.test.ts +170 -26
- package/src/core/wasm/bridge.ts +64 -19
- package/src/core/wasm/worker/types.ts +0 -1
- package/src/plugins/impl/DataEditorPlugin.tsx +32 -21
- package/src/plugins/impl/SliderPlugin.tsx +35 -3
- package/src/plugins/impl/__test__/slider-labels.test.tsx +30 -0
- package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +103 -12
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +40 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +159 -4
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +30 -10
- package/src/plugins/impl/data-editor/glide-portal.tsx +5 -3
- package/src/plugins/layout/AccordionPlugin.tsx +2 -1
- package/src/plugins/layout/__tests__/AccordionPlugin.test.tsx +21 -0
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -76,6 +76,9 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
76
76
|
/** Runs asked to stop before they ran; the buffer is raised once they do. */
|
|
77
77
|
private pendingInterrupts = new Set<string>();
|
|
78
78
|
|
|
79
|
+
// Match the worker mutation receipt window, including requests still in flight.
|
|
80
|
+
private interruptReceipts = new Map<string, { expiresAt: number }>();
|
|
81
|
+
|
|
79
82
|
public initialized = new Deferred<void>();
|
|
80
83
|
|
|
81
84
|
private getSaveWorker(): SaveWorker {
|
|
@@ -251,19 +254,53 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
251
254
|
throw error;
|
|
252
255
|
}
|
|
253
256
|
},
|
|
254
|
-
call: (call) => {
|
|
257
|
+
call: async (call) => {
|
|
258
|
+
if (
|
|
259
|
+
call.operation !== "runtime.interrupt" &&
|
|
260
|
+
call.operation !== "runtime.cancel"
|
|
261
|
+
) {
|
|
262
|
+
return this.rpc.proxy.request.notebookCall(call);
|
|
263
|
+
}
|
|
264
|
+
const now = performance.now();
|
|
265
|
+
for (const [key, receipt] of this.interruptReceipts) {
|
|
266
|
+
if (receipt.expiresAt <= now) {
|
|
267
|
+
this.interruptReceipts.delete(key);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const key = call.headers["idempotency-key"];
|
|
271
|
+
if (!key || this.interruptReceipts.has(key)) {
|
|
272
|
+
return this.rpc.proxy.request.notebookCall(call);
|
|
273
|
+
}
|
|
274
|
+
const receipt = { expiresAt: Infinity };
|
|
275
|
+
this.interruptReceipts.set(key, receipt);
|
|
276
|
+
// The worker cannot take a call while a cell runs, so the interrupt
|
|
277
|
+
// is raised on the shared buffer first and the call follows.
|
|
278
|
+
// A queued run is ended by the worker, unless it starts before the
|
|
279
|
+
// call arrives. Then it is raised once the run reports running.
|
|
255
280
|
if (call.operation === "runtime.interrupt") {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// unless it starts before the call arrives: then it is raised
|
|
259
|
-
// when the run is reported running.
|
|
281
|
+
this.interruptEveryRun();
|
|
282
|
+
} else if (call.operation === "runtime.cancel") {
|
|
260
283
|
if (this.isRunning(call.params.run_id)) {
|
|
261
|
-
|
|
284
|
+
this.raiseInterrupt();
|
|
262
285
|
} else {
|
|
263
286
|
this.pendingInterrupts.add(call.params.run_id);
|
|
264
287
|
}
|
|
265
288
|
}
|
|
266
|
-
|
|
289
|
+
try {
|
|
290
|
+
const reply = await this.rpc.proxy.request.notebookCall(call);
|
|
291
|
+
if (
|
|
292
|
+
typeof reply === "object" &&
|
|
293
|
+
reply !== null &&
|
|
294
|
+
"status" in reply &&
|
|
295
|
+
reply.status !== 202
|
|
296
|
+
) {
|
|
297
|
+
this.interruptReceipts.delete(key);
|
|
298
|
+
}
|
|
299
|
+
return reply;
|
|
300
|
+
} finally {
|
|
301
|
+
// A lost reply may still have committed; retries must not signal new work.
|
|
302
|
+
receipt.expiresAt = performance.now() + 300_000;
|
|
303
|
+
}
|
|
267
304
|
},
|
|
268
305
|
});
|
|
269
306
|
}
|
|
@@ -283,12 +320,29 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
283
320
|
}
|
|
284
321
|
if (run.status === "running") {
|
|
285
322
|
this.pendingInterrupts.delete(run.id);
|
|
286
|
-
|
|
323
|
+
this.raiseInterrupt();
|
|
287
324
|
} else if (run.status === "completed") {
|
|
288
325
|
this.pendingInterrupts.delete(run.id);
|
|
289
326
|
}
|
|
290
327
|
}
|
|
291
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Signal work outside runs too. Python ignores the signal while idle;
|
|
331
|
+
* the following host call consumes it before acknowledging the interrupt.
|
|
332
|
+
*/
|
|
333
|
+
private interruptEveryRun(): void {
|
|
334
|
+
this.raiseInterrupt();
|
|
335
|
+
const replica = store.get(activeNotebookReplicaAtom);
|
|
336
|
+
if (replica === null) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
for (const run of store.get(replica.executionsAtom).runs.values()) {
|
|
340
|
+
if (run.status === "queued") {
|
|
341
|
+
this.pendingInterrupts.add(run.id);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
292
346
|
/** Whether the host reports `runId` as running, so a cell is executing. */
|
|
293
347
|
private isRunning(runId: string): boolean {
|
|
294
348
|
const replica = store.get(activeNotebookReplicaAtom);
|
|
@@ -324,14 +378,6 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
324
378
|
throwNotImplemented();
|
|
325
379
|
};
|
|
326
380
|
|
|
327
|
-
public sendStdin: EditRequests["sendStdin"] = async (request) => {
|
|
328
|
-
await this.rpc.proxy.request.bridge({
|
|
329
|
-
functionName: "put_input",
|
|
330
|
-
payload: request.text,
|
|
331
|
-
});
|
|
332
|
-
return null;
|
|
333
|
-
};
|
|
334
|
-
|
|
335
381
|
public sendPdb: EditRequests["sendPdb"] = async () => {
|
|
336
382
|
throwNotImplemented();
|
|
337
383
|
};
|
|
@@ -351,13 +397,12 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
351
397
|
});
|
|
352
398
|
return null;
|
|
353
399
|
};
|
|
354
|
-
|
|
400
|
+
private raiseInterrupt(): void {
|
|
355
401
|
if (this.interruptBuffer !== undefined) {
|
|
356
402
|
// 2 sends a SIGINT
|
|
357
403
|
this.interruptBuffer[0] = 2;
|
|
358
404
|
}
|
|
359
|
-
|
|
360
|
-
};
|
|
405
|
+
}
|
|
361
406
|
public sendShutdown: EditRequests["sendShutdown"] = async () => {
|
|
362
407
|
window.close();
|
|
363
408
|
return null;
|
|
@@ -72,7 +72,6 @@ export interface WasmSession {
|
|
|
72
72
|
|
|
73
73
|
export interface RawBridge {
|
|
74
74
|
put_control_request(operation: ClientCommand): Promise<string>;
|
|
75
|
-
put_input(input: string): Promise<string>;
|
|
76
75
|
code_complete(request: CodeCompletionRequest): Promise<string>;
|
|
77
76
|
read_code(): Promise<{ contents: string }>;
|
|
78
77
|
read_snippets(): Promise<Snippets>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import glideCss from "@glideapps/glide-data-grid/dist/index.css?inline";
|
|
4
4
|
import glideCellsCss from "@glideapps/glide-data-grid-cells/dist/index.css?inline";
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useMemo } from "react";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { inferFieldTypes } from "@/components/data-table/columns";
|
|
8
8
|
import { LoadingTable } from "@/components/data-table/loading-table";
|
|
@@ -90,15 +90,23 @@ interface Props {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const LoadingDataEditor = (props: Props) => {
|
|
93
|
-
const
|
|
93
|
+
const source = useMemo(
|
|
94
|
+
() => ({
|
|
95
|
+
data: props.data,
|
|
96
|
+
fieldTypes: props.fieldTypes,
|
|
97
|
+
columnNames: props.columnNames,
|
|
98
|
+
columnTypes: props.columnTypes,
|
|
99
|
+
}),
|
|
100
|
+
[props.data, props.fieldTypes, props.columnNames, props.columnTypes],
|
|
101
|
+
);
|
|
94
102
|
|
|
95
103
|
const { data: loadedState, error } = useAsyncData(async () => {
|
|
96
|
-
const withoutExternalTypes = toFieldTypes(
|
|
104
|
+
const withoutExternalTypes = toFieldTypes(source.fieldTypes ?? []);
|
|
97
105
|
|
|
98
|
-
const localData = Array.isArray(
|
|
99
|
-
?
|
|
106
|
+
const localData = Array.isArray(source.data)
|
|
107
|
+
? source.data
|
|
100
108
|
: await vegaLoadData<EditorRow>(
|
|
101
|
-
|
|
109
|
+
source.data,
|
|
102
110
|
{
|
|
103
111
|
type: "csv",
|
|
104
112
|
parse: getVegaFieldTypes(Object.fromEntries(withoutExternalTypes)),
|
|
@@ -106,9 +114,9 @@ const LoadingDataEditor = (props: Props) => {
|
|
|
106
114
|
{ handleBigIntAndNumberLike: true },
|
|
107
115
|
);
|
|
108
116
|
|
|
109
|
-
const columnTypes = new Map(Object.entries(
|
|
117
|
+
const columnTypes = new Map(Object.entries(source.columnTypes));
|
|
110
118
|
const inferredFields = toFieldTypes(
|
|
111
|
-
|
|
119
|
+
source.fieldTypes ?? inferFieldTypes(localData),
|
|
112
120
|
);
|
|
113
121
|
for (const [columnName, columnType] of columnTypes) {
|
|
114
122
|
inferredFields.set(
|
|
@@ -118,17 +126,23 @@ const LoadingDataEditor = (props: Props) => {
|
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
source,
|
|
130
|
+
state: {
|
|
131
|
+
data: normalizeConfiguredColumns(localData, columnTypes),
|
|
132
|
+
columnFields: orderColumnFields(inferredFields, source.columnNames),
|
|
133
|
+
columnTypes,
|
|
134
|
+
} satisfies EditorState,
|
|
135
|
+
};
|
|
136
|
+
}, [source]);
|
|
126
137
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
// useAsyncData retains previous data while a replacement source loads.
|
|
139
|
+
const editorState = useMemo(
|
|
140
|
+
() =>
|
|
141
|
+
loadedState?.source === source
|
|
142
|
+
? applyEditorEdits(loadedState.state, props.edits.edits)
|
|
143
|
+
: null,
|
|
144
|
+
[loadedState, source, props.edits.edits],
|
|
145
|
+
);
|
|
132
146
|
|
|
133
147
|
if (error) {
|
|
134
148
|
return (
|
|
@@ -156,9 +170,6 @@ const LoadingDataEditor = (props: Props) => {
|
|
|
156
170
|
columnTypes={editorState.columnTypes}
|
|
157
171
|
editableColumns={props.editableColumns}
|
|
158
172
|
onAddEdits={(edits) => {
|
|
159
|
-
setEditorState((state) =>
|
|
160
|
-
state === null ? null : applyEditorEdits(state, edits),
|
|
161
|
-
);
|
|
162
173
|
props.onEdits((v) => ({ ...v, edits: [...v.edits, ...edits] }));
|
|
163
174
|
}}
|
|
164
175
|
/>
|
|
@@ -7,6 +7,8 @@ import { useInternalStateWithSync } from "@/hooks/useInternalStateWithSync";
|
|
|
7
7
|
import { cn } from "@/utils/cn";
|
|
8
8
|
import { clamp } from "@/utils/math";
|
|
9
9
|
import {
|
|
10
|
+
countFractionDigits,
|
|
11
|
+
fractionDigitsForSlider,
|
|
10
12
|
maxFractionDigitsForSteps,
|
|
11
13
|
prettyScientificNumber,
|
|
12
14
|
roundToFractionDigits,
|
|
@@ -155,6 +157,7 @@ const SliderComponent = ({
|
|
|
155
157
|
const id = useId();
|
|
156
158
|
const labelId = useId();
|
|
157
159
|
const { locale } = useLocale();
|
|
160
|
+
const isFullWidthHorizontal = fullWidth && orientation === "horizontal";
|
|
158
161
|
|
|
159
162
|
// Hold internal value
|
|
160
163
|
const [internalValue, setInternalValue] = useInternalStateWithSync(
|
|
@@ -163,6 +166,18 @@ const SliderComponent = ({
|
|
|
163
166
|
);
|
|
164
167
|
|
|
165
168
|
const stepsConfig = useMemo(() => computeStepsConfig(steps), [steps]);
|
|
169
|
+
const stepDigits = countFractionDigits(step ?? 0);
|
|
170
|
+
// Keep exact decimals; repeating fractions need only enough digits to distinguish steps.
|
|
171
|
+
const isDecimalStep =
|
|
172
|
+
step != null && roundToFractionDigits(step, stepDigits) === step;
|
|
173
|
+
const fractionDigits =
|
|
174
|
+
step != null && Number.isFinite(step) && step > 0 && !steps?.length
|
|
175
|
+
? Math.max(
|
|
176
|
+
countFractionDigits(start),
|
|
177
|
+
isDecimalStep ? stepDigits : 2,
|
|
178
|
+
Math.ceil(-Math.log10(step)),
|
|
179
|
+
)
|
|
180
|
+
: fractionDigitsForSlider(step, steps, start, [valueMap(internalValue)]);
|
|
166
181
|
|
|
167
182
|
const handleInputChange = (nextValue: number | null | undefined): void => {
|
|
168
183
|
if (stepsConfig) {
|
|
@@ -232,6 +247,10 @@ const SliderComponent = ({
|
|
|
232
247
|
<div
|
|
233
248
|
className={cn(
|
|
234
249
|
"flex items-center gap-2",
|
|
250
|
+
isFullWidthHorizontal && "grid w-full grid-cols-1 gap-y-1",
|
|
251
|
+
isFullWidthHorizontal &&
|
|
252
|
+
includeInput &&
|
|
253
|
+
"grid-cols-[minmax(0,1fr)_auto]",
|
|
235
254
|
orientation === "vertical" &&
|
|
236
255
|
"items-end inline-flex justify-center self-center mx-2",
|
|
237
256
|
)}
|
|
@@ -264,18 +283,31 @@ const SliderComponent = ({
|
|
|
264
283
|
}
|
|
265
284
|
}}
|
|
266
285
|
valueMap={valueMap} // Pass valueMap to Slider
|
|
286
|
+
maximumFractionDigits={fractionDigits}
|
|
267
287
|
disabled={disabled}
|
|
268
288
|
/>
|
|
269
289
|
{showValue && (
|
|
270
|
-
<div
|
|
271
|
-
{
|
|
290
|
+
<div
|
|
291
|
+
className={cn(
|
|
292
|
+
"text-xs text-muted-foreground min-w-[16px]",
|
|
293
|
+
isFullWidthHorizontal &&
|
|
294
|
+
"col-span-full row-start-2 min-w-0 max-w-full justify-self-end break-all",
|
|
295
|
+
)}
|
|
296
|
+
>
|
|
297
|
+
{prettyScientificNumber(valueMap(internalValue), {
|
|
298
|
+
locale,
|
|
299
|
+
maximumFractionDigits: fractionDigits,
|
|
300
|
+
})}
|
|
272
301
|
</div>
|
|
273
302
|
)}
|
|
274
303
|
{includeInput && (
|
|
275
304
|
<NumberField
|
|
276
305
|
{...inputProps}
|
|
277
306
|
onChange={handleInputChange}
|
|
278
|
-
className=
|
|
307
|
+
className={cn(
|
|
308
|
+
"w-24",
|
|
309
|
+
isFullWidthHorizontal && "col-start-2 row-start-1",
|
|
310
|
+
)}
|
|
279
311
|
aria-label={`${label || "Slider"} value input`}
|
|
280
312
|
isDisabled={disabled}
|
|
281
313
|
/>
|
|
@@ -134,3 +134,33 @@ describe("SliderPlugin accessible labels", () => {
|
|
|
134
134
|
expect(screen.getByRole("tooltip")).toHaveTextContent("50");
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
describe("SliderPlugin value formatting", () => {
|
|
139
|
+
test.each([
|
|
140
|
+
{ step: 0.125, value: 0.125, expected: "0.125" },
|
|
141
|
+
{ step: 0.025, value: 0.025, expected: "0.025" },
|
|
142
|
+
{ step: 0.125, value: 0.5, expected: "0.5" },
|
|
143
|
+
{ step: 0.025, value: 1, expected: "1" },
|
|
144
|
+
{ step: 1 / 30, value: 1 / 30, expected: "0.03" },
|
|
145
|
+
{ step: 1 / 30, value: 35.86666666666667, expected: "35.87" },
|
|
146
|
+
{ step: 0.1, value: 0.30000000000000004, expected: "0.3" },
|
|
147
|
+
])(
|
|
148
|
+
"formats $value with step $step consistently",
|
|
149
|
+
({ step, value, expected }) => {
|
|
150
|
+
const initial = props("Value");
|
|
151
|
+
render(
|
|
152
|
+
plugin.render({
|
|
153
|
+
...initial,
|
|
154
|
+
value,
|
|
155
|
+
data: { ...initial.data, step, stop: 60, showValue: true },
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
expect(screen.getByText(expected)).toBeVisible();
|
|
159
|
+
const thumb = screen.getByRole("slider");
|
|
160
|
+
fireEvent.focus(thumb);
|
|
161
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(expected);
|
|
162
|
+
expect(thumb).toHaveAttribute("aria-valuenow", String(value));
|
|
163
|
+
expect(initial.setValue).not.toHaveBeenCalled();
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
});
|
|
@@ -4,14 +4,17 @@ import React, { Suspense } from "react";
|
|
|
4
4
|
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import type { z } from "zod";
|
|
7
|
-
import type { IPluginProps } from "../../types";
|
|
8
|
-
import type {
|
|
7
|
+
import type { IPluginProps, Setter } from "../../types";
|
|
8
|
+
import type { GlideDataEditor } from "../data-editor/glide-data-editor";
|
|
9
|
+
import type { ColumnType, EditorRow, Edits } from "../data-editor/types";
|
|
9
10
|
import { DataEditorPlugin } from "../DataEditorPlugin";
|
|
10
11
|
|
|
11
12
|
type PluginData = z.infer<typeof DataEditorPlugin.validator>;
|
|
12
13
|
|
|
13
14
|
const mocks = vi.hoisted(() => ({
|
|
14
15
|
loadData: vi.fn(),
|
|
16
|
+
renderEditor:
|
|
17
|
+
vi.fn<(props: React.ComponentProps<typeof GlideDataEditor>) => void>(),
|
|
15
18
|
}));
|
|
16
19
|
|
|
17
20
|
vi.mock("../vega/loader", () => ({
|
|
@@ -21,8 +24,9 @@ vi.mock("../vega/loader", () => ({
|
|
|
21
24
|
vi.mock("../data-editor/glide-data-editor", async () => {
|
|
22
25
|
const React = await import("react");
|
|
23
26
|
return {
|
|
24
|
-
default: (props:
|
|
25
|
-
|
|
27
|
+
default: (props: React.ComponentProps<typeof GlideDataEditor>) => {
|
|
28
|
+
mocks.renderEditor(props);
|
|
29
|
+
return React.createElement(
|
|
26
30
|
React.Fragment,
|
|
27
31
|
null,
|
|
28
32
|
React.createElement(
|
|
@@ -33,22 +37,23 @@ vi.mock("../data-editor/glide-data-editor", async () => {
|
|
|
33
37
|
React.createElement(
|
|
34
38
|
"pre",
|
|
35
39
|
{ "data-testid": "editor-fields" },
|
|
36
|
-
JSON.stringify(
|
|
37
|
-
props.columnFields instanceof Map
|
|
38
|
-
? [...props.columnFields]
|
|
39
|
-
: props.columnFields,
|
|
40
|
-
),
|
|
40
|
+
JSON.stringify([...props.columnFields]),
|
|
41
41
|
),
|
|
42
|
-
)
|
|
42
|
+
);
|
|
43
|
+
},
|
|
43
44
|
};
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
function renderPlugin(
|
|
47
|
+
function renderPlugin(
|
|
48
|
+
data: PluginData,
|
|
49
|
+
value: Edits,
|
|
50
|
+
setValue: Setter<Edits> = vi.fn(),
|
|
51
|
+
) {
|
|
47
52
|
const props: IPluginProps<Edits, PluginData> = {
|
|
48
53
|
host: document.createElement("div"),
|
|
49
54
|
data,
|
|
50
55
|
value,
|
|
51
|
-
setValue
|
|
56
|
+
setValue,
|
|
52
57
|
functions: {},
|
|
53
58
|
};
|
|
54
59
|
return React.createElement(
|
|
@@ -61,6 +66,7 @@ function renderPlugin(data: PluginData, value: Edits) {
|
|
|
61
66
|
describe("DataEditorPlugin", () => {
|
|
62
67
|
beforeEach(() => {
|
|
63
68
|
mocks.loadData.mockReset();
|
|
69
|
+
mocks.renderEditor.mockClear();
|
|
64
70
|
});
|
|
65
71
|
|
|
66
72
|
it("normalizes an unrecognized field type", () => {
|
|
@@ -212,6 +218,7 @@ describe("DataEditorPlugin", () => {
|
|
|
212
218
|
);
|
|
213
219
|
});
|
|
214
220
|
|
|
221
|
+
mocks.renderEditor.mockClear();
|
|
215
222
|
result.rerender(
|
|
216
223
|
renderPlugin(data, {
|
|
217
224
|
edits: [{ rowIdx: 0, columnId: "name", value: "updated" }],
|
|
@@ -223,6 +230,90 @@ describe("DataEditorPlugin", () => {
|
|
|
223
230
|
JSON.stringify([{ name: "updated" }]),
|
|
224
231
|
);
|
|
225
232
|
});
|
|
233
|
+
expect(mocks.renderEditor).toHaveBeenCalled();
|
|
234
|
+
for (const [props] of mocks.renderEditor.mock.calls) {
|
|
235
|
+
expect(props.data).toEqual([{ name: "updated" }]);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("accumulates cell edits through the plugin value", async () => {
|
|
240
|
+
const data = DataEditorPlugin.validator.parse({
|
|
241
|
+
initialValue: { edits: [] },
|
|
242
|
+
label: null,
|
|
243
|
+
data: [{ name: "original" }],
|
|
244
|
+
editableColumns: "all",
|
|
245
|
+
});
|
|
246
|
+
const Editor = () => {
|
|
247
|
+
const [value, setValue] = React.useState<Edits>({ edits: [] });
|
|
248
|
+
return renderPlugin(data, value, setValue);
|
|
249
|
+
};
|
|
250
|
+
render(React.createElement(Editor));
|
|
251
|
+
await screen.findByTestId("editor-data");
|
|
252
|
+
|
|
253
|
+
act(() => {
|
|
254
|
+
const onAddEdits = mocks.renderEditor.mock.lastCall?.[0].onAddEdits;
|
|
255
|
+
onAddEdits?.([{ rowIdx: 0, columnId: "name", value: "updated" }]);
|
|
256
|
+
onAddEdits?.([{ rowIdx: 1, columnId: "name", value: "appended" }]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(screen.getByTestId("editor-data").textContent).toBe(
|
|
260
|
+
JSON.stringify([{ name: "updated" }, { name: "appended" }]),
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("removes the previous editor while loading a replacement schema", async () => {
|
|
265
|
+
const original = DataEditorPlugin.validator.parse({
|
|
266
|
+
initialValue: { edits: [] },
|
|
267
|
+
label: null,
|
|
268
|
+
data: [{ name: "original", day: "2026-11-06" }],
|
|
269
|
+
editableColumns: "all",
|
|
270
|
+
});
|
|
271
|
+
const result = render(renderPlugin(original, { edits: [] }));
|
|
272
|
+
await screen.findByTestId("editor-data");
|
|
273
|
+
mocks.renderEditor.mockClear();
|
|
274
|
+
|
|
275
|
+
let resolveLoad: ((data: EditorRow[]) => void) | undefined;
|
|
276
|
+
mocks.loadData.mockImplementationOnce(
|
|
277
|
+
() =>
|
|
278
|
+
new Promise<EditorRow[]>((resolve) => {
|
|
279
|
+
resolveLoad = resolve;
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
const replacement = DataEditorPlugin.validator.parse({
|
|
283
|
+
initialValue: { edits: [] },
|
|
284
|
+
label: null,
|
|
285
|
+
data: "replacement.csv",
|
|
286
|
+
columnNames: ["task", "done", "priority"],
|
|
287
|
+
columnTypes: { done: "boolean", priority: ["01", "02", "10"] },
|
|
288
|
+
editableColumns: ["task"],
|
|
289
|
+
});
|
|
290
|
+
result.rerender(
|
|
291
|
+
renderPlugin(replacement, {
|
|
292
|
+
edits: [{ rowIdx: 0, columnId: "task", value: "latest" }],
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(screen.queryByTestId("editor-data")).toBeNull();
|
|
297
|
+
expect(mocks.renderEditor).not.toHaveBeenCalled();
|
|
298
|
+
await act(async () => {
|
|
299
|
+
resolveLoad?.([{ task: "Review", done: "yes", priority: "01" }]);
|
|
300
|
+
});
|
|
301
|
+
await screen.findByTestId("editor-data");
|
|
302
|
+
expect(mocks.renderEditor).toHaveBeenLastCalledWith(
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
data: [{ task: "latest", done: true, priority: "01" }],
|
|
305
|
+
columnFields: new Map([
|
|
306
|
+
["task", "string"],
|
|
307
|
+
["done", "boolean"],
|
|
308
|
+
["priority", "string"],
|
|
309
|
+
]),
|
|
310
|
+
columnTypes: new Map<string, ColumnType>([
|
|
311
|
+
["done", "boolean"],
|
|
312
|
+
["priority", ["01", "02", "10"]],
|
|
313
|
+
]),
|
|
314
|
+
editableColumns: ["task"],
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
226
317
|
});
|
|
227
318
|
|
|
228
319
|
it("ignores a superseded async data load", async () => {
|
|
@@ -134,6 +134,46 @@ describe("SliderPlugin", () => {
|
|
|
134
134
|
});
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
+
it.each([
|
|
138
|
+
{ step: 1 / 30, value: 35.86666666666667, expected: "35.87" },
|
|
139
|
+
{ step: 0.1, value: 0.30000000000000004, expected: "0.3" },
|
|
140
|
+
{ step: 0.25, value: 1.25, expected: "1.25" },
|
|
141
|
+
{ step: 0.001, value: 1.234, expected: "1.234" },
|
|
142
|
+
{ step: 1e-7, value: 3e-7, expected: "3.0e-7" },
|
|
143
|
+
{ step: 1e-15, value: 3e-15, expected: "3.0e-15" },
|
|
144
|
+
{ step: 1, value: 1.0001, expected: "1.0001", start: 0.0001 },
|
|
145
|
+
{ step: undefined, value: 0.1234, expected: "0.1234" },
|
|
146
|
+
{ step: 1, value: 1, expected: "1.234", steps: [1, 1.234, 2] },
|
|
147
|
+
])(
|
|
148
|
+
"formats the displayed value for step $step",
|
|
149
|
+
({ step, value, expected, start = 0, steps = null }) => {
|
|
150
|
+
const plugin = new SliderPlugin();
|
|
151
|
+
const setValue = vi.fn();
|
|
152
|
+
const props = createProps({
|
|
153
|
+
debounce: false,
|
|
154
|
+
includeInput: false,
|
|
155
|
+
setValue,
|
|
156
|
+
});
|
|
157
|
+
const { getByText } = render(
|
|
158
|
+
plugin.render({
|
|
159
|
+
...props,
|
|
160
|
+
value,
|
|
161
|
+
data: {
|
|
162
|
+
...props.data,
|
|
163
|
+
step,
|
|
164
|
+
start,
|
|
165
|
+
steps,
|
|
166
|
+
stop: 60,
|
|
167
|
+
showValue: true,
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(getByText(expected)).toBeVisible();
|
|
173
|
+
expect(setValue).not.toHaveBeenCalled();
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
137
177
|
it("slider triggers setValue immediately when debounce is false", () => {
|
|
138
178
|
const plugin = new SliderPlugin();
|
|
139
179
|
const setValue = vi.fn();
|