diff --git a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx index 36c8bcf..340311b 100644 --- a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx +++ b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx @@ -4,12 +4,14 @@ import styles from './FormGeneratorControls.module.css'; import { Button } from '../../UiComponents/Button'; import { IoIosRefresh } from "react-icons/io"; import { FaTrash } from "react-icons/fa"; +import { isCheckboxType } from '../../../utils/attributeTypeMapper'; +import type { AttributeType } from '../../../utils/attributeTypeMapper'; // Generic field/column config interface export interface FilterableField { key: string; label: string; - type?: 'string' | 'number' | 'date' | 'boolean' | 'enum' | 'readonly'; + type?: AttributeType; filterable?: boolean; filterOptions?: string[]; } @@ -215,7 +217,7 @@ export function FormGeneratorControls({
{filterableFields.map(field => (
- {field.type === 'boolean' ? ( + {field.type && isCheckboxType(field.type) ? (
>({ } // Default input field (text, email, date, time, url, password, number, integer, float) - const inputType = attr.type === 'email' ? 'email' : - attr.type === 'date' ? 'date' : - attr.type === 'time' ? 'time' : - attr.type === 'timestamp' ? 'datetime-local' : - attr.type === 'url' ? 'url' : - attr.type === 'password' ? 'password' : - (attr.type === 'number' || attr.type === 'integer' || attr.type === 'float') ? 'number' : - 'text'; + const inputType = attributeTypeToInputType(attr.type); return (
@@ -674,7 +673,7 @@ export function FormGeneratorForm>({ value={value || ''} onChange={(e) => { let newValue: any = e.target.value; - if (attr.type === 'number' || attr.type === 'integer' || attr.type === 'float') { + if (isNumberType(attr.type)) { newValue = e.target.value === '' ? '' : Number(e.target.value); } handleFieldChange(attr.name, newValue); diff --git a/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx b/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx index d53779c..0c1ba9e 100644 --- a/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx +++ b/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx @@ -13,12 +13,18 @@ import { import { formatUnixTimestamp } from '../../../utils/time'; import TextField from '../../UiComponents/TextField/TextField'; import { FormGeneratorControls } from '../FormGeneratorControls'; +import { + isSelectType, + isCheckboxType, + attributeTypeToInputType +} from '../../../utils/attributeTypeMapper'; +import type { AttributeType } from '../../../utils/attributeTypeMapper'; // Types for the FormGeneratorList export interface FieldConfig { key: string; label: string; - type?: 'string' | 'number' | 'date' | 'boolean' | 'enum' | 'readonly'; + type?: AttributeType; editable?: boolean; required?: boolean; formatter?: (value: any, row: any) => React.ReactNode; @@ -456,7 +462,7 @@ export function FormGeneratorList>({ ); } - if (field.type === 'enum' && field.options) { + if (isSelectType(field.type || 'string') && field.options) { return ( >({ key={field.key} value={value || ''} onChange={(newValue) => onFieldChange?.(row, field.key, newValue)} - type={field.type === 'date' ? 'date' : field.type === 'number' ? 'number' : 'text'} + type={attributeTypeToInputType(field.type || 'string')} required={field.required} readonly={!field.editable} className={styles.fieldInput} diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx index f39cb15..3ac8498 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx @@ -13,12 +13,16 @@ import { import { formatUnixTimestamp } from '../../../utils/time'; import { FormGeneratorControls } from '../FormGeneratorControls'; import { CopyableTruncatedValue } from '../../UiComponents/CopyableTruncatedValue'; +import { + isDateTimeType +} from '../../../utils/attributeTypeMapper'; +import type { AttributeType } from '../../../utils/attributeTypeMapper'; // Types for the FormGeneratorTable export interface ColumnConfig { key: string; label: string; - type?: 'string' | 'number' | 'date' | 'boolean' | 'enum'; + type?: AttributeType; width?: number; minWidth?: number; maxWidth?: number; @@ -526,7 +530,8 @@ export function FormGeneratorTable>({ const isLikelyTimestamp = typeof value === 'number' && value > 0 && value < 4102444800000; // If it's a timestamp field or looks like a timestamp, format as date - if ((isTimestampField || isLikelyTimestamp) && typeof value === 'number') { + // Also check if column type is a date/time type + if ((isTimestampField || isLikelyTimestamp || (column.type && isDateTimeType(column.type))) && typeof value === 'number') { try { // Handle Unix timestamps in seconds (backend format) let timestamp: number; @@ -557,6 +562,8 @@ export function FormGeneratorTable>({ switch (column.type) { case 'date': + case 'timestamp': + case 'time': try { // Handle Unix timestamps in seconds (backend format) let timestamp: number; diff --git a/src/components/FormGenerator/index.ts b/src/components/FormGenerator/index.ts index b9ceecb..f7f6249 100644 --- a/src/components/FormGenerator/index.ts +++ b/src/components/FormGenerator/index.ts @@ -1,5 +1,12 @@ -export { default as FormGenerator } from './FormGenerator'; -export type { ColumnConfig, FormGeneratorProps } from './FormGenerator'; +// Re-export FormGenerator components +export * from './FormGeneratorTable'; +export * from './FormGeneratorList'; +export * from './FormGeneratorForm'; +export * from './FormGeneratorControls'; + +// Alias FormGeneratorTable as FormGenerator for backward compatibility +export { FormGeneratorTable as FormGenerator, FormGeneratorTableComponent as FormGeneratorComponent } from './FormGeneratorTable'; +export type { FormGeneratorTableProps as FormGeneratorProps, ColumnConfig } from './FormGeneratorTable'; // Re-export action button components and types export * from './ActionButtons'; \ No newline at end of file diff --git a/src/components/UiComponents/Popup/ViewForm.module.css b/src/components/UiComponents/Popup/ViewForm.module.css deleted file mode 100644 index 9927ac7..0000000 --- a/src/components/UiComponents/Popup/ViewForm.module.css +++ /dev/null @@ -1,56 +0,0 @@ -/* ViewForm container */ -.viewForm { - width: 100%; -} - -/* Field styling */ -.fieldGroup { - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid #f3f4f6; -} - -.fieldGroup:last-child { - border-bottom: none; - margin-bottom: 0; -} - -.fieldLabel { - display: block; - font-weight: 600; - color: #374151; - margin-bottom: 6px; - font-size: 14px; - text-transform: capitalize; -} - -.fieldValue { - color: #6b7280; - font-size: 14px; - line-height: 1.5; - word-break: break-word; - padding: 4px 0; -} - -/* Special styling for different value types */ -.fieldValue:empty::before { - content: 'N/A'; - color: #9ca3af; - font-style: italic; -} - -/* Responsive design */ -@media (max-width: 640px) { - .fieldGroup { - margin-bottom: 12px; - padding-bottom: 8px; - } - - .fieldLabel { - font-size: 13px; - } - - .fieldValue { - font-size: 13px; - } -} \ No newline at end of file diff --git a/src/components/UiComponents/Popup/ViewForm.tsx b/src/components/UiComponents/Popup/ViewForm.tsx deleted file mode 100644 index d66e853..0000000 --- a/src/components/UiComponents/Popup/ViewForm.tsx +++ /dev/null @@ -1,46 +0,0 @@ - -import styles from './ViewForm.module.css'; - -// Field configuration interface for ViewForm -export interface ViewFieldConfig { - key: string; - label: string; - formatter?: (value: any) => string; -} - -// ViewForm props - for display-only purposes -export interface ViewFormProps { - data: T; - fields: ViewFieldConfig[]; - className?: string; -} - -// ViewForm component - displays data in read-only format -export function ViewForm>({ - data, - fields, - className = '' -}: ViewFormProps) { - - // Render field in view-only mode - const renderField = (field: ViewFieldConfig) => { - const value = data[field.key]; - - return ( -
- -
- {field.formatter ? field.formatter(value) : (value || 'N/A')} -
-
- ); - }; - - return ( -
- {fields.map(field => renderField(field))} -
- ); -} - -export default ViewForm; \ No newline at end of file diff --git a/src/components/UiComponents/Popup/index.ts b/src/components/UiComponents/Popup/index.ts index 207d73b..76d8513 100644 --- a/src/components/UiComponents/Popup/index.ts +++ b/src/components/UiComponents/Popup/index.ts @@ -4,8 +4,4 @@ export type { PopupProps, PopupAction } from './Popup'; // FormGeneratorForm component (recommended for backend-driven forms) export { FormGeneratorForm } from '../../FormGenerator/FormGeneratorForm'; -export type { FormGeneratorFormProps, AttributeDefinition, AttributeOption } from '../../FormGenerator/FormGeneratorForm'; - -// ViewForm component -export { ViewForm } from './ViewForm'; -export type { ViewFormProps } from './ViewForm'; \ No newline at end of file +export type { FormGeneratorFormProps, AttributeDefinition, AttributeOption } from '../../FormGenerator/FormGeneratorForm'; \ No newline at end of file diff --git a/src/components/UiComponents/ViewForm/ViewForm.module.css b/src/components/UiComponents/ViewForm/ViewForm.module.css new file mode 100644 index 0000000..ef53889 --- /dev/null +++ b/src/components/UiComponents/ViewForm/ViewForm.module.css @@ -0,0 +1,43 @@ +.viewForm { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 1rem 0; +} + +.fieldGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.fieldLabel { + font-weight: 600; + font-size: 0.875rem; + color: var(--text-secondary, #666); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.fieldValue { + font-size: 1rem; + color: var(--text-primary, #333); + padding: 0.75rem; + background-color: var(--background-secondary, #f5f5f5); + border-radius: 4px; + min-height: 2.5rem; + display: flex; + align-items: center; + word-break: break-word; +} + +/* Dark theme support */ +[data-theme="dark"] .fieldLabel { + color: var(--text-secondary, #aaa); +} + +[data-theme="dark"] .fieldValue { + color: var(--text-primary, #e0e0e0); + background-color: var(--background-secondary, #2a2a2a); +} + diff --git a/src/components/UiComponents/ViewForm/ViewForm.tsx b/src/components/UiComponents/ViewForm/ViewForm.tsx new file mode 100644 index 0000000..ca06a35 --- /dev/null +++ b/src/components/UiComponents/ViewForm/ViewForm.tsx @@ -0,0 +1,114 @@ +import styles from './ViewForm.module.css'; +import { + isCheckboxType, + isSelectType, + isMultiselectType, + isDateTimeType +} from '../../../utils/attributeTypeMapper'; +import type { AttributeType } from '../../../utils/attributeTypeMapper'; + +// Field configuration interface for ViewForm +export interface ViewFieldConfig { + key: string; + label: string; + type?: AttributeType; + formatter?: (value: any) => string; + options?: Array<{ value: string | number; label: string }>; // For select/enum types +} + +// ViewForm props - for display-only purposes +export interface ViewFormProps { + data: T; + fields: ViewFieldConfig[]; + className?: string; +} + +// ViewForm component - displays data in read-only format +export function ViewForm>({ + data, + fields, + className = '' +}: ViewFormProps) { + + // Format value based on field type + const formatValue = (field: ViewFieldConfig, value: any): string => { + // Use custom formatter if provided + if (field.formatter) { + return field.formatter(value); + } + + // Handle null/undefined + if (value === null || value === undefined) { + return 'N/A'; + } + + // Type-based formatting + if (field.type) { + // Boolean/Checkbox types + if (isCheckboxType(field.type)) { + return value ? 'Yes' : 'No'; + } + + // Select/Enum types + if (isSelectType(field.type) && field.options) { + const option = field.options.find(opt => String(opt.value) === String(value)); + return option ? option.label : String(value); + } + + // Multiselect types + if (isMultiselectType(field.type) && field.options) { + const selectedValues = Array.isArray(value) ? value : (value ? [value] : []); + if (selectedValues.length === 0) { + return 'None'; + } + return selectedValues.map(v => { + const option = field.options!.find(opt => String(opt.value) === String(v)); + return option ? option.label : String(v); + }).join(', '); + } + + // Date/Time/Timestamp types + if (isDateTimeType(field.type)) { + try { + const date = value instanceof Date ? value : new Date(value); + if (!isNaN(date.getTime())) { + return date.toLocaleString(); + } + } catch { + // Fall through to default + } + } + } + + // Default: convert to string + if (Array.isArray(value)) { + return value.length > 0 ? value.join(', ') : 'None'; + } + + return String(value); + }; + + // Render field in view-only mode + const renderField = (field: ViewFieldConfig) => { + const value = data[field.key]; + const formattedValue = formatValue(field, value); + + return ( +
+ +
+ {formattedValue} +
+
+ ); + }; + + return ( +
+ {fields.map(field => renderField(field))} +
+ ); +} + +export default ViewForm; + diff --git a/src/components/UiComponents/ViewForm/index.ts b/src/components/UiComponents/ViewForm/index.ts new file mode 100644 index 0000000..dfee856 --- /dev/null +++ b/src/components/UiComponents/ViewForm/index.ts @@ -0,0 +1,3 @@ +export { ViewForm, default as DefaultViewForm } from './ViewForm'; +export type { ViewFormProps, ViewFieldConfig } from './ViewForm'; + diff --git a/src/core/PageManager/PageManager.tsx b/src/core/PageManager/PageManager.tsx index d9aa788..3fbc2ec 100644 --- a/src/core/PageManager/PageManager.tsx +++ b/src/core/PageManager/PageManager.tsx @@ -26,7 +26,7 @@ const PageManager: React.FC = ({ const currentPath = getCurrentPath(); - // Check if user has access to a page using RBAC + // Check if user has access to a page using backend RBAC permissions const checkPageAccess = async (pageData: GenericPageData): Promise => { try { return await canView('UI', pageData.path); diff --git a/src/core/PageManager/data/pages/connections.ts b/src/core/PageManager/data/pages/connections.ts index 28c546a..cc2811d 100644 --- a/src/core/PageManager/data/pages/connections.ts +++ b/src/core/PageManager/data/pages/connections.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { GenericPageData } from '../../pageInterface'; import { FaGoogle, FaMicrosoft, FaLink } from 'react-icons/fa'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { useConnections } from '../../../../hooks/useConnections'; // Helper function to convert attribute definitions to column config @@ -233,9 +232,6 @@ export const connectionsPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Page behavior persistent: false, preload: false, diff --git a/src/core/PageManager/data/pages/dashboard.ts b/src/core/PageManager/data/pages/dashboard.ts index 5382489..cefbab2 100644 --- a/src/core/PageManager/data/pages/dashboard.ts +++ b/src/core/PageManager/data/pages/dashboard.ts @@ -3,7 +3,6 @@ import { LuTicket } from 'react-icons/lu'; import { IoMdSend } from 'react-icons/io'; import { MdStop } from 'react-icons/md'; import { HiOutlineCollection } from 'react-icons/hi'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { createDashboardHook } from '../../../../hooks/usePlayground'; export const dashboardPageData: GenericPageData = { @@ -81,9 +80,6 @@ export const dashboardPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Page behavior persistent: true, preserveState: true, diff --git a/src/core/PageManager/data/pages/files.ts b/src/core/PageManager/data/pages/files.ts index 410ebe0..6236736 100644 --- a/src/core/PageManager/data/pages/files.ts +++ b/src/core/PageManager/data/pages/files.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { GenericPageData } from '../../pageInterface'; import { FaRegFileAlt, FaUpload } from 'react-icons/fa'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles'; // Helper function to convert attribute definitions to column config @@ -272,9 +271,6 @@ export const filesPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Page behavior persistent: false, preload: false, diff --git a/src/core/PageManager/data/pages/pek-tables.ts b/src/core/PageManager/data/pages/pek-tables.ts index 2c37fe9..af79347 100644 --- a/src/core/PageManager/data/pages/pek-tables.ts +++ b/src/core/PageManager/data/pages/pek-tables.ts @@ -1,7 +1,6 @@ import { GenericPageData } from '../../pageInterface'; import { FaTable, FaBuilding } from 'react-icons/fa'; import { IoMdSend } from 'react-icons/io'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { usePekTablesContext } from '../../../../contexts/PekTablesContext'; import PekTablesDropdown from './pek-tables/PekTablesDropdown'; import PekTablesPageWrapper from './pek-tables/PekTablesPageWrapper'; @@ -104,9 +103,6 @@ export const pekTablesPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Page behavior persistent: false, preload: false, diff --git a/src/core/PageManager/data/pages/pek.ts b/src/core/PageManager/data/pages/pek.ts index ac7fdb2..a52cb6e 100644 --- a/src/core/PageManager/data/pages/pek.ts +++ b/src/core/PageManager/data/pages/pek.ts @@ -1,7 +1,6 @@ import { GenericPageData } from '../../pageInterface'; import { FaBuilding } from 'react-icons/fa'; import { IoMdSend } from 'react-icons/io'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import PekLocationInput from './pek/PekLocationInput'; import PekMapView from './pek/PekMapView'; import { usePek } from '../../../../hooks/usePek'; @@ -93,9 +92,6 @@ export const pekPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Page behavior persistent: false, preload: false, diff --git a/src/core/PageManager/data/pages/prompts.ts b/src/core/PageManager/data/pages/prompts.ts index 31b6be6..7a05595 100644 --- a/src/core/PageManager/data/pages/prompts.ts +++ b/src/core/PageManager/data/pages/prompts.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { GenericPageData } from '../../pageInterface'; import { FaLightbulb, FaPlus } from 'react-icons/fa'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { usePrompts, usePromptOperations } from '../../../../hooks/usePrompts'; // Helper function to convert attribute definitions to column config @@ -267,9 +266,6 @@ export const promptsPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Page behavior persistent: false, preload: false, diff --git a/src/core/PageManager/data/pages/settings.ts b/src/core/PageManager/data/pages/settings.ts index c74d641..c4c7925 100644 --- a/src/core/PageManager/data/pages/settings.ts +++ b/src/core/PageManager/data/pages/settings.ts @@ -1,6 +1,5 @@ import { GenericPageData } from '../../pageInterface'; import { FaCog } from 'react-icons/fa'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { createSettingsHook } from '../../../../hooks/useSettings'; export const settingsPageData: GenericPageData = { @@ -14,9 +13,6 @@ export const settingsPageData: GenericPageData = { title: 'settings.title', subtitle: 'Manage your account settings and preferences', - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Page behavior persistent: false, preserveState: false, diff --git a/src/core/PageManager/data/pages/speech-transcripts.ts b/src/core/PageManager/data/pages/speech-transcripts.ts index 2cb0168..f3de874 100644 --- a/src/core/PageManager/data/pages/speech-transcripts.ts +++ b/src/core/PageManager/data/pages/speech-transcripts.ts @@ -1,7 +1,6 @@ import { GenericPageData } from '../../pageInterface'; import { FaDownload, FaTrash, FaSearch } from 'react-icons/fa'; import { IoIosDocument } from 'react-icons/io'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; export const speechTranscriptsPageData: GenericPageData = { id: '8-1', @@ -99,9 +98,6 @@ export const speechTranscriptsPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.speechSignup, - // Page behavior persistent: false, preload: false, diff --git a/src/core/PageManager/data/pages/speech.ts b/src/core/PageManager/data/pages/speech.ts index ebc421d..3f619bc 100644 --- a/src/core/PageManager/data/pages/speech.ts +++ b/src/core/PageManager/data/pages/speech.ts @@ -1,6 +1,5 @@ import { GenericPageData } from '../../pageInterface'; import { FaRegFileAlt, FaMicrophone, FaCog, FaHistory } from 'react-icons/fa'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; export const speechPageData: GenericPageData = { id: 'start-speech', @@ -50,8 +49,7 @@ export const speechPageData: GenericPageData = { onClick: () => { console.log('Opening transcript history...'); // Navigate to transcripts - }, - privilegeChecker: privilegeCheckers.speechSignup + } } ], @@ -111,12 +109,8 @@ export const speechPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Subpage support hasSubpages: true, - subpagePrivilegeChecker: privilegeCheckers.speechSignup, // Page behavior persistent: false, diff --git a/src/core/PageManager/data/pages/team-members.ts b/src/core/PageManager/data/pages/team-members.ts index e311a08..2d5375c 100644 --- a/src/core/PageManager/data/pages/team-members.ts +++ b/src/core/PageManager/data/pages/team-members.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { GenericPageData } from '../../pageInterface'; import { FaUsers, FaPlus } from 'react-icons/fa'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { useOrgUsers, useUserOperations } from '../../../../hooks/useUsers'; // Helper function to convert attribute definitions to column config @@ -268,9 +267,6 @@ export const teamMembersPageData: GenericPageData = { } ], - // Privilege system - only admin and sysadmin can access - privilegeChecker: privilegeCheckers.adminRole, - // Page behavior persistent: false, preload: false, diff --git a/src/core/PageManager/data/pages/workflows.ts b/src/core/PageManager/data/pages/workflows.ts index 5e02cce..3fbda95 100644 --- a/src/core/PageManager/data/pages/workflows.ts +++ b/src/core/PageManager/data/pages/workflows.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { GenericPageData } from '../../pageInterface'; import { FaProjectDiagram } from 'react-icons/fa'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { useUserWorkflows, useWorkflowOperations } from '../../../../hooks/useWorkflows'; // Helper function to convert attribute definitions to column config @@ -221,9 +220,6 @@ export const workflowsPageData: GenericPageData = { } ], - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - // Page behavior persistent: false, preload: false, diff --git a/src/core/PageManager/pageInterface.ts b/src/core/PageManager/pageInterface.ts index 3356dfa..22fe91a 100644 --- a/src/core/PageManager/pageInterface.ts +++ b/src/core/PageManager/pageInterface.ts @@ -49,7 +49,6 @@ export interface PageButton { icon?: IconType; onClick?: (hookData?: any) => void | Promise; disabled?: boolean | ((hookData?: any) => boolean | { disabled: boolean; message?: string }); - privilegeChecker?: PrivilegeChecker; // Form configuration for create buttons formConfig?: { fields: ButtonFormField[]; @@ -128,7 +127,6 @@ export interface PageContent { items?: (string | LanguageText)[]; // For lists language?: string; // For code blocks customComponent?: React.ComponentType; - privilegeChecker?: PrivilegeChecker; // Table-specific properties tableConfig?: TableContentConfig; // Input form-specific properties @@ -275,9 +273,6 @@ export interface GenericPageData { // Content sections content?: PageContent[]; - // Privilege system - privilegeChecker?: PrivilegeChecker; - // Page behavior persistent?: boolean; preserveState?: boolean; @@ -287,7 +282,6 @@ export interface GenericPageData { // Subpage support hasSubpages?: boolean; - subpagePrivilegeChecker?: PrivilegeChecker; // Lifecycle hooks onActivate?: () => void | Promise; diff --git a/src/utils/privilegeCheckers.ts b/src/utils/privilegeCheckers.ts index b9965b1..2c69c3d 100644 --- a/src/utils/privilegeCheckers.ts +++ b/src/utils/privilegeCheckers.ts @@ -1,11 +1,14 @@ import { PrivilegeChecker } from '../core/PageManager/pageInterface'; import { getUserDataCache } from './userCache'; +import type { PermissionContext } from '../hooks/usePermissions'; /** * Privilege Checkers * * Read-only access to user data for privilege checking. * Does not manage user data storage - that's handled by authentication hooks. + * + * Now supports both client-side checks (roles, localStorage) and backend RBAC integration. */ // Function to get current user privilege from sessionStorage cache @@ -96,6 +99,123 @@ export const createCustomPrivilegeChecker = ( return checkFunction; }; +/** + * Create a privilege checker that uses backend RBAC permissions + * This integrates privilegeCheckers with usePermissions for backend-controlled access + * + * @param canViewFunction - The canView function from usePermissions hook + * @param context - Permission context ('UI', 'DATA', or 'RESOURCE') + * @param item - The item/resource path to check permissions for + * @returns A PrivilegeChecker function that checks backend RBAC permissions + */ +export const createRBACPrivilegeChecker = ( + canViewFunction: (context: PermissionContext, item: string) => Promise, + context: PermissionContext, + item: string +): PrivilegeChecker => { + return async (): Promise => { + try { + return await canViewFunction(context, item); + } catch (error) { + console.error(`Error checking RBAC privilege for ${context}:${item}:`, error); + return false; + } + }; +}; + +/** + * Create a privilege checker that combines RBAC with client-side role checks + * First checks backend RBAC, then falls back to client-side role check if RBAC allows + * + * @param canViewFunction - The canView function from usePermissions hook + * @param context - Permission context ('UI', 'DATA', or 'RESOURCE') + * @param item - The item/resource path to check permissions for + * @param requiredRoles - Fallback client-side roles to check if RBAC passes + * @returns A PrivilegeChecker function that checks both RBAC and roles + */ +export const createCombinedPrivilegeChecker = ( + canViewFunction: (context: PermissionContext, item: string) => Promise, + context: PermissionContext, + item: string, + requiredRoles: string[] +): PrivilegeChecker => { + return async (): Promise => { + try { + // First check backend RBAC + const hasRBACAccess = await canViewFunction(context, item); + if (!hasRBACAccess) { + return false; + } + + // If RBAC allows, also check client-side roles as additional validation + const userPrivilege = getCurrentUserPrivilege(); + if (userPrivilege && requiredRoles.includes(userPrivilege)) { + return true; + } + + // If no role match, still allow if RBAC said yes (backend is source of truth) + return hasRBACAccess; + } catch (error) { + console.error(`Error checking combined privilege for ${context}:${item}:`, error); + return false; + } + }; +}; + +/** + * Helper to create RBAC-based privilege checkers for page data + * These checkers will use backend RBAC permissions via usePermissions + * + * Usage in page data: + * import { createRBACPageChecker } from '@/utils/privilegeCheckers'; + * + * // In PageManager, initialize with canView function: + * const rbacCheckers = createRBACPageCheckers(canView); + * + * // In page data: + * privilegeChecker: rbacCheckers.forPage('administration/workflows') + */ +export const createRBACPageCheckers = ( + canViewFunction: (context: PermissionContext, item: string) => Promise +) => { + return { + /** + * Create a privilege checker for a specific page path + * Checks backend RBAC permissions for UI context + */ + forPage: (pagePath: string): PrivilegeChecker => { + return createRBACPrivilegeChecker(canViewFunction, 'UI', pagePath); + }, + + /** + * Create a privilege checker that combines RBAC with role requirements + * First checks backend RBAC, then validates user role + */ + forPageWithRole: ( + pagePath: string, + requiredRoles: string[] + ): PrivilegeChecker => { + return createCombinedPrivilegeChecker(canViewFunction, 'UI', pagePath, requiredRoles); + }, + + /** + * Create a privilege checker for a data resource + * Checks backend RBAC permissions for DATA context + */ + forData: (resourcePath: string): PrivilegeChecker => { + return createRBACPrivilegeChecker(canViewFunction, 'DATA', resourcePath); + }, + + /** + * Create a privilege checker for a UI resource + * Checks backend RBAC permissions for UI context + */ + forUI: (resourcePath: string): PrivilegeChecker => { + return createRBACPrivilegeChecker(canViewFunction, 'UI', resourcePath); + } + }; +}; + // Predefined privilege checkers for common use cases export const privilegeCheckers = { // Speech signup checker (existing functionality)