1
0
forked from baron/baron-sso

chore: snapshot local state before dev merge

This commit is contained in:
2026-06-17 21:25:42 +09:00
parent b2808759d2
commit 49560e8a8c
107 changed files with 8958 additions and 939 deletions

View File

@@ -104,6 +104,10 @@ import {
type TenantImportPreviewRow,
type TenantImportResolution,
} from "../utils/tenantCsvImport";
import {
TENANT_VISIBILITY_OPTIONS,
type TenantVisibility,
} from "../utils/orgConfig";
import {
filterTenantsByScope,
filterTenantViewRowsBySearch,
@@ -119,14 +123,30 @@ const tenantCSVTemplate =
const tenantPageSize = 500;
const _tenantVirtualizationThreshold = 250;
const _tenantEstimatedRowHeight = 73;
type TenantSortKey = keyof TenantSummary | "recursiveMemberCount";
const tenantTableHeadClassName =
"h-9 px-3 py-1 text-xs leading-tight align-middle whitespace-nowrap";
const tenantTableHeadInteractiveClassName = `${tenantTableHeadClassName} cursor-pointer transition-colors hover:bg-muted/50`;
const tenantTableHeadContentClassName = "flex h-full items-center gap-1";
const _tenantLoadAheadPx = 360;
const _tenantLoadAheadRows = 30;
type TenantSortKey = keyof TenantSummary | "recursiveMemberCount";
const backendTenantSortKeys = new Set<TenantSortKey>([
"createdAt",
"id",
"name",
"slug",
"status",
"type",
"updatedAt",
]);
const bulkTenantTypeOptions = [
{ value: "COMPANY", label: "COMPANY (일반 기업)" },
{ value: "COMPANY_GROUP", label: "COMPANY_GROUP (그룹사/지주사)" },
{ value: "ORGANIZATION", label: "ORGANIZATION (정규 조직)" },
{ value: "USER_GROUP", label: "USER_GROUP (내부 부서/팀)" },
{ value: "PERSONAL", label: "PERSONAL (개인 워크스페이스)" },
] as const;
const getTenantIcon = (type?: string) => {
switch (type?.toUpperCase()) {
@@ -370,6 +390,10 @@ function TenantListPage() {
const [search, setSearch] = React.useState("");
const debouncedSearch = React.useDeferredValue(search.trim());
const [selectedBulkStatus, setSelectedBulkStatus] = React.useState("");
const [selectedBulkType, setSelectedBulkType] = React.useState("");
const [selectedBulkVisibility, setSelectedBulkVisibility] = React.useState<
TenantVisibility | ""
>("");
const _tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
const { data: profile } = useQuery({
@@ -380,9 +404,20 @@ function TenantListPage() {
const isWritable =
profileRole === "super_admin" ||
!!profile?.systemPermissions?.manage_tenants;
const backendSortKey =
sortConfig && backendTenantSortKeys.has(sortConfig.key)
? sortConfig.key
: undefined;
const query = useInfiniteQuery({
queryKey: ["tenants", "lazy", debouncedSearch, scopeTenantId],
queryKey: [
"tenants",
"lazy",
debouncedSearch,
scopeTenantId,
backendSortKey,
sortConfig?.direction,
],
queryFn: ({ pageParam }) =>
fetchTenants(
tenantPageSize,
@@ -390,12 +425,19 @@ function TenantListPage() {
scopeTenantId || undefined,
pageParam ? (pageParam as string) : undefined,
debouncedSearch,
backendSortKey,
sortConfig?.direction,
),
initialPageParam: "",
getNextPageParam: (lastPage) =>
lastPage.nextCursor || lastPage.next_cursor || undefined,
});
const rawTenants = React.useMemo(
() => query.data?.pages.flatMap((page) => page.items) ?? [],
[query.data?.pages],
);
const deleteBulkMutation = useMutation({
mutationFn: (ids: string[]) => deleteTenantsBulk(ids),
onSuccess: () => {
@@ -404,21 +446,37 @@ function TenantListPage() {
},
});
const bulkUpdateStatusMutation = useMutation({
const bulkUpdateTenantsMutation = useMutation({
mutationFn: async ({
tenantIds,
status,
type,
visibility,
}: {
tenantIds: string[];
status: string;
status?: string;
type?: string;
visibility?: TenantVisibility;
}) => {
// Execute sequential updates to avoid rate limits or partial failures
await Promise.all(tenantIds.map((id) => updateTenant(id, { status })));
await Promise.all(
tenantIds.map((id) => {
const source = rawTenants.find((tenant) => tenant.id === id);
return updateTenant(id, {
...(status ? { status } : {}),
...(type ? { type } : {}),
...(visibility
? { config: { ...(source?.config ?? {}), visibility } }
: {}),
});
}),
);
},
onSuccess: () => {
query.refetch();
setSelectedIds([]);
setSelectedBulkStatus("");
setSelectedBulkType("");
setSelectedBulkVisibility("");
toast.success(
t(
"msg.admin.tenants.bulk.update_success",
@@ -437,10 +495,19 @@ function TenantListPage() {
});
const handleApplyBulkStatus = () => {
if (selectedIds.length === 0 || !selectedBulkStatus) return;
bulkUpdateStatusMutation.mutate({
if (
selectedIds.length === 0 ||
(!selectedBulkStatus && !selectedBulkType && !selectedBulkVisibility)
) {
return;
}
bulkUpdateTenantsMutation.mutate({
tenantIds: selectedIds,
status: selectedBulkStatus,
...(selectedBulkStatus ? { status: selectedBulkStatus } : {}),
...(selectedBulkType ? { type: selectedBulkType } : {}),
...(selectedBulkVisibility
? { visibility: selectedBulkVisibility }
: {}),
});
};
@@ -491,11 +558,6 @@ function TenantListPage() {
},
});
const rawTenants = React.useMemo(
() => query.data?.pages.flatMap((page) => page.items) ?? [],
[query.data?.pages],
);
const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response
?.data?.error;
const fallbackError =
@@ -1067,15 +1129,66 @@ function TenantListPage() {
</SelectItem>
</SelectContent>
</Select>
<Select value={selectedBulkType} onValueChange={setSelectedBulkType}>
<SelectTrigger
className="h-8 w-[180px] bg-transparent border-background/20 text-background text-xs"
data-testid="tenant-bulk-type-select"
>
<SelectValue
placeholder={t(
"ui.admin.tenants.bulk.type_placeholder",
"유형 선택",
)}
/>
</SelectTrigger>
<SelectContent>
{bulkTenantTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{t(
`domain.tenant_type.${option.value.toLowerCase()}`,
option.label,
)}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={selectedBulkVisibility}
onValueChange={(value) =>
setSelectedBulkVisibility(value as TenantVisibility)
}
>
<SelectTrigger
className="h-8 w-[130px] bg-transparent border-background/20 text-background text-xs"
data-testid="tenant-bulk-visibility-select"
>
<SelectValue
placeholder={t(
"ui.admin.tenants.bulk.visibility_placeholder",
"공개 범위",
)}
/>
</SelectTrigger>
<SelectContent>
{TENANT_VISIBILITY_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="sm"
className="text-background hover:bg-background/10 h-8"
onClick={handleApplyBulkStatus}
disabled={
!selectedBulkStatus || bulkUpdateStatusMutation.isPending
(!selectedBulkStatus &&
!selectedBulkType &&
!selectedBulkVisibility) ||
bulkUpdateTenantsMutation.isPending
}
data-testid="tenant-bulk-apply-status-btn"
data-testid="tenant-bulk-apply-btn"
>
{t("ui.common.apply", "적용")}
</Button>