From 9fcfef8d75528561dacc0933a7d4af6cb37bb3cd Mon Sep 17 00:00:00 2001 From: Jeremy <168515712+jeremy-babylonlabs@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:29:47 +0800 Subject: [PATCH] table improvement (#67) --- .changeset/cyan-clouds-play.md | 5 + src/components/Table/Table.stories.tsx | 173 +++++++++---------------- src/components/Table/Table.tsx | 81 +++++++----- src/components/Table/types/index.ts | 3 +- 4 files changed, 121 insertions(+), 141 deletions(-) create mode 100644 .changeset/cyan-clouds-play.md diff --git a/.changeset/cyan-clouds-play.md b/.changeset/cyan-clouds-play.md new file mode 100644 index 0000000..6355bf8 --- /dev/null +++ b/.changeset/cyan-clouds-play.md @@ -0,0 +1,5 @@ +--- +"@babylonlabs-io/bbn-core-ui": patch +--- + +table improvement diff --git a/src/components/Table/Table.stories.tsx b/src/components/Table/Table.stories.tsx index 2e093bf..b201225 100644 --- a/src/components/Table/Table.stories.tsx +++ b/src/components/Table/Table.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { useState } from "react"; +import { useState, useCallback } from "react"; -import { Table } from "./"; +import { ColumnProps, Table } from "./"; import { Avatar } from "../Avatar"; const meta: Meta = { @@ -37,7 +37,7 @@ const data: FinalityProvider[] = [ id: "2", name: "Solv Protocol", icon: "/images/fps/solv.jpeg", - status: "Active", + status: "Inactive", btcPk: "1234...4321", totalDelegation: 20, commission: 3, @@ -53,80 +53,48 @@ const data: FinalityProvider[] = [ }, ]; -export const Default: Story = { - render: () => { - const [tableData, setTableData] = useState(data.slice(0, 3)); - const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); - - const handleLoadMore = async () => { - setLoading(true); - await new Promise((resolve) => setTimeout(resolve, 1000)); - - const nextItems = data.slice(tableData.length, tableData.length + 3); - setTableData((prev) => [...prev, ...nextItems]); - setHasMore(tableData.length + nextItems.length < data.length); - setLoading(false); - }; - - const handleRowSelect = (row: FinalityProvider) => { - console.log(row); - }; - - return ( -
- ( -
- - {row.name} -
- ), - sorter: (a, b) => a.name.localeCompare(b.name), - }, - { - key: "status", - header: "Status", - }, - { - key: "btcPk", - header: "BTC PK", - }, - { - key: "totalDelegation", - header: "Total Delegation", - render: (value) => `${value} sBTC`, - sorter: (a, b) => a.totalDelegation - b.totalDelegation, - }, - { - key: "commission", - header: "Commission", - render: (value) => `${value}%`, - sorter: (a, b) => a.commission - b.commission, - }, - ]} - /> +const columns: ColumnProps[] = [ + { + key: "name", + header: "Finality Provider", + render: (_: unknown, row: FinalityProvider) => ( +
+ + {row.name}
- ); + ), + sorter: (a, b) => a.name.localeCompare(b.name), }, -}; + { + key: "status", + header: "Status", + }, + { + key: "btcPk", + header: "BTC PK", + }, + { + key: "totalDelegation", + header: "Total Delegation", + render: (_: unknown, row: FinalityProvider) => `${row.totalDelegation} sBTC`, + sorter: (a, b) => a.totalDelegation - b.totalDelegation, + }, + { + key: "commission", + header: "Commission", + render: (_: unknown, row: FinalityProvider) => `${row.commission}%`, + sorter: (a, b) => a.commission - b.commission, + }, +]; -export const WithoutRowSelect: Story = { +export const Default: Story = { render: () => { const [tableData, setTableData] = useState(data.slice(0, 3)); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); + const [selectedProvider, setSelectedProvider] = useState(null); - const handleLoadMore = async () => { + const handleLoadMore = useCallback(async () => { setLoading(true); await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -134,49 +102,34 @@ export const WithoutRowSelect: Story = { setTableData((prev) => [...prev, ...nextItems]); setHasMore(tableData.length + nextItems.length < data.length); setLoading(false); - }; + }, [tableData]); + + const handleRowSelect = useCallback((row: FinalityProvider | null) => { + setSelectedProvider(row); + }, []); + + const isRowSelectable = useCallback((row: FinalityProvider) => { + return row.status === "Active"; + }, []); return ( -
-
( -
- - {row.name} -
- ), - sorter: (a, b) => a.name.localeCompare(b.name), - }, - { - key: "status", - header: "Status", - }, - { - key: "btcPk", - header: "BTC PK", - }, - { - key: "totalDelegation", - header: "Total Delegation", - render: (value) => `${value} sBTC`, - sorter: (a, b) => a.totalDelegation - b.totalDelegation, - }, - { - key: "commission", - header: "Commission", - render: (value) => `${value}%`, - sorter: (a, b) => a.commission - b.commission, - }, - ]} - /> +
+
+
+ + {selectedProvider && ( +
+ Selected Provider: {selectedProvider.name} (Commission: {selectedProvider.commission}%) +
+ )} ); }, diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 456a37c..9c0d46a 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,4 +1,4 @@ -import { useRef, useMemo, useState } from "react"; +import { useRef, useMemo, useState, useCallback } from "react"; import { twJoin } from "tailwind-merge"; import { useTableScroll } from "@/hooks/useTableScroll"; import { TableContext, TableContextType } from "../../context/Table.context"; @@ -15,6 +15,7 @@ export function Table({ loading = false, onLoadMore, onRowSelect, + isRowSelectable, ...restProps }: TableProps) { const tableRef = useRef(null); @@ -22,7 +23,7 @@ export function Table({ const [sortStates, setSortStates] = useState<{ [key: string]: { direction: "asc" | "desc" | null; priority: number }; }>({}); - const [selectedRow, setSelectedRow] = useState(undefined); + const [selectedRow, setSelectedRow] = useState(null); const { isScrolledTop } = useTableScroll(tableRef, { onLoadMore, @@ -30,19 +31,32 @@ export function Table({ loading, }); - const handleHoveredColumn = (column: string) => { - if (hoveredColumn === column) return; - setHoveredColumn(column); - }; + const handleHoveredColumn = useCallback( + (column: string) => { + if (hoveredColumn !== column) { + setHoveredColumn(column); + } + }, + [hoveredColumn], + ); - const handleRowSelect = (row: T) => { - if (!onRowSelect) return; - if (selectedRow === row.id) return; - setSelectedRow(row.id); - onRowSelect(row); - }; + const handleRowSelect = useCallback( + (row: T) => { + if (!onRowSelect) return; + if (isRowSelectable && !isRowSelectable(row)) return; + if (selectedRow === row.id) { + setSelectedRow(null); + onRowSelect(null); + return; + } + + setSelectedRow(row.id); + onRowSelect(row); + }, + [onRowSelect, isRowSelectable, selectedRow], + ); - const handleColumnSort = (columnKey: string, sorter?: (a: T, b: T) => number) => { + const handleColumnSort = useCallback((columnKey: string, sorter?: (a: T, b: T) => number) => { if (!sorter) return; setSortStates((prev) => { @@ -73,7 +87,7 @@ export function Table({ }, }; }); - }; + }, []); const sortedData = useMemo(() => { const activeSorters = Object.entries(sortStates) @@ -125,22 +139,29 @@ export function Table({ - {sortedData.map((row) => ( - handleRowSelect(row)} - > - {columns.map((column) => ( - column.render!(value, row) : undefined} - /> - ))} - - ))} + {sortedData.map((row) => { + const isSelectable = isRowSelectable ? isRowSelectable(row) : true; + return ( + handleRowSelect(row)} + > + {columns.map((column) => ( + column.render!(value, row) : undefined} + /> + ))} + + ); + })}
diff --git a/src/components/Table/types/index.ts b/src/components/Table/types/index.ts index 127f9bd..995fded 100644 --- a/src/components/Table/types/index.ts +++ b/src/components/Table/types/index.ts @@ -14,5 +14,6 @@ export interface TableProps { hasMore?: boolean; loading?: boolean; onLoadMore?: () => void; - onRowSelect?: (row: T) => void; + onRowSelect?: (row: T | null) => void; + isRowSelectable?: (row: T) => boolean; }