@marimo-team/frontend 0.19.7-dev30 → 0.19.7-dev33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/assets/{CellStatus-DpVrMkoX.js → CellStatus-DLlfsrEI.js} +1 -1
  2. package/dist/assets/ConnectedDataExplorerComponent-CCkOUPJb.js +1 -0
  3. package/dist/assets/{JsonOutput-DqRUfM9x.js → JsonOutput-Dw_ZIGoz.js} +1 -1
  4. package/dist/assets/{MarimoErrorOutput-CI9ZOqK5.js → MarimoErrorOutput-DscugIeA.js} +1 -1
  5. package/dist/assets/{RenderHTML-DCy5wLx9.js → RenderHTML-DPkeBHFB.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-1AcZwwMS.js → add-cell-with-ai-BHgqYu8P.js} +1 -1
  7. package/dist/assets/{add-database-form-B45yZ_nV.js → add-database-form-DAMSmPZS.js} +1 -1
  8. package/dist/assets/{agent-panel-BuwVy_u2.js → agent-panel-BOEtQvcD.js} +1 -1
  9. package/dist/assets/{ai-model-dropdown-Ce9P2gAN.js → ai-model-dropdown-DxImvtE1.js} +1 -1
  10. package/dist/assets/{app-config-button-V0ZGbZwL.js → app-config-button-D4NHNYdV.js} +1 -1
  11. package/dist/assets/assertNever-CBU83Y6o.js +1 -0
  12. package/dist/assets/capitalize-CmNnkG9y.js +1 -0
  13. package/dist/assets/{cell-editor-Bs8pOtdM.js → cell-editor-Bed2HJRi.js} +1 -1
  14. package/dist/assets/{cell-link-Draa5Cqg.js → cell-link-a1r84hCk.js} +1 -1
  15. package/dist/assets/{cells-D3A832Pq.js → cells-Mf-pdsEh.js} +4 -4
  16. package/dist/assets/{chat-components-BIikypOv.js → chat-components-BpunBJu9.js} +1 -1
  17. package/dist/assets/chat-display-B0625p01.js +1 -0
  18. package/dist/assets/{chat-panel-hwTI5twF.js → chat-panel-CkHdco_X.js} +1 -1
  19. package/dist/assets/{column-preview-BI7AO_9o.js → column-preview-YJ759JhT.js} +1 -1
  20. package/dist/assets/{command-BmRMpmeq.js → command-BmWAYrdT.js} +1 -1
  21. package/dist/assets/{command-palette-Bf5XRuku.js → command-palette-c1vQwlGd.js} +1 -1
  22. package/dist/assets/{common-DhAkBsKv.js → common-kVa9xjZc.js} +1 -1
  23. package/dist/assets/{datasource-DH4rSucG.js → datasource-jW7Cq5OE.js} +1 -1
  24. package/dist/assets/{dependency-graph-panel-BgRP2qVg.js → dependency-graph-panel-BWvUSpJI.js} +1 -1
  25. package/dist/assets/{documentation-panel-BKQG1zId.js → documentation-panel-z2oxdgzR.js} +1 -1
  26. package/dist/assets/{download-BJ50folP.js → download-vBVDTXQk.js} +2 -2
  27. package/dist/assets/{edit-page-Dzfy3wo-.js → edit-page-D-_8Dpee.js} +3 -3
  28. package/dist/assets/{error-panel-B23uWV5g.js → error-panel-BxmwSms7.js} +1 -1
  29. package/dist/assets/{file-explorer-panel-DQucxlTZ.js → file-explorer-panel-Bhrg_Wb0.js} +1 -1
  30. package/dist/assets/{floating-outline-C0NCQURJ.js → floating-outline-mEMcQGQK.js} +1 -1
  31. package/dist/assets/{focus-DdU24YN9.js → focus-ay4g-SB6.js} +1 -1
  32. package/dist/assets/{form-C4ZlGvVV.js → form-DFq7l6gy.js} +1 -1
  33. package/dist/assets/glide-data-editor-BBSxoBI-.js +132 -0
  34. package/dist/assets/{globals-DJ12V9Kv.js → globals-Du9rkBEf.js} +1 -1
  35. package/dist/assets/{home-page-DUgD5VOD.js → home-page-Cg221ah6.js} +1 -1
  36. package/dist/assets/hooks-CPM5R4f-.js +1 -0
  37. package/dist/assets/html-to-image-Cdx1xsbU.js +2 -0
  38. package/dist/assets/{index-BaIb0_Ut.js → index-DL0FpFYQ.js} +6 -6
  39. package/dist/assets/index-DmMvDRRC.css +2 -0
  40. package/dist/assets/{kiosk-mode-CiC9R5CJ.js → kiosk-mode-Y_Rs0fTN.js} +1 -1
  41. package/dist/assets/{layout-CQQNB4AA.js → layout-C50dPCAb.js} +3 -3
  42. package/dist/assets/{logs-panel-BKwAB4Sl.js → logs-panel-bOS3V2nr.js} +1 -1
  43. package/dist/assets/{markdown-renderer-BxPFtrG5.js → markdown-renderer-DSY-ElEE.js} +1 -1
  44. package/dist/assets/{mode-BGSml4Q4.js → mode-TsexQOfl.js} +1 -1
  45. package/dist/assets/{name-cell-input-wYLF2zUR.js → name-cell-input-B_OPYPFz.js} +1 -1
  46. package/dist/assets/{outline-panel-Cp48IMlZ.js → outline-panel-DkOJrPIL.js} +1 -1
  47. package/dist/assets/{packages-panel-Drho5urs.js → packages-panel-thVjzLK4.js} +1 -1
  48. package/dist/assets/panels-H0GWZM90.js +1 -0
  49. package/dist/assets/{process-output-Bw6cqAbr.js → process-output-ccUMEFYE.js} +1 -1
  50. package/dist/assets/{readonly-python-code-CxRVkq45.js → readonly-python-code-B1UBDfCT.js} +1 -1
  51. package/dist/assets/{run-page-DI11aUJZ.js → run-page-CiOYN_zF.js} +1 -1
  52. package/dist/assets/{scratchpad-panel-vzWFhKxS.js → scratchpad-panel-BiE33kLt.js} +1 -1
  53. package/dist/assets/session-panel-DCRBt85z.js +1 -0
  54. package/dist/assets/{snippets-panel-Dx1FdjOx.js → snippets-panel-DeCtu_nU.js} +1 -1
  55. package/dist/assets/spec-D1kBp3jX.js +1 -0
  56. package/dist/assets/{state-D1OFbTRC.js → state-BXbqGYab.js} +1 -1
  57. package/dist/assets/{switch-Cqx3FjIU.js → switch-NTEWSiVz.js} +1 -1
  58. package/dist/assets/{textarea-BqB4s8zj.js → textarea-Ddtm_ohJ.js} +1 -1
  59. package/dist/assets/{tracing-Bu-VQRRo.js → tracing-Hw-L0vPw.js} +1 -1
  60. package/dist/assets/tracing-panel-BowfBZDI.js +2 -0
  61. package/dist/assets/{types-D8lESWyo.js → types-1gCn5Ky0.js} +1 -1
  62. package/dist/assets/{useAddCell-B_b-bEhC.js → useAddCell-M6_IuI4C.js} +1 -1
  63. package/dist/assets/{useCellActionButton-DjwaDuay.js → useCellActionButton-Ddy42rre.js} +1 -1
  64. package/dist/assets/{useDeleteCell-7CQhKujJ.js → useDeleteCell-Ct7dOEmt.js} +1 -1
  65. package/dist/assets/{useDependencyPanelTab-CB_Ogk32.js → useDependencyPanelTab-RSRpRVKD.js} +1 -1
  66. package/dist/assets/useLifecycle-D35CBukS.js +1 -0
  67. package/dist/assets/useNotebookActions-C1MxSBln.js +1 -0
  68. package/dist/assets/{useRunCells-DCMO0B1l.js → useRunCells-BgXbioPv.js} +1 -1
  69. package/dist/assets/{useSplitCell-BAsTzBSc.js → useSplitCell-BZ2kxLtm.js} +1 -1
  70. package/dist/assets/{utilities.esm-ZpMudNeK.js → utilities.esm-BsBOP1QQ.js} +1 -1
  71. package/dist/index.html +37 -37
  72. package/package.json +1 -1
  73. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  74. package/src/core/export/__tests__/hooks.test.ts +72 -33
  75. package/src/core/export/hooks.ts +17 -8
  76. package/src/plugins/impl/DataEditorPlugin.tsx +1 -1
  77. package/src/plugins/impl/data-editor/glide-data-editor.tsx +49 -33
  78. package/src/utils/__tests__/download.test.tsx +18 -18
  79. package/src/utils/download.ts +9 -11
  80. package/src/utils/html-to-image.ts +139 -0
  81. package/dist/assets/ConnectedDataExplorerComponent-ec_bSRbX.js +0 -1
  82. package/dist/assets/capitalize-CkclHYWI.js +0 -1
  83. package/dist/assets/chat-display-C9Ma_Z24.js +0 -1
  84. package/dist/assets/glide-data-editor-DVw6MlCk.js +0 -132
  85. package/dist/assets/hooks-ntzLWruV.js +0 -1
  86. package/dist/assets/html-to-image-B02su1Lp.js +0 -2
  87. package/dist/assets/index-DHsXOI_M.css +0 -2
  88. package/dist/assets/panels-DCSfU3wX.js +0 -1
  89. package/dist/assets/session-panel-HNDQ-XhU.js +0 -1
  90. package/dist/assets/spec-BSxN05D8.js +0 -1
  91. package/dist/assets/tracing-panel-Fwxu1-K1.js +0 -2
  92. package/dist/assets/type-DUK-1jKc.js +0 -1
  93. package/dist/assets/useLifecycle-ChNbzbYY.js +0 -1
  94. package/dist/assets/useNotebookActions-C47M79_m.js +0 -1
@@ -102,8 +102,8 @@ describe("useEnrichCellOutputs", () => {
102
102
 
103
103
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
104
104
 
105
- const enrichCellOutputs = result.current;
106
- const output = await enrichCellOutputs(progress);
105
+ const takeScreenshots = result.current;
106
+ const output = await takeScreenshots({ progress, snappy: false });
107
107
 
108
108
  expect(output).toEqual({});
109
109
  expect(document.getElementById).not.toHaveBeenCalled();
@@ -134,8 +134,8 @@ describe("useEnrichCellOutputs", () => {
134
134
 
135
135
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
136
136
 
137
- const enrichCellOutputs = result.current;
138
- const output = await enrichCellOutputs(progress);
137
+ const takeScreenshots = result.current;
138
+ const output = await takeScreenshots({ progress, snappy: false });
139
139
 
140
140
  expect(document.getElementById).toHaveBeenCalledWith(
141
141
  CellOutputId.create(cellId),
@@ -152,6 +152,50 @@ describe("useEnrichCellOutputs", () => {
152
152
  });
153
153
  });
154
154
 
155
+ it("should pass snappy=true to toPng with includeStyleProperties", async () => {
156
+ const cellId = "cell-1" as CellId;
157
+ const mockElement = document.createElement("div");
158
+ const mockDataUrl = "data:image/png;base64,mockImageData";
159
+
160
+ // Mock document.getElementById
161
+ vi.spyOn(document, "getElementById").mockReturnValue(mockElement);
162
+ vi.mocked(toPng).mockResolvedValue(mockDataUrl);
163
+
164
+ setCellsRuntime(
165
+ createMockCellRuntimes({
166
+ [cellId]: {
167
+ output: {
168
+ channel: "output",
169
+ mimetype: "text/html",
170
+ data: "<div>Chart</div>",
171
+ timestamp: 0,
172
+ },
173
+ },
174
+ }),
175
+ );
176
+
177
+ const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
178
+
179
+ const takeScreenshots = result.current;
180
+ const output = await takeScreenshots({ progress, snappy: true });
181
+
182
+ expect(document.getElementById).toHaveBeenCalledWith(
183
+ CellOutputId.create(cellId),
184
+ );
185
+ // When snappy=true, includeStyleProperties should be set
186
+ expect(toPng).toHaveBeenCalledWith(
187
+ mockElement,
188
+ expect.objectContaining({
189
+ filter: expect.any(Function),
190
+ onImageErrorHandler: expect.any(Function),
191
+ includeStyleProperties: expect.any(Array),
192
+ }),
193
+ );
194
+ expect(output).toEqual({
195
+ [cellId]: ["image/png", mockDataUrl],
196
+ });
197
+ });
198
+
155
199
  it("should skip cells where output has not changed", async () => {
156
200
  const cellId = "cell-1" as CellId;
157
201
  const mockElement = document.createElement("div");
@@ -179,8 +223,8 @@ describe("useEnrichCellOutputs", () => {
179
223
  });
180
224
 
181
225
  // First call - should capture
182
- let enrichCellOutputs = result.current;
183
- let output = await enrichCellOutputs(progress);
226
+ let takeScreenshots = result.current;
227
+ let output = await takeScreenshots({ progress, snappy: false });
184
228
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl] });
185
229
  expect(toPng).toHaveBeenCalledTimes(1);
186
230
 
@@ -188,8 +232,8 @@ describe("useEnrichCellOutputs", () => {
188
232
  rerender();
189
233
 
190
234
  // Second call with same output - should not capture again
191
- enrichCellOutputs = result.current;
192
- output = await enrichCellOutputs(progress);
235
+ takeScreenshots = result.current;
236
+ output = await takeScreenshots({ progress, snappy: false });
193
237
  expect(output).toEqual({}); // Empty because output hasn't changed
194
238
  expect(toPng).toHaveBeenCalledTimes(1); // Still only 1 call
195
239
  });
@@ -217,8 +261,8 @@ describe("useEnrichCellOutputs", () => {
217
261
 
218
262
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
219
263
 
220
- const enrichCellOutputs = result.current;
221
- const output = await enrichCellOutputs(progress);
264
+ const takeScreenshots = result.current;
265
+ const output = await takeScreenshots({ progress, snappy: false });
222
266
 
223
267
  expect(output).toEqual({}); // Failed screenshot should be filtered out
224
268
  expect(Logger.error).toHaveBeenCalledWith(
@@ -247,8 +291,8 @@ describe("useEnrichCellOutputs", () => {
247
291
 
248
292
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
249
293
 
250
- const enrichCellOutputs = result.current;
251
- const output = await enrichCellOutputs(progress);
294
+ const takeScreenshots = result.current;
295
+ const output = await takeScreenshots({ progress, snappy: false });
252
296
 
253
297
  expect(output).toEqual({});
254
298
  expect(Logger.error).toHaveBeenCalledWith(
@@ -296,8 +340,8 @@ describe("useEnrichCellOutputs", () => {
296
340
 
297
341
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
298
342
 
299
- const enrichCellOutputs = result.current;
300
- const output = await enrichCellOutputs(progress);
343
+ const takeScreenshots = result.current;
344
+ const output = await takeScreenshots({ progress, snappy: false });
301
345
 
302
346
  expect(output).toEqual({
303
347
  [cell1]: ["image/png", mockDataUrl1],
@@ -342,8 +386,8 @@ describe("useEnrichCellOutputs", () => {
342
386
 
343
387
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
344
388
 
345
- const enrichCellOutputs = result.current;
346
- const output = await enrichCellOutputs(progress);
389
+ const takeScreenshots = result.current;
390
+ const output = await takeScreenshots({ progress, snappy: false });
347
391
 
348
392
  // Only the successful screenshot should be in the result
349
393
  expect(output).toEqual({
@@ -384,14 +428,14 @@ describe("useEnrichCellOutputs", () => {
384
428
  });
385
429
 
386
430
  // First screenshot
387
- let enrichCellOutputs = result.current;
388
- let output = await enrichCellOutputs(progress);
431
+ let takeScreenshots = result.current;
432
+ let output = await takeScreenshots({ progress, snappy: false });
389
433
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl1] });
390
434
 
391
435
  // Second call - same output, should not be captured
392
436
  rerender();
393
- enrichCellOutputs = result.current;
394
- output = await enrichCellOutputs(progress);
437
+ takeScreenshots = result.current;
438
+ output = await takeScreenshots({ progress, snappy: false });
395
439
  expect(output).toEqual({});
396
440
 
397
441
  // Third call - output changed, should be captured
@@ -409,8 +453,8 @@ describe("useEnrichCellOutputs", () => {
409
453
  );
410
454
 
411
455
  rerender();
412
- enrichCellOutputs = result.current;
413
- output = await enrichCellOutputs(progress);
456
+ takeScreenshots = result.current;
457
+ output = await takeScreenshots({ progress, snappy: false });
414
458
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl2] });
415
459
  expect(toPng).toHaveBeenCalledTimes(2);
416
460
  });
@@ -449,8 +493,8 @@ describe("useEnrichCellOutputs", () => {
449
493
 
450
494
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
451
495
 
452
- const enrichCellOutputs = result.current;
453
- const output = await enrichCellOutputs(progress);
496
+ const takeScreenshots = result.current;
497
+ const output = await takeScreenshots({ progress, snappy: false });
454
498
 
455
499
  // None of these should trigger screenshots
456
500
  expect(output).toEqual({});
@@ -474,8 +518,8 @@ describe("useEnrichCellOutputs", () => {
474
518
 
475
519
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
476
520
 
477
- const enrichCellOutputs = result.current;
478
- const output = await enrichCellOutputs(progress);
521
+ const takeScreenshots = result.current;
522
+ const output = await takeScreenshots({ progress, snappy: false });
479
523
 
480
524
  expect(output).toEqual({});
481
525
  expect(document.getElementById).not.toHaveBeenCalled();
@@ -506,8 +550,8 @@ describe("useEnrichCellOutputs", () => {
506
550
 
507
551
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
508
552
 
509
- const enrichCellOutputs = result.current;
510
- const output = await enrichCellOutputs(progress);
553
+ const takeScreenshots = result.current;
554
+ const output = await takeScreenshots({ progress, snappy: false });
511
555
 
512
556
  // Verify the exact return type structure
513
557
  expect(output).toHaveProperty(cellId);
@@ -539,7 +583,6 @@ describe("updateCellOutputsWithScreenshots", () => {
539
583
  const updateCellOutputs = vi.fn().mockResolvedValue(null);
540
584
 
541
585
  await updateCellOutputsWithScreenshots({
542
- progress,
543
586
  takeScreenshots,
544
587
  updateCellOutputs,
545
588
  });
@@ -556,7 +599,6 @@ describe("updateCellOutputsWithScreenshots", () => {
556
599
  const updateCellOutputs = vi.fn().mockResolvedValue(null);
557
600
 
558
601
  await updateCellOutputsWithScreenshots({
559
- progress,
560
602
  takeScreenshots,
561
603
  updateCellOutputs,
562
604
  });
@@ -583,7 +625,6 @@ describe("updateCellOutputsWithScreenshots", () => {
583
625
  const updateCellOutputs = vi.fn().mockResolvedValue(null);
584
626
 
585
627
  await updateCellOutputsWithScreenshots({
586
- progress,
587
628
  takeScreenshots,
588
629
  updateCellOutputs,
589
630
  });
@@ -600,7 +641,6 @@ describe("updateCellOutputsWithScreenshots", () => {
600
641
 
601
642
  // Should not throw - errors are caught and shown via toast
602
643
  await updateCellOutputsWithScreenshots({
603
- progress,
604
644
  takeScreenshots,
605
645
  updateCellOutputs,
606
646
  });
@@ -633,7 +673,6 @@ describe("updateCellOutputsWithScreenshots", () => {
633
673
 
634
674
  // Should not throw - errors are caught and shown via toast
635
675
  await updateCellOutputsWithScreenshots({
636
- progress,
637
676
  takeScreenshots,
638
677
  updateCellOutputs,
639
678
  });
@@ -64,9 +64,13 @@ export function useAutoExport() {
64
64
 
65
65
  useInterval(
66
66
  async () => {
67
+ const screenshotFn = () =>
68
+ takeScreenshots({
69
+ progress: ProgressState.indeterminate(),
70
+ snappy: true,
71
+ });
67
72
  await updateCellOutputsWithScreenshots({
68
- progress: ProgressState.indeterminate(),
69
- takeScreenshots,
73
+ takeScreenshots: screenshotFn,
70
74
  updateCellOutputs,
71
75
  });
72
76
  await autoExportAsIPYNB({
@@ -107,7 +111,13 @@ export function useEnrichCellOutputs() {
107
111
  const [richCellsOutput, setRichCellsOutput] = useAtom(richCellsToOutputAtom);
108
112
  const cellRuntimes = useAtomValue(cellsRuntimeAtom);
109
113
 
110
- return async (progress: ProgressState): Promise<ScreenshotResults> => {
114
+ return async ({
115
+ progress,
116
+ snappy,
117
+ }: {
118
+ progress: ProgressState;
119
+ snappy: boolean;
120
+ }): Promise<ScreenshotResults> => {
111
121
  const trackedCellsOutput: Record<CellId, unknown> = {};
112
122
 
113
123
  const cellsToCaptureScreenshot: [CellId, unknown][] = [];
@@ -138,7 +148,7 @@ export function useEnrichCellOutputs() {
138
148
  const results: ScreenshotResults = {};
139
149
  for (const [cellId] of cellsToCaptureScreenshot) {
140
150
  try {
141
- const dataUrl = await getImageDataUrlForCell(cellId, false);
151
+ const dataUrl = await getImageDataUrlForCell(cellId, snappy);
142
152
  if (!dataUrl) {
143
153
  Logger.error(`Failed to capture screenshot for cell ${cellId}`);
144
154
  continue;
@@ -159,13 +169,12 @@ export function useEnrichCellOutputs() {
159
169
  * Utility function to take screenshots of cells with HTML outputs and update the cell outputs.
160
170
  */
161
171
  export async function updateCellOutputsWithScreenshots(opts: {
162
- progress: ProgressState;
163
- takeScreenshots: (progress: ProgressState) => Promise<ScreenshotResults>;
172
+ takeScreenshots: () => Promise<ScreenshotResults>;
164
173
  updateCellOutputs: (request: UpdateCellOutputsRequest) => Promise<null>;
165
174
  }) {
166
- const { progress, takeScreenshots, updateCellOutputs } = opts;
175
+ const { takeScreenshots, updateCellOutputs } = opts;
167
176
  try {
168
- const cellIdsToOutput = await takeScreenshots(progress);
177
+ const cellIdsToOutput = await takeScreenshots();
169
178
  if (Objects.size(cellIdsToOutput) > 0) {
170
179
  await updateCellOutputs({ cellIdsToOutput });
171
180
  }
@@ -132,7 +132,7 @@ const LoadingDataEditor = (props: Props) => {
132
132
  columnFields={columnFields}
133
133
  setColumnFields={setColumnFields}
134
134
  editableColumns={props.editableColumns}
135
- edits={props.edits.edits} // TODO: This is returning old edits upon refresh
135
+ edits={props.edits.edits}
136
136
  onAddEdits={(edits) => {
137
137
  props.onEdits((v) => ({ ...v, edits: [...v.edits, ...edits] }));
138
138
  }}
@@ -14,7 +14,13 @@ import DataEditor, {
14
14
  type Rectangle,
15
15
  } from "@glideapps/glide-data-grid";
16
16
  import { CopyIcon, TrashIcon } from "lucide-react";
17
- import React, { useCallback, useMemo, useRef, useState } from "react";
17
+ import React, {
18
+ useCallback,
19
+ useEffect,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ } from "react";
18
24
  import useEvent from "react-use-event-hook";
19
25
  import type { FieldTypes } from "@/components/data-table/types";
20
26
  import {
@@ -40,7 +46,6 @@ import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
40
46
  import { Button } from "@/components/ui/button";
41
47
  import { toast } from "@/components/ui/use-toast";
42
48
  import type { DataType } from "@/core/kernel/messages";
43
- import { useOnMount } from "@/hooks/useLifecycle";
44
49
  import { useNonce } from "@/hooks/useNonce";
45
50
  import { logNever } from "@/utils/assertNever";
46
51
  import { Events } from "@/utils/events";
@@ -93,9 +98,47 @@ export const GlideDataEditor = <T,>({
93
98
 
94
99
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
95
100
  const rerender = useNonce();
101
+ const hasAppliedEdits = useRef(false);
96
102
 
97
- // Handle initial edits passed in
98
- useOnMount(() => {
103
+ const columns: ModifiedGridColumn[] = useMemo(() => {
104
+ const columns: ModifiedGridColumn[] = [];
105
+ for (const [columnName, fieldType] of Object.entries(columnFields)) {
106
+ const editable =
107
+ editableColumns === "all" || editableColumns.includes(columnName);
108
+
109
+ columns.push({
110
+ id: columnName,
111
+ title: columnName,
112
+ width: columnWidths[columnName], // Enables resizing
113
+ icon: editable
114
+ ? getColumnHeaderIcon(fieldType)
115
+ : GridColumnIcon.ProtectedColumnOverlay,
116
+ style: "normal",
117
+ kind: getColumnKind(fieldType),
118
+ dataType: fieldType,
119
+ hasMenu: true,
120
+ themeOverride: editable
121
+ ? undefined
122
+ : {
123
+ bgCell: theme === "light" ? "#F9F9FA" : "#1e1e21",
124
+ },
125
+ });
126
+ }
127
+
128
+ return columns;
129
+ }, [columnFields, columnWidths, editableColumns, theme]);
130
+
131
+ // Apply initial edits after data has loaded
132
+ useEffect(() => {
133
+ // Don't apply if already applied or data hasn't loaded yet
134
+ if (hasAppliedEdits.current || data.length === 0) {
135
+ return;
136
+ }
137
+
138
+ // Mark as applied once data loads - prevents re-applying user edits
139
+ hasAppliedEdits.current = true;
140
+
141
+ // No initial edits to apply
99
142
  if (edits.length === 0) {
100
143
  return;
101
144
  }
@@ -184,35 +227,8 @@ export const GlideDataEditor = <T,>({
184
227
 
185
228
  // Force re-render to update the total rows
186
229
  rerender();
187
- });
188
-
189
- const columns: ModifiedGridColumn[] = useMemo(() => {
190
- const columns: ModifiedGridColumn[] = [];
191
- for (const [columnName, fieldType] of Object.entries(columnFields)) {
192
- const editable =
193
- editableColumns === "all" || editableColumns.includes(columnName);
194
-
195
- columns.push({
196
- id: columnName,
197
- title: columnName,
198
- width: columnWidths[columnName], // Enables resizing
199
- icon: editable
200
- ? getColumnHeaderIcon(fieldType)
201
- : GridColumnIcon.ProtectedColumnOverlay,
202
- style: "normal",
203
- kind: getColumnKind(fieldType),
204
- dataType: fieldType,
205
- hasMenu: true,
206
- themeOverride: editable
207
- ? undefined
208
- : {
209
- bgCell: theme === "light" ? "#F9F9FA" : "#1e1e21",
210
- },
211
- });
212
- }
213
-
214
- return columns;
215
- }, [columnFields, columnWidths, editableColumns, theme]);
230
+ // eslint-disable-next-line react-hooks/exhaustive-deps
231
+ }, [data.length]);
216
232
 
217
233
  const getCellContent = useCallback(
218
234
  (cell: Item): GridCell => {
@@ -166,7 +166,7 @@ describe("getImageDataUrlForCell", () => {
166
166
  );
167
167
  });
168
168
 
169
- it("should add printing classes before capture when enablePrintMode is true", async () => {
169
+ it("should add printing classes before capture when snappy is false", async () => {
170
170
  vi.mocked(toPng).mockImplementation(async () => {
171
171
  // Check classes are applied during capture
172
172
  expect(mockElement.classList.contains("printing-output")).toBe(true);
@@ -175,36 +175,36 @@ describe("getImageDataUrlForCell", () => {
175
175
  return mockDataUrl;
176
176
  });
177
177
 
178
- await getImageDataUrlForCell("cell-1" as CellId, true);
178
+ await getImageDataUrlForCell("cell-1" as CellId, false);
179
179
  });
180
180
 
181
- it("should remove printing classes after capture when enablePrintMode is true", async () => {
181
+ it("should remove printing classes after capture when snappy is false", async () => {
182
182
  vi.mocked(toPng).mockResolvedValue(mockDataUrl);
183
183
 
184
- await getImageDataUrlForCell("cell-1" as CellId, true);
184
+ await getImageDataUrlForCell("cell-1" as CellId, false);
185
185
 
186
186
  expect(mockElement.classList.contains("printing-output")).toBe(false);
187
187
  expect(document.body.classList.contains("printing")).toBe(false);
188
188
  });
189
189
 
190
- it("should add printing-output but NOT body.printing when enablePrintMode is false", async () => {
190
+ it("should add printing-output but NOT body.printing when snappy is true", async () => {
191
191
  vi.mocked(toPng).mockImplementation(async () => {
192
192
  // printing-output should still be added to the element
193
193
  expect(mockElement.classList.contains("printing-output")).toBe(true);
194
- // but body.printing should NOT be added
194
+ // but body.printing should NOT be added when snappy mode is on
195
195
  expect(document.body.classList.contains("printing")).toBe(false);
196
196
  expect(mockElement.style.overflow).toBe("auto");
197
197
  return mockDataUrl;
198
198
  });
199
199
 
200
- await getImageDataUrlForCell("cell-1" as CellId, false);
200
+ await getImageDataUrlForCell("cell-1" as CellId, true);
201
201
  });
202
202
 
203
- it("should cleanup printing-output when enablePrintMode is false", async () => {
203
+ it("should cleanup printing-output when snappy is true", async () => {
204
204
  mockElement.style.overflow = "hidden";
205
205
  vi.mocked(toPng).mockResolvedValue(mockDataUrl);
206
206
 
207
- await getImageDataUrlForCell("cell-1" as CellId, false);
207
+ await getImageDataUrlForCell("cell-1" as CellId, true);
208
208
 
209
209
  expect(mockElement.classList.contains("printing-output")).toBe(false);
210
210
  expect(document.body.classList.contains("printing")).toBe(false);
@@ -239,7 +239,7 @@ describe("getImageDataUrlForCell", () => {
239
239
  expect(mockElement.style.overflow).toBe("scroll");
240
240
  });
241
241
 
242
- it("should maintain body.printing during concurrent captures when enablePrintMode is true", async () => {
242
+ it("should maintain body.printing during concurrent captures when snappy is false", async () => {
243
243
  // Create a second element
244
244
  const mockElement2 = document.createElement("div");
245
245
  mockElement2.id = CellOutputId.create("cell-2" as CellId);
@@ -273,9 +273,9 @@ describe("getImageDataUrlForCell", () => {
273
273
  return mockDataUrl;
274
274
  });
275
275
 
276
- // Start both captures concurrently with enablePrintMode = true
277
- const capture1 = getImageDataUrlForCell("cell-1" as CellId, true);
278
- const capture2 = getImageDataUrlForCell("cell-2" as CellId, true);
276
+ // Start both captures concurrently with snappy = false (body.printing should be added)
277
+ const capture1 = getImageDataUrlForCell("cell-1" as CellId, false);
278
+ const capture2 = getImageDataUrlForCell("cell-2" as CellId, false);
279
279
 
280
280
  // Let second capture complete first
281
281
  resolveSecond!();
@@ -297,21 +297,21 @@ describe("getImageDataUrlForCell", () => {
297
297
  mockElement2.remove();
298
298
  });
299
299
 
300
- it("should not interfere with body.printing during concurrent captures when enablePrintMode is false", async () => {
300
+ it("should not interfere with body.printing during concurrent captures when snappy is true", async () => {
301
301
  // Create a second element
302
302
  const mockElement2 = document.createElement("div");
303
303
  mockElement2.id = CellOutputId.create("cell-2" as CellId);
304
304
  document.body.append(mockElement2);
305
305
 
306
306
  vi.mocked(toPng).mockImplementation(async () => {
307
- // body.printing should never be added when enablePrintMode is false
307
+ // body.printing should never be added when snappy is true
308
308
  expect(document.body.classList.contains("printing")).toBe(false);
309
309
  return mockDataUrl;
310
310
  });
311
311
 
312
- // Start both captures concurrently with enablePrintMode = false
313
- const capture1 = getImageDataUrlForCell("cell-1" as CellId, false);
314
- const capture2 = getImageDataUrlForCell("cell-2" as CellId, false);
312
+ // Start both captures concurrently with snappy = true (body.printing should NOT be added)
313
+ const capture1 = getImageDataUrlForCell("cell-1" as CellId, true);
314
+ const capture2 = getImageDataUrlForCell("cell-2" as CellId, true);
315
315
 
316
316
  await Promise.all([capture1, capture2]);
317
317
 
@@ -70,16 +70,15 @@ function releaseBodyPrinting() {
70
70
  * Prepare a cell element for screenshot capture.
71
71
  *
72
72
  * @param element - The cell output element to prepare
73
- * @param enablePrintMode - When true, adds a 'printing' class to the body.
74
- * This can cause layout shifts that cause the page to scroll.
73
+ * @param snappy - When true, avoids layout shifts and speeds up the capture.
75
74
  * @returns A cleanup function to restore the element's original state
76
75
  */
77
76
  function prepareCellElementForScreenshot(
78
77
  element: HTMLElement,
79
- enablePrintMode: boolean,
78
+ snappy: boolean,
80
79
  ) {
81
80
  element.classList.add("printing-output");
82
- if (enablePrintMode) {
81
+ if (!snappy) {
83
82
  acquireBodyPrinting();
84
83
  }
85
84
  const originalOverflow = element.style.overflow;
@@ -87,7 +86,7 @@ function prepareCellElementForScreenshot(
87
86
 
88
87
  return () => {
89
88
  element.classList.remove("printing-output");
90
- if (enablePrintMode) {
89
+ if (!snappy) {
91
90
  releaseBodyPrinting();
92
91
  }
93
92
  element.style.overflow = originalOverflow;
@@ -100,13 +99,12 @@ const THRESHOLD_TIME_MS = 500;
100
99
  * Capture a cell output as a PNG data URL.
101
100
  *
102
101
  * @param cellId - The ID of the cell to capture
103
- * @param enablePrintMode - When true, enables print mode which adds a 'printing' class to the body.
104
- * This can cause layout shifts that cause the page to scroll.
102
+ * @param snappy - When true, uses a faster method to capture the image. Avoids layout shifts.
105
103
  * @returns The PNG as a data URL, or undefined if the cell element wasn't found
106
104
  */
107
105
  export async function getImageDataUrlForCell(
108
106
  cellId: CellId,
109
- enablePrintMode = true,
107
+ snappy = false,
110
108
  ): Promise<string | undefined> {
111
109
  const element = findElementForCell(cellId);
112
110
  if (!element) {
@@ -118,11 +116,11 @@ export async function getImageDataUrlForCell(
118
116
  return iframeDataUrl;
119
117
  }
120
118
 
121
- const cleanup = prepareCellElementForScreenshot(element, enablePrintMode);
119
+ const cleanup = prepareCellElementForScreenshot(element, snappy);
122
120
 
123
121
  try {
124
122
  const startTime = Date.now();
125
- const dataUrl = await toPng(element);
123
+ const dataUrl = await toPng(element, undefined, snappy);
126
124
  const timeTaken = Date.now() - startTime;
127
125
  if (timeTaken > THRESHOLD_TIME_MS) {
128
126
  Logger.debug(
@@ -159,7 +157,7 @@ export async function downloadCellOutputAsImage(
159
157
  await downloadHTMLAsImage({
160
158
  element,
161
159
  filename,
162
- prepare: () => prepareCellElementForScreenshot(element, true),
160
+ prepare: () => prepareCellElementForScreenshot(element, false),
163
161
  });
164
162
  }
165
163