Geliştirme Kiti düzenlemesi
This commit is contained in:
parent
39af771d91
commit
93578c49a6
9 changed files with 735 additions and 620 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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,125 +180,205 @@ 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
|
||||||
<div className="flex items-center justify-between">
|
enableReinitialize
|
||||||
<div className="flex items-center gap-4">
|
initialValues={initialValues}
|
||||||
<button
|
validationSchema={validationSchema}
|
||||||
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.components)}
|
onSubmit={handleSubmit}
|
||||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 transition-colors"
|
>
|
||||||
>
|
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
|
||||||
<FaArrowLeft className="w-4 h-4" />
|
<>
|
||||||
{translate('::App.DeveloperKit.ComponentEditor.Back')}
|
{/* Enhanced Header */}
|
||||||
</button>
|
<div className="bg-white shadow-lg border-b border-slate-200 sticky top-0 z-10">
|
||||||
</div>
|
<div className="px-4 py-3">
|
||||||
</div>
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
{/* Component Details */}
|
type="button"
|
||||||
<div className="bg-white border-b border-slate-200 px-6 py-4 flex-shrink-0">
|
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.components)}
|
||||||
{/* Form alanları tek satırda ve sayfaya yayılmış */}
|
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"
|
||||||
<div className="grid grid-cols-12 gap-6">
|
>
|
||||||
<div className="col-span-3">
|
<FaArrowLeft className="w-4 h-4" />
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
{translate('::App.DeveloperKit.ComponentEditor.Back')}
|
||||||
{translate('::App.DeveloperKit.ComponentEditor.ComponentName')} *
|
</button>
|
||||||
</label>
|
<div className="h-6 w-px bg-slate-300"></div>
|
||||||
<input
|
<div className="flex items-center gap-3">
|
||||||
type="text"
|
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
|
||||||
value={name}
|
<FaCode className="w-5 h-5 text-white" />
|
||||||
onChange={(e) => setName(e.target.value)}
|
</div>
|
||||||
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"
|
<div>
|
||||||
placeholder="e.g., Button, Card, Modal"
|
<h1 className="text-xl font-bold text-slate-900">
|
||||||
/>
|
{isEditing
|
||||||
</div>
|
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
|
||||||
<div className="col-span-6">
|
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
</h1>
|
||||||
{translate('::App.DeveloperKit.ComponentEditor.Dependencies')}
|
<p className="text-sm text-slate-600">
|
||||||
</label>
|
{isEditing
|
||||||
<input
|
? 'Modify your React component'
|
||||||
type="text"
|
: 'Create a new React component'}
|
||||||
value={(dependencies || []).join(', ')}
|
</p>
|
||||||
onChange={(e) =>
|
</div>
|
||||||
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>
|
</div>
|
||||||
))}
|
</div>
|
||||||
{validationErrors.length > 5 && (
|
|
||||||
<div className="text-sm text-red-600 italic">
|
{/* Save Button in Header */}
|
||||||
... {translate('::App.DeveloperKit.ComponentEditor.ValidationError.And')}{' '}
|
<div className="flex items-center gap-3">
|
||||||
{validationErrors.length - 5}{' '}
|
<button
|
||||||
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.More')}
|
type="button"
|
||||||
{validationErrors.length - 5 !== 1 ? 's' : ''}
|
onClick={submitForm}
|
||||||
</div>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ComponentPreview componentName={name} />
|
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-2">
|
||||||
</div>
|
{/* 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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
|
||||||
>
|
<>
|
||||||
<FaArrowLeft className="w-4 h-4" />
|
{/* Enhanced Header */}
|
||||||
{translate('::App.DeveloperKit.EntityEditor.Back')}
|
<div className="bg-white shadow border-b border-slate-200 sticky top-0 z-10">
|
||||||
</button>
|
<div className="px-3 py-2">
|
||||||
<div className="h-6 w-px bg-slate-300" />
|
<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"
|
||||||
{isEditing
|
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.entities)}
|
||||||
? translate('::App.DeveloperKit.EntityEditor.Title.Edit')
|
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"
|
||||||
: translate('::App.DeveloperKit.EntityEditor.Title.Create')}
|
>
|
||||||
</h1>
|
<FaArrowLeft className="w-3 h-3" />
|
||||||
</div>
|
<span className="text-sm">{translate('::App.DeveloperKit.EntityEditor.Back')}</span>
|
||||||
</div>
|
</button>
|
||||||
<div className="flex items-center gap-3">
|
<div className="h-4 w-px bg-slate-300"></div>
|
||||||
{/* Help Tooltip */}
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative group">
|
<div className="bg-gradient-to-r from-green-500 to-blue-600 p-1 rounded">
|
||||||
<button className="p-2 text-slate-400 hover:text-slate-600 transition-colors">
|
<FaDatabase className="w-3 h-3 text-white" />
|
||||||
<FaQuestionCircle className="w-4 h-4" />
|
</div>
|
||||||
</button>
|
<div>
|
||||||
<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">
|
<h1 className="text-sm font-bold text-slate-900">
|
||||||
<div className="absolute -top-1 right-4 w-2 h-2 bg-slate-900 rotate-45"></div>
|
{isEditing
|
||||||
<div className="flex items-center gap-2 mb-2">
|
? `${translate('::App.DeveloperKit.EntityEditor.Title.Edit')} - ${values.name || initialValues.name || 'Entity'}`
|
||||||
<FaDatabase className="w-4 h-4 text-blue-400" />
|
: translate('::App.DeveloperKit.EntityEditor.Title.Create')}
|
||||||
<h4 className="font-semibold text-blue-200">
|
</h1>
|
||||||
{translate('::App.DeveloperKit.EntityEditor.Tooltip.Title')}
|
</div>
|
||||||
</h4>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-slate-300 leading-relaxed">
|
|
||||||
{translate('::App.DeveloperKit.EntityEditor.Tooltip.Content')}
|
{/* Save Button in Header */}
|
||||||
</p>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
onClick={submitForm}
|
||||||
<button
|
disabled={isSubmitting || !isValid}
|
||||||
onClick={handleSave}
|
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"
|
||||||
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-3 h-3" />
|
||||||
>
|
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
|
||||||
<FaSave className="w-4 h-4" />
|
</button>
|
||||||
{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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{fields.length === 0 && (
|
<Form className="space-y-2 pt-2">
|
||||||
<div className="text-center py-8 text-slate-500">
|
{/* Basic Entity Information */}
|
||||||
<FaDatabase className="w-12 h-12 mx-auto mb-2 text-slate-300" />
|
<div className="bg-white rounded border border-slate-200 p-2">
|
||||||
<p>{translate('::App.DeveloperKit.EntityEditor.NoFields')}</p>
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
<p className="text-sm">
|
<div className="bg-blue-100 p-1 rounded">
|
||||||
{translate('::App.DeveloperKit.EntityEditor.NoFieldsDescription')}
|
<FaCog className="w-3 h-3 text-blue-600" />
|
||||||
</p>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue