[MOB-69] Infinite scroll & explorer query hooks (#2197)

* run sim before everything

* move passwordmeter

* annoying maestro

* remove bad extension from recom

* move explorer query logic to @sd/client

* update mobile packages

* working explorer

* search with the new query

* tag explorer

* revert maestro version bump
This commit is contained in:
Utku 2024-03-13 16:53:00 -04:00 committed by GitHub
parent 72451a07bc
commit 4fc8dcfb48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 733 additions and 485 deletions

View file

@ -201,7 +201,9 @@ jobs:
- name: Run Simulator
uses: futureware-tech/simulator-action@v3
with:
model: 'iPhone SE (3rd generation)'
model: 'iPhone 15'
os_version: 17
erase_before_boot: false
- name: Run Tests
env:

View file

@ -7,7 +7,6 @@
"bradlc.vscode-tailwindcss", // Provides Tailwind CSS IntelliSense
"prisma.prisma", // Prisma is an open-source database toolkit
"dbaeumer.vscode-eslint", // Integrates ESLint JavaScript into VS Code
"esbenp.prettier-vscode", // Code formatter using prettier
"inlang.vs-code-extension" // Improved i18n DX (Internationalization Developer Experience)
"esbenp.prettier-vscode" // Code formatter using prettier
]
}

View file

@ -23,12 +23,12 @@
"@hookform/resolvers": "^3.1.0",
"@oscartbeaumont-sd/rspc-client": "=0.0.0-main-dc31e5b2",
"@oscartbeaumont-sd/rspc-react": "=0.0.0-main-dc31e5b2",
"@react-native-async-storage/async-storage": "~1.21.0",
"@react-native-masked-view/masked-view": "^0.3.0",
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/drawer": "^6.6.3",
"@react-navigation/native": "^6.1.7",
"@react-navigation/stack": "^6.3.17",
"@react-native-async-storage/async-storage": "~1.22.3",
"@react-native-masked-view/masked-view": "^0.3.1",
"@react-navigation/bottom-tabs": "^6.5.19",
"@react-navigation/drawer": "^6.6.14",
"@react-navigation/native": "^6.1.16",
"@react-navigation/stack": "^6.3.28",
"@sd/assets": "workspace:*",
"@sd/client": "workspace:*",
"@shopify/flash-list": "1.6.3",
@ -36,8 +36,8 @@
"babel-preset-solid": "^1.8.9",
"class-variance-authority": "^0.7.0",
"dayjs": "^1.11.10",
"event-target-polyfill": "^0.0.3",
"expo": "~50.0.7",
"event-target-polyfill": "^0.0.4",
"expo": "~50.0.11",
"expo-av": "^13.10.5",
"expo-blur": "^12.9.2",
"expo-build-properties": "~0.11.1",
@ -45,9 +45,9 @@
"expo-media-library": "~15.9.1",
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
"lottie-react-native": "6.5.1",
"lottie-react-native": "6.7.0",
"metro-react-native-babel-transformer": "^0.77.0",
"moti": "^0.26.0",
"moti": "^0.28.1",
"phosphor-react-native": "^2.0.0",
"react": "^18.2.0",
"react-hook-form": "^7.47.0",
@ -67,7 +67,7 @@
"react-native-wheel-color-picker": "^1.2.0",
"rive-react-native": "^6.2.3",
"solid-js": "^1.8.8",
"twrnc": "^3.6.4",
"twrnc": "^4.1.0",
"use-count-up": "^3.0.1",
"use-debounce": "^9.0.4",
"valtio": "^1.11.2",
@ -75,7 +75,7 @@
},
"devDependencies": {
"@babel/core": "^7.23.9",
"@rnx-kit/metro-config": "^1.3.14",
"@rnx-kit/metro-config": "^1.3.15",
"@sd/config": "workspace:*",
"@types/react": "^18.2.61",
"babel-plugin-module-resolver": "^5.0.0",

View file

@ -1,10 +1,11 @@
import { useNavigation } from '@react-navigation/native';
import { FlashList } from '@shopify/flash-list';
import { FlashList, FlashListProps } from '@shopify/flash-list';
import { UseInfiniteQueryResult } from '@tanstack/react-query';
import { AnimatePresence, MotiView } from 'moti';
import { MonitorPlay, Rows, SlidersHorizontal, SquaresFour } from 'phosphor-react-native';
import { useState } from 'react';
import { Pressable, View } from 'react-native';
import { isPath, type ExplorerItem } from '@sd/client';
import { ActivityIndicator, Pressable, View } from 'react-native';
import { isPath, SearchData, type ExplorerItem } from '@sd/client';
import Layout from '~/constants/Layout';
import { tw } from '~/lib/tailwind';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
@ -16,11 +17,15 @@ import FileItem from './FileItem';
import FileRow from './FileRow';
type ExplorerProps = {
items?: ExplorerItem[];
tabHeight?: boolean;
items: ExplorerItem[] | null;
/** Function to fetch next page of items. */
loadMore: () => void;
query: UseInfiniteQueryResult<SearchData<ExplorerItem>>;
count?: number;
};
const Explorer = ({ items, tabHeight }: ExplorerProps) => {
const Explorer = (props: ExplorerProps) => {
const navigation = useNavigation<BrowseStackScreenProps<'Location'>['navigation']>();
const explorerStore = useExplorerStore();
const [layoutMode, setLayoutMode] = useState<ExplorerLayoutMode>(getExplorerStore().layoutMode);
@ -46,7 +51,7 @@ const Explorer = ({ items, tabHeight }: ExplorerProps) => {
}
return (
<ScreenContainer tabHeight={tabHeight} scrollview={false} style={'gap-0 py-0'}>
<ScreenContainer tabHeight={props.tabHeight} scrollview={false} style={'gap-0 py-0'}>
{/* Header */}
<View style={tw`flex flex-row items-center justify-between`}>
{/* Sort By */}
@ -80,36 +85,33 @@ const Explorer = ({ items, tabHeight }: ExplorerProps) => {
)} */}
</View>
{/* Items */}
{items && (
<FlashList
key={layoutMode}
numColumns={layoutMode === 'grid' ? getExplorerStore().gridNumColumns : 1}
data={items}
keyExtractor={(item) =>
item.type === 'NonIndexedPath'
? item.item.path
: item.type === 'SpacedropPeer'
? item.item.name
: item.item.id.toString()
}
renderItem={({ item }) => (
<Pressable onPress={() => handlePress(item)}>
{layoutMode === 'grid' ? (
<FileItem data={item} />
) : (
<FileRow data={item} />
)}
</Pressable>
)}
contentContainerStyle={tw`p-2`}
extraData={layoutMode}
estimatedItemSize={
layoutMode === 'grid'
? Layout.window.width / getExplorerStore().gridNumColumns
: getExplorerStore().listItemSize
}
/>
)}
<FlashList
key={layoutMode}
numColumns={layoutMode === 'grid' ? getExplorerStore().gridNumColumns : 1}
data={props.items ?? []}
keyExtractor={(item) =>
item.type === 'NonIndexedPath'
? item.item.path
: item.type === 'SpacedropPeer'
? item.item.name
: item.item.id.toString()
}
renderItem={({ item }) => (
<Pressable onPress={() => handlePress(item)}>
{layoutMode === 'grid' ? <FileItem data={item} /> : <FileRow data={item} />}
</Pressable>
)}
contentContainerStyle={tw`p-2`}
extraData={layoutMode}
estimatedItemSize={
layoutMode === 'grid'
? Layout.window.width / getExplorerStore().gridNumColumns
: getExplorerStore().listItemSize
}
onEndReached={() => props.loadMore?.()}
onEndReachedThreshold={0.6}
ListFooterComponent={props.query.isFetchingNextPage ? <ActivityIndicator /> : null}
/>
</ScreenContainer>
);
};

View file

@ -95,11 +95,11 @@ const OverviewStats = ({ stats }: Props) => {
}
return (
<StatItem
key={`${library.uuid} ${key}`}
key={`${library.uuid}_${key}`}
title={StatItemNames[key as keyof Statistics]!}
bytes={bytes}
isLoading={stats.isLoading}
style={tw`${isTotalStat ? 'h-[101px] w-full' : 'w-full'} flex-1`}
style={twStyle(isTotalStat && 'h-[101px]', 'w-full flex-1')}
/>
);
});

View file

@ -24,6 +24,7 @@ export const kinds = Object.keys(ObjectKind)
const Kind = () => {
const searchStore = useSearchStore();
return (
<MotiView
layout={LinearTransition.duration(300)}

View file

@ -6,7 +6,7 @@ import { changeTwTheme, tw } from '~/lib/tailwind';
export function useTheme() {
// Enables screen size breakpoints, etc. for tailwind
useDeviceContext(tw, { withDeviceColorScheme: false });
useDeviceContext(tw, { initialColorScheme: 'light', observeDeviceColorSchemeChanges: false });
const [_, forceUpdate] = useReducer((x) => x + 1, 0);

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo } from 'react';
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
import { useEffect } from 'react';
import { useCache, useLibraryQuery, useNodes, usePathsExplorerQuery } from '@sd/client';
import Explorer from '~/components/explorer/Explorer';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
import { getExplorerStore } from '~/stores/explorerStore';
@ -11,25 +11,31 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
useNodes(location.data?.nodes);
const locationData = useCache(location.data?.item);
// FIXME: This is the correct query, but it doesn't work and then provides a deserialization error.
const paths = useLibraryQuery([
'search.paths',
{
const paths = usePathsExplorerQuery({
arg: {
filters: [
// ...search.allFilters,
{ filePath: { locations: { in: [id] } } },
{
filePath: {
// locations: { in: [Number(id)] }, // temporarlily disabled to navigate into folders. Note: This makes the query return all locations in the library.
path: { location_id: id, path: path ?? '', include_descendants: true }
path: {
location_id: id,
path: path ?? '',
include_descendants: false
// include_descendants:
// search.search !== '' ||
// search.dynamicFilters.length > 0 ||
// (layoutMode === 'media' && mediaViewWithDescendants)
}
}
}
],
take: 100
}
]);
const pathsItemsReferences = useMemo(() => paths.data?.items ?? [], [paths.data]);
useNodes(paths.data?.nodes);
const pathsItems = useCache(pathsItemsReferences);
// !showHiddenFiles && { filePath: { hidden: false } }
].filter(Boolean) as any,
take: 30
},
order: null,
onSuccess: () => getExplorerStore().resetNewThumbnails()
});
useEffect(() => {
// Set screen title to location.
@ -53,5 +59,5 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
getExplorerStore().path = path ?? '';
}, [id, path]);
return <Explorer items={pathsItems} />;
return <Explorer {...paths} />;
}

View file

@ -1,25 +1,20 @@
import { useEffect } from 'react';
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
import { useCache, useLibraryQuery, useNodes, useObjectsExplorerQuery } from '@sd/client';
import Explorer from '~/components/explorer/Explorer';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
export default function TagScreen({ navigation, route }: BrowseStackScreenProps<'Tag'>) {
const { id } = route.params;
const search = useLibraryQuery([
'search.objects',
{
filters: [{ object: { tags: { in: [id] } } }],
take: 100
}
]);
useNodes(search.data?.nodes);
const searchData = useCache(search.data?.items);
const tag = useLibraryQuery(['tags.get', id]);
useNodes(tag.data?.nodes);
const tagData = useCache(tag.data?.item);
const objects = useObjectsExplorerQuery({
arg: { filters: [{ object: { tags: { in: [id] } } }], take: 30 },
order: null
});
useEffect(() => {
// Set screen title to tag name.
navigation.setOptions({
@ -27,5 +22,5 @@ export default function TagScreen({ navigation, route }: BrowseStackScreenProps<
});
}, [tagData?.name, navigation]);
return <Explorer items={searchData} />;
return <Explorer {...objects} />;
}

View file

@ -1,12 +1,15 @@
import { CheckCircle } from 'phosphor-react-native';
import React from 'react';
import { useLibraryQuery } from '@sd/client';
import { Pressable, View } from 'react-native';
import { JobManagerContextProvider, useLibraryQuery } from '@sd/client';
import { PulseAnimation } from '~/components/animation/lottie';
import BrowseCategories from '~/components/browse/BrowseCategories';
import BrowseLocations from '~/components/browse/BrowseLocations';
import BrowseTags from '~/components/browse/BrowseTags';
import Jobs from '~/components/browse/Jobs';
import { ModalRef } from '~/components/layout/Modal';
import ScreenContainer from '~/components/layout/ScreenContainer';
import { JobManagerModal } from '~/components/modal/job/JobManagerModal';
import { tw } from '~/lib/tailwind';
function JobIcon() {
@ -19,20 +22,23 @@ function JobIcon() {
}
export default function BrowseScreen() {
const modalRef = React.useRef<ModalRef>(null);
return (
<ScreenContainer>
<BrowseCategories />
<BrowseLocations />
<BrowseTags />
<Jobs />
{/* <View style={tw`flex-row items-center w-full gap-x-4`}>
<JobManagerContextProvider>
<Pressable onPress={() => modalRef.current?.present()}>
<JobIcon />
</Pressable>
<JobManagerModal ref={modalRef} />
</JobManagerContextProvider>
</View> */}
{/* TODO: Remove this when the new job manager is live, this is here for debugging purposes. */}
<View style={tw`w-full flex-row items-center gap-x-4`}>
<JobManagerContextProvider>
<Pressable onPress={() => modalRef.current?.present()}>
<JobIcon />
</Pressable>
<JobManagerModal ref={modalRef} />
</JobManagerContextProvider>
</View>
</ScreenContainer>
);
}

View file

@ -2,7 +2,7 @@ import { ArrowLeft, FunnelSimple, MagnifyingGlass } from 'phosphor-react-native'
import { Suspense, useDeferredValue, useMemo, useState } from 'react';
import { ActivityIndicator, Pressable, TextInput, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { getExplorerItemData, SearchFilterArgs, useCache, useLibraryQuery } from '@sd/client';
import { SearchFilterArgs, useObjectsExplorerQuery } from '@sd/client';
import Explorer from '~/components/explorer/Explorer';
import FiltersBar from '~/components/search/filters/FiltersBar';
import { tw, twStyle } from '~/lib/tailwind';
@ -10,8 +10,6 @@ import { SearchStackScreenProps } from '~/navigation/SearchStack';
import { getExplorerStore } from '~/stores/explorerStore';
import { useSearchStore } from '~/stores/searchStore';
// TODO: Animations!
const SearchScreen = ({ navigation }: SearchStackScreenProps<'Home'>) => {
const { top } = useSafeAreaInsets();
const [loading, setLoading] = useState(false);
@ -35,36 +33,16 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Home'>) => {
return filters;
}, [deferredSearch]);
const query = useLibraryQuery(
[
'search.paths',
{
// ...args,
filters,
take: 100
}
],
{
suspense: true,
enabled: !!deferredSearch,
onSuccess: () => getExplorerStore().resetNewThumbnails()
}
);
const pathsItemsReferences = useMemo(() => query.data?.items ?? [], [query.data]);
const pathsItems = useCache(pathsItemsReferences);
const items = useMemo(() => {
// Mobile does not thave media layout
// if (explorerStore.layoutMode !== 'media') return pathsItems;
return (
pathsItems?.filter((item) => {
const { kind } = getExplorerItemData(item);
return kind === 'Video' || kind === 'Image';
}) ?? []
);
}, [pathsItems]);
const objects = useObjectsExplorerQuery({
arg: {
take: 30,
filters
},
order: null,
suspense: true,
enabled: !!deferredSearch,
onSuccess: () => getExplorerStore().resetNewThumbnails()
});
return (
<View
@ -74,7 +52,7 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Home'>) => {
>
{/* Header */}
<View style={tw`border-b border-app-line/50`}>
{/* Search area input container*/}
{/* Search area input container */}
<View style={tw`flex-row items-center gap-4 px-5 pb-3`}>
{/* Back Button */}
<Pressable
@ -116,11 +94,7 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Home'>) => {
/>
</View>
</View>
<Pressable
onPress={() => {
navigation.navigate('Filters');
}}
>
<Pressable onPress={() => navigation.navigate('Filters')}>
<View
style={tw`h-10 w-10 items-center justify-center rounded-md border border-app-line/50 bg-app-box/50`}
>
@ -134,7 +108,7 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Home'>) => {
{/* Content */}
<View style={tw`flex-1`}>
<Suspense fallback={<ActivityIndicator />}>
<Explorer tabHeight={false} items={items} />
<Explorer {...objects} tabHeight={false} />
</Suspense>
</View>
</View>

View file

@ -1,6 +1,6 @@
import { ContextType, createContext, PropsWithChildren, useContext } from 'react';
import { type Ordering } from '@sd/client';
import { Ordering } from './store';
import { UseExplorer } from './useExplorer';
/**

View file

@ -1,11 +1,16 @@
import {
createOrdering,
explorerLayout,
getOrderingDirection,
orderingKey,
useExplorerLayoutStore
} from '@sd/client';
import { RadixCheckbox, Select, SelectOption, Slider, tw, z } from '@sd/ui';
import { explorerLayout, useExplorerLayoutStore } from '~/../packages/client/src';
import i18n from '~/app/I18n';
import { SortOrderSchema } from '~/app/route-schemas';
import { useLocale } from '~/hooks';
import { useExplorerContext } from './Context';
import { createOrdering, getOrderingDirection, orderingKey } from './store';
const Subheading = tw.div`text-ink-dull mb-1 text-xs font-medium`;

View file

@ -6,7 +6,7 @@ import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState
import BasicSticky from 'react-sticky-el';
import { useWindowEventListener } from 'rooks';
import useResizeObserver from 'use-resize-observer';
import { type ExplorerItem } from '@sd/client';
import { createOrdering, getOrderingDirection, orderingKey, type ExplorerItem } from '@sd/client';
import { ContextMenu } from '@sd/ui';
import { TruncatedText } from '~/components';
import { useShortcut } from '~/hooks';
@ -15,7 +15,6 @@ import { isNonEmptyObject } from '~/util';
import { useLayoutContext } from '../../../Layout/Context';
import { useExplorerContext } from '../../Context';
import { getQuickPreviewStore, useQuickPreviewStore } from '../../QuickPreview/store';
import { createOrdering, getOrderingDirection, orderingKey } from '../../store';
import { uniqueId } from '../../util';
import { useExplorerViewContext } from '../Context';
import { useDragScrollable } from '../useDragScrollable';

View file

@ -1,9 +1,8 @@
import { LoadMoreTrigger, useGrid, useScrollMargin, useVirtualizer } from '@virtual-grid/react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { getExplorerItemData } from '@sd/client';
import { getExplorerItemData, getOrderingDirection, orderingKey } from '@sd/client';
import { useExplorerContext } from '../../Context';
import { getOrderingDirection, orderingKey } from '../../store';
import { getItemData, getItemId, uniqueId } from '../../util';
import { useExplorerViewContext } from '../Context';
import { DragSelect } from '../Grid/DragSelect';

View file

@ -1,7 +0,0 @@
export * from './useExplorerInfiniteQuery';
export * from './usePathsInfiniteQuery';
export * from './usePathsOffsetInfiniteQuery';
export * from './usePathsExplorerQuery';
export * from './useObjectsInfiniteQuery';
export * from './useObjectsOffsetInfiniteQuery';
export * from './useObjectsExplorerQuery';

View file

@ -7,7 +7,7 @@ import {
type ExplorerItem,
type ExplorerLayout,
type ExplorerSettings,
type SortOrder
type Ordering
} from '@sd/client';
export enum ExplorerKind {
@ -16,54 +16,6 @@ export enum ExplorerKind {
Space
}
export type Ordering = { field: string; value: SortOrder | Ordering };
// branded type for added type-safety
export type OrderingKey = string & {};
type OrderingValue<T extends Ordering, K extends string> = Extract<T, { field: K }>['value'];
export type OrderingKeys<T extends Ordering> = T extends Ordering
? {
[K in T['field']]: OrderingValue<T, K> extends SortOrder
? K
: OrderingValue<T, K> extends Ordering
? `${K}.${OrderingKeys<OrderingValue<T, K>>}`
: never;
}[T['field']]
: never;
export function orderingKey(ordering: Ordering): OrderingKey {
let base = ordering.field;
if (typeof ordering.value === 'object') {
base += `.${orderingKey(ordering.value)}`;
}
return base;
}
export function createOrdering<TOrdering extends Ordering = Ordering>(
key: OrderingKey,
value: SortOrder
): TOrdering {
return key
.split('.')
.reverse()
.reduce((acc, field, i) => {
if (i === 0)
return {
field,
value
};
else return { field, value: acc };
}, {} as any);
}
export function getOrderingDirection(ordering: Ordering): SortOrder {
if (typeof ordering.value === 'object') return getOrderingDirection(ordering.value);
else return ordering.value;
}
export const createDefaultExplorerSettings = <TOrder extends Ordering>(args?: {
order?: TOrder | null;
}) =>

View file

@ -11,8 +11,9 @@ import type {
NodeState,
Tag
} from '@sd/client';
import { type Ordering, type OrderingKeys } from '@sd/client';
import { createDefaultExplorerSettings, type Ordering, type OrderingKeys } from './store';
import { createDefaultExplorerSettings } from './store';
import { uniqueId } from './util';
export type ExplorerParent =

View file

@ -1,11 +1,10 @@
import { useMemo } from 'react';
import { ObjectFilterArgs, ObjectKindEnum, ObjectOrder, SearchFilterArgs } from '@sd/client';
import { ObjectKindEnum, ObjectOrder, SearchFilterArgs, useObjectsExplorerQuery } from '@sd/client';
import { Icon } from '~/components';
import { useRouteTitle } from '~/hooks';
import Explorer from './Explorer';
import { ExplorerContextProvider } from './Explorer/Context';
import { useObjectsExplorerQuery } from './Explorer/queries/useObjectsExplorerQuery';
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explorer/store';
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
@ -49,7 +48,7 @@ export function Component() {
{ object: { favorite: true } }
]
},
explorerSettings
order: explorerSettings.useSettingsSnapshot().order
});
const explorer = useExplorer({

View file

@ -14,6 +14,7 @@ import {
useLibrarySubscription,
useNodes,
useOnlineLocations,
usePathsExplorerQuery,
useRspcLibraryContext
} from '@sd/client';
import { Loader, Tooltip } from '@sd/ui';
@ -31,8 +32,11 @@ import { useQuickRescan } from '~/hooks/useQuickRescan';
import Explorer from '../Explorer';
import { ExplorerContextProvider } from '../Explorer/Context';
import { usePathsExplorerQuery } from '../Explorer/queries';
import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store';
import {
createDefaultExplorerSettings,
explorerStore,
filePathOrderingKeysSchema
} from '../Explorer/store';
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
import { useExplorerSearchParams } from '../Explorer/util';
@ -89,7 +93,8 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
].filter(Boolean) as any,
take
},
explorerSettings
order: explorerSettings.useSettingsSnapshot().order,
onSuccess: () => explorerStore.resetNewThumbnails()
});
const explorer = useExplorer({

View file

@ -1,11 +1,10 @@
import { useMemo } from 'react';
import { ObjectKindEnum, ObjectOrder, SearchFilterArgs } from '@sd/client';
import { ObjectKindEnum, ObjectOrder, SearchFilterArgs, useObjectsExplorerQuery } from '@sd/client';
import { Icon } from '~/components';
import { useRouteTitle } from '~/hooks';
import Explorer from './Explorer';
import { ExplorerContextProvider } from './Explorer/Context';
import { useObjectsExplorerQuery } from './Explorer/queries/useObjectsExplorerQuery';
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explorer/store';
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
@ -49,7 +48,7 @@ export function Component() {
{ object: { dateAccessed: { from: new Date(0).toISOString() } } }
]
},
explorerSettings
order: explorerSettings.useSettingsSnapshot().order
});
const explorer = useExplorer({

View file

@ -4,9 +4,9 @@ import { useMemo } from 'react';
import {
FilePathOrder,
SearchFilterArgs,
useCache,
useLibraryMutation,
useLibraryQuery
useLibraryQuery,
usePathsExplorerQuery
} from '@sd/client';
import { Button } from '@sd/ui';
import { SearchIdParamsSchema } from '~/app/route-schemas';
@ -14,8 +14,11 @@ import { useRouteTitle, useZodRouteParams } from '~/hooks';
import Explorer from '../Explorer';
import { ExplorerContextProvider } from '../Explorer/Context';
import { usePathsExplorerQuery } from '../Explorer/queries';
import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store';
import {
createDefaultExplorerSettings,
explorerStore,
filePathOrderingKeysSchema
} from '../Explorer/store';
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
import { EmptyNotice } from '../Explorer/View/EmptyNotice';
@ -55,7 +58,8 @@ export const Component = () => {
const paths = usePathsExplorerQuery({
arg: { filters: search.allFilters, take: 50 },
explorerSettings
order: explorerSettings.useSettingsSnapshot().order,
onSuccess: () => explorerStore.resetNewThumbnails()
});
const explorer = useExplorer({

View file

@ -1,13 +1,12 @@
import { useEffect, useMemo } from 'react';
import { useSearchParams as useRawSearchParams } from 'react-router-dom';
import { ObjectKindEnum, ObjectOrder } from '@sd/client';
import { ObjectKindEnum, ObjectOrder, useObjectsExplorerQuery } from '@sd/client';
import { Icon } from '~/components';
import { useRouteTitle } from '~/hooks';
import { SearchContextProvider, SearchOptions, useSearch } from '.';
import Explorer from '../Explorer';
import { ExplorerContextProvider } from '../Explorer/Context';
import { useObjectsExplorerQuery } from '../Explorer/queries/useObjectsExplorerQuery';
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from '../Explorer/store';
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
@ -36,7 +35,7 @@ export function Component() {
take: 100,
filters: search.allFilters
},
explorerSettings
order: explorerSettings.useSettingsSnapshot().order
});
const explorer = useExplorer({

View file

@ -1,12 +1,18 @@
import { useMemo } from 'react';
import { ObjectKindEnum, ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client';
import {
ObjectKindEnum,
ObjectOrder,
useCache,
useLibraryQuery,
useNodes,
useObjectsExplorerQuery
} from '@sd/client';
import { LocationIdParamsSchema } from '~/app/route-schemas';
import { Icon } from '~/components';
import { useRouteTitle, useZodRouteParams } from '~/hooks';
import Explorer from '../Explorer';
import { ExplorerContextProvider } from '../Explorer/Context';
import { useObjectsExplorerQuery } from '../Explorer/queries';
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from '../Explorer/store';
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
@ -48,7 +54,7 @@ export function Component() {
const objects = useObjectsExplorerQuery({
arg: { take: 100, filters: search.allFilters },
explorerSettings
order: explorerSettings.useSettingsSnapshot().order
});
const explorer = useExplorer({

View file

@ -0,0 +1,57 @@
import { SortOrder } from '../core';
export * from './useExplorerInfiniteQuery';
export * from './usePathsInfiniteQuery';
export * from './usePathsOffsetInfiniteQuery';
export * from './usePathsExplorerQuery';
export * from './useObjectsInfiniteQuery';
export * from './useObjectsOffsetInfiniteQuery';
export * from './useObjectsExplorerQuery';
export type Ordering = { field: string; value: SortOrder | Ordering };
// branded type for added type-safety
export type OrderingKey = string & {};
type OrderingValue<T extends Ordering, K extends string> = Extract<T, { field: K }>['value'];
export type OrderingKeys<T extends Ordering> = T extends Ordering
? {
[K in T['field']]: OrderingValue<T, K> extends SortOrder
? K
: OrderingValue<T, K> extends Ordering
? `${K}.${OrderingKeys<OrderingValue<T, K>>}`
: never;
}[T['field']]
: never;
export function orderingKey(ordering: Ordering): OrderingKey {
let base = ordering.field;
if (typeof ordering.value === 'object') {
base += `.${orderingKey(ordering.value)}`;
}
return base;
}
export function createOrdering<TOrdering extends Ordering = Ordering>(
key: OrderingKey,
value: SortOrder
): TOrdering {
return key
.split('.')
.reverse()
.reduce((acc, field, i) => {
if (i === 0)
return {
field,
value
};
else return { field, value: acc };
}, {} as any);
}
export function getOrderingDirection(ordering: Ordering): SortOrder {
if (typeof ordering.value === 'object') return getOrderingDirection(ordering.value);
else return ordering.value;
}

View file

@ -1,10 +1,10 @@
import { UseInfiniteQueryOptions } from '@tanstack/react-query';
import { ExplorerItem, SearchData } from '@sd/client';
import { Ordering } from '../store';
import { UseExplorerSettings } from '../useExplorer';
import { ExplorerItem, SearchData } from '../core';
import { Ordering } from './index';
export type UseExplorerInfiniteQueryArgs<TArg, TOrder extends Ordering> = {
arg: TArg;
explorerSettings: UseExplorerSettings<TOrder>;
order: TOrder | null;
onSuccess?: () => void;
} & Pick<UseInfiniteQueryOptions<SearchData<ExplorerItem>>, 'enabled' | 'suspense'>;

View file

@ -1,6 +1,8 @@
import { UseInfiniteQueryResult, UseQueryResult } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { SearchData, useCache } from '@sd/client';
import { useCache } from '../cache';
import { SearchData } from '../core';
export function useExplorerQuery<Q>(
query: UseInfiniteQueryResult<SearchData<Q>>,

View file

@ -1,13 +1,12 @@
import { ObjectOrder, ObjectSearchArgs, useLibraryQuery } from '@sd/client';
import { UseExplorerSettings } from '../useExplorer';
import { ObjectOrder, ObjectSearchArgs } from '../core';
import { useLibraryQuery } from '../rspc';
import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery';
import { useExplorerQuery } from './useExplorerQuery';
import { useObjectsOffsetInfiniteQuery } from './useObjectsOffsetInfiniteQuery';
export function useObjectsExplorerQuery(props: {
arg: ObjectSearchArgs;
explorerSettings: UseExplorerSettings<ObjectOrder>;
}) {
export function useObjectsExplorerQuery(
props: UseExplorerInfiniteQueryArgs<ObjectSearchArgs, ObjectOrder>
) {
const query = useObjectsOffsetInfiniteQuery(props);
const count = useLibraryQuery(['search.objectsCount', { filters: props.arg.filters }], {

View file

@ -1,35 +1,28 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import {
ExplorerItem,
ObjectCursor,
ObjectOrder,
ObjectSearchArgs,
useLibraryContext,
useNodes,
useRspcLibraryContext
} from '@sd/client';
import { useNodes } from '../cache';
import { ExplorerItem, ObjectCursor, ObjectOrder, ObjectSearchArgs } from '../core';
import { useLibraryContext } from '../hooks';
import { useRspcLibraryContext } from '../rspc';
import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery';
export function useObjectsInfiniteQuery({
arg,
explorerSettings,
order,
...args
}: UseExplorerInfiniteQueryArgs<ObjectSearchArgs, ObjectOrder>) {
const { library } = useLibraryContext();
const ctx = useRspcLibraryContext();
const settings = explorerSettings.useSettingsSnapshot();
if (settings.order) {
arg.orderAndPagination = { orderOnly: settings.order };
if (order) {
arg.orderAndPagination = { orderOnly: order };
}
const query = useInfiniteQuery({
queryKey: ['search.objects', { library_id: library.uuid, arg }] as const,
queryFn: ({ pageParam, queryKey: [_, { arg }] }) => {
const cItem: Extract<ExplorerItem, { type: 'Object' }> = pageParam;
const { order } = settings;
let orderAndPagination: (typeof arg)['orderAndPagination'];

View file

@ -1,36 +1,28 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import {
ExplorerItem,
ObjectOrder,
ObjectSearchArgs,
useLibraryContext,
useNodes,
useNormalisedCache,
useRspcLibraryContext
} from '@sd/client';
import { useNodes, useNormalisedCache } from '../cache';
import { ObjectOrder, ObjectSearchArgs } from '../core';
import { useLibraryContext } from '../hooks';
import { useRspcLibraryContext } from '../rspc';
import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery';
export function useObjectsOffsetInfiniteQuery({
arg,
explorerSettings,
order,
...args
}: UseExplorerInfiniteQueryArgs<ObjectSearchArgs, ObjectOrder>) {
const { library } = useLibraryContext();
const ctx = useRspcLibraryContext();
const settings = explorerSettings.useSettingsSnapshot();
const cache = useNormalisedCache();
if (settings.order) {
arg.orderAndPagination = { orderOnly: settings.order };
if (order) {
arg.orderAndPagination = { orderOnly: order };
}
const query = useInfiniteQuery({
queryKey: ['search.objects', { library_id: library.uuid, arg }] as const,
queryFn: async ({ pageParam, queryKey: [_, { arg }] }) => {
const { order } = settings;
let orderAndPagination: (typeof arg)['orderAndPagination'];
if (!pageParam) {

View file

@ -1,12 +1,13 @@
import { FilePathOrder, FilePathSearchArgs, useLibraryQuery } from '@sd/client';
import { UseExplorerSettings } from '../useExplorer';
import { FilePathOrder, FilePathSearchArgs } from '../core';
import { useLibraryQuery } from '../rspc';
import { useExplorerQuery } from './useExplorerQuery';
import { usePathsOffsetInfiniteQuery } from './usePathsOffsetInfiniteQuery';
export function usePathsExplorerQuery(props: {
arg: FilePathSearchArgs;
explorerSettings: UseExplorerSettings<FilePathOrder>;
order: FilePathOrder | null;
/** This callback will fire any time the query successfully fetches new data. (NOTE: This will be removed on the next major version (react-query)) */
onSuccess?: () => void;
}) {
const query = usePathsOffsetInfiniteQuery(props);

View file

@ -1,32 +1,30 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useNodes, useNormalisedCache } from '../cache';
import {
ExplorerItem,
FilePathCursorVariant,
FilePathObjectCursor,
FilePathOrder,
FilePathSearchArgs,
useLibraryContext,
useNodes,
useNormalisedCache,
useRspcLibraryContext
} from '@sd/client';
import { explorerStore } from '../store';
FilePathSearchArgs
} from '../core';
import { useLibraryContext } from '../hooks';
import { useRspcLibraryContext } from '../rspc';
import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery';
export function usePathsInfiniteQuery({
arg,
explorerSettings,
order,
onSuccess,
...args
}: UseExplorerInfiniteQueryArgs<FilePathSearchArgs, FilePathOrder>) {
const { library } = useLibraryContext();
const ctx = useRspcLibraryContext();
const settings = explorerSettings.useSettingsSnapshot();
const cache = useNormalisedCache();
if (settings.order) {
arg.orderAndPagination = { orderOnly: settings.order };
if (order) {
arg.orderAndPagination = { orderOnly: order };
if (arg.orderAndPagination.orderOnly.field === 'sizeInBytes') delete arg.take;
}
@ -34,7 +32,6 @@ export function usePathsInfiniteQuery({
queryKey: ['search.paths', { library_id: library.uuid, arg }] as const,
queryFn: async ({ pageParam, queryKey: [_, { arg }] }) => {
const cItem: Extract<ExplorerItem, { type: 'Path' }> = pageParam;
const { order } = settings;
let orderAndPagination: (typeof arg)['orderAndPagination'];
@ -133,7 +130,7 @@ export function usePathsInfiniteQuery({
if (lastPage.items.length < arg.take) return undefined;
else return lastPage.nodes[arg.take - 1];
},
onSuccess: () => explorerStore.resetNewThumbnails(),
onSuccess,
...args
});

View file

@ -1,31 +1,26 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import {
FilePathOrder,
FilePathSearchArgs,
useLibraryContext,
useNodes,
useNormalisedCache,
useRspcLibraryContext
} from '@sd/client';
import { explorerStore } from '../store';
import { useNodes, useNormalisedCache } from '../cache';
import { FilePathOrder, FilePathSearchArgs } from '../core';
import { useLibraryContext } from '../hooks';
import { useRspcLibraryContext } from '../rspc';
import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery';
export function usePathsOffsetInfiniteQuery({
arg,
explorerSettings,
order,
onSuccess,
...args
}: UseExplorerInfiniteQueryArgs<FilePathSearchArgs, FilePathOrder>) {
const take = arg.take ?? 100;
const { library } = useLibraryContext();
const ctx = useRspcLibraryContext();
const settings = explorerSettings.useSettingsSnapshot();
const cache = useNormalisedCache();
if (settings.order) {
arg.orderAndPagination = { orderOnly: settings.order };
if (order) {
arg.orderAndPagination = { orderOnly: order };
if (arg.orderAndPagination.orderOnly.field === 'sizeInBytes') delete arg.take;
}
@ -38,8 +33,6 @@ export function usePathsOffsetInfiniteQuery({
}
] satisfies [any, any],
queryFn: async ({ pageParam, queryKey: [_, { arg }] }) => {
const { order } = settings;
let orderAndPagination: (typeof arg)['orderAndPagination'];
if (!pageParam) {
@ -63,7 +56,7 @@ export function usePathsOffsetInfiniteQuery({
getNextPageParam: ({ nodes, offset, arg }) => {
if (nodes.length >= arg.take) return (offset ?? 0) + 1;
},
onSuccess: () => explorerStore.resetNewThumbnails(),
onSuccess,
...args
});

View file

@ -33,3 +33,4 @@ export * from './form';
export * from './cache';
export * from './color';
export * from './solid';
export * from './explorer';

File diff suppressed because it is too large Load diff