@marimo-team/islands 0.19.7-dev24 → 0.19.7-dev26

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 (35) hide show
  1. package/dist/{Combination-BOmAhdTT.js → Combination-Bg-xN8JV.js} +1 -1
  2. package/dist/{ConnectedDataExplorerComponent-BW-EEUju.js → ConnectedDataExplorerComponent-CukLYFVV.js} +9 -9
  3. package/dist/{any-language-editor-jlqKouk6.js → any-language-editor-DZc6NCTp.js} +4 -4
  4. package/dist/{button-D6ZIdUA3.js → button-BWvsJ2Wr.js} +1 -1
  5. package/dist/{check-S8ldILuD.js → check-CM_kewwn.js} +1 -1
  6. package/dist/{copy-ACOJ1BXr.js → copy-B59Bw3-w.js} +2 -2
  7. package/dist/{error-banner-uJ4xh94e.js → error-banner-C7KLpECd.js} +3 -3
  8. package/dist/{esm-DNwkt4aO.js → esm-D4WO8J3G.js} +4 -4
  9. package/dist/{glide-data-editor-QbwryjAp.js → glide-data-editor-uGGDZv6S.js} +7 -7
  10. package/dist/{hotkeys-C4e3s3sJ.js → hotkeys-B5WnGZXF.js} +3 -0
  11. package/dist/{label-o68OaAQk.js → label-DC2pbeUJ.js} +4 -4
  12. package/dist/{loader-WBQoIunJ.js → loader-B0KEFFi-.js} +1 -1
  13. package/dist/main.js +522 -519
  14. package/dist/{mermaid-CDdsQEyr.js → mermaid-Bqp2Xw99.js} +3 -3
  15. package/dist/{slides-component-BGEYjTca.js → slides-component-BVjvNo92.js} +2 -2
  16. package/dist/{spec-CuHgWmcK.js → spec-C9rnT0AN.js} +5 -5
  17. package/dist/style.css +1 -1
  18. package/dist/{types-BIvEidLq.js → types-ZLLMdAtn.js} +6 -6
  19. package/dist/{useAsyncData-DFBEii2l.js → useAsyncData-BjNwqCfS.js} +1 -1
  20. package/dist/{useDeepCompareMemoize-HKwezUBx.js → useDeepCompareMemoize-CfoxVor3.js} +5 -5
  21. package/dist/{useIframeCapabilities-wWXCXV88.js → useIframeCapabilities-BBO_R0ww.js} +1 -1
  22. package/dist/{useTheme-D0zT6HZe.js → useTheme-BYG2SH8J.js} +1 -1
  23. package/dist/{vega-component-2gqZksJH.js → vega-component-rDX7xwxH.js} +8 -8
  24. package/package.json +1 -1
  25. package/src/components/editor/actions/useNotebookActions.tsx +5 -3
  26. package/src/components/ui/progress.tsx +22 -5
  27. package/src/core/export/__tests__/hooks.test.ts +42 -19
  28. package/src/core/export/hooks.ts +33 -32
  29. package/src/utils/__tests__/download.test.tsx +6 -4
  30. package/src/utils/__tests__/objects.test.ts +263 -0
  31. package/src/utils/__tests__/progress.test.ts +156 -0
  32. package/src/utils/download.ts +7 -2
  33. package/src/utils/objects.ts +3 -0
  34. package/src/utils/progress.ts +61 -0
  35. package/src/utils/toast-progress.tsx +41 -0
@@ -5,15 +5,15 @@ var _a;
5
5
  import { s as __toESM } from "./chunk-BNovOVIE.js";
6
6
  import { t as require_react } from "./react-DdA8EBol.js";
7
7
  import { t as require_compiler_runtime } from "./compiler-runtime-DHFVbq0b.js";
8
- import { D as dispatchDiscreteCustomEvent, E as Primitive, M as composeEventHandlers, N as createLucideIcon, O as createSlot, S as useId, T as useCallbackRef, _ as Arrow, b as createPopperScope, c as withSmartCollisionBoundary, g as Anchor, h as Portal, i as useFocusGuards, j as createContextScope, m as Presence, n as hideOthers, o as MAX_HEIGHT_OFFSET, p as useControllableState, r as FocusScope, s as withFullScreenAsRoot, t as Combination_default, u as StyleNamespace, v as Content, w as DismissableLayer, y as Root2$1 } from "./Combination-BOmAhdTT.js";
9
- import { t as Check } from "./check-S8ldILuD.js";
10
- import { B as ChevronDown, C as useDirection, L as $b5e257d569688ac6$export$619500959fc48b26, M as $a916eb452884faea$export$b7a616150fdb9f44, N as $488c6ddbf4ef74c2$export$cc77c4ff7e8673c5, P as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, R as X, _ as menuItemVariants, b as menuSubTriggerVariants, g as menuControlVariants, h as menuControlCheckVariants, j as upperFirst_default, m as menuContentCommon, v as menuLabelVariants, w as createCollection, y as menuSeparatorVariants, z as ChevronUp } from "./label-o68OaAQk.js";
8
+ import { D as dispatchDiscreteCustomEvent, E as Primitive, M as composeEventHandlers, N as createLucideIcon, O as createSlot, S as useId, T as useCallbackRef, _ as Arrow, b as createPopperScope, c as withSmartCollisionBoundary, g as Anchor, h as Portal, i as useFocusGuards, j as createContextScope, m as Presence, n as hideOthers, o as MAX_HEIGHT_OFFSET, p as useControllableState, r as FocusScope, s as withFullScreenAsRoot, t as Combination_default, u as StyleNamespace, v as Content, w as DismissableLayer, y as Root2$1 } from "./Combination-Bg-xN8JV.js";
9
+ import { t as Check } from "./check-CM_kewwn.js";
10
+ import { B as ChevronDown, C as useDirection, L as $b5e257d569688ac6$export$619500959fc48b26, M as $a916eb452884faea$export$b7a616150fdb9f44, N as $488c6ddbf4ef74c2$export$cc77c4ff7e8673c5, P as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, R as X, _ as menuItemVariants, b as menuSubTriggerVariants, g as menuControlVariants, h as menuControlCheckVariants, j as upperFirst_default, m as menuContentCommon, v as menuLabelVariants, w as createCollection, y as menuSeparatorVariants, z as ChevronUp } from "./label-DC2pbeUJ.js";
11
11
  import { n as clsx_default } from "./clsx-D2KVTYnW.js";
12
- import { c as useComposedRefs, l as Events, s as composeRefs, t as Button, u as cn } from "./button-D6ZIdUA3.js";
13
- import { s as Logger } from "./hotkeys-C4e3s3sJ.js";
12
+ import { c as useComposedRefs, l as Events, s as composeRefs, t as Button, u as cn } from "./button-BWvsJ2Wr.js";
13
+ import { s as Logger } from "./hotkeys-B5WnGZXF.js";
14
14
  import { t as require_jsx_runtime } from "./jsx-runtime-CTBg5pdT.js";
15
15
  import { t as require_react_dom } from "./react-dom-DJW8xUDg.js";
16
- import { h as useEvent_default } from "./useTheme-D0zT6HZe.js";
16
+ import { h as useEvent_default } from "./useTheme-BYG2SH8J.js";
17
17
  import { t as toString_default } from "./toString-C4TLO6FA.js";
18
18
  import { i as debounce_default, n as Constants } from "./constants-DrOu5vvd.js";
19
19
  import { t as memoizeLastValue } from "./once-BqS42WgZ.js";
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DdA8EBol.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-DHFVbq0b.js";
4
- import { h as useEvent_default } from "./useTheme-D0zT6HZe.js";
4
+ import { h as useEvent_default } from "./useTheme-BYG2SH8J.js";
5
5
  import { t as invariant } from "./invariant-D9QLJ4SZ.js";
6
6
  var import_compiler_runtime = require_compiler_runtime(), import_react = /* @__PURE__ */ __toESM(require_react(), 1), Result = {
7
7
  error(e, s) {
@@ -4,13 +4,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4
4
  import { s as __toESM, t as __commonJSMin } from "./chunk-BNovOVIE.js";
5
5
  import { t as require_react } from "./react-DdA8EBol.js";
6
6
  import { t as require_compiler_runtime } from "./compiler-runtime-DHFVbq0b.js";
7
- import { N as createLucideIcon } from "./Combination-BOmAhdTT.js";
8
- import { a as cva, u as cn } from "./button-D6ZIdUA3.js";
9
- import { s as Logger } from "./hotkeys-C4e3s3sJ.js";
7
+ import { N as createLucideIcon } from "./Combination-Bg-xN8JV.js";
8
+ import { a as cva, u as cn } from "./button-BWvsJ2Wr.js";
9
+ import { s as Logger } from "./hotkeys-B5WnGZXF.js";
10
10
  import { t as require_jsx_runtime } from "./jsx-runtime-CTBg5pdT.js";
11
- import { f as waitFor, p as isIslands, u as store, x as atom } from "./useTheme-D0zT6HZe.js";
11
+ import { f as waitFor, p as isIslands, u as store, x as atom } from "./useTheme-BYG2SH8J.js";
12
12
  import { r as KnownQueryParams } from "./constants-DrOu5vvd.js";
13
- import { i as tableFromIPC } from "./loader-WBQoIunJ.js";
13
+ import { i as tableFromIPC } from "./loader-B0KEFFi-.js";
14
14
  var CircleQuestionMark = createLucideIcon("circle-question-mark", [
15
15
  ["circle", {
16
16
  cx: "12",
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DdA8EBol.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-DHFVbq0b.js";
4
- import { s as Logger } from "./hotkeys-C4e3s3sJ.js";
4
+ import { s as Logger } from "./hotkeys-B5WnGZXF.js";
5
5
  import { n as once } from "./once-BqS42WgZ.js";
6
6
  function testStorage(e) {
7
7
  try {
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DdA8EBol.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-DHFVbq0b.js";
4
- import { a as resolvePlatform, n as OverridingHotkeyProvider, s as Logger } from "./hotkeys-C4e3s3sJ.js";
4
+ import { a as resolvePlatform, n as OverridingHotkeyProvider, s as Logger } from "./hotkeys-B5WnGZXF.js";
5
5
  import { t as _baseIsEqual_default } from "./_baseIsEqual-CBSjxu-D.js";
6
6
  import { t as merge_default } from "./merge-Dl1bfxsj.js";
7
7
  import { A as looseObject, B as union, I as record, N as number, P as object, R as string, T as boolean, b as _enum, w as array } from "./zod-DITCj31F.js";
@@ -1,19 +1,19 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DdA8EBol.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-DHFVbq0b.js";
4
- import "./Combination-BOmAhdTT.js";
5
- import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-HKwezUBx.js";
6
- import { l as Events } from "./button-D6ZIdUA3.js";
7
- import { o as Objects, s as Logger } from "./hotkeys-C4e3s3sJ.js";
4
+ import "./Combination-Bg-xN8JV.js";
5
+ import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-CfoxVor3.js";
6
+ import { l as Events } from "./button-BWvsJ2Wr.js";
7
+ import { o as Objects, s as Logger } from "./hotkeys-B5WnGZXF.js";
8
8
  import { t as require_jsx_runtime } from "./jsx-runtime-CTBg5pdT.js";
9
9
  import "./react-dom-DJW8xUDg.js";
10
- import { l as Tooltip, n as ErrorBanner } from "./error-banner-uJ4xh94e.js";
11
- import { h as useEvent_default, n as useTheme } from "./useTheme-D0zT6HZe.js";
10
+ import { l as Tooltip, n as ErrorBanner } from "./error-banner-C7KLpECd.js";
11
+ import { h as useEvent_default, n as useTheme } from "./useTheme-BYG2SH8J.js";
12
12
  import { t as _baseUniq_default } from "./_baseUniq-4lqa8rDi.js";
13
13
  import { i as debounce_default } from "./constants-DrOu5vvd.js";
14
- import { a as tooltipHandler, n as vegaLoadData } from "./loader-WBQoIunJ.js";
14
+ import { a as tooltipHandler, n as vegaLoadData } from "./loader-B0KEFFi-.js";
15
15
  import { n as formats } from "./vega-loader.browser-BJ1uJidF.js";
16
- import { t as useAsyncData } from "./useAsyncData-DFBEii2l.js";
16
+ import { t as useAsyncData } from "./useAsyncData-BjNwqCfS.js";
17
17
  import { t as j } from "./react-vega-CjiPWyw0.js";
18
18
  import "./defaultLocale-qS7DaAmi.js";
19
19
  import "./defaultLocale-Bxoo2-30.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.19.7-dev24",
3
+ "version": "0.19.7-dev26",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -77,6 +77,7 @@ import {
77
77
  } from "@/utils/download";
78
78
  import { Filenames } from "@/utils/filenames";
79
79
  import { Objects } from "@/utils/objects";
80
+ import type { ProgressState } from "@/utils/progress";
80
81
  import { newNotebookURL } from "@/utils/urls";
81
82
  import { useRunAllCells } from "../cell/useRunCells";
82
83
  import { useChromeActions, useChromeState } from "../chrome/state";
@@ -238,11 +239,12 @@ export function useNotebookActions() {
238
239
  return;
239
240
  }
240
241
 
241
- const downloadPDF = async () => {
242
- await updateCellOutputsWithScreenshots(
242
+ const downloadPDF = async (progress: ProgressState) => {
243
+ await updateCellOutputsWithScreenshots({
244
+ progress,
243
245
  takeScreenshots,
244
246
  updateCellOutputs,
245
- );
247
+ });
246
248
  await downloadAsPDF({
247
249
  filename: filename,
248
250
  webpdf: false,
@@ -5,10 +5,18 @@ import * as React from "react";
5
5
 
6
6
  import { cn } from "@/utils/cn";
7
7
 
8
+ interface ProgressProps
9
+ extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
10
+ /**
11
+ * When true, shows an indeterminate animated progress bar.
12
+ */
13
+ indeterminate?: boolean;
14
+ }
15
+
8
16
  const Progress = React.forwardRef<
9
- React.ElementRef<typeof ProgressPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
11
- >(({ className, value, ...props }, ref) => (
17
+ React.ComponentRef<typeof ProgressPrimitive.Root>,
18
+ ProgressProps
19
+ >(({ className, value, indeterminate, ...props }, ref) => (
12
20
  <ProgressPrimitive.Root
13
21
  ref={ref}
14
22
  className={cn(
@@ -18,8 +26,17 @@ const Progress = React.forwardRef<
18
26
  {...props}
19
27
  >
20
28
  <ProgressPrimitive.Indicator
21
- className="h-full w-full flex-1 bg-primary transition-all pulse"
22
- style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
29
+ className={cn(
30
+ "h-full flex-1 bg-primary",
31
+ indeterminate
32
+ ? "w-1/3 animate-progress-indeterminate"
33
+ : "w-full transition-transform duration-300 ease-out",
34
+ )}
35
+ style={
36
+ indeterminate
37
+ ? undefined
38
+ : { transform: `translateX(-${100 - (value || 0)}%)` }
39
+ }
23
40
  />
24
41
  </ProgressPrimitive.Root>
25
42
  ));
@@ -8,6 +8,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
8
8
  import type { CellId } from "@/core/cells/ids";
9
9
  import { CellOutputId } from "@/core/cells/ids";
10
10
  import type { CellRuntimeState } from "@/core/cells/types";
11
+ import { ProgressState } from "@/utils/progress";
11
12
  import {
12
13
  updateCellOutputsWithScreenshots,
13
14
  useEnrichCellOutputs,
@@ -38,6 +39,8 @@ vi.mock("@/core/cells/cells", async () => {
38
39
  };
39
40
  });
40
41
 
42
+ const progress = ProgressState.indeterminate();
43
+
41
44
  import { toPng } from "html-to-image";
42
45
  import { toast } from "@/components/ui/use-toast";
43
46
  import { cellsRuntimeAtom } from "@/core/cells/cells";
@@ -100,7 +103,7 @@ describe("useEnrichCellOutputs", () => {
100
103
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
101
104
 
102
105
  const enrichCellOutputs = result.current;
103
- const output = await enrichCellOutputs();
106
+ const output = await enrichCellOutputs(progress);
104
107
 
105
108
  expect(output).toEqual({});
106
109
  expect(document.getElementById).not.toHaveBeenCalled();
@@ -132,7 +135,7 @@ describe("useEnrichCellOutputs", () => {
132
135
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
133
136
 
134
137
  const enrichCellOutputs = result.current;
135
- const output = await enrichCellOutputs();
138
+ const output = await enrichCellOutputs(progress);
136
139
 
137
140
  expect(document.getElementById).toHaveBeenCalledWith(
138
141
  CellOutputId.create(cellId),
@@ -177,7 +180,7 @@ describe("useEnrichCellOutputs", () => {
177
180
 
178
181
  // First call - should capture
179
182
  let enrichCellOutputs = result.current;
180
- let output = await enrichCellOutputs();
183
+ let output = await enrichCellOutputs(progress);
181
184
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl] });
182
185
  expect(toPng).toHaveBeenCalledTimes(1);
183
186
 
@@ -186,7 +189,7 @@ describe("useEnrichCellOutputs", () => {
186
189
 
187
190
  // Second call with same output - should not capture again
188
191
  enrichCellOutputs = result.current;
189
- output = await enrichCellOutputs();
192
+ output = await enrichCellOutputs(progress);
190
193
  expect(output).toEqual({}); // Empty because output hasn't changed
191
194
  expect(toPng).toHaveBeenCalledTimes(1); // Still only 1 call
192
195
  });
@@ -215,7 +218,7 @@ describe("useEnrichCellOutputs", () => {
215
218
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
216
219
 
217
220
  const enrichCellOutputs = result.current;
218
- const output = await enrichCellOutputs();
221
+ const output = await enrichCellOutputs(progress);
219
222
 
220
223
  expect(output).toEqual({}); // Failed screenshot should be filtered out
221
224
  expect(Logger.error).toHaveBeenCalledWith(
@@ -245,7 +248,7 @@ describe("useEnrichCellOutputs", () => {
245
248
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
246
249
 
247
250
  const enrichCellOutputs = result.current;
248
- const output = await enrichCellOutputs();
251
+ const output = await enrichCellOutputs(progress);
249
252
 
250
253
  expect(output).toEqual({});
251
254
  expect(Logger.error).toHaveBeenCalledWith(
@@ -294,7 +297,7 @@ describe("useEnrichCellOutputs", () => {
294
297
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
295
298
 
296
299
  const enrichCellOutputs = result.current;
297
- const output = await enrichCellOutputs();
300
+ const output = await enrichCellOutputs(progress);
298
301
 
299
302
  expect(output).toEqual({
300
303
  [cell1]: ["image/png", mockDataUrl1],
@@ -340,7 +343,7 @@ describe("useEnrichCellOutputs", () => {
340
343
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
341
344
 
342
345
  const enrichCellOutputs = result.current;
343
- const output = await enrichCellOutputs();
346
+ const output = await enrichCellOutputs(progress);
344
347
 
345
348
  // Only the successful screenshot should be in the result
346
349
  expect(output).toEqual({
@@ -382,13 +385,13 @@ describe("useEnrichCellOutputs", () => {
382
385
 
383
386
  // First screenshot
384
387
  let enrichCellOutputs = result.current;
385
- let output = await enrichCellOutputs();
388
+ let output = await enrichCellOutputs(progress);
386
389
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl1] });
387
390
 
388
391
  // Second call - same output, should not be captured
389
392
  rerender();
390
393
  enrichCellOutputs = result.current;
391
- output = await enrichCellOutputs();
394
+ output = await enrichCellOutputs(progress);
392
395
  expect(output).toEqual({});
393
396
 
394
397
  // Third call - output changed, should be captured
@@ -407,7 +410,7 @@ describe("useEnrichCellOutputs", () => {
407
410
 
408
411
  rerender();
409
412
  enrichCellOutputs = result.current;
410
- output = await enrichCellOutputs();
413
+ output = await enrichCellOutputs(progress);
411
414
  expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl2] });
412
415
  expect(toPng).toHaveBeenCalledTimes(2);
413
416
  });
@@ -447,7 +450,7 @@ describe("useEnrichCellOutputs", () => {
447
450
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
448
451
 
449
452
  const enrichCellOutputs = result.current;
450
- const output = await enrichCellOutputs();
453
+ const output = await enrichCellOutputs(progress);
451
454
 
452
455
  // None of these should trigger screenshots
453
456
  expect(output).toEqual({});
@@ -472,7 +475,7 @@ describe("useEnrichCellOutputs", () => {
472
475
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
473
476
 
474
477
  const enrichCellOutputs = result.current;
475
- const output = await enrichCellOutputs();
478
+ const output = await enrichCellOutputs(progress);
476
479
 
477
480
  expect(output).toEqual({});
478
481
  expect(document.getElementById).not.toHaveBeenCalled();
@@ -504,7 +507,7 @@ describe("useEnrichCellOutputs", () => {
504
507
  const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
505
508
 
506
509
  const enrichCellOutputs = result.current;
507
- const output = await enrichCellOutputs();
510
+ const output = await enrichCellOutputs(progress);
508
511
 
509
512
  // Verify the exact return type structure
510
513
  expect(output).toHaveProperty(cellId);
@@ -535,7 +538,11 @@ describe("updateCellOutputsWithScreenshots", () => {
535
538
  const takeScreenshots = vi.fn().mockResolvedValue(mockScreenshots);
536
539
  const updateCellOutputs = vi.fn().mockResolvedValue(null);
537
540
 
538
- await updateCellOutputsWithScreenshots(takeScreenshots, updateCellOutputs);
541
+ await updateCellOutputsWithScreenshots({
542
+ progress,
543
+ takeScreenshots,
544
+ updateCellOutputs,
545
+ });
539
546
 
540
547
  expect(takeScreenshots).toHaveBeenCalledTimes(1);
541
548
  expect(updateCellOutputs).toHaveBeenCalledTimes(1);
@@ -548,7 +555,11 @@ describe("updateCellOutputsWithScreenshots", () => {
548
555
  const takeScreenshots = vi.fn().mockResolvedValue({});
549
556
  const updateCellOutputs = vi.fn().mockResolvedValue(null);
550
557
 
551
- await updateCellOutputsWithScreenshots(takeScreenshots, updateCellOutputs);
558
+ await updateCellOutputsWithScreenshots({
559
+ progress,
560
+ takeScreenshots,
561
+ updateCellOutputs,
562
+ });
552
563
 
553
564
  expect(takeScreenshots).toHaveBeenCalledTimes(1);
554
565
  expect(updateCellOutputs).not.toHaveBeenCalled();
@@ -571,7 +582,11 @@ describe("updateCellOutputsWithScreenshots", () => {
571
582
  const takeScreenshots = vi.fn().mockResolvedValue(mockScreenshots);
572
583
  const updateCellOutputs = vi.fn().mockResolvedValue(null);
573
584
 
574
- await updateCellOutputsWithScreenshots(takeScreenshots, updateCellOutputs);
585
+ await updateCellOutputsWithScreenshots({
586
+ progress,
587
+ takeScreenshots,
588
+ updateCellOutputs,
589
+ });
575
590
 
576
591
  expect(updateCellOutputs).toHaveBeenCalledWith({
577
592
  cellIdsToOutput: mockScreenshots,
@@ -584,7 +599,11 @@ describe("updateCellOutputsWithScreenshots", () => {
584
599
  const updateCellOutputs = vi.fn().mockResolvedValue(null);
585
600
 
586
601
  // Should not throw - errors are caught and shown via toast
587
- await updateCellOutputsWithScreenshots(takeScreenshots, updateCellOutputs);
602
+ await updateCellOutputsWithScreenshots({
603
+ progress,
604
+ takeScreenshots,
605
+ updateCellOutputs,
606
+ });
588
607
 
589
608
  expect(updateCellOutputs).not.toHaveBeenCalled();
590
609
  expect(Logger.error).toHaveBeenCalledWith(
@@ -613,7 +632,11 @@ describe("updateCellOutputsWithScreenshots", () => {
613
632
  const updateCellOutputs = vi.fn().mockRejectedValue(error);
614
633
 
615
634
  // Should not throw - errors are caught and shown via toast
616
- await updateCellOutputsWithScreenshots(takeScreenshots, updateCellOutputs);
635
+ await updateCellOutputsWithScreenshots({
636
+ progress,
637
+ takeScreenshots,
638
+ updateCellOutputs,
639
+ });
617
640
 
618
641
  expect(Logger.error).toHaveBeenCalledWith(
619
642
  "Error updating cell outputs with screenshots:",
@@ -7,6 +7,7 @@ import { useInterval } from "@/hooks/useInterval";
7
7
  import { getImageDataUrlForCell } from "@/utils/download";
8
8
  import { Logger } from "@/utils/Logger";
9
9
  import { Objects } from "@/utils/objects";
10
+ import { ProgressState } from "@/utils/progress";
10
11
  import { cellsRuntimeAtom } from "../cells/cells";
11
12
  import type { CellId } from "../cells/ids";
12
13
  import { connectionAtom } from "../network/connection";
@@ -63,10 +64,11 @@ export function useAutoExport() {
63
64
 
64
65
  useInterval(
65
66
  async () => {
66
- await updateCellOutputsWithScreenshots(
67
+ await updateCellOutputsWithScreenshots({
68
+ progress: ProgressState.indeterminate(),
67
69
  takeScreenshots,
68
70
  updateCellOutputs,
69
- );
71
+ });
70
72
  await autoExportAsIPYNB({
71
73
  download: false,
72
74
  });
@@ -95,6 +97,8 @@ const MIME_TYPES_TO_CAPTURE_SCREENSHOTS = new Set<MimeType>([
95
97
  "application/vnd.vega.v6+json",
96
98
  ]);
97
99
 
100
+ type ScreenshotResults = Record<CellId, ["image/png", string]>;
101
+
98
102
  /**
99
103
  * Take screenshots of cells with HTML outputs. These images will be sent to the backend to be exported to IPYNB.
100
104
  * @returns A map of cell IDs to their screenshots data.
@@ -103,7 +107,7 @@ export function useEnrichCellOutputs() {
103
107
  const [richCellsOutput, setRichCellsOutput] = useAtom(richCellsToOutputAtom);
104
108
  const cellRuntimes = useAtomValue(cellsRuntimeAtom);
105
109
 
106
- return async (): Promise<Record<CellId, ["image/png", string]>> => {
110
+ return async (progress: ProgressState): Promise<ScreenshotResults> => {
107
111
  const trackedCellsOutput: Record<CellId, unknown> = {};
108
112
 
109
113
  const cellsToCaptureScreenshot: [CellId, unknown][] = [];
@@ -129,43 +133,40 @@ export function useEnrichCellOutputs() {
129
133
  }
130
134
 
131
135
  // Capture screenshots
132
- const results = await Promise.all(
133
- cellsToCaptureScreenshot.map(async ([cellId]) => {
134
- try {
135
- const dataUrl = await getImageDataUrlForCell(cellId, false);
136
- if (!dataUrl) {
137
- Logger.error(`Failed to capture screenshot for cell ${cellId}`);
138
- return null;
139
- }
140
- return [cellId, ["image/png", dataUrl]] as [
141
- CellId,
142
- ["image/png", string],
143
- ];
144
- } catch (error) {
145
- Logger.error(`Error screenshotting cell ${cellId}:`, error);
146
- return null;
136
+ const total = cellsToCaptureScreenshot.length;
137
+ progress.addTotal(total);
138
+ const results: ScreenshotResults = {};
139
+ for (const [cellId] of cellsToCaptureScreenshot) {
140
+ try {
141
+ const dataUrl = await getImageDataUrlForCell(cellId, false);
142
+ if (!dataUrl) {
143
+ Logger.error(`Failed to capture screenshot for cell ${cellId}`);
144
+ continue;
147
145
  }
148
- }),
149
- );
150
-
151
- return Objects.fromEntries(
152
- results.filter(
153
- (result): result is [CellId, ["image/png", string]] => result !== null,
154
- ),
155
- );
146
+ results[cellId] = ["image/png", dataUrl];
147
+ } catch (error) {
148
+ Logger.error(`Error screenshotting cell ${cellId}:`, error);
149
+ } finally {
150
+ progress.increment(1);
151
+ }
152
+ }
153
+
154
+ return results;
156
155
  };
157
156
  }
158
157
 
159
158
  /**
160
159
  * Utility function to take screenshots of cells with HTML outputs and update the cell outputs.
161
160
  */
162
- export async function updateCellOutputsWithScreenshots(
163
- takeScreenshots: () => Promise<Record<CellId, ["image/png", string]>>,
164
- updateCellOutputs: (request: UpdateCellOutputsRequest) => Promise<null>,
165
- ) {
161
+ export async function updateCellOutputsWithScreenshots(opts: {
162
+ progress: ProgressState;
163
+ takeScreenshots: (progress: ProgressState) => Promise<ScreenshotResults>;
164
+ updateCellOutputs: (request: UpdateCellOutputsRequest) => Promise<null>;
165
+ }) {
166
+ const { progress, takeScreenshots, updateCellOutputs } = opts;
166
167
  try {
167
- const cellIdsToOutput = await takeScreenshots();
168
- if (Object.keys(cellIdsToOutput).length > 0) {
168
+ const cellIdsToOutput = await takeScreenshots(progress);
169
+ if (Objects.size(cellIdsToOutput) > 0) {
169
170
  await updateCellOutputs({ cellIdsToOutput });
170
171
  }
171
172
  } catch (error) {
@@ -57,10 +57,12 @@ describe("withLoadingToast", () => {
57
57
  });
58
58
 
59
59
  expect(toast).toHaveBeenCalledTimes(1);
60
- expect(toast).toHaveBeenCalledWith({
61
- title: "Loading...",
62
- duration: Infinity,
63
- });
60
+ expect(toast).toHaveBeenCalledWith(
61
+ expect.objectContaining({
62
+ title: "Loading...",
63
+ duration: Infinity,
64
+ }),
65
+ );
64
66
  expect(mockDismiss).toHaveBeenCalledTimes(1);
65
67
  expect(result).toBe("success");
66
68
  });