mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-26 06:44:58 +00:00
refactor: modularize level 4 schema editor
This commit is contained in:
@@ -1,19 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
|
||||
import { Input } from '@/components/ui'
|
||||
import { Label } from '@/components/ui'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui'
|
||||
import { Switch } from '@/components/ui'
|
||||
import { SchemaTabs } from '@/components/schema/level4/Tabs'
|
||||
import { useSchemaLevel4 } from '@/components/schema/level4/useSchemaLevel4'
|
||||
import type { ModelSchema } from '@/lib/schema-types'
|
||||
import { Plus, Trash } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { ModelSchema, FieldSchema, FieldType } from '@/lib/schema-types'
|
||||
|
||||
interface SchemaEditorLevel4Props {
|
||||
schemas: ModelSchema[]
|
||||
@@ -21,74 +11,17 @@ interface SchemaEditorLevel4Props {
|
||||
}
|
||||
|
||||
export function SchemaEditorLevel4({ schemas, onSchemasChange }: SchemaEditorLevel4Props) {
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(
|
||||
schemas.length > 0 ? schemas[0].name : null
|
||||
)
|
||||
|
||||
const currentModel = schemas.find(s => s.name === selectedModel)
|
||||
|
||||
const handleAddModel = () => {
|
||||
const newModel: ModelSchema = {
|
||||
name: `Model_${Date.now()}`,
|
||||
label: 'New Model',
|
||||
fields: [],
|
||||
}
|
||||
onSchemasChange([...schemas, newModel])
|
||||
setSelectedModel(newModel.name)
|
||||
toast.success('Model created')
|
||||
}
|
||||
|
||||
const handleDeleteModel = (modelName: string) => {
|
||||
onSchemasChange(schemas.filter(s => s.name !== modelName))
|
||||
if (selectedModel === modelName) {
|
||||
setSelectedModel(schemas.length > 1 ? schemas[0].name : null)
|
||||
}
|
||||
toast.success('Model deleted')
|
||||
}
|
||||
|
||||
const handleUpdateModel = (updates: Partial<ModelSchema>) => {
|
||||
if (!currentModel) return
|
||||
|
||||
onSchemasChange(
|
||||
schemas.map(s => s.name === selectedModel ? { ...s, ...updates } : s)
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddField = () => {
|
||||
if (!currentModel) return
|
||||
|
||||
const newField: FieldSchema = {
|
||||
name: `field_${Date.now()}`,
|
||||
type: 'string',
|
||||
label: 'New Field',
|
||||
required: false,
|
||||
editable: true,
|
||||
}
|
||||
|
||||
handleUpdateModel({
|
||||
fields: [...currentModel.fields, newField],
|
||||
})
|
||||
toast.success('Field added')
|
||||
}
|
||||
|
||||
const handleDeleteField = (fieldName: string) => {
|
||||
if (!currentModel) return
|
||||
|
||||
handleUpdateModel({
|
||||
fields: currentModel.fields.filter(f => f.name !== fieldName),
|
||||
})
|
||||
toast.success('Field deleted')
|
||||
}
|
||||
|
||||
const handleUpdateField = (fieldName: string, updates: Partial<FieldSchema>) => {
|
||||
if (!currentModel) return
|
||||
|
||||
handleUpdateModel({
|
||||
fields: currentModel.fields.map(f =>
|
||||
f.name === fieldName ? { ...f, ...updates } : f
|
||||
),
|
||||
})
|
||||
}
|
||||
const {
|
||||
currentModel,
|
||||
selectedModel,
|
||||
selectModel,
|
||||
handleAddField,
|
||||
handleAddModel,
|
||||
handleDeleteField,
|
||||
handleDeleteModel,
|
||||
handleUpdateField,
|
||||
handleUpdateModel,
|
||||
} = useSchemaLevel4({ schemas, onSchemasChange })
|
||||
|
||||
return (
|
||||
<div className="grid md:grid-cols-3 gap-6 h-full">
|
||||
@@ -117,7 +50,7 @@ export function SchemaEditorLevel4({ schemas, onSchemasChange }: SchemaEditorLev
|
||||
? 'bg-accent border-accent-foreground'
|
||||
: 'hover:bg-muted border-border'
|
||||
}`}
|
||||
onClick={() => setSelectedModel(schema.name)}
|
||||
onClick={() => selectModel(schema.name)}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{schema.label || schema.name}</div>
|
||||
@@ -150,179 +83,13 @@ export function SchemaEditorLevel4({ schemas, onSchemasChange }: SchemaEditorLev
|
||||
</div>
|
||||
</CardContent>
|
||||
) : (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle>Edit Model: {currentModel.label}</CardTitle>
|
||||
<CardDescription>Configure model properties and fields</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Model Name (ID)</Label>
|
||||
<Input
|
||||
value={currentModel.name}
|
||||
onChange={(e) => handleUpdateModel({ name: e.target.value })}
|
||||
placeholder="user_model"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Display Label</Label>
|
||||
<Input
|
||||
value={currentModel.label || ''}
|
||||
onChange={(e) => handleUpdateModel({ label: e.target.value })}
|
||||
placeholder="User"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Plural Label</Label>
|
||||
<Input
|
||||
value={currentModel.labelPlural || ''}
|
||||
onChange={(e) => handleUpdateModel({ labelPlural: e.target.value })}
|
||||
placeholder="Users"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Icon Name</Label>
|
||||
<Input
|
||||
value={currentModel.icon || ''}
|
||||
onChange={(e) => handleUpdateModel({ icon: e.target.value })}
|
||||
placeholder="Users"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label className="text-base">Fields</Label>
|
||||
<Button size="sm" onClick={handleAddField}>
|
||||
<Plus className="mr-2" size={16} />
|
||||
Add Field
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{currentModel.fields.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8 border border-dashed rounded-lg">
|
||||
No fields yet. Add a field to start.
|
||||
</p>
|
||||
) : (
|
||||
currentModel.fields.map((field) => (
|
||||
<Card key={field.name} className="border-2">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 flex-1">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Field Name</Label>
|
||||
<Input
|
||||
value={field.name}
|
||||
onChange={(e) =>
|
||||
handleUpdateField(field.name, { name: e.target.value })
|
||||
}
|
||||
placeholder="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Label</Label>
|
||||
<Input
|
||||
value={field.label || ''}
|
||||
onChange={(e) =>
|
||||
handleUpdateField(field.name, { label: e.target.value })
|
||||
}
|
||||
placeholder="Email Address"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Type</Label>
|
||||
<Select
|
||||
value={field.type}
|
||||
onValueChange={(value) =>
|
||||
handleUpdateField(field.name, { type: value as FieldType })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">String</SelectItem>
|
||||
<SelectItem value="text">Text</SelectItem>
|
||||
<SelectItem value="number">Number</SelectItem>
|
||||
<SelectItem value="boolean">Boolean</SelectItem>
|
||||
<SelectItem value="date">Date</SelectItem>
|
||||
<SelectItem value="datetime">DateTime</SelectItem>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="url">URL</SelectItem>
|
||||
<SelectItem value="select">Select</SelectItem>
|
||||
<SelectItem value="relation">Relation</SelectItem>
|
||||
<SelectItem value="json">JSON</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Default Value</Label>
|
||||
<Input
|
||||
value={field.default || ''}
|
||||
onChange={(e) =>
|
||||
handleUpdateField(field.name, { default: e.target.value })
|
||||
}
|
||||
placeholder="Default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteField(field.name)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={field.required || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateField(field.name, { required: checked })
|
||||
}
|
||||
/>
|
||||
<Label className="text-xs">Required</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={field.unique || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateField(field.name, { unique: checked })
|
||||
}
|
||||
/>
|
||||
<Label className="text-xs">Unique</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={field.editable !== false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateField(field.name, { editable: checked })
|
||||
}
|
||||
/>
|
||||
<Label className="text-xs">Editable</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={field.searchable || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateField(field.name, { searchable: checked })
|
||||
}
|
||||
/>
|
||||
<Label className="text-xs">Searchable</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
<SchemaTabs
|
||||
currentModel={currentModel}
|
||||
onUpdateModel={handleUpdateModel}
|
||||
onAddField={handleAddField}
|
||||
onDeleteField={handleDeleteField}
|
||||
onUpdateField={handleUpdateField}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,7 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
|
||||
<Level4Tabs
|
||||
appConfig={appConfig}
|
||||
user={user}
|
||||
nerdMode={nerdMode}
|
||||
onSchemasChange={handleSchemasChange}
|
||||
onWorkflowsChange={handleWorkflowsChange}
|
||||
|
||||
186
frontends/nextjs/src/components/schema/level4/Tabs.tsx
Normal file
186
frontends/nextjs/src/components/schema/level4/Tabs.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Button, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
|
||||
import { Input, Label } from '@/components/ui'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui'
|
||||
import { ValidationPanel } from '@/components/schema/level4/ValidationPanel'
|
||||
import type { FieldSchema, FieldType, ModelSchema } from '@/lib/schema-types'
|
||||
import { Plus, Trash } from '@phosphor-icons/react'
|
||||
|
||||
interface SchemaTabsProps {
|
||||
currentModel: ModelSchema
|
||||
onUpdateModel: (updates: Partial<ModelSchema>) => void
|
||||
onAddField: () => void
|
||||
onDeleteField: (fieldName: string) => void
|
||||
onUpdateField: (fieldName: string, updates: Partial<FieldSchema>) => void
|
||||
}
|
||||
|
||||
export function SchemaTabs({
|
||||
currentModel,
|
||||
onUpdateModel,
|
||||
onAddField,
|
||||
onDeleteField,
|
||||
onUpdateField,
|
||||
}: SchemaTabsProps) {
|
||||
const handleFieldChange = (fieldName: string, updates: Partial<FieldSchema>) => {
|
||||
onUpdateField(fieldName, updates)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle>Edit Model: {currentModel.label ?? currentModel.name}</CardTitle>
|
||||
<CardDescription>Configure model properties and fields</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<TextField
|
||||
label="Model Name (ID)"
|
||||
value={currentModel.name}
|
||||
onChange={(value) => onUpdateModel({ name: value })}
|
||||
placeholder="user_model"
|
||||
/>
|
||||
<TextField
|
||||
label="Display Label"
|
||||
value={currentModel.label || ''}
|
||||
onChange={(value) => onUpdateModel({ label: value })}
|
||||
placeholder="User"
|
||||
/>
|
||||
<TextField
|
||||
label="Plural Label"
|
||||
value={currentModel.labelPlural || ''}
|
||||
onChange={(value) => onUpdateModel({ labelPlural: value })}
|
||||
placeholder="Users"
|
||||
/>
|
||||
<TextField
|
||||
label="Icon Name"
|
||||
value={currentModel.icon || ''}
|
||||
onChange={(value) => onUpdateModel({ icon: value })}
|
||||
placeholder="users"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label className="text-base">Fields</Label>
|
||||
<Button size="sm" onClick={onAddField}>
|
||||
<Plus className="mr-2" size={16} />
|
||||
Add Field
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{currentModel.fields.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8 border border-dashed rounded-lg">
|
||||
No fields yet. Add a field to start.
|
||||
</p>
|
||||
) : (
|
||||
currentModel.fields.map((field) => (
|
||||
<FieldCard
|
||||
key={field.name}
|
||||
field={field}
|
||||
onChange={(updates) => handleFieldChange(field.name, updates)}
|
||||
onDelete={() => onDeleteField(field.name)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface FieldCardProps {
|
||||
field: FieldSchema
|
||||
onChange: (updates: Partial<FieldSchema>) => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
function FieldCard({ field, onChange, onDelete }: FieldCardProps) {
|
||||
return (
|
||||
<div className="border-2 rounded-lg">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 flex-1">
|
||||
<TextField
|
||||
label="Field Name"
|
||||
value={field.name}
|
||||
onChange={(value) => onChange({ name: value })}
|
||||
placeholder="email"
|
||||
labelClassName="text-xs"
|
||||
/>
|
||||
<TextField
|
||||
label="Label"
|
||||
value={field.label || ''}
|
||||
onChange={(value) => onChange({ label: value })}
|
||||
placeholder="Email Address"
|
||||
labelClassName="text-xs"
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Type</Label>
|
||||
<Select
|
||||
value={field.type}
|
||||
onValueChange={(value) => onChange({ type: value as FieldType })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">String</SelectItem>
|
||||
<SelectItem value="text">Text</SelectItem>
|
||||
<SelectItem value="number">Number</SelectItem>
|
||||
<SelectItem value="boolean">Boolean</SelectItem>
|
||||
<SelectItem value="date">Date</SelectItem>
|
||||
<SelectItem value="datetime">DateTime</SelectItem>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="url">URL</SelectItem>
|
||||
<SelectItem value="select">Select</SelectItem>
|
||||
<SelectItem value="relation">Relation</SelectItem>
|
||||
<SelectItem value="json">JSON</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<TextField
|
||||
label="Default Value"
|
||||
value={field.default ?? ''}
|
||||
onChange={(value) => onChange({ default: value || undefined })}
|
||||
placeholder="Default"
|
||||
labelClassName="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={onDelete}>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ValidationPanel field={field} onChange={onChange} />
|
||||
</CardContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TextFieldProps {
|
||||
label: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder?: string
|
||||
labelClassName?: string
|
||||
}
|
||||
|
||||
function TextField({ label, value, onChange, placeholder, labelClassName }: TextFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className={labelClassName}>{label}</Label>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Input, Label, Switch } from '@/components/ui'
|
||||
import type { FieldSchema } from '@/lib/schema-types'
|
||||
|
||||
interface ValidationPanelProps {
|
||||
field: FieldSchema
|
||||
onChange: (updates: Partial<FieldSchema>) => void
|
||||
}
|
||||
|
||||
const numericKeys = ['min', 'max', 'minLength', 'maxLength'] as const
|
||||
type NumericValidationKey = (typeof numericKeys)[number]
|
||||
|
||||
const labels: Record<NumericValidationKey, string> = {
|
||||
min: 'Minimum Value',
|
||||
max: 'Maximum Value',
|
||||
minLength: 'Minimum Length',
|
||||
maxLength: 'Maximum Length',
|
||||
}
|
||||
|
||||
export function ValidationPanel({ field, onChange }: ValidationPanelProps) {
|
||||
const handleNumberChange = (key: NumericValidationKey, value: string) => {
|
||||
const parsedValue = value === '' ? undefined : Number(value)
|
||||
const nextValidation = {
|
||||
...field.validation,
|
||||
[key]: Number.isNaN(parsedValue) ? undefined : parsedValue,
|
||||
}
|
||||
|
||||
onChange({ validation: nextValidation })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{numericKeys.map((key) => (
|
||||
<div key={key} className="space-y-2">
|
||||
<Label className="text-xs">{labels[key]}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.validation?.[key] ?? ''}
|
||||
onChange={(event) => handleNumberChange(key, event.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="space-y-2 lg:col-span-2">
|
||||
<Label className="text-xs">Pattern (regex)</Label>
|
||||
<Input
|
||||
value={field.validation?.pattern ?? ''}
|
||||
onChange={(event) =>
|
||||
onChange({
|
||||
validation: {
|
||||
...field.validation,
|
||||
pattern: event.target.value || undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="^\\d{3}-\\d{3}-\\d{4}$"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Toggle label="Required" checked={field.required} onCheckedChange={(checked) => onChange({ required: checked })} />
|
||||
<Toggle label="Unique" checked={field.unique} onCheckedChange={(checked) => onChange({ unique: checked })} />
|
||||
<Toggle label="Editable" checked={field.editable !== false} onCheckedChange={(checked) => onChange({ editable: checked })} />
|
||||
<Toggle label="Searchable" checked={field.searchable} onCheckedChange={(checked) => onChange({ searchable: checked })} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
label: string
|
||||
checked?: boolean
|
||||
onCheckedChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
function Toggle({ label, checked, onCheckedChange }: ToggleProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch checked={checked ?? false} onCheckedChange={onCheckedChange} />
|
||||
<Label className="text-xs">{label}</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
127
frontends/nextjs/src/components/schema/level4/useSchemaLevel4.ts
Normal file
127
frontends/nextjs/src/components/schema/level4/useSchemaLevel4.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { FieldSchema, ModelSchema } from '@/lib/schema-types'
|
||||
|
||||
interface UseSchemaLevel4Props {
|
||||
schemas: ModelSchema[]
|
||||
onSchemasChange: (schemas: ModelSchema[]) => void
|
||||
}
|
||||
|
||||
export function useSchemaLevel4({ schemas, onSchemasChange }: UseSchemaLevel4Props) {
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(schemas[0]?.name ?? null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedModel && schemas[0]) {
|
||||
setSelectedModel(schemas[0].name)
|
||||
}
|
||||
|
||||
if (selectedModel && !schemas.some(schema => schema.name === selectedModel)) {
|
||||
setSelectedModel(schemas[0]?.name ?? null)
|
||||
}
|
||||
}, [schemas, selectedModel])
|
||||
|
||||
const currentModel = useMemo(
|
||||
() => schemas.find((schema) => schema.name === selectedModel) ?? null,
|
||||
[schemas, selectedModel]
|
||||
)
|
||||
|
||||
const applyChanges = useCallback(
|
||||
(nextSchemas: ModelSchema[]) => {
|
||||
onSchemasChange(nextSchemas)
|
||||
},
|
||||
[onSchemasChange]
|
||||
)
|
||||
|
||||
const handleAddModel = useCallback(() => {
|
||||
const newModel: ModelSchema = {
|
||||
name: `Model_${Date.now()}`,
|
||||
label: 'New Model',
|
||||
fields: [],
|
||||
}
|
||||
|
||||
applyChanges([...schemas, newModel])
|
||||
setSelectedModel(newModel.name)
|
||||
toast.success('Model created')
|
||||
}, [applyChanges, schemas])
|
||||
|
||||
const handleDeleteModel = useCallback(
|
||||
(modelName: string) => {
|
||||
const updatedSchemas = schemas.filter((schema) => schema.name !== modelName)
|
||||
|
||||
applyChanges(updatedSchemas)
|
||||
if (selectedModel === modelName) {
|
||||
setSelectedModel(updatedSchemas[0]?.name ?? null)
|
||||
}
|
||||
toast.success('Model deleted')
|
||||
},
|
||||
[applyChanges, schemas, selectedModel]
|
||||
)
|
||||
|
||||
const handleUpdateModel = useCallback(
|
||||
(updates: Partial<ModelSchema>) => {
|
||||
if (!currentModel) return
|
||||
|
||||
applyChanges(
|
||||
schemas.map((schema) =>
|
||||
schema.name === currentModel.name ? { ...schema, ...updates } : schema
|
||||
)
|
||||
)
|
||||
},
|
||||
[applyChanges, currentModel, schemas]
|
||||
)
|
||||
|
||||
const handleAddField = useCallback(() => {
|
||||
if (!currentModel) return
|
||||
|
||||
const newField: FieldSchema = {
|
||||
name: `field_${Date.now()}`,
|
||||
type: 'string',
|
||||
label: 'New Field',
|
||||
required: false,
|
||||
editable: true,
|
||||
}
|
||||
|
||||
handleUpdateModel({
|
||||
fields: [...currentModel.fields, newField],
|
||||
})
|
||||
toast.success('Field added')
|
||||
}, [currentModel, handleUpdateModel])
|
||||
|
||||
const handleDeleteField = useCallback(
|
||||
(fieldName: string) => {
|
||||
if (!currentModel) return
|
||||
|
||||
handleUpdateModel({
|
||||
fields: currentModel.fields.filter((field) => field.name !== fieldName),
|
||||
})
|
||||
toast.success('Field deleted')
|
||||
},
|
||||
[currentModel, handleUpdateModel]
|
||||
)
|
||||
|
||||
const handleUpdateField = useCallback(
|
||||
(fieldName: string, updates: Partial<FieldSchema>) => {
|
||||
if (!currentModel) return
|
||||
|
||||
handleUpdateModel({
|
||||
fields: currentModel.fields.map((field) =>
|
||||
field.name === fieldName ? { ...field, ...updates } : field
|
||||
),
|
||||
})
|
||||
},
|
||||
[currentModel, handleUpdateModel]
|
||||
)
|
||||
|
||||
return {
|
||||
currentModel,
|
||||
selectedModel,
|
||||
selectModel: setSelectedModel,
|
||||
handleAddField,
|
||||
handleAddModel,
|
||||
handleDeleteField,
|
||||
handleDeleteModel,
|
||||
handleUpdateField,
|
||||
handleUpdateModel,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user