forked from baron/baron-sso
dev/admin 테이블 정렬 헤더 UI 공통화
This commit is contained in:
@@ -19,7 +19,11 @@ import {
|
|||||||
} 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";
|
||||||
import { SortableTableHead } from "../../../../../common/core/components/sort";
|
import {
|
||||||
|
SortableTableHead,
|
||||||
|
sortableTableHeadBaseClassName,
|
||||||
|
sortableTableHeaderClassName,
|
||||||
|
} from "../../../../../common/core/components/sort";
|
||||||
import {
|
import {
|
||||||
type SortConfig,
|
type SortConfig,
|
||||||
type SortResolverMap,
|
type SortResolverMap,
|
||||||
@@ -954,9 +958,11 @@ function TenantListPage() {
|
|||||||
onScroll={handleTenantTableScroll}
|
onScroll={handleTenantTableScroll}
|
||||||
>
|
>
|
||||||
<Table className="min-w-[1180px]">
|
<Table className="min-w-[1180px]">
|
||||||
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
|
<TableHeader className={sortableTableHeaderClassName}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[48px] whitespace-nowrap">
|
<TableHead
|
||||||
|
className={`${sortableTableHeadBaseClassName} w-[48px] whitespace-nowrap`}
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={
|
checked={
|
||||||
tenants.length > 0 &&
|
tenants.length > 0 &&
|
||||||
@@ -969,55 +975,57 @@ function TenantListPage() {
|
|||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
className="min-w-[220px] whitespace-nowrap sticky top-0 bg-inherit"
|
className="min-w-[220px] whitespace-nowrap"
|
||||||
label={t("ui.admin.tenants.table.id", "ID")}
|
label={t("ui.admin.tenants.table.id", "ID")}
|
||||||
onSort={requestSort}
|
onSort={requestSort}
|
||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="id"
|
sortKey="id"
|
||||||
/>
|
/>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
className="whitespace-nowrap sticky top-0 bg-inherit"
|
className="whitespace-nowrap"
|
||||||
label={t("ui.admin.tenants.table.name", "NAME")}
|
label={t("ui.admin.tenants.table.name", "NAME")}
|
||||||
onSort={requestSort}
|
onSort={requestSort}
|
||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="name"
|
sortKey="name"
|
||||||
/>
|
/>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
className="whitespace-nowrap sticky top-0 bg-inherit"
|
className="whitespace-nowrap"
|
||||||
label={t("ui.admin.tenants.table.type", "TYPE")}
|
label={t("ui.admin.tenants.table.type", "TYPE")}
|
||||||
onSort={requestSort}
|
onSort={requestSort}
|
||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="type"
|
sortKey="type"
|
||||||
/>
|
/>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
className="whitespace-nowrap sticky top-0 bg-inherit"
|
className="whitespace-nowrap"
|
||||||
label={t("ui.admin.tenants.table.slug", "SLUG")}
|
label={t("ui.admin.tenants.table.slug", "SLUG")}
|
||||||
onSort={requestSort}
|
onSort={requestSort}
|
||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="slug"
|
sortKey="slug"
|
||||||
/>
|
/>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
className="whitespace-nowrap sticky top-0 bg-inherit"
|
className="whitespace-nowrap"
|
||||||
label={t("ui.admin.tenants.table.status", "STATUS")}
|
label={t("ui.admin.tenants.table.status", "STATUS")}
|
||||||
onSort={requestSort}
|
onSort={requestSort}
|
||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="status"
|
sortKey="status"
|
||||||
/>
|
/>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
className="whitespace-nowrap sticky top-0 bg-inherit"
|
className="whitespace-nowrap"
|
||||||
label={t("ui.admin.tenants.table.members", "MEMBERS")}
|
label={t("ui.admin.tenants.table.members", "MEMBERS")}
|
||||||
onSort={requestSort}
|
onSort={requestSort}
|
||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="recursiveMemberCount"
|
sortKey="recursiveMemberCount"
|
||||||
/>
|
/>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
className="whitespace-nowrap sticky top-0 bg-inherit"
|
className="whitespace-nowrap"
|
||||||
label={t("ui.admin.tenants.table.created", "CREATED")}
|
label={t("ui.admin.tenants.table.created", "CREATED")}
|
||||||
onSort={requestSort}
|
onSort={requestSort}
|
||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="createdAt"
|
sortKey="createdAt"
|
||||||
/>
|
/>
|
||||||
<TableHead className="whitespace-nowrap">
|
<TableHead
|
||||||
|
className={`${sortableTableHeadBaseClassName} whitespace-nowrap`}
|
||||||
|
>
|
||||||
{t("ui.common.actions", "액션")}
|
{t("ui.common.actions", "액션")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import {
|
import {
|
||||||
ArrowDown,
|
|
||||||
ArrowUp,
|
|
||||||
ArrowUpDown,
|
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
FileDown,
|
FileDown,
|
||||||
@@ -22,6 +19,11 @@ import {
|
|||||||
sortItems,
|
sortItems,
|
||||||
toggleSort,
|
toggleSort,
|
||||||
} from "../../../../common/core/utils";
|
} from "../../../../common/core/utils";
|
||||||
|
import {
|
||||||
|
SortableTableHead,
|
||||||
|
sortableTableHeadBaseClassName,
|
||||||
|
sortableTableHeaderClassName,
|
||||||
|
} from "../../../../common/core/components/sort";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -285,17 +287,6 @@ function UserListPage() {
|
|||||||
setSortConfig((current) => toggleSort(current, key));
|
setSortConfig((current) => toggleSort(current, key));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSortIcon = (key: UserSortKey) => {
|
|
||||||
if (!sortConfig || sortConfig.key !== key) {
|
|
||||||
return <ArrowUpDown size={14} className="ml-1 opacity-50" />;
|
|
||||||
}
|
|
||||||
return sortConfig.direction === "asc" ? (
|
|
||||||
<ArrowUp size={14} className="ml-1" />
|
|
||||||
) : (
|
|
||||||
<ArrowDown size={14} className="ml-1" />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const total = query.data?.total ?? 0;
|
const total = query.data?.total ?? 0;
|
||||||
const totalPages = Math.ceil(total / limit);
|
const totalPages = Math.ceil(total / limit);
|
||||||
const canPromoteSuperAdmin = isSuperAdminRole(profile?.role);
|
const canPromoteSuperAdmin = isSuperAdminRole(profile?.role);
|
||||||
@@ -567,9 +558,11 @@ function UserListPage() {
|
|||||||
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
|
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
|
||||||
<div className="flex-1 overflow-auto relative custom-scrollbar">
|
<div className="flex-1 overflow-auto relative custom-scrollbar">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
|
<TableHeader className={sortableTableHeaderClassName}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-12">
|
<TableHead
|
||||||
|
className={`${sortableTableHeadBaseClassName} w-12`}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||||
@@ -580,82 +573,61 @@ function UserListPage() {
|
|||||||
onChange={toggleSelectAll}
|
onChange={toggleSelectAll}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead
|
<SortableTableHead
|
||||||
className="min-w-[220px] whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
|
className="min-w-[220px] whitespace-nowrap"
|
||||||
onClick={() => requestSort("id")}
|
label={t("ui.admin.users.list.table.id", "ID")}
|
||||||
>
|
onSort={requestSort}
|
||||||
<div className="flex items-center">
|
sortConfig={sortConfig}
|
||||||
{t("ui.admin.users.list.table.id", "ID")}
|
sortKey="id"
|
||||||
{getSortIcon("id")}
|
/>
|
||||||
</div>
|
<SortableTableHead
|
||||||
</TableHead>
|
className="min-w-[200px] whitespace-nowrap"
|
||||||
<TableHead
|
label={t(
|
||||||
className="min-w-[200px] whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
|
"ui.admin.users.list.table.name_email",
|
||||||
onClick={() => requestSort("name_email")}
|
"이름 / 이메일 / 전화번호",
|
||||||
>
|
)}
|
||||||
<div className="flex items-center">
|
onSort={requestSort}
|
||||||
{t(
|
sortConfig={sortConfig}
|
||||||
"ui.admin.users.list.table.name_email",
|
sortKey="name_email"
|
||||||
"이름 / 이메일 / 전화번호",
|
/>
|
||||||
)}
|
<SortableTableHead
|
||||||
{getSortIcon("name_email")}
|
className="whitespace-nowrap"
|
||||||
</div>
|
label={t("ui.admin.users.list.table.status", "STATUS")}
|
||||||
</TableHead>
|
onSort={requestSort}
|
||||||
<TableHead
|
sortConfig={sortConfig}
|
||||||
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
|
sortKey="status"
|
||||||
onClick={() => requestSort("status")}
|
/>
|
||||||
>
|
<SortableTableHead
|
||||||
<div className="flex items-center">
|
className="whitespace-nowrap"
|
||||||
{t("ui.admin.users.list.table.status", "STATUS")}
|
label={t(
|
||||||
{getSortIcon("status")}
|
"ui.admin.users.list.table.tenant_dept",
|
||||||
</div>
|
"TENANT / DEPT",
|
||||||
</TableHead>
|
)}
|
||||||
<TableHead
|
onSort={requestSort}
|
||||||
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
|
sortConfig={sortConfig}
|
||||||
onClick={() => requestSort("role")}
|
sortKey="tenant_dept"
|
||||||
>
|
/>
|
||||||
<div className="flex items-center">
|
|
||||||
{t("ui.admin.users.list.table.role", "ROLE")}
|
|
||||||
{getSortIcon("role")}
|
|
||||||
</div>
|
|
||||||
</TableHead>
|
|
||||||
<TableHead
|
|
||||||
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
|
|
||||||
onClick={() => requestSort("tenant_dept")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{t(
|
|
||||||
"ui.admin.users.list.table.tenant_dept",
|
|
||||||
"TENANT / DEPT",
|
|
||||||
)}
|
|
||||||
{getSortIcon("tenant_dept")}
|
|
||||||
</div>
|
|
||||||
</TableHead>
|
|
||||||
{/* Dynamic Columns from Schema */}
|
{/* Dynamic Columns from Schema */}
|
||||||
{userSchema.map(
|
{userSchema.map(
|
||||||
(field) =>
|
(field) =>
|
||||||
visibleColumns[field.key] !== false && (
|
visibleColumns[field.key] !== false && (
|
||||||
<TableHead
|
<SortableTableHead
|
||||||
key={field.key}
|
key={field.key}
|
||||||
className="whitespace-nowrap uppercase cursor-pointer hover:bg-muted/50 transition-colors"
|
className="whitespace-nowrap"
|
||||||
onClick={() => requestSort(field.key)}
|
label={field.label}
|
||||||
>
|
onSort={requestSort}
|
||||||
<div className="flex items-center">
|
sortConfig={sortConfig}
|
||||||
{field.label}
|
sortKey={field.key}
|
||||||
{getSortIcon(field.key)}
|
/>
|
||||||
</div>
|
|
||||||
</TableHead>
|
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
<TableHead
|
<SortableTableHead
|
||||||
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
|
className="whitespace-nowrap"
|
||||||
onClick={() => requestSort("createdAt")}
|
label={t("ui.admin.users.list.table.created", "CREATED")}
|
||||||
>
|
onSort={requestSort}
|
||||||
<div className="flex items-center">
|
sortConfig={sortConfig}
|
||||||
{t("ui.admin.users.list.table.created", "CREATED")}
|
sortKey="createdAt"
|
||||||
{getSortIcon("createdAt")}
|
/>
|
||||||
</div>
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type { ReactNode, ThHTMLAttributes } from "react";
|
import type { ReactNode, ThHTMLAttributes } from "react";
|
||||||
import type { SortConfig } from "../../utils";
|
import type { SortConfig } from "../../utils";
|
||||||
|
|
||||||
|
export const sortableTableHeadBaseClassName =
|
||||||
|
"h-12 px-6 text-left text-xs font-sans font-bold uppercase tracking-[0.08em] text-foreground align-middle";
|
||||||
|
|
||||||
|
export const sortableTableHeaderClassName =
|
||||||
|
"sticky top-0 z-10 bg-secondary shadow-sm";
|
||||||
|
|
||||||
function SortAscendingIcon() {
|
function SortAscendingIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
className="h-4 w-4"
|
className="h-3.5 w-3.5"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
@@ -25,7 +31,7 @@ function SortDescendingIcon() {
|
|||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
className="h-4 w-4"
|
className="h-3.5 w-3.5"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
@@ -44,7 +50,7 @@ function SortIdleIcon() {
|
|||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
className="h-4 w-4 opacity-50"
|
className="ml-1 h-3.5 w-3.5 opacity-50"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
@@ -122,7 +128,7 @@ export function SortableTableHead<Key extends string>({
|
|||||||
<th
|
<th
|
||||||
aria-sort={sortAriaValue(isActive, direction)}
|
aria-sort={sortAriaValue(isActive, direction)}
|
||||||
className={[
|
className={[
|
||||||
"h-12 px-6 text-xs font-bold uppercase tracking-[0.08em] align-middle",
|
sortableTableHeadBaseClassName,
|
||||||
alignClassName(align),
|
alignClassName(align),
|
||||||
disabled ? "" : "transition-colors hover:bg-muted/50",
|
disabled ? "" : "transition-colors hover:bg-muted/50",
|
||||||
className,
|
className,
|
||||||
@@ -136,7 +142,7 @@ export function SortableTableHead<Key extends string>({
|
|||||||
onClick={() => onSort(sortKey)}
|
onClick={() => onSort(sortKey)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={[
|
className={[
|
||||||
"flex w-full items-center gap-1",
|
"flex w-full items-center font-inherit",
|
||||||
buttonAlignClassName(align),
|
buttonAlignClassName(align),
|
||||||
disabled ? "cursor-default opacity-70" : "cursor-pointer",
|
disabled ? "cursor-default opacity-70" : "cursor-pointer",
|
||||||
contentClassName,
|
contentClassName,
|
||||||
@@ -146,9 +152,13 @@ export function SortableTableHead<Key extends string>({
|
|||||||
>
|
>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{direction === "asc" ? (
|
{direction === "asc" ? (
|
||||||
<SortAscendingIcon />
|
<span className="ml-1 inline-flex">
|
||||||
|
<SortAscendingIcon />
|
||||||
|
</span>
|
||||||
) : direction === "desc" ? (
|
) : direction === "desc" ? (
|
||||||
<SortDescendingIcon />
|
<span className="ml-1 inline-flex">
|
||||||
|
<SortDescendingIcon />
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<SortIdleIcon />
|
<SortIdleIcon />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { SortableTableHead } from "../../../../common/core/components/sort";
|
import {
|
||||||
|
SortableTableHead,
|
||||||
|
sortableTableHeadBaseClassName,
|
||||||
|
sortableTableHeaderClassName,
|
||||||
|
} from "../../../../common/core/components/sort";
|
||||||
import {
|
import {
|
||||||
type SortConfig,
|
type SortConfig,
|
||||||
type SortResolverMap,
|
type SortResolverMap,
|
||||||
@@ -440,7 +444,7 @@ function ClientsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader className={sortableTableHeaderClassName}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
label={t("ui.dev.clients.table.application", "애플리케이션")}
|
label={t("ui.dev.clients.table.application", "애플리케이션")}
|
||||||
@@ -472,7 +476,9 @@ function ClientsPage() {
|
|||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="createdAt"
|
sortKey="createdAt"
|
||||||
/>
|
/>
|
||||||
<TableHead className="text-right">
|
<TableHead
|
||||||
|
className={`${sortableTableHeadBaseClassName} text-right`}
|
||||||
|
>
|
||||||
{t("ui.dev.clients.table.actions", "액션")}
|
{t("ui.dev.clients.table.actions", "액션")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user