fixed trustee access
This commit is contained in:
parent
c5d60c4c82
commit
2b96ab7b66
11 changed files with 148 additions and 45 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue