From 2b96ab7b665980eb6f9572950a196d3f095264f8 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 13 Jan 2026 23:16:37 +0100 Subject: [PATCH] fixed trustee access --- .../FormGeneratorForm/FormGeneratorForm.tsx | 51 +++-------- .../FormGeneratorTable/FormGeneratorTable.tsx | 3 +- src/core/PageManager/PageRenderer.tsx | 19 +++- .../PageManager/data/pages/trustee/access.ts | 2 + .../data/pages/trustee/contracts.ts | 2 + .../data/pages/trustee/documents.ts | 2 + .../data/pages/trustee/organisations.ts | 2 + .../data/pages/trustee/positions.ts | 2 + .../PageManager/data/pages/trustee/roles.ts | 2 + src/hooks/useTrustee.ts | 89 ++++++++++++++++++- src/hooks/useUsers.ts | 19 +++- 11 files changed, 148 insertions(+), 45 deletions(-) diff --git a/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx b/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx index 46b12a4..2e9e7a7 100644 --- a/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx +++ b/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx @@ -26,36 +26,8 @@ const isTextMultilingual = (value: any): boolean => { 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.) - // Note: "name" is NOT multilingual - Mandate.name and other name fields are plain strings - const multilingualPatterns = [ - /^description$/i, - /^label$/i, // Only exact "label", not "roleLabel" - /^title$/i // Only exact "title" - ]; - - // 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)); -}; +// Note: Field types are determined ONLY by the explicit 'type' property. +// FormGeneratorForm does NOT infer types from field names. // Attribute definition interface (matches backend structure) export interface AttributeDefinition { @@ -222,8 +194,10 @@ export function FormGeneratorForm>({ // Initialize form data with defaults useEffect(() => { // Helper to check if a field should be treated as multilingual - const isMultilingual = (attr: AttributeDefinition) => - isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name); + // Only use explicit type - do NOT infer from field name + const isMultilingual = (attr: AttributeDefinition) => { + return isMultilingualType(attr.type as AttributeType); + }; if (data) { // Ensure TextMultilingual fields are properly initialized @@ -385,9 +359,9 @@ export function FormGeneratorForm>({ // Check required fields if (attr.required) { - // Special handling for TextMultilingual fields (by type or field name) - const isMultilingual = isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name); - if (isMultilingual && isTextMultilingual(value)) { + // Special handling for TextMultilingual fields (by explicit type only) + const isMultilingualField = isMultilingualType(attr.type as AttributeType); + if (isMultilingualField && 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; @@ -637,9 +611,10 @@ export function FormGeneratorForm>({ const hasError = errors[attr.name]; const isReadonly = mode === 'display' || attr.readonly || attr.editable === false; - // Check if this is a multilingual field - either by type or by field name convention - if ((isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name)) && - (isTextMultilingual(value) || value === undefined || value === null || value === '')) { + // Check if this is a multilingual field - by explicit type ONLY + const shouldRenderAsMultilingual = isMultilingualType(attr.type as AttributeType) && + (isTextMultilingual(value) || value === undefined || value === null || value === ''); + if (shouldRenderAsMultilingual) { return renderMultilingualField(attr); } diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx index 5976703..2fbabc1 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx @@ -806,7 +806,8 @@ export function FormGeneratorTable>({ if (onInlineUpdate) { await onInlineUpdate(row, column.key, newValue); } else if (hookData?.handleInlineUpdate) { - await hookData.handleInlineUpdate(rowId, { [column.key]: newValue }); + // Pass row as third parameter for hooks that need to merge changes with existing data + await hookData.handleInlineUpdate(rowId, { [column.key]: newValue }, row); } // Only refetch if we DON'T have optimistic update (to get fresh data) diff --git a/src/core/PageManager/PageRenderer.tsx b/src/core/PageManager/PageRenderer.tsx index 692a0d1..9fb2e01 100644 --- a/src/core/PageManager/PageRenderer.tsx +++ b/src/core/PageManager/PageRenderer.tsx @@ -2182,6 +2182,9 @@ const PageRenderer: React.FC = ({ // Create a wrapper for onCreate that ensures attributes are loaded (define before use) const wrappedCreateOperation = async (formData: any) => { + // Debug: Log form data being submitted + console.warn('🔧 wrappedCreateOperation - formData:', formData); + // Ensure attributes are loaded before creating (if function exists) if (hookDataAny.ensureAttributesLoaded && typeof hookDataAny.ensureAttributesLoaded === 'function') { await hookDataAny.ensureAttributesLoaded(); @@ -2189,8 +2192,20 @@ const PageRenderer: React.FC = ({ return await createOperation(formData); }; - // Use dynamic fields from backend attributes - const generatedFields = generateFieldsFunction(); + // Prefer custom formConfig.fields if defined, otherwise use dynamic fields from backend attributes + const customFields = button.formConfig.fields; + const generatedFields = customFields && customFields.length > 0 + ? customFields + : generateFieldsFunction(); + + // Debug: Log which function is used and what fields are generated + console.log('🔧 CreateButton fields generation:', { + hasCustomFields: !!customFields && customFields.length > 0, + hasCreateFields: !!hookDataAny.generateCreateFieldsFromAttributes, + hasEditFields: !!hookDataAny.generateEditFieldsFromAttributes, + generatedFieldsCount: generatedFields?.length || 0, + generatedFieldKeys: generatedFields?.map((f: any) => f.key) || [] + }); // Check if attributes are still loading const attributes = hookDataAny.attributes; diff --git a/src/core/PageManager/data/pages/trustee/access.ts b/src/core/PageManager/data/pages/trustee/access.ts index c28457a..5a8f7be 100644 --- a/src/core/PageManager/data/pages/trustee/access.ts +++ b/src/core/PageManager/data/pages/trustee/access.ts @@ -36,6 +36,7 @@ const createAccessHook = () => { permissions, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded } = useTrusteeAccess(); const { @@ -96,6 +97,7 @@ const createAccessHook = () => { columns: generatedColumns, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded }; }; diff --git a/src/core/PageManager/data/pages/trustee/contracts.ts b/src/core/PageManager/data/pages/trustee/contracts.ts index ac0c03a..f540e12 100644 --- a/src/core/PageManager/data/pages/trustee/contracts.ts +++ b/src/core/PageManager/data/pages/trustee/contracts.ts @@ -36,6 +36,7 @@ const createContractsHook = () => { permissions, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded } = useTrusteeContracts(); const { @@ -96,6 +97,7 @@ const createContractsHook = () => { columns: generatedColumns, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded }; }; diff --git a/src/core/PageManager/data/pages/trustee/documents.ts b/src/core/PageManager/data/pages/trustee/documents.ts index f12089e..4ce603d 100644 --- a/src/core/PageManager/data/pages/trustee/documents.ts +++ b/src/core/PageManager/data/pages/trustee/documents.ts @@ -41,6 +41,7 @@ const createDocumentsHook = () => { permissions, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded } = useTrusteeDocuments(); const { @@ -101,6 +102,7 @@ const createDocumentsHook = () => { columns: generatedColumns, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded }; }; diff --git a/src/core/PageManager/data/pages/trustee/organisations.ts b/src/core/PageManager/data/pages/trustee/organisations.ts index d850e5f..e28ec2c 100644 --- a/src/core/PageManager/data/pages/trustee/organisations.ts +++ b/src/core/PageManager/data/pages/trustee/organisations.ts @@ -38,6 +38,7 @@ const createOrganisationsHook = () => { permissions, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded } = useTrusteeOrganisations(); const { @@ -98,6 +99,7 @@ const createOrganisationsHook = () => { columns: generatedColumns, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded }; }; diff --git a/src/core/PageManager/data/pages/trustee/positions.ts b/src/core/PageManager/data/pages/trustee/positions.ts index e927f0f..aebf24c 100644 --- a/src/core/PageManager/data/pages/trustee/positions.ts +++ b/src/core/PageManager/data/pages/trustee/positions.ts @@ -36,6 +36,7 @@ const createPositionsHook = () => { permissions, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded } = useTrusteePositions(); const { @@ -100,6 +101,7 @@ const createPositionsHook = () => { columns: generatedColumns, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded }; }; diff --git a/src/core/PageManager/data/pages/trustee/roles.ts b/src/core/PageManager/data/pages/trustee/roles.ts index 79b02eb..2a20d20 100644 --- a/src/core/PageManager/data/pages/trustee/roles.ts +++ b/src/core/PageManager/data/pages/trustee/roles.ts @@ -36,6 +36,7 @@ const createRolesHook = () => { permissions, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded } = useTrusteeRoles(); const { @@ -96,6 +97,7 @@ const createRolesHook = () => { columns: generatedColumns, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded }; }; diff --git a/src/hooks/useTrustee.ts b/src/hooks/useTrustee.ts index 9d68038..490d69b 100644 --- a/src/hooks/useTrustee.ts +++ b/src/hooks/useTrustee.ts @@ -198,9 +198,14 @@ function _createTrusteeEntityHook(config: TrusteeEntit return attributes .filter(attr => { + // For EDIT mode: filter out readonly fields and system fields if (attr.readonly === true || attr.editable === false) { return false; } + // Also filter out 'id' for edit mode (id cannot be changed) + if (attr.name === 'id') { + return false; + } const nonEditableFields = ['_createdBy', '_createdAt', '_modifiedBy', '_modifiedAt']; return !nonEditableFields.includes(attr.name); }) @@ -260,6 +265,75 @@ function _createTrusteeEntityHook(config: TrusteeEntit }); }, [attributes]); + // Generate fields for CREATE forms - includes all required fields like 'id' + const generateCreateFieldsFromAttributes = useCallback(() => { + if (!attributes || attributes.length === 0) { + return []; + } + + return attributes + .filter(attr => { + // For CREATE mode: include all user-editable fields including 'id' + // Only filter out system-generated fields + const systemFields = ['_createdBy', '_createdAt', '_modifiedBy', '_modifiedAt', 'mandateId']; + return !systemFields.includes(attr.name); + }) + .map(attr => { + let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' | 'number' = 'string'; + let options: Array<{ value: string | number; label: string }> | undefined = undefined; + let optionsReference: string | undefined = undefined; + + if (attr.type === 'checkbox') { + fieldType = 'boolean'; + } else if (attr.type === 'email') { + fieldType = 'email'; + } else if (attr.type === 'date') { + fieldType = 'date'; + } else if (attr.type === 'number') { + fieldType = 'number'; + } else if (attr.type === 'select') { + fieldType = 'enum'; + if (Array.isArray(attr.options)) { + options = attr.options.map((opt: any) => { + const labelValue = typeof opt.label === 'string' + ? opt.label + : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); + return { value: opt.value, label: labelValue }; + }); + } else if (typeof attr.options === 'string') { + optionsReference = attr.options; + } + } else if (attr.type === 'multiselect') { + fieldType = 'multiselect'; + if (Array.isArray(attr.options)) { + options = attr.options.map((opt: any) => { + const labelValue = typeof opt.label === 'string' + ? opt.label + : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); + return { value: opt.value, label: labelValue }; + }); + } else if (typeof attr.options === 'string') { + optionsReference = attr.options; + } + } else if (attr.type === 'textarea') { + fieldType = 'textarea'; + } else if (attr.type === 'timestamp') { + fieldType = 'readonly'; + } + + return { + key: attr.name, + label: attr.label || attr.name, + type: fieldType, + editable: true, // All fields are editable in create mode + required: attr.required === true, + options, + optionsReference, + dependsOn: attr.dependsOn + }; + }); + }, [attributes]); + const ensureAttributesLoaded = useCallback(async () => { if (attributes && attributes.length > 0) { return attributes; @@ -288,6 +362,7 @@ function _createTrusteeEntityHook(config: TrusteeEntit pagination, fetchById, generateEditFieldsFromAttributes, + generateCreateFieldsFromAttributes, ensureAttributesLoaded }; }; @@ -326,12 +401,22 @@ function _createTrusteeOperationsHook(config: TrusteeE setCreateError(null); setCreatingItem(true); + // Debug: Log what data is being sent to the backend + console.warn('🔧 handleCreate called with itemData:', itemData); + try { const newItem = await config.create(request, itemData); return { success: true, data: newItem }; } catch (error: any) { - setCreateError(error.message); - return { success: false, error: error.message }; + // Debug: Log full error details + console.error('🔧 handleCreate error:', { + message: error.message, + response: error.response?.data, + status: error.response?.status + }); + const errorMessage = error.response?.data?.detail || error.message; + setCreateError(errorMessage); + return { success: false, error: errorMessage }; } finally { setCreatingItem(false); } diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts index c174040..263bba7 100644 --- a/src/hooks/useUsers.ts +++ b/src/hooks/useUsers.ts @@ -923,8 +923,23 @@ export function useUserOperations() { }; // Generic inline update handler for FormGeneratorTable - const handleInlineUpdate = async (userId: string, changes: Partial) => { - const result = await handleUserUpdate(userId, changes as UserUpdateData); + // Must merge changes with existing row data because backend requires full object + // The existingRow parameter is passed from FormGeneratorTable which has access to row data + const handleInlineUpdate = async (userId: string, changes: Partial, existingRow?: any) => { + if (!existingRow) { + throw new Error(`Existing row data required for inline update`); + } + + // Merge changes with existing row data (backend requires full object with required fields) + const mergedData: UserUpdateData = { + username: existingRow.username, + email: existingRow.email, + enabled: existingRow.enabled, + roleLabels: existingRow.roleLabels, + ...changes + }; + + const result = await handleUserUpdate(userId, mergedData); if (!result.success) { throw new Error(result.error || 'Failed to update'); }