mirror of https://github.com/buster-so/buster.git
handle after print
This commit is contained in:
parent
5c6bfdf96a
commit
cb02074cba
|
@ -17,6 +17,7 @@ import { MarkdownKit } from './plugins/markdown-kit';
|
|||
import { BaseMathKit } from './plugins/math-base-kit';
|
||||
import { BaseMediaKit } from './plugins/media-base-kit';
|
||||
import { BaseMentionKit } from './plugins/mention-base-kit';
|
||||
import { MetricBaseKit } from './plugins/metric-kit/metric-base-kit';
|
||||
import { BaseSuggestionKit } from './plugins/suggestion-base-kit';
|
||||
import { BaseTableKit } from './plugins/table-base-kit';
|
||||
import { BaseTocKit } from './plugins/toc-base-kit';
|
||||
|
@ -44,5 +45,6 @@ export const BaseEditorKit = [
|
|||
...BaseCommentKit,
|
||||
...BaseSuggestionKit,
|
||||
...MarkdownKit,
|
||||
...BusterStreamKit
|
||||
...BusterStreamKit,
|
||||
...MetricBaseKit
|
||||
];
|
||||
|
|
|
@ -34,16 +34,16 @@ export function ExportToolbarButton({ children, ...props }: DropdownMenuProps) {
|
|||
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={() => exportToHtml(editor)}>
|
||||
<DropdownMenuItem onSelect={() => exportToHtml({ editor })}>
|
||||
{NodeTypeLabels.exportAsHtml.label}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => exportToPdf(editor)}>
|
||||
<DropdownMenuItem onSelect={() => exportToPdf({ editor })}>
|
||||
{NodeTypeLabels.exportAsPdf.label}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => exportToImage(editor)}>
|
||||
<DropdownMenuItem onSelect={() => exportToImage({ editor })}>
|
||||
{NodeTypeLabels.exportAsImage.label}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => exportToMarkdown(editor)}>
|
||||
<DropdownMenuItem onSelect={() => exportToMarkdown({ editor })}>
|
||||
{NodeTypeLabels.exportAsMarkdown.label}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
|
|
@ -5,17 +5,18 @@ import { useChatLayoutContextSelector } from '@/layouts/ChatLayout';
|
|||
import { assetParamsToRoute } from '@/lib/assets/assetParamsToRoute';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { useMetricContentThreeDotMenuItems } from './useMetricContentThreeDotMenuItems';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
export const MetricContent = React.memo(
|
||||
({
|
||||
metricId,
|
||||
metricVersionNumber,
|
||||
isBaseElement = false,
|
||||
readOnly = false
|
||||
}: {
|
||||
metricId: string;
|
||||
metricVersionNumber: number | undefined;
|
||||
readOnly?: boolean;
|
||||
isBaseElement?: boolean;
|
||||
}) => {
|
||||
const chatId = useChatLayoutContextSelector((x) => x.chatId);
|
||||
const reportId = useChatLayoutContextSelector((x) => x.reportId) || '';
|
||||
|
@ -23,7 +24,7 @@ export const MetricContent = React.memo(
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [inViewport] = useInViewport(ref, {
|
||||
threshold: 0.33
|
||||
threshold: isBaseElement ? 0 : 0.33
|
||||
});
|
||||
const renderChart = inViewport;
|
||||
|
||||
|
@ -72,7 +73,7 @@ export const MetricContent = React.memo(
|
|||
return (
|
||||
<MetricCard
|
||||
metricLink={link}
|
||||
animate
|
||||
animate={!isBaseElement}
|
||||
metricId={metricId}
|
||||
readOnly={readOnly}
|
||||
isDragOverlay={false}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { ResizableProvider, useResizableValue } from '@platejs/resizable';
|
|||
import { MetricEmbedPlaceholder } from './MetricPlaceholder';
|
||||
import { Caption, CaptionTextarea } from '../CaptionNode';
|
||||
import { mediaResizeHandleVariants, Resizable, ResizeHandle } from '../ResizeHandle';
|
||||
import { type TMetricElement } from '../../plugins/metric-plugin';
|
||||
import { type TMetricElement } from '../../plugins/metric-kit';
|
||||
import React, { useMemo, useRef, type PropsWithChildren } from 'react';
|
||||
import { useSize } from '@/hooks/useSize';
|
||||
import { MetricContent } from './MetricContent';
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { MetricContent } from './MetricContent';
|
||||
import type { TMetricElement } from '../../plugins/metric-kit';
|
||||
import {
|
||||
SlateElement,
|
||||
type TCaptionProps,
|
||||
type TResizableProps,
|
||||
type SlateElementProps
|
||||
} from 'platejs';
|
||||
import { MetricEmbedPlaceholder } from './MetricPlaceholder';
|
||||
|
||||
export const MetricElementStatic = (
|
||||
props: SlateElementProps<TMetricElement & TCaptionProps & TResizableProps>
|
||||
) => {
|
||||
const metricId = props.element.metricId;
|
||||
const metricVersionNumber = props.element.metricVersionNumber;
|
||||
const readOnly = true;
|
||||
const { align = 'center', caption, url, width } = props.element;
|
||||
|
||||
const content = metricId ? (
|
||||
<MetricContent
|
||||
metricId={metricId}
|
||||
metricVersionNumber={metricVersionNumber}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : (
|
||||
<MetricEmbedPlaceholder />
|
||||
);
|
||||
|
||||
return (
|
||||
<SlateElement {...props}>
|
||||
<figure className="group relative m-0 inline-block" style={{ width }}>
|
||||
<div className="relative max-w-full min-w-[92px]" style={{ textAlign: align }}>
|
||||
{content}
|
||||
</div>
|
||||
<div className="h-0">{props.children}</div>
|
||||
</figure>
|
||||
</SlateElement>
|
||||
);
|
||||
};
|
|
@ -13,7 +13,7 @@ import {
|
|||
import React, { useEffect } from 'react';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
import { AddMetricModal } from '@/components/features/modal/AddMetricModal';
|
||||
import { MetricPlugin, type TMetricElement } from '../../plugins/metric-plugin';
|
||||
import { MetricPlugin, type TMetricElement } from '../../plugins/metric-kit';
|
||||
|
||||
export const MetricEmbedPlaceholder: React.FC = () => {
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
|
|
|
@ -19,7 +19,7 @@ import { TablePlugin } from '@platejs/table/react';
|
|||
import { insertToc } from '@platejs/toc';
|
||||
import { type NodeEntry, type Path, type TElement, KEYS, PathApi } from 'platejs';
|
||||
import { CUSTOM_KEYS } from '../config/keys';
|
||||
import { insertMetric } from '../plugins/metric-plugin';
|
||||
import { insertMetric } from '../plugins/metric-kit';
|
||||
|
||||
const ACTION_THREE_COLUMNS = 'action_three_columns';
|
||||
|
||||
|
|
|
@ -6,7 +6,18 @@ import { createSlateEditor, serializeHtml } from 'platejs';
|
|||
// computed CSS styles so no external CSS is required. Additionally,
|
||||
// snapshot <canvas> elements inside metrics to <img> tags to avoid
|
||||
// blank canvases in the exported HTML.
|
||||
export const buildExportHtml = async (editor: PlateEditor): Promise<string> => {
|
||||
// Options for building the export HTML document
|
||||
type BuildExportHtmlOptions = {
|
||||
// Optional document title to embed in the HTML head
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const buildExportHtml = async (
|
||||
editor: PlateEditor,
|
||||
options?: BuildExportHtmlOptions
|
||||
): Promise<string> => {
|
||||
// Resolve the document title with a sensible default
|
||||
const documentTitle = options?.title || 'Buster Report';
|
||||
// Prefer using the live editor DOM to inline computed styles
|
||||
const liveRoot = document.querySelector('[contenteditable="true"]') as HTMLElement | null;
|
||||
|
||||
|
@ -89,35 +100,7 @@ export const buildExportHtml = async (editor: PlateEditor): Promise<string> => {
|
|||
wrapper.appendChild(clonedRoot);
|
||||
contentHtml = wrapper.outerHTML;
|
||||
} else {
|
||||
// Fallback: serialize using static editor and include minimal inline styles
|
||||
const BaseEditorKit = await import('../editor-base-kit').then((module) => module.BaseEditorKit);
|
||||
|
||||
const editorStatic = createSlateEditor({
|
||||
plugins: BaseEditorKit,
|
||||
value: editor.children
|
||||
});
|
||||
|
||||
const serializedHtml = await serializeHtml(editorStatic, {
|
||||
editorComponent: EditorStatic,
|
||||
props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } }
|
||||
});
|
||||
|
||||
// Wrap the serialized HTML to match the centered, fixed-width layout
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.setAttribute(
|
||||
'style',
|
||||
[
|
||||
'width: 816px',
|
||||
'max-width: 816px',
|
||||
'min-width: 816px',
|
||||
'margin: 24px auto',
|
||||
'background: #ffffff',
|
||||
'padding: 40px',
|
||||
'box-sizing: border-box'
|
||||
].join('; ')
|
||||
);
|
||||
wrapper.innerHTML = serializedHtml;
|
||||
contentHtml = wrapper.outerHTML;
|
||||
throw new Error('No live root found');
|
||||
}
|
||||
|
||||
// Build a minimal HTML document without external CSS
|
||||
|
@ -127,6 +110,7 @@ export const buildExportHtml = async (editor: PlateEditor): Promise<string> => {
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>${documentTitle}</title>
|
||||
<style>
|
||||
body { margin: 0; background: #f5f5f5; }
|
||||
@media print {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import type { PlateEditor } from 'platejs/react';
|
||||
|
||||
export const buildExportHtml = async (editor: PlateEditor, options?: {}): Promise<string> => {
|
||||
return '';
|
||||
};
|
|
@ -5,23 +5,29 @@ import { downloadFile } from './downloadFile';
|
|||
|
||||
type Notifier = (message: string) => void;
|
||||
|
||||
export const exportToHtml = async (
|
||||
editor: PlateEditor,
|
||||
openInfoMessage: Notifier,
|
||||
openErrorMessage: Notifier
|
||||
) => {
|
||||
type ExportToHtmlOptions = {
|
||||
editor: PlateEditor;
|
||||
filename?: string;
|
||||
openInfoMessage?: Notifier;
|
||||
openErrorMessage?: Notifier;
|
||||
};
|
||||
|
||||
export const exportToHtml = async ({
|
||||
editor,
|
||||
filename = 'buster-report.html',
|
||||
openInfoMessage,
|
||||
openErrorMessage
|
||||
}: ExportToHtmlOptions) => {
|
||||
try {
|
||||
const html = await buildExportHtml(editor);
|
||||
const html = await buildExportHtml(editor, { title: filename.replace(/\.html$/i, '') });
|
||||
const url = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||
|
||||
await downloadFile(url, 'plate.html');
|
||||
openInfoMessage(NodeTypeLabels.htmlExportedSuccessfully.label);
|
||||
await downloadFile(url, filename);
|
||||
openInfoMessage?.(NodeTypeLabels.htmlExportedSuccessfully.label);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
openErrorMessage(NodeTypeLabels.failedToExportHtml.label);
|
||||
openErrorMessage?.(NodeTypeLabels.failedToExportHtml.label);
|
||||
}
|
||||
};
|
||||
|
||||
export default exportToHtml;
|
||||
|
||||
|
||||
|
|
|
@ -5,18 +5,26 @@ import { downloadFile } from './downloadFile';
|
|||
|
||||
type Notifier = (message: string) => void;
|
||||
|
||||
export const exportToImage = async (
|
||||
editor: PlateEditor,
|
||||
openInfoMessage: Notifier,
|
||||
openErrorMessage: Notifier
|
||||
) => {
|
||||
type ExportToImageOptions = {
|
||||
editor: PlateEditor;
|
||||
filename?: string;
|
||||
openInfoMessage?: Notifier;
|
||||
openErrorMessage?: Notifier;
|
||||
};
|
||||
|
||||
export const exportToImage = async ({
|
||||
editor,
|
||||
filename = 'plate.png',
|
||||
openInfoMessage,
|
||||
openErrorMessage
|
||||
}: ExportToImageOptions) => {
|
||||
try {
|
||||
const canvas = await getCanvas(editor);
|
||||
await downloadFile(canvas.toDataURL('image/png'), 'plate.png');
|
||||
openInfoMessage(NodeTypeLabels.imageExportedSuccessfully.label);
|
||||
await downloadFile(canvas.toDataURL('image/png'), filename);
|
||||
openInfoMessage?.(NodeTypeLabels.imageExportedSuccessfully.label);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
openErrorMessage(NodeTypeLabels.failedToExportImage.label);
|
||||
openErrorMessage?.(NodeTypeLabels.failedToExportImage.label);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,22 +5,26 @@ import { downloadFile } from './downloadFile';
|
|||
|
||||
type Notifier = (message: string) => void;
|
||||
|
||||
export const exportToMarkdown = async (
|
||||
editor: PlateEditor,
|
||||
openInfoMessage: Notifier,
|
||||
openErrorMessage: Notifier
|
||||
) => {
|
||||
type ExportToMarkdownOptions = {
|
||||
editor: PlateEditor;
|
||||
openInfoMessage?: Notifier;
|
||||
openErrorMessage?: Notifier;
|
||||
};
|
||||
|
||||
export const exportToMarkdown = async ({
|
||||
editor,
|
||||
openInfoMessage,
|
||||
openErrorMessage
|
||||
}: ExportToMarkdownOptions) => {
|
||||
try {
|
||||
const md = editor.getApi(MarkdownPlugin).markdown.serialize();
|
||||
const url = `data:text/markdown;charset=utf-8,${encodeURIComponent(md)}`;
|
||||
await downloadFile(url, 'plate.md');
|
||||
openInfoMessage(NodeTypeLabels.markdownExportedSuccessfully.label);
|
||||
openInfoMessage?.(NodeTypeLabels.markdownExportedSuccessfully.label);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
openErrorMessage(NodeTypeLabels.failedToExportMarkdown.label);
|
||||
openErrorMessage?.(NodeTypeLabels.failedToExportMarkdown.label);
|
||||
}
|
||||
};
|
||||
|
||||
export default exportToMarkdown;
|
||||
|
||||
|
||||
|
|
|
@ -4,13 +4,21 @@ import { buildExportHtml } from './buildExportHtml';
|
|||
|
||||
type Notifier = (message: string) => void;
|
||||
|
||||
export const exportToPdf = async (
|
||||
editor: PlateEditor,
|
||||
openInfoMessage: Notifier,
|
||||
openErrorMessage: Notifier
|
||||
) => {
|
||||
type ExportToPdfOptions = {
|
||||
filename?: string;
|
||||
openInfoMessage?: Notifier;
|
||||
openErrorMessage?: Notifier;
|
||||
editor: PlateEditor;
|
||||
};
|
||||
|
||||
export const exportToPdf = async ({
|
||||
editor,
|
||||
filename = 'Buster Report',
|
||||
openInfoMessage,
|
||||
openErrorMessage
|
||||
}: ExportToPdfOptions) => {
|
||||
try {
|
||||
const html = await buildExportHtml(editor);
|
||||
const html = await buildExportHtml(editor, { title: filename });
|
||||
|
||||
// Open a print window with the rendered HTML so the user can save as PDF
|
||||
const printWindow = window.open('', '_blank');
|
||||
|
@ -20,10 +28,22 @@ export const exportToPdf = async (
|
|||
printWindow.document.write(html);
|
||||
printWindow.document.close();
|
||||
|
||||
// Close the print window after the user prints or cancels
|
||||
const handleAfterPrint = () => {
|
||||
try {
|
||||
// printWindow.close();
|
||||
} catch (e) {
|
||||
console.error('Failed to close print window', e);
|
||||
}
|
||||
};
|
||||
printWindow.addEventListener('afterprint', handleAfterPrint);
|
||||
|
||||
// Trigger print when resources are loaded
|
||||
const triggerPrint = () => {
|
||||
try {
|
||||
printWindow.focus();
|
||||
// Set the title for the print window so the OS save dialog suggests it
|
||||
printWindow.document.title = filename;
|
||||
printWindow.print();
|
||||
} catch (e) {
|
||||
console.error('Failed to trigger print dialog', e);
|
||||
|
@ -37,13 +57,11 @@ export const exportToPdf = async (
|
|||
printWindow.addEventListener('load', () => setTimeout(triggerPrint, 100));
|
||||
}
|
||||
|
||||
openInfoMessage(NodeTypeLabels.pdfExportedSuccessfully.label);
|
||||
openInfoMessage?.(NodeTypeLabels.pdfExportedSuccessfully.label);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
openErrorMessage(NodeTypeLabels.failedToExportPdf.label);
|
||||
openErrorMessage?.(NodeTypeLabels.failedToExportPdf.label);
|
||||
}
|
||||
};
|
||||
|
||||
export default exportToPdf;
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||
import type { PlateEditor } from 'platejs/react';
|
||||
import { useMemo } from 'react';
|
||||
import { exportToPdf } from './exportToPdf';
|
||||
import { exportToImage } from './exportToImage';
|
||||
|
@ -9,17 +8,17 @@ import { exportToMarkdown } from './exportToMarkdown';
|
|||
export const useExportReport = () => {
|
||||
const { openErrorMessage, openInfoMessage } = useBusterNotifications();
|
||||
|
||||
const exportToPdfLocal = async (editor: PlateEditor) =>
|
||||
exportToPdf(editor, openInfoMessage, openErrorMessage);
|
||||
const exportToPdfLocal = async (params: Parameters<typeof exportToPdf>[0]) =>
|
||||
exportToPdf({ ...params, openInfoMessage, openErrorMessage });
|
||||
|
||||
const exportToImageLocal = async (editor: PlateEditor) =>
|
||||
exportToImage(editor, openInfoMessage, openErrorMessage);
|
||||
const exportToImageLocal = async (params: Parameters<typeof exportToImage>[0]) =>
|
||||
exportToImage({ ...params, openInfoMessage, openErrorMessage });
|
||||
|
||||
const exportToHtmlLocal = async (editor: PlateEditor) =>
|
||||
exportToHtml(editor, openInfoMessage, openErrorMessage);
|
||||
const exportToHtmlLocal = async (params: Parameters<typeof exportToHtml>[0]) =>
|
||||
exportToHtml({ ...params, openInfoMessage, openErrorMessage });
|
||||
|
||||
const exportToMarkdownLocal = async (editor: PlateEditor) =>
|
||||
exportToMarkdown(editor, openInfoMessage, openErrorMessage);
|
||||
const exportToMarkdownLocal = async (params: Parameters<typeof exportToMarkdown>[0]) =>
|
||||
exportToMarkdown({ ...params, openInfoMessage, openErrorMessage });
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
import { BannerPlugin } from './banner-plugin';
|
||||
import { CharacterCounterPlugin } from './character-counter-kit';
|
||||
import { MetricPlugin } from './metric-plugin';
|
||||
import { MetricKit } from './metric-kit';
|
||||
|
||||
export const BusterStreamKit = [
|
||||
//BannerPlugin,
|
||||
CharacterCounterPlugin,
|
||||
MetricPlugin
|
||||
...MetricKit
|
||||
];
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export * from './metric-plugin';
|
||||
export * from './metric-kit';
|
||||
export * from './insert-metric';
|
|
@ -1,7 +1,7 @@
|
|||
import type { InsertNodesOptions } from 'platejs';
|
||||
import type { PlateEditor } from 'platejs/react';
|
||||
import { CUSTOM_KEYS } from '../../config/keys';
|
||||
import { MetricPlugin, type TMetricElement } from './metric-plugin';
|
||||
import { MetricPlugin, type TMetricElement } from './metric-kit';
|
||||
|
||||
export const insertMetric = (editor: PlateEditor, options?: InsertNodesOptions) => {
|
||||
editor.tf.insertNode<TMetricElement>(
|
||||
|
@ -19,5 +19,5 @@ export const insertMetric = (editor: PlateEditor, options?: InsertNodesOptions)
|
|||
setTimeout(() => {
|
||||
const plugin = editor.getPlugin(MetricPlugin);
|
||||
plugin.api.metric.openAddMetricModal();
|
||||
}, 50);
|
||||
}, 25);
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { createPlatePlugin } from 'platejs/react';
|
||||
import type { CUSTOM_KEYS } from '../../config/keys';
|
||||
import type { TMetricElement } from './metric-kit';
|
||||
import { MetricElementStatic } from '../../elements/MetricElement/MetricElementStatic';
|
||||
|
||||
export const MetricBaseKit = [
|
||||
createPlatePlugin<typeof CUSTOM_KEYS.metric, {}, {}, TMetricElement>({
|
||||
key: 'metric',
|
||||
node: {
|
||||
isElement: true,
|
||||
isVoid: false,
|
||||
component: MetricElementStatic
|
||||
}
|
||||
})
|
||||
];
|
|
@ -48,3 +48,5 @@ export const MetricPlugin = createPlatePlugin<
|
|||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const MetricKit = [MetricPlugin];
|
|
@ -314,6 +314,7 @@ const useDuplicateReportSelectMenu = (): DropdownItem => {
|
|||
// Download as PDF
|
||||
const useDownloadPdfSelectMenu = ({ reportId }: { reportId: string }): DropdownItem => {
|
||||
const { openErrorMessage } = useBusterNotifications();
|
||||
const { data: reportName } = useGetReport({ reportId }, { select: (x) => x.name });
|
||||
const { exportToPdf } = useExportReport();
|
||||
|
||||
const onClick = async () => {
|
||||
|
@ -326,7 +327,7 @@ const useDownloadPdfSelectMenu = ({ reportId }: { reportId: string }): DropdownI
|
|||
return;
|
||||
}
|
||||
|
||||
await exportToPdf(editor);
|
||||
await exportToPdf({ editor, filename: reportName });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
openErrorMessage('Failed to export report as PDF');
|
||||
|
|
Loading…
Reference in New Issue