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';
|
return 'en' in value && typeof value.en === 'string';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to check if a field name suggests it's a multilingual field
|
// Note: Field types are determined ONLY by the explicit 'type' property.
|
||||||
// Only specific fields should be multilingual, not fields that just contain these words
|
// FormGeneratorForm does NOT infer types from field names.
|
||||||
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));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attribute definition interface (matches backend structure)
|
// Attribute definition interface (matches backend structure)
|
||||||
export interface AttributeDefinition {
|
export interface AttributeDefinition {
|
||||||
|
|
@ -222,8 +194,10 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
// Initialize form data with defaults
|
// Initialize form data with defaults
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Helper to check if a field should be treated as multilingual
|
// Helper to check if a field should be treated as multilingual
|
||||||
const isMultilingual = (attr: AttributeDefinition) =>
|
// Only use explicit type - do NOT infer from field name
|
||||||
isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name);
|
const isMultilingual = (attr: AttributeDefinition) => {
|
||||||
|
return isMultilingualType(attr.type as AttributeType);
|
||||||
|
};
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// Ensure TextMultilingual fields are properly initialized
|
// Ensure TextMultilingual fields are properly initialized
|
||||||
|
|
@ -385,9 +359,9 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
|
|
||||||
// Check required fields
|
// Check required fields
|
||||||
if (attr.required) {
|
if (attr.required) {
|
||||||
// Special handling for TextMultilingual fields (by type or field name)
|
// Special handling for TextMultilingual fields (by explicit type only)
|
||||||
const isMultilingual = isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name);
|
const isMultilingualField = isMultilingualType(attr.type as AttributeType);
|
||||||
if (isMultilingual && isTextMultilingual(value)) {
|
if (isMultilingualField && isTextMultilingual(value)) {
|
||||||
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
||||||
newErrors[attr.name] = t('formgen.form.required', `${attr.label} (English) is required`);
|
newErrors[attr.name] = t('formgen.form.required', `${attr.label} (English) is required`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -637,9 +611,10 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
const hasError = errors[attr.name];
|
const hasError = errors[attr.name];
|
||||||
const isReadonly = mode === 'display' || attr.readonly || attr.editable === false;
|
const isReadonly = mode === 'display' || attr.readonly || attr.editable === false;
|
||||||
|
|
||||||
// Check if this is a multilingual field - either by type or by field name convention
|
// Check if this is a multilingual field - by explicit type ONLY
|
||||||
if ((isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name)) &&
|
const shouldRenderAsMultilingual = isMultilingualType(attr.type as AttributeType) &&
|
||||||
(isTextMultilingual(value) || value === undefined || value === null || value === '')) {
|
(isTextMultilingual(value) || value === undefined || value === null || value === '');
|
||||||
|
if (shouldRenderAsMultilingual) {
|
||||||
return renderMultilingualField(attr);
|
return renderMultilingualField(attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -806,7 +806,8 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
if (onInlineUpdate) {
|
if (onInlineUpdate) {
|
||||||
await onInlineUpdate(row, column.key, newValue);
|
await onInlineUpdate(row, column.key, newValue);
|
||||||
} else if (hookData?.handleInlineUpdate) {
|
} 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)
|
// 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)
|
// Create a wrapper for onCreate that ensures attributes are loaded (define before use)
|
||||||
const wrappedCreateOperation = async (formData: any) => {
|
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)
|
// Ensure attributes are loaded before creating (if function exists)
|
||||||
if (hookDataAny.ensureAttributesLoaded && typeof hookDataAny.ensureAttributesLoaded === 'function') {
|
if (hookDataAny.ensureAttributesLoaded && typeof hookDataAny.ensureAttributesLoaded === 'function') {
|
||||||
await hookDataAny.ensureAttributesLoaded();
|
await hookDataAny.ensureAttributesLoaded();
|
||||||
|
|
@ -2189,8 +2192,20 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
return await createOperation(formData);
|
return await createOperation(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use dynamic fields from backend attributes
|
// Prefer custom formConfig.fields if defined, otherwise use dynamic fields from backend attributes
|
||||||
const generatedFields = generateFieldsFunction();
|
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
|
// Check if attributes are still loading
|
||||||
const attributes = hookDataAny.attributes;
|
const attributes = hookDataAny.attributes;
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const createAccessHook = () => {
|
||||||
permissions,
|
permissions,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
} = useTrusteeAccess();
|
} = useTrusteeAccess();
|
||||||
const {
|
const {
|
||||||
|
|
@ -96,6 +97,7 @@ const createAccessHook = () => {
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const createContractsHook = () => {
|
||||||
permissions,
|
permissions,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
} = useTrusteeContracts();
|
} = useTrusteeContracts();
|
||||||
const {
|
const {
|
||||||
|
|
@ -96,6 +97,7 @@ const createContractsHook = () => {
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ const createDocumentsHook = () => {
|
||||||
permissions,
|
permissions,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
} = useTrusteeDocuments();
|
} = useTrusteeDocuments();
|
||||||
const {
|
const {
|
||||||
|
|
@ -101,6 +102,7 @@ const createDocumentsHook = () => {
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ const createOrganisationsHook = () => {
|
||||||
permissions,
|
permissions,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
} = useTrusteeOrganisations();
|
} = useTrusteeOrganisations();
|
||||||
const {
|
const {
|
||||||
|
|
@ -98,6 +99,7 @@ const createOrganisationsHook = () => {
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const createPositionsHook = () => {
|
||||||
permissions,
|
permissions,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
} = useTrusteePositions();
|
} = useTrusteePositions();
|
||||||
const {
|
const {
|
||||||
|
|
@ -100,6 +101,7 @@ const createPositionsHook = () => {
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const createRolesHook = () => {
|
||||||
permissions,
|
permissions,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
} = useTrusteeRoles();
|
} = useTrusteeRoles();
|
||||||
const {
|
const {
|
||||||
|
|
@ -96,6 +97,7 @@ const createRolesHook = () => {
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -198,9 +198,14 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
.filter(attr => {
|
.filter(attr => {
|
||||||
|
// For EDIT mode: filter out readonly fields and system fields
|
||||||
if (attr.readonly === true || attr.editable === false) {
|
if (attr.readonly === true || attr.editable === false) {
|
||||||
return 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'];
|
const nonEditableFields = ['_createdBy', '_createdAt', '_modifiedBy', '_modifiedAt'];
|
||||||
return !nonEditableFields.includes(attr.name);
|
return !nonEditableFields.includes(attr.name);
|
||||||
})
|
})
|
||||||
|
|
@ -260,6 +265,75 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
||||||
});
|
});
|
||||||
}, [attributes]);
|
}, [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 () => {
|
const ensureAttributesLoaded = useCallback(async () => {
|
||||||
if (attributes && attributes.length > 0) {
|
if (attributes && attributes.length > 0) {
|
||||||
return attributes;
|
return attributes;
|
||||||
|
|
@ -288,6 +362,7 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
||||||
pagination,
|
pagination,
|
||||||
fetchById,
|
fetchById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -326,12 +401,22 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
|
||||||
setCreateError(null);
|
setCreateError(null);
|
||||||
setCreatingItem(true);
|
setCreatingItem(true);
|
||||||
|
|
||||||
|
// Debug: Log what data is being sent to the backend
|
||||||
|
console.warn('🔧 handleCreate called with itemData:', itemData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newItem = await config.create(request, itemData);
|
const newItem = await config.create(request, itemData);
|
||||||
return { success: true, data: newItem };
|
return { success: true, data: newItem };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setCreateError(error.message);
|
// Debug: Log full error details
|
||||||
return { success: false, error: error.message };
|
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 {
|
} finally {
|
||||||
setCreatingItem(false);
|
setCreatingItem(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -923,8 +923,23 @@ export function useUserOperations() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generic inline update handler for FormGeneratorTable
|
// Generic inline update handler for FormGeneratorTable
|
||||||
const handleInlineUpdate = async (userId: string, changes: Partial<UserUpdateData>) => {
|
// Must merge changes with existing row data because backend requires full object
|
||||||
const result = await handleUserUpdate(userId, changes as UserUpdateData);
|
// 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) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || 'Failed to update');
|
throw new Error(result.error || 'Failed to update');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue