From c703170571d5e8b0b1c6ca145f9306ddeb3c0851 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 15 Mar 2023 14:38:53 -0700 Subject: [PATCH 01/23] Create paginator component Fixes #1398 --- .../lib/components/Toolbar/Paginator.tsx | 56 ++++++++ .../lib/components/Toolbar/RecordSets.tsx | 121 ++++++++++-------- 2 files changed, 124 insertions(+), 53 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/Toolbar/Paginator.tsx diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/Paginator.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/Paginator.tsx new file mode 100644 index 00000000000..acfb0cd3b31 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/Paginator.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Select } from '../Atoms/Form'; +import { Slider } from '../FormSliders/Slider'; + +export function usePaginator({ + totalCount, + onPageChange: handlePageChange, + currentPage, + rowsPerPage, + rowsPerPageValue = 10, +}: { + readonly totalCount: number | undefined; + readonly onPageChange: (selected: number) => void; + readonly currentPage: number; + readonly rowsPerPage: (selected: number) => void; + readonly rowsPerPageValue: number; +}): { paginator: JSX.Element } { + const pageCount = + totalCount === undefined ? 0 : Math.ceil(totalCount / rowsPerPageValue); + return { + paginator: ( +
+ + {rowsPerPageValue === 500 ? ( + '' + ) : ( +
+ +
+ )} +
+ +
+
+ ), + }; +} diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx index ddd652c2cf1..94bf3ef45fd 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx @@ -27,6 +27,7 @@ import { TableIcon } from '../Molecules/TableIcon'; import { hasToolPermission } from '../Permissions/helpers'; import { OverlayContext } from '../Router/Router'; import { EditRecordSet } from './RecordSetEdit'; +import { usePaginator } from './Paginator'; export function RecordSetsOverlay(): JSX.Element { const handleClose = React.useContext(OverlayContext); @@ -65,18 +66,29 @@ export function RecordSetsDialog({ 'name' ); + const [currentPage, setCurrentPage] = React.useState(0); + const [rowsPerPage, setRowPerPage] = React.useState(10); + const { paginator } = usePaginator({ + totalCount: data?.totalCount, + onPageChange: handlePageChange, + currentPage, + rowsPerPage, + rowsPerPageValue, + }); + const [unsortedData] = useAsyncState( React.useCallback( async () => fetchCollection('RecordSet', { specifyUser: userInformation.id, type: 0, - limit: 5000, + limit: rowsPerPage, domainFilter: true, orderBy: '-timestampCreated', + offset: currentPage * rowsPerPage, dbTableId: table?.tableId, }), - [table] + [table, rowsPerPage] ), true ); @@ -100,59 +112,62 @@ export function RecordSetsDialog({ children({ ...data, children: ( - - - - - - - - - - {data.records.map((recordSet) => ( - - setState({ - type: 'EditState', - recordSet: deserializeResource(recordSet), - }) - } - onSelect={ - typeof handleSelect === 'function' - ? (): void => handleSelect(recordSet) - : undefined - } - /> - ))} - {data.totalCount !== data.records.length && ( + <> +
- handleSort('name')}> - {tables.RecordSet.label} - - - - handleSort('timestampCreated')} - > - {getField(tables.RecordSet, 'timestampCreated').label} - - - {commonText.size()} -
+ - + + + + - )} - -
{commonText.listTruncated()} + handleSort('name')}> + {tables.RecordSet.label} + + + + handleSort('timestampCreated')} + > + {getField(tables.RecordSet, 'timestampCreated').label} + + + {commonText.size()}
+ + + {data.records.map((recordSet) => ( + + setState({ + type: 'EditState', + recordSet: deserializeResource(recordSet), + }) + } + onSelect={ + typeof handleSelect === 'function' + ? (): void => handleSelect(recordSet) + : undefined + } + /> + ))} + {data.totalCount !== data.records.length && ( + + {commonText.listTruncated()} + + )} + + + {paginator} + ), dialog: (children, buttons) => ( Date: Thu, 16 Mar 2023 12:40:02 -0700 Subject: [PATCH 02/23] Move Paginator component to molecule and delete uncessary libraries Fixes #1398 --- .../js_src/lib/components/{Toolbar => Molecules}/Paginator.tsx | 0 .../frontend/js_src/lib/components/Toolbar/RecordSets.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename specifyweb/frontend/js_src/lib/components/{Toolbar => Molecules}/Paginator.tsx (100%) diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/Paginator.tsx b/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/Toolbar/Paginator.tsx rename to specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx index 94bf3ef45fd..07291853237 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx @@ -27,7 +27,7 @@ import { TableIcon } from '../Molecules/TableIcon'; import { hasToolPermission } from '../Permissions/helpers'; import { OverlayContext } from '../Router/Router'; import { EditRecordSet } from './RecordSetEdit'; -import { usePaginator } from './Paginator'; +import { usePaginator } from '../Molecules/Paginator'; export function RecordSetsOverlay(): JSX.Element { const handleClose = React.useContext(OverlayContext); From 928be8b4816e7dd0e6aedbbc83ca1fd663cb5b47 Mon Sep 17 00:00:00 2001 From: Max Patiiuk Date: Fri, 17 Mar 2023 09:33:09 -0500 Subject: [PATCH 03/23] Simplify usage of usePaginator() Shift burden of responsibility to the hook, from the component. Since the hook is going to be called in many places, that will result in reduction of code. --- .../lib/components/Molecules/Paginator.tsx | 103 ++++++++++-------- .../lib/components/Toolbar/RecordSets.tsx | 21 +--- .../js_src/lib/localization/common.ts | 3 + 3 files changed, 65 insertions(+), 62 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx b/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx index acfb0cd3b31..887e2185c70 100644 --- a/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx +++ b/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx @@ -1,56 +1,65 @@ import React from 'react'; + +import { commonText } from '../../localization/common'; +import type { GetSet } from '../../utils/types'; import { Select } from '../Atoms/Form'; +import { formatNumber } from '../Atoms/Internationalization'; import { Slider } from '../FormSliders/Slider'; -export function usePaginator({ - totalCount, - onPageChange: handlePageChange, - currentPage, - rowsPerPage, - rowsPerPageValue = 10, -}: { - readonly totalCount: number | undefined; - readonly onPageChange: (selected: number) => void; - readonly currentPage: number; - readonly rowsPerPage: (selected: number) => void; - readonly rowsPerPageValue: number; -}): { paginator: JSX.Element } { - const pageCount = - totalCount === undefined ? 0 : Math.ceil(totalCount / rowsPerPageValue); +/** + * Infinity hitting the practical limits + */ +const infinity = 500; +const pageSizes = [10, 50, 100]; + +export function usePaginator(defaultRowsPerPage: number = 10): { + readonly paginator: (totalCount: number | undefined) => JSX.Element; + readonly currentPage: GetSet; + readonly limit: number; + readonly offset: number; +} { + const getSetPage = React.useState(0); + const [currentPage, setCurrentPage] = getSetPage; + const [pageSize, setPageSize] = React.useState(defaultRowsPerPage); return { - paginator: ( -
- - {rowsPerPageValue === 500 ? ( - '' - ) : ( -
- + paginator(totalCount): JSX.Element { + const pageCount = + totalCount === undefined ? 0 : Math.ceil(totalCount / pageSize); + return ( +
+ + {pageSize === infinity ? undefined : ( +
+ +
+ )} +
+
- )} -
-
-
- ), + ); + }, + currentPage: getSetPage, + limit: pageSize, + offset: currentPage * pageSize, }; } diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx index 07291853237..3b4958ee7e0 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx @@ -22,12 +22,12 @@ import type { RecordSet } from '../DataModel/types'; import { userInformation } from '../InitialContext/userInformation'; import { DateElement } from '../Molecules/DateElement'; import { Dialog } from '../Molecules/Dialog'; +import { usePaginator } from '../Molecules/Paginator'; import { SortIndicator, useSortConfig } from '../Molecules/Sorting'; import { TableIcon } from '../Molecules/TableIcon'; import { hasToolPermission } from '../Permissions/helpers'; import { OverlayContext } from '../Router/Router'; import { EditRecordSet } from './RecordSetEdit'; -import { usePaginator } from '../Molecules/Paginator'; export function RecordSetsOverlay(): JSX.Element { const handleClose = React.useContext(OverlayContext); @@ -66,29 +66,20 @@ export function RecordSetsDialog({ 'name' ); - const [currentPage, setCurrentPage] = React.useState(0); - const [rowsPerPage, setRowPerPage] = React.useState(10); - const { paginator } = usePaginator({ - totalCount: data?.totalCount, - onPageChange: handlePageChange, - currentPage, - rowsPerPage, - rowsPerPageValue, - }); - + const { paginator, limit, offset } = usePaginator(); const [unsortedData] = useAsyncState( React.useCallback( async () => fetchCollection('RecordSet', { specifyUser: userInformation.id, type: 0, - limit: rowsPerPage, + limit, domainFilter: true, orderBy: '-timestampCreated', - offset: currentPage * rowsPerPage, + offset, dbTableId: table?.tableId, }), - [table, rowsPerPage] + [table, limit, offset] ), true ); @@ -166,7 +157,7 @@ export function RecordSetsDialog({ )} - {paginator} + {paginator(data?.totalCount)} ), dialog: (children, buttons) => ( diff --git a/specifyweb/frontend/js_src/lib/localization/common.ts b/specifyweb/frontend/js_src/lib/localization/common.ts index 9cd7fdde6ba..7ee3dff4d39 100644 --- a/specifyweb/frontend/js_src/lib/localization/common.ts +++ b/specifyweb/frontend/js_src/lib/localization/common.ts @@ -609,4 +609,7 @@ export const commonText = createDictionary({ 'ru-ru': 'Времени осталось', 'uk-ua': 'Час, що залишився', }, + unlimited: { + 'en-us': 'Unlimited', + }, } as const); From 08bb7ba9f3e6957c6dd54d9c3712d43d7ae407d1 Mon Sep 17 00:00:00 2001 From: Max Patiiuk Date: Fri, 17 Mar 2023 09:36:29 -0500 Subject: [PATCH 04/23] Sort Record Sets on the back-end Fixes https://github.com/specify/specify7/pull/3195/files#r1140301202 --- .../lib/components/Toolbar/RecordSets.tsx | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx index 3b4958ee7e0..17a6cd9f149 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx @@ -61,13 +61,10 @@ export function RecordSetsDialog({ | State<'MainState'> >({ type: 'MainState' }); - const [sortConfig, handleSort, applySortConfig] = useSortConfig( - 'listOfRecordSets', - 'name' - ); + const [sortConfig, handleSort] = useSortConfig('listOfRecordSets', 'name'); const { paginator, limit, offset } = usePaginator(); - const [unsortedData] = useAsyncState( + const [data] = useAsyncState( React.useCallback( async () => fetchCollection('RecordSet', { @@ -75,27 +72,14 @@ export function RecordSetsDialog({ type: 0, limit, domainFilter: true, - orderBy: '-timestampCreated', + orderBy: `${sortConfig.ascending ? '' : '-'}${sortConfig.sortField}`, offset, dbTableId: table?.tableId, }), - [table, limit, offset] + [table, limit, offset, sortConfig] ), true ); - const data = React.useMemo( - () => - typeof unsortedData === 'object' - ? { - ...unsortedData, - records: applySortConfig( - unsortedData.records, - (recordSet) => recordSet[sortConfig.sortField] - ), - } - : undefined, - [unsortedData, sortConfig] - ); const isReadOnly = React.useContext(ReadOnlyContext); return typeof data === 'object' ? ( From 5cfa38e7e7a223d9faaa886009adfa3fd57bf765 Mon Sep 17 00:00:00 2001 From: Max Patiiuk Date: Fri, 17 Mar 2023 09:45:17 -0500 Subject: [PATCH 05/23] Fix sort config causing infinite fetch loop Because default value for sort config was a new object that was created on each render --- .../js_src/lib/components/Molecules/Sorting.tsx | 10 ++++++++-- .../js_src/lib/components/Toolbar/RecordSets.tsx | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Molecules/Sorting.tsx b/specifyweb/frontend/js_src/lib/components/Molecules/Sorting.tsx index 84eb612a77d..58a045c677d 100644 --- a/specifyweb/frontend/js_src/lib/components/Molecules/Sorting.tsx +++ b/specifyweb/frontend/js_src/lib/components/Molecules/Sorting.tsx @@ -51,8 +51,14 @@ export function useSortConfig( mapper: (item: T) => boolean | number | string | null | undefined ) => RA ] { - const [sortConfig = { sortField: defaultField, ascending }, setSortConfig] = - useCachedState('sortConfig', cacheKey); + const defaultValue = React.useMemo( + () => ({ sortField: defaultField, ascending }), + [defaultField, ascending] + ); + const [sortConfig = defaultValue, setSortConfig] = useCachedState( + 'sortConfig', + cacheKey + ); const handleClick = React.useCallback( (sortField: SortConfigs[NAME]) => { const newSortConfig: SortConfig = { diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx index 17a6cd9f149..058a6d5949b 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx @@ -64,6 +64,9 @@ export function RecordSetsDialog({ const [sortConfig, handleSort] = useSortConfig('listOfRecordSets', 'name'); const { paginator, limit, offset } = usePaginator(); + const orderBy = `${sortConfig.ascending ? '' : '-'}${ + sortConfig.sortField + }` as const; const [data] = useAsyncState( React.useCallback( async () => @@ -72,11 +75,11 @@ export function RecordSetsDialog({ type: 0, limit, domainFilter: true, - orderBy: `${sortConfig.ascending ? '' : '-'}${sortConfig.sortField}`, + orderBy, offset, dbTableId: table?.tableId, }), - [table, limit, offset, sortConfig] + [table, limit, offset, orderBy] ), true ); From 1ec1f2528ced133b0ed4927e370d9718e5ac859c Mon Sep 17 00:00:00 2001 From: Max Patiiuk Date: Fri, 17 Mar 2023 10:00:56 -0500 Subject: [PATCH 06/23] Fix wrong sizing for the paginator as it was causing issues in many places. Instead, you should add it back only for small screens, and only in places where it was necessary --- specifyweb/frontend/js_src/lib/components/Atoms/Form.tsx | 2 +- .../frontend/js_src/lib/components/Molecules/Paginator.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/Form.tsx b/specifyweb/frontend/js_src/lib/components/Atoms/Form.tsx index 864fba8ea43..2f9100e9920 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/Form.tsx +++ b/specifyweb/frontend/js_src/lib/components/Atoms/Form.tsx @@ -259,7 +259,7 @@ export const Select = wrap< >( 'Select', 'select', - `${className.notTouchedInput} w-full pr-5 bg-right cursor-pointer min-w-[theme(spacing.40)]`, + `${className.notTouchedInput} w-full pr-5 bg-right cursor-pointer`, ({ onValueChange, onValuesChange, ...props }) => ({ ...props, /* diff --git a/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx b/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx index 887e2185c70..a8d5e615745 100644 --- a/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx +++ b/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx @@ -39,6 +39,7 @@ export function usePaginator(defaultRowsPerPage: number = 10): { )}
diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx index 058a6d5949b..9f03031161c 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx @@ -63,7 +63,7 @@ export function RecordSetsDialog({ const [sortConfig, handleSort] = useSortConfig('listOfRecordSets', 'name'); - const { paginator, limit, offset } = usePaginator(); + const { paginator, limit, offset } = usePaginator('recordSets'); const orderBy = `${sortConfig.ascending ? '' : '-'}${ sortConfig.sortField }` as const; diff --git a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts index e876fc92c84..49b519622d8 100644 --- a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts +++ b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts @@ -19,11 +19,13 @@ import type { LeafletCacheSalt, MarkerLayerName, } from '../../components/Leaflet/addOns'; +import type { pageSizes } from '../../components/Molecules/Paginator'; +import { Paginators } from '../../components/Molecules/Paginator'; import type { SortConfig } from '../../components/Molecules/Sorting'; import type { Conformations } from '../../components/TreeView/helpers'; import type { UserPreferences } from '../../components/UserPreferences/helpers'; import type { WbSearchPreferences } from '../../components/WorkBench/AdvancedSearch'; -import type { IR, RA } from '../types'; +import type { IR, RA, RR } from '../types'; import { ensure } from '../types'; /** The types of cached values are defined here */ @@ -141,6 +143,7 @@ export type CacheDefinitions = { readonly filters: AppResourceFilters; readonly showHiddenTables: boolean; }; + readonly pageSizes: RR; }; export type SortConfigs = { From 88137e78cafc4dc185b3768cd108689550b872fa Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Mon, 20 Mar 2023 13:18:26 -0700 Subject: [PATCH 08/23] Implement pagination in Query overlay and refactor it Fixes #1398 --- .../lib/components/Molecules/Paginator.tsx | 4 +- .../lib/components/QueryBuilder/Import.tsx | 34 +- .../js_src/lib/components/Toolbar/Query.tsx | 307 ++++++++++-------- .../lib/components/Toolbar/QueryTables.tsx | 7 +- .../lib/components/Toolbar/RecordSetEdit.tsx | 13 +- .../lib/components/Toolbar/RecordSets.tsx | 3 + 6 files changed, 199 insertions(+), 169 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx b/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx index c4feb8f61bd..a03b994a4b3 100644 --- a/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx +++ b/specifyweb/frontend/js_src/lib/components/Molecules/Paginator.tsx @@ -13,7 +13,7 @@ import { Slider } from '../FormSliders/Slider'; const infinity = 500; export const pageSizes = [10, 50, 100, infinity]; -export type Paginators = 'recordSets'; +export type Paginators = 'recordSets' | 'queryBuilder'; export function usePaginator( cacheName: Paginators, @@ -48,7 +48,7 @@ export function usePaginator( )}