forked from baron/baron-sso
사용자 상태 세분화
This commit is contained in:
@@ -44,6 +44,13 @@ import {
|
||||
} from "../../components/ui/dialog";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Label } from "../../components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../components/ui/select";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import {
|
||||
Tabs,
|
||||
@@ -78,6 +85,11 @@ import {
|
||||
parseOrgChartTenantSelection,
|
||||
} from "./orgChartPicker";
|
||||
import type { UserSchemaField } from "./userSchemaFields";
|
||||
import {
|
||||
normalizeUserStatusValue,
|
||||
userStatusLabel,
|
||||
userStatusValues,
|
||||
} from "./userStatus";
|
||||
import { resolvePersonalTenant } from "./utils/personalTenant";
|
||||
|
||||
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
|
||||
@@ -603,7 +615,7 @@ function UserDetailPage() {
|
||||
name: user.name,
|
||||
phone: user.phone || "",
|
||||
role: user.role,
|
||||
status: user.status,
|
||||
status: normalizeUserStatusValue(user.status),
|
||||
tenantSlug:
|
||||
user.tenantSlug ||
|
||||
user.joinedTenants?.find(
|
||||
@@ -1044,21 +1056,25 @@ function UserDetailPage() {
|
||||
>
|
||||
{t("ui.admin.users.detail.form.status", "상태")}
|
||||
</Label>
|
||||
<div className="flex h-11 items-center gap-3 rounded-md border border-input bg-background px-3">
|
||||
<Switch
|
||||
id="status"
|
||||
checked={watchedStatus === "active"}
|
||||
onCheckedChange={(checked) =>
|
||||
setValue("status", checked ? "active" : "inactive")
|
||||
}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
`ui.common.status.${watchedStatus}`,
|
||||
watchedStatus || "inactive",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<Select
|
||||
value={normalizeUserStatusValue(watchedStatus || "")}
|
||||
onValueChange={(value) =>
|
||||
setValue("status", normalizeUserStatusValue(value), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="status" className="h-11 shadow-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{userStatusValues.map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{userStatusLabel(status)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../components/ui/select";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -93,6 +92,7 @@ import { t } from "../../lib/i18n";
|
||||
import { isSuperAdminRole } from "../../lib/roles";
|
||||
import { UserBulkUploadModal } from "./components/UserBulkUploadModal";
|
||||
import {
|
||||
normalizeUserStatusValue,
|
||||
type UserStatusValue,
|
||||
userStatusLabel,
|
||||
userStatusValues,
|
||||
@@ -776,30 +776,37 @@ function UserListPage() {
|
||||
{user.id}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={user.status === "active"}
|
||||
onCheckedChange={(checked) =>
|
||||
statusMutation.mutate({
|
||||
userId: user.id,
|
||||
status: checked ? "active" : "inactive",
|
||||
})
|
||||
}
|
||||
disabled={
|
||||
statusMutation.isPending ||
|
||||
user.id === profile?.id
|
||||
}
|
||||
<Select
|
||||
value={normalizeUserStatusValue(user.status)}
|
||||
onValueChange={(status) =>
|
||||
statusMutation.mutate({
|
||||
userId: user.id,
|
||||
status,
|
||||
})
|
||||
}
|
||||
disabled={
|
||||
statusMutation.isPending || user.id === profile?.id
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-8 w-[150px] border-none bg-transparent hover:bg-muted/50 transition-colors px-0 font-medium"
|
||||
aria-label={t(
|
||||
"ui.admin.users.list.toggle_status",
|
||||
"{{name}} 활성 상태",
|
||||
"ui.admin.users.list.change_status",
|
||||
"{{name}} 상태 변경",
|
||||
{ name: user.name },
|
||||
)}
|
||||
data-testid={`user-status-toggle-${user.id}`}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t(`ui.common.status.${user.status}`, user.status)}
|
||||
</span>
|
||||
</div>
|
||||
data-testid={`user-status-select-${user.id}`}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{userStatusValues.map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{userStatusLabel(status)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Select
|
||||
@@ -894,13 +901,11 @@ function UserListPage() {
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{userStatusValues
|
||||
.filter((s) => s === "active" || s === "inactive")
|
||||
.map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{userStatusLabel(status)}
|
||||
</SelectItem>
|
||||
))}
|
||||
{userStatusValues.map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{userStatusLabel(status)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{canPromoteSuperAdmin && (
|
||||
|
||||
38
adminfront/src/features/users/userStatus.test.ts
Normal file
38
adminfront/src/features/users/userStatus.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
normalizeUserStatusValue,
|
||||
userStatusLabel,
|
||||
userStatusValues,
|
||||
} from "./userStatus";
|
||||
|
||||
vi.mock("../../lib/i18n", () => ({
|
||||
t: (key: string, fallback?: string) => fallback ?? key,
|
||||
}));
|
||||
|
||||
describe("userStatus", () => {
|
||||
it("exposes canonical user status values", () => {
|
||||
expect(userStatusValues).toEqual([
|
||||
"active",
|
||||
"temporary_leave",
|
||||
"suspended",
|
||||
"preboarding",
|
||||
"baron_guest",
|
||||
"extended_leave",
|
||||
"archived",
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes legacy status values", () => {
|
||||
expect(normalizeUserStatusValue("inactive")).toBe("preboarding");
|
||||
expect(normalizeUserStatusValue("leave_of_absence")).toBe(
|
||||
"temporary_leave",
|
||||
);
|
||||
expect(normalizeUserStatusValue("baron_only")).toBe("baron_guest");
|
||||
});
|
||||
|
||||
it("uses canonical labels for legacy status values", () => {
|
||||
expect(userStatusLabel("baron_only")).toBe("baron_guest");
|
||||
});
|
||||
});
|
||||
@@ -2,13 +2,42 @@ import { t } from "../../lib/i18n";
|
||||
|
||||
export const userStatusValues = [
|
||||
"active",
|
||||
"inactive",
|
||||
"temporary_leave",
|
||||
"suspended",
|
||||
"leave_of_absence",
|
||||
"preboarding",
|
||||
"baron_guest",
|
||||
"extended_leave",
|
||||
"archived",
|
||||
] as const;
|
||||
|
||||
export type UserStatusValue = (typeof userStatusValues)[number];
|
||||
|
||||
export function userStatusLabel(status: string) {
|
||||
return t(`ui.common.status.${status}`, status);
|
||||
export function normalizeUserStatusValue(status: string): UserStatusValue {
|
||||
switch (status.trim().toLowerCase()) {
|
||||
case "active":
|
||||
return "active";
|
||||
case "temporary_leave":
|
||||
case "leave_of_absence":
|
||||
return "temporary_leave";
|
||||
case "suspended":
|
||||
case "blocked":
|
||||
return "suspended";
|
||||
case "preboarding":
|
||||
case "inactive":
|
||||
return "preboarding";
|
||||
case "baron_guest":
|
||||
case "baron_only":
|
||||
return "baron_guest";
|
||||
case "extended_leave":
|
||||
return "extended_leave";
|
||||
case "archived":
|
||||
return "archived";
|
||||
default:
|
||||
return "preboarding";
|
||||
}
|
||||
}
|
||||
|
||||
export function userStatusLabel(status: string) {
|
||||
const normalized = normalizeUserStatusValue(status);
|
||||
return t(`ui.common.status.${normalized}`, normalized);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user