From ecd04fa1a0fe00351756b290ca9864e015845b97 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 18:43:54 +0000 Subject: [PATCH] refactor: modularize level 4 schema editor --- .../editors/schema/SchemaEditorLevel4.tsx | 277 ++---------------- .../src/components/level/levels/Level4.tsx | 1 + .../src/components/schema/level4/Tabs.tsx | 186 ++++++++++++ .../schema/level4/ValidationPanel.tsx | 84 ++++++ .../schema/level4/useSchemaLevel4.ts | 127 ++++++++ 5 files changed, 420 insertions(+), 255 deletions(-) create mode 100644 frontends/nextjs/src/components/schema/level4/Tabs.tsx create mode 100644 frontends/nextjs/src/components/schema/level4/ValidationPanel.tsx create mode 100644 frontends/nextjs/src/components/schema/level4/useSchemaLevel4.ts diff --git a/frontends/nextjs/src/components/editors/schema/SchemaEditorLevel4.tsx b/frontends/nextjs/src/components/editors/schema/SchemaEditorLevel4.tsx index f6a56a682..7acbee432 100644 --- a/frontends/nextjs/src/components/editors/schema/SchemaEditorLevel4.tsx +++ b/frontends/nextjs/src/components/editors/schema/SchemaEditorLevel4.tsx @@ -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( - 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) => { - 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) => { - 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 (
@@ -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)} >
{schema.label || schema.name}
@@ -150,179 +83,13 @@ export function SchemaEditorLevel4({ schemas, onSchemasChange }: SchemaEditorLev
) : ( - <> - - Edit Model: {currentModel.label} - Configure model properties and fields - - -
-
- - handleUpdateModel({ name: e.target.value })} - placeholder="user_model" - /> -
-
- - handleUpdateModel({ label: e.target.value })} - placeholder="User" - /> -
-
- - handleUpdateModel({ labelPlural: e.target.value })} - placeholder="Users" - /> -
-
- - handleUpdateModel({ icon: e.target.value })} - placeholder="Users" - /> -
-
- -
-
- - -
- -
- {currentModel.fields.length === 0 ? ( -

- No fields yet. Add a field to start. -

- ) : ( - currentModel.fields.map((field) => ( - - -
-
-
- - - handleUpdateField(field.name, { name: e.target.value }) - } - placeholder="email" - /> -
-
- - - handleUpdateField(field.name, { label: e.target.value }) - } - placeholder="Email Address" - /> -
-
- - -
-
- - - handleUpdateField(field.name, { default: e.target.value }) - } - placeholder="Default" - /> -
-
- -
- -
-
- - handleUpdateField(field.name, { required: checked }) - } - /> - -
-
- - handleUpdateField(field.name, { unique: checked }) - } - /> - -
-
- - handleUpdateField(field.name, { editable: checked }) - } - /> - -
-
- - handleUpdateField(field.name, { searchable: checked }) - } - /> - -
-
-
-
- )) - )} -
-
-
- + )}
diff --git a/frontends/nextjs/src/components/level/levels/Level4.tsx b/frontends/nextjs/src/components/level/levels/Level4.tsx index be74a350a..ee10645e9 100644 --- a/frontends/nextjs/src/components/level/levels/Level4.tsx +++ b/frontends/nextjs/src/components/level/levels/Level4.tsx @@ -55,6 +55,7 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) { ) => void + onAddField: () => void + onDeleteField: (fieldName: string) => void + onUpdateField: (fieldName: string, updates: Partial) => void +} + +export function SchemaTabs({ + currentModel, + onUpdateModel, + onAddField, + onDeleteField, + onUpdateField, +}: SchemaTabsProps) { + const handleFieldChange = (fieldName: string, updates: Partial) => { + onUpdateField(fieldName, updates) + } + + return ( + <> + + Edit Model: {currentModel.label ?? currentModel.name} + Configure model properties and fields + + +
+ onUpdateModel({ name: value })} + placeholder="user_model" + /> + onUpdateModel({ label: value })} + placeholder="User" + /> + onUpdateModel({ labelPlural: value })} + placeholder="Users" + /> + onUpdateModel({ icon: value })} + placeholder="users" + /> +
+ +
+
+ + +
+ +
+ {currentModel.fields.length === 0 ? ( +

+ No fields yet. Add a field to start. +

+ ) : ( + currentModel.fields.map((field) => ( + handleFieldChange(field.name, updates)} + onDelete={() => onDeleteField(field.name)} + /> + )) + )} +
+
+
+ + ) +} + +interface FieldCardProps { + field: FieldSchema + onChange: (updates: Partial) => void + onDelete: () => void +} + +function FieldCard({ field, onChange, onDelete }: FieldCardProps) { + return ( +
+ +
+
+ onChange({ name: value })} + placeholder="email" + labelClassName="text-xs" + /> + onChange({ label: value })} + placeholder="Email Address" + labelClassName="text-xs" + /> +
+ + +
+ onChange({ default: value || undefined })} + placeholder="Default" + labelClassName="text-xs" + /> +
+ +
+ + +
+
+ ) +} + +interface TextFieldProps { + label: string + value: string + onChange: (value: string) => void + placeholder?: string + labelClassName?: string +} + +function TextField({ label, value, onChange, placeholder, labelClassName }: TextFieldProps) { + return ( +
+ + onChange(event.target.value)} + placeholder={placeholder} + /> +
+ ) +} diff --git a/frontends/nextjs/src/components/schema/level4/ValidationPanel.tsx b/frontends/nextjs/src/components/schema/level4/ValidationPanel.tsx new file mode 100644 index 000000000..ce8bdb6f1 --- /dev/null +++ b/frontends/nextjs/src/components/schema/level4/ValidationPanel.tsx @@ -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) => void +} + +const numericKeys = ['min', 'max', 'minLength', 'maxLength'] as const +type NumericValidationKey = (typeof numericKeys)[number] + +const labels: Record = { + 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 ( +
+
+ {numericKeys.map((key) => ( +
+ + handleNumberChange(key, event.target.value)} + placeholder="0" + /> +
+ ))} +
+ + + onChange({ + validation: { + ...field.validation, + pattern: event.target.value || undefined, + }, + }) + } + placeholder="^\\d{3}-\\d{3}-\\d{4}$" + /> +
+
+ +
+ onChange({ required: checked })} /> + onChange({ unique: checked })} /> + onChange({ editable: checked })} /> + onChange({ searchable: checked })} /> +
+
+ ) +} + +interface ToggleProps { + label: string + checked?: boolean + onCheckedChange: (value: boolean) => void +} + +function Toggle({ label, checked, onCheckedChange }: ToggleProps) { + return ( +
+ + +
+ ) +} diff --git a/frontends/nextjs/src/components/schema/level4/useSchemaLevel4.ts b/frontends/nextjs/src/components/schema/level4/useSchemaLevel4.ts new file mode 100644 index 000000000..77c6e8590 --- /dev/null +++ b/frontends/nextjs/src/components/schema/level4/useSchemaLevel4.ts @@ -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(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) => { + 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) => { + 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, + } +}