1
0
forked from baron/baron-sso

feat: enhance tenant admin experience with form locking and column visibility settings

This commit is contained in:
2026-03-04 13:55:54 +09:00
parent 88720b48c4
commit 2b4b40c0d9
3 changed files with 105 additions and 6 deletions

View File

@@ -50,10 +50,16 @@ function UserCreatePage() {
});
const tenants = tenantsData?.items ?? [];
const { data: profile } = useQuery({
queryKey: ["me"],
queryFn: fetchMe,
});
const {
register,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<UserFormValues>({
defaultValues: {
@@ -70,6 +76,13 @@ function UserCreatePage() {
},
});
// Lock company for tenant_admin
React.useEffect(() => {
if (profile?.role === "tenant_admin" && profile.companyCode) {
setValue("companyCode", profile.companyCode);
}
}, [profile, setValue]);
const selectedCompanyCode = watch("companyCode");
const selectedTenant = tenants.find((t) => t.slug === selectedCompanyCode);
@@ -352,6 +365,7 @@ function UserCreatePage() {
id="companyCode"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
{...register("companyCode")}
disabled={profile?.role === "tenant_admin"}
>
<option value="">
{t(

View File

@@ -16,6 +16,7 @@ import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import {
type UserUpdateRequest,
fetchMe,
fetchTenant,
fetchTenants,
fetchUser,
@@ -42,6 +43,11 @@ function UserDetailPage() {
const [error, setError] = React.useState<string | null>(null);
const [successMsg, setSuccessMsg] = React.useState<string | null>(null);
const { data: profile } = useQuery({
queryKey: ["me"],
queryFn: fetchMe,
});
const {
data: user,
isLoading,
@@ -332,6 +338,7 @@ function UserDetailPage() {
id="companyCode"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
{...register("companyCode")}
disabled={profile?.role === "tenant_admin"}
>
<option value="">
{t(

View File

@@ -7,6 +7,7 @@ import {
Plus,
RefreshCw,
Search,
Settings2,
Trash2,
User,
} from "lucide-react";
@@ -21,6 +22,15 @@ import {
CardHeader,
CardTitle,
} from "../../components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import {
Table,
@@ -52,6 +62,7 @@ function UserListPage() {
const [search, setSearch] = React.useState("");
const [searchDraft, setSearchDraft] = React.useState("");
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
const [visibleColumns, setVisibleColumns] = React.useState<Record<string, boolean>>({});
const limit = 50;
const offset = (page - 1) * limit;
@@ -89,6 +100,28 @@ function UserListPage() {
? (tenantDetail?.config?.userSchema as UserSchemaField[])
: [];
// Initialize visible columns when schema changes
React.useEffect(() => {
if (userSchema.length > 0) {
const initial: Record<string, boolean> = {};
for (const field of userSchema) {
initial[field.key] = true;
}
setVisibleColumns((prev) => {
// Only set if not already set for these keys to avoid reset on every render
const next = { ...initial, ...prev };
return next;
});
}
}, [userSchema]);
const toggleColumn = (key: string) => {
setVisibleColumns((prev) => ({
...prev,
[key]: !prev[key],
}));
};
const query = useQuery({
queryKey: ["users", { limit, offset, search, companyCode: selectedCompany }],
queryFn: () => fetchUsers(limit, offset, search, selectedCompany),
@@ -173,6 +206,47 @@ function UserListPage() {
{t("ui.common.refresh", "새로고침")}
</Button>
<UserBulkUploadModal onSuccess={() => query.refetch()} />
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Settings2 size={16} />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("ui.admin.users.list.columns.title", "표시 컬럼 설정")}</DialogTitle>
<DialogDescription>
{t("msg.admin.users.list.columns.description", "사용자 목록에 표시할 커스텀 필드를 선택하세요.")}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
{userSchema.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">
{t("msg.admin.users.list.columns.no_custom", "현재 테넌트에 정의된 커스텀 필드가 없습니다.")}
</p>
)}
{userSchema.map((field) => (
<label key={field.key} className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 cursor-pointer">
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
checked={visibleColumns[field.key] !== false}
onChange={() => toggleColumn(field.key)}
/>
<div className="flex flex-col">
<span className="text-sm font-medium">{field.label}</span>
<span className="text-xs text-muted-foreground font-mono">{field.key}</span>
</div>
</label>
))}
</div>
<DialogFooter>
<DialogTrigger asChild>
<Button variant="secondary">{t("ui.common.close", "닫기")}</Button>
</DialogTrigger>
</DialogFooter>
</DialogContent>
</Dialog>
<Button asChild>
<Link to="/users/new">
<Plus size={16} />
@@ -267,9 +341,11 @@ function UserListPage() {
</TableHead>
{/* Dynamic Columns from Schema */}
{userSchema.map((field) => (
<TableHead key={field.key} className="uppercase">
{field.label}
</TableHead>
visibleColumns[field.key] !== false && (
<TableHead key={field.key} className="uppercase">
{field.label}
</TableHead>
)
))}
<TableHead>
{t("ui.admin.users.list.table.created", "CREATED")}
@@ -341,9 +417,11 @@ function UserListPage() {
</TableCell>
{/* Dynamic Metadata Cells */}
{userSchema.map((field) => (
<TableCell key={field.key} className="text-sm">
{String(user.metadata?.[field.key] ?? "-")}
</TableCell>
visibleColumns[field.key] !== false && (
<TableCell key={field.key} className="text-sm">
{String(user.metadata?.[field.key] ?? "-")}
</TableCell>
)
))}
<TableCell className="text-sm text-muted-foreground">
{new Date(user.createdAt).toLocaleDateString()}