From 695fed2a0c484628a150eb2193eb3728b39457ea Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Mon, 16 Dec 2024 09:53:28 +0100 Subject: [PATCH 01/11] Add custom `useQueryParams` hook It's built on top of `useSearchParams` from `react-router`. --- .../src/app/utils/useQueryParams.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx diff --git a/apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx b/apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx new file mode 100644 index 000000000..08e2f024b --- /dev/null +++ b/apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx @@ -0,0 +1,69 @@ +import t from 'io-ts'; +import { type NavigateOptions, useSearchParams } from 'react-router'; +import { isRight } from 'fp-ts/lib/Either'; + +type Props = { + codec: t.Type; + initialValues: T; +}; + +function toURLSearchParams(params: T): URLSearchParams { + const searchParams = new URLSearchParams(); + for (const key in params) { + if (params[key] !== undefined) { + searchParams.set(key, String(params[key])); + } + } + return searchParams; +} + +function useQueryParams>({ + codec, + initialValues, +}: Props) { + const stringifiedInitialValues: Record = {}; + for (const key in initialValues) { + stringifiedInitialValues[key] = String(initialValues[key]); + } + + const [searchParams, setSearchParams] = useSearchParams( + stringifiedInitialValues + ); + + const decodeParams = (params: URLSearchParams): T => { + const obj: Record = {}; + for (const [key, value] of params) { + if (value !== undefined) { + obj[key] = value; + } + } + + const result = codec.decode(obj); + + if (!isRight(result)) { + console.error('Invalid query params:', result); + console.warn('Reverting back to initial values...'); + return initialValues; + } + + return result.right; + }; + + const setValidatedSearchParams = ( + newParams: T, + flushSync?: NavigateOptions['flushSync'] + ) => { + const result = codec.decode(newParams); + + if (!isRight(result)) { + console.error('Invalid parameters to set:', result.left); + return; + } + const validatedParams = toURLSearchParams(newParams); + setSearchParams(validatedParams, { flushSync }); + }; + + return [decodeParams(searchParams), setValidatedSearchParams] as const; +} + +export default useQueryParams; From 3e81ba1843dd2894288069c292cc95b50340698d Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Mon, 16 Dec 2024 09:55:38 +0100 Subject: [PATCH 02/11] Add codecs for validation --- apps/hpc-ftsadmin/src/app/utils/codecs.ts | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 apps/hpc-ftsadmin/src/app/utils/codecs.ts diff --git a/apps/hpc-ftsadmin/src/app/utils/codecs.ts b/apps/hpc-ftsadmin/src/app/utils/codecs.ts new file mode 100644 index 000000000..ad8d58cfb --- /dev/null +++ b/apps/hpc-ftsadmin/src/app/utils/codecs.ts @@ -0,0 +1,65 @@ +import * as t from 'io-ts'; +import { util } from '@unocha/hpc-data'; +import { + DEFAULT_FLOW_TABLE_HEADERS, + DEFAULT_KEYWORD_TABLE_HEADERS, + DEFAULT_ORGANIZATION_TABLE_HEADERS, + FlowHeaderID, + KeywordHeaderID, + OrganizationHeaderID, + TableHeadersProps, +} from './table-headers'; + +const PARAMS_CODEC = t.intersection([ + t.type({ + page: util.INTEGER_FROM_STRING, + rowsPerPage: util.INTEGER_FROM_STRING, + orderDir: t.keyof({ + ASC: 'ASC', + DESC: 'DESC', + }), + filters: t.string, + tableHeaders: t.string, + }), + t.partial({ + prevPageCursor: util.INTEGER_FROM_STRING, + nextPageCursor: util.INTEGER_FROM_STRING, + }), +]); + +const extractIdentifierIds = < + T extends OrganizationHeaderID | FlowHeaderID | KeywordHeaderID, +>( + val: TableHeadersProps[] +) => { + return val.reduce( + (acc, { identifierID: id }) => { + acc[id] = id; + return acc; + }, + {} as Record + ); +}; + +export const FLOW_PARAMS_CODEC = t.intersection([ + PARAMS_CODEC, + t.type({ + orderBy: t.keyof(extractIdentifierIds(DEFAULT_FLOW_TABLE_HEADERS)), + }), +]); + +export const ORGANIZATION_PARAMS_CODEC = t.intersection([ + PARAMS_CODEC, + t.type({ + orderBy: t.keyof(extractIdentifierIds(DEFAULT_ORGANIZATION_TABLE_HEADERS)), + }), +]); + +export const KEYWORD_PARAMS_CODEC = t.type({ + orderBy: t.keyof(extractIdentifierIds(DEFAULT_KEYWORD_TABLE_HEADERS)), + orderDir: t.keyof({ + ASC: 'ASC', + DESC: 'DESC', + }), + tableHeaders: t.string, +}); From 9f5606f0f67b03663cd20d753d876aa43865e92a Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Mon, 16 Dec 2024 09:56:09 +0100 Subject: [PATCH 03/11] Replace all usages of `use-query-params` for new `useQueryParams` --- .../components/filters/filter-flows-table.tsx | 19 ++++--- .../filters/filter-organization-table.tsx | 19 ++++--- .../filters/filter-pending-flows-table.tsx | 6 +- .../src/app/components/tables/flows-table.tsx | 7 ++- .../app/components/tables/keywords-table.tsx | 14 ++--- .../components/tables/organizations-table.tsx | 7 ++- .../src/app/components/tables/table-utils.tsx | 35 ++++++++++-- .../src/app/pages/flows/flows-list.tsx | 55 +++++-------------- .../app/pages/flows/pending-flows-list.tsx | 54 +++++------------- .../src/app/pages/keywords/keyword-list.tsx | 29 +++------- .../pages/organizations/organization-list.tsx | 53 ++++-------------- .../src/app/utils/table-headers.ts | 14 ++--- 12 files changed, 120 insertions(+), 192 deletions(-) diff --git a/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx b/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx index 4ae5acc9f..080c31aa1 100644 --- a/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx @@ -9,7 +9,7 @@ import { t } from '../../../i18n'; import { LocalStorageSchema } from '../../utils/local-storage-type'; import { util } from '@unocha/hpc-core'; import { Alert } from '@mui/material'; -import { Query } from '../tables/table-utils'; +import type { FlowQuery, SetQuery } from '../tables/table-utils'; import { AppContext } from '../../context'; import { util as codecs, FormObjectValue } from '@unocha/hpc-data'; import validateForm from '../../utils/form-validation'; @@ -25,8 +25,8 @@ import { } from '../../utils/fn-promises'; interface Props { - query: Query; - setQuery: (newQuery: Query) => void; + query: FlowQuery; + setQuery: SetQuery; handleAbortController: () => void; } export interface FlowsFilterValues { @@ -136,11 +136,14 @@ export const FilterFlowsTable = (props: Props) => { if (query.filters !== encodedFilters) { handleAbortController(); } - setQuery({ - ...query, - page: 0, - filters: encodedFilters, - }); + setQuery( + { + ...query, + page: 0, + filters: encodedFilters, + }, + true + ); }; return ( s.components.flowsFilter.title)}> diff --git a/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx b/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx index 276d2c62d..d4407f0b3 100644 --- a/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx @@ -7,7 +7,7 @@ import { Environment } from '../../../environments/interface'; import { decodeFilters, encodeFilters } from '../../utils/parse-filters'; import { LanguageKey, t } from '../../../i18n'; import { Dayjs } from 'dayjs'; -import { Query } from '../tables/table-utils'; +import type { OrganizationQuery, SetQuery } from '../tables/table-utils'; import * as io from 'io-ts'; import validateForm from '../../utils/form-validation'; import { @@ -17,8 +17,8 @@ import { } from '../../utils/fn-promises'; interface Props { environment: Environment; - query: Query; - setQuery: (newQuery: Query) => void; + query: OrganizationQuery; + setQuery: SetQuery; lang: LanguageKey; } export interface OrganizationFilterValues { @@ -69,11 +69,14 @@ export const FilterOrganizationsTable = (props: Props) => { ) => void ) => { formikResetForm(); - setQuery({ - ...query, - page: 0, - filters: encodeFilters({}, ORGANIZATIONS_FILTER_INITIAL_VALUES), - }); + setQuery( + { + ...query, + page: 0, + filters: encodeFilters({}, ORGANIZATIONS_FILTER_INITIAL_VALUES), + }, + true + ); }; return ( void; + query: FlowQuery; + setQuery: SetQuery; handleAbortController: () => void; } export interface PendingFlowsFilterValues { diff --git a/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx b/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx index 932e9b06f..2976e6dd6 100644 --- a/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx @@ -45,7 +45,8 @@ import { downloadExcel } from '../../utils/download-excel'; import DownloadIcon from '@mui/icons-material/Download'; import { ChipDiv, - Query, + type FlowQuery, + type SetQuery, RejectPendingFlowsButton, RenderChipsRow, StyledLoader, @@ -64,8 +65,8 @@ export interface FlowsTableProps { headers: TableHeadersProps[]; initialValues: FlowsFilterValues | PendingFlowsFilterValues; rowsPerPageOption: number[]; - query: Query; - setQuery: (newQuery: Query) => void; + query: FlowQuery; + setQuery: SetQuery; abortSignal?: AbortSignal; pending?: boolean; } diff --git a/apps/hpc-ftsadmin/src/app/components/tables/keywords-table.tsx b/apps/hpc-ftsadmin/src/app/components/tables/keywords-table.tsx index c6ea549d5..0dfc67909 100644 --- a/apps/hpc-ftsadmin/src/app/components/tables/keywords-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/tables/keywords-table.tsx @@ -37,7 +37,8 @@ import { import { ChipDiv, - Query, + type KeywordQuery, + type SetQuery, StyledLoader, TableHeaderButton, TopRowContainer, @@ -51,15 +52,10 @@ import { LocalStorageSchema } from '../../utils/local-storage-type'; import { Strings } from '../../../i18n/iface'; import { parseError } from '../../utils/map-functions'; -export type KeywordQuery = { - orderBy: string; - orderDir: string; - tableHeaders: string; -}; export interface KeywordTableProps { headers: TableHeadersProps[]; query: KeywordQuery; - setQuery: (newQuery: KeywordQuery) => void; + setQuery: SetQuery; } /** @@ -507,7 +503,7 @@ export default function KeywordTable(props: KeywordTableProps) { query.tableHeaders, lang, 'keywords', - query as Query, + query, setQuery )} onClick={(element) => { @@ -517,7 +513,7 @@ export default function KeywordTable(props: KeywordTableProps) { tableHeaders: encodeTableHeaders( element, 'keywords', - query as Query, + query, setQuery ), }); diff --git a/apps/hpc-ftsadmin/src/app/components/tables/organizations-table.tsx b/apps/hpc-ftsadmin/src/app/components/tables/organizations-table.tsx index b45105148..992abecf6 100644 --- a/apps/hpc-ftsadmin/src/app/components/tables/organizations-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/tables/organizations-table.tsx @@ -41,7 +41,8 @@ import { parseUpdatedCreatedBy } from '../../utils/map-functions'; import { OrganizationFilterValues } from '../filters/filter-organization-table'; import { ChipDiv, - Query, + type OrganizationQuery, + type SetQuery, RenderChipsRow, StyledLoader, TableHeaderButton, @@ -59,8 +60,8 @@ export interface OrganizationTableProps { headers: TableHeadersProps[]; initialValues: OrganizationFilterValues; rowsPerPageOption: number[]; - query: Query; - setQuery: (newQuery: Query) => void; + query: OrganizationQuery; + setQuery: SetQuery; } export default function OrganizationTable(props: OrganizationTableProps) { diff --git a/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx b/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx index b40204e1f..b521ce81c 100644 --- a/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx +++ b/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx @@ -7,18 +7,43 @@ import CancelRoundedIcon from '@mui/icons-material/CancelRounded'; import { C } from '@unocha/hpc-ui'; import { util } from '@unocha/hpc-core'; import { LocalStorageSchema } from '../../utils/local-storage-type'; +import type { + FlowHeaderID, + KeywordHeaderID, + OrganizationHeaderID, +} from '../../utils/table-headers'; +import type { NavigateOptions } from 'react-router'; export type Query = { - page: number; - rowsPerPage: number; - orderBy: string; - orderDir: string; - filters: string; + orderDir: 'ASC' | 'DESC'; tableHeaders: string; prevPageCursor?: number; nextPageCursor?: number; }; +export type FlowQuery = Query & { + page: number; + rowsPerPage: number; + orderBy: FlowHeaderID; + filters: string; +}; + +export type OrganizationQuery = Query & { + page: number; + rowsPerPage: number; + orderBy: OrganizationHeaderID; + filters: string; +}; + +export type KeywordQuery = Query & { + orderBy: KeywordHeaderID; +}; + +export type SetQuery = ( + newQuery: T, + flushSync?: NavigateOptions['flushSync'] +) => void; + export const StyledLoader = tw(C.Loader)` mx-auto `; diff --git a/apps/hpc-ftsadmin/src/app/pages/flows/flows-list.tsx b/apps/hpc-ftsadmin/src/app/pages/flows/flows-list.tsx index 10133ae6e..fb5a0d1b5 100644 --- a/apps/hpc-ftsadmin/src/app/pages/flows/flows-list.tsx +++ b/apps/hpc-ftsadmin/src/app/pages/flows/flows-list.tsx @@ -3,16 +3,6 @@ import { t } from '../../../i18n'; import PageMeta from '../../components/page-meta'; import { AppContext } from '../../context'; import tw from 'twin.macro'; - -import { - JsonParam, - NumberParam, - StringParam, - createEnumParam, - decodeNumber, - useQueryParams, - withDefault, -} from 'use-query-params'; import { DEFAULT_FLOW_TABLE_HEADERS, encodeTableHeaders, @@ -24,6 +14,8 @@ import FilterFlowsTable, { FLOWS_FILTER_INITIAL_VALUES, } from '../../components/filters/filter-flows-table'; import { useCallback, useEffect, useRef } from 'react'; +import useQueryParams from '../../utils/useQueryParams'; +import { FLOW_PARAMS_CODEC } from '../../utils/codecs'; interface Props { className?: string; @@ -51,44 +43,23 @@ export default (props: Props) => { }, []); const [query, setQuery] = useQueryParams({ - page: withDefault(NumberParam, 0), - rowsPerPage: withDefault( - { - ...NumberParam, - decode: (string) => { - // Prevent user requesting more than max number of rows - const number = decodeNumber(string); - return number && Math.min(number, Math.max(...rowsPerPageOptions)); - }, - }, - 50 - ), - orderBy: withDefault( - createEnumParam( - // Same as filter then map but this is acceptable to typescript - DEFAULT_FLOW_TABLE_HEADERS.reduce((acc, curr) => { - if (curr.sortable) { - return [...acc, curr.identifierID]; - } - - return acc; - }, [] as string[]) - ), - 'flow.updatedAt' - ), - orderDir: withDefault(createEnumParam(['ASC', 'DESC']), 'DESC'), - filters: withDefault(JsonParam, JSON.stringify({})), - tableHeaders: withDefault(StringParam, encodeTableHeaders([])), // Default value of table headers - prevPageCursor: withDefault(NumberParam, undefined), - nextPageCursor: withDefault(NumberParam, undefined), + codec: FLOW_PARAMS_CODEC, + initialValues: { + page: 0, + rowsPerPage: 50, + orderBy: 'flow.updatedAt', + orderDir: 'DESC', + filters: JSON.stringify({}), + tableHeaders: encodeTableHeaders([]), // Default value of table headers + }, }); const flowsTableProps: FlowsTableProps = { headers: DEFAULT_FLOW_TABLE_HEADERS, rowsPerPageOption: rowsPerPageOptions, initialValues: FLOWS_FILTER_INITIAL_VALUES, - query: query, - setQuery: setQuery, + query, + setQuery, abortSignal: abortControllerRef.current.signal, }; diff --git a/apps/hpc-ftsadmin/src/app/pages/flows/pending-flows-list.tsx b/apps/hpc-ftsadmin/src/app/pages/flows/pending-flows-list.tsx index 7442ddcc7..7bb8138cd 100644 --- a/apps/hpc-ftsadmin/src/app/pages/flows/pending-flows-list.tsx +++ b/apps/hpc-ftsadmin/src/app/pages/flows/pending-flows-list.tsx @@ -2,15 +2,6 @@ import { C, CLASSES, combineClasses } from '@unocha/hpc-ui'; import { t } from '../../../i18n'; import PageMeta from '../../components/page-meta'; import { AppContext } from '../../context'; -import { - JsonParam, - NumberParam, - StringParam, - createEnumParam, - decodeNumber, - useQueryParams, - withDefault, -} from 'use-query-params'; import FilterPendingFlowsTable, { PENDING_FLOWS_FILTER_INITIAL_VALUES, } from '../../components/filters/filter-pending-flows-table'; @@ -23,6 +14,8 @@ import FlowsTable, { FlowsTableProps, } from '../../components/tables/flows-table'; import { useCallback, useEffect, useRef } from 'react'; +import useQueryParams from '../../utils/useQueryParams'; +import { FLOW_PARAMS_CODEC } from '../../utils/codecs'; interface Props { className?: string; @@ -38,36 +31,15 @@ export default (props: Props) => { const abortControllerRef = useRef(new AbortController()); const [query, setQuery] = useQueryParams({ - page: withDefault(NumberParam, 0), - rowsPerPage: withDefault( - { - ...NumberParam, - decode: (string) => { - // Prevent user requesting more than max number of rows - const number = decodeNumber(string); - return number && Math.min(number, Math.max(...[10, 25, 50, 100])); - }, - }, - 50 - ), - orderBy: withDefault( - createEnumParam( - // Same as filter then map but this is acceptable to typescript - DEFAULT_FLOW_TABLE_HEADERS.reduce((acc, curr) => { - if (curr.sortable) { - return [...acc, curr.identifierID]; - } - - return acc; - }, [] as string[]) - ), - 'flow.updatedAt' - ), - orderDir: withDefault(createEnumParam(['ASC', 'DESC']), 'DESC'), - filters: withDefault(JsonParam, JSON.stringify({})), - tableHeaders: withDefault(StringParam, encodeTableHeaders([])), // Default value of table headers - prevPageCursor: withDefault(NumberParam, 0), - nextPageCursor: withDefault(NumberParam, 0), + codec: FLOW_PARAMS_CODEC, + initialValues: { + page: 0, + rowsPerPage: 50, + orderBy: 'flow.updatedAt', + orderDir: 'DESC', + filters: JSON.stringify({}), + tableHeaders: encodeTableHeaders([]), // Default value of table headers + }, }); const handleAbortController = useCallback(() => { @@ -85,8 +57,8 @@ export default (props: Props) => { headers: DEFAULT_FLOW_TABLE_HEADERS, initialValues: PENDING_FLOWS_FILTER_INITIAL_VALUES, rowsPerPageOption: [10, 25, 50, 100], - query: query, - setQuery: setQuery, + query, + setQuery, pending: true, abortSignal: abortControllerRef.current.signal, }; diff --git a/apps/hpc-ftsadmin/src/app/pages/keywords/keyword-list.tsx b/apps/hpc-ftsadmin/src/app/pages/keywords/keyword-list.tsx index 8d1aec373..12cff7b86 100644 --- a/apps/hpc-ftsadmin/src/app/pages/keywords/keyword-list.tsx +++ b/apps/hpc-ftsadmin/src/app/pages/keywords/keyword-list.tsx @@ -3,12 +3,6 @@ import { t } from '../../../i18n'; import PageMeta from '../../components/page-meta'; import { AppContext } from '../../context'; import tw from 'twin.macro'; -import { - StringParam, - createEnumParam, - useQueryParams, - withDefault, -} from 'use-query-params'; import { DEFAULT_KEYWORD_TABLE_HEADERS, encodeTableHeaders, @@ -16,6 +10,8 @@ import { import KeywordTable, { KeywordTableProps, } from '../../components/tables/keywords-table'; +import useQueryParams from '../../utils/useQueryParams'; +import { KEYWORD_PARAMS_CODEC } from '../../utils/codecs'; interface Props { className?: string; @@ -29,21 +25,12 @@ const LandingContainer = tw.div` `; export default (props: Props) => { const [query, setQuery] = useQueryParams({ - orderBy: withDefault( - createEnumParam( - // Same as filter then map but this is acceptable to typescript - DEFAULT_KEYWORD_TABLE_HEADERS.reduce((acc, curr) => { - if (curr.sortable) { - return [...acc, curr.identifierID]; - } - - return acc; - }, [] as string[]) - ), - 'keyword.name' - ), - orderDir: withDefault(createEnumParam(['ASC', 'DESC']), 'ASC'), - tableHeaders: withDefault(StringParam, encodeTableHeaders([], 'keywords')), + codec: KEYWORD_PARAMS_CODEC, + initialValues: { + orderBy: 'keyword.name', + orderDir: 'ASC', + tableHeaders: encodeTableHeaders([], 'keywords'), + }, }); const keywordTableProps: KeywordTableProps = { diff --git a/apps/hpc-ftsadmin/src/app/pages/organizations/organization-list.tsx b/apps/hpc-ftsadmin/src/app/pages/organizations/organization-list.tsx index 42f9e1a95..dc2ec9d70 100644 --- a/apps/hpc-ftsadmin/src/app/pages/organizations/organization-list.tsx +++ b/apps/hpc-ftsadmin/src/app/pages/organizations/organization-list.tsx @@ -3,15 +3,6 @@ import { t } from '../../../i18n'; import PageMeta from '../../components/page-meta'; import { AppContext, getEnv } from '../../context'; import tw from 'twin.macro'; -import { - JsonParam, - NumberParam, - StringParam, - createEnumParam, - decodeNumber, - useQueryParams, - withDefault, -} from 'use-query-params'; import { DEFAULT_ORGANIZATION_TABLE_HEADERS, encodeTableHeaders, @@ -22,6 +13,8 @@ import OrganizationTable, { import FilterOrganizationsTable, { ORGANIZATIONS_FILTER_INITIAL_VALUES, } from '../../components/filters/filter-organization-table'; +import useQueryParams from '../../utils/useQueryParams'; +import { ORGANIZATION_PARAMS_CODEC } from '../../utils/codecs'; interface Props { className?: string; @@ -37,39 +30,15 @@ export default (props: Props) => { const rowsPerPageOptions = [10, 25, 50, 100]; const [query, setQuery] = useQueryParams({ - page: withDefault(NumberParam, 0), - rowsPerPage: withDefault( - { - ...NumberParam, - decode: (string) => { - // Prevent user requesting more than max number of rows - const number = decodeNumber(string); - return number && Math.min(number, Math.max(...rowsPerPageOptions)); - }, - }, - 50 - ), - orderBy: withDefault( - createEnumParam( - // Same as filter then map but this is acceptable to typescript - DEFAULT_ORGANIZATION_TABLE_HEADERS.reduce((acc, curr) => { - if (curr.sortable) { - return [...acc, curr.identifierID]; - } - - return acc; - }, [] as string[]) - ), - 'organization.name' - ), - orderDir: withDefault(createEnumParam(['ASC', 'DESC']), 'ASC'), - filters: withDefault(JsonParam, JSON.stringify({})), - tableHeaders: withDefault( - StringParam, - encodeTableHeaders([], 'organizations') - ), - prevPageCursor: withDefault(NumberParam, 0), - nextPageCursor: withDefault(NumberParam, 0), + codec: ORGANIZATION_PARAMS_CODEC, + initialValues: { + page: 0, + rowsPerPage: 50, + orderBy: 'organization.name', + orderDir: 'ASC', + filters: JSON.stringify({}), + tableHeaders: encodeTableHeaders([], 'organizations'), + }, }); const organizationTableProps: OrganizationTableProps = { diff --git a/apps/hpc-ftsadmin/src/app/utils/table-headers.ts b/apps/hpc-ftsadmin/src/app/utils/table-headers.ts index bd092e988..90773fd03 100644 --- a/apps/hpc-ftsadmin/src/app/utils/table-headers.ts +++ b/apps/hpc-ftsadmin/src/app/utils/table-headers.ts @@ -1,6 +1,6 @@ import { LanguageKey, t } from '../../i18n'; import { Strings } from '../../i18n/iface'; -import { Query } from '../components/tables/table-utils'; +import type { Query, SetQuery } from '../components/tables/table-utils'; import { FilterKeys } from './parse-filters'; /** Declare which tables there can be */ @@ -310,11 +310,11 @@ const defaultEncodeTableHeaders = (table: TableType) => { * Encodes the query param to obtain a string suitable for the URL, * use it alongside `decodeTableHeaders()` */ -export const encodeTableHeaders = ( +export const encodeTableHeaders = ( headers: Array, table: TableType = 'flows', - query?: Query, - setQuery?: (newQuery: Query) => void + query?: T, + setQuery?: SetQuery ): string => { if (headers.length === 0) { return defaultEncodeTableHeaders(table); @@ -399,12 +399,12 @@ const defaultDecodeTableHeaders = ( /** * Decodes the query param to obtain an ordered list of table headers */ -export const decodeTableHeaders = ( +export const decodeTableHeaders = ( queryParam: string, lang: LanguageKey, table: TableType = 'flows', - query?: Query, - setQuery?: (newQuery: Query) => void + query?: T, + setQuery?: SetQuery ): Array< TableHeadersProps > => { From 3bce56ccd40e24fb07b352d3054f879a572e7dac Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Mon, 16 Dec 2024 09:58:56 +0100 Subject: [PATCH 04/11] Remove `QueryParamProvider` and adapter --- apps/hpc-ftsadmin/src/libs/useQueryParams.ts | 44 -------------------- apps/hpc-ftsadmin/src/main.tsx | 8 +--- 2 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 apps/hpc-ftsadmin/src/libs/useQueryParams.ts diff --git a/apps/hpc-ftsadmin/src/libs/useQueryParams.ts b/apps/hpc-ftsadmin/src/libs/useQueryParams.ts deleted file mode 100644 index adb640c50..000000000 --- a/apps/hpc-ftsadmin/src/libs/useQueryParams.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useContext } from 'react'; -import { - useNavigate, - useLocation, - UNSAFE_DataRouterContext, - UNSAFE_LocationContext, -} from 'react-router'; -import { type QueryParamAdapter, type PartialLocation } from 'use-query-params'; - -export const ReactRouter7Adapter = ({ - children, -}: { - children: (adapter: QueryParamAdapter) => React.ReactElement | null; -}) => { - // we need the navigator directly so we can access the current version - // of location in case of multiple updates within a render (e.g. #233) - // but we will limit our usage of it and have a backup to just use - // useLocation() output in case of some kind of breaking change we miss. - // see: https://github.com/remix-run/react-router/blob/f3d87dcc91fbd6fd646064b88b4be52c15114603/packages/react-router-dom/index.tsx#L113-L131 - const { location: unsafeLocation } = useContext(UNSAFE_LocationContext); - const navigate = useNavigate(); - const router = useContext(UNSAFE_DataRouterContext)?.router; - const location = useLocation(); - - const adapter: QueryParamAdapter = { - replace(location2: PartialLocation) { - navigate(location2.search || '?', { - replace: true, - state: location2.state, - }); - }, - push(location2: PartialLocation) { - navigate(location2.search || '?', { - replace: false, - state: location2.state, - }); - }, - get location() { - return router?.state?.location ?? unsafeLocation ?? location; - }, - }; - - return children(adapter); -}; diff --git a/apps/hpc-ftsadmin/src/main.tsx b/apps/hpc-ftsadmin/src/main.tsx index 14d0617e9..b30cbbfa1 100644 --- a/apps/hpc-ftsadmin/src/main.tsx +++ b/apps/hpc-ftsadmin/src/main.tsx @@ -13,8 +13,6 @@ import PageOrganizationsList from './app/pages/organizations/organization-list'; import PageOrganization from './app/pages/organizations/organization'; import { RouteParamsValidator } from './app/components/route-params-validator'; -import { QueryParamProvider } from 'use-query-params'; -import { ReactRouter7Adapter } from './libs/useQueryParams'; const rootElement = document.getElementById('root'); if (!rootElement) { @@ -25,11 +23,7 @@ const root = ReactDOM.createRoot(rootElement); const router = createBrowserRouter([ { path: paths.home(), - element: ( - - - - ), + element: , children: [ { path: paths.home(), element: }, { path: paths.flows(), element: }, From 4f04bc364d0a509392eb41b7a305e51c4adec119 Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Mon, 16 Dec 2024 10:00:55 +0100 Subject: [PATCH 05/11] Uninstall `use-query-params` package --- package-lock.json | 84 +---------------------------------------------- package.json | 3 +- 2 files changed, 2 insertions(+), 85 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b466d2eb..04cfe5733 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,8 +86,7 @@ "ts-jest": "29.1.0", "ts-node": "10.9.1", "twin.macro": "3.4.1", - "typescript": "5.2.2", - "use-query-params": "2.2.1" + "typescript": "5.2.2" }, "engines": { "node": ">=18.18.2 || >=20.9.0", @@ -5424,18 +5423,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@remix-run/router": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", - "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -17657,44 +17644,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz", - "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@remix-run/router": "1.21.0", - "react-router": "6.28.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-router-dom/node_modules/react-router": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", - "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@remix-run/router": "1.21.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, "node_modules/react-router/node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -18348,13 +18297,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/serialize-query-params": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz", - "integrity": "sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q==", - "dev": true, - "license": "ISC" - }, "node_modules/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -20338,30 +20280,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/use-query-params": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz", - "integrity": "sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "serialize-query-params": "^2.0.2" - }, - "peerDependencies": { - "@reach/router": "^1.2.1", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "react-router-dom": ">=5" - }, - "peerDependenciesMeta": { - "@reach/router": { - "optional": true - }, - "react-router-dom": { - "optional": true - } - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index c01387e7c..69d6ea8ad 100644 --- a/package.json +++ b/package.json @@ -117,8 +117,7 @@ "ts-jest": "29.1.0", "ts-node": "10.9.1", "twin.macro": "3.4.1", - "typescript": "5.2.2", - "use-query-params": "2.2.1" + "typescript": "5.2.2" }, "babelMacros": { "twin": { From 2332fe39a4377e9f7360acef12e129399617e8d8 Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Mon, 13 Jan 2025 09:20:50 +0100 Subject: [PATCH 06/11] Remove `flushSync` option The proposed solution is to use `setTimeout`, reseting the form and changing the URL are two actions that trigger a re-render. In order to avoid missbehaviours on how they get added to React's event queue, we add a `setTimeout` so the execution is done in the next tick. --- .../src/app/components/filters/filter-flows-table.tsx | 9 ++++----- .../components/filters/filter-organization-table.tsx | 9 ++++----- .../components/filters/filter-pending-flows-table.tsx | 10 ++++++---- .../src/app/components/tables/table-utils.tsx | 6 +----- apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx | 9 +++------ 5 files changed, 18 insertions(+), 25 deletions(-) diff --git a/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx b/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx index 080c31aa1..b7e203c0e 100644 --- a/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx @@ -136,14 +136,13 @@ export const FilterFlowsTable = (props: Props) => { if (query.filters !== encodedFilters) { handleAbortController(); } - setQuery( - { + setTimeout(() => { + setQuery({ ...query, page: 0, filters: encodedFilters, - }, - true - ); + }); + }); }; return ( s.components.flowsFilter.title)}> diff --git a/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx b/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx index d4407f0b3..7aad1d027 100644 --- a/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx @@ -69,14 +69,13 @@ export const FilterOrganizationsTable = (props: Props) => { ) => void ) => { formikResetForm(); - setQuery( - { + setTimeout(() => { + setQuery({ ...query, page: 0, filters: encodeFilters({}, ORGANIZATIONS_FILTER_INITIAL_VALUES), - }, - true - ); + }); + }); }; return ( { if (query.filters !== encodedFilters) { handleAbortController(); } - setQuery({ - ...query, - page: 0, - filters: encodedFilters, + setTimeout(() => { + setQuery({ + ...query, + page: 0, + filters: encodedFilters, + }); }); }; return ( diff --git a/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx b/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx index b521ce81c..516aaf73d 100644 --- a/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx +++ b/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx @@ -12,7 +12,6 @@ import type { KeywordHeaderID, OrganizationHeaderID, } from '../../utils/table-headers'; -import type { NavigateOptions } from 'react-router'; export type Query = { orderDir: 'ASC' | 'DESC'; @@ -39,10 +38,7 @@ export type KeywordQuery = Query & { orderBy: KeywordHeaderID; }; -export type SetQuery = ( - newQuery: T, - flushSync?: NavigateOptions['flushSync'] -) => void; +export type SetQuery = (newQuery: T) => void; export const StyledLoader = tw(C.Loader)` mx-auto diff --git a/apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx b/apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx index 08e2f024b..3300f5b35 100644 --- a/apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx +++ b/apps/hpc-ftsadmin/src/app/utils/useQueryParams.tsx @@ -1,5 +1,5 @@ import t from 'io-ts'; -import { type NavigateOptions, useSearchParams } from 'react-router'; +import { useSearchParams } from 'react-router'; import { isRight } from 'fp-ts/lib/Either'; type Props = { @@ -49,10 +49,7 @@ function useQueryParams>({ return result.right; }; - const setValidatedSearchParams = ( - newParams: T, - flushSync?: NavigateOptions['flushSync'] - ) => { + const setValidatedSearchParams = (newParams: T) => { const result = codec.decode(newParams); if (!isRight(result)) { @@ -60,7 +57,7 @@ function useQueryParams>({ return; } const validatedParams = toURLSearchParams(newParams); - setSearchParams(validatedParams, { flushSync }); + setSearchParams(validatedParams); }; return [decodeParams(searchParams), setValidatedSearchParams] as const; From 9c1ddf48c9cc0fa50e6240375353fbac6e90e1f0 Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Mon, 13 Jan 2025 09:55:05 +0100 Subject: [PATCH 07/11] Update codecs to have more restrictions --- apps/hpc-ftsadmin/src/app/utils/codecs.ts | 38 ++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/apps/hpc-ftsadmin/src/app/utils/codecs.ts b/apps/hpc-ftsadmin/src/app/utils/codecs.ts index ad8d58cfb..021a4e082 100644 --- a/apps/hpc-ftsadmin/src/app/utils/codecs.ts +++ b/apps/hpc-ftsadmin/src/app/utils/codecs.ts @@ -4,16 +4,38 @@ import { DEFAULT_FLOW_TABLE_HEADERS, DEFAULT_KEYWORD_TABLE_HEADERS, DEFAULT_ORGANIZATION_TABLE_HEADERS, - FlowHeaderID, - KeywordHeaderID, - OrganizationHeaderID, - TableHeadersProps, + type FlowHeaderID, + type KeywordHeaderID, + type OrganizationHeaderID, + type TableHeadersProps, } from './table-headers'; +const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, 100] as const; +const ROWS_PER_PAGE = new t.Type( + 'ROWS_PER_PAGE', + t.number.is, + (v, c) => { + if (typeof v === 'number') { + return Number.isInteger(v) && + ROWS_PER_PAGE_OPTIONS.some((row) => row === v) + ? t.success(v) + : t.failure(v, c); + } else if (typeof v === 'string') { + return /^[0-9]+$/.test(v) && + ROWS_PER_PAGE_OPTIONS.some((row) => row === parseInt(v)) + ? t.success(parseInt(v)) + : t.failure(v, c); + } else { + return t.failure(v, c); + } + }, + t.identity +); + const PARAMS_CODEC = t.intersection([ t.type({ page: util.INTEGER_FROM_STRING, - rowsPerPage: util.INTEGER_FROM_STRING, + rowsPerPage: ROWS_PER_PAGE, orderDir: t.keyof({ ASC: 'ASC', DESC: 'DESC', @@ -33,8 +55,10 @@ const extractIdentifierIds = < val: TableHeadersProps[] ) => { return val.reduce( - (acc, { identifierID: id }) => { - acc[id] = id; + (acc, { identifierID: id, sortable }) => { + if (sortable) { + acc[id] = id; + } return acc; }, {} as Record From 1aa89660d4d055de42988c2b7d7f755ed3e5b928 Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Mon, 13 Jan 2025 16:50:20 +0100 Subject: [PATCH 08/11] Remove pageCursors --- .../src/app/components/tables/flows-table.tsx | 44 ++++--------------- .../src/app/components/tables/table-utils.tsx | 2 - apps/hpc-ftsadmin/src/app/utils/codecs.ts | 24 ++++------ libs/hpc-data/src/lib/flows.ts | 4 -- libs/hpc-live/src/lib/model.ts | 8 ---- 5 files changed, 17 insertions(+), 65 deletions(-) diff --git a/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx b/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx index 2976e6dd6..bb1be22bf 100644 --- a/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx @@ -82,6 +82,7 @@ export default function FlowsTable(props: FlowsTableProps) { const [tableInfoDisplay, setTableInfoDisplay] = useState( util.getLocalStorageItem('tableSettings', true) ); + const parsedFilters = parseFlowFilters(tableFilters, props.pending); const navigate = useNavigate(); const [state, load] = useDataLoader([query], () => @@ -91,8 +92,6 @@ export default function FlowsTable(props: FlowsTableProps) { sortOrder: query.orderDir, ...parsedFilters, signal: props.abortSignal, - prevPageCursor: query.prevPageCursor, - nextPageCursor: query.nextPageCursor, }) ); const handleChipDelete = (fieldName: T) => { @@ -106,26 +105,11 @@ export default function FlowsTable(props: FlowsTableProps) { } }; - const handleChangePage = ( - newPage: number, - prevPageCursor: number, - nextPageCursor: number - ) => { - if (newPage > props.query.page) { - setQuery({ - ...query, - prevPageCursor: undefined, - nextPageCursor: nextPageCursor, - page: newPage, - }); - } else { - setQuery({ - ...query, - prevPageCursor: prevPageCursor, - nextPageCursor: undefined, - page: newPage, - }); - } + const handleChangePage = (newPage: number) => { + setQuery({ + ...query, + page: newPage, + }); }; const handleChangeRowsPerPage = ( @@ -859,13 +843,7 @@ export default function FlowsTable(props: FlowsTableProps) { count={data.searchFlows.total} rowsPerPage={query.rowsPerPage} page={query.page} - onPageChange={(_, newPage) => - handleChangePage( - newPage, - data.searchFlows.prevPageCursor, - data.searchFlows.nextPageCursor - ) - } + onPageChange={(_, newPage) => handleChangePage(newPage)} onRowsPerPageChange={handleChangeRowsPerPage} /> @@ -896,13 +874,7 @@ export default function FlowsTable(props: FlowsTableProps) { count={data.searchFlows.total} rowsPerPage={query.rowsPerPage} page={query.page} - onPageChange={(_, newPage) => - handleChangePage( - newPage, - data.searchFlows.prevPageCursor, - data.searchFlows.nextPageCursor - ) - } + onPageChange={(_, newPage) => handleChangePage(newPage)} onRowsPerPageChange={handleChangeRowsPerPage} /> diff --git a/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx b/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx index 516aaf73d..52c6b83ef 100644 --- a/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx +++ b/apps/hpc-ftsadmin/src/app/components/tables/table-utils.tsx @@ -16,8 +16,6 @@ import type { export type Query = { orderDir: 'ASC' | 'DESC'; tableHeaders: string; - prevPageCursor?: number; - nextPageCursor?: number; }; export type FlowQuery = Query & { diff --git a/apps/hpc-ftsadmin/src/app/utils/codecs.ts b/apps/hpc-ftsadmin/src/app/utils/codecs.ts index 021a4e082..a244ee80a 100644 --- a/apps/hpc-ftsadmin/src/app/utils/codecs.ts +++ b/apps/hpc-ftsadmin/src/app/utils/codecs.ts @@ -32,22 +32,16 @@ const ROWS_PER_PAGE = new t.Type( t.identity ); -const PARAMS_CODEC = t.intersection([ - t.type({ - page: util.INTEGER_FROM_STRING, - rowsPerPage: ROWS_PER_PAGE, - orderDir: t.keyof({ - ASC: 'ASC', - DESC: 'DESC', - }), - filters: t.string, - tableHeaders: t.string, - }), - t.partial({ - prevPageCursor: util.INTEGER_FROM_STRING, - nextPageCursor: util.INTEGER_FROM_STRING, +const PARAMS_CODEC = t.type({ + page: util.INTEGER_FROM_STRING, + rowsPerPage: ROWS_PER_PAGE, + orderDir: t.keyof({ + ASC: 'ASC', + DESC: 'DESC', }), -]); + filters: t.string, + tableHeaders: t.string, +}); const extractIdentifierIds = < T extends OrganizationHeaderID | FlowHeaderID | KeywordHeaderID, diff --git a/libs/hpc-data/src/lib/flows.ts b/libs/hpc-data/src/lib/flows.ts index da9096b23..2c27df147 100644 --- a/libs/hpc-data/src/lib/flows.ts +++ b/libs/hpc-data/src/lib/flows.ts @@ -223,8 +223,6 @@ export const SEARCH_FLOWS_RESULT = t.type({ searchFlows: t.type({ total: t.number, flows: FLOW_RESULT, - prevPageCursor: t.number, - nextPageCursor: t.number, hasNextPage: t.boolean, hasPreviousPage: t.boolean, pageSize: t.number, @@ -282,8 +280,6 @@ export type NestedFlowFilters = t.TypeOf; export const SEARCH_FLOWS_PARAMS = t.partial({ limit: t.number, - prevPageCursor: t.number, - nextPageCursor: t.number, sortOrder: t.string, sortField: t.string, ...FLOW_FILTERS.props, diff --git a/libs/hpc-live/src/lib/model.ts b/libs/hpc-live/src/lib/model.ts index 2a310c0cd..0d9fcab92 100644 --- a/libs/hpc-live/src/lib/model.ts +++ b/libs/hpc-live/src/lib/model.ts @@ -627,17 +627,9 @@ export class LiveModel implements Model { const query = `query { searchFlows${searchFlowsParams(params)} { total - - prevPageCursor - hasNextPage - - nextPageCursor - hasPreviousPage - pageSize - ${this.searchFlowFields} } }`; From 5beda13619dcd42cb33af61f9a23e09a0a407d39 Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Fri, 17 Jan 2025 10:16:38 +0100 Subject: [PATCH 09/11] Add page to endpoint parameters --- apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx | 1 + libs/hpc-data/src/lib/flows.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx b/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx index bb1be22bf..18c638806 100644 --- a/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/tables/flows-table.tsx @@ -88,6 +88,7 @@ export default function FlowsTable(props: FlowsTableProps) { const [state, load] = useDataLoader([query], () => env.model.flows.searchFlows({ limit: query.rowsPerPage, + page: query.page, sortField: query.orderBy, sortOrder: query.orderDir, ...parsedFilters, diff --git a/libs/hpc-data/src/lib/flows.ts b/libs/hpc-data/src/lib/flows.ts index 2c27df147..af6ad801a 100644 --- a/libs/hpc-data/src/lib/flows.ts +++ b/libs/hpc-data/src/lib/flows.ts @@ -280,6 +280,7 @@ export type NestedFlowFilters = t.TypeOf; export const SEARCH_FLOWS_PARAMS = t.partial({ limit: t.number, + page: t.number, sortOrder: t.string, sortField: t.string, ...FLOW_FILTERS.props, From dfa8d62720b0516319c9750c371085d20f6983e2 Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Tue, 21 Jan 2025 14:30:18 +0100 Subject: [PATCH 10/11] Add comments to `useTimeout()` usage --- .../src/app/components/filters/filter-flows-table.tsx | 3 +++ .../src/app/components/filters/filter-organization-table.tsx | 3 +++ .../src/app/components/filters/filter-pending-flows-table.tsx | 3 +++ 3 files changed, 9 insertions(+) diff --git a/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx b/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx index b7e203c0e..6a7fc5f00 100644 --- a/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/filters/filter-flows-table.tsx @@ -136,6 +136,9 @@ export const FilterFlowsTable = (props: Props) => { if (query.filters !== encodedFilters) { handleAbortController(); } + // We need to delay this action in a synchronous way to avoid + // calling 2 setState() actions in an uncontrolled way that could + // mess with internal React's component update cycle setTimeout(() => { setQuery({ ...query, diff --git a/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx b/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx index 7aad1d027..f9a1a1a2d 100644 --- a/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/filters/filter-organization-table.tsx @@ -69,6 +69,9 @@ export const FilterOrganizationsTable = (props: Props) => { ) => void ) => { formikResetForm(); + // We need to delay this action in a synchronous way to avoid + // calling 2 setState() actions in an uncontrolled way that could + // mess with internal React's component update cycle setTimeout(() => { setQuery({ ...query, diff --git a/apps/hpc-ftsadmin/src/app/components/filters/filter-pending-flows-table.tsx b/apps/hpc-ftsadmin/src/app/components/filters/filter-pending-flows-table.tsx index 1aa29e630..0240352f3 100644 --- a/apps/hpc-ftsadmin/src/app/components/filters/filter-pending-flows-table.tsx +++ b/apps/hpc-ftsadmin/src/app/components/filters/filter-pending-flows-table.tsx @@ -80,6 +80,9 @@ export const FilterPendingFlowsTable = (props: Props) => { if (query.filters !== encodedFilters) { handleAbortController(); } + // We need to delay this action in a synchronous way to avoid + // calling 2 setState() actions in an uncontrolled way that could + // mess with internal React's component update cycle setTimeout(() => { setQuery({ ...query, From b7756d71d60716ce414530f7ae5e97b5d08d2e24 Mon Sep 17 00:00:00 2001 From: Onitoxan Date: Tue, 21 Jan 2025 14:31:48 +0100 Subject: [PATCH 11/11] Refactor `ROWS_PER_PAGE` codec into a generic one --- apps/hpc-ftsadmin/src/app/utils/codecs.ts | 21 +------------------ libs/hpc-data/src/lib/util.ts | 25 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/apps/hpc-ftsadmin/src/app/utils/codecs.ts b/apps/hpc-ftsadmin/src/app/utils/codecs.ts index a244ee80a..4195e116b 100644 --- a/apps/hpc-ftsadmin/src/app/utils/codecs.ts +++ b/apps/hpc-ftsadmin/src/app/utils/codecs.ts @@ -11,26 +11,7 @@ import { } from './table-headers'; const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, 100] as const; -const ROWS_PER_PAGE = new t.Type( - 'ROWS_PER_PAGE', - t.number.is, - (v, c) => { - if (typeof v === 'number') { - return Number.isInteger(v) && - ROWS_PER_PAGE_OPTIONS.some((row) => row === v) - ? t.success(v) - : t.failure(v, c); - } else if (typeof v === 'string') { - return /^[0-9]+$/.test(v) && - ROWS_PER_PAGE_OPTIONS.some((row) => row === parseInt(v)) - ? t.success(parseInt(v)) - : t.failure(v, c); - } else { - return t.failure(v, c); - } - }, - t.identity -); +const ROWS_PER_PAGE = util.validInteger(ROWS_PER_PAGE_OPTIONS); const PARAMS_CODEC = t.type({ page: util.INTEGER_FROM_STRING, diff --git a/libs/hpc-data/src/lib/util.ts b/libs/hpc-data/src/lib/util.ts index dfdf44f48..d2bfea88a 100644 --- a/libs/hpc-data/src/lib/util.ts +++ b/libs/hpc-data/src/lib/util.ts @@ -47,6 +47,31 @@ export const POSITIVE_INTEGER_FROM_STRING = new t.Type( t.identity ); +/** + * Accepts either an integer, or a string of an integer, that is contained in + * `integerOptions`. serializes to a number. + */ +export const validInteger = (integerOptions: readonly number[]) => + new t.Type( + 'VALID_INTEGER', + t.number.is, + (v, c) => { + if (typeof v === 'number') { + return Number.isInteger(v) && integerOptions.some((row) => row === v) + ? t.success(v) + : t.failure(v, c); + } else if (typeof v === 'string') { + return /^\d+$/.test(v) && + integerOptions.some((row) => row === parseInt(v)) + ? t.success(parseInt(v)) + : t.failure(v, c); + } else { + return t.failure(v, c); + } + }, + t.identity + ); + /** * Accepts either a number, or a string of a number, serializes to a number type. */