forked from baron/baron-sso
조직도 표현 개선
This commit is contained in:
@@ -81,6 +81,7 @@ import {
|
||||
} from "../../components/ui/table";
|
||||
import { toast } from "../../components/ui/use-toast";
|
||||
import {
|
||||
type TenantSummary,
|
||||
type UserSummary,
|
||||
bulkDeleteUsers,
|
||||
bulkUpdateUsers,
|
||||
@@ -130,11 +131,115 @@ function assignableSystemRoleValue(role?: string | null) {
|
||||
return isSuperAdminRole(role) ? "super_admin" : "user";
|
||||
}
|
||||
|
||||
function userMatchesSearch(user: UserSummary, search: string) {
|
||||
const normalizedSearch = search.trim().toLowerCase();
|
||||
if (!normalizedSearch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [
|
||||
user.name,
|
||||
user.email,
|
||||
user.phone,
|
||||
user.id,
|
||||
user.tenantSlug,
|
||||
user.tenant?.name,
|
||||
user.department,
|
||||
].some((value) => value?.toLowerCase().includes(normalizedSearch));
|
||||
}
|
||||
|
||||
type UserListSearchControlsProps = {
|
||||
search: string;
|
||||
selectedCompany: string;
|
||||
tenants: TenantSummary[];
|
||||
profileRole?: string | null;
|
||||
onSearch: (value: string) => void;
|
||||
onCompanyChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const UserListSearchControls = React.memo(function UserListSearchControls({
|
||||
search,
|
||||
selectedCompany,
|
||||
tenants,
|
||||
profileRole,
|
||||
onSearch,
|
||||
onCompanyChange,
|
||||
}: UserListSearchControlsProps) {
|
||||
const [searchDraft, setSearchDraft] = React.useState(search);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSearchDraft(search);
|
||||
}, [search]);
|
||||
|
||||
const handleSearch = React.useCallback(() => {
|
||||
onSearch(searchDraft);
|
||||
}, [onSearch, searchDraft]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
[handleSearch],
|
||||
);
|
||||
|
||||
const tenantOptions = React.useMemo(
|
||||
() =>
|
||||
tenants.map((tenant) => (
|
||||
<option key={tenant.id} value={tenant.slug}>
|
||||
{tenant.name}
|
||||
</option>
|
||||
)),
|
||||
[tenants],
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchFilterBar
|
||||
primary={
|
||||
<>
|
||||
<div className="relative w-48">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t(
|
||||
"ui.admin.users.list.search_placeholder",
|
||||
"이름 또는 이메일 검색...",
|
||||
)}
|
||||
className="h-9 pl-9"
|
||||
value={searchDraft}
|
||||
onChange={(event) => setSearchDraft(event.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||
value={selectedCompany}
|
||||
onChange={(event) => onCompanyChange(event.target.value)}
|
||||
disabled={profileRole === "tenant_admin"}
|
||||
>
|
||||
<option value="">{t("ui.common.all", "전체 테넌트")}</option>
|
||||
{tenantOptions}
|
||||
</select>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleSearch}
|
||||
className="h-9"
|
||||
>
|
||||
{t("ui.common.search", "검색")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
function UserListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [searchDraft, setSearchDraft] = React.useState("");
|
||||
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
|
||||
const [visibleColumns, setVisibleColumns] = React.useState<
|
||||
Record<string, boolean>
|
||||
@@ -254,16 +359,15 @@ function UserListPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
setSearch(searchDraft);
|
||||
const handleSearch = React.useCallback((nextSearch: string) => {
|
||||
setSearch(nextSearch);
|
||||
setPage(1);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
const handleCompanyChange = React.useCallback((nextCompany: string) => {
|
||||
setSelectedCompany(nextCompany);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const handleExport = (includeIds = false) => {
|
||||
exportMutation.mutate(includeIds);
|
||||
@@ -279,7 +383,14 @@ function UserListPage() {
|
||||
)
|
||||
: null;
|
||||
|
||||
const rawItems = query.data?.items ?? [];
|
||||
const serverItems = query.data?.items ?? [];
|
||||
const rawItems = React.useMemo(() => {
|
||||
if (!query.isFetching || search.trim() === "") {
|
||||
return serverItems;
|
||||
}
|
||||
|
||||
return serverItems.filter((user) => userMatchesSearch(user, search));
|
||||
}, [query.isFetching, search, serverItems]);
|
||||
const userSortResolvers = React.useMemo<
|
||||
SortResolverMap<UserSummary, UserSortKey>
|
||||
>(
|
||||
@@ -436,52 +547,13 @@ function UserListPage() {
|
||||
)}
|
||||
actions={
|
||||
<>
|
||||
<SearchFilterBar
|
||||
primary={
|
||||
<>
|
||||
<div className="relative w-48">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t(
|
||||
"ui.admin.users.list.search_placeholder",
|
||||
"이름 또는 이메일 검색...",
|
||||
)}
|
||||
className="h-9 pl-9"
|
||||
value={searchDraft}
|
||||
onChange={(e) => setSearchDraft(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||
value={selectedCompany}
|
||||
onChange={(e) => {
|
||||
setSelectedCompany(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
disabled={profile?.role === "tenant_admin"}
|
||||
>
|
||||
<option value="">
|
||||
{t("ui.common.all", "전체 테넌트")}
|
||||
</option>
|
||||
{tenants.map((t) => (
|
||||
<option key={t.id} value={t.slug}>
|
||||
{t.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleSearch}
|
||||
className="h-9"
|
||||
>
|
||||
{t("ui.common.search", "검색")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
<UserListSearchControls
|
||||
search={search}
|
||||
selectedCompany={selectedCompany}
|
||||
tenants={tenants}
|
||||
profileRole={profile?.role}
|
||||
onSearch={handleSearch}
|
||||
onCompanyChange={handleCompanyChange}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user