From aa32ce382239b182e6e87bb79705f623e16d2da0 Mon Sep 17 00:00:00 2001 From: casperiv0 <53900565+casperiv0@users.noreply.github.com> Date: Sat, 21 Oct 2023 11:38:19 +0200 Subject: [PATCH] feat: server sorting --- apps/api/package.json | 10 +++--- .../admin/values/values-controller.ts | 14 +++++++- .../src/components/shared/table/table.tsx | 27 ++++++++++++++-- .../src/hooks/shared/table/use-async-table.ts | 32 +++++++++++++++++-- .../src/hooks/shared/table/use-table-state.ts | 14 ++++++-- apps/client/src/pages/admin/values/[path].tsx | 7 ++++ pnpm-lock.yaml | 22 +++++++++++++ 7 files changed, 113 insertions(+), 13 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 8337378fa..e6bd81e94 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -24,6 +24,7 @@ "@types/express": "^4.17.19", "@types/glob": "^8.1.0", "@types/jsonwebtoken": "9.0.3", + "@types/lodash.set": "^4.3.8", "@types/multer": "^1.4.8", "@types/node": "^20.8.6", "@types/qrcode": "^1.5.2", @@ -36,9 +37,6 @@ "vitest": "^0.34.6" }, "dependencies": { - "multer": "^1.4.5-lts.1", - "ts-node": "^10.9.1", - "tslib": "^2.6.2", "@discordjs/rest": "^2.0.1", "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.4.2", @@ -78,15 +76,19 @@ "glob": "^10.3.10", "is-ip": "3.1.0", "jsonwebtoken": "9.0.2", + "lodash.set": "^4.3.2", + "multer": "^1.4.5-lts.1", "nanoid": "^3.3.4", "nodemon": "^3.0.1", "otplib": "^12.0.1", "prisma": "^5.4.2", "puppeteer": "^21.3.8", "qrcode": "^1.5.3", - "sharp": "^0.32.6", "reflect-metadata": "^0.1.13", + "sharp": "^0.32.6", "socket.io": "^4.7.2", + "ts-node": "^10.9.1", + "tslib": "^2.6.2", "undici": "^5.26.3", "use-intl": "^2.20.2", "zod": "^3.22.4" diff --git a/apps/api/src/controllers/admin/values/values-controller.ts b/apps/api/src/controllers/admin/values/values-controller.ts index da41891b0..9899b3696 100644 --- a/apps/api/src/controllers/admin/values/values-controller.ts +++ b/apps/api/src/controllers/admin/values/values-controller.ts @@ -32,6 +32,7 @@ import { validateSchema } from "lib/data/validate-schema"; import { createSearchWhereObject } from "lib/values/create-where-object"; import generateBlurPlaceholder from "lib/images/generate-image-blur-data"; import { AuditLogActionType, createAuditLogEntry } from "@snailycad/audit-logger/server"; +import set from "lodash.set"; export const GET_VALUES: Partial> = { QUALIFICATION: { @@ -74,6 +75,7 @@ export class ValuesController { @QueryParams("skip", Number) skip = 0, @QueryParams("query", String) query = "", @QueryParams("includeAll", Boolean) includeAll = true, + @QueryParams("sorting") sorting: string = "", ): Promise { // allow more paths in one request let paths = @@ -83,6 +85,16 @@ export class ValuesController { paths = validValuePaths.filter((v) => v !== "penal_code_group"); } + const orderBy = sorting.split(",").reduce((obj, cv) => { + const [key, sortOrder] = cv.split(":") as [string, "asc" | "desc"]; + + return set(obj, key, sortOrder); + }, {}); + + console.log({ + orderBy, + }); + const values = await Promise.all( paths.map(async (path) => { const type = getTypeFromPath(path) as ValueType; @@ -114,7 +126,7 @@ export class ValuesController { ...(type === "ADDRESS" ? {} : { _count: true }), value: true, }, - orderBy: { value: { position: "asc" } }, + orderBy: sorting ? orderBy : { value: { position: "asc" } }, take: includeAll ? undefined : 35, skip: includeAll ? undefined : skip, }), diff --git a/apps/client/src/components/shared/table/table.tsx b/apps/client/src/components/shared/table/table.tsx index d69c1102c..a391b0214 100644 --- a/apps/client/src/components/shared/table/table.tsx +++ b/apps/client/src/components/shared/table/table.tsx @@ -73,8 +73,28 @@ export function Table({ cols.unshift(createTableDragDropColumn(tableState.dragDrop)); } + if (tableState.sorting.useServerSorting) { + const disabledSorting = cols.map((col) => { + const accessorKey = (col as { accessorKey: string }).accessorKey; + + return { + ...col, + enableSorting: Boolean(tableState.sorting.sortingSchema?.[accessorKey]), + }; + }); + return disabledSorting; + } + return cols; - }, [columns, tableActionsAlignment, features?.dragAndDrop, tableState.dragDrop?.disabledIndices]); // eslint-disable-line + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + columns, + tableActionsAlignment, + tableState.sorting.sortingSchema, + tableState.sorting.useServerSorting, + features?.dragAndDrop, + tableState.dragDrop?.disabledIndices, + ]); const table = useReactTable({ data, @@ -83,18 +103,19 @@ export function Table({ enableRowSelection: true, enableSorting: true, manualPagination: true, + manualSorting: tableState.sorting.useServerSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), - onSortingChange: tableState.setSorting, + onSortingChange: tableState.sorting.setSorting, onRowSelectionChange: tableState.setRowSelection, onPaginationChange: tableState.setPagination, onColumnVisibilityChange: tableState.setColumnVisibility, state: { - sorting: tableState.sorting, + sorting: tableState.sorting.sorting, rowSelection: tableState.rowSelection, pagination: tableState.pagination, columnVisibility: tableState.columnVisibility, diff --git a/apps/client/src/hooks/shared/table/use-async-table.ts b/apps/client/src/hooks/shared/table/use-async-table.ts index 18c61ddec..b7f54ed51 100644 --- a/apps/client/src/hooks/shared/table/use-async-table.ts +++ b/apps/client/src/hooks/shared/table/use-async-table.ts @@ -3,6 +3,7 @@ import useFetch from "lib/useFetch"; import { useDebounce } from "react-use"; import { useQuery, type QueryFunctionContext } from "@tanstack/react-query"; import { useList } from "./use-list"; +import type { SortingState } from "@tanstack/react-table"; interface FetchOptions { pageSize?: number; @@ -16,6 +17,7 @@ interface FetchOptions { interface Options { search?: string; + sortingSchema?: Record; disabled?: boolean; totalCount?: number; @@ -35,6 +37,7 @@ export function useAsyncTable(options: Options) { }); const { state: loadingState, execute } = useFetch(); + const [sorting, setSorting] = React.useState([]); const [debouncedSearch, setDebouncedSearch] = React.useState(options.search); const [filters, setFilters] = React.useState | null>(null); const [paginationOptions, setPagination] = React.useState({ @@ -47,7 +50,13 @@ export function useAsyncTable(options: Options) { enabled: !options.disabled, initialData: options.initialData ?? undefined, queryFn: fetchData, - queryKey: [paginationOptions.pageIndex, debouncedSearch, filters, options.fetchOptions.path], + queryKey: [ + paginationOptions.pageIndex, + debouncedSearch, + sorting, + filters, + options.fetchOptions.path, + ], refetchOnMount: options.fetchOptions.refetchOnMount, refetchOnWindowFocus: options.fetchOptions.refetchOnWindowFocus, }); @@ -68,13 +77,25 @@ export function useAsyncTable(options: Options) { }, [options.initialData]); // eslint-disable-line react-hooks/exhaustive-deps async function fetchData(context: QueryFunctionContext) { - const [pageIndex, search, _filters] = context.queryKey; + const [pageIndex, search, _sorting, _filters] = context.queryKey; const path = options.fetchOptions.path; const skip = Number(pageIndex * paginationOptions.pageSize) || 0; const filters = _filters || {}; + const sorting = []; const searchParams = new URLSearchParams(); + for (const sort of _sorting as SortingState) { + const key = options.sortingSchema?.[sort.id]; + if (!key) continue; + + sorting.push(`${key}:${sort.desc ? "desc" : "asc"}`); + } + + if (sorting.length > 0) { + searchParams.append("sorting", sorting.join(",")); + } + filters.query = search; filters.skip = skip; @@ -123,8 +144,15 @@ export function useAsyncTable(options: Options) { ...paginationOptions, } as const; + const sortingState = { + sorting, + setSorting, + sortingSchema: options.sortingSchema, + } as const; + return { ...list, + sorting: sortingState, noItemsAvailable: !isInitialLoading && !error && list.items.length <= 0, isInitialLoading, filters, diff --git a/apps/client/src/hooks/shared/table/use-table-state.ts b/apps/client/src/hooks/shared/table/use-table-state.ts index 866743a33..1edb7508b 100644 --- a/apps/client/src/hooks/shared/table/use-table-state.ts +++ b/apps/client/src/hooks/shared/table/use-table-state.ts @@ -8,6 +8,7 @@ interface TableStateOptions { onListChange(list: any[]): void; disabledIndices?: number[]; }; + sorting?: Partial["sorting"]>; pagination?: Partial["pagination"]>; defaultHiddenColumns?: string[]; tableId?: string; @@ -17,6 +18,7 @@ export function useTableState({ pagination, dragDrop, tableId, + sorting, defaultHiddenColumns, }: TableStateOptions = {}) { const isMounted = useMounted(); @@ -51,7 +53,7 @@ export function useTableState({ } }, [columnVisibility, tableId]); - const [sorting, setSorting] = React.useState([]); + const [regularSorting, setRegularSorting] = React.useState([]); const [rowSelection, setRowSelection] = React.useState({}); const _pagination = { @@ -63,10 +65,16 @@ export function useTableState({ error: pagination?.error, }; + const _sorting = { + sorting: sorting?.sorting ?? regularSorting, + setSorting: sorting?.setSorting ?? setRegularSorting, + useServerSorting: Boolean(sorting?.sorting), + sortingSchema: sorting?.sortingSchema, + }; + return { tableId, - sorting, - setSorting, + sorting: _sorting, rowSelection, setRowSelection, pagination: _pagination, diff --git a/apps/client/src/pages/admin/values/[path].tsx b/apps/client/src/pages/admin/values/[path].tsx index 512aaa0eb..83e8038cf 100644 --- a/apps/client/src/pages/admin/values/[path].tsx +++ b/apps/client/src/pages/admin/values/[path].tsx @@ -87,6 +87,12 @@ export default function ValuePath({ pathValues: { totalCount, type, values: data const [search, setSearch] = React.useState(""); const asyncTable = useAsyncTable({ search, + sortingSchema: { + value: "value.value", + gameHash: "hash", + isDisabled: "value.isDisabled", + createdAt: "value.createdAt", + }, fetchOptions: { onResponse(json: GetValuesData) { const [forType] = json; @@ -112,6 +118,7 @@ export default function ValuePath({ pathValues: { totalCount, type, values: data const extraTableHeaders = useTableHeadersOfType(type); const extraTableData = useTableDataOfType(type); const tableState = useTableState({ + sorting: asyncTable.sorting, pagination: asyncTable.pagination, dragDrop: { onListChange: setList }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0dc766e3..8d0b8b67b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,6 +176,9 @@ importers: jsonwebtoken: specifier: 9.0.2 version: 9.0.2 + lodash.set: + specifier: ^4.3.2 + version: 4.3.2 multer: specifier: ^1.4.5-lts.1 version: 1.4.5-lts.1 @@ -258,6 +261,9 @@ importers: "@types/jsonwebtoken": specifier: 9.0.3 version: 9.0.3 + "@types/lodash.set": + specifier: ^4.3.8 + version: 4.3.8 "@types/multer": specifier: ^1.4.8 version: 1.4.8 @@ -10489,6 +10495,15 @@ packages: "@types/geojson": 7946.0.10 dev: false + /@types/lodash.set@4.3.8: + resolution: + { + integrity: sha512-WYIWnVO5xkcEKehhZf0Whrf9wj9D1AuaGTpwT/mCEJXKgdC2UWcMpvRqJahKQNhnOjmGEhpUqbYNJ6gUgdGSQw==, + } + dependencies: + "@types/lodash": 4.14.200 + dev: true + /@types/lodash@4.14.200: resolution: { @@ -19610,6 +19625,13 @@ packages: } dev: false + /lodash.set@4.3.2: + resolution: + { + integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==, + } + dev: false + /lodash.sortby@4.7.0: resolution: {