forked from baron/baron-sso
feat: enhance tenant admin experience with form locking and column visibility settings
This commit is contained in:
@@ -50,10 +50,16 @@ function UserCreatePage() {
|
|||||||
});
|
});
|
||||||
const tenants = tenantsData?.items ?? [];
|
const tenants = tenantsData?.items ?? [];
|
||||||
|
|
||||||
|
const { data: profile } = useQuery({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<UserFormValues>({
|
} = useForm<UserFormValues>({
|
||||||
defaultValues: {
|
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 selectedCompanyCode = watch("companyCode");
|
||||||
const selectedTenant = tenants.find((t) => t.slug === selectedCompanyCode);
|
const selectedTenant = tenants.find((t) => t.slug === selectedCompanyCode);
|
||||||
|
|
||||||
@@ -352,6 +365,7 @@ function UserCreatePage() {
|
|||||||
id="companyCode"
|
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"
|
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")}
|
{...register("companyCode")}
|
||||||
|
disabled={profile?.role === "tenant_admin"}
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Input } from "../../components/ui/input";
|
|||||||
import { Label } from "../../components/ui/label";
|
import { Label } from "../../components/ui/label";
|
||||||
import {
|
import {
|
||||||
type UserUpdateRequest,
|
type UserUpdateRequest,
|
||||||
|
fetchMe,
|
||||||
fetchTenant,
|
fetchTenant,
|
||||||
fetchTenants,
|
fetchTenants,
|
||||||
fetchUser,
|
fetchUser,
|
||||||
@@ -42,6 +43,11 @@ function UserDetailPage() {
|
|||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [successMsg, setSuccessMsg] = React.useState<string | null>(null);
|
const [successMsg, setSuccessMsg] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const { data: profile } = useQuery({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: user,
|
data: user,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -332,6 +338,7 @@ function UserDetailPage() {
|
|||||||
id="companyCode"
|
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"
|
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")}
|
{...register("companyCode")}
|
||||||
|
disabled={profile?.role === "tenant_admin"}
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
|
Settings2,
|
||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -21,6 +22,15 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "../../components/ui/card";
|
} from "../../components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../../components/ui/dialog";
|
||||||
import { Input } from "../../components/ui/input";
|
import { Input } from "../../components/ui/input";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -52,6 +62,7 @@ function UserListPage() {
|
|||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
const [searchDraft, setSearchDraft] = React.useState("");
|
const [searchDraft, setSearchDraft] = React.useState("");
|
||||||
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
|
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
|
||||||
|
const [visibleColumns, setVisibleColumns] = React.useState<Record<string, boolean>>({});
|
||||||
const limit = 50;
|
const limit = 50;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
@@ -89,6 +100,28 @@ function UserListPage() {
|
|||||||
? (tenantDetail?.config?.userSchema as UserSchemaField[])
|
? (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({
|
const query = useQuery({
|
||||||
queryKey: ["users", { limit, offset, search, companyCode: selectedCompany }],
|
queryKey: ["users", { limit, offset, search, companyCode: selectedCompany }],
|
||||||
queryFn: () => fetchUsers(limit, offset, search, selectedCompany),
|
queryFn: () => fetchUsers(limit, offset, search, selectedCompany),
|
||||||
@@ -173,6 +206,47 @@ function UserListPage() {
|
|||||||
{t("ui.common.refresh", "새로고침")}
|
{t("ui.common.refresh", "새로고침")}
|
||||||
</Button>
|
</Button>
|
||||||
<UserBulkUploadModal onSuccess={() => query.refetch()} />
|
<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>
|
<Button asChild>
|
||||||
<Link to="/users/new">
|
<Link to="/users/new">
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
@@ -267,9 +341,11 @@ function UserListPage() {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
{/* Dynamic Columns from Schema */}
|
{/* Dynamic Columns from Schema */}
|
||||||
{userSchema.map((field) => (
|
{userSchema.map((field) => (
|
||||||
<TableHead key={field.key} className="uppercase">
|
visibleColumns[field.key] !== false && (
|
||||||
{field.label}
|
<TableHead key={field.key} className="uppercase">
|
||||||
</TableHead>
|
{field.label}
|
||||||
|
</TableHead>
|
||||||
|
)
|
||||||
))}
|
))}
|
||||||
<TableHead>
|
<TableHead>
|
||||||
{t("ui.admin.users.list.table.created", "CREATED")}
|
{t("ui.admin.users.list.table.created", "CREATED")}
|
||||||
@@ -341,9 +417,11 @@ function UserListPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
{/* Dynamic Metadata Cells */}
|
{/* Dynamic Metadata Cells */}
|
||||||
{userSchema.map((field) => (
|
{userSchema.map((field) => (
|
||||||
<TableCell key={field.key} className="text-sm">
|
visibleColumns[field.key] !== false && (
|
||||||
{String(user.metadata?.[field.key] ?? "-")}
|
<TableCell key={field.key} className="text-sm">
|
||||||
</TableCell>
|
{String(user.metadata?.[field.key] ?? "-")}
|
||||||
|
</TableCell>
|
||||||
|
)
|
||||||
))}
|
))}
|
||||||
<TableCell className="text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
{new Date(user.createdAt).toLocaleDateString()}
|
{new Date(user.createdAt).toLocaleDateString()}
|
||||||
|
|||||||
Reference in New Issue
Block a user