1
0
forked from baron/baron-sso

feat: implement bulk user actions and organization tree search with auto-expansion

This commit is contained in:
2026-03-04 15:43:00 +09:00
parent d3b4e3ef5e
commit a5102d9b25
5 changed files with 331 additions and 3 deletions

View File

@@ -581,19 +581,42 @@ const TenantTreeRow: React.FC<{
isRoot: boolean;
onRemove: (id: string, name: string) => void;
isUpdating: boolean;
}> = ({ node, level, isRoot, onRemove, isUpdating }) => {
searchTerm?: string;
}> = ({ node, level, isRoot, onRemove, isUpdating, searchTerm }) => {
const navigate = useNavigate();
const [isExpanded, setIsExpanded] = useState(true);
const [isUserAddOpen, setIsUserAddOpen] = useState(false);
const [isMemberListOpen, setIsMemberListOpen] = useState(false);
const hasChildren = node.children && node.children.length > 0;
// Auto expand if search matches children
React.useEffect(() => {
if (searchTerm) {
const hasMatchingChild = (n: TenantNode): boolean => {
return n.children.some(
(c) =>
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.slug.toLowerCase().includes(searchTerm.toLowerCase()) ||
hasMatchingChild(c),
);
};
if (hasMatchingChild(node)) {
setIsExpanded(true);
}
}
}, [searchTerm, node]);
const isMatching =
searchTerm &&
(node.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
node.slug.toLowerCase().includes(searchTerm.toLowerCase()));
const TypeIcon = getTenantIcon(node.type);
return (
<>
<TableRow
className={`group hover:bg-muted/30 transition-colors ${isRoot ? "bg-primary/5 font-bold" : ""}`}
className={`group hover:bg-muted/30 transition-colors ${isRoot ? "bg-primary/5 font-bold" : ""} ${isMatching ? "bg-primary/10 ring-1 ring-primary/20" : ""}`}
>
<TableCell style={{ paddingLeft: `${1 + level * 2}rem` }}>
<div className="flex items-center gap-2">
@@ -750,6 +773,7 @@ const TenantTreeRow: React.FC<{
isRoot={false}
onRemove={onRemove}
isUpdating={isUpdating}
searchTerm={searchTerm}
/>
))}
</>
@@ -762,6 +786,7 @@ function TenantUserGroupsTab() {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isHeaderUserAddOpen, setIsHeaderUserAddOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [treeSearchTerm, setTreeSearchTerm] = useState("");
if (!tenantId) return null;
@@ -1008,6 +1033,30 @@ function TenantUserGroupsTab() {
</Dialog>
</div>
</CardHeader>
<div className="px-6 py-3 bg-muted/5 border-b flex items-center gap-4">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t(
"ui.admin.tenants.sub.tree_search_placeholder",
"조직도 내 검색...",
)}
className="pl-9 h-9"
value={treeSearchTerm}
onChange={(e) => setTreeSearchTerm(e.target.value)}
/>
</div>
{treeSearchTerm && (
<Button
variant="ghost"
size="sm"
onClick={() => setTreeSearchTerm("")}
className="text-xs"
>
{t("ui.common.clear_search", "검색 초기화")}
</Button>
)}
</div>
<CardContent className="p-0">
<Table>
<TableHeader className="bg-muted/5">
@@ -1036,6 +1085,7 @@ function TenantUserGroupsTab() {
isRoot={true}
onRemove={handleRemove}
isUpdating={updateParentMutation.isPending}
searchTerm={treeSearchTerm}
/>
</TableBody>
</Table>