forked from baron/baron-sso
feat: implement bulk user actions and organization tree search with auto-expansion
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user