feat: implement granular permission checks for tenant actions and add tenant metadata display to settings.
This commit is contained in:
parent
28f3d886ba
commit
43abf370c7
@ -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 (
|
||||
|
||||
@ -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<WorkflowDefinition[]>([]);
|
||||
@ -286,50 +288,59 @@ const WorkflowDefinitionsTable = forwardRef<
|
||||
<div className="flex justify-end">
|
||||
<ActionDropdown
|
||||
actions={[
|
||||
{
|
||||
icon: <Copy className="w-4 h-4" />,
|
||||
label: "Clone",
|
||||
onClick: () => handleClone(wf.id, wf.name),
|
||||
},
|
||||
{
|
||||
icon: <Eye className="w-4 h-4" />,
|
||||
label: "View",
|
||||
onClick: () => {
|
||||
setViewDefinitionId(wf.id);
|
||||
setIsViewModalOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <Edit className="w-4 h-4" />,
|
||||
label: "Edit",
|
||||
onClick: () => {
|
||||
setSelectedDefinition(wf);
|
||||
setIsModalOpen(true);
|
||||
},
|
||||
},
|
||||
wf.status === "draft" || wf.status === "deprecated"
|
||||
canCreate("workflow")
|
||||
? {
|
||||
icon: <Copy className="w-4 h-4" />,
|
||||
label: "Clone",
|
||||
onClick: () => handleClone(wf.id, wf.name),
|
||||
}
|
||||
: null,
|
||||
canRead("workflow")
|
||||
? {
|
||||
icon: <Eye className="w-4 h-4" />,
|
||||
label: "View",
|
||||
onClick: () => {
|
||||
setViewDefinitionId(wf.id);
|
||||
setIsViewModalOpen(true);
|
||||
},
|
||||
}
|
||||
: null,
|
||||
canCreate("workflow")
|
||||
? {
|
||||
icon: <Edit className="w-4 h-4" />,
|
||||
label: "Edit",
|
||||
onClick: () => {
|
||||
setSelectedDefinition(wf);
|
||||
setIsModalOpen(true);
|
||||
},
|
||||
}
|
||||
: null,
|
||||
(wf.status === "draft" || wf.status === "deprecated") &&
|
||||
canCreate("workflow")
|
||||
? {
|
||||
icon: <Play className="w-4 h-4" />,
|
||||
label: "Activate",
|
||||
onClick: () => handleActivate(wf.id),
|
||||
}
|
||||
: null,
|
||||
wf.status === "active"
|
||||
wf.status === "active" && canCreate("workflow")
|
||||
? {
|
||||
icon: <Power className="w-4 h-4" />,
|
||||
label: "Deprecate",
|
||||
onClick: () => handleDeprecate(wf.id),
|
||||
}
|
||||
: null,
|
||||
{
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
label: "Delete",
|
||||
variant: "danger",
|
||||
onClick: () => {
|
||||
setSelectedDefinition(wf);
|
||||
setIsDeleteModalOpen(true);
|
||||
},
|
||||
},
|
||||
hasPermission("workflow", "admin")
|
||||
? {
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
label: "Delete",
|
||||
variant: "danger",
|
||||
onClick: () => {
|
||||
setSelectedDefinition(wf);
|
||||
setIsDeleteModalOpen(true);
|
||||
},
|
||||
}
|
||||
: null,
|
||||
].filter((a): a is any => a !== null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -56,7 +56,7 @@ interface RolesTableProps {
|
||||
export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
||||
({ tenantId, showHeader = true, compact = false }, ref): ReactElement => {
|
||||
// const { primaryColor } = useAppTheme();
|
||||
const { canCreate, canUpdate, canDelete } = usePermissions();
|
||||
const { isTenantAdmin } = usePermissions();
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -239,11 +239,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
render: (role) => (
|
||||
<span className="">
|
||||
{role.name}
|
||||
</span>
|
||||
),
|
||||
render: (role) => <span className="">{role.name}</span>,
|
||||
},
|
||||
{
|
||||
key: "code",
|
||||
@ -287,19 +283,13 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
||||
{
|
||||
key: "user_count",
|
||||
label: "Users",
|
||||
render: (role) => (
|
||||
<span className="">
|
||||
{role.user_count || 0}
|
||||
</span>
|
||||
),
|
||||
render: (role) => <span className="">{role.user_count || 0}</span>,
|
||||
},
|
||||
{
|
||||
key: "created_at",
|
||||
label: "Created Date",
|
||||
render: (role) => (
|
||||
<span className="">
|
||||
{formatDate(role.created_at)}
|
||||
</span>
|
||||
<span className="">{formatDate(role.created_at)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -311,12 +301,12 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
||||
<ActionDropdown
|
||||
onView={() => 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<RolesTableRef, RolesTableProps>(
|
||||
<ActionDropdown
|
||||
onView={() => 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<RolesTableRef, RolesTableProps>(
|
||||
onChange={setSearch}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
{canCreate("roles") && (
|
||||
{isTenantAdmin && (
|
||||
<PrimaryButton
|
||||
size="default"
|
||||
className="flex items-center gap-2"
|
||||
@ -535,9 +525,8 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
{/* New Role Button */}
|
||||
{canCreate("roles") && (
|
||||
{isTenantAdmin && (
|
||||
<PrimaryButton
|
||||
size="default"
|
||||
className="flex items-center gap-2"
|
||||
|
||||
@ -67,6 +67,7 @@ interface UsersTableProps {
|
||||
|
||||
export const UsersTable = forwardRef<UsersTableRef, UsersTableProps>(
|
||||
({ tenantId, showHeader = true, compact = false }, ref): ReactElement => {
|
||||
const { canUpdate } = usePermissions();
|
||||
const { primaryColor } = useAppTheme();
|
||||
const { canCreate } = usePermissions();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
@ -456,11 +457,14 @@ export const UsersTable = forwardRef<UsersTableRef, UsersTableProps>(
|
||||
<div className="flex justify-end">
|
||||
<ActionDropdown
|
||||
onView={() => 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}`)
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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" }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -89,14 +89,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})$/;
|
||||
@ -1465,12 +1481,8 @@ const EditTenant = (): ReactElement => {
|
||||
src={faviconPreviewUrl || faviconFileUrl}
|
||||
tenantId={id}
|
||||
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" }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -240,7 +240,7 @@ const CreateDocument = (): ReactElement => {
|
||||
const loadFiles = async (): Promise<void> => {
|
||||
setIsLoadingFiles(true);
|
||||
try {
|
||||
const res = await documentService.listFiles();
|
||||
const res = await documentService.listForDropdown();
|
||||
setFiles(res.data || []);
|
||||
} catch (err: any) {
|
||||
showToast.error(
|
||||
|
||||
@ -22,6 +22,7 @@ import { showToast } from "@/utils/toast";
|
||||
import { Plus, Eye, Edit, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import CodeBadge from "@/components/shared/CodeBadge";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
|
||||
const categorySchema = z.object({
|
||||
name: z.string().min(1, "Category name is required"),
|
||||
@ -54,6 +55,7 @@ const DocumentCategories = (): ReactElement => {
|
||||
useState<DocumentCategory | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { canCreate } = usePermissions();
|
||||
|
||||
const {
|
||||
register,
|
||||
@ -214,17 +216,21 @@ const DocumentCategories = (): ReactElement => {
|
||||
onClick: () => handleView(category),
|
||||
icon: <Eye 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" />,
|
||||
variant: "danger",
|
||||
},
|
||||
...(canCreate("document")
|
||||
? [
|
||||
{
|
||||
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" />,
|
||||
variant: "danger" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
),
|
||||
@ -274,7 +280,7 @@ const DocumentCategories = (): ReactElement => {
|
||||
title: "Document Categories",
|
||||
description:
|
||||
"View and manage the document categories and their retention policies.",
|
||||
action: (
|
||||
action: canCreate("document") && (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
setEditingCategory(null);
|
||||
|
||||
@ -14,6 +14,7 @@ import type { DocumentCategory, DocumentSummary } from "@/types/document";
|
||||
import type { Module } from "@/types/module";
|
||||
import { useAppTheme } from "@/hooks/useAppTheme";
|
||||
import { Plus, Search } from "lucide-react";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
|
||||
const formatDate = (value?: string | null): string => {
|
||||
if (!value) return "-";
|
||||
@ -50,6 +51,7 @@ const Documents = (): ReactElement => {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { canCreate } = usePermissions();
|
||||
|
||||
const offset = (currentPage - 1) * limit;
|
||||
const totalPages = Math.max(1, Math.ceil(total / limit));
|
||||
@ -112,8 +114,8 @@ const Documents = (): ReactElement => {
|
||||
offset,
|
||||
]);
|
||||
|
||||
const columns: Column<DocumentSummary>[] = useMemo(
|
||||
() => [
|
||||
const columns: Column<DocumentSummary>[] = useMemo(() => {
|
||||
const cols: Column<DocumentSummary>[] = [
|
||||
{
|
||||
key: "document_number",
|
||||
label: "Document No",
|
||||
@ -136,16 +138,12 @@ const Documents = (): ReactElement => {
|
||||
{
|
||||
key: "document_type",
|
||||
label: "Type",
|
||||
render: (doc) => (
|
||||
<span className="">{doc.document_type || "-"}</span>
|
||||
),
|
||||
render: (doc) => <span className="">{doc.document_type || "-"}</span>,
|
||||
},
|
||||
{
|
||||
key: "category",
|
||||
label: "Category",
|
||||
render: (doc) => (
|
||||
<span className="">{doc.category || "-"}</span>
|
||||
),
|
||||
render: (doc) => <span className="">{doc.category || "-"}</span>,
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
@ -166,9 +164,7 @@ const Documents = (): ReactElement => {
|
||||
key: "module_name",
|
||||
label: "Module",
|
||||
render: (doc) => (
|
||||
<span className="">
|
||||
{doc.module_name || "Platform"}
|
||||
</span>
|
||||
<span className="">{doc.module_name || "Platform"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -185,7 +181,10 @@ const Documents = (): ReactElement => {
|
||||
<span className="">{formatDate(doc.updated_at)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
];
|
||||
|
||||
if (canCreate("document")) {
|
||||
cols.push({
|
||||
key: "actions",
|
||||
label: "Actions",
|
||||
render: (doc) => (
|
||||
@ -201,10 +200,11 @@ const Documents = (): ReactElement => {
|
||||
Edit
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[navigate],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return cols;
|
||||
}, [navigate, canCreate, primaryColor]);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
@ -222,10 +222,14 @@ const Documents = (): ReactElement => {
|
||||
>
|
||||
Manage Categories
|
||||
</button> */}
|
||||
<PrimaryButton onClick={() => navigate("/tenant/documents/create")}>
|
||||
<Plus className="w-3.5 h-3.5 mr-1" />
|
||||
New Document
|
||||
</PrimaryButton>
|
||||
{canCreate("document") && (
|
||||
<PrimaryButton
|
||||
onClick={() => navigate("/tenant/documents/create")}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 mr-1" />
|
||||
New Document
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
|
||||
@ -447,6 +447,38 @@ const Settings = ({ customTenantId, hideLayout = false }: SettingsProps = {}): R
|
||||
</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 */}
|
||||
<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 */}
|
||||
@ -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" }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -6,8 +6,10 @@ import {
|
||||
PrimaryButton,
|
||||
} from "@/components/shared";
|
||||
import { Plus } from "lucide-react";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
|
||||
const WorkflowDefinationPage = (): ReactElement => {
|
||||
const { canCreate } = usePermissions();
|
||||
const tableRef = useRef<WorkflowDefinitionsTableRef>(null);
|
||||
|
||||
return (
|
||||
@ -15,8 +17,9 @@ const WorkflowDefinationPage = (): ReactElement => {
|
||||
currentPage="Workflow Definitions"
|
||||
pageHeader={{
|
||||
title: "Workflow Definitions",
|
||||
description: "Create and manage document approval workflow definitions.",
|
||||
action: (
|
||||
description:
|
||||
"Create and manage document approval workflow definitions.",
|
||||
action: canCreate("workflow") && (
|
||||
<PrimaryButton
|
||||
size="default"
|
||||
className="flex items-center gap-2"
|
||||
|
||||
@ -100,6 +100,11 @@ export const documentService = {
|
||||
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<{
|
||||
success: boolean;
|
||||
data: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user