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