@@ -11,7 +11,7 @@ import type { DataModelEntity, DataModelField, DataModelRelationship, DesignerCa
1111import { Database , Plus , Trash2 , Link2 , Undo2 , Redo2 , Grid3X3 , ZoomIn , ZoomOut , RotateCcw , ChevronDown , ChevronRight , Copy , Clipboard , Users } from 'lucide-react' ;
1212import { clsx } from 'clsx' ;
1313import { twMerge } from 'tailwind-merge' ;
14- import { useUndoRedo } from './hooks/useUndoRedo ' ;
14+ import { useDesignerHistory } from './hooks/useDesignerHistory ' ;
1515import { useConfirmDialog } from './hooks/useConfirmDialog' ;
1616import { useMultiSelect } from './hooks/useMultiSelect' ;
1717import { useClipboard } from './hooks/useClipboard' ;
@@ -25,6 +25,14 @@ function cn(...inputs: (string | undefined | false)[]) {
2525 return twMerge ( clsx ( inputs ) ) ;
2626}
2727
28+ /** Supported data field types for the DataModel designer */
29+ const DATA_MODEL_FIELD_TYPES = [
30+ 'text' , 'number' , 'boolean' , 'date' , 'datetime' , 'uuid' ,
31+ 'email' , 'url' , 'phone' , 'json' , 'integer' , 'float' ,
32+ 'decimal' , 'currency' , 'percent' , 'textarea' , 'select' ,
33+ 'multiselect' , 'lookup' , 'attachment' , 'formula' , 'autonumber' ,
34+ ] as const ;
35+
2836/** Arrange entities in a grid layout */
2937function calculateGridAutoLayout (
3038 entities : DataModelEntity [ ] ,
@@ -88,7 +96,7 @@ export function DataModelDesigner({
8896 const containerRef = useRef < HTMLDivElement > ( null ) ;
8997
9098 // --- Undo/Redo ---
91- const undoRedo = useUndoRedo < DesignerState > (
99+ const undoRedo = useDesignerHistory < DesignerState > (
92100 { entities : initialEntities , relationships : initialRelationships } ,
93101 { maxHistory : 50 } ,
94102 ) ;
@@ -131,6 +139,10 @@ export function DataModelDesigner({
131139 const [ editingField , setEditingField ] = useState < { entityId : string ; fieldIndex : number } | null > ( null ) ;
132140 const [ editingFieldValue , setEditingFieldValue ] = useState ( '' ) ;
133141
142+ // --- Inline entity label editing ---
143+ const [ editingEntityLabel , setEditingEntityLabel ] = useState < string | null > ( null ) ;
144+ const [ editingEntityLabelValue , setEditingEntityLabelValue ] = useState ( '' ) ;
145+
134146 // --- Drag state ---
135147 const dragRef = useRef < { entityId : string ; offsetX : number ; offsetY : number } | null > ( null ) ;
136148
@@ -305,6 +317,58 @@ export function DataModelDesigner({
305317 setEditingField ( null ) ;
306318 } , [ ] ) ;
307319
320+ // --- Inline entity label editing ---
321+ const handleStartEntityLabelEdit = useCallback (
322+ ( entityId : string , currentLabel : string ) => {
323+ if ( readOnly ) return ;
324+ setEditingEntityLabel ( entityId ) ;
325+ setEditingEntityLabelValue ( currentLabel ) ;
326+ } ,
327+ [ readOnly ] ,
328+ ) ;
329+
330+ const handleCommitEntityLabelEdit = useCallback ( ( ) => {
331+ if ( ! editingEntityLabel ) return ;
332+ const trimmed = editingEntityLabelValue . trim ( ) ;
333+ if ( trimmed === '' ) {
334+ setEditingEntityLabel ( null ) ;
335+ return ;
336+ }
337+ const current = undoRedo . current ;
338+ const next : DesignerState = {
339+ entities : current . entities . map ( ( e ) =>
340+ e . id === editingEntityLabel ? { ...e , label : trimmed } : e ,
341+ ) ,
342+ relationships : current . relationships ,
343+ } ;
344+ pushState ( next ) ;
345+ setEditingEntityLabel ( null ) ;
346+ } , [ editingEntityLabel , editingEntityLabelValue , undoRedo , pushState ] ) ;
347+
348+ const handleCancelEntityLabelEdit = useCallback ( ( ) => {
349+ setEditingEntityLabel ( null ) ;
350+ } , [ ] ) ;
351+
352+ // --- Field type change ---
353+ const handleFieldTypeChange = useCallback (
354+ ( entityId : string , fieldIndex : number , newType : string ) => {
355+ if ( readOnly ) return ;
356+ const current = undoRedo . current ;
357+ const next : DesignerState = {
358+ entities : current . entities . map ( ( e ) => {
359+ if ( e . id !== entityId ) return e ;
360+ const fields = e . fields . map ( ( f , i ) =>
361+ i === fieldIndex ? { ...f , type : newType } : f ,
362+ ) ;
363+ return { ...e , fields } ;
364+ } ) ,
365+ relationships : current . relationships ,
366+ } ;
367+ pushState ( next ) ;
368+ } ,
369+ [ readOnly , undoRedo , pushState ] ,
370+ ) ;
371+
308372 // --- Drag to reposition ---
309373 const handleDragStart = useCallback (
310374 ( e : React . DragEvent , entityId : string ) => {
@@ -411,6 +475,7 @@ export function DataModelDesigner({
411475 } else if ( e . key === 'Escape' ) {
412476 multiSelect . clearSelection ( ) ;
413477 setEditingField ( null ) ;
478+ setEditingEntityLabel ( null ) ;
414479 }
415480 } ;
416481 el . addEventListener ( 'keydown' , handleKeyDown ) ;
@@ -654,7 +719,34 @@ export function DataModelDesigner({
654719 style = { { backgroundColor : entity . color ?? 'hsl(var(--primary) / 0.1)' } }
655720 >
656721 < Database className = "h-3.5 w-3.5" />
657- < span className = "truncate" > { entity . label } </ span >
722+ { editingEntityLabel === entity . id ? (
723+ < input
724+ type = "text"
725+ value = { editingEntityLabelValue }
726+ onChange = { ( e ) => setEditingEntityLabelValue ( e . target . value ) }
727+ onBlur = { handleCommitEntityLabelEdit }
728+ onKeyDown = { ( e ) => {
729+ if ( e . key === 'Enter' ) handleCommitEntityLabelEdit ( ) ;
730+ if ( e . key === 'Escape' ) handleCancelEntityLabelEdit ( ) ;
731+ } }
732+ className = "text-sm font-medium px-1 py-0 border rounded bg-background w-32 focus:outline-none focus:ring-1 focus:ring-primary"
733+ autoFocus
734+ onClick = { ( e ) => e . stopPropagation ( ) }
735+ data-testid = { `entity-label-input-${ entity . id } ` }
736+ />
737+ ) : (
738+ < span
739+ className = "truncate cursor-text"
740+ onDoubleClick = { ( e ) => {
741+ e . stopPropagation ( ) ;
742+ handleStartEntityLabelEdit ( entity . id , entity . label ) ;
743+ } }
744+ title = "Double-click to edit label"
745+ data-testid = { `entity-label-${ entity . id } ` }
746+ >
747+ { entity . label }
748+ </ span >
749+ ) }
658750 { ! readOnly && (
659751 < button
660752 onClick = { ( e ) => {
@@ -709,7 +801,24 @@ export function DataModelDesigner({
709801 { field . name }
710802 </ span >
711803 ) }
712- < span className = "text-muted-foreground ml-auto" > { field . type } </ span >
804+ { ! readOnly ? (
805+ < select
806+ value = { field . type }
807+ onChange = { ( e ) => {
808+ e . stopPropagation ( ) ;
809+ handleFieldTypeChange ( entity . id , fieldIndex , e . target . value ) ;
810+ } }
811+ onClick = { ( e ) => e . stopPropagation ( ) }
812+ className = "text-xs text-muted-foreground ml-auto bg-transparent border-none focus:ring-1 focus:ring-primary rounded cursor-pointer p-0"
813+ data-testid = { `field-type-${ entity . id } -${ fieldIndex } ` }
814+ >
815+ { DATA_MODEL_FIELD_TYPES . map ( ( t ) => (
816+ < option key = { t } value = { t } > { t } </ option >
817+ ) ) }
818+ </ select >
819+ ) : (
820+ < span className = "text-muted-foreground ml-auto" > { field . type } </ span >
821+ ) }
713822 { field . required && < span className = "text-destructive" > *</ span > }
714823 </ div >
715824 ) ) }
0 commit comments