From effee2140dcdc806c6602842eafd414b2c665434 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 12 Apr 2024 18:17:46 +0800 Subject: [PATCH] Tag preferences (#2313) * tag preferences * types --- core/src/preferences/library.rs | 22 ++++- .../app/$libraryId/Explorer/useExplorer.ts | 31 +++---- interface/app/$libraryId/location/$id.tsx | 2 +- .../search/useSearchExplorerQuery.ts | 2 +- interface/app/$libraryId/tag/$id.tsx | 88 +++++++++++++++---- packages/client/src/core.ts | 4 +- 6 files changed, 112 insertions(+), 37 deletions(-) diff --git a/core/src/preferences/library.rs b/core/src/preferences/library.rs index 2d36e7ccb..0facd1adb 100644 --- a/core/src/preferences/library.rs +++ b/core/src/preferences/library.rs @@ -16,6 +16,9 @@ pub struct LibraryPreferences { #[serde(default)] #[specta(optional)] location: HashMap>, + #[serde(default)] + #[specta(optional)] + tag: HashMap>, } impl LibraryPreferences { @@ -56,6 +59,12 @@ pub struct LocationSettings { explorer: ExplorerSettings, } +#[derive(Clone, Serialize, Deserialize, Type, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TagSettings { + explorer: ExplorerSettings, +} + #[derive(Clone, Serialize, Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] pub struct ExplorerSettings { @@ -95,9 +104,14 @@ pub enum DoubleClickAction { impl Preferences for LibraryPreferences { fn to_kvs(self) -> PreferenceKVs { - let Self { location } = self; + let Self { location, tag } = self; - location.to_kvs().with_prefix("location") + let mut ret = vec![]; + + ret.extend(location.to_kvs().with_prefix("location")); + ret.extend(tag.to_kvs().with_prefix("tag")); + + PreferenceKVs::new(ret) } fn from_entries(mut entries: Entries) -> Self { @@ -106,6 +120,10 @@ impl Preferences for LibraryPreferences { .remove("location") .map(|value| HashMap::from_entries(value.expect_nested())) .unwrap_or_default(), + tag: entries + .remove("tag") + .map(|value| HashMap::from_entries(value.expect_nested())) + .unwrap_or_default(), } } } diff --git a/interface/app/$libraryId/Explorer/useExplorer.ts b/interface/app/$libraryId/Explorer/useExplorer.ts index 368af4111..99986a894 100644 --- a/interface/app/$libraryId/Explorer/useExplorer.ts +++ b/interface/app/$libraryId/Explorer/useExplorer.ts @@ -48,7 +48,7 @@ export interface UseExplorerProps { * @defaultValue `true` */ selectable?: boolean; - settings: ReturnType>; + settings: ReturnType>; /** * @defaultValue `true` */ @@ -89,29 +89,26 @@ export function useExplorer({ export type UseExplorer = ReturnType>; -export function useExplorerSettings({ +export function useExplorerSettings({ settings, onSettingsChanged, orderingKeys, - location + data }: { settings: ReturnType>; - onSettingsChanged?: (settings: ExplorerSettings, location: Location) => void; + onSettingsChanged?: (settings: ExplorerSettings, data: T) => void; orderingKeys?: z.ZodUnion< [z.ZodLiteral>, ...z.ZodLiteral>[]] >; - location?: Location | null; + data?: T | null; }) { const [store] = useState(() => proxy(settings)); - const updateSettings = useDebouncedCallback( - (settings: ExplorerSettings, location: Location) => { - onSettingsChanged?.(settings, location); - }, - 500 - ); + const updateSettings = useDebouncedCallback((settings: ExplorerSettings, data: T) => { + onSettingsChanged?.(settings, data); + }, 500); - useEffect(() => updateSettings.flush(), [location, updateSettings]); + useEffect(() => updateSettings.flush(), [data, updateSettings]); useEffect(() => { if (updateSettings.isPending()) return; @@ -119,12 +116,12 @@ export function useExplorerSettings({ }, [settings, store, updateSettings]); useEffect(() => { - if (!onSettingsChanged || !location) return; + if (!onSettingsChanged || !data) return; const unsubscribe = subscribe(store, () => { - updateSettings(snapshot(store) as ExplorerSettings, location); + updateSettings(snapshot(store) as ExplorerSettings, data); }); return () => unsubscribe(); - }, [store, updateSettings, location, onSettingsChanged]); + }, [store, updateSettings, data, onSettingsChanged]); return { useSettingsSnapshot: () => useSnapshot(store), @@ -139,8 +136,8 @@ export function useExplorerSettings({ }; } -export type UseExplorerSettings = ReturnType< - typeof useExplorerSettings +export type UseExplorerSettings = ReturnType< + typeof useExplorerSettings >; function useSelectedItems(items: ExplorerItem[] | null) { diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx index 67091eeb6..f5f54739a 100644 --- a/interface/app/$libraryId/location/$id.tsx +++ b/interface/app/$libraryId/location/$id.tsx @@ -272,7 +272,7 @@ function useLocationExplorerSettings(location: Location) { settings, onSettingsChanged, orderingKeys: filePathOrderingKeysSchema, - location + data: location }), preferences }; diff --git a/interface/app/$libraryId/search/useSearchExplorerQuery.ts b/interface/app/$libraryId/search/useSearchExplorerQuery.ts index 1007ca57d..400c135d4 100644 --- a/interface/app/$libraryId/search/useSearchExplorerQuery.ts +++ b/interface/app/$libraryId/search/useSearchExplorerQuery.ts @@ -13,7 +13,7 @@ import { UseSearch, UseSearchSource } from './useSearch'; export function useSearchExplorerQuery(props: { search: UseSearch; - explorerSettings: UseExplorerSettings; + explorerSettings: UseExplorerSettings; filters: SearchFilterArgs[]; take: number; paths?: { arg?: Omit; order?: FilePathOrder | null }; diff --git a/interface/app/$libraryId/tag/$id.tsx b/interface/app/$libraryId/tag/$id.tsx index 6c2e19ec3..704dc0ed7 100644 --- a/interface/app/$libraryId/tag/$id.tsx +++ b/interface/app/$libraryId/tag/$id.tsx @@ -1,8 +1,18 @@ import { useMemo } from 'react'; -import { ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client'; +import { + ExplorerSettings, + ObjectOrder, + Tag, + useCache, + useLibraryMutation, + useLibraryQuery, + useNodes, + useRspcLibraryContext +} from '@sd/client'; import { LocationIdParamsSchema } from '~/app/route-schemas'; import { Icon } from '~/components'; import { useLocale, useRouteTitle, useZodRouteParams } from '~/hooks'; +import { stringify } from '~/util/uuid'; import Explorer from '../Explorer'; import { ExplorerContextProvider } from '../Explorer/Context'; @@ -25,12 +35,7 @@ export function Component() { useRouteTitle(tag!.name ?? 'Tag'); - const explorerSettings = useExplorerSettings({ - settings: useMemo(() => { - return createDefaultExplorerSettings({ order: null }); - }, []), - orderingKeys: objectOrderingKeysSchema - }); + const { explorerSettings, preferences } = useTagExplorerSettings(tag); const search = useSearchFromSearchParams(); @@ -47,6 +52,7 @@ export function Component() { const explorer = useExplorer({ ...items, isFetchingNextPage: items.query.isFetchingNextPage, + isLoadingPreferences: preferences.isLoading, settings: explorerSettings, parent: { type: 'Tag', tag: tag! } }); @@ -76,14 +82,66 @@ export function Component() { - } - message={t('tags_notice_message')} - /> - } - /> + {!preferences.isLoading && ( + } + message={t('tags_notice_message')} + /> + } + /> + )} ); } + +function useTagExplorerSettings(tag: Tag) { + const rspc = useRspcLibraryContext(); + + const preferences = useLibraryQuery(['preferences.get']); + const updatePreferences = useLibraryMutation('preferences.update'); + + const settings = useMemo(() => { + const defaults = createDefaultExplorerSettings({ order: null }); + + if (!location) return defaults; + + const pubId = stringify(tag.pub_id); + + const settings = preferences.data?.location?.[pubId]?.explorer; + + if (!settings) return defaults; + + for (const [key, value] of Object.entries(settings)) { + if (value !== null) Object.assign(defaults, { [key]: value }); + } + + return defaults; + }, [tag, preferences.data?.location]); + + const onSettingsChanged = async (settings: ExplorerSettings, changedTag: Tag) => { + if (changedTag.id === tag.id && preferences.isLoading) return; + + const pubId = stringify(changedTag.pub_id); + + try { + await updatePreferences.mutateAsync({ + tag: { [pubId]: { explorer: settings } } + }); + rspc.queryClient.invalidateQueries(['preferences.get']); + } catch (e) { + alert('An error has occurred while updating your preferences.'); + } + }; + + return { + explorerSettings: useExplorerSettings({ + settings, + onSettingsChanged, + orderingKeys: objectOrderingKeysSchema, + data: tag + }), + preferences + }; +} diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 5d160ec19..59ce86fd3 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -403,7 +403,7 @@ export type LibraryConfigWrapped = { uuid: string; instance_id: string; instance export type LibraryName = string -export type LibraryPreferences = { location?: { [key in string]: LocationSettings } } +export type LibraryPreferences = { location?: { [key in string]: LocationSettings }; tag?: { [key in string]: TagSettings } } export type LightScanArgs = { location_id: number; sub_path: string } @@ -601,6 +601,8 @@ export type Tag = { id: number; pub_id: number[]; name: string | null; color: st export type TagCreateArgs = { name: string; color: string } +export type TagSettings = { explorer: ExplorerSettings } + export type TagUpdateArgs = { id: number; name: string | null; color: string | null } export type Target = { Object: number } | { FilePath: number }