feat: implement modal auto-opening via location state for table pages and enhance StorageDashboard feedback with toasts and input validation.

This commit is contained in:
SibarchanNayak 2026-07-16 15:51:15 +05:30
parent b8a4f4825a
commit e27ab25376
9 changed files with 168 additions and 73 deletions

View File

@ -6,6 +6,7 @@ import {
Settings, Settings,
Building2, Building2,
BadgeCheck, BadgeCheck,
AppWindow,
} from "lucide-react"; } from "lucide-react";
import { useAppSelector } from "@/hooks/redux-hooks"; import { useAppSelector } from "@/hooks/redux-hooks";
import type { QuickAction } from "@/types/dashboard"; import type { QuickAction } from "@/types/dashboard";
@ -35,7 +36,7 @@ export const QuickActions = () => {
btncolor: "#4C89FA", btncolor: "#4C89FA",
onClick: () => navigate("/tenants/create-wizard"), onClick: () => navigate("/tenants/create-wizard"),
}, },
{ icon: UserPlus, label: "Module", btncolor: "#16C784", onClick: () => navigate("/modules") }, { icon: AppWindow, label: "Module", btncolor: "#16C784", onClick: () => navigate("/modules") },
{ {
icon: Shield, icon: Shield,
label: "Notification", label: "Notification",
@ -55,25 +56,25 @@ export const QuickActions = () => {
icon: UserPlus, icon: UserPlus,
label: "New User", label: "New User",
btncolor: "#4C89FA", btncolor: "#4C89FA",
onClick: () => navigate("/tenant/users"), onClick: () => navigate("/tenant/users", { state: { openCreateModal: true } }),
}, },
hasPermission("roles", "create") && { hasPermission("roles", "create") && {
icon: Shield, icon: Shield,
label: "New Role", label: "New Role",
btncolor: "#16C784", btncolor: "#16C784",
onClick: () => navigate("/tenant/roles"), onClick: () => navigate("/tenant/roles", { state: { openCreateModal: true } }),
}, },
hasPermission("departments", "create") && { hasPermission("departments", "create") && {
icon: Building2, icon: Building2,
label: "New Dept", label: "New Dept",
btncolor: "#FCA004", btncolor: "#FCA004",
onClick: () => navigate("/tenant/departments"), onClick: () => navigate("/tenant/departments", { state: { openCreateModal: true } }),
}, },
hasPermission("designations", "create") && { hasPermission("designations", "create") && {
icon: BadgeCheck, icon: BadgeCheck,
label: "New Desig", label: "New Desig",
btncolor: "#6B7280", btncolor: "#6B7280",
onClick: () => navigate("/tenant/designations"), onClick: () => navigate("/tenant/designations", { state: { openCreateModal: true } }),
}, },
].filter(Boolean) as QuickAction[]; ].filter(Boolean) as QuickAction[];

View File

@ -11,18 +11,18 @@ import {
PrimaryButton, PrimaryButton,
SecondaryButton, SecondaryButton,
MultiselectPaginatedSelect, MultiselectPaginatedSelect,
AuthenticatedImage, // AuthenticatedImage,
} from "@/components/shared"; } from "@/components/shared";
import { tenantService } from "@/services/tenant-service"; import { tenantService } from "@/services/tenant-service";
import { moduleService } from "@/services/module-service"; import { moduleService } from "@/services/module-service";
import { fileService } from "@/services/file-service"; // import { fileService } from "@/services/file-service";
import { platformServiceApi, type PlatformService } from "@/services/platform-service"; import { platformServiceApi, type PlatformService } from "@/services/platform-service";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
import { import {
ChevronRight, ChevronRight,
ChevronLeft, ChevronLeft,
Image as ImageIcon, // Image as ImageIcon,
X, // X,
FileText, FileText,
GraduationCap, GraduationCap,
AlertTriangle, AlertTriangle,
@ -40,7 +40,7 @@ import {
Settings, Settings,
Loader2, Loader2,
} from "lucide-react"; } from "lucide-react";
import { generateUUID } from "@/lib/utils"; // import { generateUUID } from "@/lib/utils";
// Icon mapping helper // Icon mapping helper
const serviceIconMap: Record<string, any> = { const serviceIconMap: Record<string, any> = {
@ -311,7 +311,8 @@ const CreateTenantWizard = (): ReactElement => {
}, },
}); });
// File upload state for branding // File upload state for branding (commented out as company logo and favicon uploads are disabled during tenant creation)
/*
const [logoFile, setLogoFile] = useState<File | null>(null); const [logoFile, setLogoFile] = useState<File | null>(null);
const [faviconFile, setFaviconFile] = useState<File | null>(null); const [faviconFile, setFaviconFile] = useState<File | null>(null);
const [logoFileAttachmentUuid, setLogoFileAttachmentUuid] = useState< const [logoFileAttachmentUuid, setLogoFileAttachmentUuid] = useState<
@ -330,6 +331,7 @@ const CreateTenantWizard = (): ReactElement => {
const [isUploadingFavicon, setIsUploadingFavicon] = useState<boolean>(false); const [isUploadingFavicon, setIsUploadingFavicon] = useState<boolean>(false);
const [logoError, setLogoError] = useState<string | null>(null); const [logoError, setLogoError] = useState<string | null>(null);
const [faviconError, setFaviconError] = useState<string | null>(null); const [faviconError, setFaviconError] = useState<string | null>(null);
*/
// Auto-generate slug and domain from name // Auto-generate slug and domain from name
const nameValue = tenantDetailsForm.watch("name"); const nameValue = tenantDetailsForm.watch("name");
@ -459,6 +461,7 @@ const CreateTenantWizard = (): ReactElement => {
} }
}; };
/*
const handleDeleteLogo = (): void => { const handleDeleteLogo = (): void => {
if (logoPreviewUrl) { if (logoPreviewUrl) {
URL.revokeObjectURL(logoPreviewUrl); URL.revokeObjectURL(logoPreviewUrl);
@ -494,11 +497,13 @@ const CreateTenantWizard = (): ReactElement => {
fileInput.value = ""; fileInput.value = "";
} }
}; };
*/
const handleSubmit = async (): Promise<void> => { const handleSubmit = async (): Promise<void> => {
const isValid = await settingsForm.trigger(); const isValid = await settingsForm.trigger();
if (!isValid) return; if (!isValid) return;
/*
// Validate logo and favicon are uploaded // Validate logo and favicon are uploaded
setLogoError(null); setLogoError(null);
setFaviconError(null); setFaviconError(null);
@ -518,6 +523,7 @@ const CreateTenantWizard = (): ReactElement => {
setCurrentStep(3); // Go to settings step where logo/favicon are setCurrentStep(3); // Go to settings step where logo/favicon are
return; return;
} }
*/
try { try {
setIsSubmitting(true); setIsSubmitting(true);
@ -560,6 +566,8 @@ const CreateTenantWizard = (): ReactElement => {
}; };
const response = await tenantService.create(tenantData); const response = await tenantService.create(tenantData);
/*
const createdTenant = response.data; const createdTenant = response.data;
const createdTenantId = createdTenant.id; const createdTenantId = createdTenant.id;
const slug = createdTenant.slug || tenantDetails.slug; const slug = createdTenant.slug || tenantDetails.slug;
@ -630,6 +638,7 @@ const CreateTenantWizard = (): ReactElement => {
}; };
await tenantService.update(createdTenantId, updateData); await tenantService.update(createdTenantId, updateData);
} }
*/
const message = response.message || "Tenant created successfully"; const message = response.message || "Tenant created successfully";
showToast.success(message); showToast.success(message);
@ -672,9 +681,9 @@ const CreateTenantWizard = (): ReactElement => {
const fieldName = path.replace("settings.branding.", ""); const fieldName = path.replace("settings.branding.", "");
// Map file_path fields to form fields // Map file_path fields to form fields
if (fieldName === "logo_file_path") { if (fieldName === "logo_file_path") {
setLogoError(detail.message); // setLogoError(detail.message);
} else if (fieldName === "favicon_file_path") { } else if (fieldName === "favicon_file_path") {
setFaviconError(detail.message); // setFaviconError(detail.message);
} else { } else {
settingsForm.setError(fieldName as keyof SettingsForm, { settingsForm.setError(fieldName as keyof SettingsForm, {
type: "server", type: "server",
@ -1288,9 +1297,9 @@ const CreateTenantWizard = (): ReactElement => {
</p> </p>
</div> </div>
{/* Logo and Favicon Upload */} {/* Logo and Favicon Upload commented out since file attachment bucket can only be set after tenant creation */}
{/*
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Company Logo */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-sm font-medium text-[#0f1724]"> <label className="text-sm font-medium text-[#0f1724]">
Company Logo <span className="text-[#e02424]">*</span> Company Logo <span className="text-[#e02424]">*</span>
@ -1317,19 +1326,16 @@ const CreateTenantWizard = (): ReactElement => {
onChange={async (e) => { onChange={async (e) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
// Clean up previous preview URL if exists
if (logoPreviewUrl) { if (logoPreviewUrl) {
URL.revokeObjectURL(logoPreviewUrl); URL.revokeObjectURL(logoPreviewUrl);
} }
// Validate file size (2MB max)
if (file.size > 2 * 1024 * 1024) { if (file.size > 2 * 1024 * 1024) {
showToast.error( showToast.error(
"Logo file size must be less than 2MB", "Logo file size must be less than 2MB",
); );
return; return;
} }
// Validate file type
const validTypes = [ const validTypes = [
"image/png", "image/png",
"image/svg+xml", "image/svg+xml",
@ -1342,7 +1348,6 @@ const CreateTenantWizard = (): ReactElement => {
); );
return; return;
} }
// Create local preview URL immediately
const previewUrl = URL.createObjectURL(file); const previewUrl = URL.createObjectURL(file);
setLogoFile(file); setLogoFile(file);
setLogoPreviewUrl(previewUrl); setLogoPreviewUrl(previewUrl);
@ -1388,7 +1393,6 @@ const CreateTenantWizard = (): ReactElement => {
)} )}
</div> </div>
{/* Favicon */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-sm font-medium text-[#0f1724]"> <label className="text-sm font-medium text-[#0f1724]">
Favicon <span className="text-[#e02424]">*</span> Favicon <span className="text-[#e02424]">*</span>
@ -1415,19 +1419,16 @@ const CreateTenantWizard = (): ReactElement => {
onChange={async (e) => { onChange={async (e) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
// Clean up previous preview URL if exists
if (faviconPreviewUrl) { if (faviconPreviewUrl) {
URL.revokeObjectURL(faviconPreviewUrl); URL.revokeObjectURL(faviconPreviewUrl);
} }
// Validate file size (500KB max)
if (file.size > 500 * 1024) { if (file.size > 500 * 1024) {
showToast.error( showToast.error(
"Favicon file size must be less than 500KB", "Favicon file size must be less than 500KB",
); );
return; return;
} }
// Validate file type
const validTypes = [ const validTypes = [
"image/x-icon", "image/x-icon",
"image/png", "image/png",
@ -1439,7 +1440,6 @@ const CreateTenantWizard = (): ReactElement => {
); );
return; return;
} }
// Create local preview URL immediately
const previewUrl = URL.createObjectURL(file); const previewUrl = URL.createObjectURL(file);
setFaviconFile(file); setFaviconFile(file);
setFaviconPreviewUrl(previewUrl); setFaviconPreviewUrl(previewUrl);
@ -1485,6 +1485,7 @@ const CreateTenantWizard = (): ReactElement => {
)} )}
</div> </div>
</div> </div>
*/}
{/* Primary Color */} {/* Primary Color */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

@ -1,4 +1,4 @@
import { useRef, type ReactElement } from "react"; import { useRef, useEffect, type ReactElement } from "react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { import {
DepartmentsTable, DepartmentsTable,
@ -7,10 +7,21 @@ import {
import { PrimaryButton } from "@/components/shared"; import { PrimaryButton } from "@/components/shared";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { usePermissions } from "@/hooks/usePermissions"; import { usePermissions } from "@/hooks/usePermissions";
import { useLocation } from "react-router-dom";
const Departments = (): ReactElement => { const Departments = (): ReactElement => {
const tableRef = useRef<DepartmentsTableRef>(null); const tableRef = useRef<DepartmentsTableRef>(null);
const { canCreate } = usePermissions(); const { canCreate } = usePermissions();
const location = useLocation();
useEffect(() => {
if (location.state?.openCreateModal) {
window.history.replaceState({}, document.title);
setTimeout(() => {
tableRef.current?.openNewModal();
}, 100);
}
}, [location.state]);
return ( return (
<Layout <Layout

View File

@ -1,4 +1,4 @@
import { useRef, type ReactElement } from "react"; import { useRef, useEffect, type ReactElement } from "react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { import {
DesignationsTable, DesignationsTable,
@ -7,10 +7,21 @@ import {
import { PrimaryButton } from "@/components/shared"; import { PrimaryButton } from "@/components/shared";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { usePermissions } from "@/hooks/usePermissions"; import { usePermissions } from "@/hooks/usePermissions";
import { useLocation } from "react-router-dom";
const Designations = (): ReactElement => { const Designations = (): ReactElement => {
const tableRef = useRef<DesignationsTableRef>(null); const tableRef = useRef<DesignationsTableRef>(null);
const { canCreate } = usePermissions(); const { canCreate } = usePermissions();
const location = useLocation();
useEffect(() => {
if (location.state?.openCreateModal) {
window.history.replaceState({}, document.title);
setTimeout(() => {
tableRef.current?.openNewModal();
}, 100);
}
}, [location.state]);
return ( return (
<Layout <Layout

View File

@ -1,8 +1,21 @@
import { type ReactElement } from "react"; import { useRef, useEffect, type ReactElement } from "react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { RolesTable } from "@/components/superadmin"; import { RolesTable, type RolesTableRef } from "@/components/superadmin";
import { useLocation } from "react-router-dom";
const Roles = (): ReactElement => { const Roles = (): ReactElement => {
const tableRef = useRef<RolesTableRef>(null);
const location = useLocation();
useEffect(() => {
if (location.state?.openCreateModal) {
window.history.replaceState({}, document.title);
setTimeout(() => {
tableRef.current?.openNewModal();
}, 100);
}
}, [location.state]);
return ( return (
<Layout <Layout
currentPage="Roles" currentPage="Roles"
@ -11,7 +24,7 @@ const Roles = (): ReactElement => {
description: "Define and manage roles to control user access based on job responsibilities", description: "Define and manage roles to control user access based on job responsibilities",
}} }}
> >
<RolesTable showHeader={true} /> <RolesTable ref={tableRef} showHeader={true} />
</Layout> </Layout>
); );
}; };

View File

@ -29,6 +29,7 @@ import {
// SecondaryButton, // SecondaryButton,
} from "@/components/shared"; } from "@/components/shared";
import { useAppTheme } from "@/hooks/useAppTheme"; import { useAppTheme } from "@/hooks/useAppTheme";
import { showToast } from "@/utils/toast";
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Helpers // Helpers
@ -83,8 +84,11 @@ const QuotaEditModal = ({
}); });
onUpdated(); onUpdated();
onClose(); onClose();
} catch (err) { showToast.success("Quota updated successfully");
alert("Failed to update quota"); } catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to update quota",
);
} finally { } finally {
setIsUpdating(false); setIsUpdating(false);
} }
@ -114,22 +118,22 @@ const QuotaEditModal = ({
</> </>
} }
> >
<div className="p-6 space-y-5"> {/* <div > */}
<FormField <FormField
label="Max Total Storage (GB)" label="Max Total Storage (GB)"
type="number" type="number"
value={maxStorageGB} value={maxStorageGB}
onChange={(e) => setMaxStorageGB(parseInt(e.target.value) || 0)} onChange={(e) => setMaxStorageGB(parseInt(e.target.value) || 1)}
placeholder="e.g. 10 for 10GB" placeholder="e.g. 10 for 10GB"
/> />
<FormField <FormField
label="Max Per-File Size (MB)" label="Max Per-File Size (MB)"
type="number" type="number"
value={maxFileMB} value={maxFileMB}
onChange={(e) => setMaxFileMB(parseInt(e.target.value) || 0)} onChange={(e) => setMaxFileMB(parseInt(e.target.value) || 1)}
placeholder="e.g. 50 for 50MB" placeholder="e.g. 50 for 50MB"
/> />
</div> {/* </div> */}
</Modal> </Modal>
); );
}; };
@ -137,7 +141,9 @@ const QuotaEditModal = ({
const StorageDashboard = (): ReactElement => { const StorageDashboard = (): ReactElement => {
const { primaryColor } = useAppTheme(); const { primaryColor } = useAppTheme();
const [activeTab, setActiveTab] = useState<"stats" | "quota" | "cleanup">("stats"); const [activeTab, setActiveTab] = useState<"stats" | "quota" | "cleanup">(
"stats",
);
const [stats, setStats] = useState<StorageStats | null>(null); const [stats, setStats] = useState<StorageStats | null>(null);
const [quota, setQuota] = useState<StorageQuota | null>(null); const [quota, setQuota] = useState<StorageQuota | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -157,7 +163,8 @@ const StorageDashboard = (): ReactElement => {
setPurgeResult(null); setPurgeResult(null);
setPurgeError(null); setPurgeError(null);
try { try {
const res = await fileAttachmentService.purgeSoftDeletedBlobs(olderThanHours); const res =
await fileAttachmentService.purgeSoftDeletedBlobs(olderThanHours);
if (res.success) { if (res.success) {
setPurgeResult(res.data.purged); setPurgeResult(res.data.purged);
// Refresh usage stats and quota following successful purge // Refresh usage stats and quota following successful purge
@ -167,7 +174,8 @@ const StorageDashboard = (): ReactElement => {
} }
} catch (err: any) { } catch (err: any) {
setPurgeError( setPurgeError(
err?.response?.data?.error?.message || "Failed to purge unreferenced files." err?.response?.data?.error?.message ||
"Failed to purge unreferenced files.",
); );
console.error("Purge error:", err); console.error("Purge error:", err);
} finally { } finally {
@ -516,9 +524,7 @@ const StorageDashboard = (): ReactElement => {
<div className="space-y-6 animate-in slide-in-from-bottom-2 duration-300"> <div className="space-y-6 animate-in slide-in-from-bottom-2 duration-300">
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-2xl overflow-hidden shadow-sm"> <div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-2xl overflow-hidden shadow-sm">
<div className="px-6 py-5 border-b border-[rgba(0,0,0,0.08)] flex items-center gap-2"> <div className="px-6 py-5 border-b border-[rgba(0,0,0,0.08)] flex items-center gap-2">
<Trash2 <Trash2 className="w-5 h-5 text-red-500" />
className="w-5 h-5 text-red-500"
/>
<h3 className="text-base font-black text-[#0e1b2a]"> <h3 className="text-base font-black text-[#0e1b2a]">
Storage Maintenance & Cleanup Storage Maintenance & Cleanup
</h3> </h3>
@ -526,22 +532,30 @@ const StorageDashboard = (): ReactElement => {
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="max-w-2xl"> <div className="max-w-2xl">
<p className="text-sm text-[#475569] leading-relaxed"> <p className="text-sm text-[#475569] leading-relaxed">
When files are deleted, the system decrements their reference counts. Files marked as{" "} When files are deleted, the system decrements their
<strong>soft-deleted with 0 active references</strong> are preserved temporarily on the storage provider reference counts. Files marked as{" "}
to allow background services (like AI embeddings, audits, or indexes) to finish processing them. <strong>soft-deleted with 0 active references</strong> are
preserved temporarily on the storage provider to allow
background services (like AI embeddings, audits, or indexes)
to finish processing them.
</p> </p>
<p className="text-sm text-[#475569] mt-3 leading-relaxed"> <p className="text-sm text-[#475569] mt-3 leading-relaxed">
Use this administrative tool to permanently purge these unreferenced physical files from the storage provider and reclaim space. Use this administrative tool to permanently purge these
unreferenced physical files from the storage provider and
reclaim space.
</p> </p>
</div> </div>
<div className="border border-red-100 rounded-xl p-5 bg-red-50/30 max-w-2xl flex gap-4"> <div className="border border-red-100 rounded-xl p-5 bg-red-50/30 max-w-2xl flex gap-4">
<AlertTriangle className="w-5 h-5 text-red-500 shrink-0 mt-0.5" /> <AlertTriangle className="w-5 h-5 text-red-500 shrink-0 mt-0.5" />
<div> <div>
<h4 className="text-sm font-bold text-red-800">Warning: Permanent Action</h4> <h4 className="text-sm font-bold text-red-800">
Warning: Permanent Action
</h4>
<p className="text-xs text-red-700 mt-1 leading-relaxed"> <p className="text-xs text-red-700 mt-1 leading-relaxed">
Purging files is irreversible. Once deleted from the physical storage provider (local disk, Azure, or AWS), Purging files is irreversible. Once deleted from the
the file binaries cannot be recovered. physical storage provider (local disk, Azure, or AWS), the
file binaries cannot be recovered.
</p> </p>
</div> </div>
</div> </div>
@ -551,18 +565,25 @@ const StorageDashboard = (): ReactElement => {
label="Purge Files Older Than (Hours)" label="Purge Files Older Than (Hours)"
type="number" type="number"
value={olderThanHours} value={olderThanHours}
onChange={(e) => setOlderThanHours(Math.max(0, parseInt(e.target.value) || 0))} onChange={(e) =>
setOlderThanHours(
Math.max(0, parseInt(e.target.value) || 0),
)
}
placeholder="e.g. 24 (0 = purge all eligible immediately)" placeholder="e.g. 24 (0 = purge all eligible immediately)"
/> />
<span className="text-[11px] text-[#9aa6b2] block -mt-2"> <span className="text-[11px] text-[#9aa6b2] block -mt-2">
Set to 0 to instantly purge all unreferenced soft-deleted blobs, or specify hours (e.g., 24) to keep files deleted within that timeframe. Set to 0 to instantly purge all unreferenced soft-deleted
blobs, or specify hours (e.g., 24) to keep files deleted
within that timeframe.
</span> </span>
{purgeResult !== null && ( {purgeResult !== null && (
<div className="p-4 rounded-xl bg-green-50 border border-green-200 text-green-800 text-sm flex items-center gap-2"> <div className="p-4 rounded-xl bg-green-50 border border-green-200 text-green-800 text-sm flex items-center gap-2">
<CheckCircle2 className="w-4.5 h-4.5 text-green-600 shrink-0" /> <CheckCircle2 className="w-4.5 h-4.5 text-green-600 shrink-0" />
<span> <span>
Successfully purged <strong>{purgeResult}</strong> orphaned file binary/binaries, freeing up storage space! Successfully purged <strong>{purgeResult}</strong>{" "}
orphaned file binary/binaries, freeing up storage space!
</span> </span>
</div> </div>
)} )}
@ -625,10 +646,13 @@ const StorageDashboard = (): ReactElement => {
<AlertTriangle className="w-6 h-6 text-red-600" /> <AlertTriangle className="w-6 h-6 text-red-600" />
</div> </div>
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<h3 className="text-base font-bold text-gray-900">Are you absolutely sure?</h3> <h3 className="text-base font-bold text-gray-900">
Are you absolutely sure?
</h3>
<p className="text-sm text-gray-500 leading-relaxed"> <p className="text-sm text-gray-500 leading-relaxed">
This will permanently delete all unreferenced physical file binaries older than{" "} This will permanently delete all unreferenced physical file
<strong>{olderThanHours} hours</strong> from your storage provider. This action is irreversible. binaries older than <strong>{olderThanHours} hours</strong> from
your storage provider. This action is irreversible.
</p> </p>
</div> </div>
</div> </div>

View File

@ -137,14 +137,36 @@ const Tasks = (): ReactElement => {
key: "assignment", key: "assignment",
label: "Assigned To", label: "Assigned To",
render: (task) => { render: (task) => {
const user = task.assignment.assigned_to_name; const assignedUsers = task.assignment.assigned_users || [];
const roleIds = task.assignment.assigned_role_ids; const roleIds = task.assignment.assigned_role_ids;
if (assignedUsers.length > 0) {
if (assignedUsers.length === 1) {
const u = assignedUsers[0];
const text = u.name ? `${u.email} (${u.name})` : u.email;
return <span className="text-gray-900 font-medium">{text}</span>;
} else {
const firstUser = assignedUsers[0];
const displayText = firstUser.name
? `${firstUser.email} (${firstUser.name}) ...`
: `${firstUser.email} ...`;
const allUsersTooltip = assignedUsers.map((u) => u.name ? `${u.email} (${u.name})` : u.email).join("\n");
return (
<span
className="text-gray-900 font-medium cursor-help border-b border-dashed border-gray-400"
title={allUsersTooltip}
>
{displayText}
</span>
);
}
}
return ( return (
<span className=""> <span className="">
{user || {roleIds && roleIds.length > 0
(roleIds && roleIds.length > 0 ? `${roleIds.length} roles`
? `${roleIds.length} roles` : "-"}
: "-")}
</span> </span>
); );
}, },

View File

@ -1,13 +1,24 @@
import { useRef, type ReactElement } from "react"; import { useRef, useEffect, type ReactElement } from "react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { UsersTable, type UsersTableRef } from "@/components/superadmin"; import { UsersTable, type UsersTableRef } from "@/components/superadmin";
import { PrimaryButton } from "@/components/shared"; import { PrimaryButton } from "@/components/shared";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { usePermissions } from "@/hooks/usePermissions"; import { usePermissions } from "@/hooks/usePermissions";
import { useLocation } from "react-router-dom";
const Users = (): ReactElement => { const Users = (): ReactElement => {
const tableRef = useRef<UsersTableRef>(null); const tableRef = useRef<UsersTableRef>(null);
const { canCreate } = usePermissions(); const { canCreate } = usePermissions();
const location = useLocation();
useEffect(() => {
if (location.state?.openCreateModal) {
window.history.replaceState({}, document.title);
setTimeout(() => {
tableRef.current?.openNewModal();
}, 100);
}
}, [location.state]);
return ( return (
<Layout <Layout

View File

@ -176,6 +176,7 @@ export interface WorkflowTask {
assignment: { assignment: {
assigned_user_ids?: string[] | null; assigned_user_ids?: string[] | null;
assigned_to_name: string | null; assigned_to_name: string | null;
assigned_users?: Array<{ email: string; name: string | null }> | null;
assigned_role_ids?: string[] | null; assigned_role_ids?: string[] | null;
assigned_at: string; assigned_at: string;
}; };