From 6a6862f8408c226f7907621e881d14477202a357 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Mon, 1 Apr 2024 20:23:38 -0400 Subject: [PATCH] feat: add table column sort --- .../api/cadt/v1/projects/projects.api.ts | 7 +- src/renderer/api/cadt/v1/units/units.api.ts | 7 +- .../blocks/tables/ProjectsListTable.tsx | 10 +- .../blocks/tables/UnitsListTable.tsx | 6 + src/renderer/components/layout/DataTable.tsx | 112 +++++++++++------- src/renderer/hooks/index.ts | 3 +- src/renderer/hooks/useColumnOrder.tsx | 40 +++++++ src/renderer/hooks/useQueryParamState.tsx | 2 +- .../pages/ProjectsList/ProjectsList.tsx | 9 +- src/renderer/pages/UnitsList/UnitsList.tsx | 8 +- src/renderer/routes/route-constants.ts | 4 +- 11 files changed, 151 insertions(+), 57 deletions(-) create mode 100644 src/renderer/hooks/useColumnOrder.tsx diff --git a/src/renderer/api/cadt/v1/projects/projects.api.ts b/src/renderer/api/cadt/v1/projects/projects.api.ts index 32fd830d..b5582aa5 100644 --- a/src/renderer/api/cadt/v1/projects/projects.api.ts +++ b/src/renderer/api/cadt/v1/projects/projects.api.ts @@ -7,6 +7,7 @@ interface GetProjectsParams { page: number; orgUid?: string; search?: string; + order?: string; } interface GetProjectsResponse { @@ -18,7 +19,7 @@ interface GetProjectsResponse { const projectsApi = cadtApi.injectEndpoints({ endpoints: (builder) => ({ getProjects: builder.query({ - query: ({ page, orgUid, search }: GetProjectsParams) => { + query: ({ page, orgUid, search, order }: GetProjectsParams) => { // Initialize the params object with page and limit const params: GetProjectsParams & {limit: number} = { page, limit: 10 }; @@ -30,6 +31,10 @@ const projectsApi = cadtApi.injectEndpoints({ params.search = search.replace(/[^a-zA-Z0-9 _.-]+/, ''); } + if (order) { + params.order = order; + } + return { url: `${host}/v1/projects`, params, // Use the constructed params object diff --git a/src/renderer/api/cadt/v1/units/units.api.ts b/src/renderer/api/cadt/v1/units/units.api.ts index 345dc400..5a5e6300 100644 --- a/src/renderer/api/cadt/v1/units/units.api.ts +++ b/src/renderer/api/cadt/v1/units/units.api.ts @@ -6,6 +6,7 @@ interface GetUnitsParams { page: number; orgUid?: string; search?: string; + order?: string; } interface GetUnitsResponse { @@ -17,7 +18,7 @@ interface GetUnitsResponse { const unitsApi = cadtApi.injectEndpoints({ endpoints: (builder) => ({ getUnits: builder.query({ - query: ({ page, orgUid, search }: GetUnitsParams) => { + query: ({ page, orgUid, search, order }: GetUnitsParams) => { // Initialize the params object with page and limit const params: GetUnitsParams & {limit: number} = { page, limit: 10 }; @@ -29,6 +30,10 @@ const unitsApi = cadtApi.injectEndpoints({ params.search = search.replace(/[^a-zA-Z0-9 _.-]+/, ''); } + if (order) { + params.order = order; + } + return { url: `${host}/v1/units`, params, // Use the constructed params object diff --git a/src/renderer/components/blocks/tables/ProjectsListTable.tsx b/src/renderer/components/blocks/tables/ProjectsListTable.tsx index 0640ee03..db67b883 100644 --- a/src/renderer/components/blocks/tables/ProjectsListTable.tsx +++ b/src/renderer/components/blocks/tables/ProjectsListTable.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; -import { DebouncedFunc }from 'lodash' +import { DebouncedFunc } from 'lodash'; import { DataTable, PageCounter, Pagination } from '@/components'; interface TableProps { @@ -8,6 +8,8 @@ interface TableProps { isLoading: boolean; currentPage: number; onPageChange: DebouncedFunc<(page: any) => void>; + setOrder?: (sort: string) => void; + order?: string; totalPages: number; totalCount: number; } @@ -17,9 +19,13 @@ const ProjectsListTable: React.FC = ({ isLoading, currentPage, onPageChange, + setOrder, + order, totalPages, totalCount, }) => { + + const columns = useMemo( () => [ { @@ -74,6 +80,8 @@ const ProjectsListTable: React.FC = ({
void>; + setOrder?: (sort: string) => void; + order?: string; totalPages: number; totalCount: number; } @@ -17,6 +19,8 @@ const UnitsListTable: React.FC = ({ isLoading, currentPage, onPageChange, + setOrder, + order, totalPages, totalCount, }) => { @@ -75,6 +79,8 @@ const UnitsListTable: React.FC = ({ void; + onChangeOrder?: (column: string) => void; + order?: string; footer?: JSX.Element | null; } -const DataTable: React.FC = - ({ - columns, - primaryKey = 'id', - data, - isLoading = false, - onRowClick = noop, - footer = null, - }) => { - +const DataTable: React.FC = ({ + columns, + primaryKey = 'id', + data, + isLoading = false, + onRowClick = noop, + onChangeOrder, + order, + footer = null, +}) => { if (isLoading) { return null; } @@ -80,44 +83,61 @@ const DataTable: React.FC = style={{ height: data.length > 5 ? 'calc(100vh - 265px)' : 'auto' }} > - - {columns.map((column) => ( - - {column.renderHeader ? column.renderHeader(column) : column.title} - - ))} - - - - {data?.length > 0 && - data.map((row, index) => ( - onRowClick(row)} - className={ - index % 2 === 0 - ? 'bg-gray-50 dark:bg-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700' - : 'bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700' - } - > - {columns.map((column) => ( - -
- {column.render ? ( - column.render(row) - ) : ( -
- {row[column.key]} + + {columns.map((column) => { + const isActive = order?.startsWith(column.key); + return ( + onChangeOrder && onChangeOrder(column.key)} + > +
+ {column.renderHeader ? column.renderHeader(column) : column.title} + {order?.includes(column.key) && ( +
+ {order.includes('ASC') && } + {order.includes('DESC') && }
)}
- - ))} - - ))} + + ); + })} + + + + {data?.length > 0 && + data.map((row, index) => ( + onRowClick(row)} + className={ + index % 2 === 0 + ? 'bg-gray-50 dark:bg-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700' + : 'bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700' + } + > + {columns.map((column) => ( + +
+ {column.render ? ( + column.render(row) + ) : ( +
+ {row[column.key]} +
+ )} +
+ + ))} + + ))} @@ -135,4 +155,4 @@ const DataTable: React.FC = ); }; -export { DataTable }; \ No newline at end of file +export { DataTable }; diff --git a/src/renderer/hooks/index.ts b/src/renderer/hooks/index.ts index 961451e6..5eae9f65 100644 --- a/src/renderer/hooks/index.ts +++ b/src/renderer/hooks/index.ts @@ -1,2 +1,3 @@ export * from './useUrlHash'; -export * from './useQueryParamState'; \ No newline at end of file +export * from './useQueryParamState'; +export * from './useColumnOrder'; \ No newline at end of file diff --git a/src/renderer/hooks/useColumnOrder.tsx b/src/renderer/hooks/useColumnOrder.tsx new file mode 100644 index 00000000..6a67fa87 --- /dev/null +++ b/src/renderer/hooks/useColumnOrder.tsx @@ -0,0 +1,40 @@ +import { useCallback } from 'react'; + +/** + * A custom hook to manage table column sorting order logic. + * It provides a function to update the order based on user interaction. + * The order cycles through 'ASC', 'DESC', and no order ('') when the same column is clicked. + * Clicking a new column sets the order to 'ASC'. + * + * @param {string} order The current sorting order. + * @param {Function} setOrder The function to update the sorting order. + * @returns {Function} The function to handle updating the order based on column clicks. + */ +function useColumnOrderHandler(order, setOrder) { + const handleSetOrder = useCallback((column) => { + const currentColumn = order.split(':')[0]; + const currentDirection = order.split(':')[1]; + + if (currentColumn === column) { + // Cycle through 'ASC', 'DESC', and no order ('') + switch (currentDirection) { + case 'ASC': + setOrder(`${column}:DESC`); + break; + case 'DESC': + setOrder(''); + break; + default: + setOrder(`${column}:ASC`); + break; + } + } else { + // Default to ascending order for a new column + setOrder(`${column}:ASC`); + } + }, [order, setOrder]); + + return handleSetOrder; +} + +export { useColumnOrderHandler }; diff --git a/src/renderer/hooks/useQueryParamState.tsx b/src/renderer/hooks/useQueryParamState.tsx index f6ae692e..5d382853 100644 --- a/src/renderer/hooks/useQueryParamState.tsx +++ b/src/renderer/hooks/useQueryParamState.tsx @@ -13,7 +13,7 @@ const useQueryParamState: QueryParamState = (name, defaultValue = '') => const location = useLocation(); const setQueryStringParameter = useCallback( - (value: string) => { + (value?: string) => { const params = new URLSearchParams(window.location.search || window.location.hash.replace(/#.*\?/, "")); if (_.isNil(value) || value === '') { diff --git a/src/renderer/pages/ProjectsList/ProjectsList.tsx b/src/renderer/pages/ProjectsList/ProjectsList.tsx index 2728eaaa..60d9277d 100644 --- a/src/renderer/pages/ProjectsList/ProjectsList.tsx +++ b/src/renderer/pages/ProjectsList/ProjectsList.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { useGetProjectsQuery } from '@/api'; -import { useQueryParamState } from '@/hooks'; +import { useQueryParamState, useColumnOrderHandler } from '@/hooks'; import { debounce } from 'lodash'; import { OrganizationSelector, @@ -15,13 +15,15 @@ const ProjectsList: React.FC = () => { const [currentPage, setCurrentPage] = useQueryParamState('page', '1'); const [orgUid, setOrgUid] = useQueryParamState('orgUid', undefined); const [search, setSearch] = useQueryParamState('search', undefined); + const [order, setOrder] = useQueryParamState('order', undefined); + const handleSetOrder = useColumnOrderHandler(order, setOrder); const { data: projectsData, isLoading: projectsLoading, isFetching: projectsFetching, error: projectsError, - } = useGetProjectsQuery({ page: Number(currentPage), orgUid, search }); + } = useGetProjectsQuery({ page: Number(currentPage), orgUid, search, order }); const handlePageChange = useCallback( debounce((page) => setCurrentPage(page), 800), @@ -42,6 +44,7 @@ const ProjectsList: React.FC = () => { [setSearch, debounce], ); + if (projectsLoading) { return ; } @@ -70,6 +73,8 @@ const ProjectsList: React.FC = () => { isLoading={projectsLoading} currentPage={Number(currentPage)} onPageChange={handlePageChange} + setOrder={handleSetOrder} + order={order} totalPages={projectsData.pageCount} totalCount={projectsData.pageCount * 10} /> diff --git a/src/renderer/pages/UnitsList/UnitsList.tsx b/src/renderer/pages/UnitsList/UnitsList.tsx index f9de327e..e47cfb18 100644 --- a/src/renderer/pages/UnitsList/UnitsList.tsx +++ b/src/renderer/pages/UnitsList/UnitsList.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { useGetUnitsQuery } from '@/api'; -import { useQueryParamState } from '@/hooks'; +import { useQueryParamState, useColumnOrderHandler } from '@/hooks'; import { debounce } from 'lodash'; import { OrganizationSelector, @@ -15,13 +15,15 @@ const UnitsList: React.FC = () => { const [currentPage, setCurrentPage] = useQueryParamState('page', '1'); const [orgUid, setOrgUid] = useQueryParamState('orgUid', undefined); const [search, setSearch] = useQueryParamState('search', undefined); + const [order, setOrder] = useQueryParamState('order', undefined); + const handleSetOrder = useColumnOrderHandler(order, setOrder); const { data: unitsData, isLoading: unitsLoading, isFetching: unitsFetching, error: unitsError, - } = useGetUnitsQuery({ page: Number(currentPage), orgUid, search }); + } = useGetUnitsQuery({ page: Number(currentPage), orgUid, search, order }); const handlePageChange = useCallback( debounce((page) => setCurrentPage(page), 800), @@ -70,6 +72,8 @@ const UnitsList: React.FC = () => { isLoading={unitsLoading} currentPage={Number(currentPage)} onPageChange={handlePageChange} + setOrder={handleSetOrder} + order={order} totalPages={unitsData.pageCount} totalCount={unitsData.pageCount * 10} /> diff --git a/src/renderer/routes/route-constants.ts b/src/renderer/routes/route-constants.ts index e3f3bb66..e3273dc6 100644 --- a/src/renderer/routes/route-constants.ts +++ b/src/renderer/routes/route-constants.ts @@ -1,4 +1,4 @@ export default { - PROJECTS_LIST: '/ProjectsList', - UNITS_LIST: '/UnitsList' + PROJECTS_LIST: '/projects', + UNITS_LIST: '/units' }; \ No newline at end of file