From e27ab25376f45909895f3acca5367e3889093147 Mon Sep 17 00:00:00 2001 From: SibarchanNayak Date: Thu, 16 Jul 2026 15:51:15 +0530 Subject: [PATCH] feat: implement modal auto-opening via location state for table pages and enhance StorageDashboard feedback with toasts and input validation. --- .../dashboard/components/QuickActions.tsx | 11 +- src/pages/superadmin/CreateTenantWizard.tsx | 39 +++---- src/pages/tenant/Departments.tsx | 13 ++- src/pages/tenant/Designations.tsx | 13 ++- src/pages/tenant/Roles.tsx | 19 +++- src/pages/tenant/StorageDashboard.tsx | 100 +++++++++++------- src/pages/tenant/Tasks.tsx | 32 +++++- src/pages/tenant/Users.tsx | 13 ++- src/types/workflow.ts | 1 + 9 files changed, 168 insertions(+), 73 deletions(-) diff --git a/src/features/dashboard/components/QuickActions.tsx b/src/features/dashboard/components/QuickActions.tsx index 9ce3be4..976586b 100644 --- a/src/features/dashboard/components/QuickActions.tsx +++ b/src/features/dashboard/components/QuickActions.tsx @@ -6,6 +6,7 @@ import { Settings, Building2, BadgeCheck, + AppWindow, } from "lucide-react"; import { useAppSelector } from "@/hooks/redux-hooks"; import type { QuickAction } from "@/types/dashboard"; @@ -35,7 +36,7 @@ export const QuickActions = () => { btncolor: "#4C89FA", 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, label: "Notification", @@ -55,25 +56,25 @@ export const QuickActions = () => { icon: UserPlus, label: "New User", btncolor: "#4C89FA", - onClick: () => navigate("/tenant/users"), + onClick: () => navigate("/tenant/users", { state: { openCreateModal: true } }), }, hasPermission("roles", "create") && { icon: Shield, label: "New Role", btncolor: "#16C784", - onClick: () => navigate("/tenant/roles"), + onClick: () => navigate("/tenant/roles", { state: { openCreateModal: true } }), }, hasPermission("departments", "create") && { icon: Building2, label: "New Dept", btncolor: "#FCA004", - onClick: () => navigate("/tenant/departments"), + onClick: () => navigate("/tenant/departments", { state: { openCreateModal: true } }), }, hasPermission("designations", "create") && { icon: BadgeCheck, label: "New Desig", btncolor: "#6B7280", - onClick: () => navigate("/tenant/designations"), + onClick: () => navigate("/tenant/designations", { state: { openCreateModal: true } }), }, ].filter(Boolean) as QuickAction[]; diff --git a/src/pages/superadmin/CreateTenantWizard.tsx b/src/pages/superadmin/CreateTenantWizard.tsx index 62f8b7a..306a123 100644 --- a/src/pages/superadmin/CreateTenantWizard.tsx +++ b/src/pages/superadmin/CreateTenantWizard.tsx @@ -11,18 +11,18 @@ import { PrimaryButton, SecondaryButton, MultiselectPaginatedSelect, - AuthenticatedImage, + // AuthenticatedImage, } from "@/components/shared"; import { tenantService } from "@/services/tenant-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 { showToast } from "@/utils/toast"; import { ChevronRight, ChevronLeft, - Image as ImageIcon, - X, + // Image as ImageIcon, + // X, FileText, GraduationCap, AlertTriangle, @@ -40,7 +40,7 @@ import { Settings, Loader2, } from "lucide-react"; -import { generateUUID } from "@/lib/utils"; +// import { generateUUID } from "@/lib/utils"; // Icon mapping helper const serviceIconMap: Record = { @@ -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(null); const [faviconFile, setFaviconFile] = useState(null); const [logoFileAttachmentUuid, setLogoFileAttachmentUuid] = useState< @@ -330,6 +331,7 @@ const CreateTenantWizard = (): ReactElement => { const [isUploadingFavicon, setIsUploadingFavicon] = useState(false); const [logoError, setLogoError] = useState(null); const [faviconError, setFaviconError] = useState(null); + */ // Auto-generate slug and domain from name const nameValue = tenantDetailsForm.watch("name"); @@ -459,6 +461,7 @@ const CreateTenantWizard = (): ReactElement => { } }; + /* const handleDeleteLogo = (): void => { if (logoPreviewUrl) { URL.revokeObjectURL(logoPreviewUrl); @@ -494,11 +497,13 @@ const CreateTenantWizard = (): ReactElement => { fileInput.value = ""; } }; + */ const handleSubmit = async (): Promise => { const isValid = await settingsForm.trigger(); if (!isValid) return; + /* // Validate logo and favicon are uploaded setLogoError(null); setFaviconError(null); @@ -518,6 +523,7 @@ const CreateTenantWizard = (): ReactElement => { setCurrentStep(3); // Go to settings step where logo/favicon are return; } + */ try { setIsSubmitting(true); @@ -560,6 +566,8 @@ const CreateTenantWizard = (): ReactElement => { }; const response = await tenantService.create(tenantData); + + /* const createdTenant = response.data; const createdTenantId = createdTenant.id; const slug = createdTenant.slug || tenantDetails.slug; @@ -630,6 +638,7 @@ const CreateTenantWizard = (): ReactElement => { }; await tenantService.update(createdTenantId, updateData); } + */ const message = response.message || "Tenant created successfully"; showToast.success(message); @@ -672,9 +681,9 @@ const CreateTenantWizard = (): ReactElement => { const fieldName = path.replace("settings.branding.", ""); // Map file_path fields to form fields if (fieldName === "logo_file_path") { - setLogoError(detail.message); + // setLogoError(detail.message); } else if (fieldName === "favicon_file_path") { - setFaviconError(detail.message); + // setFaviconError(detail.message); } else { settingsForm.setError(fieldName as keyof SettingsForm, { type: "server", @@ -1288,9 +1297,9 @@ const CreateTenantWizard = (): ReactElement => {

- {/* Logo and Favicon Upload */} + {/* Logo and Favicon Upload commented out since file attachment bucket can only be set after tenant creation */} + {/*
- {/* Company Logo */}
- {/* Favicon */}
+ */} {/* Primary Color */}
diff --git a/src/pages/tenant/Departments.tsx b/src/pages/tenant/Departments.tsx index ea6e37b..d34f48b 100644 --- a/src/pages/tenant/Departments.tsx +++ b/src/pages/tenant/Departments.tsx @@ -1,4 +1,4 @@ -import { useRef, type ReactElement } from "react"; +import { useRef, useEffect, type ReactElement } from "react"; import { Layout } from "@/components/layout/Layout"; import { DepartmentsTable, @@ -7,10 +7,21 @@ import { import { PrimaryButton } from "@/components/shared"; import { Plus } from "lucide-react"; import { usePermissions } from "@/hooks/usePermissions"; +import { useLocation } from "react-router-dom"; const Departments = (): ReactElement => { const tableRef = useRef(null); const { canCreate } = usePermissions(); + const location = useLocation(); + + useEffect(() => { + if (location.state?.openCreateModal) { + window.history.replaceState({}, document.title); + setTimeout(() => { + tableRef.current?.openNewModal(); + }, 100); + } + }, [location.state]); return ( { const tableRef = useRef(null); const { canCreate } = usePermissions(); + const location = useLocation(); + + useEffect(() => { + if (location.state?.openCreateModal) { + window.history.replaceState({}, document.title); + setTimeout(() => { + tableRef.current?.openNewModal(); + }, 100); + } + }, [location.state]); return ( { + const tableRef = useRef(null); + const location = useLocation(); + + useEffect(() => { + if (location.state?.openCreateModal) { + window.history.replaceState({}, document.title); + setTimeout(() => { + tableRef.current?.openNewModal(); + }, 100); + } + }, [location.state]); + return ( { description: "Define and manage roles to control user access based on job responsibilities", }} > - + ); }; diff --git a/src/pages/tenant/StorageDashboard.tsx b/src/pages/tenant/StorageDashboard.tsx index bf1f857..a688467 100644 --- a/src/pages/tenant/StorageDashboard.tsx +++ b/src/pages/tenant/StorageDashboard.tsx @@ -29,6 +29,7 @@ import { // SecondaryButton, } from "@/components/shared"; import { useAppTheme } from "@/hooks/useAppTheme"; +import { showToast } from "@/utils/toast"; // ───────────────────────────────────────────────────────────────────────────── // Helpers @@ -83,8 +84,11 @@ const QuotaEditModal = ({ }); onUpdated(); onClose(); - } catch (err) { - alert("Failed to update quota"); + showToast.success("Quota updated successfully"); + } catch (err: any) { + showToast.error( + err?.response?.data?.error?.message || "Failed to update quota", + ); } finally { setIsUpdating(false); } @@ -114,22 +118,22 @@ const QuotaEditModal = ({ } > -
- setMaxStorageGB(parseInt(e.target.value) || 0)} - placeholder="e.g. 10 for 10GB" - /> - setMaxFileMB(parseInt(e.target.value) || 0)} - placeholder="e.g. 50 for 50MB" - /> -
+ {/*
*/} + setMaxStorageGB(parseInt(e.target.value) || 1)} + placeholder="e.g. 10 for 10GB" + /> + setMaxFileMB(parseInt(e.target.value) || 1)} + placeholder="e.g. 50 for 50MB" + /> + {/*
*/} ); }; @@ -137,7 +141,9 @@ const QuotaEditModal = ({ const StorageDashboard = (): ReactElement => { const { primaryColor } = useAppTheme(); - const [activeTab, setActiveTab] = useState<"stats" | "quota" | "cleanup">("stats"); + const [activeTab, setActiveTab] = useState<"stats" | "quota" | "cleanup">( + "stats", + ); const [stats, setStats] = useState(null); const [quota, setQuota] = useState(null); const [loading, setLoading] = useState(true); @@ -157,7 +163,8 @@ const StorageDashboard = (): ReactElement => { setPurgeResult(null); setPurgeError(null); try { - const res = await fileAttachmentService.purgeSoftDeletedBlobs(olderThanHours); + const res = + await fileAttachmentService.purgeSoftDeletedBlobs(olderThanHours); if (res.success) { setPurgeResult(res.data.purged); // Refresh usage stats and quota following successful purge @@ -167,7 +174,8 @@ const StorageDashboard = (): ReactElement => { } } catch (err: any) { 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); } finally { @@ -183,7 +191,7 @@ const StorageDashboard = (): ReactElement => { fileAttachmentService.getQuota(), ]); console.log(quotaRes.data); - + setStats(statsRes.data); setQuota(quotaRes.data); } catch (err: any) { @@ -516,9 +524,7 @@ const StorageDashboard = (): ReactElement => {
- +

Storage Maintenance & Cleanup

@@ -526,22 +532,30 @@ const StorageDashboard = (): ReactElement => {

- When files are deleted, the system decrements their reference counts. Files marked as{" "} - soft-deleted with 0 active references are preserved temporarily on the storage provider - to allow background services (like AI embeddings, audits, or indexes) to finish processing them. + When files are deleted, the system decrements their + reference counts. Files marked as{" "} + soft-deleted with 0 active references are + preserved temporarily on the storage provider to allow + background services (like AI embeddings, audits, or indexes) + to finish processing them.

- 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.

-

Warning: Permanent Action

+

+ Warning: Permanent Action +

- Purging files is irreversible. Once deleted from the physical storage provider (local disk, Azure, or AWS), - the file binaries cannot be recovered. + Purging files is irreversible. Once deleted from the + physical storage provider (local disk, Azure, or AWS), the + file binaries cannot be recovered.

@@ -551,18 +565,25 @@ const StorageDashboard = (): ReactElement => { label="Purge Files Older Than (Hours)" type="number" 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)" /> - 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. {purgeResult !== null && (
- Successfully purged {purgeResult} orphaned file binary/binaries, freeing up storage space! + Successfully purged {purgeResult}{" "} + orphaned file binary/binaries, freeing up storage space!
)} @@ -625,10 +646,13 @@ const StorageDashboard = (): ReactElement => {
-

Are you absolutely sure?

+

+ Are you absolutely sure? +

- This will permanently delete all unreferenced physical file binaries older than{" "} - {olderThanHours} hours from your storage provider. This action is irreversible. + This will permanently delete all unreferenced physical file + binaries older than {olderThanHours} hours from + your storage provider. This action is irreversible.

diff --git a/src/pages/tenant/Tasks.tsx b/src/pages/tenant/Tasks.tsx index 11be115..19f5ec7 100644 --- a/src/pages/tenant/Tasks.tsx +++ b/src/pages/tenant/Tasks.tsx @@ -137,14 +137,36 @@ const Tasks = (): ReactElement => { key: "assignment", label: "Assigned To", render: (task) => { - const user = task.assignment.assigned_to_name; + const assignedUsers = task.assignment.assigned_users || []; 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 {text}; + } 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 ( + + {displayText} + + ); + } + } + return ( - {user || - (roleIds && roleIds.length > 0 - ? `${roleIds.length} roles` - : "-")} + {roleIds && roleIds.length > 0 + ? `${roleIds.length} roles` + : "-"} ); }, diff --git a/src/pages/tenant/Users.tsx b/src/pages/tenant/Users.tsx index e7f4646..c0f79ea 100644 --- a/src/pages/tenant/Users.tsx +++ b/src/pages/tenant/Users.tsx @@ -1,13 +1,24 @@ -import { useRef, type ReactElement } from "react"; +import { useRef, useEffect, type ReactElement } from "react"; import { Layout } from "@/components/layout/Layout"; import { UsersTable, type UsersTableRef } from "@/components/superadmin"; import { PrimaryButton } from "@/components/shared"; import { Plus } from "lucide-react"; import { usePermissions } from "@/hooks/usePermissions"; +import { useLocation } from "react-router-dom"; const Users = (): ReactElement => { const tableRef = useRef(null); const { canCreate } = usePermissions(); + const location = useLocation(); + + useEffect(() => { + if (location.state?.openCreateModal) { + window.history.replaceState({}, document.title); + setTimeout(() => { + tableRef.current?.openNewModal(); + }, 100); + } + }, [location.state]); return ( | null; assigned_role_ids?: string[] | null; assigned_at: string; };