feat: implement granular permission checks for tenant actions and add tenant metadata display to settings.

This commit is contained in:
Yashwin 2026-06-04 13:19:39 +05:30
parent 28f3d886ba
commit 43abf370c7
13 changed files with 217 additions and 126 deletions

View File

@ -78,19 +78,30 @@ export const AuthenticatedImage = ({
const isAuthRequired = !!(extractedFileId || isBackendUrl); const isAuthRequired = !!(extractedFileId || isBackendUrl);
useEffect(() => { 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 it's already a blob URL (local preview) or a data URL, use it directly
if (src && (src.startsWith("blob:") || src.startsWith("data:"))) { if (src && (src.startsWith("blob:") || src.startsWith("data:"))) {
setBlobUrl(src); setBlobUrl(src);
return; return;
} }
if (!cacheKey) return; if (!cacheKey) {
setBlobUrl(null);
// If we already have the blobUrl from initial state (cached), we don't need to fetch return;
if (blobUrl) return; }
// 2. If we have a fileId or a backend URL, fetch it via authenticated request // 2. If we have a fileId or a backend URL, fetch it via authenticated request
if (isAuthRequired) { 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; let isMounted = true;
const fetchImage = async () => { const fetchImage = async () => {
// 3. Check if there's already a pending request for this same image // 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 // For other external URLs, use them directly
setBlobUrl(src); setBlobUrl(src);
} }
}, [fileId, src, cacheKey, isAuthRequired, blobUrl]); }, [fileId, src, cacheKey, isAuthRequired, tenantId]);
if (isLoading) { if (isLoading) {
return ( return (

View File

@ -25,6 +25,7 @@ import { showToast } from "@/utils/toast";
import type { RootState } from "@/store/store"; import type { RootState } from "@/store/store";
import { formatDate } from "@/utils/format-date"; import { formatDate } from "@/utils/format-date";
import CodeBadge from "./CodeBadge"; import CodeBadge from "./CodeBadge";
import { usePermissions } from "@/hooks/usePermissions";
export interface WorkflowDefinitionsTableRef { export interface WorkflowDefinitionsTableRef {
openNewModal: () => void; openNewModal: () => void;
@ -49,6 +50,7 @@ const WorkflowDefinitionsTable = forwardRef<
const reduxTenantId = useSelector( const reduxTenantId = useSelector(
(state: RootState) => state.auth.tenantId, (state: RootState) => state.auth.tenantId,
); );
const { canCreate, canRead, hasPermission } = usePermissions();
const effectiveTenantId = tenantId || reduxTenantId || undefined; const effectiveTenantId = tenantId || reduxTenantId || undefined;
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]); const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
@ -286,50 +288,59 @@ const WorkflowDefinitionsTable = forwardRef<
<div className="flex justify-end"> <div className="flex justify-end">
<ActionDropdown <ActionDropdown
actions={[ actions={[
{ canCreate("workflow")
icon: <Copy className="w-4 h-4" />, ? {
label: "Clone", icon: <Copy className="w-4 h-4" />,
onClick: () => handleClone(wf.id, wf.name), label: "Clone",
}, onClick: () => handleClone(wf.id, wf.name),
{ }
icon: <Eye className="w-4 h-4" />, : null,
label: "View", canRead("workflow")
onClick: () => { ? {
setViewDefinitionId(wf.id); icon: <Eye className="w-4 h-4" />,
setIsViewModalOpen(true); label: "View",
}, onClick: () => {
}, setViewDefinitionId(wf.id);
{ setIsViewModalOpen(true);
icon: <Edit className="w-4 h-4" />, },
label: "Edit", }
onClick: () => { : null,
setSelectedDefinition(wf); canCreate("workflow")
setIsModalOpen(true); ? {
}, icon: <Edit className="w-4 h-4" />,
}, label: "Edit",
wf.status === "draft" || wf.status === "deprecated" onClick: () => {
setSelectedDefinition(wf);
setIsModalOpen(true);
},
}
: null,
(wf.status === "draft" || wf.status === "deprecated") &&
canCreate("workflow")
? { ? {
icon: <Play className="w-4 h-4" />, icon: <Play className="w-4 h-4" />,
label: "Activate", label: "Activate",
onClick: () => handleActivate(wf.id), onClick: () => handleActivate(wf.id),
} }
: null, : null,
wf.status === "active" wf.status === "active" && canCreate("workflow")
? { ? {
icon: <Power className="w-4 h-4" />, icon: <Power className="w-4 h-4" />,
label: "Deprecate", label: "Deprecate",
onClick: () => handleDeprecate(wf.id), onClick: () => handleDeprecate(wf.id),
} }
: null, : null,
{ hasPermission("workflow", "admin")
icon: <Trash2 className="w-4 h-4" />, ? {
label: "Delete", icon: <Trash2 className="w-4 h-4" />,
variant: "danger", label: "Delete",
onClick: () => { variant: "danger",
setSelectedDefinition(wf); onClick: () => {
setIsDeleteModalOpen(true); setSelectedDefinition(wf);
}, setIsDeleteModalOpen(true);
}, },
}
: null,
].filter((a): a is any => a !== null)} ].filter((a): a is any => a !== null)}
/> />
</div> </div>

View File

@ -56,7 +56,7 @@ interface RolesTableProps {
export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>( export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
({ tenantId, showHeader = true, compact = false }, ref): ReactElement => { ({ tenantId, showHeader = true, compact = false }, ref): ReactElement => {
// const { primaryColor } = useAppTheme(); // const { primaryColor } = useAppTheme();
const { canCreate, canUpdate, canDelete } = usePermissions(); const { isTenantAdmin } = usePermissions();
const [roles, setRoles] = useState<Role[]>([]); const [roles, setRoles] = useState<Role[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -239,11 +239,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
{ {
key: "name", key: "name",
label: "Name", label: "Name",
render: (role) => ( render: (role) => <span className="">{role.name}</span>,
<span className="">
{role.name}
</span>
),
}, },
{ {
key: "code", key: "code",
@ -287,19 +283,13 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
{ {
key: "user_count", key: "user_count",
label: "Users", label: "Users",
render: (role) => ( render: (role) => <span className="">{role.user_count || 0}</span>,
<span className="">
{role.user_count || 0}
</span>
),
}, },
{ {
key: "created_at", key: "created_at",
label: "Created Date", label: "Created Date",
render: (role) => ( render: (role) => (
<span className=""> <span className="">{formatDate(role.created_at)}</span>
{formatDate(role.created_at)}
</span>
), ),
}, },
{ {
@ -311,12 +301,12 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
<ActionDropdown <ActionDropdown
onView={() => handleViewRole(role.id)} onView={() => handleViewRole(role.id)}
onEdit={ onEdit={
canUpdate("roles") isTenantAdmin
? () => handleEditRole(role.id, role.name) ? () => handleEditRole(role.id, role.name)
: undefined : undefined
} }
onDelete={ onDelete={
canDelete("roles") isTenantAdmin
? () => handleDeleteRole(role.id, role.name) ? () => handleDeleteRole(role.id, role.name)
: undefined : undefined
} }
@ -341,12 +331,12 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
<ActionDropdown <ActionDropdown
onView={() => handleViewRole(role.id)} onView={() => handleViewRole(role.id)}
onEdit={ onEdit={
canUpdate("roles") isTenantAdmin
? () => handleEditRole(role.id, role.name) ? () => handleEditRole(role.id, role.name)
: undefined : undefined
} }
onDelete={ onDelete={
canDelete("roles") isTenantAdmin
? () => handleDeleteRole(role.id, role.name) ? () => handleDeleteRole(role.id, role.name)
: undefined : undefined
} }
@ -398,7 +388,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
onChange={setSearch} onChange={setSearch}
placeholder="Search..." placeholder="Search..."
/> />
{canCreate("roles") && ( {isTenantAdmin && (
<PrimaryButton <PrimaryButton
size="default" size="default"
className="flex items-center gap-2" className="flex items-center gap-2"
@ -535,9 +525,8 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
{/* Actions */} {/* Actions */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* New Role Button */} {/* New Role Button */}
{canCreate("roles") && ( {isTenantAdmin && (
<PrimaryButton <PrimaryButton
size="default" size="default"
className="flex items-center gap-2" className="flex items-center gap-2"

View File

@ -67,6 +67,7 @@ interface UsersTableProps {
export const UsersTable = forwardRef<UsersTableRef, UsersTableProps>( export const UsersTable = forwardRef<UsersTableRef, UsersTableProps>(
({ tenantId, showHeader = true, compact = false }, ref): ReactElement => { ({ tenantId, showHeader = true, compact = false }, ref): ReactElement => {
const { canUpdate } = usePermissions();
const { primaryColor } = useAppTheme(); const { primaryColor } = useAppTheme();
const { canCreate } = usePermissions(); const { canCreate } = usePermissions();
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
@ -456,11 +457,14 @@ export const UsersTable = forwardRef<UsersTableRef, UsersTableProps>(
<div className="flex justify-end"> <div className="flex justify-end">
<ActionDropdown <ActionDropdown
onView={() => handleViewUser(user.id)} onView={() => handleViewUser(user.id)}
onEdit={() => onEdit={
handleEditUser( canUpdate("users")
user.id, ? () =>
// , `${user.first_name} ${user.last_name}` handleEditUser(
) user.id,
// , `${user.first_name} ${user.last_name}`
)
: undefined
} }
// onDelete={() => // onDelete={() =>
// handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`) // handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)

View File

@ -80,6 +80,11 @@ export const usePermissions = () => {
[hasPermission] [hasPermission]
); );
const isTenantAdmin = useMemo(() => {
const rolesArray = Array.isArray(roles) ? roles : [];
return rolesArray.includes('tenant_admin') || rolesArray.includes('super_admin');
}, [roles]);
return { return {
hasPermission, hasPermission,
canCreate, canCreate,
@ -87,5 +92,6 @@ export const usePermissions = () => {
canUpdate, canUpdate,
canDelete, canDelete,
isSuperAdmin, isSuperAdmin,
isTenantAdmin,
}; };
}; };

View File

@ -82,14 +82,30 @@ const contactDetailsSchema = z.object({
message: "Phone number must be exactly 10 digits", message: "Phone number must be exactly 10 digits",
}, },
), ),
address_line1: z.string().min(1, "Address is required"), address_line1: z
address_line2: z.string().optional().nullable(), .string()
city: z.string().min(1, "City is required"), .min(1, "Address is required")
state: z.string().min(1, "State 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 postal_code: z
.string() .string()
.regex(/^[1-9]\d{5}$/, "Postal code must be a valid 6-digit PIN code"), .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})$/; const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
@ -1308,12 +1324,8 @@ const CreateTenantWizard = (): ReactElement => {
fileId={faviconFileAttachmentUuid} fileId={faviconFileAttachmentUuid}
src={faviconPreviewUrl || faviconFileUrl} src={faviconPreviewUrl || faviconFileUrl}
alt="Favicon preview" alt="Favicon preview"
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white" className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
style={{ style={{ display: "block", maxHeight: "80px" }}
display: "block",
width: "64px",
height: "64px",
}}
/> />
<button <button
type="button" type="button"

View File

@ -89,14 +89,30 @@ const contactDetailsSchema = z.object({
message: "Phone number must be exactly 10 digits", message: "Phone number must be exactly 10 digits",
}, },
), ),
address_line1: z.string().min(1, "Address is required"), address_line1: z
address_line2: z.string().optional().nullable(), .string()
city: z.string().min(1, "City is required"), .min(1, "Address is required")
state: z.string().min(1, "State 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 postal_code: z
.string() .string()
.regex(/^[1-9]\d{5}$/, "Postal code must be a valid 6-digit PIN code"), .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})$/; const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
@ -1465,12 +1481,8 @@ const EditTenant = (): ReactElement => {
src={faviconPreviewUrl || faviconFileUrl} src={faviconPreviewUrl || faviconFileUrl}
tenantId={id} tenantId={id}
alt="Favicon preview" alt="Favicon preview"
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white" className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
style={{ style={{ display: "block", maxHeight: "80px" }}
display: "block",
width: "64px",
height: "64px",
}}
/> />
<button <button
type="button" type="button"

View File

@ -240,7 +240,7 @@ const CreateDocument = (): ReactElement => {
const loadFiles = async (): Promise<void> => { const loadFiles = async (): Promise<void> => {
setIsLoadingFiles(true); setIsLoadingFiles(true);
try { try {
const res = await documentService.listFiles(); const res = await documentService.listForDropdown();
setFiles(res.data || []); setFiles(res.data || []);
} catch (err: any) { } catch (err: any) {
showToast.error( showToast.error(

View File

@ -22,6 +22,7 @@ import { showToast } from "@/utils/toast";
import { Plus, Eye, Edit, Trash2 } from "lucide-react"; import { Plus, Eye, Edit, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import CodeBadge from "@/components/shared/CodeBadge"; import CodeBadge from "@/components/shared/CodeBadge";
import { usePermissions } from "@/hooks/usePermissions";
const categorySchema = z.object({ const categorySchema = z.object({
name: z.string().min(1, "Category name is required"), name: z.string().min(1, "Category name is required"),
@ -54,6 +55,7 @@ const DocumentCategories = (): ReactElement => {
useState<DocumentCategory | null>(null); useState<DocumentCategory | null>(null);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { canCreate } = usePermissions();
const { const {
register, register,
@ -214,17 +216,21 @@ const DocumentCategories = (): ReactElement => {
onClick: () => handleView(category), onClick: () => handleView(category),
icon: <Eye className="w-4 h-4" />, icon: <Eye className="w-4 h-4" />,
}, },
{ ...(canCreate("document")
label: "Edit Category", ? [
onClick: () => handleEdit(category), {
icon: <Edit className="w-4 h-4" />, label: "Edit Category",
}, onClick: () => handleEdit(category),
{ icon: <Edit className="w-4 h-4" />,
label: "Delete", },
onClick: () => handleDeleteClick(category), {
icon: <Trash2 className="w-4 h-4" />, label: "Delete",
variant: "danger", onClick: () => handleDeleteClick(category),
}, icon: <Trash2 className="w-4 h-4" />,
variant: "danger" as const,
},
]
: []),
]} ]}
/> />
), ),
@ -274,7 +280,7 @@ const DocumentCategories = (): ReactElement => {
title: "Document Categories", title: "Document Categories",
description: description:
"View and manage the document categories and their retention policies.", "View and manage the document categories and their retention policies.",
action: ( action: canCreate("document") && (
<PrimaryButton <PrimaryButton
onClick={() => { onClick={() => {
setEditingCategory(null); setEditingCategory(null);

View File

@ -14,6 +14,7 @@ import type { DocumentCategory, DocumentSummary } from "@/types/document";
import type { Module } from "@/types/module"; import type { Module } from "@/types/module";
import { useAppTheme } from "@/hooks/useAppTheme"; import { useAppTheme } from "@/hooks/useAppTheme";
import { Plus, Search } from "lucide-react"; import { Plus, Search } from "lucide-react";
import { usePermissions } from "@/hooks/usePermissions";
const formatDate = (value?: string | null): string => { const formatDate = (value?: string | null): string => {
if (!value) return "-"; if (!value) return "-";
@ -50,6 +51,7 @@ const Documents = (): ReactElement => {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { canCreate } = usePermissions();
const offset = (currentPage - 1) * limit; const offset = (currentPage - 1) * limit;
const totalPages = Math.max(1, Math.ceil(total / limit)); const totalPages = Math.max(1, Math.ceil(total / limit));
@ -112,8 +114,8 @@ const Documents = (): ReactElement => {
offset, offset,
]); ]);
const columns: Column<DocumentSummary>[] = useMemo( const columns: Column<DocumentSummary>[] = useMemo(() => {
() => [ const cols: Column<DocumentSummary>[] = [
{ {
key: "document_number", key: "document_number",
label: "Document No", label: "Document No",
@ -136,16 +138,12 @@ const Documents = (): ReactElement => {
{ {
key: "document_type", key: "document_type",
label: "Type", label: "Type",
render: (doc) => ( render: (doc) => <span className="">{doc.document_type || "-"}</span>,
<span className="">{doc.document_type || "-"}</span>
),
}, },
{ {
key: "category", key: "category",
label: "Category", label: "Category",
render: (doc) => ( render: (doc) => <span className="">{doc.category || "-"}</span>,
<span className="">{doc.category || "-"}</span>
),
}, },
{ {
key: "status", key: "status",
@ -166,9 +164,7 @@ const Documents = (): ReactElement => {
key: "module_name", key: "module_name",
label: "Module", label: "Module",
render: (doc) => ( render: (doc) => (
<span className=""> <span className="">{doc.module_name || "Platform"}</span>
{doc.module_name || "Platform"}
</span>
), ),
}, },
{ {
@ -185,7 +181,10 @@ const Documents = (): ReactElement => {
<span className="">{formatDate(doc.updated_at)}</span> <span className="">{formatDate(doc.updated_at)}</span>
), ),
}, },
{ ];
if (canCreate("document")) {
cols.push({
key: "actions", key: "actions",
label: "Actions", label: "Actions",
render: (doc) => ( render: (doc) => (
@ -201,10 +200,11 @@ const Documents = (): ReactElement => {
Edit Edit
</button> </button>
), ),
}, });
], }
[navigate],
); return cols;
}, [navigate, canCreate, primaryColor]);
return ( return (
<Layout <Layout
@ -222,10 +222,14 @@ const Documents = (): ReactElement => {
> >
Manage Categories Manage Categories
</button> */} </button> */}
<PrimaryButton onClick={() => navigate("/tenant/documents/create")}> {canCreate("document") && (
<Plus className="w-3.5 h-3.5 mr-1" /> <PrimaryButton
New Document onClick={() => navigate("/tenant/documents/create")}
</PrimaryButton> >
<Plus className="w-3.5 h-3.5 mr-1" />
New Document
</PrimaryButton>
)}
</div> </div>
), ),
}} }}

View File

@ -447,6 +447,38 @@ const Settings = ({ customTenantId, hideLayout = false }: SettingsProps = {}): R
</div> </div>
)} )}
{/* Tenant Details Section */}
{tenant && (
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h3 className="text-base font-semibold text-[#0f1724]">Tenant Details</h3>
<p className="text-sm font-normal text-[#9ca3af]">
General workspace details for this tenant.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-[#0f1724]">Tenant Name</label>
<input
type="text"
value={tenant.name}
readOnly
className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#6b7280] w-full focus:outline-none cursor-not-allowed"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-[#0f1724]">Slug</label>
<input
type="text"
value={tenant.slug}
readOnly
className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#6b7280] w-full focus:outline-none cursor-not-allowed"
/>
</div>
</div>
</div>
)}
{/* Branding Section */} {/* Branding Section */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4"> <div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
{/* Section Header */} {/* Section Header */}
@ -578,12 +610,8 @@ const Settings = ({ customTenantId, hideLayout = false }: SettingsProps = {}): R
fileId={faviconFileAttachmentUuid} fileId={faviconFileAttachmentUuid}
src={faviconPreviewUrl || faviconFileUrl} src={faviconPreviewUrl || faviconFileUrl}
alt="Favicon preview" alt="Favicon preview"
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white" className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
style={{ style={{ display: "block", maxHeight: "80px" }}
display: "block",
width: "64px",
height: "64px",
}}
/> />
<button <button
type="button" type="button"

View File

@ -6,8 +6,10 @@ import {
PrimaryButton, PrimaryButton,
} from "@/components/shared"; } from "@/components/shared";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { usePermissions } from "@/hooks/usePermissions";
const WorkflowDefinationPage = (): ReactElement => { const WorkflowDefinationPage = (): ReactElement => {
const { canCreate } = usePermissions();
const tableRef = useRef<WorkflowDefinitionsTableRef>(null); const tableRef = useRef<WorkflowDefinitionsTableRef>(null);
return ( return (
@ -15,8 +17,9 @@ const WorkflowDefinationPage = (): ReactElement => {
currentPage="Workflow Definitions" currentPage="Workflow Definitions"
pageHeader={{ pageHeader={{
title: "Workflow Definitions", title: "Workflow Definitions",
description: "Create and manage document approval workflow definitions.", description:
action: ( "Create and manage document approval workflow definitions.",
action: canCreate("workflow") && (
<PrimaryButton <PrimaryButton
size="default" size="default"
className="flex items-center gap-2" className="flex items-center gap-2"

View File

@ -100,6 +100,11 @@ export const documentService = {
return response.data; return response.data;
}, },
listForDropdown: async (): Promise<{ success: boolean; data: FileAttachmentItem[] }> => {
const response = await apiClient.get<{ success: boolean; data: FileAttachmentItem[] }>("/files/dropdown-options");
return response.data;
},
getFileContent: async (fileId: string): Promise<{ getFileContent: async (fileId: string): Promise<{
success: boolean; success: boolean;
data: { data: {