fix: consolidated priviledgechecker and usepermissions hook
This commit is contained in:
parent
14273c248a
commit
d3c950d735
25 changed files with 343 additions and 200 deletions
|
|
@ -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({
|
|||
<div className={styles.filtersContainer}>
|
||||
{filterableFields.map(field => (
|
||||
<div key={field.key} className={styles.filterGroup}>
|
||||
{field.type === 'boolean' ? (
|
||||
{field.type && isCheckboxType(field.type) ? (
|
||||
<div className={styles.customSelectContainer}>
|
||||
<select
|
||||
value={filters[field.key] || ''}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,23 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
import api from '../../../api';
|
||||
import styles from './FormGeneratorForm.module.css';
|
||||
import {
|
||||
attributeTypeToInputType,
|
||||
isTextareaType,
|
||||
isSelectType,
|
||||
isMultiselectType,
|
||||
isCheckboxType,
|
||||
isFileType,
|
||||
isNumberType,
|
||||
isDateTimeType,
|
||||
getDefaultValueForType
|
||||
} from '../../../utils/attributeTypeMapper';
|
||||
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
||||
|
||||
// Attribute definition interface (matches backend structure)
|
||||
// Attribute definition interface (matches backend structure)
|
||||
export interface AttributeDefinition {
|
||||
name: string;
|
||||
type: 'text' | 'email' | 'date' | 'checkbox' | 'select' | 'multiselect' | 'number' | 'textarea' |
|
||||
'timestamp' | 'time' | 'url' | 'password' | 'file' | 'integer' | 'float' | 'string' |
|
||||
'boolean' | 'enum' | 'readonly';
|
||||
type: AttributeType;
|
||||
label: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
|
|
@ -177,12 +187,8 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
filteredAttrs.forEach(attr => {
|
||||
if (attr.default !== undefined) {
|
||||
initialData[attr.name] = attr.default;
|
||||
} else if (attr.type === 'checkbox' || attr.type === 'boolean') {
|
||||
initialData[attr.name] = false;
|
||||
} else if (attr.type === 'multiselect') {
|
||||
initialData[attr.name] = [];
|
||||
} else {
|
||||
initialData[attr.name] = '';
|
||||
initialData[attr.name] = getDefaultValueForType(attr.type);
|
||||
}
|
||||
});
|
||||
setFormData(initialData as T);
|
||||
|
|
@ -332,7 +338,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// Number/Float validation
|
||||
if (attr.type === 'number' || attr.type === 'float') {
|
||||
if (isNumberType(attr.type)) {
|
||||
if (isNaN(Number(value))) {
|
||||
newErrors[attr.name] = t('formgen.form.invalidNumber', `${attr.label} must be a valid number`);
|
||||
return;
|
||||
|
|
@ -358,7 +364,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// Select/Multiselect option validation
|
||||
if (attr.type === 'select' || attr.type === 'enum') {
|
||||
if (isSelectType(attr.type)) {
|
||||
const options = normalizeOptions(attr);
|
||||
if (options.length > 0 && !options.some(opt => String(opt.value) === String(value))) {
|
||||
newErrors[attr.name] = t('formgen.form.invalidOption', 'Invalid option selected');
|
||||
|
|
@ -367,7 +373,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// Timestamp/Date validation
|
||||
if (attr.type === 'timestamp' || attr.type === 'date' || attr.type === 'time') {
|
||||
if (isDateTimeType(attr.type)) {
|
||||
const dateValue = new Date(String(value));
|
||||
if (isNaN(dateValue.getTime())) {
|
||||
newErrors[attr.name] = t('formgen.form.invalidDate', 'Invalid date format');
|
||||
|
|
@ -451,13 +457,13 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
// Readonly/Display field
|
||||
if (isReadonly) {
|
||||
let displayValue = value;
|
||||
if (attr.type === 'checkbox' || attr.type === 'boolean') {
|
||||
if (isCheckboxType(attr.type)) {
|
||||
displayValue = value ? t('common.yes', 'Yes') : t('common.no', 'No');
|
||||
} else if (attr.type === 'select' || attr.type === 'enum') {
|
||||
} else if (isSelectType(attr.type)) {
|
||||
const options = normalizeOptions(attr);
|
||||
const selectedOption = options.find(opt => String(opt.value) === String(value));
|
||||
displayValue = selectedOption ? selectedOption.label : value;
|
||||
} else if (attr.type === 'multiselect') {
|
||||
} else if (isMultiselectType(attr.type)) {
|
||||
const options = normalizeOptions(attr);
|
||||
const selectedValues = Array.isArray(value) ? value : (value ? [value] : []);
|
||||
displayValue = selectedValues.map(v => {
|
||||
|
|
@ -479,7 +485,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// Select/Enum field
|
||||
if (attr.type === 'select' || attr.type === 'enum') {
|
||||
if (isSelectType(attr.type)) {
|
||||
const options = normalizeOptions(attr);
|
||||
const isLoading = typeof attr.options === 'string' && loadingOptions[attr.name];
|
||||
|
||||
|
|
@ -508,7 +514,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// Multiselect field
|
||||
if (attr.type === 'multiselect') {
|
||||
if (isMultiselectType(attr.type)) {
|
||||
const options = normalizeOptions(attr);
|
||||
const currentValues = Array.isArray(value) ? value : (value ? [value] : []);
|
||||
const isLoading = typeof attr.options === 'string' && loadingOptions[attr.name];
|
||||
|
|
@ -562,7 +568,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// Checkbox/Boolean field
|
||||
if (attr.type === 'checkbox' || attr.type === 'boolean') {
|
||||
if (isCheckboxType(attr.type)) {
|
||||
return (
|
||||
<div className={styles.fieldGroup} key={attr.name}>
|
||||
<label className={styles.checkboxLabel}>
|
||||
|
|
@ -583,7 +589,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// Textarea field
|
||||
if (attr.type === 'textarea') {
|
||||
if (isTextareaType(attr.type)) {
|
||||
const minRows = attr.minRows || 4;
|
||||
const maxRows = attr.maxRows || 8;
|
||||
const minHeight = minRows * 1.5 * 16;
|
||||
|
|
@ -635,7 +641,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// File field
|
||||
if (attr.type === 'file') {
|
||||
if (isFileType(attr.type)) {
|
||||
return (
|
||||
<div className={styles.floatingLabelInput} key={attr.name}>
|
||||
<input
|
||||
|
|
@ -658,14 +664,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
|
||||
// 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 (
|
||||
<div className={styles.floatingLabelInput} key={attr.name}>
|
||||
|
|
@ -674,7 +673,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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<T extends Record<string, any>>({
|
|||
);
|
||||
}
|
||||
|
||||
if (field.type === 'enum' && field.options) {
|
||||
if (isSelectType(field.type || 'string') && field.options) {
|
||||
return (
|
||||
<select
|
||||
key={field.key}
|
||||
|
|
@ -472,7 +478,7 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
|||
);
|
||||
}
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
if (isCheckboxType(field.type || 'string')) {
|
||||
return (
|
||||
<input
|
||||
key={field.key}
|
||||
|
|
@ -490,7 +496,7 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
|||
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}
|
||||
|
|
|
|||
|
|
@ -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<T extends Record<string, any>>({
|
|||
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<T extends Record<string, any>>({
|
|||
|
||||
switch (column.type) {
|
||||
case 'date':
|
||||
case 'timestamp':
|
||||
case 'time':
|
||||
try {
|
||||
// Handle Unix timestamps in seconds (backend format)
|
||||
let timestamp: number;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T = any> {
|
||||
data: T;
|
||||
fields: ViewFieldConfig[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ViewForm component - displays data in read-only format
|
||||
export function ViewForm<T extends Record<string, any>>({
|
||||
data,
|
||||
fields,
|
||||
className = ''
|
||||
}: ViewFormProps<T>) {
|
||||
|
||||
// Render field in view-only mode
|
||||
const renderField = (field: ViewFieldConfig) => {
|
||||
const value = data[field.key];
|
||||
|
||||
return (
|
||||
<div className={styles.fieldGroup} key={field.key}>
|
||||
<label className={styles.fieldLabel}>{field.label}</label>
|
||||
<div className={styles.fieldValue}>
|
||||
{field.formatter ? field.formatter(value) : (value || 'N/A')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.viewForm} ${className}`}>
|
||||
{fields.map(field => renderField(field))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewForm;
|
||||
|
|
@ -5,7 +5,3 @@ 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';
|
||||
43
src/components/UiComponents/ViewForm/ViewForm.module.css
Normal file
43
src/components/UiComponents/ViewForm/ViewForm.module.css
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
||||
114
src/components/UiComponents/ViewForm/ViewForm.tsx
Normal file
114
src/components/UiComponents/ViewForm/ViewForm.tsx
Normal file
|
|
@ -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<T = any> {
|
||||
data: T;
|
||||
fields: ViewFieldConfig[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ViewForm component - displays data in read-only format
|
||||
export function ViewForm<T extends Record<string, any>>({
|
||||
data,
|
||||
fields,
|
||||
className = ''
|
||||
}: ViewFormProps<T>) {
|
||||
|
||||
// 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 (
|
||||
<div className={styles.fieldGroup} key={field.key}>
|
||||
<label className={styles.fieldLabel}>{field.label}</label>
|
||||
<div className={styles.fieldValue}>
|
||||
{formattedValue}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.viewForm} ${className}`}>
|
||||
{fields.map(field => renderField(field))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewForm;
|
||||
|
||||
3
src/components/UiComponents/ViewForm/index.ts
Normal file
3
src/components/UiComponents/ViewForm/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { ViewForm, default as DefaultViewForm } from './ViewForm';
|
||||
export type { ViewFormProps, ViewFieldConfig } from './ViewForm';
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ const PageManager: React.FC<PageManagerProps> = ({
|
|||
|
||||
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<boolean> => {
|
||||
try {
|
||||
return await canView('UI', pageData.path);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ export interface PageButton {
|
|||
icon?: IconType;
|
||||
onClick?: (hookData?: any) => void | Promise<void>;
|
||||
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<any>;
|
||||
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<void>;
|
||||
|
|
|
|||
|
|
@ -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<boolean>,
|
||||
context: PermissionContext,
|
||||
item: string
|
||||
): PrivilegeChecker => {
|
||||
return async (): Promise<boolean> => {
|
||||
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<boolean>,
|
||||
context: PermissionContext,
|
||||
item: string,
|
||||
requiredRoles: string[]
|
||||
): PrivilegeChecker => {
|
||||
return async (): Promise<boolean> => {
|
||||
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<boolean>
|
||||
) => {
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue