fixed trustee access

This commit is contained in:
ValueOn AG 2026-01-13 23:16:37 +01:00
parent c5d60c4c82
commit 2b96ab7b66
11 changed files with 148 additions and 45 deletions

View file

@ -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<T extends Record<string, any>>({
// 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<T extends Record<string, any>>({
// 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<T extends Record<string, any>>({
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);
}

View file

@ -806,7 +806,8 @@ export function FormGeneratorTable<T extends Record<string, any>>({
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)

View file

@ -2182,6 +2182,9 @@ const PageRenderer: React.FC<PageRendererProps> = ({
// 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<PageRendererProps> = ({
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;

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -198,9 +198,14 @@ function _createTrusteeEntityHook<T extends { id: string }>(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<T extends { id: string }>(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<T extends { id: string }>(config: TrusteeEntit
pagination,
fetchById,
generateEditFieldsFromAttributes,
generateCreateFieldsFromAttributes,
ensureAttributesLoaded
};
};
@ -326,12 +401,22 @@ function _createTrusteeOperationsHook<T extends { id: string }>(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);
}

View file

@ -923,8 +923,23 @@ export function useUserOperations() {
};
// Generic inline update handler for FormGeneratorTable
const handleInlineUpdate = async (userId: string, changes: Partial<UserUpdateData>) => {
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<UserUpdateData>, 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');
}