From 2d1ce9af03729e119002ecd083051675b8f0123c Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 5 Sep 2023 16:11:04 +0800 Subject: [PATCH] [ENG-1078] Fix pagination (#1299) * fix 'load more' breaking * paginate all paginated queries by model id * arrays start at 0 stupid --- core/src/api/search.rs | 68 +++++++++++++------ .../app/$libraryId/Explorer/queries/index.ts | 3 + .../queries/useExplorerInfiniteQuery.ts | 10 +++ .../{ => queries}/useObjectsInfiniteQuery.ts | 22 ++---- .../{ => queries}/usePathsInfiniteQuery.ts | 24 +++---- interface/app/$libraryId/location/$id.tsx | 2 +- interface/app/$libraryId/overview/data.ts | 3 +- packages/client/src/core.ts | 10 +-- 8 files changed, 81 insertions(+), 61 deletions(-) create mode 100644 interface/app/$libraryId/Explorer/queries/index.ts create mode 100644 interface/app/$libraryId/Explorer/queries/useExplorerInfiniteQuery.ts rename interface/app/$libraryId/Explorer/{ => queries}/useObjectsInfiniteQuery.ts (69%) rename interface/app/$libraryId/Explorer/{ => queries}/usePathsInfiniteQuery.ts (82%) diff --git a/core/src/api/search.rs b/core/src/api/search.rs index 71d916333..c4261ac91 100644 --- a/core/src/api/search.rs +++ b/core/src/api/search.rs @@ -203,7 +203,7 @@ pub enum FilePathObjectCursor { #[derive(Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] pub enum FilePathCursorVariant { - None(file_path::pub_id::Type), + None, Name(CursorOrderItem), // SizeInBytes(CursorOrderItem>), DateCreated(CursorOrderItem>), @@ -222,7 +222,7 @@ pub struct FilePathCursor { #[derive(Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] pub enum ObjectCursor { - None(object::pub_id::Type), + None, DateAccessed(CursorOrderItem>), Kind(CursorOrderItem), } @@ -256,10 +256,10 @@ impl ObjectOrder { #[derive(Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] -pub enum OrderAndPagination { +pub enum OrderAndPagination { OrderOnly(TOrder), Offset { offset: i32, order: Option }, - Cursor(TCursor), + Cursor { id: TId, cursor: TCursor }, } #[derive(Deserialize, Type, Debug, Default, Clone, Copy)] @@ -396,7 +396,8 @@ pub fn mount() -> AlphaRouter { struct FilePathSearchArgs { take: u8, #[specta(optional)] - order_and_pagination: Option>, + order_and_pagination: + Option>, #[serde(default)] filter: FilePathFilterArgs, #[serde(default = "default_group_directories")] @@ -442,7 +443,7 @@ pub fn mount() -> AlphaRouter { query = query.order_by(order.into_param()) } } - OrderAndPagination::Cursor(cursor) => { + OrderAndPagination::Cursor { id, cursor } => { // This may seem dumb but it's vital! // If we're grouping by directories + all directories have been fetched, // we don't want to include them in the results. @@ -457,10 +458,21 @@ pub fn mount() -> AlphaRouter { ($field:ident, $item:ident) => {{ let item = $item; - query.add_where(match item.order { - SortOrder::Asc => file_path::$field::gt(item.data), - SortOrder::Desc => file_path::$field::lt(item.data), - }); + let data = item.data.clone(); + + query.add_where(or![ + match item.order { + SortOrder::Asc => file_path::$field::gt(data), + SortOrder::Desc => file_path::$field::lt(data), + }, + prisma_client_rust::and![ + file_path::$field::equals(Some(item.data)), + match item.order { + SortOrder::Asc => file_path::id::gt(id), + SortOrder::Desc => file_path::id::lt(id), + } + ] + ]); query = query .order_by(file_path::$field::order(item.order.into())); @@ -468,8 +480,8 @@ pub fn mount() -> AlphaRouter { } match cursor.variant { - FilePathCursorVariant::None(item) => { - query = query.cursor(file_path::pub_id::equals(item)); + FilePathCursorVariant::None => { + query.add_where(file_path::id::gt(id)); } FilePathCursorVariant::Name(item) => arm!(name, item), FilePathCursorVariant::DateCreated(item) => { @@ -511,8 +523,8 @@ pub fn mount() -> AlphaRouter { } }; - query = query - .order_by(file_path::pub_id::order(prisma::SortOrder::Asc)); + query = + query.order_by(file_path::id::order(prisma::SortOrder::Asc)); } } } @@ -574,7 +586,8 @@ pub fn mount() -> AlphaRouter { struct ObjectSearchArgs { take: u8, #[specta(optional)] - order_and_pagination: Option>, + order_and_pagination: + Option>, #[serde(default)] filter: ObjectFilterArgs, } @@ -607,15 +620,26 @@ pub fn mount() -> AlphaRouter { query = query.order_by(order.into_param()) } } - OrderAndPagination::Cursor(cursor) => { + OrderAndPagination::Cursor { id, cursor } => { macro_rules! arm { ($field:ident, $item:ident) => {{ let item = $item; - query.add_where(match item.order { - SortOrder::Asc => object::$field::gt(item.data), - SortOrder::Desc => object::$field::lt(item.data), - }); + let data = item.data.clone(); + + query.add_where(or![ + match item.order { + SortOrder::Asc => object::$field::gt(data), + SortOrder::Desc => object::$field::lt(data), + }, + prisma_client_rust::and![ + object::$field::equals(Some(item.data)), + match item.order { + SortOrder::Asc => object::id::gt(id), + SortOrder::Desc => object::id::lt(id), + } + ] + ]); query = query .order_by(object::$field::order(item.order.into())); @@ -623,8 +647,8 @@ pub fn mount() -> AlphaRouter { } match cursor { - ObjectCursor::None(item) => { - query = query.cursor(object::pub_id::equals(item)); + ObjectCursor::None => { + query.add_where(object::id::gt(id)); } ObjectCursor::Kind(item) => arm!(kind, item), ObjectCursor::DateAccessed(item) => arm!(date_accessed, item), diff --git a/interface/app/$libraryId/Explorer/queries/index.ts b/interface/app/$libraryId/Explorer/queries/index.ts new file mode 100644 index 000000000..254abf092 --- /dev/null +++ b/interface/app/$libraryId/Explorer/queries/index.ts @@ -0,0 +1,3 @@ +export * from './useExplorerInfiniteQuery'; +export * from './usePathsInfiniteQuery'; +export * from './useObjectsInfiniteQuery'; diff --git a/interface/app/$libraryId/Explorer/queries/useExplorerInfiniteQuery.ts b/interface/app/$libraryId/Explorer/queries/useExplorerInfiniteQuery.ts new file mode 100644 index 000000000..7729008e2 --- /dev/null +++ b/interface/app/$libraryId/Explorer/queries/useExplorerInfiniteQuery.ts @@ -0,0 +1,10 @@ +import { UseInfiniteQueryOptions } from '@tanstack/react-query'; +import { ExplorerItem, LibraryConfigWrapped, SearchData } from '@sd/client'; +import { Ordering } from '../store'; +import { UseExplorerSettings } from '../useExplorer'; + +export type UseExplorerInfiniteQueryArgs = { + library: LibraryConfigWrapped; + arg: TArg; + settings: UseExplorerSettings; +} & Pick>, 'enabled'>; diff --git a/interface/app/$libraryId/Explorer/useObjectsInfiniteQuery.ts b/interface/app/$libraryId/Explorer/queries/useObjectsInfiniteQuery.ts similarity index 69% rename from interface/app/$libraryId/Explorer/useObjectsInfiniteQuery.ts rename to interface/app/$libraryId/Explorer/queries/useObjectsInfiniteQuery.ts index 92a6f8a8a..f7fbf94be 100644 --- a/interface/app/$libraryId/Explorer/useObjectsInfiniteQuery.ts +++ b/interface/app/$libraryId/Explorer/queries/useObjectsInfiniteQuery.ts @@ -1,27 +1,19 @@ -import { UseInfiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; import { ExplorerItem, - LibraryConfigWrapped, ObjectCursor, ObjectOrder, ObjectSearchArgs, - OrderAndPagination, - SearchData, useRspcLibraryContext } from '@sd/client'; -import { getExplorerStore } from './store'; -import { UseExplorerSettings } from './useExplorer'; +import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery'; export function useObjectsInfiniteQuery({ library, arg, settings, ...args -}: { - library: LibraryConfigWrapped; - arg: ObjectSearchArgs; - settings: UseExplorerSettings; -} & Pick>, 'enabled'>) { +}: UseExplorerInfiniteQueryArgs) { const ctx = useRspcLibraryContext(); const explorerSettings = settings.useSettingsSnapshot(); @@ -35,14 +27,14 @@ export function useObjectsInfiniteQuery({ const cItem: Extract = pageParam; const { order } = explorerSettings; - let orderAndPagination: OrderAndPagination | undefined; + let orderAndPagination: (typeof arg)['orderAndPagination']; if (!cItem) { if (order) orderAndPagination = { orderOnly: order }; } else { let cursor: ObjectCursor | undefined; - if (!order) cursor = { none: [] }; + if (!order) cursor = 'none'; else if (cItem) { const direction = order.value; @@ -61,7 +53,7 @@ export function useObjectsInfiniteQuery({ } } - if (cursor) orderAndPagination = { cursor }; + if (cursor) orderAndPagination = { cursor: { cursor, id: cItem.item.id } }; } arg.orderAndPagination = orderAndPagination; @@ -70,7 +62,7 @@ export function useObjectsInfiniteQuery({ }, getNextPageParam: (lastPage) => { if (lastPage.items.length < arg.take) return undefined; - else return lastPage.items[arg.take]; + else return lastPage.items[arg.take - 1]; }, ...args }); diff --git a/interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts b/interface/app/$libraryId/Explorer/queries/usePathsInfiniteQuery.ts similarity index 82% rename from interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts rename to interface/app/$libraryId/Explorer/queries/usePathsInfiniteQuery.ts index b6e1b4130..48cfaac2a 100644 --- a/interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts +++ b/interface/app/$libraryId/Explorer/queries/usePathsInfiniteQuery.ts @@ -1,29 +1,21 @@ -import { UseInfiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; import { ExplorerItem, - FilePathCursor, FilePathCursorVariant, FilePathObjectCursor, FilePathOrder, FilePathSearchArgs, - LibraryConfigWrapped, - OrderAndPagination, - SearchData, useRspcLibraryContext } from '@sd/client'; -import { getExplorerStore } from './store'; -import { UseExplorerSettings } from './useExplorer'; +import { getExplorerStore } from '../store'; +import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery'; export function usePathsInfiniteQuery({ library, arg, settings, ...args -}: { - library: LibraryConfigWrapped; - arg: FilePathSearchArgs; - settings: UseExplorerSettings; -} & Pick>, 'enabled'>) { +}: UseExplorerInfiniteQueryArgs) { const ctx = useRspcLibraryContext(); const explorerSettings = settings.useSettingsSnapshot(); @@ -37,14 +29,14 @@ export function usePathsInfiniteQuery({ const cItem: Extract = pageParam; const { order } = explorerSettings; - let orderAndPagination: OrderAndPagination | undefined; + let orderAndPagination: (typeof arg)['orderAndPagination']; if (!cItem) { if (order) orderAndPagination = { orderOnly: order }; } else { let variant: FilePathCursorVariant | undefined; - if (!order) variant = { none: [] }; + if (!order) variant = 'none'; else if (cItem) { switch (order.field) { case 'name': { @@ -136,7 +128,7 @@ export function usePathsInfiniteQuery({ if (variant) orderAndPagination = { - cursor: { variant, isDir: cItem.item.is_dir } + cursor: { cursor: { variant, isDir: cItem.item.is_dir }, id: cItem.item.id } }; } @@ -146,7 +138,7 @@ export function usePathsInfiniteQuery({ }, getNextPageParam: (lastPage) => { if (lastPage.items.length < arg.take) return undefined; - else return lastPage.items[arg.take]; + else return lastPage.items[arg.take - 1]; }, onSuccess: () => getExplorerStore().resetNewThumbnails(), ...args diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx index 0a4d8f27c..9f331bcdb 100644 --- a/interface/app/$libraryId/location/$id.tsx +++ b/interface/app/$libraryId/location/$id.tsx @@ -18,9 +18,9 @@ import { useKeyDeleteFile, useZodRouteParams } from '~/hooks'; import Explorer from '../Explorer'; import { ExplorerContextProvider } from '../Explorer/Context'; import { DefaultTopBarOptions } from '../Explorer/TopBarOptions'; +import { usePathsInfiniteQuery } from '../Explorer/queries'; import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store'; import { UseExplorerSettings, useExplorer, useExplorerSettings } from '../Explorer/useExplorer'; -import { usePathsInfiniteQuery } from '../Explorer/usePathsInfiniteQuery'; import { useExplorerSearchParams } from '../Explorer/util'; import { TopBarPortal } from '../TopBar/Portal'; import LocationOptions from './LocationOptions'; diff --git a/interface/app/$libraryId/overview/data.ts b/interface/app/$libraryId/overview/data.ts index 1953f6465..e84712478 100644 --- a/interface/app/$libraryId/overview/data.ts +++ b/interface/app/$libraryId/overview/data.ts @@ -11,14 +11,13 @@ import { useLibraryQuery, useRspcLibraryContext } from '@sd/client'; +import { useObjectsInfiniteQuery, usePathsInfiniteQuery } from '../Explorer/queries'; import { createDefaultExplorerSettings, filePathOrderingKeysSchema, objectOrderingKeysSchema } from '../Explorer/store'; import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer'; -import { useObjectsInfiniteQuery } from '../Explorer/useObjectsInfiniteQuery'; -import { usePathsInfiniteQuery } from '../Explorer/usePathsInfiniteQuery'; import { usePageLayoutContext } from '../PageLayout/Context'; export const IconForCategory: Partial> = { diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 0d711149b..19e56ae3f 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -168,7 +168,7 @@ export type FilePath = { id: number; pub_id: number[]; is_dir: boolean | null; c export type FilePathCursor = { isDir: boolean; variant: FilePathCursorVariant } -export type FilePathCursorVariant = { none: number[] } | { name: CursorOrderItem } | { dateCreated: CursorOrderItem } | { dateModified: CursorOrderItem } | { dateIndexed: CursorOrderItem } | { object: FilePathObjectCursor } +export type FilePathCursorVariant = "none" | { name: CursorOrderItem } | { dateCreated: CursorOrderItem } | { dateModified: CursorOrderItem } | { dateIndexed: CursorOrderItem } | { object: FilePathObjectCursor } export type FilePathFilterArgs = { locationId?: number | null; search?: string | null; extension?: string | null; createdAt?: OptionalRange; path?: string | null; object?: ObjectFilterArgs | null } @@ -176,7 +176,7 @@ export type FilePathObjectCursor = { dateAccessed: CursorOrderItem } | { export type FilePathOrder = { field: "name"; value: SortOrder } | { field: "sizeInBytes"; value: SortOrder } | { field: "dateCreated"; value: SortOrder } | { field: "dateModified"; value: SortOrder } | { field: "dateIndexed"; value: SortOrder } | { field: "object"; value: ObjectOrder } -export type FilePathSearchArgs = { take: number; orderAndPagination?: OrderAndPagination | null; filter?: FilePathFilterArgs; groupDirectories?: boolean } +export type FilePathSearchArgs = { take: number; orderAndPagination?: OrderAndPagination | null; filter?: FilePathFilterArgs; groupDirectories?: boolean } export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: Object | null } @@ -307,7 +307,7 @@ export type NotificationId = { type: "library"; id: [string, number] } | { type: export type Object = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null } -export type ObjectCursor = { none: number[] } | { dateAccessed: CursorOrderItem } | { kind: CursorOrderItem } +export type ObjectCursor = "none" | { dateAccessed: CursorOrderItem } | { kind: CursorOrderItem } export type ObjectFilterArgs = { favorite?: boolean | null; hidden?: ObjectHiddenFilter; dateAccessed?: MaybeNot | null; kind?: number[]; tags?: number[]; category?: Category | null } @@ -315,7 +315,7 @@ export type ObjectHiddenFilter = "exclude" | "include" export type ObjectOrder = { field: "dateAccessed"; value: SortOrder } | { field: "kind"; value: SortOrder } -export type ObjectSearchArgs = { take: number; orderAndPagination?: OrderAndPagination | null; filter?: ObjectFilterArgs } +export type ObjectSearchArgs = { take: number; orderAndPagination?: OrderAndPagination | null; filter?: ObjectFilterArgs } export type ObjectValidatorArgs = { id: number; path: string } @@ -329,7 +329,7 @@ export type OperatingSystem = "Windows" | "Linux" | "MacOS" | "Ios" | "Android" export type OptionalRange = { from: T | null; to: T | null } -export type OrderAndPagination = { orderOnly: TOrder } | { offset: { offset: number; order: TOrder | null } } | { cursor: TCursor } +export type OrderAndPagination = { orderOnly: TOrder } | { offset: { offset: number; order: TOrder | null } } | { cursor: { id: TId; cursor: TCursor } } export type Orientation = "Normal" | "MirroredHorizontal" | "CW90" | "MirroredVertical" | "MirroredHorizontalAnd270CW" | "MirroredHorizontalAnd90CW" | "CW180" | "CW270"