fix: labels stored as label.text now persist across page refresh

convertToExcalidrawElements silently dropped label.text on reload.
Added expandLabelsToNative() that pre-expands server-format labels
into native Excalidraw bound text elements before the scene is set,
ensuring labels survive DB round-trips.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
newblacc
2026-03-30 00:43:50 +02:00
co-authored by Claude Opus 4.6
parent d8ef0379f5
commit 1e535a5e31
2 changed files with 48 additions and 3 deletions
+3
View File
@@ -64,3 +64,6 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Changed
- Default theme set to dark
### Fixed
- Labels stored as label.text (e.g. from MCP updates) now survive page refresh — expandLabelsToNative pre-converts them to bound text before Excalidraw renders
+45 -3
View File
@@ -192,6 +192,45 @@ function App(): JSX.Element {
return [...convertedNonImage, ...normalizedImages]
}
// Expand server-format label.text into native Excalidraw bound text elements.
// Without this, labels stored as label.text on containers vanish on page reload
// because convertToExcalidrawElements silently drops them.
const expandLabelsToNative = (elements: any[]): any[] => {
const expanded: any[] = []
const LABEL_TYPES = new Set(['rectangle', 'ellipse', 'diamond', 'arrow'])
for (const el of elements) {
if (el.label?.text && LABEL_TYPES.has(el.type)) {
const boundTextId = `${el.id}_label`
const { label, ...rest } = el
// Container: add text binding, preserve existing non-text bindings
const existingBindings = (rest.boundElements || []).filter((b: any) => b.type !== 'text')
expanded.push({
...rest,
boundElements: [...existingBindings, { id: boundTextId, type: 'text' }]
})
// Bound text element positioned at container center
expanded.push({
id: boundTextId, type: 'text', containerId: el.id,
x: (el.x ?? 0) + ((el.width ?? 100) / 2) - 20,
y: (el.y ?? 0) + ((el.height ?? 40) / 2) - 10,
width: el.width ?? 100, height: 25, angle: 0,
text: label.text, originalText: label.text,
fontSize: el.fontSize ?? 20, fontFamily: el.fontFamily ?? 5,
textAlign: 'center', verticalAlign: 'middle',
strokeColor: el.strokeColor ?? '#1e1e1e',
backgroundColor: 'transparent', fillStyle: 'solid',
strokeWidth: 1, strokeStyle: 'solid',
roughness: el.roughness ?? 1, opacity: el.opacity ?? 100,
groupIds: [], roundness: null, isDeleted: false,
autoResize: true, lineHeight: 1.25,
})
} else {
expanded.push(el)
}
}
return expanded
}
const loadExistingElements = async (): Promise<void> => {
try {
const response = await fetch('/api/elements', { headers: tenantHeaders() })
@@ -204,12 +243,15 @@ function App(): JSX.Element {
return
}
const cleanedElements = result.elements.map(cleanElementForExcalidraw)
const hasNativeFormat = cleanedElements.some((el: any) => el.containerId)
// Expand server-format label.text into native Excalidraw bound text
// elements so labels survive round-trips through the DB.
const expandedElements = expandLabelsToNative(cleanedElements)
const hasNativeFormat = expandedElements.some((el: any) => el.containerId)
if (hasNativeFormat) {
const validated = validateAndFixBindings(cleanedElements)
const validated = validateAndFixBindings(expandedElements)
excalidrawAPI?.updateScene({ elements: validated as any })
} else {
const convertedElements = convertElementsPreservingImageProps(cleanedElements)
const convertedElements = convertElementsPreservingImageProps(expandedElements)
excalidrawAPI?.updateScene({ elements: convertedElements })
}