Geliştirme Kiti düzenlemesi

This commit is contained in:
Sedat Öztürk 2025-10-31 00:38:51 +03:00
parent 39af771d91
commit 93578c49a6
9 changed files with 735 additions and 620 deletions

View file

@ -10659,9 +10659,27 @@
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKit.ComponentEditor.Dependencies", "key": "App.DeveloperKit.ComponentEditor.Description",
"en": "Dependencies", "en": "Description",
"tr": "Bağımlılıklar" "tr": "Açıklama"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.ComponentEditor.Title.Edit",
"en": "Edit Component",
"tr": "Bileşeni Düzenle"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.ComponentEditor.Title.Create",
"en": "Create Component",
"tr": "Bileşen Oluştur"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.ComponentEditor.Active",
"en": "Active",
"tr": "Aktif"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",

View file

@ -47,7 +47,7 @@ const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, clas
} }
return ( return (
<div className={`p-4 bg-white border rounded ${className}`}> <div className={`bg-white ${className}`}>
<DynamicRenderer componentName={componentName} dependencies={dependencies} /> <DynamicRenderer componentName={componentName} dependencies={dependencies} />
</div> </div>
) )

View file

@ -366,10 +366,10 @@ const ApiManager: React.FC = () => {
} }
return ( return (
<div className="space-y-8"> <div className="space-y-4">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-4">
<div> <div>
<h1 className="text-3xl font-bold text-slate-900 mb-2"> <h1 className="text-2xl font-bold text-slate-900">
{translate('::App.DeveloperKit.Endpoint.Title')} {translate('::App.DeveloperKit.Endpoint.Title')}
</h1> </h1>
<p className="text-slate-600"> <p className="text-slate-600">

View file

@ -1,12 +1,23 @@
import React, { useState, useEffect, useCallback } from 'react' import React, { useState, useEffect, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { useComponents } from '../../contexts/ComponentContext' import { useComponents } from '../../contexts/ComponentContext'
import { FaRegSave, FaArrowLeft, FaExclamationCircle, FaSync } from 'react-icons/fa' import {
FaRegSave,
FaArrowLeft,
FaExclamationCircle,
FaSync,
FaCode,
FaEye,
FaCog,
} from 'react-icons/fa'
import { parseReactCode } from '../../utils/codeParser' import { parseReactCode } from '../../utils/codeParser'
import ComponentPreview from '../../components/componentEditor/ComponentPreview' import ComponentPreview from '../../components/componentEditor/ComponentPreview'
import { EditorState } from '../../@types/componentInfo' import { EditorState } from '../../@types/componentInfo'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormItem } from '@/components/ui'
// Error tipini tanımla // Error tipini tanımla
interface ValidationError { interface ValidationError {
@ -15,6 +26,15 @@ interface ValidationError {
startLineNumber?: number startLineNumber?: number
} }
// Validation schema
const validationSchema = Yup.object({
name: Yup.string().required('Component name is required'),
description: Yup.string(),
dependencies: Yup.array().of(Yup.string()),
code: Yup.string(),
isActive: Yup.boolean(),
})
const ComponentEditor: React.FC = () => { const ComponentEditor: React.FC = () => {
const { id } = useParams() const { id } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
@ -22,13 +42,7 @@ const ComponentEditor: React.FC = () => {
const { getComponent, addComponent, updateComponent } = useComponents() const { getComponent, addComponent, updateComponent } = useComponents()
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [dependencies, setDependencies] = useState<string[]>([])
const [code, setCode] = useState('')
const [isActive, setIsActive] = useState(true)
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]) const [validationErrors, setValidationErrors] = useState<ValidationError[]>([])
const [isSaving, setIsSaving] = useState(false)
const [isLoaded, setIsLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)
const isEditing = !!id const isEditing = !!id
@ -40,6 +54,15 @@ const ComponentEditor: React.FC = () => {
selectedComponentId: null, selectedComponentId: null,
}) })
// Initial values for Formik
const [initialValues, setInitialValues] = useState({
name: '',
description: '',
dependencies: [] as string[],
code: '',
isActive: true,
})
const parseAndUpdateComponents = useCallback((code: string) => { const parseAndUpdateComponents = useCallback((code: string) => {
try { try {
const parsed = parseReactCode(code) const parsed = parseReactCode(code)
@ -58,39 +81,42 @@ const ComponentEditor: React.FC = () => {
} }
}, []) }, [])
useEffect(() => {
if (code && editorState.components?.length === 0) {
parseAndUpdateComponents(code)
}
}, [code, editorState.components?.length])
// Load existing component data - sadece edit modunda // Load existing component data - sadece edit modunda
useEffect(() => { useEffect(() => {
if (isEditing && id && !isLoaded) { if (isEditing && id && !isLoaded) {
const component = getComponent(id) const component = getComponent(id)
if (component) { if (component) {
setName(component.name)
setDescription(component.description || '')
// Parse dependencies from JSON string // Parse dependencies from JSON string
let deps: string[] = []
try { try {
const deps = component.dependencies ? JSON.parse(component.dependencies) : [] deps = component.dependencies ? JSON.parse(component.dependencies) : []
setDependencies(Array.isArray(deps) ? deps : []) deps = Array.isArray(deps) ? deps : []
} catch { } catch {
setDependencies([]) deps = []
} }
setCode(component.code) // Mevcut kodu yükle
setIsActive(component.isActive) const values = {
name: component.name,
description: component.description || '',
dependencies: deps,
code: component.code,
isActive: component.isActive,
}
setInitialValues(values)
parseAndUpdateComponents(component.code)
setIsLoaded(true) setIsLoaded(true)
} }
} else if (!isEditing && !isLoaded) { } else if (!isEditing && !isLoaded) {
// Yeni komponent için boş başla - TEMPLATE YOK // Yeni komponent için boş başla - TEMPLATE YOK
setIsLoaded(true) setIsLoaded(true)
} }
}, [id, isEditing, getComponent, isLoaded]) }, [id, isEditing, getComponent, isLoaded, parseAndUpdateComponents])
const handleSave = async () => { const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
if (!name.trim()) { if (!values.name.trim()) {
alert('Please enter a component name') alert('Please enter a component name')
setSubmitting(false)
return return
} }
@ -109,18 +135,19 @@ const ComponentEditor: React.FC = () => {
const proceed = window.confirm( const proceed = window.confirm(
`There are ${criticalErrors.length} critical error(s) in your code. Do you want to save anyway?`, `There are ${criticalErrors.length} critical error(s) in your code. Do you want to save anyway?`,
) )
if (!proceed) return if (!proceed) {
setSubmitting(false)
return
}
} }
setIsSaving(true)
try { try {
const componentData = { const componentData = {
name: name.trim(), name: values.name.trim(),
description: description.trim(), description: values.description.trim(),
dependencies: JSON.stringify(dependencies), // Serialize dependencies to JSON string dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string
code: code.trim(), code: values.code.trim(),
isActive, isActive: values.isActive,
} }
if (isEditing && id) { if (isEditing && id) {
@ -134,7 +161,7 @@ const ComponentEditor: React.FC = () => {
console.error('Error saving component:', error) console.error('Error saving component:', error)
alert('Failed to save component. Please try again.') alert('Failed to save component. Please try again.')
} finally { } finally {
setIsSaving(false) setSubmitting(false)
} }
} }
@ -153,114 +180,181 @@ const ComponentEditor: React.FC = () => {
} }
return ( return (
<div className="h-screen flex flex-col"> <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
{/* Header */} <div className="mx-auto">
<div className="bg-white border-b border-slate-200 px-6 py-4 flex-shrink-0"> <Formik
enableReinitialize
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
<>
{/* Enhanced Header */}
<div className="bg-white shadow-lg border-b border-slate-200 sticky top-0 z-10">
<div className="px-4 py-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
type="button"
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.components)} onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.components)}
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 transition-colors" className="flex items-center gap-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 px-3 py-2 rounded-lg transition-all duration-200"
> >
<FaArrowLeft className="w-4 h-4" /> <FaArrowLeft className="w-4 h-4" />
{translate('::App.DeveloperKit.ComponentEditor.Back')} {translate('::App.DeveloperKit.ComponentEditor.Back')}
</button> </button>
<div className="h-6 w-px bg-slate-300"></div>
<div className="flex items-center gap-3">
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
<FaCode className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-slate-900">
{isEditing
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
</h1>
<p className="text-sm text-slate-600">
{isEditing
? 'Modify your React component'
: 'Create a new React component'}
</p>
</div> </div>
</div> </div>
</div> </div>
{/* Component Details */} {/* Save Button in Header */}
<div className="bg-white border-b border-slate-200 px-6 py-4 flex-shrink-0"> <div className="flex items-center gap-3">
{/* Form alanları tek satırda ve sayfaya yayılmış */}
<div className="grid grid-cols-12 gap-6">
<div className="col-span-3">
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.ComponentEditor.ComponentName')} *
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="e.g., Button, Card, Modal"
/>
</div>
<div className="col-span-6">
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.ComponentEditor.Dependencies')}
</label>
<input
type="text"
value={(dependencies || []).join(', ')}
onChange={(e) =>
setDependencies(
e.target.value
.split(',')
.map((s) => s.trim())
.filter(Boolean),
)
}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="MyComponent, AnotherComponent, etc."
/>
</div>
<div className="col-span-1">
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.ComponentEditor.Active')}
</label>
<input
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
</div>
<div className="col-span-2 flex items-center justify-end">
<button <button
onClick={handleSave} type="button"
disabled={isSaving || !name.trim()} onClick={submitForm}
className="flex items-center gap-2 bg-yellow-600 text-white px-4 py-2 rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" disabled={isSubmitting || !values.name.trim() || !isValid}
className="flex items-center gap-2 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold px-4 py-2 rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
> >
<FaRegSave className="w-4 h-4" /> <FaRegSave className="w-4 h-4" />
{isSaving {isSubmitting
? translate('::App.DeveloperKit.ComponentEditor.Saving') ? translate('::App.DeveloperKit.ComponentEditor.Saving')
: translate('::App.DeveloperKit.ComponentEditor.Save')} : translate('::App.DeveloperKit.ComponentEditor.Save')}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Main Content Area - Editor and Preview */} <Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-2">
<div className="flex-1 flex overflow-hidden"> {/* Left Side - Component Settings */}
<div className="flex-1 p-4 overflow-auto"> <div className="space-y-4 col-span-1">
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-3">
<div className="flex items-center gap-2 mb-4">
<div className="bg-blue-100 p-1.5 rounded-lg">
<FaCog className="w-4 h-4 text-blue-600" />
</div>
<h2 className="text-base font-semibold text-slate-900">Component Settings</h2>
</div>
<div className="space-y-3">
<FormItem
label={translate('::App.DeveloperKit.ComponentEditor.ComponentName')}
invalid={!!(errors.name && touched.name)}
errorMessage={errors.name as string}
>
<Field
name="name"
type="text"
placeholder="e.g., Button, Card, Modal"
className="w-full px-2 py-1.5 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 text-sm bg-slate-50 focus:bg-white"
/>
</FormItem>
<FormItem
label={translate('::App.DeveloperKit.ComponentEditor.Description')}
invalid={!!(errors.description && touched.description)}
errorMessage={errors.description as string}
>
<Field
name="description"
type="text"
placeholder="Brief description of the component"
className="w-full px-2 py-1.5 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 text-sm bg-slate-50 focus:bg-white"
/>
</FormItem>
<FormItem
label={translate('::App.DeveloperKit.ComponentEditor.Dependencies')}
invalid={!!(errors.dependencies && touched.dependencies)}
errorMessage={errors.dependencies as string}
>
<Field name="dependencies">
{({ field }: FieldProps) => (
<input
type="text"
value={(values.dependencies || []).join(', ')}
onChange={(e) =>
setFieldValue(
'dependencies',
e.target.value
.split(',')
.map((s) => s.trim())
.filter(Boolean),
)
}
className="w-full px-2 py-1.5 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 text-sm bg-slate-50 focus:bg-white"
placeholder="MyComponent, AnotherComponent, etc."
/>
)}
</Field>
</FormItem>
<FormItem>
<div className="flex items-center p-2 bg-slate-50 rounded border border-slate-200">
<Field
name="isActive"
type="checkbox"
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500 w-3.5 h-3.5"
/>
<label className="ml-2 text-xs font-medium text-slate-700">
{translate('::App.DeveloperKit.ComponentEditor.Active')}
</label>
</div>
</FormItem>
</div>
</div>
</div>
{/* Right Side - Preview and Validation */}
<div className="space-y-4 col-span-2">
{/* Validation Errors */}
{validationErrors.length > 0 && ( {validationErrors.length > 0 && (
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-4"> <div className="bg-red-50 border border-red-200 rounded-lg p-3 shadow-sm">
<div className="flex items-start gap-3"> <div className="flex items-start gap-2">
<div className="bg-red-100 rounded p-1"> <div className="bg-red-100 rounded-full p-1.5">
<FaExclamationCircle className="w-4 h-4 text-red-600" /> <FaExclamationCircle className="w-4 h-4 text-red-600" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-red-800 font-medium mb-2"> <h3 className="text-base font-semibold text-red-800 mb-1">
{validationErrors.length}{' '} Validation Issues
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')} </h3>
{validationErrors.length !== 1 ? 's' : ''}{' '} <p className="text-xs text-red-700 mb-3">
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')} {validationErrors.length} issue
{validationErrors.length !== 1 ? 's' : ''} found in your code
</p> </p>
<div className="space-y-1"> <div className="space-y-1.5 max-h-32 overflow-y-auto">
{validationErrors.slice(0, 5).map((error, index) => ( {validationErrors.slice(0, 5).map((error, index) => (
<div key={index} className="text-sm text-red-700"> <div
<span className="font-medium"> key={index}
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Line')}{' '} className="bg-white p-2 rounded border border-red-100"
{error.startLineNumber}: >
</span>{' '} <div className="text-xs text-red-800">
{error.message} <span className="font-medium bg-red-100 px-1.5 py-0.5 rounded text-xs">
Line {error.startLineNumber}
</span>
<span className="ml-2">{error.message}</span>
</div>
</div> </div>
))} ))}
{validationErrors.length > 5 && ( {validationErrors.length > 5 && (
<div className="text-sm text-red-600 italic"> <div className="text-xs text-red-600 italic text-center py-1">
... {translate('::App.DeveloperKit.ComponentEditor.ValidationError.And')}{' '} ... and {validationErrors.length - 5} more issue
{validationErrors.length - 5}{' '}
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.More')}
{validationErrors.length - 5 !== 1 ? 's' : ''} {validationErrors.length - 5 !== 1 ? 's' : ''}
</div> </div>
)} )}
@ -270,8 +364,21 @@ const ComponentEditor: React.FC = () => {
</div> </div>
)} )}
<ComponentPreview componentName={name} /> {/* Component Preview */}
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-3">
<div className="flex items-center gap-2 mb-3">
<div className="bg-purple-100 p-1.5 rounded-lg">
<FaEye className="w-4 h-4 text-purple-600" />
</div> </div>
<h2 className="text-base font-semibold text-slate-900">Preview</h2>
</div>
<ComponentPreview componentName={values.name} />
</div>
</div>
</Form>
</>
)}
</Formik>
</div> </div>
</div> </div>
) )

View file

@ -88,10 +88,10 @@ const ComponentManager: React.FC = () => {
} }
return ( return (
<div className="space-y-8"> <div className="space-y-4">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-4">
<div> <div>
<h1 className="text-3xl font-bold text-slate-900 mb-2"> <h1 className="text-2xl font-bold text-slate-900">
{translate('::App.DeveloperKit.Component.Title')} {translate('::App.DeveloperKit.Component.Title')}
</h1> </h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Component.Description')}</p> <p className="text-slate-600">{translate('::App.DeveloperKit.Component.Description')}</p>

View file

@ -162,7 +162,7 @@ const Dashboard: React.FC = () => {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-slate-900 mb-2"> <h1 className="text-2xl font-bold text-slate-900">
{translate('::App.DeveloperKit.Dashboard.Title')} {translate('::App.DeveloperKit.Dashboard.Title')}
</h1> </h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Dashboard.Description')}</p> <p className="text-slate-600">{translate('::App.DeveloperKit.Dashboard.Description')}</p>

View file

@ -7,31 +7,73 @@ import {
FaPlus, FaPlus,
FaTrashAlt, FaTrashAlt,
FaDatabase, FaDatabase,
FaQuestionCircle, FaCog,
FaTable,
FaColumns,
} from 'react-icons/fa' } from 'react-icons/fa'
import { CreateUpdateCustomEntityFieldDto, CustomEntityField } from '@/proxy/developerKit/models' import { CreateUpdateCustomEntityFieldDto, CustomEntityField } from '@/proxy/developerKit/models'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps, FieldArray } from 'formik'
import * as Yup from 'yup'
import { FormItem, Input, Select, Checkbox } from '@/components/ui'
import { SelectBoxOption } from '@/shared/types'
// Validation schema
const validationSchema = Yup.object({
name: Yup.string().required('Entity name is required'),
displayName: Yup.string().required('Display name is required'),
tableName: Yup.string().required('Table name is required'),
description: Yup.string(),
fields: Yup.array()
.of(
Yup.object({
name: Yup.string().required('Field name is required'),
type: Yup.string().required('Field type is required'),
isRequired: Yup.boolean(),
maxLength: Yup.number().nullable(),
isUnique: Yup.boolean(),
defaultValue: Yup.string(),
description: Yup.string(),
}),
)
.min(1, 'At least one field is required'),
isActive: Yup.boolean(),
hasAuditFields: Yup.boolean(),
hasSoftDelete: Yup.boolean(),
})
const EntityEditor: React.FC = () => { const EntityEditor: React.FC = () => {
const { id } = useParams() const { id } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const { translate } = useLocalization() const { translate } = useLocalization()
const { getEntity, addEntity, updateEntity, refreshEntities } = useEntities() const { getEntity, addEntity, updateEntity } = useEntities()
const [name, setName] = useState('')
const [displayName, setDisplayName] = useState('')
const [tableName, setTableName] = useState('')
const [description, setDescription] = useState('')
const [fields, setFields] = useState<CustomEntityField[]>([])
const [isActive, setIsActive] = useState(true)
const [hasAuditFields, setHasAuditFields] = useState(true)
const [hasSoftDelete, setHasSoftDelete] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const isEditing = !!id const isEditing = !!id
// Initial values for Formik
const [initialValues, setInitialValues] = useState({
name: '',
displayName: '',
tableName: '',
description: '',
fields: [
{
id: crypto.randomUUID(),
entityId: id || '',
name: 'Name',
type: 'string' as EntityFieldType,
isRequired: true,
maxLength: 100,
description: 'Entity name',
},
] as CustomEntityField[],
isActive: true,
hasAuditFields: true,
hasSoftDelete: true,
})
// Check if migration is applied to disable certain fields // Check if migration is applied to disable certain fields
const isMigrationApplied = Boolean( const isMigrationApplied = Boolean(
isEditing && id && getEntity(id)?.migrationStatus === 'applied', isEditing && id && getEntity(id)?.migrationStatus === 'applied',
@ -41,86 +83,25 @@ const EntityEditor: React.FC = () => {
if (isEditing && id) { if (isEditing && id) {
const entity = getEntity(id) const entity = getEntity(id)
if (entity) { if (entity) {
setName(entity.name) setInitialValues({
setDisplayName(entity.displayName) name: entity.name,
setTableName(entity.tableName) displayName: entity.displayName,
setDescription(entity.description || '') tableName: entity.tableName,
setFields(entity.fields) description: entity.description || '',
setIsActive(entity.isActive) fields: entity.fields,
setHasAuditFields(entity.hasAuditFields) isActive: entity.isActive,
setHasSoftDelete(entity.hasSoftDelete) hasAuditFields: entity.hasAuditFields,
hasSoftDelete: entity.hasSoftDelete,
})
} }
} else {
// Initialize with default field
setFields([
{
id: crypto.randomUUID(),
entityId: id || '',
name: 'Name',
type: 'string',
isRequired: true,
maxLength: 100,
description: 'Entity name',
},
])
} }
}, [id, isEditing, getEntity]) }, [id, isEditing, getEntity])
const addField = () => { const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
const newField: CustomEntityField = {
id: crypto.randomUUID(),
entityId: id || '',
name: '',
type: 'string',
isRequired: false,
description: '',
}
setFields((prev) => [...prev, newField])
}
const updateField = (fieldKey: string, updates: Partial<CustomEntityField>) => {
setFields((prev) =>
prev.map((field) => (field.id === fieldKey ? { ...field, ...updates } : field)),
)
}
const removeField = (fieldKey: string) => {
setFields((prev) => prev.filter((field) => field.id !== fieldKey))
}
const handleSave = async () => {
if (!name.trim()) {
alert('Please enter an entity name')
return
}
if (!displayName.trim()) {
alert('Please enter a display name')
return
}
if (!tableName.trim()) {
alert('Please enter a table name')
return
}
if (fields.length === 0) {
alert('Please add at least one field')
return
}
const invalidFields = fields.filter((f) => !f.name.trim())
if (invalidFields.length > 0) {
alert('Please fill in all field names')
return
}
setIsSaving(true)
try { try {
const sanitizedFields = fields.map((f) => { const sanitizedFields = values.fields.map((f) => {
const sanitized: CreateUpdateCustomEntityFieldDto = { const sanitized: CreateUpdateCustomEntityFieldDto = {
...(f.id && isEditing ? { id: f.id } : {}), // sadece güncelleme modunda varsa gönder ...(f.id && isEditing ? { id: f.id } : {}),
name: f.name.trim(), name: f.name.trim(),
type: f.type, type: f.type,
isRequired: f.isRequired, isRequired: f.isRequired,
@ -134,14 +115,14 @@ const EntityEditor: React.FC = () => {
}) })
const entityData = { const entityData = {
name: name.trim(), name: values.name.trim(),
displayName: displayName.trim(), displayName: values.displayName.trim(),
tableName: tableName.trim(), tableName: values.tableName.trim(),
description: description.trim(), description: values.description.trim(),
fields: sanitizedFields, fields: sanitizedFields,
isActive, isActive: values.isActive,
hasAuditFields, hasAuditFields: values.hasAuditFields,
hasSoftDelete, hasSoftDelete: values.hasSoftDelete,
} }
if (isEditing && id) { if (isEditing && id) {
@ -150,13 +131,12 @@ const EntityEditor: React.FC = () => {
await addEntity(entityData) await addEntity(entityData)
} }
// Başarılı kaydetme sonrasında Varlık Yönetimi sayfasına yönlendir
navigate(ROUTES_ENUM.protected.saas.developerKit.entities, { replace: true }) navigate(ROUTES_ENUM.protected.saas.developerKit.entities, { replace: true })
} catch (error) { } catch (error) {
console.error('Error saving entity:', error) console.error('Error saving entity:', error)
alert('Failed to save entity. Please try again.') alert('Failed to save entity. Please try again.')
} finally { } finally {
setIsSaving(false) setSubmitting(false)
} }
} }
@ -170,374 +150,384 @@ const EntityEditor: React.FC = () => {
] ]
return ( return (
<div className="min-h-screen bg-slate-50"> <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<div className="w-full"> <div className="mx-auto">
{/* Header */} <Formik
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm"> enableReinitialize
<div className="flex items-center justify-between"> initialValues={initialValues}
<div className="flex items-center gap-4"> validationSchema={validationSchema}
<button onSubmit={handleSubmit}
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.entities)}
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 transition-colors"
> >
<FaArrowLeft className="w-4 h-4" /> {({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
{translate('::App.DeveloperKit.EntityEditor.Back')} <>
</button> {/* Enhanced Header */}
<div className="h-6 w-px bg-slate-300" /> <div className="bg-white shadow border-b border-slate-200 sticky top-0 z-10">
<div className="px-3 py-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaDatabase className="w-6 h-6 text-blue-600" /> <button
<h1 className="text-xl font-semibold text-slate-900"> type="button"
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.entities)}
className="flex items-center gap-1 text-slate-600 hover:text-blue-600 hover:bg-blue-50 px-2 py-1.5 rounded transition-all duration-200"
>
<FaArrowLeft className="w-3 h-3" />
<span className="text-sm">{translate('::App.DeveloperKit.EntityEditor.Back')}</span>
</button>
<div className="h-4 w-px bg-slate-300"></div>
<div className="flex items-center gap-2">
<div className="bg-gradient-to-r from-green-500 to-blue-600 p-1 rounded">
<FaDatabase className="w-3 h-3 text-white" />
</div>
<div>
<h1 className="text-sm font-bold text-slate-900">
{isEditing {isEditing
? translate('::App.DeveloperKit.EntityEditor.Title.Edit') ? `${translate('::App.DeveloperKit.EntityEditor.Title.Edit')} - ${values.name || initialValues.name || 'Entity'}`
: translate('::App.DeveloperKit.EntityEditor.Title.Create')} : translate('::App.DeveloperKit.EntityEditor.Title.Create')}
</h1> </h1>
</div> </div>
</div> </div>
<div className="flex items-center gap-3">
{/* Help Tooltip */}
<div className="relative group">
<button className="p-2 text-slate-400 hover:text-slate-600 transition-colors">
<FaQuestionCircle className="w-4 h-4" />
</button>
<div className="absolute right-0 top-full mt-2 w-96 bg-slate-900 text-white text-sm rounded-lg p-4 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 shadow-xl">
<div className="absolute -top-1 right-4 w-2 h-2 bg-slate-900 rotate-45"></div>
<div className="flex items-center gap-2 mb-2">
<FaDatabase className="w-4 h-4 text-blue-400" />
<h4 className="font-semibold text-blue-200">
{translate('::App.DeveloperKit.EntityEditor.Tooltip.Title')}
</h4>
</div>
<p className="text-slate-300 leading-relaxed">
{translate('::App.DeveloperKit.EntityEditor.Tooltip.Content')}
</p>
</div>
</div> </div>
{/* Save Button in Header */}
<div className="flex items-center gap-2">
<button <button
onClick={handleSave} type="button"
disabled={isSaving || !name.trim() || !displayName.trim() || !tableName.trim()} onClick={submitForm}
className="flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" disabled={isSubmitting || !isValid}
className="flex items-center gap-1 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-semibold px-2 py-1.5 rounded shadow transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
> >
<FaSave className="w-4 h-4" /> <FaSave className="w-3 h-3" />
{isSaving {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
? translate('::App.DeveloperKit.EntityEditor.Saving')
: translate('::App.DeveloperKit.EntityEditor.Save')}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Entity Basic Info */} <Form className="space-y-2 pt-2">
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm"> {/* Basic Entity Information */}
<h2 className="text-lg font-semibold text-slate-900 mb-4"> <div className="bg-white rounded border border-slate-200 p-2">
{translate('::App.DeveloperKit.EntityEditor.BasicInfo')} <div className="flex items-center gap-1.5 mb-2">
</h2> <div className="bg-blue-100 p-1 rounded">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <FaCog className="w-3 h-3 text-blue-600" />
<div> </div>
<label className="block text-sm font-medium text-slate-700 mb-1"> <h2 className="text-sm font-semibold text-slate-900">Entity Settings</h2>
{translate('::App.DeveloperKit.EntityEditor.EntityName')} </div>
</label>
<input <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
autoFocus <FormItem
type="text" label={translate('::App.DeveloperKit.EntityEditor.EntityName')}
value={name} invalid={!!(errors.name && touched.name)}
onChange={(e) => setName(e.target.value)} errorMessage={errors.name as string}
onBlur={() => { >
if (!tableName) { <Field name="name">
setTableName(name + 's') {({ field }: FieldProps) => (
<Input
{...field}
onBlur={(e) => {
field.onBlur(e)
if (!values.tableName) {
setFieldValue('tableName', values.name + 's')
} }
if (!displayName) { if (!values.displayName) {
setDisplayName(name) setFieldValue('displayName', values.name)
} }
}} }}
disabled={isMigrationApplied} disabled={isMigrationApplied}
className={`w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent ${
isMigrationApplied ? 'bg-slate-100 text-slate-500 cursor-not-allowed' : ''
}`}
placeholder="e.g., Product, User, Order" placeholder="e.g., Product, User, Order"
className="px-2 py-1.5 bg-slate-50 focus:bg-white transition-all duration-200 text-sm h-7"
/> />
{isMigrationApplied && (
<p className="text-xs text-slate-500 mt-1">
{translate('::App.DeveloperKit.EntityEditor.CannotChange')}
</p>
)} )}
</div> </Field>
<div> </FormItem>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.DisplayName')} * <FormItem
</label> label={translate('::App.DeveloperKit.EntityEditor.DisplayName')}
<input invalid={!!(errors.displayName && touched.displayName)}
type="text" errorMessage={errors.displayName as string}
value={displayName} >
onChange={(e) => setDisplayName(e.target.value)} <Field
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent" name="displayName"
component={Input}
placeholder="e.g., Product, User, Order" placeholder="e.g., Product, User, Order"
className="px-2 py-1.5 bg-slate-50 focus:bg-white transition-all duration-200 text-sm h-7"
/> />
</div> </FormItem>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1"> <FormItem
{translate('::App.DeveloperKit.EntityEditor.TableName')} * label={translate('::App.DeveloperKit.EntityEditor.TableName')}
</label> invalid={!!(errors.tableName && touched.tableName)}
<input errorMessage={errors.tableName as string}
type="text" >
value={tableName} <Field
onChange={(e) => setTableName(e.target.value)} name="tableName"
component={Input}
disabled={isMigrationApplied} disabled={isMigrationApplied}
className={`w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent ${
isMigrationApplied ? 'bg-slate-100 text-slate-500 cursor-not-allowed' : ''
}`}
placeholder="e.g., Products, Users, Orders" placeholder="e.g., Products, Users, Orders"
className="px-2 py-1.5 bg-slate-50 focus:bg-white transition-all duration-200 disabled:bg-slate-100 disabled:text-slate-500 text-sm h-7"
/> />
{isMigrationApplied && ( </FormItem>
<p className="text-xs text-slate-500 mt-1">
Cannot be changed after migration is applied <FormItem
</p> label={translate('::App.DeveloperKit.EntityEditor.Description')}
)} invalid={!!(errors.description && touched.description)}
</div> errorMessage={errors.description as string}
<div className="mb-4"> >
<label className="block text-sm font-medium text-slate-700 mb-1"> <Field
{translate('::App.DeveloperKit.EntityEditor.Description')} name="description"
</label> component={Input}
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
placeholder="Brief description of this entity" placeholder="Brief description of this entity"
className="px-2 py-1.5 bg-slate-50 focus:bg-white transition-all duration-200 text-sm h-7"
/> />
</div> </FormItem>
</div> </div>
{isEditing && ( <div className="grid grid-cols-1 md:grid-cols-3 gap-2 mt-2 pt-2 border-t border-slate-200">
<div className="mb-4 p-4 bg-slate-50 rounded-lg border border-slate-200"> <div className="flex items-center p-1 bg-slate-50 rounded border border-slate-200">
<h3 className="text-sm font-medium text-slate-700 mb-3"> <Field name="isActive" component={Checkbox} className="w-3 h-3" />
{translate('::App.DeveloperKit.EntityEditor.Status')} <label className="ml-1 text-sm font-medium text-slate-700">
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">
{translate('::App.DeveloperKit.EntityEditor.Status.Migration')}
</span>
<span
className={`text-xs px-2 py-1 rounded-full ${
getEntity(id!)?.migrationStatus === 'applied'
? 'bg-green-100 text-green-700'
: getEntity(id!)?.migrationStatus === 'pending'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}
>
{getEntity(id!)?.migrationStatus || 'pending'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">
{translate('::App.DeveloperKit.EntityEditor.Status.Endpoint')}
</span>
<span
className={`text-xs px-2 py-1 rounded-full ${
getEntity(id!)?.endpointStatus === 'applied'
? 'bg-blue-100 text-blue-700'
: getEntity(id!)?.endpointStatus === 'pending'
? 'bg-orange-100 text-orange-700'
: 'bg-red-100 text-red-700'
}`}
>
{getEntity(id!)?.endpointStatus || 'pending'}
</span>
</div>
</div>
</div>
)}
<div className="flex gap-6">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500"
/>
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Active')} {translate('::App.DeveloperKit.EntityEditor.Active')}
</span>
</label> </label>
<label className="flex items-center gap-2"> </div>
<input
type="checkbox" <div className="flex items-center p-1 bg-slate-50 rounded border border-slate-200">
checked={hasAuditFields} <Field name="hasAuditFields" component={Checkbox} className="w-3 h-3" />
onChange={(e) => setHasAuditFields(e.target.checked)} <label className="ml-1 text-sm font-medium text-slate-700">
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500"
/>
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Audit')} {translate('::App.DeveloperKit.EntityEditor.Audit')}
<span className="text-slate-500 ml-1">
(creationTime, lastModificationTime, etc.)
</span>
</span>
</label> </label>
<label className="flex items-center gap-2"> </div>
<input
type="checkbox" <div className="flex items-center p-1 bg-slate-50 rounded border border-slate-200">
checked={hasSoftDelete} <Field name="hasSoftDelete" component={Checkbox} className="w-3 h-3" />
onChange={(e) => setHasSoftDelete(e.target.checked)} <label className="ml-1 text-sm font-medium text-slate-700">
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500"
/>
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.SoftDelete')} {translate('::App.DeveloperKit.EntityEditor.SoftDelete')}
<span className="text-slate-500 ml-1">(IsDeleted field)</span>
</span>
</label> </label>
</div> </div>
</div> </div>
</div>
{/* Entity Fields */} {/* Fields Section */}
<div className="bg-white rounded-lg border border-slate-200 p-6 shadow-sm"> <div className="bg-white rounded border border-slate-200 p-2">
<div className="flex items-center justify-between mb-4"> <FieldArray name="fields">
<h2 className="text-lg font-semibold text-slate-900"> {({ push, remove }) => (
<>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1">
<div className="bg-green-100 p-1 rounded">
<FaColumns className="w-3 h-3 text-green-600" />
</div>
<h2 className="text-sm font-semibold text-slate-900">
{translate('::App.DeveloperKit.EntityEditor.Fields')} {translate('::App.DeveloperKit.EntityEditor.Fields')}
</h2> </h2>
</div>
<button <button
onClick={addField} type="button"
className="flex items-center gap-2 bg-blue-600 text-white px-3 py-2 rounded-lg hover:bg-blue-700 transition-colors text-sm" onClick={() =>
push({
id: crypto.randomUUID(),
entityId: id || '',
name: '',
type: 'string',
isRequired: false,
description: '',
})
}
className="flex items-center gap-1 bg-gradient-to-r from-blue-600 to-blue-700 text-white px-2 py-1.5 rounded hover:from-blue-700 hover:to-blue-800 transition-all duration-200 text-sm"
> >
<FaPlus className="w-4 h-4" /> <FaPlus className="w-2.5 h-2.5" />
{translate('::App.DeveloperKit.EntityEditor.AddField')} {translate('::App.DeveloperKit.EntityEditor.AddField')}
</button> </button>
</div> </div>
<div className="space-y-4"> <div className="space-y-2">
{fields.map((field, index) => ( {values.fields.map((field, index) => (
<div <div
key={field.id || `new-${index}`} key={field.id || `new-${index}`}
className="border border-slate-200 rounded-lg p-4 bg-slate-50" className="bg-gradient-to-r from-slate-50 to-slate-100 border border-slate-200 rounded p-2 shadow-sm hover:shadow-md transition-all duration-200"
> >
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-3"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 mb-2">
<div> <FormItem
<label className="block text-sm font-medium text-slate-700 mb-1"> label={`${translate('::App.DeveloperKit.EntityEditor.FieldName')} *`}
{translate('::App.DeveloperKit.EntityEditor.FieldName')} * invalid={
</label> !!(
<input errors.fields &&
type="text" (errors.fields as any)[index]?.name &&
value={field.name} touched.fields &&
onChange={(e) => updateField(field.id, { name: e.target.value })} (touched.fields as any)[index]?.name
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" )
placeholder="e.g., Name, Price, IsActive"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.Type')} *
</label>
<select
value={field.type}
onChange={(e) =>
updateField(field.id, {
type: e.target.value as EntityFieldType,
})
} }
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" errorMessage={(errors.fields as any)?.[index]?.name as string}
> >
{fieldTypes.map((type) => ( <Field
<option key={type.value} value={type.value}> name={`fields.${index}.name`}
{type.label} component={Input}
</option> placeholder="e.g., Name, Email, Age"
))} className="px-2 py-1.5 bg-white border-slate-300 focus:border-blue-500 focus:ring-blue-500 transition-all duration-200 text-sm h-6"
</select> />
</div> </FormItem>
{field.type === 'string' && (
<div> <FormItem
<label className="block text-sm font-medium text-slate-700 mb-1"> label={`${translate('::App.DeveloperKit.EntityEditor.Type')} *`}
{translate('::App.DeveloperKit.EntityEditor.MaxLength')} invalid={
</label> !!(
<input errors.fields &&
type="number" (errors.fields as any)[index]?.type &&
value={field.maxLength || ''} touched.fields &&
onChange={(e) => (touched.fields as any)[index]?.type
updateField(field.id, { )
maxLength: e.target.value ? parseInt(e.target.value) : undefined, }
}) errorMessage={(errors.fields as any)?.[index]?.type as string}
} >
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" <Field name={`fields.${index}.type`}>
placeholder="e.g., 100" {({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={fieldTypes.map((type) => ({
value: type.value,
label: type.label,
}))}
value={fieldTypes
.map((type) => ({ value: type.value, label: type.label }))
.filter((option) => option.value === field.value)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value)
}
className="bg-white border-slate-300 focus:border-blue-500 focus:ring-blue-500"
/> />
</div>
)} )}
<div> </Field>
<label className="block text-sm font-medium text-slate-700 mb-1"> </FormItem>
{translate('::App.DeveloperKit.EntityEditor.DefaultValue')}
</label> {field.type === 'string' && (
<input <FormItem
type="text" label={translate('::App.DeveloperKit.EntityEditor.MaxLength')}
value={field.defaultValue || ''} invalid={
onChange={(e) => updateField(field.id, { defaultValue: e.target.value })} !!(
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" errors.fields &&
placeholder="Optional default value" (errors.fields as any)[index]?.maxLength &&
/> touched.fields &&
</div> (touched.fields as any)[index]?.maxLength
</div> )
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3"> }
<div> errorMessage={
<label className="block text-sm font-medium text-slate-700 mb-1"> (errors.fields as any)?.[index]?.maxLength as string
{translate('::App.DeveloperKit.EntityEditor.FieldDescription')}
</label>
<input
type="text"
value={field.description || ''}
onChange={(e) => updateField(field.id, { description: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
placeholder="Field description"
/>
</div>
<div className="flex items-end gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={field.isRequired}
onChange={(e) =>
updateField(field.id, {
isRequired: e.target.checked,
})
} }
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Required')}
</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={field.isUnique || false}
onChange={(e) => updateField(field.id, { isUnique: e.target.checked })}
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Unique')}
</span>
</label>
<button
onClick={() => removeField(field.id)}
className="p-2 text-slate-600 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
title={translate('::App.DeveloperKit.EntityEditor.RemoveField')}
> >
<FaTrashAlt className="w-4 h-4" /> <Field
name={`fields.${index}.maxLength`}
component={Input}
type="number"
placeholder="e.g., 100"
className="px-2 py-1.5 bg-white border-slate-300 focus:border-blue-500 focus:ring-blue-500 transition-all duration-200 text-sm h-6"
/>
</FormItem>
)}
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.DefaultValue')}
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.defaultValue &&
touched.fields &&
(touched.fields as any)[index]?.defaultValue
)
}
errorMessage={
(errors.fields as any)?.[index]?.defaultValue as string
}
>
<Field
name={`fields.${index}.defaultValue`}
component={Input}
placeholder="Optional default value"
className="px-2 py-1.5 bg-white border-slate-300 focus:border-blue-500 focus:ring-blue-500 transition-all duration-200 text-sm h-6"
/>
</FormItem>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-200">
<div className="flex-1 mr-2">
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.Description')}
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.description &&
touched.fields &&
(touched.fields as any)[index]?.description
)
}
errorMessage={
(errors.fields as any)?.[index]?.description as string
}
>
<Field
name={`fields.${index}.description`}
component={Input}
placeholder="Field description"
className="px-2 py-1.5 bg-white border-slate-300 focus:border-blue-500 focus:ring-blue-500 transition-all duration-200 text-sm h-6"
/>
</FormItem>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center p-1 bg-white rounded border border-slate-200">
<Field
name={`fields.${index}.isRequired`}
component={Checkbox}
className="w-3 h-3"
/>
<label className="ml-1 text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Required')}
</label>
</div>
<div className="flex items-center p-1 bg-white rounded border border-slate-200">
<Field
name={`fields.${index}.isUnique`}
component={Checkbox}
className="w-3 h-3"
/>
<label className="ml-1 text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Unique')}
</label>
</div>
<button
type="button"
onClick={() => remove(index)}
className="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded transition-all duration-200 border border-red-200 hover:border-red-300"
title="Remove field"
>
<FaTrashAlt className="w-3 h-3" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div>
{fields.length === 0 && ( {values.fields.length === 0 && (
<div className="text-center py-8 text-slate-500"> <div className="text-center py-4 bg-slate-50 rounded border-2 border-dashed border-slate-300">
<FaDatabase className="w-12 h-12 mx-auto mb-2 text-slate-300" /> <FaTable className="w-8 h-8 mx-auto mb-2 text-slate-400" />
<p>{translate('::App.DeveloperKit.EntityEditor.NoFields')}</p> <h3 className="text-sm font-medium text-slate-600 mb-1">
<p className="text-sm"> {translate('::App.DeveloperKit.EntityEditor.NoFields')}
</h3>
<p className="text-sm text-slate-500">
{translate('::App.DeveloperKit.EntityEditor.NoFieldsDescription')} {translate('::App.DeveloperKit.EntityEditor.NoFieldsDescription')}
</p> </p>
</div> </div>
)} )}
</div> </div>
</>
)}
</FieldArray>
</div>
</Form>
</>
)}
</Formik>
</div> </div>
</div> </div>
) )

View file

@ -85,10 +85,10 @@ const EntityManager: React.FC = () => {
} }
return ( return (
<div className="space-y-8"> <div className="space-y-4">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-4">
<div> <div>
<h1 className="text-3xl font-bold text-slate-900 mb-2"> <h1 className="text-2xl font-bold text-slate-900">
{translate('::App.DeveloperKit.Entity.Title')} {translate('::App.DeveloperKit.Entity.Title')}
</h1> </h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Entity.Description')}</p> <p className="text-slate-600">{translate('::App.DeveloperKit.Entity.Description')}</p>

View file

@ -148,7 +148,7 @@ const MigrationManager: React.FC = () => {
) )
return ( return (
<div className="space-y-8"> <div className="space-y-4">
{/* Error Message */} {/* Error Message */}
{error && ( {error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4"> <div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
@ -160,9 +160,9 @@ const MigrationManager: React.FC = () => {
</div> </div>
)} )}
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-4">
<div> <div>
<h1 className="text-3xl font-bold text-slate-900 mb-2"> <h1 className="text-xl font-bold text-slate-900 mb-2">
{translate('::App.DeveloperKit.Migration.Title')} {translate('::App.DeveloperKit.Migration.Title')}
</h1> </h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Migration.Description')}</p> <p className="text-slate-600">{translate('::App.DeveloperKit.Migration.Description')}</p>