1
0
forked from baron/baron-sso

feat(adminfront): add Data Management menu to User tab

This commit introduces a 'Data Management' dropdown menu to the User list page, consolidating user CSV import, template download, and export actions. It aligns the UI with the existing Tenant list page.
This commit is contained in:
2026-05-20 13:25:21 +09:00
parent 0f61425bbf
commit 53dacda5d5
2 changed files with 74 additions and 27 deletions

View File

@@ -7,7 +7,9 @@ import {
ChevronDown, ChevronDown,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Download,
FileDown, FileDown,
FileSpreadsheet,
LayoutDashboard, LayoutDashboard,
Plus, Plus,
RefreshCw, RefreshCw,
@@ -15,6 +17,7 @@ import {
Settings2, Settings2,
ShieldCheck, ShieldCheck,
Trash2, Trash2,
Upload,
} from "lucide-react"; } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
@@ -90,7 +93,10 @@ import {
} from "../../lib/adminApi"; } from "../../lib/adminApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import { isSuperAdminRole } from "../../lib/roles"; import { isSuperAdminRole } from "../../lib/roles";
import { UserBulkUploadModal } from "./components/UserBulkUploadModal"; import {
UserBulkUploadModal,
downloadUserTemplate,
} from "./components/UserBulkUploadModal";
import { import {
normalizeUserStatusValue, normalizeUserStatusValue,
type UserStatusValue, type UserStatusValue,
@@ -485,27 +491,53 @@ function UserListPage() {
<RefreshCw size={16} /> <RefreshCw size={16} />
{t("ui.common.refresh", "새로고침")} {t("ui.common.refresh", "새로고침")}
</Button> </Button>
<Button <DropdownMenu>
variant="outline" <DropdownMenuTrigger asChild>
onClick={() => handleExport(false)} <Button
className="gap-2" variant="outline"
disabled={exportMutation.isPending} data-testid="user-data-mgmt-btn"
data-testid="user-export-without-ids-btn" className="gap-2 h-9"
> >
<FileDown size={16} /> <LayoutDashboard size={16} />
{t("ui.common.export_without_ids", "UUID 제외 내보내기")} {t("ui.admin.users.data_mgmt", "데이터 관리")}
</Button> <ChevronDown size={14} className="opacity-50" />
<Button </Button>
variant="outline" </DropdownMenuTrigger>
onClick={() => handleExport(true)} <DropdownMenuContent align="end" className="w-56">
className="gap-2" <DropdownMenuItem
disabled={exportMutation.isPending} onClick={downloadUserTemplate}
data-testid="user-export-with-ids-btn" data-testid="user-template-menu-item"
> className="cursor-pointer"
<FileDown size={16} /> >
{t("ui.common.export_with_ids", "UUID 포함")} <FileSpreadsheet size={16} className="mr-2 opacity-50" />
</Button> {t("ui.admin.users.csv_template", "템플릿 다운로드")}
<UserBulkUploadModal onSuccess={() => query.refetch()} /> </DropdownMenuItem>
<DropdownMenuSeparator />
<UserBulkUploadModal
variant="dropdown"
onSuccess={() => query.refetch()}
/>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handleExport(false)}
disabled={exportMutation.isPending}
data-testid="user-export-menu-item"
className="cursor-pointer"
>
<FileDown size={16} className="mr-2 opacity-50" />
{t("ui.common.export_without_ids", "UUID 제외 내보내기")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleExport(true)}
disabled={exportMutation.isPending}
data-testid="user-export-with-ids-menu-item"
className="cursor-pointer"
>
<FileDown size={16} className="mr-2 opacity-50" />
{t("ui.common.export_with_ids", "UUID 포함 내보내기")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" size="icon" className="h-9 w-9"> <Button variant="outline" size="icon" className="h-9 w-9">

View File

@@ -18,6 +18,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "../../../components/ui/dialog"; } from "../../../components/ui/dialog";
import { DropdownMenuItem } from "../../../components/ui/dropdown-menu";
import { ScrollArea } from "../../../components/ui/scroll-area"; import { ScrollArea } from "../../../components/ui/scroll-area";
import { import {
type BulkUserItem, type BulkUserItem,
@@ -121,6 +122,22 @@ function hanmacEmailStatusClass(preview?: HanmacImportEmailPreview) {
return "text-muted-foreground"; return "text-muted-foreground";
} }
export const downloadUserTemplate = () => {
const headers =
"email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1";
const example =
"user1@example.com,홍길동,010-1234-5678,user,tenant-slug,개발팀,수석,팀장,프론트엔드,EMP001,second-tenant,센터,책임,,Architecture,EMP002";
const blob = new Blob([`${headers}\n${example}`], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "user_bulk_template.csv";
a.click();
URL.revokeObjectURL(url);
};
export function UserBulkUploadModal({ export function UserBulkUploadModal({
onSuccess, onSuccess,
variant = "button", variant = "button",
@@ -334,16 +351,14 @@ export function UserBulkUploadModal({
const triggerNode = const triggerNode =
variant === "dropdown" ? ( variant === "dropdown" ? (
<div <DropdownMenuItem
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
role="menuitem"
tabIndex={-1}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="cursor-pointer"
{...triggerProps} {...triggerProps}
> >
<Upload size={16} className="mr-2 opacity-50" /> <Upload size={16} className="mr-2 opacity-50" />
{t("ui.admin.users.list.bulk_import", "일괄 등록 (CSV)")} {t("ui.admin.users.list.bulk_import", "일괄 등록 (CSV)")}
</div> </DropdownMenuItem>
) : ( ) : (
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="gap-2" {...triggerProps}> <Button variant="outline" className="gap-2" {...triggerProps}>