feat(FE-114): integrate row selection functionality in RecordingTable and Table components

This commit is contained in:
rstubryan
2025-10-24 10:18:56 +07:00
parent 41e6848d75
commit 9cbc703a63
2 changed files with 131 additions and 136 deletions
+99 -86
View File
@@ -48,6 +48,8 @@ export interface TableProps<TData extends object> {
sorting?: SortingState; sorting?: SortingState;
setSorting?: OnChangeFn<SortingState>; setSorting?: OnChangeFn<SortingState>;
manualSorting?: boolean; manualSorting?: boolean;
rowSelection?: Record<string, boolean>;
setRowSelection?: OnChangeFn<Record<string, boolean>>;
} }
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
@@ -61,32 +63,34 @@ const emptyContentDefaultValue = (
); );
const Table = <TData extends object>({ const Table = <TData extends object>({
data = [], data = [],
columns = [], columns = [],
pageSize = 10, pageSize = 10,
totalItems, totalItems,
page, page,
onPageChange, onPageChange,
isLoading = false, isLoading = false,
fuzzySearchValue, fuzzySearchValue,
onFuzzySearchValueChange, onFuzzySearchValueChange,
className = { className = {
containerClassName: '', containerClassName: '',
tableWrapperClassName: '', tableWrapperClassName: '',
tableClassName: '', tableClassName: '',
tableHeaderClassName: '', tableHeaderClassName: '',
headerRowClassName: '', headerRowClassName: '',
headerColumnClassName: '', headerColumnClassName: '',
tableBodyClassName: '', tableBodyClassName: '',
bodyRowClassName: '', bodyRowClassName: '',
bodyColumnClassName: '', bodyColumnClassName: '',
paginationClassName: '', paginationClassName: '',
}, },
emptyContent = emptyContentDefaultValue, emptyContent = emptyContentDefaultValue,
sorting, sorting,
setSorting, setSorting,
manualSorting = false, manualSorting = false,
}: TableProps<TData>) => { rowSelection,
setRowSelection,
}: TableProps<TData>) => {
const isServerSideTable = const isServerSideTable =
totalItems !== undefined && totalItems !== undefined &&
page !== undefined && page !== undefined &&
@@ -137,6 +141,15 @@ const Table = <TData extends object>({
}; };
} }
if (rowSelection && setRowSelection) {
tableOptions.onRowSelectionChange = setRowSelection;
tableOptions.state = {
...tableOptions.state,
rowSelection,
};
tableOptions.getRowId = (row) => (row as { id: string }).id;
}
const table = useReactTable(tableOptions); const table = useReactTable(tableOptions);
const { setPageSize } = table; const { setPageSize } = table;
@@ -175,72 +188,72 @@ const Table = <TData extends object>({
<div className={className.tableWrapperClassName}> <div className={className.tableWrapperClassName}>
<table className={className.tableClassName}> <table className={className.tableClassName}>
<thead className={className.tableHeaderClassName}> <thead className={className.tableHeaderClassName}>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className={className.headerRowClassName}> <tr key={headerGroup.id} className={className.headerRowClassName}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<th <th
key={header.id} key={header.id}
colSpan={header.colSpan} colSpan={header.colSpan}
onClick={header.column.getToggleSortingHandler()} onClick={header.column.getToggleSortingHandler()}
className={cn( className={cn(
header.column.getCanSort() header.column.getCanSort()
? 'cursor-pointer select-none' ? 'cursor-pointer select-none'
: '', : '',
className.headerColumnClassName className.headerColumnClassName
)}
>
<div className='flex items-center gap-1'>
{flexRender(
header.column.columnDef.header,
header.getContext()
)} )}
>
<div className='flex items-center gap-1'>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && ( {header.column.getCanSort() && (
<div className='flex items-center'> <div className='flex items-center'>
<Icon <Icon
icon='lucide:arrow-up' icon='lucide:arrow-up'
width={12} width={12}
height={12} height={12}
className={cn( className={cn(
'transition-all ease-in-out duration-200', 'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc' header.column.getIsSorted() === 'asc'
? 'text-black' ? 'text-black'
: 'text-black/30' : 'text-black/30'
)} )}
/> />
<Icon <Icon
icon='lucide:arrow-down' icon='lucide:arrow-down'
width={12} width={12}
height={12} height={12}
className={cn( className={cn(
'transition-all ease-in-out duration-200', 'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc' header.column.getIsSorted() === 'desc'
? 'text-black' ? 'text-black'
: 'text-black/30' : 'text-black/30'
)} )}
/> />
</div> </div>
)} )}
</div> </div>
</th> </th>
))} ))}
</tr> </tr>
))} ))}
</thead> </thead>
<tbody className={className.tableBodyClassName}> <tbody className={className.tableBodyClassName}>
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows.map((row) => (
<tr key={row.id} className={className.bodyRowClassName}> <tr key={row.id} className={className.bodyRowClassName}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td key={cell.id} className={className.bodyColumnClassName}> <td key={cell.id} className={className.bodyColumnClassName}>
{!isLoading && {!isLoading &&
flexRender(cell.column.columnDef.cell, cell.getContext())} flexRender(cell.column.columnDef.cell, cell.getContext())}
{isLoading && <div className='skeleton w-full h-4' />} {isLoading && <div className='skeleton w-full h-4' />}
</td> </td>
))} ))}
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -9,6 +9,7 @@ import Button from '@/components/Button';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { OptionType } from '@/components/input/SelectInput'; import { OptionType } from '@/components/input/SelectInput';
import { ROWS_OPTIONS } from '@/config/constant'; import { ROWS_OPTIONS } from '@/config/constant';
import CheckboxInput from '@/components/input/CheckboxInput';
import { TableToolbar } from '@/components/table/TableToolbar'; import { TableToolbar } from '@/components/table/TableToolbar';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import Table from '@/components/Table'; import Table from '@/components/Table';
@@ -117,7 +118,7 @@ const RecordingTable = () => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [selectedRecordings, setSelectedRecordings] = useState<number[]>([]); const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [, setSelectedRecording] = useState<RecordingWithRelations | undefined>(undefined); const [, setSelectedRecording] = useState<RecordingWithRelations | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false); const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false);
@@ -160,28 +161,24 @@ const RecordingTable = () => {
return filteredData.slice(start, start + pageSize); return filteredData.slice(start, start + pageSize);
}, [page, pageSize, search]); }, [page, pageSize, search]);
const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item));
const bulkApproveHandler = async () => { const bulkApproveHandler = async () => {
setIsBulkApproveLoading(true); setIsBulkApproveLoading(true);
console.log( console.log('Approved recordings:', selectedRowIds);
'Approved recordings:',
paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
);
setTimeout(() => { setTimeout(() => {
setIsBulkApproveLoading(false); setIsBulkApproveLoading(false);
setSelectedRecordings([]); setRowSelection({});
bulkApproveModal.closeModal(); bulkApproveModal.closeModal();
}, 1000); }, 1000);
}; };
const bulkRejectHandler = async () => { const bulkRejectHandler = async () => {
setIsBulkRejectLoading(true); setIsBulkRejectLoading(true);
console.log( console.log('Rejected recordings:', selectedRowIds);
'Rejected recordings:',
paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
);
setTimeout(() => { setTimeout(() => {
setIsBulkRejectLoading(false); setIsBulkRejectLoading(false);
setSelectedRecordings([]); setRowSelection({});
bulkRejectModal.closeModal(); bulkRejectModal.closeModal();
}, 1000); }, 1000);
}; };
@@ -217,7 +214,7 @@ const RecordingTable = () => {
{/* Bulk action buttons */} {/* Bulk action buttons */}
<div className={'flex justify-end items-center'}> <div className={'flex justify-end items-center'}>
{selectedRecordings.length > 0 && ( {selectedRowIds.length > 0 && (
<div className='flex gap-2 mb-4'> <div className='flex gap-2 mb-4'>
<Button <Button
type='button' type='button'
@@ -230,7 +227,7 @@ const RecordingTable = () => {
width={20} width={20}
height={20} height={20}
/> />
Approve ({selectedRecordings.length}) Approve ({selectedRowIds.length})
</Button> </Button>
<Button <Button
type='button' type='button'
@@ -243,7 +240,7 @@ const RecordingTable = () => {
width={20} width={20}
height={20} height={20}
/> />
Reject ({selectedRecordings.length}) Reject ({selectedRowIds.length})
</Button> </Button>
</div> </div>
)} )}
@@ -251,7 +248,7 @@ const RecordingTable = () => {
<ConfirmationModal <ConfirmationModal
ref={bulkApproveModal.ref} ref={bulkApproveModal.ref}
type='success' type='success'
text={`Apakah anda yakin ingin menyetujui ${selectedRecordings.length} data Recording yang dipilih?`} text={`Apakah anda yakin ingin menyetujui ${selectedRowIds.length} data Recording yang dipilih?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
@@ -266,7 +263,7 @@ const RecordingTable = () => {
<ConfirmationModal <ConfirmationModal
ref={bulkRejectModal.ref} ref={bulkRejectModal.ref}
type='error' type='error'
text={`Apakah anda yakin ingin menolak ${selectedRecordings.length} data Recording yang dipilih?`} text={`Apakah anda yakin ingin menolak ${selectedRowIds.length} data Recording yang dipilih?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
@@ -284,43 +281,26 @@ const RecordingTable = () => {
columns={[ columns={[
{ {
id: 'select', id: 'select',
accessorKey: 'id',
header: ({ table }) => ( header: ({ table }) => (
<input <div className='w-full flex flex-row justify-center'>
type='checkbox' <CheckboxInput
className='checkbox' name='allRow'
checked={ checked={table.getIsAllRowsSelected()}
table.getRowModel().rows.length > 0 && indeterminate={table.getIsSomeRowsSelected()}
table onChange={table.getToggleAllRowsSelectedHandler()}
.getRowModel() />
.rows.every((row) => selectedRecordings.includes(row.index)) </div>
}
onChange={(e) => {
if (e.target.checked) {
setSelectedRecordings(
table.getRowModel().rows.map((row) => row.index)
);
} else {
setSelectedRecordings([]);
}
}}
/>
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<input <div>
type='checkbox' <CheckboxInput
className='checkbox' name='row'
checked={selectedRecordings.includes(row.index)} checked={row.getIsSelected()}
onChange={(e) => { disabled={!row.getCanSelect()}
if (e.target.checked) { indeterminate={row.getIsSomeSelected()}
setSelectedRecordings([...selectedRecordings, row.index]); onChange={row.getToggleSelectedHandler()}
} else { />
setSelectedRecordings( </div>
selectedRecordings.filter((i) => i !== row.index)
);
}
}}
/>
), ),
}, },
{ {
@@ -403,6 +383,8 @@ const RecordingTable = () => {
isLoading={false} isLoading={false}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{ className={{
containerClassName: cn({ containerClassName: cn({
'mb-20': paginatedData.length === 0, 'mb-20': paginatedData.length === 0,