forked from baron/baron-sso
feat: implement bulk user actions and organization tree search with auto-expansion
This commit is contained in:
@@ -41,6 +41,8 @@ import {
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import {
|
||||
bulkDeleteUsers,
|
||||
bulkUpdateUsers,
|
||||
deleteUser,
|
||||
fetchMe,
|
||||
fetchTenant,
|
||||
@@ -63,6 +65,7 @@ function UserListPage() {
|
||||
const [searchDraft, setSearchDraft] = React.useState("");
|
||||
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
|
||||
const [visibleColumns, setVisibleColumns] = React.useState<Record<string, boolean>>({});
|
||||
const [selectedUserIds, setSelectedUserIds] = React.useState<string[]>([]);
|
||||
const limit = 50;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
@@ -160,6 +163,50 @@ function UserListPage() {
|
||||
const total = query.data?.total ?? 0;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedUserIds.length === items.length) {
|
||||
setSelectedUserIds([]);
|
||||
} else {
|
||||
setSelectedUserIds(items.map((u) => u.id));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelectUser = (id: string) => {
|
||||
setSelectedUserIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
const bulkDeleteMutation = useMutation({
|
||||
mutationFn: bulkDeleteUsers,
|
||||
onSuccess: () => {
|
||||
query.refetch();
|
||||
setSelectedUserIds([]);
|
||||
toast.success(t("msg.admin.users.bulk.delete_success", "선택한 사용자들이 삭제되었습니다."));
|
||||
},
|
||||
});
|
||||
|
||||
const bulkUpdateMutation = useMutation({
|
||||
mutationFn: bulkUpdateUsers,
|
||||
onSuccess: () => {
|
||||
query.refetch();
|
||||
setSelectedUserIds([]);
|
||||
toast.success(t("msg.admin.users.bulk.update_success", "선택한 사용자들의 정보가 수정되었습니다."));
|
||||
},
|
||||
});
|
||||
|
||||
const handleBulkStatusChange = (status: string) => {
|
||||
if (selectedUserIds.length === 0) return;
|
||||
bulkUpdateMutation.mutate({ userIds: selectedUserIds, status });
|
||||
};
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
if (selectedUserIds.length === 0) return;
|
||||
if (window.confirm(t("msg.admin.users.bulk.delete_confirm", "{{count}}명의 사용자를 정말 삭제하시겠습니까?", { count: selectedUserIds.length }))) {
|
||||
bulkDeleteMutation.mutate(selectedUserIds);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (userId: string, userName: string) => {
|
||||
if (
|
||||
!window.confirm(
|
||||
@@ -324,6 +371,14 @@ function UserListPage() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||
checked={items.length > 0 && selectedUserIds.length === items.length}
|
||||
onChange={toggleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[200px]">
|
||||
{t("ui.admin.users.list.table.name_email", "NAME / EMAIL")}
|
||||
</TableHead>
|
||||
@@ -377,7 +432,18 @@ function UserListPage() {
|
||||
</TableRow>
|
||||
)}
|
||||
{items.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableRow
|
||||
key={user.id}
|
||||
className={selectedUserIds.includes(user.id) ? "bg-primary/5" : ""}
|
||||
>
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||
checked={selectedUserIds.includes(user.id)}
|
||||
onChange={() => toggleSelectUser(user.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-secondary text-secondary-foreground">
|
||||
@@ -452,6 +518,51 @@ function UserListPage() {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Bulk Action Bar */}
|
||||
{selectedUserIds.length > 0 && (
|
||||
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 flex items-center gap-4 px-6 py-3 rounded-2xl bg-foreground text-background shadow-2xl animate-in slide-in-from-bottom-4 duration-300">
|
||||
<span className="text-sm font-medium border-r border-background/20 pr-4 mr-2">
|
||||
{t("ui.admin.users.bulk.selected_count", "{{count}}명 선택됨", { count: selectedUserIds.length })}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-background hover:bg-background/10 h-8"
|
||||
onClick={() => handleBulkStatusChange("active")}
|
||||
>
|
||||
{t("ui.common.status.active", "활성화")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-background hover:bg-background/10 h-8"
|
||||
onClick={() => handleBulkStatusChange("inactive")}
|
||||
>
|
||||
{t("ui.common.status.inactive", "비활성화")}
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-background/20 mx-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive-foreground hover:bg-destructive/20 h-8 gap-1.5"
|
||||
onClick={handleBulkDelete}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
{t("ui.common.delete", "삭제")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-background/50 hover:text-background h-8 w-8 ml-2"
|
||||
onClick={() => setSelectedUserIds([])}
|
||||
>
|
||||
<Plus size={16} className="rotate-45" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
|
||||
Reference in New Issue
Block a user