diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx
index 9cdbe340..06ab22b5 100644
--- a/adminfront/src/features/tenants/routes/TenantListPage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx
@@ -1,11 +1,11 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
- ChevronDown,
- ChevronUp,
+ ArrowDown,
+ ArrowUp,
+ ArrowUpDown,
Download,
FileSpreadsheet,
- Loader2,
Pencil,
Plus,
RefreshCw,
@@ -53,25 +53,29 @@ import {
importTenantsCSV,
} from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
+import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree";
+import { isSeedTenant } from "../utils/protectedTenants";
import {
- type TenantImportResolution,
type TenantImportPreviewRow,
+ type TenantImportResolution,
buildTenantImportPreview,
parseTenantCSV,
serializeTenantImportCSV,
} from "../utils/tenantCsvImport";
-import { isSeedTenant } from "../utils/protectedTenants";
-import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree";
const tenantCSVTemplate =
"name,type,parent_tenant_slug,slug,memo,email_domain\n";
+type SortConfig = {
+ key: keyof TenantSummary | "recursiveMemberCount";
+ direction: "asc" | "desc";
+};
+
function TenantListPage() {
const navigate = useNavigate();
const [selectedIds, setSelectedIds] = React.useState([]);
const [search, setSearch] = React.useState("");
- const [sortKey, setSortKey] = React.useState("name");
- const [sortOrder, setSortOrder] = React.useState<"asc" | "desc">("asc");
+ const [sortConfig, setSortConfig] = React.useState(null);
const fileInputRef = React.useRef(null);
const [importMessage, setImportMessage] = React.useState("");
const [previewRows, setPreviewRows] = React.useState<
@@ -119,15 +123,6 @@ function TenantListPage() {
},
});
- const handleSort = (key: string) => {
- if (sortKey === key) {
- setSortOrder(sortOrder === "asc" ? "desc" : "asc");
- } else {
- setSortKey(key);
- setSortOrder("asc");
- }
- };
-
const deleteBulkMutation = useMutation({
mutationFn: (ids: string[]) => deleteTenantsBulk(ids),
onSuccess: () => {
@@ -212,45 +207,80 @@ function TenantListPage() {
: null;
const allTenants = query.data?.items ?? [];
- const tenantsWithRecursiveCount = React.useMemo(() => {
- // Build tree to calculate recursiveMemberCount, but we map it back to a flat array for the table
- const { subTree } = buildTenantFullTree(allTenants);
+ const tenants = React.useMemo(() => {
+ // 1. Calculate recursive counts
+ // buildTenantFullTree returns subTree which represents roots, but it also mutates the mapped nodes internally.
+ // However, to easily map them back to a flat list, we can just run the builder,
+ // and then extract the recursive counts.
+ const treeResult = buildTenantFullTree(allTenants);
- const flatMap = new Map();
- const flatten = (nodes: TenantNode[]) => {
+ // Flatten the tree or just extract from allTenants map?
+ // buildTenantFullTree does NOT mutate the objects passed in allTenants. It creates new ones.
+ // Let's create a map of id -> recursiveMemberCount
+ const recursiveCounts = new Map();
+ const extractCounts = (nodes: TenantNode[]) => {
for (const node of nodes) {
- flatMap.set(node.id, node);
- flatten(node.children);
+ recursiveCounts.set(node.id, node.recursiveMemberCount);
+ if (node.children) extractCounts(node.children);
}
};
- flatten(subTree);
+ extractCounts(treeResult.subTree);
- // Map back to allTenants ensuring recursiveMemberCount is present
- return allTenants.map((t) => flatMap.get(t.id) ?? { ...t, children: [], recursiveMemberCount: t.memberCount || 0 });
- }, [allTenants]);
+ let enriched = allTenants.map((t) => ({
+ ...t,
+ recursiveMemberCount: recursiveCounts.get(t.id) ?? t.memberCount ?? 0,
+ }));
- const tenants = React.useMemo(() => {
- let filtered = tenantsWithRecursiveCount;
if (search.trim()) {
const term = search.toLowerCase();
- filtered = filtered.filter(
+ enriched = enriched.filter(
(t) =>
t.name.toLowerCase().includes(term) ||
t.slug.toLowerCase().includes(term),
);
}
- return [...filtered].sort((a, b) => {
- const valA = (a[sortKey as keyof typeof a] || "")
- .toString()
- .toLowerCase();
- const valB = (b[sortKey as keyof typeof b] || "")
- .toString()
- .toLowerCase();
- if (valA < valB) return sortOrder === "asc" ? -1 : 1;
- if (valA > valB) return sortOrder === "asc" ? 1 : -1;
- return 0;
- });
- }, [tenantsWithRecursiveCount, search, sortKey, sortOrder]);
+
+ if (sortConfig) {
+ enriched.sort((a, b) => {
+ const aValue = a[sortConfig.key as keyof typeof a];
+ const bValue = b[sortConfig.key as keyof typeof b];
+
+ if (aValue === bValue) return 0;
+ if (aValue === null || aValue === undefined) return 1;
+ if (bValue === null || bValue === undefined) return -1;
+
+ if (sortConfig.direction === "asc") {
+ return aValue < bValue ? -1 : 1;
+ }
+ return aValue > bValue ? -1 : 1;
+ });
+ }
+
+ return enriched;
+ }, [allTenants, search, sortConfig]);
+
+ const requestSort = (key: SortConfig["key"]) => {
+ let direction: "asc" | "desc" = "asc";
+ if (
+ sortConfig &&
+ sortConfig.key === key &&
+ sortConfig.direction === "asc"
+ ) {
+ direction = "desc";
+ }
+ setSortConfig({ key, direction });
+ };
+
+ const getSortIcon = (key: SortConfig["key"]) => {
+ if (!sortConfig || sortConfig.key !== key) {
+ return ;
+ }
+ return sortConfig.direction === "asc" ? (
+
+ ) : (
+
+ );
+ };
const deletableTenants = React.useMemo(
() => tenants.filter((tenant) => !isSeedTenant(tenant)),
@@ -403,6 +433,19 @@ function TenantListPage() {
+
+
+ setSearch(e.target.value)}
+ />
+
+
{selectedIds.length > 0 && (