Drop page / section descriptions, state notes and repeated captions from the representative Stories of 44 components, the Patterns, Center Graphic and OrgChart pages. Value readouts, spec lists and captions hide only on the representative page; hidden verification Stories keep them. Tabs Default API and the Filter Bar draft / applied counts move to the hidden DefaultApi and DraftApplied Stories; Center Overview is hidden with its direct URL kept. Story IDs are unchanged. Align the component, pattern and OrgChart docs with the pages, point the quality reference check at the DraftApplied Story, and make the P8 styles check expect no section header on the preview-only Expanded page.
172 lines
7.6 KiB
TypeScript
172 lines
7.6 KiB
TypeScript
import { useMemo, useState } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import {
|
|
Card, CardContent, CardHeader, CardTitle,
|
|
Badge,
|
|
Button,
|
|
Combobox,
|
|
DataTable,
|
|
DataTableColumnHeader,
|
|
DataTableViewOptions,
|
|
DatePicker,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
FilterBar,
|
|
FilterBarActions,
|
|
FilterBarSearch,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
dataTableSelectColumn,
|
|
toast,
|
|
useDataTable,
|
|
type DataTableColumnDef,
|
|
} from "@hm-design/ui"
|
|
import "../list-screen.css"
|
|
import { MoreHorizontalIcon } from "lucide-react"
|
|
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
import { formatDate, sameDay, sampleUsers, statusLabel, type SampleUser } from "./sample-users"
|
|
|
|
// Data Table is a pattern: the HM Table primitive driven by TanStack Table v9 (useDataTable). Columns and data stay referentially stable
|
|
// (module scope / useMemo). Row actions use the HM Dropdown Menu and report through Sonner `toast` (Story composition only).
|
|
|
|
const statusBadge = (status: SampleUser["status"]) => (
|
|
<Badge variant={status === "active" ? "default" : status === "pending" ? "secondary" : "outline"}>{statusLabel[status]}</Badge>
|
|
)
|
|
|
|
const columnLabels = { name: "이름", department: "부서", status: "상태", createdAt: "등록일" }
|
|
|
|
const columns: DataTableColumnDef<SampleUser>[] = [
|
|
dataTableSelectColumn<SampleUser>(),
|
|
{ accessorKey: "name", header: ({ column }) => <DataTableColumnHeader column={column} title="이름" /> },
|
|
{ accessorKey: "department", header: "부서" },
|
|
{
|
|
accessorKey: "status",
|
|
header: "상태",
|
|
cell: ({ row }) => statusBadge(row.original.status),
|
|
filterFn: "equalsString",
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: "createdAt",
|
|
header: ({ column }) => <DataTableColumnHeader column={column} title="등록일" />,
|
|
cell: ({ row }) => formatDate(row.original.createdAt),
|
|
filterFn: (row, columnId, value: Date) => sameDay(row.getValue<string>(columnId), value),
|
|
},
|
|
{
|
|
id: "actions",
|
|
enableHiding: false,
|
|
cell: ({ row }) => (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button type="button" variant="primary" appearance="outline" size="icon-sm" aria-label={`${row.original.name} 작업`}>
|
|
<MoreHorizontalIcon />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onSelect={() => toast(`${row.original.name} 보기`)}>보기</DropdownMenuItem>
|
|
<DropdownMenuItem onSelect={() => toast(`${row.original.name} 수정`)}>수정</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem variant="destructive" onSelect={() => toast.error(`${row.original.name} 삭제 요청`)}>
|
|
삭제
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
),
|
|
},
|
|
]
|
|
|
|
function Basic({ data = sampleUsers, loading }: { data?: SampleUser[]; loading?: boolean }) {
|
|
const table = useDataTable({ columns, data, getRowId: (row) => row.id })
|
|
return (
|
|
<div className="grid w-full gap-3">
|
|
<div className="flex justify-end">
|
|
<DataTableViewOptions table={table} labels={columnLabels} />
|
|
</div>
|
|
<DataTable table={table} loading={loading} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const statusOptions = [
|
|
{ value: "active", label: "활성" },
|
|
{ value: "pending", label: "대기" },
|
|
{ value: "inactive", label: "비활성" },
|
|
]
|
|
const departmentOptions = [...new Set(sampleUsers.map((u) => u.department))].map((d) => ({ value: d, label: d }))
|
|
|
|
// Filter Bar → filter state → Data Table: search is the TanStack global filter, status / date are column filters.
|
|
function WithFilterBar({ extended = true }: { extended?: boolean }) {
|
|
const data = useMemo(() => sampleUsers, [])
|
|
const table = useDataTable({ columns, data, getRowId: (row) => row.id })
|
|
const [search, setSearch] = useState("")
|
|
const [status, setStatus] = useState("all")
|
|
const [date, setDate] = useState<Date | undefined>()
|
|
const [department, setDepartment] = useState<string | undefined>()
|
|
const apply = () => {
|
|
table.setGlobalFilter(search || undefined)
|
|
table.getColumn("status")?.setFilterValue(status === "all" ? undefined : status)
|
|
table.getColumn("department")?.setFilterValue(department)
|
|
table.getColumn("createdAt")?.setFilterValue(date)
|
|
}
|
|
const reset = () => {
|
|
setSearch(""); setStatus("all"); setDate(undefined); setDepartment(undefined)
|
|
table.resetGlobalFilter(true); table.resetColumnFilters(true)
|
|
}
|
|
// The list title belongs to the list: Card title → search → table inside the one white Card.
|
|
return <Card className="min-w-0">
|
|
<CardHeader><CardTitle>사용자 목록</CardTitle></CardHeader>
|
|
<CardContent>
|
|
<div className="grid min-w-0 gap-4">
|
|
<div className="hm-data-table-tools">
|
|
<form className="hm-data-table-search" onSubmit={event => { event.preventDefault(); apply() }}>
|
|
<FilterBar className={extended ? "hm-list-search-extended" : "hm-list-search-basic"}>
|
|
<Select value={status} onValueChange={setStatus}>
|
|
<SelectTrigger size="medium" aria-label="상태"><SelectValue placeholder="상태" /></SelectTrigger>
|
|
<SelectContent><SelectItem value="all">전체 상태</SelectItem>{statusOptions.map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectContent>
|
|
</Select>
|
|
{extended && <><Combobox controlled size="medium" aria-label="부서" options={departmentOptions} value={department} placeholder="부서" onValueChange={setDepartment} />
|
|
<DatePicker controlled size="medium" aria-label="등록일" placeholder="등록일" value={date} onValueChange={setDate} /></>}
|
|
<FilterBarSearch value={search} onValueChange={setSearch} placeholder="이름·부서 검색" />
|
|
<FilterBarActions><Button type="submit" variant="primary" appearance="solid">조회</Button><Button type="button" variant="cancel" appearance="outline" onClick={reset}>초기화</Button></FilterBarActions>
|
|
</FilterBar>
|
|
</form>
|
|
<div className="hm-data-table-view-options"><DataTableViewOptions table={table} labels={columnLabels} /></div>
|
|
</div>
|
|
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
}
|
|
|
|
const meta = { title: "Patterns/Data/Data Table", parameters: { layout: "padded", controls: { disable: true } } } satisfies Meta
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// Representative Story: selection (mixed header), sorting, column visibility, row actions and pagination with page size.
|
|
export const All: Story = {
|
|
name: "Data Table",
|
|
parameters: { layout: "fullscreen" },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="Data Table" />
|
|
<StorySection title="Overview">
|
|
<Basic />
|
|
</StorySection>
|
|
<StorySection title="기본 목록 검색"><WithFilterBar extended={false} /></StorySection>
|
|
<StorySection title="추가 조건 목록 검색"><WithFilterBar /></StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
// Combination Story: Filter Bar (search, status, department, date, reset) filtering the Data Table rows.
|
|
export const WithFilterBarStory: Story = { tags: ["verification"], name: "With Filter Bar", render: () => <WithFilterBar /> }
|
|
|
|
export const Loading: Story = { tags: ["verification"], render: () => <Basic loading /> }
|
|
export const Empty: Story = { tags: ["verification"], render: () => <Basic data={[]} /> }
|