diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 8739e474..b2622121 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -2,9 +2,6 @@ import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query"; import { useVirtualizer } from "@tanstack/react-virtual"; import type { AxiosError } from "axios"; import { - ArrowDown, - ArrowUp, - ArrowUpDown, Building2, ChevronDown, ChevronRight, @@ -22,6 +19,7 @@ import { } from "lucide-react"; import * as React from "react"; import { Link, useNavigate } from "react-router-dom"; +import { SortableTableHead } from "../../../../../common/core/components/sort"; import { type SortConfig, type SortResolverMap, @@ -513,17 +511,6 @@ function TenantListPage() { setSortConfig((current) => toggleSort(current, key)); }; - const getSortIcon = (key: TenantSortKey) => { - if (!sortConfig || sortConfig.key !== key) { - return ; - } - return sortConfig.direction === "asc" ? ( - - ) : ( - - ); - }; - const deletableTenants = React.useMemo( () => tenants.filter((tenant) => !isSeedTenant(tenant)), [tenants], @@ -977,69 +964,55 @@ function TenantListPage() { } /> - requestSort("id")} - > -
- {t("ui.admin.tenants.table.id", "ID")} - {getSortIcon("id")} -
-
- requestSort("name")} - > -
- {t("ui.admin.tenants.table.name", "NAME")} - {getSortIcon("name")} -
-
- requestSort("type")} - > -
- {t("ui.admin.tenants.table.type", "TYPE")} - {getSortIcon("type")} -
-
- requestSort("slug")} - > -
- {t("ui.admin.tenants.table.slug", "SLUG")} - {getSortIcon("slug")} -
-
- requestSort("status")} - > -
- {t("ui.admin.tenants.table.status", "STATUS")} - {getSortIcon("status")} -
-
- requestSort("recursiveMemberCount")} - > -
- {t("ui.admin.tenants.table.members", "MEMBERS")} - {getSortIcon("recursiveMemberCount")} -
-
- requestSort("updatedAt")} - > -
- {t("ui.admin.tenants.table.updated", "UPDATED")} - {getSortIcon("updatedAt")} -
-
+ + + + + + + {t("ui.common.actions", "액션")} diff --git a/adminfront/tsconfig.app.json b/adminfront/tsconfig.app.json index 91425f60..b9a4130a 100644 --- a/adminfront/tsconfig.app.json +++ b/adminfront/tsconfig.app.json @@ -15,6 +15,13 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "lucide-react": ["./node_modules/lucide-react"], + "react": ["./node_modules/@types/react/index.d.ts"], + "react/jsx-dev-runtime": ["./node_modules/@types/react/jsx-dev-runtime.d.ts"], + "react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime.d.ts"] + }, /* Linting */ "strict": true, @@ -24,6 +31,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"], + "include": ["src", "../common/**/*.ts", "../common/**/*.tsx"], "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/adminfront/vite.config.ts b/adminfront/vite.config.ts index ee24ae9f..3307996f 100644 --- a/adminfront/vite.config.ts +++ b/adminfront/vite.config.ts @@ -1,4 +1,5 @@ import react from "@vitejs/plugin-react"; +import path from "node:path"; import { defineConfig } from "vite"; const buildOutDir = @@ -6,6 +7,20 @@ const buildOutDir = export default defineConfig({ plugins: [react()], + resolve: { + alias: { + "lucide-react": path.resolve(__dirname, "node_modules/lucide-react"), + react: path.resolve(__dirname, "node_modules/react"), + "react/jsx-dev-runtime": path.resolve( + __dirname, + "node_modules/react/jsx-dev-runtime.js", + ), + "react/jsx-runtime": path.resolve( + __dirname, + "node_modules/react/jsx-runtime.js", + ), + }, + }, envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"], cacheDir: process.env.ADMINFRONT_VITE_CACHE_DIR ?? diff --git a/common/core/components/sort/SortableTableHead.tsx b/common/core/components/sort/SortableTableHead.tsx new file mode 100644 index 00000000..b6774a85 --- /dev/null +++ b/common/core/components/sort/SortableTableHead.tsx @@ -0,0 +1,158 @@ +import type { ReactNode, ThHTMLAttributes } from "react"; +import type { SortConfig } from "../../utils"; + +function SortAscendingIcon() { + return ( + + ); +} + +function SortDescendingIcon() { + return ( + + ); +} + +function SortIdleIcon() { + return ( + + ); +} + +type SortableTableHeadAlign = "left" | "center" | "right"; + +function alignClassName(align: SortableTableHeadAlign) { + switch (align) { + case "center": + return "text-center"; + case "right": + return "text-right"; + default: + return "text-left"; + } +} + +function buttonAlignClassName(align: SortableTableHeadAlign) { + switch (align) { + case "center": + return "justify-center"; + case "right": + return "justify-end"; + default: + return "justify-start"; + } +} + +function sortAriaValue( + isActive: boolean, + direction: "asc" | "desc" | null, +): ThHTMLAttributes["aria-sort"] { + if (!isActive || direction === null) { + return "none"; + } + return direction === "asc" ? "ascending" : "descending"; +} + +type SortableTableHeadProps = Omit< + ThHTMLAttributes, + "children" +> & { + align?: SortableTableHeadAlign; + contentClassName?: string; + disabled?: boolean; + label: ReactNode; + onSort: (key: Key) => void; + sortConfig: SortConfig | null; + sortKey: Key; +}; + +export function SortableTableHead({ + align = "left", + className = "", + contentClassName = "", + disabled = false, + label, + onSort, + sortConfig, + sortKey, + ...props +}: SortableTableHeadProps) { + const isActive = sortConfig?.key === sortKey; + const direction = isActive ? sortConfig?.direction ?? null : null; + + return ( + + + + ); +} diff --git a/common/core/components/sort/index.ts b/common/core/components/sort/index.ts new file mode 100644 index 00000000..a9d87fb9 --- /dev/null +++ b/common/core/components/sort/index.ts @@ -0,0 +1 @@ +export * from "./SortableTableHead"; diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 866a7a1c..fcc26b1d 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -1,9 +1,6 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { - ArrowDown, - ArrowUp, - ArrowUpDown, BookOpenText, Filter, Plus, @@ -13,6 +10,7 @@ import { import { useEffect, useMemo, useState } from "react"; import { useAuth } from "react-oidc-context"; import { Link, useNavigate } from "react-router-dom"; +import { SortableTableHead } from "../../../../common/core/components/sort"; import { type SortConfig, type SortResolverMap, @@ -230,18 +228,6 @@ function ClientsPage() { setSortConfig((current) => toggleSort(current, key)); }; - const getSortIcon = (key: ClientSortKey) => { - if (!sortConfig || sortConfig.key !== key) { - return ; - } - - return sortConfig.direction === "asc" ? ( - - ) : ( - - ); - }; - if (auth.isLoading || !hasAccessToken || isLoading) { return (
@@ -452,51 +438,41 @@ function ClientsPage() { - requestSort("application")} - > -
- {t("ui.dev.clients.table.application", "애플리케이션")} - {getSortIcon("application")} -
-
- requestSort("id")} - > -
- {t("ui.dev.clients.table.client_id", "Client ID")} - {getSortIcon("id")} -
-
- requestSort("type")} - > -
- {t("ui.dev.clients.table.type", "유형")} - {getSortIcon("type")} -
-
- requestSort("status")} - > -
- {t("ui.dev.clients.table.status", "상태")} - {getSortIcon("status")} -
-
- requestSort("createdAt")} - > -
- {t("ui.dev.clients.table.created_at", "생성일")} - {getSortIcon("createdAt")} -
-
+ + + + + {t("ui.dev.clients.table.actions", "액션")} diff --git a/devfront/tsconfig.app.json b/devfront/tsconfig.app.json index 68025a7f..5e4fc7d2 100644 --- a/devfront/tsconfig.app.json +++ b/devfront/tsconfig.app.json @@ -15,6 +15,13 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "lucide-react": ["./node_modules/lucide-react"], + "react": ["./node_modules/@types/react/index.d.ts"], + "react/jsx-dev-runtime": ["./node_modules/@types/react/jsx-dev-runtime.d.ts"], + "react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime.d.ts"] + }, /* Linting */ "strict": true, @@ -24,6 +31,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"], + "include": ["src", "../common/**/*.ts", "../common/**/*.tsx"], "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/devfront/vite.config.ts b/devfront/vite.config.ts index 1d03cb5d..01db8ec8 100644 --- a/devfront/vite.config.ts +++ b/devfront/vite.config.ts @@ -1,4 +1,5 @@ import react from "@vitejs/plugin-react"; +import path from "node:path"; import { defineConfig } from "vite"; const buildOutDir = @@ -35,6 +36,20 @@ const allowedHosts = Array.from( export default defineConfig({ plugins: [react()], + resolve: { + alias: { + "lucide-react": path.resolve(__dirname, "node_modules/lucide-react"), + react: path.resolve(__dirname, "node_modules/react"), + "react/jsx-dev-runtime": path.resolve( + __dirname, + "node_modules/react/jsx-dev-runtime.js", + ), + "react/jsx-runtime": path.resolve( + __dirname, + "node_modules/react/jsx-runtime.js", + ), + }, + }, cacheDir: process.env.DEVFRONT_VITE_CACHE_DIR ?? "/tmp/baron-sso-devfront-vite-cache", build: {