From 64d14af8d5e38a4ed4230e23f3af5d688a41f65e Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Mon, 12 Jan 2026 13:23:11 +0100 Subject: [PATCH] feat:finished admin pages --- ...implement_rbac_roles_page_8dd9fac6.plan.md | 144 ++++ src/api/mandateApi.ts | 136 +++ src/api/rbacRulesApi.ts | 136 +++ src/api/roleApi.ts | 201 +++++ .../FormGeneratorForm/FormGeneratorForm.tsx | 196 ++++- .../FormGeneratorTable.module.css | 20 +- .../FormGeneratorTable/FormGeneratorTable.tsx | 130 ++- src/core/PageManager/PageRenderer.tsx | 87 +- .../data/pages/admin/admin-settings.ts | 53 -- .../PageManager/data/pages/admin/mandates.ts | 186 ++++- .../PageManager/data/pages/admin/rbac-role.ts | 194 ++++- .../data/pages/admin/rbac-rules.ts | 189 ++++- src/hooks/useAdminMandates.ts | 563 +++++++++++++ src/hooks/useAdminRbacRoles.ts | 665 +++++++++++++++ src/hooks/useAdminRbacRules.ts | 580 +++++++++++++ src/hooks/useApi.ts | 28 +- src/locales/de.ts | 784 ------------------ src/locales/en.ts | 784 ------------------ src/locales/fr.ts | 784 ------------------ 19 files changed, 3399 insertions(+), 2461 deletions(-) create mode 100644 .cursor/plans/implement_rbac_roles_page_8dd9fac6.plan.md create mode 100644 src/api/mandateApi.ts create mode 100644 src/api/rbacRulesApi.ts create mode 100644 src/api/roleApi.ts delete mode 100644 src/core/PageManager/data/pages/admin/admin-settings.ts create mode 100644 src/hooks/useAdminMandates.ts create mode 100644 src/hooks/useAdminRbacRoles.ts create mode 100644 src/hooks/useAdminRbacRules.ts delete mode 100644 src/locales/de.ts delete mode 100644 src/locales/en.ts delete mode 100644 src/locales/fr.ts diff --git a/.cursor/plans/implement_rbac_roles_page_8dd9fac6.plan.md b/.cursor/plans/implement_rbac_roles_page_8dd9fac6.plan.md new file mode 100644 index 0000000..4828679 --- /dev/null +++ b/.cursor/plans/implement_rbac_roles_page_8dd9fac6.plan.md @@ -0,0 +1,144 @@ +# Implement RBAC Roles Page + +## Overview +Implement the RBAC roles admin page following the exact pattern used in `mandates.ts`. This includes creating the API file, custom hook for state management, updating the page configuration with CreateButton header button, and adding translations in all three languages (German, English, French). + +## Files to Create/Modify + +### 1. Create API File: `frontend_nyla/src/api/roleApi.ts` + - Follow the pattern from `mandateApi.ts` + - Implement all required endpoints: + - `fetchRoles()` - GET /api/rbac/roles (with pagination support) + - `fetchRoleById()` - GET /api/rbac/roles/{roleId} + - `fetchRoleOptions()` - GET /api/rbac/roles/options + - `createRole()` - POST /api/rbac/roles + - `updateRole()` - PUT /api/rbac/roles/{roleId} + - `deleteRole()` - DELETE /api/rbac/roles/{roleId} + - Include TypeScript types: `Role`, `RoleUpdateData`, `PaginationParams`, `PaginatedResponse` + +### 2. Create Hook: `frontend_nyla/src/hooks/useAdminRbacRoles.ts` + - Follow the exact pattern from `useAdminMandates.ts` + - Create two hooks: + - `useRbacRoles()` - Main hook for data fetching and state management + - Fetch roles with pagination support + - Fetch attributes from `/api/attributes/Role` using `fetchAttributes(request, 'Role')` + - Fetch permissions using `checkPermission('DATA', 'Role')` + - Implement `generateEditFieldsFromAttributes()` using `attributeTypeMapper` utilities + - Implement `generateCreateFieldsFromAttributes()` using `attributeTypeMapper` utilities + - Implement `ensureAttributesLoaded()` for EditActionButton + - Implement optimistic updates (`removeOptimistically`, `updateOptimistically`) + - Return pagination info, attributes, permissions, and all required functions + - `useRbacRoleOperations()` - Operations hook for CRUD + - `handleRoleDelete()` - Delete with loading state tracking + - `handleRoleCreate()` - Create with error handling + - `handleRoleUpdate()` - Update with error handling + - Track loading states in Sets (deletingRoles, editingRoles, creatingRole) + - Return error states (deleteError, createError, updateError) + +### 3. Update Page Configuration: `frontend_nyla/src/core/PageManager/data/pages/admin/rbac-role.ts` + - Follow the exact structure from `mandates.ts` + - Import `FaPlus` from `react-icons/fa` for the create button icon + - Create `createRbacRolesHook()` factory function that: + - Uses `useRbacRoles()` and `useRbacRoleOperations()` + - Converts attributes to columns using `attributesToColumns()` helper + - Implements `handleDeleteSingle` and `handleDeleteMultiple` callbacks + - Returns all required data for FormGeneratorTable + - Update `rbacRolePageData`: + - Add header button with `FaPlus` icon for creating roles (following mandates.ts pattern): + ```typescript + headerButtons: [ + { + id: 'add-role', + label: 'admin.rbac-role.new_button', + variant: 'primary', + size: 'md', + icon: FaPlus, + formConfig: { + fields: [], // Empty array - fields will be generated dynamically from attributes + popupTitle: 'admin.rbac-role.modal.create.title', + popupSize: 'medium', + createOperationName: 'handleRoleCreate', + successMessage: 'admin.rbac-role.create.success', + errorMessage: 'admin.rbac-role.create.error' + } + } + ] + ``` + - Add table content section with: + - `hookFactory: createRbacRolesHook` + - Action buttons: edit and delete (following mandates pattern) + - Configure edit button with `fetchItemFunctionName: 'fetchRoleById'` + - Configure delete button with proper operation names + - Add permission-based disabled logic + - Keep existing privilege checker (sysadmin only) + +### 4. Update Translations: All three locale files + - **German (`frontend_nyla/src/locales/de.ts`)**: Add missing translations after line 756: + - `'admin.rbac-role.new_button': 'Rolle hinzufügen'` + - `'admin.rbac-role.action.edit': 'Bearbeiten'` + - `'admin.rbac-role.action.delete': 'Löschen'` + - `'admin.rbac-role.modal.create.title': 'Neue Rolle erstellen'` + - `'admin.rbac-role.create.success': 'Rolle erfolgreich erstellt'` + - `'admin.rbac-role.create.error': 'Fehler beim Erstellen der Rolle'` + + - **English (`frontend_nyla/src/locales/en.ts`)**: Add missing translations after line 756: + - `'admin.rbac-role.new_button': 'Add Role'` + - `'admin.rbac-role.action.edit': 'Edit'` + - `'admin.rbac-role.action.delete': 'Delete'` + - `'admin.rbac-role.modal.create.title': 'Create New Role'` + - `'admin.rbac-role.create.success': 'Role created successfully'` + - `'admin.rbac-role.create.error': 'Error creating role'` + + - **French (`frontend_nyla/src/locales/fr.ts`)**: Add missing translations after line 756: + - `'admin.rbac-role.new_button': 'Ajouter un rôle'` + - `'admin.rbac-role.action.edit': 'Modifier'` + - `'admin.rbac-role.action.delete': 'Supprimer'` + - `'admin.rbac-role.modal.create.title': 'Créer un nouveau rôle'` + - `'admin.rbac-role.create.success': 'Rôle créé avec succès'` + - `'admin.rbac-role.create.error': 'Erreur lors de la création du rôle'` + +## Implementation Details + +### API File Structure +- Use `ApiRequestFunction` type from `useApi` +- Support pagination parameters (page, pageSize, sort, filters, search) +- Handle both paginated and non-paginated responses +- Use `/api/rbac/roles` as base URL +- Use `/api/attributes/Role` for attributes endpoint + +### Hook Pattern +- Use `useApiRequest` hook for API calls +- Use `usePermissions` hook for permission checking +- Use `getUserDataCache()` to check authentication before fetching +- Implement attribute type mapping using utilities from `attributeTypeMapper.ts`: + - `isCheckboxType()`, `isSelectType()`, `isMultiselectType()`, `isDateTimeType()`, `isTextareaType()` +- Filter out non-editable fields (id, readonly fields, etc.) +- Handle options arrays and option references + +### Page Configuration Pattern +- Use `attributesToColumns()` helper to convert attributes to column config +- Disable filtering for date/timestamp fields using `isDateTimeType()` +- Configure action buttons with proper field mappings and operation names +- Use permission-based disabled logic for buttons +- Set `entityType: 'Role'` for EditActionButton +- Add header button using CreateButton component pattern (via formConfig in headerButtons) + +## Key Dependencies +- `useApiRequest` from `hooks/useApi` +- `usePermissions` from `hooks/usePermissions` +- `fetchAttributes` from `api/attributesApi` +- `attributeTypeMapper` utilities from `utils/attributeTypeMapper` +- `FormGeneratorTable` component +- `EditActionButton` and `DeleteActionButton` components +- `CreateButton` component (rendered via PageRenderer from headerButtons formConfig) +- `FaPlus` icon from `react-icons/fa` + +## Testing Considerations +- Verify all API endpoints are called correctly +- Ensure attributes are fetched from `/api/attributes/Role` +- Verify permission checks work correctly +- Test create, edit, delete operations +- Verify optimistic updates work +- Check that date/timestamp fields are not filterable +- Verify CreateButton appears in header and opens create modal +- Verify translations work in all three languages diff --git a/src/api/mandateApi.ts b/src/api/mandateApi.ts new file mode 100644 index 0000000..b29d138 --- /dev/null +++ b/src/api/mandateApi.ts @@ -0,0 +1,136 @@ +import { ApiRequestOptions } from '../hooks/useApi'; + +// ============================================================================ +// TYPES & INTERFACES +// ============================================================================ + +export interface Mandate { + id: string; + [key: string]: any; // Allow additional properties from backend +} + +export type MandateUpdateData = Partial>; + +export interface PaginationParams { + page?: number; + pageSize?: number; + sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; + filters?: Record; + search?: string; +} + +export interface PaginatedResponse { + items: T[]; + pagination?: { + currentPage: number; + pageSize: number; + totalItems: number; + totalPages: number; + }; +} + +// Type for the request function passed to API functions +export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; + +// ============================================================================ +// API REQUEST FUNCTIONS +// ============================================================================ + +/** + * Fetch list of mandates with optional pagination + * Endpoint: GET /api/mandates/ + */ +export async function fetchMandates( + request: ApiRequestFunction, + params?: PaginationParams +): Promise | Mandate[]> { + const requestParams: any = {}; + + // Build pagination object if provided + if (params) { + const paginationObj: any = {}; + + if (params.page !== undefined) paginationObj.page = params.page; + if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; + if (params.sort) paginationObj.sort = params.sort; + if (params.filters) paginationObj.filters = params.filters; + if (params.search) paginationObj.search = params.search; + + if (Object.keys(paginationObj).length > 0) { + requestParams.pagination = JSON.stringify(paginationObj); + } + } + + const data = await request({ + url: '/api/mandates/', + method: 'get', + params: requestParams + }); + + return data; +} + +/** + * Fetch a single mandate by ID + * Endpoint: GET /api/mandates/{mandateId} + */ +export async function fetchMandateById( + request: ApiRequestFunction, + mandateId: string +): Promise { + try { + const data = await request({ + url: `/api/mandates/${mandateId}`, + method: 'get' + }); + return data || null; + } catch (error: any) { + console.error('Error fetching mandate by ID:', error); + return null; + } +} + +/** + * Update a mandate + * Endpoint: PUT /api/mandates/{mandateId} + */ +export async function updateMandate( + request: ApiRequestFunction, + mandateId: string, + updateData: MandateUpdateData +): Promise { + return await request({ + url: `/api/mandates/${mandateId}`, + method: 'put', + data: updateData + }); +} + +/** + * Create a new mandate + * Endpoint: POST /api/mandates/ + */ +export async function createMandate( + request: ApiRequestFunction, + mandateData: Partial +): Promise { + return await request({ + url: '/api/mandates/', + method: 'post', + data: mandateData + }); +} + +/** + * Delete a mandate + * Endpoint: DELETE /api/mandates/{mandateId} + */ +export async function deleteMandate( + request: ApiRequestFunction, + mandateId: string +): Promise { + await request({ + url: `/api/mandates/${mandateId}`, + method: 'delete' + }); +} diff --git a/src/api/rbacRulesApi.ts b/src/api/rbacRulesApi.ts new file mode 100644 index 0000000..0575cb6 --- /dev/null +++ b/src/api/rbacRulesApi.ts @@ -0,0 +1,136 @@ +import { ApiRequestOptions } from '../hooks/useApi'; + +// ============================================================================ +// TYPES & INTERFACES +// ============================================================================ + +export interface RbacRule { + id: string; + [key: string]: any; // Allow additional properties from backend +} + +export type RbacRuleUpdateData = Partial>; + +export interface PaginationParams { + page?: number; + pageSize?: number; + sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; + filters?: Record; + search?: string; +} + +export interface PaginatedResponse { + items: T[]; + pagination?: { + currentPage: number; + pageSize: number; + totalItems: number; + totalPages: number; + }; +} + +// Type for the request function passed to API functions +export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; + +// ============================================================================ +// API REQUEST FUNCTIONS +// ============================================================================ + +/** + * Fetch list of RBAC rules with optional pagination + * Endpoint: GET /api/rbac/rules + */ +export async function fetchRbacRules( + request: ApiRequestFunction, + params?: PaginationParams +): Promise | RbacRule[]> { + const requestParams: any = {}; + + // Build pagination object if provided + if (params) { + const paginationObj: any = {}; + + if (params.page !== undefined) paginationObj.page = params.page; + if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; + if (params.sort) paginationObj.sort = params.sort; + if (params.filters) paginationObj.filters = params.filters; + if (params.search) paginationObj.search = params.search; + + if (Object.keys(paginationObj).length > 0) { + requestParams.pagination = JSON.stringify(paginationObj); + } + } + + const data = await request({ + url: '/api/rbac/rules', + method: 'get', + params: requestParams + }); + + return data; +} + +/** + * Fetch a single RBAC rule by ID + * Endpoint: GET /api/rbac/rules/{ruleId} + */ +export async function fetchRbacRuleById( + request: ApiRequestFunction, + ruleId: string +): Promise { + try { + const data = await request({ + url: `/api/rbac/rules/${ruleId}`, + method: 'get' + }); + return data || null; + } catch (error: any) { + console.error('Error fetching RBAC rule by ID:', error); + return null; + } +} + +/** + * Update a RBAC rule + * Endpoint: PUT /api/rbac/rules/{ruleId} + */ +export async function updateRbacRule( + request: ApiRequestFunction, + ruleId: string, + updateData: RbacRuleUpdateData +): Promise { + return await request({ + url: `/api/rbac/rules/${ruleId}`, + method: 'put', + data: updateData + }); +} + +/** + * Create a new RBAC rule + * Endpoint: POST /api/rbac/rules + */ +export async function createRbacRule( + request: ApiRequestFunction, + ruleData: Partial +): Promise { + return await request({ + url: '/api/rbac/rules', + method: 'post', + data: ruleData + }); +} + +/** + * Delete a RBAC rule + * Endpoint: DELETE /api/rbac/rules/{ruleId} + */ +export async function deleteRbacRule( + request: ApiRequestFunction, + ruleId: string +): Promise { + await request({ + url: `/api/rbac/rules/${ruleId}`, + method: 'delete' + }); +} diff --git a/src/api/roleApi.ts b/src/api/roleApi.ts new file mode 100644 index 0000000..38885b7 --- /dev/null +++ b/src/api/roleApi.ts @@ -0,0 +1,201 @@ +import { ApiRequestOptions } from '../hooks/useApi'; + +// ============================================================================ +// TYPES & INTERFACES +// ============================================================================ + +export interface Role { + id: string; + [key: string]: any; // Allow additional properties from backend +} + +export type RoleUpdateData = Partial>; + +export interface PaginationParams { + page?: number; + pageSize?: number; + sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; + filters?: Record; + search?: string; +} + +export interface PaginatedResponse { + items: T[]; + pagination?: { + currentPage: number; + pageSize: number; + totalItems: number; + totalPages: number; + }; +} + +// Type for the request function passed to API functions +export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; + +// ============================================================================ +// API REQUEST FUNCTIONS +// ============================================================================ + +/** + * Fetch list of roles with optional pagination + * Endpoint: GET /api/rbac/roles + * Query parameter: pagination (optional, JSON-encoded string) + * Example: /api/rbac/roles?pagination={"page":1,"pageSize":10} + */ +export async function fetchRoles( + request: ApiRequestFunction, + params?: PaginationParams +): Promise | Role[]> { + const requestParams: any = {}; + + // Build pagination object if provided + if (params) { + const paginationObj: any = {}; + + if (params.page !== undefined) paginationObj.page = params.page; + if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; + if (params.sort) paginationObj.sort = params.sort; + if (params.filters) paginationObj.filters = params.filters; + if (params.search) paginationObj.search = params.search; + + if (Object.keys(paginationObj).length > 0) { + requestParams.pagination = JSON.stringify(paginationObj); + } + } + + const data = await request({ + url: '/api/rbac/roles', + method: 'get', + params: requestParams + }); + + return data; +} + +/** + * Fetch a single role by ID + * Endpoint: GET /api/rbac/roles/{roleId} + */ +export async function fetchRoleById( + request: ApiRequestFunction, + roleId: string +): Promise { + try { + const data = await request({ + url: `/api/rbac/roles/${roleId}`, + method: 'get' + }); + return data || null; + } catch (error: any) { + console.error('Error fetching role by ID:', error); + return null; + } +} + +/** + * Fetch role options + * Endpoint: GET /api/rbac/roles/options + */ +export async function fetchRoleOptions( + request: ApiRequestFunction +): Promise { + try { + const data = await request({ + url: '/api/rbac/roles/options', + method: 'get' + }); + return data || null; + } catch (error: any) { + console.error('Error fetching role options:', error); + return null; + } +} + +/** + * Create a new role + * Endpoint: POST /api/rbac/roles + * Request body: Role object + * Required fields: + * - roleLabel: string (e.g., "admin", "user") + * - description: TextMultilingual object (at least en is required) + */ +export async function createRole( + request: ApiRequestFunction, + roleData: Partial +): Promise { + console.log('🔵 createRole - Complete request structure:', { + url: '/api/rbac/roles', + method: 'post', + requestOptions: { + url: '/api/rbac/roles', + method: 'post', + data: roleData + }, + roleData: roleData, + roleDataKeys: Object.keys(roleData || {}), + roleDataValues: Object.entries(roleData || {}).map(([key, value]) => ({ + key, + value, + type: typeof value, + isObject: typeof value === 'object' && value !== null, + isArray: Array.isArray(value), + stringified: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value) + })), + dataStringified: JSON.stringify(roleData, null, 2), + dataStringifiedCompact: JSON.stringify(roleData) + }); + + try { + const result = await request({ + url: '/api/rbac/roles', + method: 'post', + data: roleData + }); + + console.log('✅ createRole - Response:', result); + + return result; + } catch (error: any) { + console.error('❌ createRole - Request failed:', { + error, + errorMessage: error.message, + errorResponse: error.response, + errorResponseData: error.response?.data, + errorResponseStatus: error.response?.status, + errorResponseHeaders: error.response?.headers, + errorRequest: error.request, + errorConfig: error.config + }); + throw error; + } +} + +/** + * Update a role + * Endpoint: PUT /api/rbac/roles/{roleId} + */ +export async function updateRole( + request: ApiRequestFunction, + roleId: string, + updateData: RoleUpdateData +): Promise { + return await request({ + url: `/api/rbac/roles/${roleId}`, + method: 'put', + data: updateData + }); +} + +/** + * Delete a role + * Endpoint: DELETE /api/rbac/roles/{roleId} + */ +export async function deleteRole( + request: ApiRequestFunction, + roleId: string +): Promise { + await request({ + url: `/api/rbac/roles/${roleId}`, + method: 'delete' + }); +} diff --git a/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx b/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx index 5b19f09..408c7c6 100644 --- a/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx +++ b/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx @@ -15,6 +15,47 @@ import { } from '../../../utils/attributeTypeMapper'; import type { AttributeType } from '../../../utils/attributeTypeMapper'; +// Helper function to detect TextMultilingual objects +// TextMultilingual has structure: { en: string, ge?: string, fr?: string, it?: string } +const isTextMultilingual = (value: any): boolean => { + if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) { + return false; + } + // Check if it has 'en' property (required) and optionally other language codes + return 'en' in value && typeof value.en === 'string'; +}; + +// Helper function to check if a field name suggests it's a multilingual field +// Only specific fields should be multilingual, not fields that just contain these words +const isMultilingualFieldName = (fieldName: string): boolean => { + const lowerFieldName = fieldName.toLowerCase(); + + // Exact matches for multilingual fields + const exactMultilingualFields = ['description']; + + // Fields that end with these patterns (but not roleLabel, etc.) + const multilingualPatterns = [ + /^description$/i, + /^label$/i, // Only exact "label", not "roleLabel" + /^title$/i, // Only exact "title" + /^name$/i // Only exact "name", not field names containing "name" + ]; + + // Check exact matches first + if (exactMultilingualFields.includes(lowerFieldName)) { + return true; + } + + // Check patterns - but exclude fields like "roleLabel" which should be strings + const excludedFields = ['rolelabel', 'role_label', 'rolename', 'role_name', 'username', 'user_name']; + if (excludedFields.includes(lowerFieldName)) { + return false; + } + + // Check if it matches multilingual patterns (exact match, not contains) + return multilingualPatterns.some(pattern => pattern.test(fieldName)); +}; + // Attribute definition interface (matches backend structure) export interface AttributeDefinition { name: string; @@ -180,13 +221,30 @@ export function FormGeneratorForm>({ // Initialize form data with defaults useEffect(() => { if (data) { - setFormData({ ...data }); + // Ensure TextMultilingual fields are properly initialized + const processedData: any = { ...data }; + const filteredAttrs = getFilteredAttributes(); + filteredAttrs.forEach(attr => { + if (isMultilingualFieldName(attr.name) && processedData[attr.name]) { + // If it's already a TextMultilingual object, keep it + if (!isTextMultilingual(processedData[attr.name])) { + // If it's a string, convert to TextMultilingual + if (typeof processedData[attr.name] === 'string') { + processedData[attr.name] = { en: processedData[attr.name] }; + } + } + } + }); + setFormData(processedData as T); } else { const filteredAttrs = getFilteredAttributes(); const initialData: any = {}; filteredAttrs.forEach(attr => { if (attr.default !== undefined) { initialData[attr.name] = attr.default; + } else if (isMultilingualFieldName(attr.name)) { + // Initialize TextMultilingual fields with empty object + initialData[attr.name] = { en: '' }; } else { initialData[attr.name] = getDefaultValueForType(attr.type); } @@ -321,10 +379,18 @@ export function FormGeneratorForm>({ const value = formData[attr.name]; // Check required fields - if (attr.required && (value === undefined || value === null || value === '' || - (Array.isArray(value) && value.length === 0))) { - newErrors[attr.name] = t('formgen.form.required', `${attr.label} is required`); - return; + if (attr.required) { + // Special handling for TextMultilingual fields + if (isMultilingualFieldName(attr.name) && isTextMultilingual(value)) { + if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') { + newErrors[attr.name] = t('formgen.form.required', `${attr.label} (English) is required`); + return; + } + } else if (value === undefined || value === null || value === '' || + (Array.isArray(value) && value.length === 0)) { + newErrors[attr.name] = t('formgen.form.required', `${attr.label} is required`); + return; + } } // Type-specific validation @@ -410,9 +476,53 @@ export function FormGeneratorForm>({ return; } + // Prepare form data - ensure roleLabel is a string if it exists + const preparedFormData = { ...formData }; + + // Ensure roleLabel is a string (not object/array/null/undefined) + if ('roleLabel' in preparedFormData) { + const roleLabelValue = (preparedFormData as any).roleLabel; + if (typeof roleLabelValue !== 'string') { + // Convert to string if it's not already + if (roleLabelValue === null || roleLabelValue === undefined) { + // Remove if null/undefined - let validation handle it + delete (preparedFormData as any).roleLabel; + } else if (typeof roleLabelValue === 'object') { + // If it's an object, try to extract a string value or remove it + console.warn('⚠️ roleLabel is an object, removing it:', roleLabelValue); + delete (preparedFormData as any).roleLabel; + } else { + // Convert to string + (preparedFormData as any).roleLabel = String(roleLabelValue); + } + } + } + + console.log('📤 FormGeneratorForm - handleSubmit - Complete formData structure:', { + originalFormData: formData, + preparedFormData: preparedFormData, + formDataKeys: Object.keys(preparedFormData), + formDataEntries: Object.entries(preparedFormData).map(([key, value]) => ({ + key, + value, + valueType: typeof value, + valueIsObject: typeof value === 'object' && value !== null && !Array.isArray(value), + valueIsArray: Array.isArray(value), + valueIsNull: value === null, + valueIsUndefined: value === undefined, + valueStringified: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value) + })), + formDataStringified: JSON.stringify(preparedFormData, null, 2), + formDataStringifiedCompact: JSON.stringify(preparedFormData), + hasRoleLabel: 'roleLabel' in preparedFormData, + roleLabelValue: (preparedFormData as any).roleLabel, + roleLabelType: typeof (preparedFormData as any).roleLabel, + roleLabelIsString: typeof (preparedFormData as any).roleLabel === 'string' + }); + try { setSubmitting(true); - await onSubmit(formData); + await onSubmit(preparedFormData); } catch (error: any) { console.error('Form submission error:', error); // Handle backend validation errors @@ -448,11 +558,83 @@ export function FormGeneratorForm>({ } }; + // Render multilingual field + const renderMultilingualField = (attr: AttributeDefinition) => { + const value = formData[attr.name] || { en: '' }; + const hasError = errors[attr.name]; + const isReadonly = mode === 'display' || attr.readonly || attr.editable === false; + + // Ensure value is a TextMultilingual object + const multilingualValue = isTextMultilingual(value) ? value : { en: typeof value === 'string' ? value : '' }; + + const languages = [ + { code: 'en', label: 'English', required: true }, + { code: 'ge', label: 'German', required: false }, + { code: 'fr', label: 'French', required: false }, + { code: 'it', label: 'Italian', required: false } + ]; + + const handleMultilingualChange = (langCode: string, langValue: string) => { + const newValue = { ...multilingualValue, [langCode]: langValue }; + handleFieldChange(attr.name, newValue); + }; + + if (isReadonly) { + // Display mode - show all languages + const displayValues = languages + .filter(lang => multilingualValue[lang.code] && multilingualValue[lang.code].trim()) + .map(lang => `${lang.label}: ${multilingualValue[lang.code]}`) + .join(' | '); + + return ( +
+
+ {displayValues || t('common.na', 'N/A')} +
+ +
+ ); + } + + return ( +
+ + {languages.map(lang => ( +
+ handleMultilingualChange(lang.code, e.target.value)} + onFocus={() => handleFieldFocus(`${attr.name}.${lang.code}`, true)} + onBlur={() => handleFieldFocus(`${attr.name}.${lang.code}`, false)} + className={`${styles.fieldInput} ${hasError && lang.code === 'en' ? styles.fieldError : ''}`} + /> + +
+ ))} + {hasError && {hasError}} +
+ ); + }; + // Render field based on attribute type const renderField = (attr: AttributeDefinition) => { const value = formData[attr.name]; const hasError = errors[attr.name]; const isReadonly = mode === 'display' || attr.readonly || attr.editable === false; + + // Check if this is a multilingual field + if (isMultilingualFieldName(attr.name) && (isTextMultilingual(value) || value === undefined || value === null || value === '')) { + return renderMultilingualField(attr); + } // Readonly/Display field if (isReadonly) { @@ -621,7 +803,6 @@ export function FormGeneratorForm>({ onBlur={() => handleFieldFocus(attr.name, false)} className={textareaClassName} rows={minRows} - placeholder={attr.placeholder} ref={(textarea) => { if (textarea) { textarea.style.setProperty('min-height', `${minHeight}px`, 'important'); @@ -681,7 +862,6 @@ export function FormGeneratorForm>({ onFocus={() => handleFieldFocus(attr.name, true)} onBlur={() => handleFieldFocus(attr.name, false)} className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`} - placeholder={attr.placeholder} />