From 72a6b0a4b407808d914a9b1026803a969204376a Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 8 Feb 2024 19:24:46 +0200 Subject: [PATCH] finish reimplementation of activity table as a client component --- .../_components/ActivityFeed/ActivityFeed.tsx | 4 +- .../ActivityFeed/ActivityFeedTable.tsx | 5 +-- ...ms.ts => useTableStateFromSearchParams.ts} | 3 +- .../_components/ActivityFeed/utils.ts | 18 ++++----- components/data-table/data-table.tsx | 2 +- hooks/use-data-table.tsx | 38 +++++++++++++++---- lib/data-table/types.ts | 7 +++- package.json | 1 + pnpm-lock.yaml | 14 ++++++- server/routers/dashboard.ts | 20 ++++------ tsconfig.json | 1 + 11 files changed, 73 insertions(+), 40 deletions(-) rename app/(dashboard)/dashboard/_components/ActivityFeed/{useTableSearchParams.ts => useTableStateFromSearchParams.ts} (95%) diff --git a/app/(dashboard)/dashboard/_components/ActivityFeed/ActivityFeed.tsx b/app/(dashboard)/dashboard/_components/ActivityFeed/ActivityFeed.tsx index 4ec47ad3..69097dba 100644 --- a/app/(dashboard)/dashboard/_components/ActivityFeed/ActivityFeed.tsx +++ b/app/(dashboard)/dashboard/_components/ActivityFeed/ActivityFeed.tsx @@ -4,10 +4,10 @@ import { DataTableSkeleton } from '~/components/data-table/data-table-skeleton'; import ActivityFeedTable from './ActivityFeedTable'; import { unstable_noStore } from 'next/cache'; import { api } from '~/trpc/client'; -import { useSearchParamsTableState } from './useTableSearchParams'; +import { useTableStateFromSearchParams } from './useTableStateFromSearchParams'; export const ActivityFeed = () => { - const { searchParams } = useSearchParamsTableState(); + const { searchParams } = useTableStateFromSearchParams(); unstable_noStore(); const tableQuery = api.dashboard.getActivities.useQuery(searchParams); diff --git a/app/(dashboard)/dashboard/_components/ActivityFeed/ActivityFeedTable.tsx b/app/(dashboard)/dashboard/_components/ActivityFeed/ActivityFeedTable.tsx index 9dede6ec..5754856b 100644 --- a/app/(dashboard)/dashboard/_components/ActivityFeed/ActivityFeedTable.tsx +++ b/app/(dashboard)/dashboard/_components/ActivityFeed/ActivityFeedTable.tsx @@ -42,10 +42,7 @@ export default function ActivityFeedTable({ searchableColumns={searchableColumns} filterableColumns={filterableColumns} // floatingBarContent={TasksTableFloatingBarContent(dataTable)} - deleteRowsAction={(_event) => { - // eslint-disable-next-line no-console - console.log('deleteSelectedrows, dataTable, event'); - }} + // deleteRowsAction={(_event) => {}} /> ); } diff --git a/app/(dashboard)/dashboard/_components/ActivityFeed/useTableSearchParams.ts b/app/(dashboard)/dashboard/_components/ActivityFeed/useTableStateFromSearchParams.ts similarity index 95% rename from app/(dashboard)/dashboard/_components/ActivityFeed/useTableSearchParams.ts rename to app/(dashboard)/dashboard/_components/ActivityFeed/useTableStateFromSearchParams.ts index 057570ea..e15055e7 100644 --- a/app/(dashboard)/dashboard/_components/ActivityFeed/useTableSearchParams.ts +++ b/app/(dashboard)/dashboard/_components/ActivityFeed/useTableStateFromSearchParams.ts @@ -2,6 +2,7 @@ import { FilterParam, pageSizes, + searchableFields, sortOrder, sortableFields, } from '~/lib/data-table/types'; @@ -23,7 +24,7 @@ import { * ways, such as in localStorage, in the URL, or even in a database. * */ -export const useSearchParamsTableState = () => { +export const useTableStateFromSearchParams = () => { const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1)); const [perPage, setPerPage] = useQueryState( 'per_page', diff --git a/app/(dashboard)/dashboard/_components/ActivityFeed/utils.ts b/app/(dashboard)/dashboard/_components/ActivityFeed/utils.ts index 0f0c572c..101724c4 100644 --- a/app/(dashboard)/dashboard/_components/ActivityFeed/utils.ts +++ b/app/(dashboard)/dashboard/_components/ActivityFeed/utils.ts @@ -30,22 +30,22 @@ const generateMessageForActivityType = (type: ActivityType) => { }; export const getBadgeColorsForActivityType = (type: ActivityType) => { - switch (type) { - case 'Protocol Installed': + switch (type.toLowerCase()) { + case 'protocol installed': return 'bg-slate-blue hover:bg-slate-blue-dark'; - case 'Protocol Uninstalled': + case 'protocol uninstalled': return 'bg-neon-carrot hover:bg-neon-carrot-dark'; - case 'Participant(s) Added': + case 'participant(s) added': return 'bg-sea-green hover:bg-sea-green'; - case 'Participant(s) Removed': + case 'participant(s) removed': return 'bg-tomato hover:bg-tomato-dark'; - case 'Interview Started': + case 'interview started': return 'bg-sea-serpent hover:bg-sea-serpent-dark'; - case 'Interview Completed': + case 'interview completed': return 'bg-purple-pizazz hover:bg-purple-pizazz-dark'; - case 'Interview(s) Deleted': + case 'interview(s) deleted': return 'bg-paradise-pink hover:bg-paradise-pink-dark'; - case 'Data Exported': + case 'data exported': return 'bg-kiwi hover:bg-kiwi-dark'; } }; diff --git a/components/data-table/data-table.tsx b/components/data-table/data-table.tsx index efe271f1..8301e6af 100644 --- a/components/data-table/data-table.tsx +++ b/components/data-table/data-table.tsx @@ -76,7 +76,7 @@ type DataTableProps = { * @example deleteRowsAction={(event) => deleteSelectedRows(dataTable, event)} */ deleteRowsAction?: React.MouseEventHandler; -} +}; export function DataTable({ dataTable, diff --git a/hooks/use-data-table.tsx b/hooks/use-data-table.tsx index aad2dabd..765c5373 100644 --- a/hooks/use-data-table.tsx +++ b/hooks/use-data-table.tsx @@ -21,7 +21,9 @@ import type { PageSize, SortableField, } from '~/lib/data-table/types'; -import { useSearchParamsTableState } from '~/app/(dashboard)/dashboard/_components/ActivityFeed/useTableSearchParams'; + +import { useTableStateFromSearchParams } from '~/app/(dashboard)/dashboard/_components/ActivityFeed/useTableStateFromSearchParams'; +import { debounce } from 'lodash'; type UseDataTableProps = { /** @@ -68,16 +70,14 @@ export function useDataTable({ searchableColumns = [], filterableColumns = [], }: UseDataTableProps) { - const { searchParams, setSearchParams } = useSearchParamsTableState(); - - const filterableColumnIds = filterableColumns.map((column) => column.id); + const { searchParams, setSearchParams } = useTableStateFromSearchParams(); // Table states const [rowSelection, setRowSelection] = React.useState({}); const [columnVisibility, setColumnVisibility] = React.useState({}); const [columnFilters, setColumnFilters] = React.useState( - searchParams.filterParams ?? [], + (searchParams.filterParams as ColumnFiltersState) ?? [], ); const [{ pageIndex, pageSize }, setPagination] = @@ -94,14 +94,38 @@ export function useDataTable({ [pageIndex, pageSize], ); + const debouncedUpdateFilterParams = debounce( + (columnFilters: ColumnFiltersState) => { + if (!columnFilters || columnFilters.length === 0) { + void setSearchParams.setFilterParams(null); + return; + } + + void setSearchParams.setFilterParams(columnFilters); + // Changing the filter params should reset the page to 1 + // void setSearchParams.setPage(1); + }, + 2000, + { + trailing: true, + leading: false, + }, + ); + // Sync any changes to columnFilters back to searchParams React.useEffect(() => { + // If we are resetting, skip the debounce if (!columnFilters || columnFilters.length === 0) { void setSearchParams.setFilterParams(null); return; } - void setSearchParams.setFilterParams(columnFilters); + debouncedUpdateFilterParams(columnFilters); + + // Changing the filter params should reset the page to 1 + void setSearchParams.setPage(1); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [columnFilters]); React.useEffect(() => { @@ -131,8 +155,6 @@ export function useDataTable({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [sorting]); - console.log({ columnFilters, filterableColumns, searchableColumns }); - const dataTable = useReactTable({ data, columns, diff --git a/lib/data-table/types.ts b/lib/data-table/types.ts index ff1ea867..4ff33975 100644 --- a/lib/data-table/types.ts +++ b/lib/data-table/types.ts @@ -72,15 +72,18 @@ export const sortableFields = [ 'type', 'message', ] as const; - export type SortableField = (typeof sortableFields)[number]; +// As above, this should be derivable from the column definition... +export const searchableFields = ['message'] as const; +export type SearchableField = (typeof searchableFields)[number]; + export const pageSizes = [10, 20, 50, 100] as const; export type PageSize = (typeof pageSizes)[number]; export const FilterParam = z.object({ id: z.string(), - value: z.array(z.string()), + value: z.union([z.string(), z.array(z.string())]), }); export const SearchParamsSchema = z.object({ diff --git a/package.json b/package.json index bb1c5201..bfbad189 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "tailwindcss-animate": "^1.0.7", "ts-node": "^10.9.1", "uploadthing": "^5.7.4", + "usehooks-ts": "^2.13.0", "uuid": "^9.0.1", "validator": "^13.11.0", "zod": "^3.22.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e75ea7a2..50aae07d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,6 +296,9 @@ dependencies: uploadthing: specifier: ^5.7.4 version: 5.7.4 + usehooks-ts: + specifier: ^2.13.0 + version: 2.13.0(react@18.2.0) uuid: specifier: ^9.0.1 version: 9.0.1 @@ -11118,7 +11121,6 @@ packages: /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - dev: true /lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} @@ -15061,6 +15063,16 @@ packages: react: 18.2.0 dev: false + /usehooks-ts@2.13.0(react@18.2.0): + resolution: {integrity: sha512-3yZ2dwbc5BTaejBvamckAqv/q29sKFeqNCdi1z9Im4GzsTT5XbdIdbB94KEoh9xSvZy/qRoztjR14pF5voEU5Q==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + lodash.debounce: 4.0.8 + react: 18.2.0 + dev: false + /utf8-byte-length@1.0.4: resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==} dev: false diff --git a/server/routers/dashboard.ts b/server/routers/dashboard.ts index d92b5bbb..bccd6cd7 100644 --- a/server/routers/dashboard.ts +++ b/server/routers/dashboard.ts @@ -23,22 +23,18 @@ export const dashboardRouter = router({ .query(async ({ input }) => { const { page, perPage, sort, sortField, filterParams } = input; - console.log(input); - - // Fallback page for invalid page numbers - const pageAsNumber = Number(page); - const fallbackPage = - isNaN(pageAsNumber) || pageAsNumber < 1 ? 1 : pageAsNumber; - // Number of items to skip - const offset = fallbackPage > 0 ? (fallbackPage - 1) * perPage : 0; + const offset = page > 0 ? (page - 1) * perPage : 0; const queryFilterParams = filterParams ? { - AND: [ - ...filterParams.map(({ id, value }) => ({ - [id]: { in: value }, - })), + OR: [ + ...filterParams.map(({ id, value }) => { + const operator = Array.isArray(value) ? 'in' : 'contains'; + return { + [id]: { [operator]: value }, + }; + }), ], } : {}; diff --git a/tsconfig.json b/tsconfig.json index f7078e91..46b0de02 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "checkJs": false, "skipLibCheck": true, "strict": true, + "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true,