diff --git a/package-lock.json b/package-lock.json index c765241cf..37ca0e837 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@mui/styled-engine": "^5.13.2", "@mui/x-data-grid": "^5.17.26", "@react-pdf-viewer/core": "^3.12.0", + "@tanstack/react-table": "^8.20.5", "@testing-library/dom": "^8.20.1", "@testing-library/user-event": "^14.4.3", "@types/node": "^20.4.2", @@ -4719,6 +4720,25 @@ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==" }, + "node_modules/@tanstack/react-table": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", + "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.10.8", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz", @@ -4735,6 +4755,18 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-core": { "version": "3.10.8", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz", diff --git a/package.json b/package.json index 39a90777c..827299bc2 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@mui/styled-engine": "^5.13.2", "@mui/x-data-grid": "^5.17.26", "@react-pdf-viewer/core": "^3.12.0", + "@tanstack/react-table": "^8.20.5", "@testing-library/dom": "^8.20.1", "@testing-library/user-event": "^14.4.3", "@types/node": "^20.4.2", diff --git a/src/components/InvitationDataPagination.tsx b/src/components/InvitationDataPagination.tsx new file mode 100644 index 000000000..12431d47d --- /dev/null +++ b/src/components/InvitationDataPagination.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { + ArrowCircleLeftIcon, + ArrowCircleRightIcon, +} from '@heroicons/react/outline'; +import { Table } from "@tanstack/react-table"; + +interface PaginationProps { + tableLib: Table; + totalPages: number; + pageSize: number; + pageIndex: number; + sizes: number[]; + loading: boolean; // Add a loading prop to control button state +} + +function Pagination({ + tableLib, + totalPages, + pageSize, + pageIndex, + sizes, + loading, +}: PaginationProps) { + const validTotalPages = totalPages && totalPages > 0 ? totalPages : 1; + + return ( +
+
+ + {/* Previous / Next Page Buttons */} +
+ + + +
+ + {/* Page Information */} +
+ Page {pageIndex + 1} of {validTotalPages} +
+ + {/* Page Size Selector */} +
+ +
+
+
+ ); +} + +export default Pagination; diff --git a/src/components/InvitationTable.tsx b/src/components/InvitationTable.tsx index a358539e2..562f60996 100644 --- a/src/components/InvitationTable.tsx +++ b/src/components/InvitationTable.tsx @@ -1,132 +1,133 @@ -// @ts-nocheck -import React, { useState, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useGlobalFilter, usePagination, useSortBy, useTable } from 'react-table'; -import { toast } from 'react-toastify'; -import DataPagination from './DataPagination'; -import SkeletonTable from '../Skeletons/SkeletonTable'; +import React, { useMemo } from "react"; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import Pagination from "../components/InvitationDataPagination"; +import SkeletonTable from '../Skeletons/SkeletonTable'; -interface TableData { - data: any[]; - columns: any; - error: string | null; - loading?: boolean; - className?: string; +interface Column { + accessor?: string | ((row: any) => any); + header: React.ReactNode; + cell: (info: any) => React.ReactNode; } -function DataTableStats({ data, columns, error, loading }: TableData) { - const [filterInput, setFilterInput] = useState(''); - const { t } = useTranslation(); - const [pageIndex, setPageIndex] = useState(0); +interface TableProps { + cols: Column[]; + data: any[]; + loading: boolean; + rowCount: number; + onPaginationChange: (pagination: { pageIndex: number; pageSize: number }) => void; + pagination: { pageSize: number; pageIndex: number }; +} - // Memoize columns and data to prevent unnecessary re-renders - const memoizedColumns = useMemo(() => [...columns], [columns]); - const memoizedData = useMemo(() => [...data], [data]); +const columnHelper = createColumnHelper(); - // Table instance - const tableInstance = useTable( - { - data: memoizedData, - columns: memoizedColumns, - initialState: { pageIndex, pageSize: 3, globalFilter: filterInput }, - }, - useGlobalFilter, - useSortBy, - usePagination, - ); +function InvitationTable({ + cols, + data, + loading, + rowCount, + onPaginationChange, + pagination, +}: TableProps) { + const { pageSize, pageIndex } = pagination; - const { - getTableProps, - setGlobalFilter, - getTableBodyProps, - page, - nextPage, - previousPage, - canPreviousPage, - canNextPage, - gotoPage, - pageCount, - setPageSize, - pageOptions, - headerGroups, - prepareRow, - state: { pageIndex: currentPageIndex, pageSize }, - } = tableInstance; + const totalPages = Math.ceil(rowCount / pageSize); - useEffect(() => { - setPageIndex(currentPageIndex); - }, [currentPageIndex]); + const tableColumns = useMemo( + () => + cols.map((column) => { + if (typeof column.accessor === 'string') { + return columnHelper.accessor(column.accessor, { + header: typeof column.header === 'string' ? column.header : column.header(), + cell: column.cell, + }); + } else if (typeof column.accessor === 'function') { + return columnHelper.accessor((row) => column.accessor(row), { + header: typeof column.header === 'string' ? column.header : column.header(), + cell: column.cell, + }); + } else { + return columnHelper.display({ + header: typeof column.header === 'string' ? column.header : column.header(), + cell: column.cell, + }); + } + }), + [cols] + ); - const handleFilterChange = (e) => { - const value = e.target.value || ''; - setGlobalFilter(value); - setFilterInput(value); - }; + const tableLib = useReactTable({ + data, + columns: tableColumns, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + state: { + pagination, + }, + onPaginationChange, + }); return ( -
+
{loading ? ( - + ) : ( - +
- {headerGroups.map((headerGroup) => ( - - {headerGroup.headers.map((column) => ( + {tableLib.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( ))} ))} - - {memoizedData.length === 0 && ( + + {data.length === 0 && ( )} - {memoizedData.length > 0 && - page.map((row) => { - prepareRow(row); - return ( - ( + + {row.getVisibleCells().map((cell) => ( + - ))} - - ); - })} - {!loading && !error && data.length === 0 && ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + {!loading && data.length === 0 && ( -
- {column.render('Header')} + {flexRender(header.column.columnDef.header, header.getContext())}
 {' '} {/* Non-breaking space to ensure it's not an empty tag */}
- {row.cells.map((cell) => ( - - {cell.render('Cell')} -
+

No records available @@ -140,22 +141,17 @@ function DataTableStats({ data, columns, error, loading }: TableData) { )}

-
- + ); } -export default DataTableStats; \ No newline at end of file +export default InvitationTable; diff --git a/src/hook/UsePagination.tsx b/src/hook/UsePagination.tsx new file mode 100644 index 000000000..043eb765d --- /dev/null +++ b/src/hook/UsePagination.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; + +const usePagination = (initialSize = 3) => { + const [pagination, setPagination] = useState({ + pageSize: initialSize, + pageIndex: 0, + }); + + const { pageSize, pageIndex } = pagination; + + return { + onPaginationChange: setPagination, + pagination, + limit: pageSize, + skip: pageSize * pageIndex, + }; +}; + +export default usePagination; diff --git a/src/pages/invitation.tsx b/src/pages/invitation.tsx index 526af1dde..419ea3d21 100644 --- a/src/pages/invitation.tsx +++ b/src/pages/invitation.tsx @@ -9,7 +9,6 @@ import { LuHourglass } from 'react-icons/lu'; import { BsPersonFillX } from 'react-icons/bs'; import { toast } from 'react-toastify'; import InvitationCard from '../components/InvitationCard'; -import DataTableStats from '../components/InvitationTable'; import InvitationModal from './invitationModalComponet'; import { GET_INVITATIONS_STATISTICS_QUERY } from '../queries/invitationStats.queries'; import InvitationCardSkeleton from '../Skeletons/InvitationCardSkeleton'; @@ -28,6 +27,8 @@ import { GET_ROLES_AND_STATUSES, } from '../queries/invitation.queries'; import { isValid } from 'date-fns'; +import InvitationTable from '../components/InvitationTable' +import usePagination from '../hook/UsePagination' interface Invitee { email: string; @@ -85,6 +86,11 @@ function Invitation() { role: '', status: '', }); + + + const [isFiltering, setIsFiltering] = useState(false); + const { limit, skip, pagination, onPaginationChange } = usePagination(3); + const[filterDisabled,setFilterDisabled]=useState(true) const modalRef = useRef(null); const organizationToken = localStorage.getItem('orgToken'); @@ -128,11 +134,71 @@ function Invitation() { } = useQuery(GET_ALL_INVITATIONS, { variables:{ orgToken: organizationToken, - sortBy:sortBy + sortBy:sortBy, + limit, + offset: skip + }, + fetchPolicy: 'network-only', + skip: isFiltering + }); + + const [ + fetchInvitations, + { data: searchData, loading: searchLoading, error: searchError }, + ] = useLazyQuery(GET_INVITATIONS, { + variables: { + query: searchQuery, + orgToken: organizationToken, + limit, + offset: skip, + sortBy:sortBy + }, + fetchPolicy: 'network-only', + }); + + const [ + filterInvitations, + { data: filterData, loading: filterLoad, error: filterError, refetch: refetchFiltered }, + ] = useLazyQuery(GET_ROLES_AND_STATUSES, { + variables:{ + ...filterVariables, + limit, + offset: skip, }, fetchPolicy: 'network-only', }); + const isSearching = searchQuery && searchQuery.trim() !== ""; + +// Refetch data on pagination change and filter clearing +useEffect(() => { + if (isSearching) { + fetchInvitations({ + variables: { + query: searchQuery, + orgToken: organizationToken, + sortBy: sortBy, + limit, + offset: skip, + }, + }); + } else if (isFiltering) { + refetchFiltered({ + ...filterVariables, + limit, + offset: skip, + }); + } else { + refetch({ + orgToken: organizationToken, + sortBy: sortBy, + limit, + offset: skip, + }); + } +}, [limit, skip, refetch, refetchFiltered, fetchInvitations, isFiltering, filterVariables, searchQuery, isSearching]); + + useEffect(() => { if (invitationStats) { setSelectedStatus(''); // Set the fetched status as the default value @@ -141,17 +207,27 @@ function Invitation() { // Set email and role when modal opens useEffect(() => { - if (data && data.getAllInvitations) { - const invitation = data.getAllInvitations.invitations.find( - (inv: Invitationn) => inv.id === selectedInvitationId, + let invitation; + + if (isSearching && searchData?.getInvitations) { + invitation = searchData.getInvitations.invitations.find( + (inv) => inv.id === selectedInvitationId + ); + } else if (isFiltering && filterData?.filterInvitations) { + invitation = filterData.filterInvitations.invitations.find( + (inv) => inv.id === selectedInvitationId + ); + } else if (data && data.getAllInvitations) { + invitation = data.getAllInvitations.invitations.find( + (inv) => inv.id === selectedInvitationId ); - - if (invitation && invitation.invitees.length > 0) { - setEmail(invitation.invitees[0].email); - setRole(invitation.invitees[0].role); - } } - }, [data, selectedInvitationId]); + + if (invitation && invitation.invitees.length > 0) { + setEmail(invitation.invitees[0].email); + setRole(invitation.invitees[0].role); + } + }, [data, searchData, filterData, selectedInvitationId, isSearching, isFiltering]); useEffect(() => { const handleClickOutside = (event: any) => { @@ -197,36 +273,6 @@ function Invitation() { setUpdateInviteeModel(newState); }; - const [ - fetchInvitations, - { data: searchData, loading: searchLoading, error: searchError }, - ] = useLazyQuery(GET_INVITATIONS, { - variables: { - query: searchQuery, - orgToken: organizationToken - }, - fetchPolicy: 'network-only', - }); - const [ - filterInvitations, - { data: filterData, loading: filterLoad, error: filterError }, - ] = useLazyQuery(GET_ROLES_AND_STATUSES, { - variables:filterVariables, - fetchPolicy: 'network-only', - }); - - useEffect(() => { - if (filterVariables.role || filterVariables.status) { - filterInvitations({ - variables: { - role: filterVariables.role || null, - status:filterVariables.status || null, - orgToken: organizationToken - }, - }); - } - }, [filterVariables, filterInvitations,organizationToken]); - // Consolidated effect to handle query and search data useEffect(() => { if (queryLoading || searchLoading || filterLoad) { @@ -241,26 +287,12 @@ function Invitation() { setError(searchError.message); } else if (filterError) { setError(filterError.message); - } else if ( - searchData && - Array.isArray(searchData.getInvitations.invitations) - ) { - setInvitations(searchData.getInvitations.invitations); - } else if (filterData && filterData.filterInvitations) { - setInvitations(filterData.filterInvitations.invitations); - setTotalInvitations(filterData.filterInvitations.totalInvitations); - } else if (data && data.getAllInvitations) { - setInvitations(data.getAllInvitations.invitations); - setTotalInvitations(data.getAllInvitations.totalInvitations); } }, [ - data, - searchData, queryLoading, searchLoading, queryError, searchError, - filterData, filterLoad, filterError, ]); @@ -277,6 +309,8 @@ function Invitation() { setError(null); setLoading(false); + onPaginationChange({ pageSize: pagination.pageSize, pageIndex: 0 }); + fetchInvitations({ variables: { query: searchQuery } }); }; @@ -307,26 +341,41 @@ const handleStatusChange=(e:React.ChangeEvent)=>{ setSelectedStatus(status) } - const handleFilter = () => { - if (!selectedRole && !selectedStatus) { - toast.info('Please select role or status.'); - return; - } - setInvitations([]); - setError(null); - setLoading(false); +// Handle filter application +const handleFilter = () => { + if (!selectedRole && !selectedStatus) { + toast.info('Please select role or status.'); + return; + } - setFilterVariables({ - role: selectedRole, - status: typeof selectedStatus === 'string' ? selectedStatus : '', - }); - filterInvitations({ - variables:{ - sortBy:sortBy - } - }) - - }; + onPaginationChange({ pageSize: pagination.pageSize, pageIndex: 0 }); + + setIsFiltering(true); + + setFilterVariables({ + role: selectedRole, + status: typeof selectedStatus === 'string' ? selectedStatus : '', + }); + + filterInvitations({ + variables: { + role: selectedRole || "", + status: selectedStatus || "", + orgToken: organizationToken, + limit, + offset: skip, + }, + }); +}; + + +useEffect(() => { + if (selectedRole || selectedStatus) { + setFilterDisabled(false); + } else { + setFilterDisabled(true); + } +}, [selectedRole, selectedStatus]); const toggleOptions = (row: string) => { setSelectedRow(selectedRow === row ? null : row); @@ -360,28 +409,28 @@ const handleStatusChange=(e:React.ChangeEvent)=>{ return str.charAt(0).toUpperCase() + str.slice(1); }; const columns = [ - { Header: t('email'), accessor: 'email' }, - { Header: t('role'), accessor: 'role' }, { - Header: t('Status'), - accessor: 'status', - Cell: ({ row }: any) => { - return ( -
0 ? ' flex' : ' hidden') - } - > - {row.original.Status} -
- ); - }, + id: "email", + header: t('email'), + accessor: (row) => row.email, + cell: (info) =>
{info.getValue()}
, }, { - Header: t('action'), - accessor: '', - Cell: ({ row }: any) => ( + id: "role", + header: t('role'), + accessor: (row) => row.role, + cell: (info) =>
{info.getValue()}
, + }, + { + id: "Status", + header: t('Status'), + accessor: (row) => row.Status, + cell: (info) =>
{info.getValue()}
, + }, + { + id: "Action", + header: t('Action'), + cell: ({ row }: any) => (
)=>{ }, ]; - const datum: any = []; - if (invitations && invitations.length > 0) { - invitations.forEach((invitation) => { - invitation.invitees?.forEach((data: any) => { - let entry: any = {}; - entry.email = data.email; - entry.role = capitalizeStrings(data.role); - entry.Status = capitalizeStrings(invitation.status); - entry.id = invitation.id; - datum.push(entry); - }); +const datum: any = []; + +const currentInvitations = isSearching && searchData?.getInvitations + ? searchData.getInvitations.invitations + : isFiltering && filterData?.filterInvitations + ? filterData.filterInvitations.invitations + : data?.getAllInvitations?.invitations; + +const currentInvitationsTotal = isSearching && searchData?.getInvitations + ? searchData.getInvitations.totalInvitations + : isFiltering && filterData?.filterInvitations + ? filterData.filterInvitations.totalInvitations + : data?.getAllInvitations?.totalInvitations; + +if (currentInvitations && currentInvitations.length > 0) { + currentInvitations.forEach((invitation) => { + invitation.invitees?.forEach((invitee: any) => { + let entry: any = {}; + entry.email = invitee.email; + entry.role = capitalizeStrings(invitee.role); + entry.Status = capitalizeStrings(invitation.status); + entry.id = invitation.id; + datum.push(entry); }); - } + }); +} + if (loading || searchLoading || filterLoad) { content = ( - 0 ? datum : []} - columns={columns} - loading={loading} - error={error} + 0 ? datum : []} + cols={columns} + loading={loading || searchLoading} + rowCount={currentInvitationsTotal} + onPaginationChange={onPaginationChange} + pagination={pagination} /> ); } else if (error || searchError || filterError) { content = ( - 0 ? datum : []} - columns={columns} - loading={loading} - error={error} + 0 ? datum : []} + cols={columns} + loading={loading || searchLoading} + rowCount={currentInvitationsTotal} + onPaginationChange={onPaginationChange} + pagination={pagination} /> ); } else { content = ( <> - 0 ? datum : []} - columns={columns} - loading={loading} - error={error} + 0 ? datum : []} + cols={columns} + loading={loading || searchLoading} + rowCount={currentInvitationsTotal} + onPaginationChange={onPaginationChange} + pagination={pagination} /> ); @@ -562,6 +631,7 @@ const handleStatusChange=(e:React.ChangeEvent)=>{ setButtonLoading(false); toast.success(data.resendInvitation.message); refetch(); + refreshData(); setResendInvatationModel(false) }) } @@ -583,6 +653,7 @@ const handleStatusChange=(e:React.ChangeEvent)=>{ setButtonLoading(false); toast.success(data.deleteInvitation.message); refetch(); + refreshData(); removeInviteeMod(); }, 1000); }, @@ -635,6 +706,7 @@ const handleStatusChange=(e:React.ChangeEvent)=>{ data.cancelInvitation.message || 'Invitation canceled successfully.', ); refetch(); + refreshData(); cancelInviteeMod(); }, 1000); }, diff --git a/src/queries/invitation.queries.tsx b/src/queries/invitation.queries.tsx index b1b97200a..f9e9b1d3c 100644 --- a/src/queries/invitation.queries.tsx +++ b/src/queries/invitation.queries.tsx @@ -28,6 +28,7 @@ export const GET_INVITATIONS = gql` id status } + totalInvitations } } `; @@ -43,6 +44,7 @@ export const GET_ROLES_AND_STATUSES = gql` id status } + totalInvitations } } `;