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",
"key": "App.DeveloperKit.ComponentEditor.Dependencies",
"en": "Dependencies",
"tr": "Bağımlılıklar"
"key": "App.DeveloperKit.ComponentEditor.Description",
"en": "Description",
"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",

View file

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

View file

@ -366,10 +366,10 @@ const ApiManager: React.FC = () => {
}
return (
<div className="space-y-8">
<div className="flex items-center justify-between mb-8">
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<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')}
</h1>
<p className="text-slate-600">

View file

@ -1,12 +1,23 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
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 ComponentPreview from '../../components/componentEditor/ComponentPreview'
import { EditorState } from '../../@types/componentInfo'
import { ROUTES_ENUM } from '@/routes/route.constant'
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
interface ValidationError {
@ -15,6 +26,15 @@ interface ValidationError {
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 { id } = useParams()
const navigate = useNavigate()
@ -22,13 +42,7 @@ const ComponentEditor: React.FC = () => {
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 [isSaving, setIsSaving] = useState(false)
const [isLoaded, setIsLoaded] = useState(false)
const isEditing = !!id
@ -40,6 +54,15 @@ const ComponentEditor: React.FC = () => {
selectedComponentId: null,
})
// Initial values for Formik
const [initialValues, setInitialValues] = useState({
name: '',
description: '',
dependencies: [] as string[],
code: '',
isActive: true,
})
const parseAndUpdateComponents = useCallback((code: string) => {
try {
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
useEffect(() => {
if (isEditing && id && !isLoaded) {
const component = getComponent(id)
if (component) {
setName(component.name)
setDescription(component.description || '')
// Parse dependencies from JSON string
let deps: string[] = []
try {
const deps = component.dependencies ? JSON.parse(component.dependencies) : []
setDependencies(Array.isArray(deps) ? deps : [])
deps = component.dependencies ? JSON.parse(component.dependencies) : []
deps = Array.isArray(deps) ? deps : []
} 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)
}
} else if (!isEditing && !isLoaded) {
// Yeni komponent için boş başla - TEMPLATE YOK
setIsLoaded(true)
}
}, [id, isEditing, getComponent, isLoaded])
}, [id, isEditing, getComponent, isLoaded, parseAndUpdateComponents])
const handleSave = async () => {
if (!name.trim()) {
const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
if (!values.name.trim()) {
alert('Please enter a component name')
setSubmitting(false)
return
}
@ -109,18 +135,19 @@ const ComponentEditor: React.FC = () => {
const proceed = window.confirm(
`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 {
const componentData = {
name: name.trim(),
description: description.trim(),
dependencies: JSON.stringify(dependencies), // Serialize dependencies to JSON string
code: code.trim(),
isActive,
name: values.name.trim(),
description: values.description.trim(),
dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string
code: values.code.trim(),
isActive: values.isActive,
}
if (isEditing && id) {
@ -134,7 +161,7 @@ const ComponentEditor: React.FC = () => {
console.error('Error saving component:', error)
alert('Failed to save component. Please try again.')
} finally {
setIsSaving(false)
setSubmitting(false)
}
}
@ -153,125 +180,205 @@ const ComponentEditor: React.FC = () => {
}
return (
<div className="h-screen flex flex-col">
{/* Header */}
<div className="bg-white border-b border-slate-200 px-6 py-4 flex-shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.components)}
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 transition-colors"
>
<FaArrowLeft className="w-4 h-4" />
{translate('::App.DeveloperKit.ComponentEditor.Back')}
</button>
</div>
</div>
</div>
{/* Component Details */}
<div className="bg-white border-b border-slate-200 px-6 py-4 flex-shrink-0">
{/* 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
onClick={handleSave}
disabled={isSaving || !name.trim()}
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"
>
<FaRegSave className="w-4 h-4" />
{isSaving
? translate('::App.DeveloperKit.ComponentEditor.Saving')
: translate('::App.DeveloperKit.ComponentEditor.Save')}
</button>
</div>
</div>
</div>
{/* Main Content Area - Editor and Preview */}
<div className="flex-1 flex overflow-hidden">
<div className="flex-1 p-4 overflow-auto">
{validationErrors.length > 0 && (
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="bg-red-100 rounded p-1">
<FaExclamationCircle className="w-4 h-4 text-red-600" />
</div>
<div className="flex-1">
<p className="text-sm text-red-800 font-medium mb-2">
{validationErrors.length}{' '}
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')}
{validationErrors.length !== 1 ? 's' : ''}{' '}
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')}
</p>
<div className="space-y-1">
{validationErrors.slice(0, 5).map((error, index) => (
<div key={index} className="text-sm text-red-700">
<span className="font-medium">
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Line')}{' '}
{error.startLineNumber}:
</span>{' '}
{error.message}
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<div className="mx-auto">
<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 gap-4">
<button
type="button"
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.components)}
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" />
{translate('::App.DeveloperKit.ComponentEditor.Back')}
</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>
))}
{validationErrors.length > 5 && (
<div className="text-sm text-red-600 italic">
... {translate('::App.DeveloperKit.ComponentEditor.ValidationError.And')}{' '}
{validationErrors.length - 5}{' '}
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.More')}
{validationErrors.length - 5 !== 1 ? 's' : ''}
</div>
)}
</div>
{/* Save Button in Header */}
<div className="flex items-center gap-3">
<button
type="button"
onClick={submitForm}
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" />
{isSubmitting
? translate('::App.DeveloperKit.ComponentEditor.Saving')
: translate('::App.DeveloperKit.ComponentEditor.Save')}
</button>
</div>
</div>
</div>
</div>
</div>
)}
<ComponentPreview componentName={name} />
</div>
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-2">
{/* Left Side - Component Settings */}
<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 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 shadow-sm">
<div className="flex items-start gap-2">
<div className="bg-red-100 rounded-full p-1.5">
<FaExclamationCircle className="w-4 h-4 text-red-600" />
</div>
<div className="flex-1">
<h3 className="text-base font-semibold text-red-800 mb-1">
Validation Issues
</h3>
<p className="text-xs text-red-700 mb-3">
{validationErrors.length} issue
{validationErrors.length !== 1 ? 's' : ''} found in your code
</p>
<div className="space-y-1.5 max-h-32 overflow-y-auto">
{validationErrors.slice(0, 5).map((error, index) => (
<div
key={index}
className="bg-white p-2 rounded border border-red-100"
>
<div className="text-xs text-red-800">
<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>
))}
{validationErrors.length > 5 && (
<div className="text-xs text-red-600 italic text-center py-1">
... and {validationErrors.length - 5} more issue
{validationErrors.length - 5 !== 1 ? 's' : ''}
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* 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>
<h2 className="text-base font-semibold text-slate-900">Preview</h2>
</div>
<ComponentPreview componentName={values.name} />
</div>
</div>
</Form>
</>
)}
</Formik>
</div>
</div>
)

View file

@ -88,10 +88,10 @@ const ComponentManager: React.FC = () => {
}
return (
<div className="space-y-8">
<div className="flex items-center justify-between mb-8">
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<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')}
</h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Component.Description')}</p>

View file

@ -162,7 +162,7 @@ const Dashboard: React.FC = () => {
{/* Header */}
<div className="flex items-center justify-between">
<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')}
</h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Dashboard.Description')}</p>

View file

@ -7,31 +7,73 @@ import {
FaPlus,
FaTrashAlt,
FaDatabase,
FaQuestionCircle,
FaCog,
FaTable,
FaColumns,
} from 'react-icons/fa'
import { CreateUpdateCustomEntityFieldDto, CustomEntityField } from '@/proxy/developerKit/models'
import { ROUTES_ENUM } from '@/routes/route.constant'
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 { id } = useParams()
const navigate = useNavigate()
const { translate } = useLocalization()
const { getEntity, addEntity, updateEntity, refreshEntities } = 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 { getEntity, addEntity, updateEntity } = useEntities()
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
const isMigrationApplied = Boolean(
isEditing && id && getEntity(id)?.migrationStatus === 'applied',
@ -41,86 +83,25 @@ const EntityEditor: React.FC = () => {
if (isEditing && id) {
const entity = getEntity(id)
if (entity) {
setName(entity.name)
setDisplayName(entity.displayName)
setTableName(entity.tableName)
setDescription(entity.description || '')
setFields(entity.fields)
setIsActive(entity.isActive)
setHasAuditFields(entity.hasAuditFields)
setHasSoftDelete(entity.hasSoftDelete)
setInitialValues({
name: entity.name,
displayName: entity.displayName,
tableName: entity.tableName,
description: entity.description || '',
fields: entity.fields,
isActive: entity.isActive,
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])
const addField = () => {
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)
const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
try {
const sanitizedFields = fields.map((f) => {
const sanitizedFields = values.fields.map((f) => {
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(),
type: f.type,
isRequired: f.isRequired,
@ -134,14 +115,14 @@ const EntityEditor: React.FC = () => {
})
const entityData = {
name: name.trim(),
displayName: displayName.trim(),
tableName: tableName.trim(),
description: description.trim(),
name: values.name.trim(),
displayName: values.displayName.trim(),
tableName: values.tableName.trim(),
description: values.description.trim(),
fields: sanitizedFields,
isActive,
hasAuditFields,
hasSoftDelete,
isActive: values.isActive,
hasAuditFields: values.hasAuditFields,
hasSoftDelete: values.hasSoftDelete,
}
if (isEditing && id) {
@ -150,13 +131,12 @@ const EntityEditor: React.FC = () => {
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 })
} catch (error) {
console.error('Error saving entity:', error)
alert('Failed to save entity. Please try again.')
} finally {
setIsSaving(false)
setSubmitting(false)
}
}
@ -170,374 +150,384 @@ const EntityEditor: React.FC = () => {
]
return (
<div className="min-h-screen bg-slate-50">
<div className="w-full">
{/* Header */}
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
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" />
{translate('::App.DeveloperKit.EntityEditor.Back')}
</button>
<div className="h-6 w-px bg-slate-300" />
<div className="flex items-center gap-2">
<FaDatabase className="w-6 h-6 text-blue-600" />
<h1 className="text-xl font-semibold text-slate-900">
{isEditing
? translate('::App.DeveloperKit.EntityEditor.Title.Edit')
: translate('::App.DeveloperKit.EntityEditor.Title.Create')}
</h1>
</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>
<button
onClick={handleSave}
disabled={isSaving || !name.trim() || !displayName.trim() || !tableName.trim()}
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"
>
<FaSave className="w-4 h-4" />
{isSaving
? translate('::App.DeveloperKit.EntityEditor.Saving')
: translate('::App.DeveloperKit.EntityEditor.Save')}
</button>
</div>
</div>
</div>
{/* Entity Basic Info */}
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm">
<h2 className="text-lg font-semibold text-slate-900 mb-4">
{translate('::App.DeveloperKit.EntityEditor.BasicInfo')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.EntityName')}
</label>
<input
autoFocus
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={() => {
if (!tableName) {
setTableName(name + 's')
}
if (!displayName) {
setDisplayName(name)
}
}}
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"
/>
{isMigrationApplied && (
<p className="text-xs text-slate-500 mt-1">
{translate('::App.DeveloperKit.EntityEditor.CannotChange')}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.DisplayName')} *
</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(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="e.g., Product, User, Order"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.TableName')} *
</label>
<input
type="text"
value={tableName}
onChange={(e) => setTableName(e.target.value)}
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"
/>
{isMigrationApplied && (
<p className="text-xs text-slate-500 mt-1">
Cannot be changed after migration is applied
</p>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.Description')}
</label>
<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"
/>
</div>
</div>
{isEditing && (
<div className="mb-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
<h3 className="text-sm font-medium text-slate-700 mb-3">
{translate('::App.DeveloperKit.EntityEditor.Status')}
</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')}
</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={hasAuditFields}
onChange={(e) => setHasAuditFields(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.Audit')}
<span className="text-slate-500 ml-1">
(creationTime, lastModificationTime, etc.)
</span>
</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={hasSoftDelete}
onChange={(e) => setHasSoftDelete(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.SoftDelete')}
<span className="text-slate-500 ml-1">(IsDeleted field)</span>
</span>
</label>
</div>
</div>
{/* Entity Fields */}
<div className="bg-white rounded-lg border border-slate-200 p-6 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900">
{translate('::App.DeveloperKit.EntityEditor.Fields')}
</h2>
<button
onClick={addField}
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"
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKit.EntityEditor.AddField')}
</button>
</div>
<div className="space-y-4">
{fields.map((field, index) => (
<div
key={field.id || `new-${index}`}
className="border border-slate-200 rounded-lg p-4 bg-slate-50"
>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.FieldName')} *
</label>
<input
type="text"
value={field.name}
onChange={(e) => updateField(field.id, { name: 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="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"
>
{fieldTypes.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
{field.type === 'string' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.MaxLength')}
</label>
<input
type="number"
value={field.maxLength || ''}
onChange={(e) =>
updateField(field.id, {
maxLength: e.target.value ? parseInt(e.target.value) : undefined,
})
}
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., 100"
/>
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<div className="mx-auto">
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
<>
{/* Enhanced Header */}
<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">
<button
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
? `${translate('::App.DeveloperKit.EntityEditor.Title.Edit')} - ${values.name || initialValues.name || 'Entity'}`
: translate('::App.DeveloperKit.EntityEditor.Title.Create')}
</h1>
</div>
</div>
</div>
{/* Save Button in Header */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={submitForm}
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-3 h-3" />
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</button>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.DefaultValue')}
</label>
<input
type="text"
value={field.defaultValue || ''}
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"
placeholder="Optional default value"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{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" />
</button>
</div>
</div>
</div>
))}
</div>
{fields.length === 0 && (
<div className="text-center py-8 text-slate-500">
<FaDatabase className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p>{translate('::App.DeveloperKit.EntityEditor.NoFields')}</p>
<p className="text-sm">
{translate('::App.DeveloperKit.EntityEditor.NoFieldsDescription')}
</p>
</div>
<Form className="space-y-2 pt-2">
{/* Basic Entity Information */}
<div className="bg-white rounded border border-slate-200 p-2">
<div className="flex items-center gap-1.5 mb-2">
<div className="bg-blue-100 p-1 rounded">
<FaCog className="w-3 h-3 text-blue-600" />
</div>
<h2 className="text-sm font-semibold text-slate-900">Entity Settings</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.EntityName')}
invalid={!!(errors.name && touched.name)}
errorMessage={errors.name as string}
>
<Field name="name">
{({ field }: FieldProps) => (
<Input
{...field}
onBlur={(e) => {
field.onBlur(e)
if (!values.tableName) {
setFieldValue('tableName', values.name + 's')
}
if (!values.displayName) {
setFieldValue('displayName', values.name)
}
}}
disabled={isMigrationApplied}
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"
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.DisplayName')}
invalid={!!(errors.displayName && touched.displayName)}
errorMessage={errors.displayName as string}
>
<Field
name="displayName"
component={Input}
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"
/>
</FormItem>
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.TableName')}
invalid={!!(errors.tableName && touched.tableName)}
errorMessage={errors.tableName as string}
>
<Field
name="tableName"
component={Input}
disabled={isMigrationApplied}
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"
/>
</FormItem>
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.Description')}
invalid={!!(errors.description && touched.description)}
errorMessage={errors.description as string}
>
<Field
name="description"
component={Input}
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"
/>
</FormItem>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 mt-2 pt-2 border-t border-slate-200">
<div className="flex items-center p-1 bg-slate-50 rounded border border-slate-200">
<Field name="isActive" component={Checkbox} className="w-3 h-3" />
<label className="ml-1 text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Active')}
</label>
</div>
<div className="flex items-center p-1 bg-slate-50 rounded border border-slate-200">
<Field name="hasAuditFields" component={Checkbox} className="w-3 h-3" />
<label className="ml-1 text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Audit')}
</label>
</div>
<div className="flex items-center p-1 bg-slate-50 rounded border border-slate-200">
<Field name="hasSoftDelete" component={Checkbox} className="w-3 h-3" />
<label className="ml-1 text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.SoftDelete')}
</label>
</div>
</div>
</div>
{/* Fields Section */}
<div className="bg-white rounded border border-slate-200 p-2">
<FieldArray name="fields">
{({ 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')}
</h2>
</div>
<button
type="button"
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-2.5 h-2.5" />
{translate('::App.DeveloperKit.EntityEditor.AddField')}
</button>
</div>
<div className="space-y-2">
{values.fields.map((field, index) => (
<div
key={field.id || `new-${index}`}
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-2 lg:grid-cols-4 gap-2 mb-2">
<FormItem
label={`${translate('::App.DeveloperKit.EntityEditor.FieldName')} *`}
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.name &&
touched.fields &&
(touched.fields as any)[index]?.name
)
}
errorMessage={(errors.fields as any)?.[index]?.name as string}
>
<Field
name={`fields.${index}.name`}
component={Input}
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"
/>
</FormItem>
<FormItem
label={`${translate('::App.DeveloperKit.EntityEditor.Type')} *`}
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.type &&
touched.fields &&
(touched.fields as any)[index]?.type
)
}
errorMessage={(errors.fields as any)?.[index]?.type as string}
>
<Field name={`fields.${index}.type`}>
{({ 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"
/>
)}
</Field>
</FormItem>
{field.type === 'string' && (
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.MaxLength')}
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.maxLength &&
touched.fields &&
(touched.fields as any)[index]?.maxLength
)
}
errorMessage={
(errors.fields as any)?.[index]?.maxLength as string
}
>
<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>
</div>
</div>
</div>
))}
{values.fields.length === 0 && (
<div className="text-center py-4 bg-slate-50 rounded border-2 border-dashed border-slate-300">
<FaTable className="w-8 h-8 mx-auto mb-2 text-slate-400" />
<h3 className="text-sm font-medium text-slate-600 mb-1">
{translate('::App.DeveloperKit.EntityEditor.NoFields')}
</h3>
<p className="text-sm text-slate-500">
{translate('::App.DeveloperKit.EntityEditor.NoFieldsDescription')}
</p>
</div>
)}
</div>
</>
)}
</FieldArray>
</div>
</Form>
</>
)}
</div>
</Formik>
</div>
</div>
)

View file

@ -85,10 +85,10 @@ const EntityManager: React.FC = () => {
}
return (
<div className="space-y-8">
<div className="flex items-center justify-between mb-8">
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<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')}
</h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Entity.Description')}</p>

View file

@ -148,7 +148,7 @@ const MigrationManager: React.FC = () => {
)
return (
<div className="space-y-8">
<div className="space-y-4">
{/* Error Message */}
{error && (
<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 className="flex items-center justify-between mb-8">
<div className="flex items-center justify-between mb-4">
<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')}
</h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Migration.Description')}</p>