import React, { useState, useEffect, useMemo } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { useEntities, EntityFieldType } from '../../contexts/EntityContext' import { FaSave, FaArrowLeft, FaPlus, FaTrashAlt, FaDatabase, FaCog, FaTable, FaColumns, } from 'react-icons/fa' import { 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, FormContainer, Button } from '@/components/ui' import { SelectBoxOption } from '@/types/shared' import { useStoreState } from '@/store/store' // Validation schema const validationSchema = Yup.object({ menu: Yup.string().required('Menu is required'), 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().notRequired(), description: Yup.string().notRequired(), displayOrder: Yup.number().required(), }), ) .min(1, 'At least one field is required'), isActive: Yup.boolean(), isFullAuditedEntity: Yup.boolean(), isMultiTenant: Yup.boolean(), }) const EntityEditor: React.FC = () => { const { id } = useParams() const navigate = useNavigate() const { translate } = useLocalization() const { mainMenu } = useStoreState((state) => state.abpConfig.menu) const { getEntity, addEntity, updateEntity } = useEntities() const isEditing = !!id // Convert first level menu items to select options with prefix const menuOptions = useMemo(() => { if (!mainMenu || mainMenu.length === 0) return [] const prefixMap: Record = { 'Platform': 'Plat', 'Saas': 'Sas', 'Administration': 'Adm', 'Participant': 'Prt', 'Coordinator': 'Crd', 'Crm': 'Crm', 'SupplyChain': 'Scp', 'Maintenance': 'Mnt', 'Warehouse': 'Str', 'Project': 'Prj', 'Hr': 'Hr', 'Mrp': 'Mrp', 'Accounting': 'Acc', } return mainMenu.map((menuItem) => ({ value: menuItem.key, label: menuItem.title, prefix: menuItem.shortName || prefixMap[menuItem.key] || menuItem.key.substring(0, 3), })) }, [mainMenu]) // Initial values for Formik const [initialValues, setInitialValues] = useState({ menu: '', name: '', displayName: '', tableName: '', description: '', fields: [ { id: crypto.randomUUID(), entityId: id || '', name: 'Name', type: 'string' as EntityFieldType, isRequired: true, maxLength: 100, description: 'Entity name', displayOrder: 1, }, ] as CustomEntityField[], isActive: true, isFullAuditedEntity: true, isMultiTenant: true, }) useEffect(() => { if (isEditing && id) { const entity = getEntity(id) if (entity) { // Ensure fields are sorted by displayOrder and normalized to sequential values const sortedFields = (entity.fields || []) .slice() .sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)) .map((f, idx) => ({ ...f, displayOrder: f.displayOrder ?? idx + 1 })) setInitialValues({ menu: entity.menu, name: entity.name, displayName: entity.displayName, tableName: entity.tableName, description: entity.description || '', fields: sortedFields, isActive: entity.isActive, isFullAuditedEntity: entity.isFullAuditedEntity, isMultiTenant: entity.isMultiTenant, }) } } }, [id, isEditing, getEntity]) const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => { try { const sanitizedFields = values.fields.map((f) => { // send both `displayOrder` (frontend proxy) and `order` (backend DTO) to be safe const sanitized: any = { ...(f.id && isEditing ? { id: f.id } : {}), name: f.name.trim(), type: f.type, isRequired: f.isRequired, maxLength: f.maxLength, isUnique: f.isUnique || false, defaultValue: f.defaultValue, description: f.description, displayOrder: f.displayOrder, order: f.displayOrder, } return sanitized }) const entityData = { menu: values.menu.trim(), name: values.name.trim(), displayName: values.displayName.trim(), tableName: values.tableName.trim(), description: values.description.trim(), fields: sanitizedFields, isActive: values.isActive, isFullAuditedEntity: values.isFullAuditedEntity, isMultiTenant: values.isMultiTenant, } if (isEditing && id) { await updateEntity(id, entityData) } else { await addEntity(entityData) } 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 { setSubmitting(false) } } // Helper function to generate table name const generateTableName = (menuValue: string, entityName: string): string => { if (!menuValue || !entityName) return '' const selectedMenu = menuOptions.find((opt) => opt.value === menuValue) if (!selectedMenu) return '' return `${selectedMenu.prefix}_D_${entityName}` } const fieldTypes = [ { value: 'string', label: 'String' }, { value: 'number', label: 'Number' }, { value: 'decimal', label: 'Decimal' }, { value: 'boolean', label: 'Boolean' }, { value: 'date', label: 'Date' }, { value: 'guid', label: 'Guid' }, ] return ( {({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => ( <> {/* Enhanced Header */}

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

{isEditing ? 'Modify your entity' : 'Create a new entity'}

{/* Save Button in Header */}
{/* Migration Status Info Banner */} {isEditing && id && getEntity(id)?.migrationStatus === 'applied' && (

Migration Applied - Changes Will Require New Migration

This entity has been migrated to the database. Any structural changes you make will reset the migration status to "pending", and you'll need to generate and apply a new migration to update the database schema.

)} {/* Basic Entity Information */}

Entity Settings

{({ field, form }: FieldProps) => ( { field.onBlur(e) const entityName = e.target.value.trim() // Update table name based on menu prefix and entity name if (entityName && values.menu) { const newTableName = generateTableName(values.menu, entityName) setFieldValue('tableName', newTableName) } // Update display name if empty if (entityName && !values.displayName) { setFieldValue('displayName', entityName) } }} 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" /> )}

Format:{' '} {values.menu ? `${menuOptions.find((opt) => opt.value === values.menu)?.prefix || '[Prefix]'}_D_` : '[Prefix]_D_'} EntityName

User-friendly name shown in the interface

Includes CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeleterId, DeletionTime

Adds TenantId column for multi-tenancy support

{/* Fields Section */}
{({ push, remove }) => ( <>

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

Order *
{translate('::ListForms.ListFormFieldEdit.FieldName')} *
{translate('::ListForms.ListFormEdit.Type')} *
{translate('::ListForms.ListFormEdit.ExtraDefaultValue')}
{translate('::App.DeveloperKit.EntityEditor.MaxLength')}
{translate('::ListForms.ListFormEdit.DetailsDescription')}
{translate('::App.Required')}
{translate('::App.DeveloperKit.EntityEditor.Unique')}
{values.fields.map((field, index) => (
{({ field, form }: FieldProps) => (