Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 81 additions & 69 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import ReactFlow, {
Controls,
} from 'reactflow'
import * as Toast from '@radix-ui/react-toast'
import { toPng } from 'html-to-image'
import { edgeTypes, nodeTypes } from './flowTypes.js'
import {
ConfirmDiscardDialog,
Expand All @@ -23,6 +22,7 @@ import AnnotationLayer from './components/annotations/AnnotationLayer.jsx'
import AnnotationToolbox from './components/annotations/AnnotationToolbox.jsx'
import { useAnnotations } from './hooks/useAnnotations.js'
import { sanitizeFileName } from './model/fileUtils.js'
import { exportFlowPng } from './model/pngExport.jsx'
import {
ASSOCIATION_EDGE_TYPE,
ASSOCIATIVE_EDGE_TYPE,
Expand Down Expand Up @@ -749,6 +749,60 @@ function App() {
onRedo,
})

const defaultValueEntries = useMemo(() => {
if (activeView !== VIEW_PHYSICAL) {
return []
}

return nodes.flatMap((node) => {
const className =
typeof node.data?.label === 'string' && node.data.label.trim()
? node.data.label.trim()
: 'Class'
const attributes = Array.isArray(node.data?.attributes)
? node.data.attributes
: []
return attributes.flatMap((attribute) => {
const value =
typeof attribute.defaultValue === 'string'
? attribute.defaultValue.trim()
: ''
if (!value) {
return []
}
const logicalName =
typeof attribute.logicalName === 'string' && attribute.logicalName.trim()
? attribute.logicalName.trim()
: ''
const attributeName =
logicalName ||
(typeof attribute.name === 'string' && attribute.name.trim()
? attribute.name.trim()
: 'attribute')
return [
{
key: `${className}.${attributeName}`,
value,
},
]
})
})
}, [activeView, nodes])

const visibleFlowNodes = useMemo(
() =>
flowNodes.filter((node) => {
if (node.type === NOTE_NODE_TYPE) {
return showNotes
}
if (node.type === AREA_NODE_TYPE) {
return showAreas
}
return true
}),
[flowNodes, showAreas, showNotes],
)

const onExportPng = useCallback(async () => {
if (!reactFlowWrapper.current) {
return
Expand All @@ -760,21 +814,23 @@ function App() {
return
}

const imageWidth = Math.round(containerRect.width)
const imageHeight = Math.round(containerRect.height)

const backgroundColor = '#ffffff'
const fallbackWidth = Math.round(containerRect.width)
const fallbackHeight = Math.round(containerRect.height)

try {
const dataUrl = await toPng(container, {
backgroundColor,
filter: (node) =>
!(node instanceof Element) ||
(!node.closest('[data-no-export="true"]') &&
!node.closest('.react-flow__background') &&
(includeAccentColorsInExport || node.dataset.accentBar !== 'true')),
width: imageWidth,
height: imageHeight,
const dataUrl = await exportFlowPng({
nodes: visibleFlowNodes,
edges: flowEdges,
nodeTypes,
edgeTypes,
annotations,
activeView,
showAnnotations,
currentStroke,
defaultValueEntries,
fallbackWidth,
fallbackHeight,
includeAccentColorsInExport,
})

const normalizedName = sanitizeFileName(modelName ?? 'Untitled model')
Expand All @@ -791,7 +847,17 @@ function App() {
} catch (error) {
console.error('Failed to export PNG', error)
}
}, [activeView, includeAccentColorsInExport, modelName])
}, [
activeView,
annotations,
currentStroke,
defaultValueEntries,
flowEdges,
includeAccentColorsInExport,
modelName,
showAnnotations,
visibleFlowNodes,
])

const onSidebarSelect = useCallback(
(item) => {
Expand Down Expand Up @@ -830,60 +896,6 @@ function App() {
],
)

const defaultValueEntries = useMemo(() => {
if (activeView !== VIEW_PHYSICAL) {
return []
}

return nodes.flatMap((node) => {
const className =
typeof node.data?.label === 'string' && node.data.label.trim()
? node.data.label.trim()
: 'Class'
const attributes = Array.isArray(node.data?.attributes)
? node.data.attributes
: []
return attributes.flatMap((attribute) => {
const value =
typeof attribute.defaultValue === 'string'
? attribute.defaultValue.trim()
: ''
if (!value) {
return []
}
const logicalName =
typeof attribute.logicalName === 'string' && attribute.logicalName.trim()
? attribute.logicalName.trim()
: ''
const attributeName =
logicalName ||
(typeof attribute.name === 'string' && attribute.name.trim()
? attribute.name.trim()
: 'attribute')
return [
{
key: `${className}.${attributeName}`,
value,
},
]
})
})
}, [activeView, nodes])

const visibleFlowNodes = useMemo(
() =>
flowNodes.filter((node) => {
if (node.type === NOTE_NODE_TYPE) {
return showNotes
}
if (node.type === AREA_NODE_TYPE) {
return showAreas
}
return true
}),
[flowNodes, showAreas, showNotes],
)

return (
<Toast.Provider duration={6500} swipeDirection="right">
<div className="h-screen overflow-hidden bg-base-200 text-base-content">
Expand Down
108 changes: 57 additions & 51 deletions src/components/flow/nodes/Class.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,60 +109,66 @@ export function Class({ data, id, selected }) {
return (
<div
ref={nodeRef}
className={`group/node min-w-[180px] rounded-lg border-2 bg-base-100 text-base-content shadow-sm hover:border-primary ${borderClass}`}
className="group/node min-w-[180px] text-base-content"
>
<div
data-accent-bar="true"
className="h-2 rounded-t-[6px]"
style={{ backgroundColor: showAccentColors ? accentColor : 'transparent' }}
/>
<div className="border-b border-base-content/70 px-3 pb-2 pt-1 text-sm font-semibold">
{data.label ?? ''}
className={`overflow-hidden rounded-lg border-2 bg-base-100 shadow-sm hover:border-primary ${borderClass}`}
>
<div
data-accent-bar="true"
className="-mx-2 h-2 rounded-t-lg"
style={{
backgroundColor: showAccentColors ? accentColor : 'transparent',
}}
/>
<div className="border-b border-base-content/70 px-3 pb-2 pt-1 text-sm font-semibold">
{data.label ?? ''}
</div>
<div className="px-3 py-2 text-xs">
{visibleAttributes.length === 0 ? (
<div className="opacity-60">No attributes</div>
) : (
<ul className="flex w-full flex-col gap-1">
{visibleAttributes.map((attr) => {
const logicalName =
typeof attr.logicalName === 'string' && attr.logicalName.trim()
? attr.logicalName
: ''
const displayName =
activeView === VIEW_CONCEPTUAL
? attr.name
: logicalName || attr.name
return (
<Attribute
key={attr.id}
attributeId={attr.id}
name={attr.name}
displayName={displayName}
type={attr.type}
typeParams={attr.typeParams}
showType={showTypeDetails}
showConstraints={showConstraints}
showDefaultMarker={showDefaultMarker}
nullDisplayMode={nullDisplayMode}
nullable={attr.nullable}
unique={attr.unique}
autoIncrement={attr.autoIncrement}
showHandles={showAttributeHandles}
columnTemplate={columnTemplate}
defaultValue={attr.defaultValue}
/>
)
})}
</ul>
)}
</div>
{showOperationsCompartment ? (
<>
<div className="border-t border-base-content/70" />
<div className="h-6 px-3" />
</>
) : null}
</div>
<div className="px-3 py-2 text-xs">
{visibleAttributes.length === 0 ? (
<div className="opacity-60">No attributes</div>
) : (
<ul className="flex w-full flex-col gap-1">
{visibleAttributes.map((attr) => {
const logicalName =
typeof attr.logicalName === 'string' && attr.logicalName.trim()
? attr.logicalName
: ''
const displayName =
activeView === VIEW_CONCEPTUAL
? attr.name
: logicalName || attr.name
return (
<Attribute
key={attr.id}
attributeId={attr.id}
name={attr.name}
displayName={displayName}
type={attr.type}
typeParams={attr.typeParams}
showType={showTypeDetails}
showConstraints={showConstraints}
showDefaultMarker={showDefaultMarker}
nullDisplayMode={nullDisplayMode}
nullable={attr.nullable}
unique={attr.unique}
autoIncrement={attr.autoIncrement}
showHandles={showAttributeHandles}
columnTemplate={columnTemplate}
defaultValue={attr.defaultValue}
/>
)
})}
</ul>
)}
</div>
{showOperationsCompartment ? (
<>
<div className="border-t border-base-content/70" />
<div className="h-6 px-3" />
</>
) : null}
{showHandles ? (
<>
<ClassHandle
Expand Down
2 changes: 1 addition & 1 deletion src/content/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ MySQL importer notes:
- composite foreign keys are ignored

### Export as PNG
- Exports the current viewport content with a white background.
- Exports the full active view with a white background.
- Respects current view and visibility.
- Includes visible annotations when annotations are enabled.
- Includes the Default Values overlay when visible.
Expand Down
Loading