Data: roles & rules: fixed id and pydantic handling ui+gateway Table generic: Fixed column width changing with persistent user width not overwritten by default in the session Table generic: fixed active item in navbar working with subpaths Table generic: fixed sort and pagination with modified routes Table generic: enhanced logic to select page directly and bar on top of the table Table generic: Always load data with pagination, fixed page counter (not updated), enhanced pagination to access pages directly Table generic: Filter function enhanced directly as dropdown in column headers, sorting enhanced to have cascading sorting Table generic: Added multilingual to AttributeType and handle it in the form renderer, removed explicit field definitions from team-members.ts and prompts.ts to use dynamic generation from backend attributes strictly Table generic: Inline editting of checkboxes and boolean values Table generig: Only generic logic, no explicit logic (e.g. id's) Table generic: Removed all specific parts like action icons. To e a parameter by calling instance.
675 lines
24 KiB
TypeScript
675 lines
24 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { useApiRequest } from './useApi';
|
|
import { getUserDataCache } from '../utils/userCache';
|
|
import { usePermissions, type UserPermissions } from './usePermissions';
|
|
import {
|
|
fetchRoles as fetchRolesApi,
|
|
fetchRoleById as fetchRoleByIdApi,
|
|
createRole as createRoleApi,
|
|
updateRole as updateRoleApi,
|
|
deleteRole as deleteRoleApi,
|
|
type Role,
|
|
type RoleUpdateData,
|
|
type PaginationParams
|
|
} from '../api/roleApi';
|
|
import { fetchAttributes } from '../api/attributesApi';
|
|
import type { AttributeDefinition } from '../api/attributesApi';
|
|
import {
|
|
isCheckboxType,
|
|
isSelectType,
|
|
isMultiselectType,
|
|
isDateTimeType,
|
|
isTextareaType,
|
|
type AttributeType
|
|
} from '../utils/attributeTypeMapper';
|
|
|
|
// Re-export types for backward compatibility
|
|
export type { Role, RoleUpdateData, AttributeDefinition, PaginationParams };
|
|
|
|
// 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));
|
|
};
|
|
|
|
// RBAC roles hook (list, update, delete) - following mandates pattern
|
|
export function useRbacRoles() {
|
|
const [roles, setRoles] = useState<Role[]>([]);
|
|
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
|
|
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
|
const [pagination, setPagination] = useState<{
|
|
currentPage: number;
|
|
pageSize: number;
|
|
totalItems: number;
|
|
totalPages: number;
|
|
} | null>(null);
|
|
const { request, isLoading: loading, error } = useApiRequest<null, Role[]>();
|
|
const { checkPermission } = usePermissions();
|
|
|
|
// Fetch attributes from backend
|
|
const fetchAttributesData = useCallback(async () => {
|
|
try {
|
|
const attrs = await fetchAttributes(request, 'Role');
|
|
setAttributes(attrs);
|
|
return attrs;
|
|
} catch (error: any) {
|
|
// Don't log 429 errors as errors (they're rate limit warnings)
|
|
if (error.response?.status === 429) {
|
|
console.warn('Rate limit exceeded while fetching role attributes. Please wait.');
|
|
} else if (error.response?.status !== 401) {
|
|
// Only log non-auth errors (401 is expected when not logged in)
|
|
console.error('Error fetching attributes:', error);
|
|
}
|
|
setAttributes([]);
|
|
return [];
|
|
}
|
|
}, [request]);
|
|
|
|
// Fetch permissions from backend
|
|
const fetchPermissions = useCallback(async () => {
|
|
try {
|
|
const perms = await checkPermission('DATA', 'Role');
|
|
setPermissions(perms);
|
|
return perms;
|
|
} catch (error: any) {
|
|
console.error('Error fetching permissions:', error);
|
|
const defaultPerms: UserPermissions = {
|
|
view: false,
|
|
read: 'n',
|
|
create: 'n',
|
|
update: 'n',
|
|
delete: 'n',
|
|
};
|
|
setPermissions(defaultPerms);
|
|
return defaultPerms;
|
|
}
|
|
}, [checkPermission]);
|
|
|
|
const fetchRoles = useCallback(async (params?: PaginationParams) => {
|
|
try {
|
|
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 fetchRolesApi(request, params);
|
|
|
|
// Handle paginated response
|
|
if (data && typeof data === 'object' && 'items' in data) {
|
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
setRoles(items);
|
|
if (data.pagination) {
|
|
setPagination(data.pagination);
|
|
}
|
|
} else {
|
|
// Handle non-paginated response (backward compatibility)
|
|
const items = Array.isArray(data) ? data : [];
|
|
setRoles(items);
|
|
setPagination(null);
|
|
}
|
|
} catch (error: any) {
|
|
// Error is already handled by useApiRequest
|
|
setRoles([]);
|
|
setPagination(null);
|
|
}
|
|
}, [request]);
|
|
|
|
// Optimistically remove a role from the local state
|
|
const removeOptimistically = (roleId: string) => {
|
|
setRoles(prevRoles => prevRoles.filter(role => role.id !== roleId));
|
|
};
|
|
|
|
// Optimistically update a role in the local state
|
|
const updateOptimistically = (roleId: string, updateData: Partial<Role>) => {
|
|
setRoles(prevRoles =>
|
|
prevRoles.map(role =>
|
|
role.id === roleId
|
|
? { ...role, ...updateData }
|
|
: role
|
|
)
|
|
);
|
|
};
|
|
|
|
// Fetch a single role by ID
|
|
const fetchRoleById = useCallback(async (roleId: string): Promise<Role | null> => {
|
|
return await fetchRoleByIdApi(request, roleId);
|
|
}, [request]);
|
|
|
|
// Generate edit fields from attributes dynamically using attributeTypeMapper utilities
|
|
const generateEditFieldsFromAttributes = useCallback((): Array<{
|
|
key: string;
|
|
label: string;
|
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
|
editable?: boolean;
|
|
required?: boolean;
|
|
validator?: (value: any) => string | null;
|
|
minRows?: number;
|
|
maxRows?: number;
|
|
options?: Array<{ value: string | number; label: string }>;
|
|
optionsReference?: string; // For options that need to be fetched (e.g., "user.role")
|
|
}> => {
|
|
if (!attributes || attributes.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const editableFields = attributes
|
|
.filter(attr => {
|
|
// Filter out non-editable fields based on readonly/editable flags
|
|
if (attr.readonly === true || attr.editable === false) {
|
|
return false; // Don't show readonly fields in edit form
|
|
}
|
|
// Also filter out common non-editable fields
|
|
const nonEditableFields = ['id', 'roleId', '_createdBy', '_hideDelete'];
|
|
return !nonEditableFields.includes(attr.name);
|
|
})
|
|
.map(attr => {
|
|
// Map backend attribute type to form field type using attributeTypeMapper utilities
|
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
|
let optionsReference: string | undefined = undefined;
|
|
|
|
const attrType = attr.type as AttributeType;
|
|
|
|
// Use attributeTypeMapper utilities to determine field type
|
|
if (isCheckboxType(attrType)) {
|
|
fieldType = 'boolean';
|
|
} else if (attrType === 'email') {
|
|
fieldType = 'email';
|
|
} else if (isDateTimeType(attrType)) {
|
|
fieldType = 'date';
|
|
} else if (isSelectType(attrType)) {
|
|
fieldType = 'enum';
|
|
// Handle options - can be array or string reference
|
|
const attrOptions = (attr as any).options;
|
|
if (Array.isArray(attrOptions)) {
|
|
options = attrOptions.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 attrOptions === 'string') {
|
|
// Options reference (e.g., "user.role", "auth.authority")
|
|
optionsReference = attrOptions;
|
|
}
|
|
} else if (isMultiselectType(attrType)) {
|
|
fieldType = 'multiselect';
|
|
// Handle options - can be array or string reference
|
|
const attrOptions = (attr as any).options;
|
|
if (Array.isArray(attrOptions)) {
|
|
options = attrOptions.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 attrOptions === 'string') {
|
|
// Options reference (e.g., "user.role", "auth.authority")
|
|
optionsReference = attrOptions;
|
|
}
|
|
} else if (isTextareaType(attrType)) {
|
|
fieldType = 'textarea';
|
|
} else if (attrType === 'readonly') {
|
|
fieldType = 'readonly';
|
|
} else {
|
|
fieldType = 'string';
|
|
}
|
|
|
|
// Define validators and required fields
|
|
let required = attr.required === true;
|
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
|
let minRows: number | undefined = undefined;
|
|
let maxRows: number | undefined = undefined;
|
|
|
|
// Email validation
|
|
if (fieldType === 'email') {
|
|
validator = (value: any) => {
|
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
|
return 'Email cannot be empty';
|
|
}
|
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
return 'Invalid email format';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
// Textarea settings
|
|
else if (fieldType === 'textarea') {
|
|
minRows = 4;
|
|
maxRows = 8;
|
|
if (attr.name.toLowerCase().includes('content')) {
|
|
minRows = 6;
|
|
maxRows = 12;
|
|
}
|
|
}
|
|
// Multiselect validation
|
|
else if (fieldType === 'multiselect' && required) {
|
|
validator = (value: any[]) => {
|
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
|
return `${attr.label} is required`;
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
return {
|
|
key: attr.name,
|
|
label: attr.label || attr.name,
|
|
type: fieldType,
|
|
editable: (attr as any).editable !== false && (attr as any).readonly !== true,
|
|
required,
|
|
validator,
|
|
minRows,
|
|
maxRows,
|
|
options,
|
|
optionsReference
|
|
};
|
|
});
|
|
|
|
return editableFields;
|
|
}, [attributes]);
|
|
|
|
// Generate create fields from attributes dynamically using attributeTypeMapper utilities
|
|
const generateCreateFieldsFromAttributes = useCallback((): Array<{
|
|
key: string;
|
|
label: string;
|
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
|
required?: boolean;
|
|
validator?: (value: any) => string | null;
|
|
minRows?: number;
|
|
maxRows?: number;
|
|
options?: Array<{ value: string | number; label: string }>;
|
|
optionsReference?: string;
|
|
placeholder?: string;
|
|
}> => {
|
|
if (!attributes || attributes.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const createFields = attributes
|
|
.filter(attr => {
|
|
// Filter out non-editable fields and auto-generated fields for create forms
|
|
if (attr.readonly === true || attr.editable === false) {
|
|
return false;
|
|
}
|
|
// Filter out ID fields and other auto-generated fields
|
|
const nonEditableFields = ['id', 'roleId', '_createdBy', '_hideDelete'];
|
|
return !nonEditableFields.includes(attr.name);
|
|
})
|
|
.map(attr => {
|
|
// Map backend attribute type to form field type using attributeTypeMapper utilities
|
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
|
let optionsReference: string | undefined = undefined;
|
|
|
|
const attrType = attr.type as AttributeType;
|
|
|
|
// Use attributeTypeMapper utilities to determine field type
|
|
if (isCheckboxType(attrType)) {
|
|
fieldType = 'boolean';
|
|
} else if (attrType === 'email') {
|
|
fieldType = 'email';
|
|
} else if (isDateTimeType(attrType)) {
|
|
fieldType = 'date';
|
|
} else if (isSelectType(attrType)) {
|
|
fieldType = 'enum';
|
|
const attrOptions = (attr as any).options;
|
|
if (Array.isArray(attrOptions)) {
|
|
options = attrOptions.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 attrOptions === 'string') {
|
|
optionsReference = attrOptions;
|
|
}
|
|
} else if (isMultiselectType(attrType)) {
|
|
fieldType = 'multiselect';
|
|
const attrOptions = (attr as any).options;
|
|
if (Array.isArray(attrOptions)) {
|
|
options = attrOptions.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 attrOptions === 'string') {
|
|
optionsReference = attrOptions;
|
|
}
|
|
} else if (isTextareaType(attrType)) {
|
|
fieldType = 'textarea';
|
|
} else {
|
|
fieldType = 'string';
|
|
}
|
|
|
|
// Define validators and required fields
|
|
let required = attr.required === true;
|
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
|
let minRows: number | undefined = undefined;
|
|
let maxRows: number | undefined = undefined;
|
|
|
|
// Email validation
|
|
if (fieldType === 'email') {
|
|
validator = (value: any) => {
|
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
|
return 'Email cannot be empty';
|
|
}
|
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
return 'Invalid email format';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
// Textarea settings
|
|
else if (fieldType === 'textarea') {
|
|
minRows = 4;
|
|
maxRows = 8;
|
|
if (attr.name.toLowerCase().includes('content')) {
|
|
minRows = 6;
|
|
maxRows = 12;
|
|
}
|
|
}
|
|
// Multiselect validation
|
|
else if (fieldType === 'multiselect' && required) {
|
|
validator = (value: any[]) => {
|
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
|
return `${attr.label} is required`;
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
// String validation for required fields
|
|
else if (fieldType === 'string' && required) {
|
|
validator = (value: any) => {
|
|
// Check if this is a multilingual field (TextMultilingual object)
|
|
if (isMultilingualFieldName(attr.name)) {
|
|
// Handle TextMultilingual object
|
|
if (isTextMultilingual(value)) {
|
|
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
|
return `${attr.label} (English) is required`;
|
|
}
|
|
return null;
|
|
}
|
|
// If it's a multilingual field but value is not yet a TextMultilingual object,
|
|
// check if it's an empty object or needs initialization
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
// Empty object or object without 'en' property
|
|
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
|
return `${attr.label} (English) is required`;
|
|
}
|
|
return null;
|
|
}
|
|
// If it's a string, that's also valid (will be converted to TextMultilingual)
|
|
if (typeof value === 'string' && value.trim() !== '') {
|
|
return null;
|
|
}
|
|
// Empty or invalid value
|
|
return `${attr.label} (English) is required`;
|
|
}
|
|
// Regular string validation for non-multilingual fields
|
|
if (typeof value !== 'string' || !value || value.trim() === '') {
|
|
return `${attr.label} is required`;
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
return {
|
|
key: attr.name,
|
|
label: attr.label || attr.name,
|
|
type: fieldType,
|
|
required,
|
|
validator,
|
|
minRows,
|
|
maxRows,
|
|
options,
|
|
optionsReference,
|
|
placeholder: (attr as any).placeholder
|
|
};
|
|
});
|
|
|
|
return createFields;
|
|
}, [attributes]);
|
|
|
|
// Ensure attributes are loaded - can be called by EditActionButton
|
|
const ensureAttributesLoaded = useCallback(async () => {
|
|
// Don't fetch attributes if user is not authenticated (prevents 401 errors)
|
|
const currentUser = getUserDataCache();
|
|
if (!currentUser) {
|
|
return [];
|
|
}
|
|
|
|
if (attributes && attributes.length > 0) {
|
|
return attributes;
|
|
}
|
|
|
|
const fetchedAttributes = await fetchAttributesData();
|
|
return fetchedAttributes;
|
|
}, [attributes, fetchAttributesData]);
|
|
|
|
// Fetch attributes and permissions on mount (only if user is authenticated)
|
|
useEffect(() => {
|
|
const currentUser = getUserDataCache();
|
|
if (currentUser) {
|
|
fetchAttributesData();
|
|
fetchPermissions();
|
|
}
|
|
}, [fetchAttributesData, fetchPermissions]);
|
|
|
|
// Initial fetch
|
|
useEffect(() => {
|
|
fetchRoles();
|
|
}, [fetchRoles]);
|
|
|
|
return {
|
|
data: roles,
|
|
loading,
|
|
error,
|
|
refetch: fetchRoles,
|
|
removeOptimistically,
|
|
updateOptimistically,
|
|
attributes,
|
|
permissions,
|
|
pagination,
|
|
fetchRoleById,
|
|
generateEditFieldsFromAttributes,
|
|
generateCreateFieldsFromAttributes,
|
|
ensureAttributesLoaded
|
|
};
|
|
}
|
|
|
|
// Role operations hook
|
|
export function useRbacRoleOperations() {
|
|
const [deletingRoles, setDeletingRoles] = useState<Set<string>>(new Set());
|
|
const [editingRoles, setEditingRoles] = useState<Set<string>>(new Set());
|
|
const [creatingRole, setCreatingRole] = useState(false);
|
|
const { request, isLoading } = useApiRequest();
|
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
const [createError, setCreateError] = useState<string | null>(null);
|
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
|
|
|
const handleRoleDelete = async (roleId: string) => {
|
|
setDeleteError(null);
|
|
setDeletingRoles(prev => new Set(prev).add(roleId));
|
|
|
|
try {
|
|
await deleteRoleApi(request, roleId);
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
return true;
|
|
} catch (error: any) {
|
|
setDeleteError(error.message);
|
|
return false;
|
|
} finally {
|
|
setDeletingRoles(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(roleId);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleRoleCreate = async (roleData: Partial<Role>) => {
|
|
console.log('🟢 handleRoleCreate - Complete input structure:', {
|
|
roleData,
|
|
roleDataType: typeof roleData,
|
|
roleDataIsArray: Array.isArray(roleData),
|
|
roleDataIsNull: roleData === null,
|
|
roleDataIsUndefined: roleData === undefined,
|
|
roleDataKeys: roleData ? Object.keys(roleData) : [],
|
|
roleDataEntries: roleData ? Object.entries(roleData).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)
|
|
})) : [],
|
|
roleDataStringified: JSON.stringify(roleData, null, 2),
|
|
roleDataStringifiedCompact: JSON.stringify(roleData)
|
|
});
|
|
|
|
setCreateError(null);
|
|
setCreatingRole(true);
|
|
|
|
try {
|
|
const newRole = await createRoleApi(request, roleData);
|
|
console.log('✅ handleRoleCreate - Success, newRole:', newRole);
|
|
|
|
return { success: true, roleData: newRole };
|
|
} catch (error: any) {
|
|
console.error('❌ handleRoleCreate - Error:', error);
|
|
console.error('❌ handleRoleCreate - Complete error details:', {
|
|
error,
|
|
message: error.message,
|
|
response: error.response,
|
|
responseData: error.response?.data,
|
|
responseStatus: error.response?.status,
|
|
responseStatusText: error.response?.statusText,
|
|
responseHeaders: error.response?.headers,
|
|
request: error.request,
|
|
config: error.config,
|
|
stack: error.stack
|
|
});
|
|
setCreateError(error.message);
|
|
return { success: false, error: error.message };
|
|
} finally {
|
|
setCreatingRole(false);
|
|
}
|
|
};
|
|
|
|
const handleRoleUpdate = async (roleId: string, updateData: RoleUpdateData, _originalData?: any) => {
|
|
setUpdateError(null);
|
|
setEditingRoles(prev => new Set(prev).add(roleId));
|
|
|
|
try {
|
|
const updatedRole = await updateRoleApi(request, roleId, updateData);
|
|
|
|
return { success: true, roleData: updatedRole };
|
|
} catch (error: any) {
|
|
const errorMessage = error.response?.data?.message || error.message || 'Failed to update role';
|
|
const statusCode = error.response?.status;
|
|
|
|
setUpdateError(errorMessage);
|
|
|
|
return {
|
|
success: false,
|
|
error: errorMessage,
|
|
statusCode,
|
|
isPermissionError: statusCode === 403,
|
|
isValidationError: statusCode === 400
|
|
};
|
|
} finally {
|
|
setEditingRoles(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(roleId);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
// Generic inline update handler for FormGeneratorTable
|
|
const handleInlineUpdate = async (roleId: string, changes: Partial<RoleUpdateData>) => {
|
|
const result = await handleRoleUpdate(roleId, changes as RoleUpdateData);
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to update');
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return {
|
|
deletingRoles,
|
|
editingRoles,
|
|
creatingRole,
|
|
deleteError,
|
|
createError,
|
|
updateError,
|
|
handleRoleDelete,
|
|
handleRoleCreate,
|
|
handleRoleUpdate,
|
|
handleInlineUpdate,
|
|
isLoading
|
|
};
|
|
}
|