mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
refactor: extract dialog fields and hierarchy tree
This commit is contained in:
@@ -1,24 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Button } from '@/components/ui'
|
||||
import { Input } from '@/components/ui'
|
||||
import { Label } from '@/components/ui'
|
||||
import { Textarea } from '@/components/ui'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
|
||||
import { Switch } from '@/components/ui'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
|
||||
import { ScrollArea } from '@/components/ui'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui'
|
||||
import { Database, ComponentNode, ComponentConfig } from '@/lib/database'
|
||||
import { componentCatalog } from '@/lib/components/component-catalog'
|
||||
import { toast } from 'sonner'
|
||||
import type { PropDefinition } from '@/lib/builder-types'
|
||||
|
||||
/** Select option type for property schema options */
|
||||
interface SelectOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
import { ComponentConfigActions } from './ComponentConfigDialog/Actions'
|
||||
import { ComponentConfigFields } from './ComponentConfigDialog/Fields'
|
||||
|
||||
interface ComponentConfigDialogProps {
|
||||
node: ComponentNode
|
||||
@@ -74,65 +60,6 @@ export function ComponentConfigDialog({ node, isOpen, onClose, onSave, nerdMode
|
||||
|
||||
const componentDef = componentCatalog.find(c => c.type === node.type)
|
||||
|
||||
const renderPropEditor = (propSchema: PropDefinition) => {
|
||||
const value = props[propSchema.name] ?? propSchema.defaultValue
|
||||
|
||||
switch (propSchema.type) {
|
||||
case 'string':
|
||||
return (
|
||||
<Input
|
||||
value={String(value || '')}
|
||||
onChange={(e) => setProps({ ...props, [propSchema.name]: e.target.value })}
|
||||
placeholder={String(propSchema.defaultValue || '')}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={String(value || '')}
|
||||
onChange={(e) => setProps({ ...props, [propSchema.name]: Number(e.target.value) })}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'boolean':
|
||||
return (
|
||||
<Switch
|
||||
checked={Boolean(value)}
|
||||
onCheckedChange={(checked) => setProps({ ...props, [propSchema.name]: checked })}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
value={String(value || propSchema.defaultValue || '')}
|
||||
onValueChange={(val) => setProps({ ...props, [propSchema.name]: val })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{propSchema.options?.map((opt: SelectOption) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
value={String(value || '')}
|
||||
onChange={(e) => setProps({ ...props, [propSchema.name]: e.target.value })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh]">
|
||||
@@ -143,147 +70,18 @@ export function ComponentConfigDialog({ node, isOpen, onClose, onSave, nerdMode
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="props" className="flex-1">
|
||||
<TabsList className={nerdMode ? "grid w-full grid-cols-3" : "grid w-full grid-cols-2"}>
|
||||
<TabsTrigger value="props">Properties</TabsTrigger>
|
||||
<TabsTrigger value="styles">Styles</TabsTrigger>
|
||||
{nerdMode && <TabsTrigger value="events">Events</TabsTrigger>}
|
||||
</TabsList>
|
||||
<ComponentConfigFields
|
||||
componentDef={componentDef}
|
||||
props={props}
|
||||
setProps={setProps}
|
||||
styles={styles}
|
||||
setStyles={setStyles}
|
||||
events={events}
|
||||
setEvents={setEvents}
|
||||
nerdMode={nerdMode}
|
||||
/>
|
||||
|
||||
<ScrollArea className="h-[500px] mt-4">
|
||||
<TabsContent value="props" className="space-y-4 px-1">
|
||||
{componentDef?.propSchema && componentDef.propSchema.length > 0 ? (
|
||||
componentDef.propSchema.map((propSchema) => (
|
||||
<div key={propSchema.name} className="space-y-2">
|
||||
<Label htmlFor={propSchema.name}>{propSchema.label}</Label>
|
||||
{renderPropEditor(propSchema)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>No configurable properties for this component</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nerdMode && (
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Custom Properties (JSON)</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Add additional props as JSON
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={JSON.stringify(props, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
setProps(JSON.parse(e.target.value))
|
||||
} catch {
|
||||
// Ignore invalid JSON during typing
|
||||
}
|
||||
}}
|
||||
className="font-mono text-xs"
|
||||
rows={6}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="styles" className="space-y-4 px-1">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="className">Tailwind Classes</Label>
|
||||
<Input
|
||||
id="className"
|
||||
value={String(props.className || '')}
|
||||
onChange={(e) => setProps({ ...props, className: e.target.value })}
|
||||
placeholder="p-4 bg-white rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{nerdMode && (
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Custom Styles (CSS-in-JS)</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Define inline styles as JSON object
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={JSON.stringify(styles, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
setStyles(JSON.parse(e.target.value))
|
||||
} catch {
|
||||
// Ignore invalid JSON during typing
|
||||
}
|
||||
}}
|
||||
className="font-mono text-xs"
|
||||
rows={12}
|
||||
placeholder='{\n "backgroundColor": "#fff",\n "padding": "16px"\n}'
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{nerdMode && (
|
||||
<TabsContent value="events" className="space-y-4 px-1">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Event Handlers</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Map events to Lua script IDs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{['onClick', 'onChange', 'onSubmit', 'onFocus', 'onBlur'].map((eventName) => (
|
||||
<div key={eventName} className="space-y-2">
|
||||
<Label htmlFor={eventName}>{eventName}</Label>
|
||||
<Input
|
||||
id={eventName}
|
||||
value={events[eventName] || ''}
|
||||
onChange={(e) => setEvents({ ...events, [eventName]: e.target.value })}
|
||||
placeholder="lua_script_id"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Custom Events (JSON)</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Define additional event handlers
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={JSON.stringify(events, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
setEvents(JSON.parse(e.target.value))
|
||||
} catch {
|
||||
// Ignore invalid JSON during typing
|
||||
}
|
||||
}}
|
||||
className="font-mono text-xs"
|
||||
rows={6}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={() => void handleSave()}>Save Configuration</Button>
|
||||
</DialogFooter>
|
||||
<ComponentConfigActions onClose={onClose} onSave={handleSave} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Button } from '@/components/ui'
|
||||
import { DialogFooter } from '@/components/ui'
|
||||
|
||||
interface ComponentConfigActionsProps {
|
||||
onClose: () => void
|
||||
onSave: () => Promise<void> | void
|
||||
}
|
||||
|
||||
export function ComponentConfigActions({ onClose, onSave }: ComponentConfigActionsProps) {
|
||||
return (
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => void onSave()}>
|
||||
Save Configuration
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import { Input } from '@/components/ui'
|
||||
import { Label } from '@/components/ui'
|
||||
import { Textarea } from '@/components/ui'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
|
||||
import { Switch } from '@/components/ui'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
|
||||
import { ScrollArea } from '@/components/ui'
|
||||
import type { ComponentDefinition, PropDefinition } from '@/lib/components/types'
|
||||
|
||||
interface SelectOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface ComponentConfigFieldsProps {
|
||||
componentDef?: ComponentDefinition
|
||||
props: Record<string, unknown>
|
||||
setProps: (value: Record<string, unknown>) => void
|
||||
styles: Record<string, unknown>
|
||||
setStyles: (value: Record<string, unknown>) => void
|
||||
events: Record<string, string>
|
||||
setEvents: (value: Record<string, string>) => void
|
||||
nerdMode: boolean
|
||||
}
|
||||
|
||||
function renderPropEditor(
|
||||
propSchema: PropDefinition,
|
||||
props: Record<string, unknown>,
|
||||
setProps: (value: Record<string, unknown>) => void
|
||||
) {
|
||||
const value = props[propSchema.name] ?? propSchema.defaultValue
|
||||
|
||||
switch (propSchema.type) {
|
||||
case 'string':
|
||||
return (
|
||||
<Input
|
||||
value={String(value || '')}
|
||||
onChange={(e) => setProps({ ...props, [propSchema.name]: e.target.value })}
|
||||
placeholder={String(propSchema.defaultValue || '')}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={String(value || '')}
|
||||
onChange={(e) => setProps({ ...props, [propSchema.name]: Number(e.target.value) })}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'boolean':
|
||||
return (
|
||||
<Switch
|
||||
checked={Boolean(value)}
|
||||
onCheckedChange={(checked) => setProps({ ...props, [propSchema.name]: checked })}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
value={String(value || propSchema.defaultValue || '')}
|
||||
onValueChange={(val) => setProps({ ...props, [propSchema.name]: val })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{propSchema.options?.map((opt: SelectOption) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
value={String(value || '')}
|
||||
onChange={(e) => setProps({ ...props, [propSchema.name]: e.target.value })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function ComponentConfigFields({
|
||||
componentDef,
|
||||
props,
|
||||
setProps,
|
||||
styles,
|
||||
setStyles,
|
||||
events,
|
||||
setEvents,
|
||||
nerdMode,
|
||||
}: ComponentConfigFieldsProps) {
|
||||
return (
|
||||
<Tabs defaultValue="props" className="flex-1">
|
||||
<TabsList className={nerdMode ? "grid w-full grid-cols-3" : "grid w-full grid-cols-2"}>
|
||||
<TabsTrigger value="props">Properties</TabsTrigger>
|
||||
<TabsTrigger value="styles">Styles</TabsTrigger>
|
||||
{nerdMode && <TabsTrigger value="events">Events</TabsTrigger>}
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea className="h-[500px] mt-4">
|
||||
<TabsContent value="props" className="space-y-4 px-1">
|
||||
{componentDef?.propSchema && componentDef.propSchema.length > 0 ? (
|
||||
componentDef.propSchema.map((propSchema) => (
|
||||
<div key={propSchema.name} className="space-y-2">
|
||||
<Label htmlFor={propSchema.name}>{propSchema.label}</Label>
|
||||
{renderPropEditor(propSchema, props, setProps)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>No configurable properties for this component</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nerdMode && (
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Custom Properties (JSON)</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Add additional props as JSON
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={JSON.stringify(props, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
setProps(JSON.parse(e.target.value))
|
||||
} catch {
|
||||
// Ignore invalid JSON during typing
|
||||
}
|
||||
}}
|
||||
className="font-mono text-xs"
|
||||
rows={6}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="styles" className="space-y-4 px-1">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="className">Tailwind Classes</Label>
|
||||
<Input
|
||||
id="className"
|
||||
value={String(props.className || '')}
|
||||
onChange={(e) => setProps({ ...props, className: e.target.value })}
|
||||
placeholder="p-4 bg-white rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{nerdMode && (
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Custom Styles (CSS-in-JS)</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Define inline styles as JSON object
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={JSON.stringify(styles, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
setStyles(JSON.parse(e.target.value))
|
||||
} catch {
|
||||
// Ignore invalid JSON during typing
|
||||
}
|
||||
}}
|
||||
className="font-mono text-xs"
|
||||
rows={12}
|
||||
placeholder={'{\n "backgroundColor": "#fff",\n "padding": "16px"\n}'}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{nerdMode && (
|
||||
<TabsContent value="events" className="space-y-4 px-1">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Event Handlers</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Map events to Lua script IDs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{['onClick', 'onChange', 'onSubmit', 'onFocus', 'onBlur'].map((eventName) => (
|
||||
<div key={eventName} className="space-y-2">
|
||||
<Label htmlFor={eventName}>{eventName}</Label>
|
||||
<Input
|
||||
id={eventName}
|
||||
value={events[eventName] || ''}
|
||||
onChange={(e) => setEvents({ ...events, [eventName]: e.target.value })}
|
||||
placeholder="lua_script_id"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Custom Events (JSON)</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Define additional event handlers
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={JSON.stringify(events, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
setEvents(JSON.parse(e.target.value))
|
||||
} catch {
|
||||
// Ignore invalid JSON during typing
|
||||
}
|
||||
}}
|
||||
className="font-mono text-xs"
|
||||
rows={6}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { ScrollArea } from '@/components/ui'
|
||||
import { Separator } from '@/components/ui'
|
||||
import {
|
||||
ArrowsOutCardinal,
|
||||
Cursor,
|
||||
Plus,
|
||||
Tree,
|
||||
} from '@phosphor-icons/react'
|
||||
@@ -14,9 +13,10 @@ import { Database, type ComponentNode } from '@/lib/database'
|
||||
import { componentCatalog } from '@/lib/components/component-catalog'
|
||||
import { toast } from 'sonner'
|
||||
import { ComponentConfigDialog } from './ComponentConfigDialog'
|
||||
import { TreeNode } from './modules/TreeNode'
|
||||
import { useHierarchyData } from './modules/useHierarchyData'
|
||||
import { useHierarchyDragDrop } from './modules/useHierarchyDragDrop'
|
||||
import { HierarchyTree } from './ComponentHierarchyEditor/Tree'
|
||||
import { selectRootNodes } from './ComponentHierarchyEditor/selectors'
|
||||
|
||||
export function ComponentHierarchyEditor({ nerdMode = false }: { nerdMode?: boolean }) {
|
||||
const { pages, selectedPageId, setSelectedPageId, hierarchy, loadHierarchy } = useHierarchyData()
|
||||
@@ -37,10 +37,7 @@ export function ComponentHierarchyEditor({ nerdMode = false }: { nerdMode?: bool
|
||||
const componentIdPrefix = useId()
|
||||
|
||||
const rootNodes = useMemo(
|
||||
() =>
|
||||
Object.values(hierarchy)
|
||||
.filter(node => node.pageId === selectedPageId && !node.parentId)
|
||||
.sort((a, b) => a.order - b.order),
|
||||
() => selectRootNodes(hierarchy, selectedPageId),
|
||||
[hierarchy, selectedPageId]
|
||||
)
|
||||
|
||||
@@ -108,50 +105,6 @@ export function ComponentHierarchyEditor({ nerdMode = false }: { nerdMode?: bool
|
||||
[hierarchy, loadHierarchy]
|
||||
)
|
||||
|
||||
const renderTree = useMemo(
|
||||
() =>
|
||||
rootNodes.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
||||
<Cursor size={48} className="mb-4" />
|
||||
<p>No components yet. Add one from the catalog!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{rootNodes.map((node) => (
|
||||
<TreeNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
hierarchy={hierarchy}
|
||||
selectedNodeId={selectedNodeId}
|
||||
expandedNodes={expandedNodes}
|
||||
onSelect={setSelectedNodeId}
|
||||
onToggle={handleToggleNode}
|
||||
onDelete={handleDeleteNode}
|
||||
onConfig={setConfigNodeId}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
draggingNodeId={draggingNodeId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
[
|
||||
expandedNodes,
|
||||
handleDeleteNode,
|
||||
handleDragOver,
|
||||
handleDragStart,
|
||||
handleDrop,
|
||||
handleToggleNode,
|
||||
hierarchy,
|
||||
rootNodes,
|
||||
selectedNodeId,
|
||||
draggingNodeId,
|
||||
setConfigNodeId,
|
||||
setSelectedNodeId,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-6 h-[calc(100vh-12rem)]">
|
||||
<div className="col-span-8 space-y-4">
|
||||
@@ -191,7 +144,20 @@ export function ComponentHierarchyEditor({ nerdMode = false }: { nerdMode?: bool
|
||||
<CardContent className="flex-1 overflow-hidden">
|
||||
<ScrollArea className="h-full pr-4">
|
||||
{selectedPageId ? (
|
||||
renderTree
|
||||
<HierarchyTree
|
||||
rootNodes={rootNodes}
|
||||
hierarchy={hierarchy}
|
||||
selectedNodeId={selectedNodeId}
|
||||
expandedNodes={expandedNodes}
|
||||
draggingNodeId={draggingNodeId}
|
||||
onSelect={setSelectedNodeId}
|
||||
onToggle={handleToggleNode}
|
||||
onDelete={handleDeleteNode}
|
||||
onConfig={setConfigNodeId}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
<p>Select a page to edit its component hierarchy</p>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Cursor } from '@phosphor-icons/react'
|
||||
import type React from 'react'
|
||||
import type { ComponentNode } from '@/lib/database'
|
||||
import { TreeNode } from '../modules/TreeNode'
|
||||
|
||||
interface HierarchyTreeProps {
|
||||
rootNodes: ComponentNode[]
|
||||
hierarchy: Record<string, ComponentNode>
|
||||
selectedNodeId: string | null
|
||||
expandedNodes: Record<string, boolean>
|
||||
draggingNodeId: string | null
|
||||
onSelect: (nodeId: string) => void
|
||||
onToggle: (nodeId: string) => void
|
||||
onDelete: (nodeId: string) => Promise<void>
|
||||
onConfig: (nodeId: string) => void
|
||||
onDragStart: (event: React.DragEvent, nodeId: string) => void
|
||||
onDragOver: (event: React.DragEvent, nodeId: string) => void
|
||||
onDrop: (event: React.DragEvent, nodeId: string) => void
|
||||
}
|
||||
|
||||
export function HierarchyTree({
|
||||
rootNodes,
|
||||
hierarchy,
|
||||
selectedNodeId,
|
||||
expandedNodes,
|
||||
draggingNodeId,
|
||||
onSelect,
|
||||
onToggle,
|
||||
onDelete,
|
||||
onConfig,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
}: HierarchyTreeProps) {
|
||||
if (rootNodes.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
||||
<Cursor size={48} className="mb-4" />
|
||||
<p>No components yet. Add one from the catalog!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{rootNodes.map((node) => (
|
||||
<TreeNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
hierarchy={hierarchy}
|
||||
selectedNodeId={selectedNodeId}
|
||||
expandedNodes={expandedNodes}
|
||||
onSelect={onSelect}
|
||||
onToggle={onToggle}
|
||||
onDelete={onDelete}
|
||||
onConfig={onConfig}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
draggingNodeId={draggingNodeId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ComponentNode } from '@/lib/database'
|
||||
|
||||
export function selectRootNodes(
|
||||
hierarchy: Record<string, ComponentNode>,
|
||||
selectedPageId: string | null
|
||||
): ComponentNode[] {
|
||||
return Object.values(hierarchy)
|
||||
.filter(node => node.pageId === selectedPageId && !node.parentId)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
Reference in New Issue
Block a user