@marimo-team/islands 0.19.5-dev26 → 0.19.5-dev29
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/main.js +234 -214
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +1 -0
- package/src/components/app-config/user-config-form.tsx +33 -0
- package/src/components/data-table/cell-utils.ts +10 -0
- package/src/components/editor/Output.tsx +17 -14
- package/src/components/editor/actions/useNotebookActions.tsx +44 -12
- package/src/components/editor/cell/cell-actions.tsx +6 -1
- package/src/components/editor/navigation/navigation.ts +39 -1
- package/src/core/codemirror/language/panel/panel.tsx +3 -0
- package/src/core/codemirror/language/panel/sql.tsx +6 -2
- package/src/core/config/feature-flag.tsx +2 -0
- package/src/core/export/__tests__/hooks.test.ts +120 -1
- package/src/core/export/hooks.ts +33 -8
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +9 -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 +2 -0
- package/src/core/wasm/bridge.ts +1 -0
- package/src/utils/__tests__/download.test.tsx +96 -0
- package/src/utils/download.ts +54 -0
- package/src/utils/filenames.ts +3 -0
|
@@ -50,6 +50,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
50
50
|
// Export operations start a connection
|
|
51
51
|
exportAsHTML: "startConnection",
|
|
52
52
|
exportAsMarkdown: "startConnection",
|
|
53
|
+
exportAsPDF: "startConnection",
|
|
53
54
|
readCode: "startConnection",
|
|
54
55
|
sendCopy: "throwError",
|
|
55
56
|
|
|
@@ -381,6 +381,15 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
381
381
|
})
|
|
382
382
|
.then(handleResponse);
|
|
383
383
|
},
|
|
384
|
+
exportAsPDF: async (request) => {
|
|
385
|
+
return getClient()
|
|
386
|
+
.POST("/api/export/pdf", {
|
|
387
|
+
body: request,
|
|
388
|
+
parseAs: "blob",
|
|
389
|
+
params: getParams(),
|
|
390
|
+
})
|
|
391
|
+
.then(handleResponse);
|
|
392
|
+
},
|
|
384
393
|
autoExportAsHTML: async (request) => {
|
|
385
394
|
return getClient()
|
|
386
395
|
.POST("/api/export/auto_export/html", {
|
|
@@ -76,6 +76,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
76
76
|
shutdownSession: throwNotInEditMode,
|
|
77
77
|
exportAsHTML: throwNotInEditMode,
|
|
78
78
|
exportAsMarkdown: throwNotInEditMode,
|
|
79
|
+
exportAsPDF: throwNotInEditMode,
|
|
79
80
|
autoExportAsHTML: throwNotInEditMode,
|
|
80
81
|
autoExportAsMarkdown: throwNotInEditMode,
|
|
81
82
|
autoExportAsIPYNB: throwNotInEditMode,
|
|
@@ -61,6 +61,7 @@ export function createErrorToastingRequests(
|
|
|
61
61
|
shutdownSession: "Failed to shutdown session",
|
|
62
62
|
exportAsHTML: "Failed to export HTML",
|
|
63
63
|
exportAsMarkdown: "Failed to export Markdown",
|
|
64
|
+
exportAsPDF: "Failed to export PDF",
|
|
64
65
|
autoExportAsHTML: "", // No toast
|
|
65
66
|
autoExportAsMarkdown: "", // No toast
|
|
66
67
|
autoExportAsIPYNB: "", // No toast
|
|
@@ -21,6 +21,7 @@ export type ExportAsHTMLRequest = schemas["ExportAsHTMLRequest"];
|
|
|
21
21
|
export type ExportAsMarkdownRequest = schemas["ExportAsMarkdownRequest"];
|
|
22
22
|
export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
|
|
23
23
|
export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
|
|
24
|
+
export type ExportAsPDFRequest = schemas["ExportAsPDFRequest"];
|
|
24
25
|
export type UpdateCellOutputsRequest = schemas["UpdateCellOutputsRequest"];
|
|
25
26
|
export type FileCreateRequest = schemas["FileCreateRequest"];
|
|
26
27
|
export type FileCreateResponse = schemas["FileCreateResponse"];
|
|
@@ -173,6 +174,7 @@ export interface EditRequests {
|
|
|
173
174
|
// Export requests
|
|
174
175
|
exportAsHTML: (request: ExportAsHTMLRequest) => Promise<string>;
|
|
175
176
|
exportAsMarkdown: (request: ExportAsMarkdownRequest) => Promise<string>;
|
|
177
|
+
exportAsPDF: (request: ExportAsPDFRequest) => Promise<Blob>;
|
|
176
178
|
autoExportAsHTML: (request: ExportAsHTMLRequest) => Promise<null>;
|
|
177
179
|
autoExportAsMarkdown: (request: ExportAsMarkdownRequest) => Promise<null>;
|
|
178
180
|
autoExportAsIPYNB: (request: ExportAsIPYNBRequest) => Promise<null>;
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -586,6 +586,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
586
586
|
getWorkspaceFiles = throwNotImplemented;
|
|
587
587
|
getRunningNotebooks = throwNotImplemented;
|
|
588
588
|
shutdownSession = throwNotImplemented;
|
|
589
|
+
exportAsPDF = throwNotImplemented;
|
|
589
590
|
autoExportAsHTML = throwNotImplemented;
|
|
590
591
|
autoExportAsMarkdown = throwNotImplemented;
|
|
591
592
|
autoExportAsIPYNB = throwNotImplemented;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { withLoadingToast } from "../download";
|
|
4
|
+
|
|
5
|
+
// Mock the toast module
|
|
6
|
+
const mockDismiss = vi.fn();
|
|
7
|
+
vi.mock("@/components/ui/use-toast", () => ({
|
|
8
|
+
toast: vi.fn(() => ({
|
|
9
|
+
dismiss: mockDismiss,
|
|
10
|
+
})),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock the Spinner component
|
|
14
|
+
vi.mock("@/components/icons/spinner", () => ({
|
|
15
|
+
Spinner: () => "MockSpinner",
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { toast } from "@/components/ui/use-toast";
|
|
19
|
+
|
|
20
|
+
describe("withLoadingToast", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should show a loading toast and dismiss on success", async () => {
|
|
26
|
+
const result = await withLoadingToast("Loading...", async () => {
|
|
27
|
+
return "success";
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(toast).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(toast).toHaveBeenCalledWith({
|
|
32
|
+
title: "Loading...",
|
|
33
|
+
});
|
|
34
|
+
expect(mockDismiss).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(result).toBe("success");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should dismiss toast and rethrow on error", async () => {
|
|
39
|
+
const error = new Error("Operation failed");
|
|
40
|
+
|
|
41
|
+
await expect(
|
|
42
|
+
withLoadingToast("Loading...", async () => {
|
|
43
|
+
throw error;
|
|
44
|
+
}),
|
|
45
|
+
).rejects.toThrow("Operation failed");
|
|
46
|
+
|
|
47
|
+
expect(toast).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(mockDismiss).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should return the value from the async function", async () => {
|
|
52
|
+
const expectedValue = { data: "test", count: 42 };
|
|
53
|
+
|
|
54
|
+
const result = await withLoadingToast("Processing...", async () => {
|
|
55
|
+
return expectedValue;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual(expectedValue);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle void functions", async () => {
|
|
62
|
+
let sideEffect = false;
|
|
63
|
+
|
|
64
|
+
await withLoadingToast("Saving...", async () => {
|
|
65
|
+
sideEffect = true;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(sideEffect).toBe(true);
|
|
69
|
+
expect(mockDismiss).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should use the provided title in the toast", async () => {
|
|
73
|
+
const customTitle = "Downloading PDF...";
|
|
74
|
+
|
|
75
|
+
await withLoadingToast(customTitle, async () => "done");
|
|
76
|
+
|
|
77
|
+
expect(toast).toHaveBeenCalledWith(
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
title: customTitle,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should wait for the async function to complete", async () => {
|
|
85
|
+
const events: string[] = [];
|
|
86
|
+
|
|
87
|
+
await withLoadingToast("Loading...", async () => {
|
|
88
|
+
events.push("start");
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
90
|
+
events.push("end");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(events).toEqual(["start", "end"]);
|
|
94
|
+
expect(mockDismiss).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/utils/download.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { toPng } from "html-to-image";
|
|
3
3
|
import { toast } from "@/components/ui/use-toast";
|
|
4
|
+
import { getRequestClient } from "@/core/network/requests";
|
|
4
5
|
import { Filenames } from "@/utils/filenames";
|
|
6
|
+
import { Paths } from "@/utils/paths";
|
|
7
|
+
import { prettyError } from "./errors";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Show a loading toast while an async operation is in progress.
|
|
11
|
+
* Automatically dismisses the toast when the operation completes or fails.
|
|
12
|
+
*/
|
|
13
|
+
export async function withLoadingToast<T>(
|
|
14
|
+
title: string,
|
|
15
|
+
fn: () => Promise<T>,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
const loadingToast = toast({
|
|
18
|
+
title,
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
const result = await fn();
|
|
22
|
+
loadingToast.dismiss();
|
|
23
|
+
return result;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
loadingToast.dismiss();
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
5
29
|
|
|
6
30
|
export async function downloadHTMLAsImage(
|
|
7
31
|
element: HTMLElement,
|
|
@@ -45,3 +69,33 @@ export function downloadBlob(blob: Blob, filename: string) {
|
|
|
45
69
|
downloadByURL(url, filename);
|
|
46
70
|
URL.revokeObjectURL(url);
|
|
47
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Download the current notebook as a PDF file.
|
|
75
|
+
*
|
|
76
|
+
* WebPDF only requires Chromium to be installed.
|
|
77
|
+
* Standard PDF requires Pandoc & TeX (~few GBs) but is of higher quality.
|
|
78
|
+
*/
|
|
79
|
+
export async function downloadAsPDF(opts: {
|
|
80
|
+
filename: string;
|
|
81
|
+
webpdf: boolean;
|
|
82
|
+
}) {
|
|
83
|
+
const client = getRequestClient();
|
|
84
|
+
const { filename, webpdf } = opts;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const pdfBlob = await client.exportAsPDF({
|
|
88
|
+
webpdf,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const filenameWithoutPath = Paths.basename(filename);
|
|
92
|
+
downloadBlob(pdfBlob, Filenames.toPDF(filenameWithoutPath));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
toast({
|
|
95
|
+
title: "Failed to download",
|
|
96
|
+
description: prettyError(error),
|
|
97
|
+
variant: "danger",
|
|
98
|
+
});
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/utils/filenames.ts
CHANGED
|
@@ -9,6 +9,9 @@ export const Filenames = {
|
|
|
9
9
|
toPNG: (filename: string): string => {
|
|
10
10
|
return Filenames.replace(filename, "png");
|
|
11
11
|
},
|
|
12
|
+
toPDF: (filename: string): string => {
|
|
13
|
+
return Filenames.replace(filename, "pdf");
|
|
14
|
+
},
|
|
12
15
|
toPY: (filename: string): string => {
|
|
13
16
|
return Filenames.replace(filename, "py");
|
|
14
17
|
},
|