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,
|
||||
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[];
|
||||
|
||||
|
||||
@ -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<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 [faviconFile, setFaviconFile] = useState<File | null>(null);
|
||||
const [logoFileAttachmentUuid, setLogoFileAttachmentUuid] = useState<
|
||||
@ -330,6 +331,7 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
const [isUploadingFavicon, setIsUploadingFavicon] = useState<boolean>(false);
|
||||
const [logoError, setLogoError] = useState<string | null>(null);
|
||||
const [faviconError, setFaviconError] = useState<string | null>(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<void> => {
|
||||
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 => {
|
||||
</p>
|
||||
</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">
|
||||
{/* Company Logo */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">
|
||||
Company Logo <span className="text-[#e02424]">*</span>
|
||||
@ -1317,19 +1326,16 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Clean up previous preview URL if exists
|
||||
if (logoPreviewUrl) {
|
||||
URL.revokeObjectURL(logoPreviewUrl);
|
||||
}
|
||||
|
||||
// Validate file size (2MB max)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
showToast.error(
|
||||
"Logo file size must be less than 2MB",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Validate file type
|
||||
const validTypes = [
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
@ -1342,7 +1348,6 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Create local preview URL immediately
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
setLogoFile(file);
|
||||
setLogoPreviewUrl(previewUrl);
|
||||
@ -1388,7 +1393,6 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Favicon */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">
|
||||
Favicon <span className="text-[#e02424]">*</span>
|
||||
@ -1415,19 +1419,16 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Clean up previous preview URL if exists
|
||||
if (faviconPreviewUrl) {
|
||||
URL.revokeObjectURL(faviconPreviewUrl);
|
||||
}
|
||||
|
||||
// Validate file size (500KB max)
|
||||
if (file.size > 500 * 1024) {
|
||||
showToast.error(
|
||||
"Favicon file size must be less than 500KB",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Validate file type
|
||||
const validTypes = [
|
||||
"image/x-icon",
|
||||
"image/png",
|
||||
@ -1439,7 +1440,6 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Create local preview URL immediately
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
setFaviconFile(file);
|
||||
setFaviconPreviewUrl(previewUrl);
|
||||
@ -1485,6 +1485,7 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
*/}
|
||||
|
||||
{/* Primary Color */}
|
||||
<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 {
|
||||
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<DepartmentsTableRef>(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 (
|
||||
<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 {
|
||||
DesignationsTable,
|
||||
@ -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 Designations = (): ReactElement => {
|
||||
const tableRef = useRef<DesignationsTableRef>(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 (
|
||||
<Layout
|
||||
|
||||
@ -1,8 +1,21 @@
|
||||
import { type ReactElement } from "react";
|
||||
import { useRef, useEffect, type ReactElement } from "react";
|
||||
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 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 (
|
||||
<Layout
|
||||
currentPage="Roles"
|
||||
@ -11,7 +24,7 @@ const Roles = (): ReactElement => {
|
||||
description: "Define and manage roles to control user access based on job responsibilities",
|
||||
}}
|
||||
>
|
||||
<RolesTable showHeader={true} />
|
||||
<RolesTable ref={tableRef} showHeader={true} />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 = ({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="p-6 space-y-5">
|
||||
<FormField
|
||||
label="Max Total Storage (GB)"
|
||||
type="number"
|
||||
value={maxStorageGB}
|
||||
onChange={(e) => setMaxStorageGB(parseInt(e.target.value) || 0)}
|
||||
placeholder="e.g. 10 for 10GB"
|
||||
/>
|
||||
<FormField
|
||||
label="Max Per-File Size (MB)"
|
||||
type="number"
|
||||
value={maxFileMB}
|
||||
onChange={(e) => setMaxFileMB(parseInt(e.target.value) || 0)}
|
||||
placeholder="e.g. 50 for 50MB"
|
||||
/>
|
||||
</div>
|
||||
{/* <div > */}
|
||||
<FormField
|
||||
label="Max Total Storage (GB)"
|
||||
type="number"
|
||||
value={maxStorageGB}
|
||||
onChange={(e) => setMaxStorageGB(parseInt(e.target.value) || 1)}
|
||||
placeholder="e.g. 10 for 10GB"
|
||||
/>
|
||||
<FormField
|
||||
label="Max Per-File Size (MB)"
|
||||
type="number"
|
||||
value={maxFileMB}
|
||||
onChange={(e) => setMaxFileMB(parseInt(e.target.value) || 1)}
|
||||
placeholder="e.g. 50 for 50MB"
|
||||
/>
|
||||
{/* </div> */}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -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<StorageStats | null>(null);
|
||||
const [quota, setQuota] = useState<StorageQuota | null>(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 => {
|
||||
<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="px-6 py-5 border-b border-[rgba(0,0,0,0.08)] flex items-center gap-2">
|
||||
<Trash2
|
||||
className="w-5 h-5 text-red-500"
|
||||
/>
|
||||
<Trash2 className="w-5 h-5 text-red-500" />
|
||||
<h3 className="text-base font-black text-[#0e1b2a]">
|
||||
Storage Maintenance & Cleanup
|
||||
</h3>
|
||||
@ -526,22 +532,30 @@ const StorageDashboard = (): ReactElement => {
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-sm text-[#475569] leading-relaxed">
|
||||
When files are deleted, the system decrements their reference counts. Files marked as{" "}
|
||||
<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.
|
||||
When files are deleted, the system decrements their
|
||||
reference counts. Files marked as{" "}
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
<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">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -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)"
|
||||
/>
|
||||
<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>
|
||||
|
||||
{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">
|
||||
<CheckCircle2 className="w-4.5 h-4.5 text-green-600 shrink-0" />
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
@ -625,10 +646,13 @@ const StorageDashboard = (): ReactElement => {
|
||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||
</div>
|
||||
<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">
|
||||
This will permanently delete all unreferenced physical file binaries older than{" "}
|
||||
<strong>{olderThanHours} hours</strong> from your storage provider. This action is irreversible.
|
||||
This will permanently delete all unreferenced physical file
|
||||
binaries older than <strong>{olderThanHours} hours</strong> from
|
||||
your storage provider. This action is irreversible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 <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 (
|
||||
<span className="">
|
||||
{user ||
|
||||
(roleIds && roleIds.length > 0
|
||||
? `${roleIds.length} roles`
|
||||
: "-")}
|
||||
{roleIds && roleIds.length > 0
|
||||
? `${roleIds.length} roles`
|
||||
: "-"}
|
||||
</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 { 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<UsersTableRef>(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 (
|
||||
<Layout
|
||||
|
||||
@ -176,6 +176,7 @@ export interface WorkflowTask {
|
||||
assignment: {
|
||||
assigned_user_ids?: string[] | null;
|
||||
assigned_to_name: string | null;
|
||||
assigned_users?: Array<{ email: string; name: string | null }> | null;
|
||||
assigned_role_ids?: string[] | null;
|
||||
assigned_at: string;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user