From 84e78c084c7b4ab03f4f09c16c78923e79aed446 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 <69841784+vutuanlinh2k2@users.noreply.github.com> Date: Thu, 11 Jul 2024 21:42:23 +0700 Subject: [PATCH] fix(tangle-dapp): Fix sort/scroll issues on nomination selection list modal (#2430) Co-authored-by: Trung-Tin Pham <60747384+AtelyPham@users.noreply.github.com> --- .../ValidatorSelectionTable.tsx | 248 +++++++----------- .../ValidatorSelectionTable/types.ts | 1 + .../components/tableCells/TokenAmountCell.tsx | 2 +- 3 files changed, 95 insertions(+), 156 deletions(-) diff --git a/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx b/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx index c3ea2337f6..1f2587ee17 100644 --- a/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx +++ b/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx @@ -1,23 +1,20 @@ 'use client'; -import { BN } from '@polkadot/util'; import { - type Column, - type ColumnSort, createColumnHelper, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, - type PaginationState, type Row, type RowSelectionState, type SortingColumn, + type SortingFn, type SortingState, type TableOptions, useReactTable, } from '@tanstack/react-table'; -import { ArrowDropDownFill, ArrowDropUpFill, Search } from '@webb-tools/icons'; +import { Search } from '@webb-tools/icons'; import { Avatar, CheckBox, @@ -41,27 +38,22 @@ import React, { import { Validator } from '../../types'; import calculateCommission from '../../utils/calculateCommission'; +import { + sortAddressOrIdentityForNomineeOrValidator, + sortBnValueForNomineeOrValidator, +} from '../../utils/table'; import { ContainerSkeleton } from '..'; import { HeaderCell } from '../tableCells'; import TokenAmountCell from '../tableCells/TokenAmountCell'; import { ValidatorSelectionTableProps } from './types'; -const DEFAULT_PAGINATION: PaginationState = { - pageIndex: 0, - pageSize: 20, -}; - -const SELECTED_VALIDATORS_COLUMN_SORT = { - id: 'address', - desc: false, -} as const satisfies ColumnSort; - const columnHelper = createColumnHelper(); const ValidatorSelectionTable: FC = ({ allValidators, defaultSelectedValidators, setSelectedValidators, + pageSize = 20, }) => { const [searchValue, setSearchValue] = useState(''); const [rowSelection, setRowSelection] = useState( @@ -71,11 +63,13 @@ const ValidatorSelectionTable: FC = ({ }, {} as RowSelectionState), ); const [sorting, setSorting] = useState([ - SELECTED_VALIDATORS_COLUMN_SORT, + { + id: 'totalStakeAmount', + desc: true, + }, ]); - const [pagination, setPagination] = - useState(DEFAULT_PAGINATION); + const isDesc = useMemo(() => sorting[0].desc, [sorting]); const toggleSortSelectionHandlerRef = useRef< SortingColumn['toggleSorting'] | null @@ -91,10 +85,9 @@ const ValidatorSelectionTable: FC = ({ const columns = useMemo( () => [ columnHelper.accessor('address', { - header: ({ header }) => { - toggleSortSelectionHandlerRef.current = header.column.toggleSorting; - return ; - }, + header: () => ( + + ), cell: (props) => { const address = props.getValue(); const identity = props.row.original.identityName; @@ -123,134 +116,116 @@ const ValidatorSelectionTable: FC = ({ textToCopy={address} isButton={false} className="cursor-pointer" + iconClassName="!fill-mono-160 dark:!fill-mono-80" /> ); }, - // Sort the selected validators first - sortingFn: (rowA, rowB) => { - const rowASelected = rowA.getIsSelected(); - const rowBSelected = rowB.getIsSelected(); - - if (rowASelected && !rowBSelected) { - return -1; - } - - if (!rowASelected && rowBSelected) { - return 1; - } - - return 0; - }, + sortingFn: (rowA, rowB, columnId) => + sortValidatorsBasedOnSortingFn( + rowA, + rowB, + columnId, + sortAddressOrIdentityForNomineeOrValidator, + isDesc, + ), }), columnHelper.accessor('totalStakeAmount', { - header: ({ header }) => ( -
- - - -
+ header: () => ( + ), cell: (props) => ( -
+
), - sortingFn, + sortingFn: (rowA, rowB, columnId) => + sortValidatorsBasedOnSortingFn( + rowA, + rowB, + columnId, + sortBnValueForNomineeOrValidator, + isDesc, + ), }), columnHelper.accessor('nominatorCount', { - header: ({ header }) => ( -
- - - -
+ header: () => ( + ), cell: (props) => ( -
+
{props.getValue()}
), - sortingFn, + sortingFn: (rowA, rowB, columnId) => + sortValidatorsBasedOnSortingFn( + rowA, + rowB, + columnId, + (rowA, rowB) => + rowA.original.nominatorCount - rowB.original.nominatorCount, + isDesc, + ), }), columnHelper.accessor('commission', { - header: ({ header }) => ( -
- - - -
+ header: () => ( + ), cell: (props) => ( -
+
{calculateCommission(props.getValue()).toFixed(2)}%
), - sortingFn, + sortingFn: (rowA, rowB, columnId) => + sortValidatorsBasedOnSortingFn( + rowA, + rowB, + columnId, + sortBnValueForNomineeOrValidator, + isDesc, + ), }), columnHelper.accessor('identityName', { header: () => , cell: (props) => props.getValue(), }), ], - [], + [isDesc], ); const tableProps = useMemo>( () => ({ data: allValidators, columns, + initialState: { + pagination: { + pageSize, + }, + }, state: { columnVisibility: { identityName: false, }, sorting, rowSelection, - pagination, globalFilter: searchValue, }, enableRowSelection: true, - onPaginationChange: setPagination, onGlobalFilterChange: (props) => { - setPagination(DEFAULT_PAGINATION); setSearchValue(props); }, onRowSelectionChange: (props) => { toggleSortSelectionHandlerRef.current?.(false); setRowSelection(props); + // Force re-sort after selecting an item + setSorting((prevSorting) => [...prevSorting]); }, - onSortingChange: (updaterOrValue) => { - if (typeof updaterOrValue === 'function') { - setSorting((prev) => { - const newSorting = updaterOrValue(prev); - - // Modify the sorting state to always sort by the selected validators first - if (newSorting.length === 0) { - return [SELECTED_VALIDATORS_COLUMN_SORT]; - } else if (newSorting[0].id === 'address') { - return newSorting; - } else { - return [SELECTED_VALIDATORS_COLUMN_SORT, ...newSorting]; - } - }); - } else { - setSorting(updaterOrValue); - } - }, + onSortingChange: setSorting, filterFns: { fuzzy: fuzzyFilter, }, @@ -261,8 +236,9 @@ const ValidatorSelectionTable: FC = ({ getSortedRowModel: getSortedRowModel(), getRowId: (row) => row.address, autoResetPageIndex: false, + enableSortingRemoval: false, }), - [columns, allValidators, pagination, rowSelection, searchValue, sorting], + [allValidators, columns, rowSelection, searchValue, sorting, pageSize], ); const table = useReactTable(tableProps); @@ -295,73 +271,35 @@ const ValidatorSelectionTable: FC = ({ )}
- - Selected: {Object.keys(rowSelection).length}/ - {table.getPreFilteredRowModel().rows.length} - + {allValidators.length > 0 && ( + + Selected: {Object.keys(rowSelection).length}/ + {table.getPreFilteredRowModel().rows.length} + + )} ); }; -type ColumnIdAssertFn = ( - columnId: string, -) => asserts columnId is keyof Validator; - -const assertColumnId: ColumnIdAssertFn = (columnId) => { - if ( - ![ - 'address', - 'effectiveAmountStaked', - 'delegations', - 'commission', - 'nominatorCount', - 'totalStakeAmount', - ].includes(columnId) - ) { - throw new Error(`Invalid columnId: ${columnId}`); - } -}; +export default React.memo(ValidatorSelectionTable); -const sortingFn = ( +function sortValidatorsBasedOnSortingFn( rowA: Row, rowB: Row, columnId: string, -) => { - assertColumnId(columnId); - - if (columnId === 'totalStakeAmount') { - const totalStakedA = rowA.original.totalStakeAmount; - const totalStakedB = rowB.original.totalStakeAmount; - const result = totalStakedA.sub(totalStakedB); - - return result.ltn(0) ? -1 : result.gtn(0) ? 1 : 0; - } - - // TODO: Avoid using Number() here, if it is a BN value, it should be compared as such. - const rowAValue = Number(rowA.original[columnId]); - const rowBValue = Number(rowB.original[columnId]); - - return rowAValue - rowBValue; -}; - -const SortArrow: FC<{ column: Column }> = ({ - column, -}) => { - const isSorted = column.getIsSorted(); - - if (!isSorted) { - return null; - } - - return isSorted === 'asc' ? ( - - ) : ( - - ); -}; - -export default React.memo(ValidatorSelectionTable); + sortFn: SortingFn, + isDesc: boolean, +) { + const rowASelected = rowA.getIsSelected(); + const rowBSelected = rowB.getIsSelected(); + + // Prioritize selected validators + if (rowASelected && !rowBSelected) return isDesc ? 1 : -1; + if (!rowASelected && rowBSelected) return isDesc ? -1 : 1; + + return sortFn(rowA, rowB, columnId); +} diff --git a/apps/tangle-dapp/components/ValidatorSelectionTable/types.ts b/apps/tangle-dapp/components/ValidatorSelectionTable/types.ts index ef1abda91e..5e28c68ea9 100644 --- a/apps/tangle-dapp/components/ValidatorSelectionTable/types.ts +++ b/apps/tangle-dapp/components/ValidatorSelectionTable/types.ts @@ -6,6 +6,7 @@ export type ValidatorSelectionTableProps = { allValidators: Validator[]; defaultSelectedValidators: string[]; setSelectedValidators: Dispatch>>; + pageSize?: number; }; export type SortBy = 'asc' | 'dsc'; diff --git a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx index 51c197928e..c4da5232eb 100644 --- a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx +++ b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx @@ -27,7 +27,7 @@ const TokenAmountCell: FC = ({ amount, className }) => { > {integerPart} - + {decimalPart !== undefined && `.${decimalPart}`} {nativeTokenSymbol}