diff --git a/src/components/shared/AuthenticatedImage.tsx b/src/components/shared/AuthenticatedImage.tsx index de01698..4095d60 100644 --- a/src/components/shared/AuthenticatedImage.tsx +++ b/src/components/shared/AuthenticatedImage.tsx @@ -78,19 +78,30 @@ export const AuthenticatedImage = ({ const isAuthRequired = !!(extractedFileId || isBackendUrl); useEffect(() => { + const currentCachedUrl = cacheKey ? BLOB_CACHE.get(cacheKey) || null : null; + // If it's already a blob URL (local preview) or a data URL, use it directly if (src && (src.startsWith("blob:") || src.startsWith("data:"))) { setBlobUrl(src); return; } - if (!cacheKey) return; - - // If we already have the blobUrl from initial state (cached), we don't need to fetch - if (blobUrl) return; + if (!cacheKey) { + setBlobUrl(null); + return; + } // 2. If we have a fileId or a backend URL, fetch it via authenticated request if (isAuthRequired) { + // If we already have the blobUrl for this cacheKey, use it and don't fetch + if (currentCachedUrl) { + setBlobUrl(currentCachedUrl); + return; + } + + // Otherwise, we need to fetch. Reset blobUrl to null first to show loading/fallback + setBlobUrl(null); + let isMounted = true; const fetchImage = async () => { // 3. Check if there's already a pending request for this same image @@ -154,7 +165,7 @@ export const AuthenticatedImage = ({ // For other external URLs, use them directly setBlobUrl(src); } - }, [fileId, src, cacheKey, isAuthRequired, blobUrl]); + }, [fileId, src, cacheKey, isAuthRequired, tenantId]); if (isLoading) { return ( diff --git a/src/components/shared/WorkflowDefinitionsTable.tsx b/src/components/shared/WorkflowDefinitionsTable.tsx index 97430c9..751a78e 100644 --- a/src/components/shared/WorkflowDefinitionsTable.tsx +++ b/src/components/shared/WorkflowDefinitionsTable.tsx @@ -25,6 +25,7 @@ import { showToast } from "@/utils/toast"; import type { RootState } from "@/store/store"; import { formatDate } from "@/utils/format-date"; import CodeBadge from "./CodeBadge"; +import { usePermissions } from "@/hooks/usePermissions"; export interface WorkflowDefinitionsTableRef { openNewModal: () => void; @@ -49,6 +50,7 @@ const WorkflowDefinitionsTable = forwardRef< const reduxTenantId = useSelector( (state: RootState) => state.auth.tenantId, ); + const { canCreate, canRead, hasPermission } = usePermissions(); const effectiveTenantId = tenantId || reduxTenantId || undefined; const [definitions, setDefinitions] = useState([]); @@ -286,50 +288,59 @@ const WorkflowDefinitionsTable = forwardRef<
, - label: "Clone", - onClick: () => handleClone(wf.id, wf.name), - }, - { - icon: , - label: "View", - onClick: () => { - setViewDefinitionId(wf.id); - setIsViewModalOpen(true); - }, - }, - { - icon: , - label: "Edit", - onClick: () => { - setSelectedDefinition(wf); - setIsModalOpen(true); - }, - }, - wf.status === "draft" || wf.status === "deprecated" + canCreate("workflow") + ? { + icon: , + label: "Clone", + onClick: () => handleClone(wf.id, wf.name), + } + : null, + canRead("workflow") + ? { + icon: , + label: "View", + onClick: () => { + setViewDefinitionId(wf.id); + setIsViewModalOpen(true); + }, + } + : null, + canCreate("workflow") + ? { + icon: , + label: "Edit", + onClick: () => { + setSelectedDefinition(wf); + setIsModalOpen(true); + }, + } + : null, + (wf.status === "draft" || wf.status === "deprecated") && + canCreate("workflow") ? { icon: , label: "Activate", onClick: () => handleActivate(wf.id), } : null, - wf.status === "active" + wf.status === "active" && canCreate("workflow") ? { icon: , label: "Deprecate", onClick: () => handleDeprecate(wf.id), } : null, - { - icon: , - label: "Delete", - variant: "danger", - onClick: () => { - setSelectedDefinition(wf); - setIsDeleteModalOpen(true); - }, - }, + hasPermission("workflow", "admin") + ? { + icon: , + label: "Delete", + variant: "danger", + onClick: () => { + setSelectedDefinition(wf); + setIsDeleteModalOpen(true); + }, + } + : null, ].filter((a): a is any => a !== null)} />
diff --git a/src/components/superadmin/RolesTable.tsx b/src/components/superadmin/RolesTable.tsx index 728d8d4..76729ec 100644 --- a/src/components/superadmin/RolesTable.tsx +++ b/src/components/superadmin/RolesTable.tsx @@ -56,7 +56,7 @@ interface RolesTableProps { export const RolesTable = forwardRef( ({ tenantId, showHeader = true, compact = false }, ref): ReactElement => { // const { primaryColor } = useAppTheme(); - const { canCreate, canUpdate, canDelete } = usePermissions(); + const { isTenantAdmin } = usePermissions(); const [roles, setRoles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -239,11 +239,7 @@ export const RolesTable = forwardRef( { key: "name", label: "Name", - render: (role) => ( - - {role.name} - - ), + render: (role) => {role.name}, }, { key: "code", @@ -287,19 +283,13 @@ export const RolesTable = forwardRef( { key: "user_count", label: "Users", - render: (role) => ( - - {role.user_count || 0} - - ), + render: (role) => {role.user_count || 0}, }, { key: "created_at", label: "Created Date", render: (role) => ( - - {formatDate(role.created_at)} - + {formatDate(role.created_at)} ), }, { @@ -311,12 +301,12 @@ export const RolesTable = forwardRef( handleViewRole(role.id)} onEdit={ - canUpdate("roles") + isTenantAdmin ? () => handleEditRole(role.id, role.name) : undefined } onDelete={ - canDelete("roles") + isTenantAdmin ? () => handleDeleteRole(role.id, role.name) : undefined } @@ -341,12 +331,12 @@ export const RolesTable = forwardRef( handleViewRole(role.id)} onEdit={ - canUpdate("roles") + isTenantAdmin ? () => handleEditRole(role.id, role.name) : undefined } onDelete={ - canDelete("roles") + isTenantAdmin ? () => handleDeleteRole(role.id, role.name) : undefined } @@ -398,7 +388,7 @@ export const RolesTable = forwardRef( onChange={setSearch} placeholder="Search..." /> - {canCreate("roles") && ( + {isTenantAdmin && ( ( {/* Actions */}
- {/* New Role Button */} - {canCreate("roles") && ( + {isTenantAdmin && ( ( ({ tenantId, showHeader = true, compact = false }, ref): ReactElement => { + const { canUpdate } = usePermissions(); const { primaryColor } = useAppTheme(); const { canCreate } = usePermissions(); const [users, setUsers] = useState([]); @@ -456,11 +457,14 @@ export const UsersTable = forwardRef(
handleViewUser(user.id)} - onEdit={() => - handleEditUser( - user.id, - // , `${user.first_name} ${user.last_name}` - ) + onEdit={ + canUpdate("users") + ? () => + handleEditUser( + user.id, + // , `${user.first_name} ${user.last_name}` + ) + : undefined } // onDelete={() => // handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`) diff --git a/src/hooks/usePermissions.ts b/src/hooks/usePermissions.ts index 84cbfe4..b3ebd2d 100644 --- a/src/hooks/usePermissions.ts +++ b/src/hooks/usePermissions.ts @@ -80,6 +80,11 @@ export const usePermissions = () => { [hasPermission] ); + const isTenantAdmin = useMemo(() => { + const rolesArray = Array.isArray(roles) ? roles : []; + return rolesArray.includes('tenant_admin') || rolesArray.includes('super_admin'); + }, [roles]); + return { hasPermission, canCreate, @@ -87,5 +92,6 @@ export const usePermissions = () => { canUpdate, canDelete, isSuperAdmin, + isTenantAdmin, }; }; diff --git a/src/pages/superadmin/CreateTenantWizard.tsx b/src/pages/superadmin/CreateTenantWizard.tsx index 103bd25..0ef78fe 100644 --- a/src/pages/superadmin/CreateTenantWizard.tsx +++ b/src/pages/superadmin/CreateTenantWizard.tsx @@ -82,14 +82,30 @@ const contactDetailsSchema = z.object({ message: "Phone number must be exactly 10 digits", }, ), - address_line1: z.string().min(1, "Address is required"), - address_line2: z.string().optional().nullable(), - city: z.string().min(1, "City is required"), - state: z.string().min(1, "State is required"), + address_line1: z + .string() + .min(1, "Address is required") + .max(255, "Maximum 255 characters allowed"), + address_line2: z + .string() + .max(255, "Maximum 255 characters allowed") + .optional() + .nullable(), + city: z + .string() + .min(1, "City is required") + .max(255, "Maximum 255 characters allowed"), + state: z + .string() + .min(1, "State is required") + .max(255, "Maximum 255 characters allowed"), postal_code: z .string() .regex(/^[1-9]\d{5}$/, "Postal code must be a valid 6-digit PIN code"), - country: z.string().min(1, "Country is required"), + country: z + .string() + .min(1, "Country is required") + .max(255, "Maximum 255 characters allowed"), }); const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; @@ -1308,12 +1324,8 @@ const CreateTenantWizard = (): ReactElement => { fileId={faviconFileAttachmentUuid} src={faviconPreviewUrl || faviconFileUrl} alt="Favicon preview" - className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white" - style={{ - display: "block", - width: "64px", - height: "64px", - }} + className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white" + style={{ display: "block", maxHeight: "80px" }} /> ), - }, - ], - [navigate], - ); + }); + } + + return cols; + }, [navigate, canCreate, primaryColor]); return ( { > Manage Categories */} - navigate("/tenant/documents/create")}> - - New Document - + {canCreate("document") && ( + navigate("/tenant/documents/create")} + > + + New Document + + )}
), }} diff --git a/src/pages/tenant/Settings.tsx b/src/pages/tenant/Settings.tsx index 0e53e2c..1932fc4 100644 --- a/src/pages/tenant/Settings.tsx +++ b/src/pages/tenant/Settings.tsx @@ -447,6 +447,38 @@ const Settings = ({ customTenantId, hideLayout = false }: SettingsProps = {}): R
)} + {/* Tenant Details Section */} + {tenant && ( +
+
+

Tenant Details

+

+ General workspace details for this tenant. +

+
+
+
+ + +
+
+ + +
+
+
+ )} + {/* Branding Section */}
{/* Section Header */} @@ -578,12 +610,8 @@ const Settings = ({ customTenantId, hideLayout = false }: SettingsProps = {}): R fileId={faviconFileAttachmentUuid} src={faviconPreviewUrl || faviconFileUrl} alt="Favicon preview" - className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white" - style={{ - display: "block", - width: "64px", - height: "64px", - }} + className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white" + style={{ display: "block", maxHeight: "80px" }} />