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);
|
||||
}
|
||||
|
||||
@@ -1281,6 +1281,7 @@ title = "Affiliation & Organization Info"
|
||||
add = "Add User"
|
||||
add_to_tenant = "Add to Tenant"
|
||||
bulk_import = "Bulk Import"
|
||||
change_status = "Change {{name}} status"
|
||||
empty = "No users found."
|
||||
fetch_error = "Failed to fetch user list."
|
||||
search_label = "Search Users"
|
||||
@@ -1360,15 +1361,20 @@ user = "User"
|
||||
|
||||
[ui.common.status]
|
||||
active = "Active"
|
||||
archived = "Archived"
|
||||
baron_guest = "Baron Guest"
|
||||
blocked = "Blocked"
|
||||
extended_leave = "Extended Leave"
|
||||
failure = "Failure"
|
||||
inactive = "Inactive"
|
||||
leave_of_absence = "Leave of absence"
|
||||
ok = "Ok"
|
||||
pending = "Pending"
|
||||
preboarding = "Preboarding"
|
||||
status = "Status"
|
||||
success = "Success"
|
||||
suspended = "Suspended"
|
||||
temporary_leave = "Temporary Leave"
|
||||
|
||||
[test]
|
||||
key = "Test"
|
||||
|
||||
@@ -1283,6 +1283,7 @@ title = "소속 및 조직 정보"
|
||||
add = "사용자 추가"
|
||||
add_to_tenant = "테넌트에 추가"
|
||||
bulk_import = "일괄 임포트"
|
||||
change_status = "{{name}} 상태 변경"
|
||||
empty = "검색 결과가 없습니다."
|
||||
fetch_error = "사용자 목록 조회에 실패했습니다."
|
||||
search_label = "사용자 검색"
|
||||
@@ -1362,15 +1363,20 @@ user = "User"
|
||||
|
||||
[ui.common.status]
|
||||
active = "활성"
|
||||
archived = "보관됨"
|
||||
baron_guest = "Baron 게스트"
|
||||
blocked = "차단됨"
|
||||
extended_leave = "장기휴직"
|
||||
failure = "실패"
|
||||
inactive = "비활성"
|
||||
leave_of_absence = "휴직"
|
||||
ok = "정상"
|
||||
pending = "준비 중"
|
||||
preboarding = "입사대기"
|
||||
status = "상태"
|
||||
success = "성공"
|
||||
suspended = "정지"
|
||||
temporary_leave = "단기휴무"
|
||||
|
||||
[test]
|
||||
key = "테스트"
|
||||
|
||||
@@ -1295,6 +1295,7 @@ title = ""
|
||||
[ui.admin.users.list]
|
||||
add = ""
|
||||
bulk_import = ""
|
||||
change_status = ""
|
||||
empty = ""
|
||||
fetch_error = ""
|
||||
search_placeholder = ""
|
||||
@@ -1340,15 +1341,20 @@ user = ""
|
||||
|
||||
[ui.common.status]
|
||||
active = ""
|
||||
archived = ""
|
||||
baron_guest = ""
|
||||
blocked = ""
|
||||
extended_leave = ""
|
||||
failure = ""
|
||||
inactive = ""
|
||||
leave_of_absence = ""
|
||||
ok = ""
|
||||
pending = ""
|
||||
preboarding = ""
|
||||
status = ""
|
||||
success = ""
|
||||
suspended = ""
|
||||
temporary_leave = ""
|
||||
|
||||
[test]
|
||||
key = ""
|
||||
|
||||
Reference in New Issue
Block a user