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:
parent
b8a4f4825a
commit
e27ab25376
@ -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[];
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -183,7 +191,7 @@ const StorageDashboard = (): ReactElement => {
|
|||||||
fileAttachmentService.getQuota(),
|
fileAttachmentService.getQuota(),
|
||||||
]);
|
]);
|
||||||
console.log(quotaRes.data);
|
console.log(quotaRes.data);
|
||||||
|
|
||||||
setStats(statsRes.data);
|
setStats(statsRes.data);
|
||||||
setQuota(quotaRes.data);
|
setQuota(quotaRes.data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user