From 4e8c0d658bc67d4efbb5424fdd4f112912252373 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:57:49 +0000 Subject: [PATCH 1/4] feat: add toolbar alignment option and export watermark support - Add toolbarAlignment prop to ERDRenderer with 'left', 'center', 'right' options (default: 'center') - Add CSS classes for toolbar positioning - Add PNG and PDF export functionality with watermark logo support - Add printExportLogo and printExportLogoPosition props to AppBarConfig - Create useExportDiagram hook with neverthrow Result types - Create ExportConfigContext for passing export configuration Co-Authored-By: Christian Screen --- frontend/packages/erd-core/package.json | 2 + .../components/ERDRenderer/AppBar/AppBar.tsx | 16 +- .../ExportDropdown/ExportConfigContext.tsx | 30 ++ .../AppBar/ExportDropdown/ExportDropdown.tsx | 63 +++++ .../AppBar/ExportDropdown/index.ts | 3 + .../AppBar/ExportDropdown/useExportDiagram.ts | 257 ++++++++++++++++++ .../components/ERDRenderer/AppBar/index.ts | 1 + .../components/ERDRenderer/AppBar/types.ts | 3 + .../ERDRenderer/ERDRenderer.module.css | 10 + .../components/ERDRenderer/ErdRenderer.tsx | 13 +- pnpm-lock.yaml | 170 ++++++++++++ 11 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportConfigContext.tsx create mode 100644 frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts diff --git a/frontend/packages/erd-core/package.json b/frontend/packages/erd-core/package.json index 38027a7497..2f6fd733c6 100644 --- a/frontend/packages/erd-core/package.json +++ b/frontend/packages/erd-core/package.json @@ -22,6 +22,8 @@ "clsx": "2.1.1", "cmdk": "1.1.1", "elkjs": "0.10.0", + "html-to-image": "1.11.13", + "jspdf": "4.0.0", "neverthrow": "8.2.0", "nuqs": "2.4.3", "pako": "2.1.0", diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/AppBar.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/AppBar.tsx index bd7ded9cb5..2ace4e9d99 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/AppBar.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/AppBar.tsx @@ -7,10 +7,11 @@ import { TooltipTrigger, } from '@dlh/erd-viewer-ui' import type { FC } from 'react' +import { useMemo } from 'react' import { CommandPaletteTriggerButton } from '../CommandPalette' import styles from './AppBar.module.css' import { CopyLinkButton } from './CopyLinkButton' -import { ExportDropdown } from './ExportDropdown' +import { ExportConfigProvider, ExportDropdown } from './ExportDropdown' import { GithubButton } from './GithubButton' import { HelpButton } from './HelpButton' import { MenuButton } from './MenuButton' @@ -40,6 +41,13 @@ export const AppBar: FC = ({ config }) => { const showHelp = config?.help?.show !== false const helpItems = config?.help?.items const showExport = config?.export?.show !== false + const exportConfig = useMemo( + () => ({ + printExportLogo: config?.export?.printExportLogo, + printExportLogoPosition: config?.export?.printExportLogoPosition, + }), + [config?.export?.printExportLogo, config?.export?.printExportLogoPosition], + ) const showCopyLink = config?.copyLink?.show !== false const copyLinkValue = config?.copyLink?.value @@ -83,7 +91,11 @@ export const AppBar: FC = ({ config }) => { )} - {showExport && } + {showExport && ( + + + + )} {showCopyLink && ( (null) + +type ExportConfigProviderProps = { + config: ExportConfig + children: ReactNode +} + +export const ExportConfigProvider: FC = ({ + config, + children, +}) => { + return ( + + {children} + + ) +} + +export const useExportConfig = (): ExportConfig => { + const context = useContext(ExportConfigContext) + return context ?? {} +} diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportDropdown.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportDropdown.tsx index c717b3f608..e873c98ae0 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportDropdown.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportDropdown.tsx @@ -6,20 +6,26 @@ import { Button, ChevronDown, Copy, + Download, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRoot, + DropdownMenuSeparator, DropdownMenuTrigger, useToast, } from '@dlh/erd-viewer-ui' import type { FC } from 'react' import { useSchemaOrThrow } from '../../../../../../stores' import { fromPromise } from '../../../../../../utils/neverthrow' +import { useExportConfig } from './ExportConfigContext' +import { useExportDiagram } from './useExportDiagram' export const ExportDropdown: FC = () => { const toast = useToast() const schema = useSchemaOrThrow() + const { exportToPng, exportToPdf } = useExportDiagram() + const exportConfig = useExportConfig() const handleCopyPostgreSQL = async () => { // Feature detection for clipboard API @@ -104,6 +110,50 @@ export const ExportDropdown: FC = () => { ) } + const handleExportPng = async () => { + try { + await exportToPng({ + watermarkLogo: exportConfig.printExportLogo, + watermarkPosition: exportConfig.printExportLogoPosition, + }) + toast({ + title: 'PNG exported!', + description: 'Diagram has been exported as PNG', + status: 'success', + }) + } catch (error) { + console.error('Failed to export PNG:', error) + toast({ + title: 'Export failed', + description: + error instanceof Error ? error.message : 'Failed to export PNG', + status: 'error', + }) + } + } + + const handleExportPdf = async () => { + try { + await exportToPdf({ + watermarkLogo: exportConfig.printExportLogo, + watermarkPosition: exportConfig.printExportLogoPosition, + }) + toast({ + title: 'PDF exported!', + description: 'Diagram has been exported as PDF', + status: 'success', + }) + } catch (error) { + console.error('Failed to export PDF:', error) + toast({ + title: 'Export failed', + description: + error instanceof Error ? error.message : 'Failed to export PDF', + status: 'error', + }) + } + } + return ( @@ -117,6 +167,19 @@ export const ExportDropdown: FC = () => { + } + onSelect={handleExportPng} + > + Download PNG + + } + onSelect={handleExportPdf} + > + Download PDF + + } onSelect={handleCopyPostgreSQL} diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/index.ts b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/index.ts index 24aff0c623..da4fb53e3a 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/index.ts +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/index.ts @@ -1 +1,4 @@ +export type { ExportConfig } from './ExportConfigContext' +export { ExportConfigProvider, useExportConfig } from './ExportConfigContext' export * from './ExportDropdown' +export type { WatermarkPosition } from './useExportDiagram' diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts new file mode 100644 index 0000000000..ea37afab11 --- /dev/null +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts @@ -0,0 +1,257 @@ +import { useReactFlow } from '@xyflow/react' +import { toPng } from 'html-to-image' +import { jsPDF } from 'jspdf' +import { err, ok, type Result } from 'neverthrow' +import { useCallback } from 'react' + +export type WatermarkPosition = + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right' + +type ExportOptions = { + watermarkLogo?: string + watermarkPosition?: WatermarkPosition +} + +const addWatermarkToCanvas = ( + canvas: HTMLCanvasElement, + watermarkLogo: string, + position: WatermarkPosition, +): Promise => { + return new Promise((resolve, reject) => { + const img = new Image() + img.crossOrigin = 'anonymous' + img.onload = () => { + const ctx = canvas.getContext('2d') + if (!ctx) { + reject(new Error('Could not get canvas context')) + return + } + + const padding = 20 + const maxWatermarkWidth = Math.min(150, canvas.width * 0.15) + const scale = maxWatermarkWidth / img.width + const watermarkWidth = img.width * scale + const watermarkHeight = img.height * scale + + let x: number + let y: number + + switch (position) { + case 'top-left': + x = padding + y = padding + break + case 'top-right': + x = canvas.width - watermarkWidth - padding + y = padding + break + case 'bottom-left': + x = padding + y = canvas.height - watermarkHeight - padding + break + case 'bottom-right': + x = canvas.width - watermarkWidth - padding + y = canvas.height - watermarkHeight - padding + break + } + + ctx.globalAlpha = 0.7 + ctx.drawImage(img, x, y, watermarkWidth, watermarkHeight) + ctx.globalAlpha = 1.0 + + resolve(canvas) + } + img.onerror = () => { + reject(new Error('Failed to load watermark image')) + } + img.src = watermarkLogo + }) +} + +const dataUrlToCanvas = (dataUrl: string): Promise => { + return new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = img.width + canvas.height = img.height + const ctx = canvas.getContext('2d') + if (!ctx) { + reject(new Error('Could not get canvas context')) + return + } + ctx.drawImage(img, 0, 0) + resolve(canvas) + } + img.onerror = () => { + reject(new Error('Failed to load image')) + } + img.src = dataUrl + }) +} + +export const useExportDiagram = () => { + const { getNodes } = useReactFlow() + + const getFlowElement = useCallback((): Result => { + const element = document.querySelector('.react-flow__viewport') + if (!element) { + return err(new Error('Could not find React Flow viewport')) + } + if (!(element instanceof HTMLElement)) { + return err(new Error('React Flow viewport is not an HTMLElement')) + } + return ok(element) + }, []) + + const exportToPng = useCallback( + async (options?: ExportOptions): Promise> => { + const flowElementResult = getFlowElement() + if (flowElementResult.isErr()) { + return err(flowElementResult.error) + } + const flowElement = flowElementResult.value + + const nodes = getNodes() + if (nodes.length === 0) { + return err(new Error('No nodes to export')) + } + + const nodesBounds = nodes.reduce( + (bounds, node) => { + const nodeWidth = node.measured?.width ?? node.width ?? 200 + const nodeHeight = node.measured?.height ?? node.height ?? 100 + return { + minX: Math.min(bounds.minX, node.position.x), + minY: Math.min(bounds.minY, node.position.y), + maxX: Math.max(bounds.maxX, node.position.x + nodeWidth), + maxY: Math.max(bounds.maxY, node.position.y + nodeHeight), + } + }, + { + minX: Number.POSITIVE_INFINITY, + minY: Number.POSITIVE_INFINITY, + maxX: Number.NEGATIVE_INFINITY, + maxY: Number.NEGATIVE_INFINITY, + }, + ) + + const padding = 50 + const width = nodesBounds.maxX - nodesBounds.minX + padding * 2 + const height = nodesBounds.maxY - nodesBounds.minY + padding * 2 + + const dataUrl = await toPng(flowElement, { + backgroundColor: '#ffffff', + width, + height, + style: { + width: `${width}px`, + height: `${height}px`, + transform: `translate(${-nodesBounds.minX + padding}px, ${-nodesBounds.minY + padding}px)`, + }, + }) + + let finalDataUrl = dataUrl + + if (options?.watermarkLogo) { + const canvas = await dataUrlToCanvas(dataUrl) + const watermarkedCanvas = await addWatermarkToCanvas( + canvas, + options.watermarkLogo, + options.watermarkPosition ?? 'top-left', + ) + finalDataUrl = watermarkedCanvas.toDataURL('image/png') + } + + const link = document.createElement('a') + link.download = 'erd-diagram.png' + link.href = finalDataUrl + link.click() + + return ok(undefined) + }, + [getFlowElement, getNodes], + ) + + const exportToPdf = useCallback( + async (options?: ExportOptions): Promise> => { + const flowElementResult = getFlowElement() + if (flowElementResult.isErr()) { + return err(flowElementResult.error) + } + const flowElement = flowElementResult.value + + const nodes = getNodes() + if (nodes.length === 0) { + return err(new Error('No nodes to export')) + } + + const nodesBounds = nodes.reduce( + (bounds, node) => { + const nodeWidth = node.measured?.width ?? node.width ?? 200 + const nodeHeight = node.measured?.height ?? node.height ?? 100 + return { + minX: Math.min(bounds.minX, node.position.x), + minY: Math.min(bounds.minY, node.position.y), + maxX: Math.max(bounds.maxX, node.position.x + nodeWidth), + maxY: Math.max(bounds.maxY, node.position.y + nodeHeight), + } + }, + { + minX: Number.POSITIVE_INFINITY, + minY: Number.POSITIVE_INFINITY, + maxX: Number.NEGATIVE_INFINITY, + maxY: Number.NEGATIVE_INFINITY, + }, + ) + + const padding = 50 + const width = nodesBounds.maxX - nodesBounds.minX + padding * 2 + const height = nodesBounds.maxY - nodesBounds.minY + padding * 2 + + const dataUrl = await toPng(flowElement, { + backgroundColor: '#ffffff', + width, + height, + style: { + width: `${width}px`, + height: `${height}px`, + transform: `translate(${-nodesBounds.minX + padding}px, ${-nodesBounds.minY + padding}px)`, + }, + }) + + let finalDataUrl = dataUrl + + if (options?.watermarkLogo) { + const canvas = await dataUrlToCanvas(dataUrl) + const watermarkedCanvas = await addWatermarkToCanvas( + canvas, + options.watermarkLogo, + options.watermarkPosition ?? 'top-left', + ) + finalDataUrl = watermarkedCanvas.toDataURL('image/png') + } + + const orientation = width > height ? 'landscape' : 'portrait' + const pdf = new jsPDF({ + orientation, + unit: 'px', + format: [width, height], + }) + + pdf.addImage(finalDataUrl, 'PNG', 0, 0, width, height) + pdf.save('erd-diagram.pdf') + + return ok(undefined) + }, + [getFlowElement, getNodes], + ) + + return { + exportToPng, + exportToPdf, + } +} diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/index.ts b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/index.ts index f6f2ada83f..127fae2e00 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/index.ts +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/index.ts @@ -1,2 +1,3 @@ export * from './AppBar' +export type { WatermarkPosition } from './ExportDropdown' export * from './types' diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/types.ts b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/types.ts index 4a95a03833..a60a67b1a8 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/types.ts +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/types.ts @@ -1,4 +1,5 @@ import type { ReactNode } from 'react' +import type { WatermarkPosition } from './ExportDropdown' export type HelpMenuItem = { label: string @@ -32,6 +33,8 @@ export type AppBarConfig = { } export?: { show?: boolean + printExportLogo?: string + printExportLogoPosition?: WatermarkPosition } copyLink?: { show?: boolean diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ERDRenderer.module.css b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ERDRenderer.module.css index 4ae12f1aff..d8b5b1e7e0 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ERDRenderer.module.css +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ERDRenderer.module.css @@ -42,6 +42,16 @@ pointer-events: none; } +.toolbarLeft { + justify-content: start; + padding-left: 1rem; +} + +.toolbarRight { + justify-content: end; + padding-right: 1rem; +} + /* NOTE: Make the toolbar itself clickable */ .toolbarWrapper > * { pointer-events: auto; diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ErdRenderer.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ErdRenderer.tsx index 1aa92a22fb..bd857c401b 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ErdRenderer.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ErdRenderer.tsx @@ -9,6 +9,7 @@ import { ToastProvider, } from '@dlh/erd-viewer-ui' import { ReactFlowProvider } from '@xyflow/react' +import clsx from 'clsx' import { type ComponentProps, createRef, @@ -40,6 +41,8 @@ import { RelationshipEdgeParticleMarker } from './RelationshipEdgeParticleMarker import { TableDetailDrawer, TableDetailDrawerRoot } from './TableDetailDrawer' import { Toolbar } from './Toolbar' +export type ToolbarAlignment = 'left' | 'center' | 'right' + type Props = { defaultSidebarOpen?: boolean | undefined errorObjects?: ComponentProps['errors'] @@ -48,6 +51,7 @@ type Props = { appBarConfig?: AppBarConfig customToolbarActions?: ReactNode showMinimap?: boolean + toolbarAlignment?: ToolbarAlignment } const SIDEBAR_COOKIE_NAME = 'sidebar:state' @@ -62,6 +66,7 @@ export const ERDRenderer: FC = ({ appBarConfig, customToolbarActions, showMinimap = false, + toolbarAlignment = 'center', }) => { const [open, setOpen] = useState(defaultSidebarOpen) const [isResizing, setIsResizing] = useState(false) @@ -185,7 +190,13 @@ export const ERDRenderer: FC = ({ )} {errorObjects.length === 0 && ( -
+
)} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57bda3eaa0..3c15b56f62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,6 +206,12 @@ importers: elkjs: specifier: 0.10.0 version: 0.10.0 + html-to-image: + specifier: 1.11.13 + version: 1.11.13 + jspdf: + specifier: 4.0.0 + version: 4.0.0 neverthrow: specifier: 8.2.0 version: 8.2.0 @@ -3005,6 +3011,9 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/react-dom@19.2.2': resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} peerDependencies: @@ -3028,6 +3037,9 @@ packages: '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -3485,6 +3497,10 @@ packages: balanced-match@2.0.0: resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3632,6 +3648,10 @@ packages: caniuse-lite@1.0.30001751: resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} engines: {node: '>=4'} @@ -3893,6 +3913,9 @@ packages: core-js-pure@3.46.0: resolution: {integrity: sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==} + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3933,6 +3956,9 @@ packages: resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} engines: {node: '>=12 || >=16'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-loader@6.11.0: resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} engines: {node: '>= 12.13.0'} @@ -4141,6 +4167,9 @@ packages: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} engines: {node: '>= 4'} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -4397,6 +4426,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} @@ -4425,6 +4457,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -4763,6 +4798,9 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html-to-image@1.11.13: + resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} + html-webpack-plugin@5.6.4: resolution: {integrity: sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==} engines: {node: '>=10.13.0'} @@ -4775,6 +4813,10 @@ packages: webpack: optional: true + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} @@ -4907,6 +4949,9 @@ packages: resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} engines: {node: '>=12.0.0'} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} @@ -5164,6 +5209,9 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jspdf@4.0.0: + resolution: {integrity: sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -5999,6 +6047,9 @@ packages: pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + pg-query-emscripten@5.1.0: resolution: {integrity: sha512-H1ZWOzLRddmHuE4GZqFjjo55hA9zMiePz/WDDGANA/EnvILCJps9pcRucyGd+MFvapeYOy6TWSYz6DbtBOaxRQ==} @@ -6221,6 +6272,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -6363,6 +6417,9 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regex-parser@2.3.1: resolution: {integrity: sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==} @@ -6446,6 +6503,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -6699,6 +6760,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -6890,6 +6955,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} @@ -6912,10 +6981,12 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.5.1: resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -6946,6 +7017,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -7322,6 +7396,9 @@ packages: utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -10574,6 +10651,9 @@ snapshots: '@types/parse-json@4.0.2': {} + '@types/raf@3.4.3': + optional: true + '@types/react-dom@19.2.2(@types/react@19.2.2)': dependencies: '@types/react': 19.2.2 @@ -10595,6 +10675,9 @@ snapshots: '@types/tinycolor2@1.4.6': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/whatwg-mimetype@3.0.2': {} '@typescript-eslint/eslint-plugin@8.46.1(@typescript-eslint/parser@8.46.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': @@ -11237,6 +11320,9 @@ snapshots: balanced-match@2.0.0: {} + base64-arraybuffer@1.0.2: + optional: true + base64-js@1.5.1: {} baseline-browser-mapping@2.8.20: {} @@ -11409,6 +11495,18 @@ snapshots: caniuse-lite@1.0.30001751: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.28.4 + '@types/raf': 3.4.3 + core-js: 3.48.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + case-sensitive-paths-webpack-plugin@2.4.0: {} chai@5.3.3: @@ -11654,6 +11752,9 @@ snapshots: core-js-pure@3.46.0: {} + core-js@3.48.0: + optional: true + core-util-is@1.0.3: {} cosmiconfig@7.1.0: @@ -11720,6 +11821,11 @@ snapshots: css-functions-list@3.2.3: {} + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + css-loader@6.11.0(webpack@5.102.1(@swc/core@1.12.11)(esbuild@0.25.11)): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -11922,6 +12028,11 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + optional: true + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 @@ -12252,6 +12363,12 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + fast-safe-stringify@2.1.1: {} fast-uri@3.1.0: {} @@ -12274,6 +12391,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.8.2: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -12694,6 +12813,8 @@ snapshots: html-tags@3.3.1: {} + html-to-image@1.11.13: {} + html-webpack-plugin@5.6.4(webpack@5.102.1(@swc/core@1.12.11)(esbuild@0.25.11)): dependencies: '@types/html-minifier-terser': 6.1.0 @@ -12714,6 +12835,12 @@ snapshots: optionalDependencies: webpack: 5.102.1(@swc/core@1.12.11) + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + htmlparser2@6.1.0: dependencies: domelementtype: 2.3.0 @@ -12889,6 +13016,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + iobuffer@5.4.0: {} + ip-address@10.0.1: {} is-arguments@1.2.0: @@ -13111,6 +13240,17 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jspdf@4.0.0: + dependencies: + '@babel/runtime': 7.28.4 + fast-png: 6.4.0 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.48.0 + dompurify: 3.3.1 + html2canvas: 1.4.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -13971,6 +14111,9 @@ snapshots: pend@1.2.0: {} + performance-now@2.1.0: + optional: true + pg-query-emscripten@5.1.0: {} picocolors@1.0.0: {} @@ -14175,6 +14318,11 @@ snapshots: queue-microtask@1.2.3: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -14346,6 +14494,9 @@ snapshots: regenerate@1.4.2: {} + regenerator-runtime@0.13.11: + optional: true + regex-parser@2.3.1: {} regexp-tree@0.1.27: {} @@ -14433,6 +14584,9 @@ snapshots: reusify@1.1.0: {} + rgbcolor@1.0.1: + optional: true + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -14726,6 +14880,9 @@ snapshots: stackback@0.0.2: {} + stackblur-canvas@2.7.0: + optional: true + stackframe@1.3.4: {} stat-mode@0.3.0: {} @@ -14972,6 +15129,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pathdata@6.0.3: + optional: true + svg-tags@1.0.0: {} swap-case@1.1.2: @@ -15066,6 +15226,11 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -15425,6 +15590,11 @@ snapshots: utila@0.4.0: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + v8-compile-cache-lib@3.0.1: {} valibot@1.1.0(typescript@5.9.3): From b90586e6953cef44381cc0704d68b4bcc158a4c9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:59:09 +0000 Subject: [PATCH 2/4] fix: use Result.match for proper error handling in export functions Co-Authored-By: Christian Screen --- .../AppBar/ExportDropdown/ExportDropdown.tsx | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportDropdown.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportDropdown.tsx index e873c98ae0..4f060c9a54 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportDropdown.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportDropdown.tsx @@ -111,47 +111,53 @@ export const ExportDropdown: FC = () => { } const handleExportPng = async () => { - try { - await exportToPng({ - watermarkLogo: exportConfig.printExportLogo, - watermarkPosition: exportConfig.printExportLogoPosition, - }) - toast({ - title: 'PNG exported!', - description: 'Diagram has been exported as PNG', - status: 'success', - }) - } catch (error) { - console.error('Failed to export PNG:', error) - toast({ - title: 'Export failed', - description: - error instanceof Error ? error.message : 'Failed to export PNG', - status: 'error', - }) - } + const result = await exportToPng({ + watermarkLogo: exportConfig.printExportLogo, + watermarkPosition: exportConfig.printExportLogoPosition, + }) + + result.match( + () => { + toast({ + title: 'PNG exported!', + description: 'Diagram has been exported as PNG', + status: 'success', + }) + }, + (error: Error) => { + console.error('Failed to export PNG:', error) + toast({ + title: 'Export failed', + description: error.message, + status: 'error', + }) + }, + ) } const handleExportPdf = async () => { - try { - await exportToPdf({ - watermarkLogo: exportConfig.printExportLogo, - watermarkPosition: exportConfig.printExportLogoPosition, - }) - toast({ - title: 'PDF exported!', - description: 'Diagram has been exported as PDF', - status: 'success', - }) - } catch (error) { - console.error('Failed to export PDF:', error) - toast({ - title: 'Export failed', - description: - error instanceof Error ? error.message : 'Failed to export PDF', - status: 'error', - }) - } + const result = await exportToPdf({ + watermarkLogo: exportConfig.printExportLogo, + watermarkPosition: exportConfig.printExportLogoPosition, + }) + + result.match( + () => { + toast({ + title: 'PDF exported!', + description: 'Diagram has been exported as PDF', + status: 'success', + }) + }, + (error: Error) => { + console.error('Failed to export PDF:', error) + toast({ + title: 'Export failed', + description: error.message, + status: 'error', + }) + }, + ) } return ( From 9c7750572b49f1f99a5f6fcf90d334aa80e12dfc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:04:43 +0000 Subject: [PATCH 3/4] fix: move useMemo before early return and reduce cognitive complexity in AppBar Co-Authored-By: Christian Screen --- .../components/ERDRenderer/AppBar/AppBar.tsx | 82 +++++++++++-------- .../ExportDropdown/ExportConfigContext.tsx | 2 +- .../AppBar/ExportDropdown/index.ts | 3 +- .../CommandPalettePreview/CommandPreview.tsx | 3 - 4 files changed, 52 insertions(+), 38 deletions(-) diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/AppBar.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/AppBar.tsx index 2ace4e9d99..f7b22a6295 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/AppBar.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/AppBar.tsx @@ -22,25 +22,27 @@ type Props = { config?: AppBarConfig } -export const AppBar: FC = ({ config }) => { - if (config?.show === false) { - return null - } +const getAppBarSettings = (config?: AppBarConfig) => ({ + logoUrl: config?.logo?.url ?? 'https://liambx.com', + logoImgUrl: config?.logo?.imgUrl ?? '', + logoImgHeight: config?.logo?.imgHeight ?? '', + logoText: config?.logo?.text ?? 'Liam ERD', + showLogoText: config?.logo?.showText !== false, + showSearch: config?.search?.show !== false, + showGithub: config?.github?.show !== false, + githubUrl: config?.github?.url ?? 'https://github.com/liam-hq/liam', + showAnnouncements: config?.announcements?.show !== false, + announcementsUrl: + config?.announcements?.url ?? 'https://github.com/liam-hq/liam/releases', + showHelp: config?.help?.show !== false, + helpItems: config?.help?.items, + showExport: config?.export?.show !== false, + showCopyLink: config?.copyLink?.show !== false, + copyLinkValue: config?.copyLink?.value, + hasCustomLogo: Boolean(config?.logo?.imgUrl), +}) - const logoUrl = config?.logo?.url ?? 'https://liambx.com' - const logoImgUrl = config?.logo?.imgUrl ?? '' - const logoImgHeight = config?.logo?.imgHeight ?? '' - const logoText = config?.logo?.text ?? 'Liam ERD' - const showLogoText = config?.logo?.showText !== false - const showSearch = config?.search?.show !== false - const showGithub = config?.github?.show !== false - const githubUrl = config?.github?.url ?? 'https://github.com/liam-hq/liam' - const showAnnouncements = config?.announcements?.show !== false - const announcementsUrl = - config?.announcements?.url ?? 'https://github.com/liam-hq/liam/releases' - const showHelp = config?.help?.show !== false - const helpItems = config?.help?.items - const showExport = config?.export?.show !== false +export const AppBar: FC = ({ config }) => { const exportConfig = useMemo( () => ({ printExportLogo: config?.export?.printExportLogo, @@ -48,8 +50,12 @@ export const AppBar: FC = ({ config }) => { }), [config?.export?.printExportLogo, config?.export?.printExportLogoPosition], ) - const showCopyLink = config?.copyLink?.show !== false - const copyLinkValue = config?.copyLink?.value + + const settings = useMemo(() => getAppBarSettings(config), [config]) + + if (config?.show === false) { + return null + } return (
@@ -61,13 +67,17 @@ export const AppBar: FC = ({ config }) => { - {config?.logo?.imgUrl ? ( - logo + {settings.hasCustomLogo ? ( + logo ) : ( )} @@ -80,25 +90,33 @@ export const AppBar: FC = ({ config }) => {
- {showLogoText &&

{logoText}

} + {settings.showLogoText && ( +

{settings.logoText}

+ )}
- {showSearch && } - {showGithub && } - {showAnnouncements && } - {showHelp && ( - + {settings.showSearch && } + {settings.showGithub && } + {settings.showAnnouncements && ( + + )} + {settings.showHelp && ( + )}
- {showExport && ( + {settings.showExport && ( )} - {showCopyLink && ( + {settings.showCopyLink && ( )}
diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportConfigContext.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportConfigContext.tsx index 14a325eeff..f3b00ceee8 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportConfigContext.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/ExportConfigContext.tsx @@ -1,7 +1,7 @@ import { createContext, type FC, type ReactNode, useContext } from 'react' import type { WatermarkPosition } from './useExportDiagram' -export type ExportConfig = { +type ExportConfig = { printExportLogo?: string | undefined printExportLogoPosition?: WatermarkPosition | undefined } diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/index.ts b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/index.ts index da4fb53e3a..47cbfb1944 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/index.ts +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/index.ts @@ -1,4 +1,3 @@ -export type { ExportConfig } from './ExportConfigContext' -export { ExportConfigProvider, useExportConfig } from './ExportConfigContext' +export { ExportConfigProvider } from './ExportConfigContext' export * from './ExportDropdown' export type { WatermarkPosition } from './useExportDiagram' diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalettePreview/CommandPreview.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalettePreview/CommandPreview.tsx index c4853d4007..e009fdaa5d 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalettePreview/CommandPreview.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalettePreview/CommandPreview.tsx @@ -5,11 +5,8 @@ type Props = { commandName: string } - // TODO: Work on this for DLH ERD VIEWER - - // TODO: set gif or image for "Show All Table" and "Hide All Table" commands const COMMAND_VIDEO_SOURCE: Record = { 'copy link': From 603fee5a62e1b9821e91847380d9ce18b76205ad Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:07:33 +0000 Subject: [PATCH 4/4] fix: add undefined to ExportOptions type for exactOptionalPropertyTypes Co-Authored-By: Christian Screen --- .../ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts index ea37afab11..3dc651a053 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/AppBar/ExportDropdown/useExportDiagram.ts @@ -11,8 +11,8 @@ export type WatermarkPosition = | 'bottom-right' type ExportOptions = { - watermarkLogo?: string - watermarkPosition?: WatermarkPosition + watermarkLogo?: string | undefined + watermarkPosition?: WatermarkPosition | undefined } const addWatermarkToCanvas = (