diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/LanguagesData.json index d89e6838..5237cbf0 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/LanguagesData.json @@ -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", diff --git a/ui/src/components/componentEditor/ComponentPreview.tsx b/ui/src/components/componentEditor/ComponentPreview.tsx index 85facdad..c0061217 100644 --- a/ui/src/components/componentEditor/ComponentPreview.tsx +++ b/ui/src/components/componentEditor/ComponentPreview.tsx @@ -47,7 +47,7 @@ const ComponentPreview: React.FC = ({ componentName, clas } return ( -
+
) diff --git a/ui/src/components/developerKit/ApiManager.tsx b/ui/src/components/developerKit/ApiManager.tsx index c20fddad..06eee634 100644 --- a/ui/src/components/developerKit/ApiManager.tsx +++ b/ui/src/components/developerKit/ApiManager.tsx @@ -366,10 +366,10 @@ const ApiManager: React.FC = () => { } return ( -
-
+
+
-

+

{translate('::App.DeveloperKit.Endpoint.Title')}

diff --git a/ui/src/components/developerKit/ComponentEditor.tsx b/ui/src/components/developerKit/ComponentEditor.tsx index a577e32c..9fa365cf 100644 --- a/ui/src/components/developerKit/ComponentEditor.tsx +++ b/ui/src/components/developerKit/ComponentEditor.tsx @@ -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([]) - const [code, setCode] = useState('') - const [isActive, setIsActive] = useState(true) const [validationErrors, setValidationErrors] = useState([]) - 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 ( -

- {/* Header */} -
-
-
- -
-
-
- - {/* Component Details */} -
- {/* Form alanları tek satırda ve sayfaya yayılmış */} -
-
- - 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" - /> -
-
- - - 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." - /> -
-
- - setIsActive(e.target.checked)} - className="rounded border-slate-300 text-blue-600 focus:ring-blue-500" - /> -
-
- -
-
-
- - {/* Main Content Area - Editor and Preview */} -
-
- {validationErrors.length > 0 && ( -
-
-
- -
-
-

- {validationErrors.length}{' '} - {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')} - {validationErrors.length !== 1 ? 's' : ''}{' '} - {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')} -

-
- {validationErrors.slice(0, 5).map((error, index) => ( -
- - {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Line')}{' '} - {error.startLineNumber}: - {' '} - {error.message} +
+
+ + {({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => ( + <> + {/* Enhanced Header */} +
+
+
+
+ +
+
+
+ +
+
+

+ {isEditing + ? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}` + : translate('::App.DeveloperKit.ComponentEditor.Title.Create')} +

+

+ {isEditing + ? 'Modify your React component' + : 'Create a new React component'} +

+
- ))} - {validationErrors.length > 5 && ( -
- ... {translate('::App.DeveloperKit.ComponentEditor.ValidationError.And')}{' '} - {validationErrors.length - 5}{' '} - {translate('::App.DeveloperKit.ComponentEditor.ValidationError.More')} - {validationErrors.length - 5 !== 1 ? 's' : ''} -
- )} +
+ + {/* Save Button in Header */} +
+ +
-
- )} - -
+
+ {/* Left Side - Component Settings */} +
+
+
+
+ +
+

Component Settings

+
+ +
+ + + + + + + + + + + {({ field }: FieldProps) => ( + + 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." + /> + )} + + + + +
+ + +
+
+
+
+
+ + {/* Right Side - Preview and Validation */} +
+ {/* Validation Errors */} + {validationErrors.length > 0 && ( +
+
+
+ +
+
+

+ Validation Issues +

+

+ {validationErrors.length} issue + {validationErrors.length !== 1 ? 's' : ''} found in your code +

+
+ {validationErrors.slice(0, 5).map((error, index) => ( +
+
+ + Line {error.startLineNumber} + + {error.message} +
+
+ ))} + {validationErrors.length > 5 && ( +
+ ... and {validationErrors.length - 5} more issue + {validationErrors.length - 5 !== 1 ? 's' : ''} +
+ )} +
+
+
+
+ )} + + {/* Component Preview */} +
+
+
+ +
+

Preview

+
+ +
+
+
+ + )} +
) diff --git a/ui/src/components/developerKit/ComponentManager.tsx b/ui/src/components/developerKit/ComponentManager.tsx index 9b0fbabf..561ef5cb 100644 --- a/ui/src/components/developerKit/ComponentManager.tsx +++ b/ui/src/components/developerKit/ComponentManager.tsx @@ -88,10 +88,10 @@ const ComponentManager: React.FC = () => { } return ( -
-
+
+
-

+

{translate('::App.DeveloperKit.Component.Title')}

{translate('::App.DeveloperKit.Component.Description')}

diff --git a/ui/src/components/developerKit/Dashboard.tsx b/ui/src/components/developerKit/Dashboard.tsx index c95f751b..27080199 100644 --- a/ui/src/components/developerKit/Dashboard.tsx +++ b/ui/src/components/developerKit/Dashboard.tsx @@ -162,7 +162,7 @@ const Dashboard: React.FC = () => { {/* Header */}
-

+

{translate('::App.DeveloperKit.Dashboard.Title')}

{translate('::App.DeveloperKit.Dashboard.Description')}

diff --git a/ui/src/components/developerKit/EntityEditor.tsx b/ui/src/components/developerKit/EntityEditor.tsx index 9050ae92..87a29e3b 100644 --- a/ui/src/components/developerKit/EntityEditor.tsx +++ b/ui/src/components/developerKit/EntityEditor.tsx @@ -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([]) - 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) => { - 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 ( -
-
- {/* Header */} -
-
-
- -
-
- -

- {isEditing - ? translate('::App.DeveloperKit.EntityEditor.Title.Edit') - : translate('::App.DeveloperKit.EntityEditor.Title.Create')} -

-
-
-
- {/* Help Tooltip */} -
- -
-
-
- -

- {translate('::App.DeveloperKit.EntityEditor.Tooltip.Title')} -

-
-

- {translate('::App.DeveloperKit.EntityEditor.Tooltip.Content')} -

-
-
- - -
-
-
- - {/* Entity Basic Info */} -
-

- {translate('::App.DeveloperKit.EntityEditor.BasicInfo')} -

-
-
- - 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 && ( -

- {translate('::App.DeveloperKit.EntityEditor.CannotChange')} -

- )} -
-
- - 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" - /> -
-
- - 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 && ( -

- Cannot be changed after migration is applied -

- )} -
-
- - 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" - /> -
-
- - {isEditing && ( -
-

- {translate('::App.DeveloperKit.EntityEditor.Status')} -

-
-
- - {translate('::App.DeveloperKit.EntityEditor.Status.Migration')} - - - {getEntity(id!)?.migrationStatus || 'pending'} - -
-
- - {translate('::App.DeveloperKit.EntityEditor.Status.Endpoint')} - - - {getEntity(id!)?.endpointStatus || 'pending'} - -
-
-
- )} -
- - - -
-
- - {/* Entity Fields */} -
-
-

- {translate('::App.DeveloperKit.EntityEditor.Fields')} -

- -
- -
- {fields.map((field, index) => ( -
-
-
- - 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" - /> -
-
- - -
- {field.type === 'string' && ( -
- - - 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" - /> +
+
+ + {({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => ( + <> + {/* Enhanced Header */} +
+
+
+
+ +
+
+
+ +
+
+

+ {isEditing + ? `${translate('::App.DeveloperKit.EntityEditor.Title.Edit')} - ${values.name || initialValues.name || 'Entity'}` + : translate('::App.DeveloperKit.EntityEditor.Title.Create')} +

+
+
+
+ + {/* Save Button in Header */} +
+
- )} -
- - 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" - /> -
-
-
-
- - 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" - /> -
-
- - -
- ))} -
- {fields.length === 0 && ( -
- -

{translate('::App.DeveloperKit.EntityEditor.NoFields')}

-

- {translate('::App.DeveloperKit.EntityEditor.NoFieldsDescription')} -

-
+
+ {/* Basic Entity Information */} +
+
+
+ +
+

Entity Settings

+
+ +
+ + + {({ field }: FieldProps) => ( + { + 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" + /> + )} + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* Fields Section */} +
+ + {({ push, remove }) => ( + <> +
+
+
+ +
+

+ {translate('::App.DeveloperKit.EntityEditor.Fields')} +

+
+ +
+ +
+ {values.fields.map((field, index) => ( +
+
+ + + + + + + {({ field, form }: FieldProps) => ( +