From ae7cb3192e8df1e9f3ae7ddefe99ed660dc0bf93 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 16 Oct 2026 12:59:43 -0400 Subject: [PATCH] refactor: library queries (#32489) * refactor: library queries * fix: mutation options signature --- .../StorageTemplateSettings.svelte | 6 +- .../navigation-bar/AccountInfoPanel.svelte | 2 +- .../settings/SystemConfigButtonRow.svelte | 6 +- .../side-bar/ServerStatus.svelte | 4 +- .../side-bar/StorageSpace.svelte | 2 +- web/src/lib/constants.ts | 9 + web/src/lib/managers/event-manager.svelte.ts | 5 - .../LibraryExclusionPatternAddModal.svelte | 18 +- .../LibraryExclusionPatternEditModal.svelte | 15 +- .../lib/modals/LibraryFolderAddModal.svelte | 18 +- .../lib/modals/LibraryFolderEditModal.svelte | 15 +- web/src/lib/queries/client.ts | 97 +++++++-- web/src/lib/queries/library.query.ts | 68 +++++- web/src/lib/queries/queue.query.ts | 12 ++ web/src/lib/queries/server.query.ts | 3 - web/src/lib/queries/system-config.query.ts | 8 +- web/src/lib/queries/user-admin.query.ts | 20 ++ web/src/lib/services/library.service.ts | 196 ++---------------- web/src/lib/services/system-config.service.ts | 2 +- .../library-management/(list)/+layout.svelte | 148 ++----------- .../library-management/(list)/+layout.ts | 21 +- .../(list)/LibraryTableRow.svelte | 100 +++++++++ .../(list)/new/+page.svelte | 21 +- .../library-management/[id]/+layout.svelte | 36 +--- .../admin/library-management/[id]/+layout.ts | 14 +- .../library-management/[id]/edit/+page.svelte | 27 +-- .../routes/admin/system-settings/+page.svelte | 4 +- .../admin/system-settings/AuthSettings.svelte | 4 +- .../system-settings/BackupSettings.svelte | 4 +- .../system-settings/FFmpegSettings.svelte | 4 +- .../system-settings/ImageSettings.svelte | 4 +- .../IntegrityChecksSettings.svelte | 4 +- .../admin/system-settings/JobSettings.svelte | 4 +- .../system-settings/LibrarySettings.svelte | 4 +- .../system-settings/LoggingSettings.svelte | 4 +- .../MachineLearningSettings.svelte | 4 +- .../admin/system-settings/MapSettings.svelte | 4 +- .../system-settings/MetadataSettings.svelte | 4 +- .../NewVersionCheckSettings.svelte | 4 +- .../NightlyTasksSettings.svelte | 4 +- .../NotificationSettings.svelte | 6 +- .../system-settings/ServerSettings.svelte | 4 +- .../system-settings/TemplateSettings.svelte | 4 +- .../system-settings/ThemeSettings.svelte | 4 +- .../system-settings/TrashSettings.svelte | 4 +- .../admin/system-settings/UserSettings.svelte | 4 +- .../admin/users/(list)/new/+page.svelte | 2 +- .../routes/admin/users/[id]/edit/+page.svelte | 2 +- .../onboarding/OnboardingServerPrivacy.svelte | 6 +- 49 files changed, 459 insertions(+), 506 deletions(-) create mode 100644 web/src/lib/queries/queue.query.ts create mode 100644 web/src/lib/queries/user-admin.query.ts create mode 100644 web/src/routes/admin/library-management/(list)/LibraryTableRow.svelte diff --git a/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte b/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte index eed418c39..05657654c 100644 --- a/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte +++ b/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte @@ -6,7 +6,7 @@ import FormatMessage from '$lib/elements/FormatMessage.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; - import { createDefinedQuery, executeMutation } from '$lib/queries/client'; + import { createPreloadedQuery, executeMutation } from '$lib/queries/client'; import { getConfigQuery, updateConfigMutation } from '$lib/queries/system-config.query'; import StorageTemplateVariablesModal from '$lib/modals/StorageTemplateVariablesModal.svelte'; import { Route } from '$lib/route'; @@ -40,7 +40,7 @@ type StorageTemplateOption = (typeof StorageTemplateOption)[keyof typeof StorageTemplateOption]; const disabled = $derived(featureFlagsManager.value.configFile); - const query = createDefinedQuery(getConfigQuery()); + const query = createPreloadedQuery(getConfigQuery); const config = $derived(query.data); let configToEdit = $state(cloneDeep(config)); @@ -155,7 +155,7 @@ onDestroy(() => { if (saveOnClose) { - void executeMutation(updateConfigMutation(), { storageTemplate: configToEdit.storageTemplate }); + void executeMutation(updateConfigMutation, { storageTemplate: configToEdit.storageTemplate }); } }); diff --git a/web/src/lib/components/shared-components/navigation-bar/AccountInfoPanel.svelte b/web/src/lib/components/shared-components/navigation-bar/AccountInfoPanel.svelte index d79fb0118..3fa820e53 100644 --- a/web/src/lib/components/shared-components/navigation-bar/AccountInfoPanel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/AccountInfoPanel.svelte @@ -19,7 +19,7 @@ let { onClose }: Props = $props(); - const query = createQuery(() => getAboutInfoQuery()); + const query = createQuery(getAboutInfoQuery); const info = $derived(query.data); diff --git a/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte b/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte index fb40e600c..f8226a923 100644 --- a/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte +++ b/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte @@ -1,5 +1,5 @@ @@ -28,9 +30,13 @@ {onSubmit} submitText={$t('add')} size="small" + disabled={!value || exists} > {$t('admin.exclusion_pattern_description')} + {#if exists} + {$t('errors.exclusion_pattern_already_exists')} + {/if} diff --git a/web/src/lib/modals/LibraryExclusionPatternEditModal.svelte b/web/src/lib/modals/LibraryExclusionPatternEditModal.svelte index d2fc52a80..0fb230476 100644 --- a/web/src/lib/modals/LibraryExclusionPatternEditModal.svelte +++ b/web/src/lib/modals/LibraryExclusionPatternEditModal.svelte @@ -1,5 +1,6 @@ - + {$t('admin.exclusion_pattern_description')} diff --git a/web/src/lib/modals/LibraryFolderAddModal.svelte b/web/src/lib/modals/LibraryFolderAddModal.svelte index 6d261c4af..780205e43 100644 --- a/web/src/lib/modals/LibraryFolderAddModal.svelte +++ b/web/src/lib/modals/LibraryFolderAddModal.svelte @@ -1,7 +1,8 @@ @@ -28,9 +30,13 @@ {onSubmit} size="small" submitText={$t('add')} + disabled={!value || exists} > {$t('admin.library_folder_description')} + {#if exists} + {$t('errors.library_folder_already_exists')} + {/if} diff --git a/web/src/lib/modals/LibraryFolderEditModal.svelte b/web/src/lib/modals/LibraryFolderEditModal.svelte index 53294b29a..a84587e1f 100644 --- a/web/src/lib/modals/LibraryFolderEditModal.svelte +++ b/web/src/lib/modals/LibraryFolderEditModal.svelte @@ -1,5 +1,6 @@ - + {$t('admin.library_folder_description')} diff --git a/web/src/lib/queries/client.ts b/web/src/lib/queries/client.ts index 513f05476..9ce0267ec 100644 --- a/web/src/lib/queries/client.ts +++ b/web/src/lib/queries/client.ts @@ -1,24 +1,70 @@ +import { toastManager } from '@immich/ui'; import { createQuery, + MutationCache, + mutationOptions, + MutationObserver, QueryClient, type CreateMutationOptions, type DataTag, type DefaultError, type DefinedCreateQueryResult, + type MutateOptions, type NonUndefinedGuard, type QueryKey, type UndefinedInitialDataOptions, } from '@tanstack/svelte-query'; import { noop } from 'lodash-es'; +import type { Translations } from 'svelte-i18n'; import { eventManager } from '$lib/managers/event-manager.svelte'; +import { handleError } from '$lib/utils/handle-error'; +import { getFormatter } from '$lib/utils/i18n'; + +type ToastMeta = + true | Translations | { color: 'primary' | 'success' | 'info' | 'warning' | 'danger'; description: Translations }; + +declare module '@tanstack/svelte-query' { + interface Register { + mutationMeta: { + success?: { toast?: ToastMeta }; + error?: { toast?: Translations }; + }; + } +} const createQueryClient = () => { const client = new QueryClient({ defaultOptions: { queries: { retry: false, + staleTime: Infinity, }, }, + mutationCache: new MutationCache({ + onSuccess: async (_data, _variables, _onMutateResult, { meta }) => { + const toast = meta?.success?.toast; + if (!toast) { + return; + } + + if (toast === true) { + toastManager.primary(); + return; + } + + const $t = await getFormatter(); + if (typeof toast === 'string') { + toastManager.primary($t(toast)); + return; + } + + toastManager[toast.color]($t(toast.description)); + }, + onError: async (error, _variables, _onMutateResult, { meta }) => { + const $t = await getFormatter(); + handleError(error, $t(meta?.error?.toast ?? 'errors.something_went_wrong')); + }, + }), }); eventManager.on({ AuthLogout: () => client.clear() }); @@ -28,22 +74,45 @@ const createQueryClient = () => { export const queryClient = createQueryClient(); -export const createDefinedQuery = ( - options: UndefinedInitialDataOptions & { +export const asMutation = ( + fn: (...args: TArgs) => Promise, + options?: Omit, 'mutationFn' | 'mutationKey'>, +) => mutationOptions({ ...options, mutationFn: (args: TArgs) => fn(...args) }); + +export const createPreloadedQuery = < + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + optionsFn: () => UndefinedInitialDataOptions & { queryKey: DataTag; }, -): DefinedCreateQueryResult => { - const initialData = queryClient.getQueryData>( - options.queryKey, - ); - if (initialData === undefined) { - throw new Error(`${options.queryKey.join('/')} must be loaded first`); - } +): DefinedCreateQueryResult => + createQuery(() => { + const options = optionsFn(); + const initialData = queryClient.getQueryData>( + options.queryKey, + ); + if (initialData === undefined) { + throw new Error(`${options.queryKey.join('/')} must be loaded first`); + } - return createQuery(() => ({ ...options, initialData })); -}; + return { ...options, initialData }; + }); + +type MutationArgs = TVariables extends void + ? [variables?: TVariables, mutateOptions?: MutateOptions] + : [variables: TVariables, mutateOptions?: MutateOptions]; export const executeMutation = ( - options: CreateMutationOptions, - variables: TVariables, -) => queryClient.getMutationCache().build(queryClient, options).execute(variables).catch(noop); + options: () => CreateMutationOptions, + ...[variables, mutateOptions]: MutationArgs +) => { + const observer = new MutationObserver(queryClient, options()); + const unsubscribe = observer.subscribe(noop); + return observer + .mutate(variables as TVariables, mutateOptions) + .catch(noop) + .finally(unsubscribe); +}; diff --git a/web/src/lib/queries/library.query.ts b/web/src/lib/queries/library.query.ts index 920de891e..4949ce557 100644 --- a/web/src/lib/queries/library.query.ts +++ b/web/src/lib/queries/library.query.ts @@ -1,8 +1,74 @@ -import { getLibraryStatistics } from '@immich/sdk'; +import { + createLibrary, + deleteLibrary, + getAllLibraries, + getLibraryStatistics, + scanLibrary, + updateLibrary, + type LibraryResponseDto, +} from '@immich/sdk'; import { queryOptions } from '@tanstack/svelte-query'; +import { asMutation, queryClient } from '$lib/queries/client'; + +const findLibrary = (libraries: LibraryResponseDto[], id: string) => { + const library = libraries.find((library) => library.id === id); + if (!library) { + throw new Error(`Library ${id} not found`); + } + return library; +}; + +export const getAllLibrariesQuery = () => + queryOptions({ + queryKey: ['libraries'], + queryFn: ({ signal }) => getAllLibraries({ signal }), + }); + +export const getLibraryQuery = (id: string) => + queryOptions({ + ...getAllLibrariesQuery(), + select: (libraries) => findLibrary(libraries, id), + }); export const getLibraryStatisticsQuery = (id: string) => queryOptions({ queryKey: ['libraries', id, 'statistics'], queryFn: ({ signal }) => getLibraryStatistics({ id }, { signal }), + refetchInterval: 30_000, + }); + +export const createLibraryMutation = () => + asMutation(createLibrary, { + onSuccess: () => queryClient.invalidateQueries({ queryKey: getAllLibrariesQuery().queryKey, exact: true }), + meta: { error: { toast: 'errors.unable_to_create_library' } }, + }); + +export const updateLibraryMutation = () => + asMutation(updateLibrary, { + onSuccess: async (library) => { + queryClient.setQueryData(getAllLibrariesQuery().queryKey, (libraries) => + libraries?.map((item) => (item.id === library.id ? library : item)), + ); + await queryClient.invalidateQueries({ queryKey: getLibraryStatisticsQuery(library.id).queryKey }); + }, + meta: { success: { toast: 'admin.library_updated' }, error: { toast: 'errors.unable_to_update_library' } }, + }); + +export const deleteLibraryMutation = () => + asMutation(deleteLibrary, { + onSuccess: (_, [{ id }]) => { + queryClient.setQueryData(getAllLibrariesQuery().queryKey, (libraries) => + libraries?.filter((library) => library.id !== id), + ); + queryClient.removeQueries({ queryKey: getLibraryStatisticsQuery(id).queryKey }); + }, + meta: { success: { toast: 'admin.library_deleted' }, error: { toast: 'errors.unable_to_remove_library' } }, + }); + +export const scanLibraryMutation = () => + asMutation(scanLibrary, { + meta: { + success: { toast: { color: 'info', description: 'admin.scanning_library' } }, + error: { toast: 'errors.unable_to_scan_library' }, + }, }); diff --git a/web/src/lib/queries/queue.query.ts b/web/src/lib/queries/queue.query.ts new file mode 100644 index 000000000..3640bf236 --- /dev/null +++ b/web/src/lib/queries/queue.query.ts @@ -0,0 +1,12 @@ +import { QueueCommand, QueueName, runQueueCommandLegacy } from '@immich/sdk'; +import { mutationOptions } from '@tanstack/svelte-query'; + +export const scanAllLibrariesMutation = () => + mutationOptions({ + mutationFn: () => + runQueueCommandLegacy({ name: QueueName.Library, queueCommandDto: { command: QueueCommand.Start } }), + meta: { + success: { toast: { color: 'info', description: 'admin.refreshing_all_libraries' } }, + error: { toast: 'errors.unable_to_scan_libraries' }, + }, + }); diff --git a/web/src/lib/queries/server.query.ts b/web/src/lib/queries/server.query.ts index 675658305..1390de3f3 100644 --- a/web/src/lib/queries/server.query.ts +++ b/web/src/lib/queries/server.query.ts @@ -5,21 +5,18 @@ export const getAboutInfoQuery = () => queryOptions({ queryKey: ['server', 'about'], queryFn: ({ signal }) => getAboutInfo({ signal }), - staleTime: Infinity, }); export const getVersionHistoryQuery = () => queryOptions({ queryKey: ['server', 'version-history'], queryFn: ({ signal }) => getVersionHistory({ signal }), - staleTime: Infinity, }); export const getStorageQuery = () => queryOptions({ queryKey: ['server', 'storage'], queryFn: ({ signal }) => getStorage({ signal }), - staleTime: Infinity, }); export const getServerStatisticsQuery = () => diff --git a/web/src/lib/queries/system-config.query.ts b/web/src/lib/queries/system-config.query.ts index fb7646570..2126d8a11 100644 --- a/web/src/lib/queries/system-config.query.ts +++ b/web/src/lib/queries/system-config.query.ts @@ -4,21 +4,18 @@ import { mutationOptions, queryOptions } from '@tanstack/svelte-query'; import { isEqual } from 'lodash-es'; import { eventManager } from '$lib/managers/event-manager.svelte'; import { queryClient } from '$lib/queries/client'; -import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; export const getConfigQuery = () => queryOptions({ queryKey: ['system-config'], queryFn: ({ signal }) => getConfig({ signal }), - staleTime: Infinity, }); export const getConfigDefaultsQuery = () => queryOptions({ queryKey: ['system-config', 'defaults'], queryFn: ({ signal }) => getConfigDefaults({ signal }), - staleTime: Infinity, }); export const updateConfigMutation = () => @@ -44,8 +41,5 @@ export const updateConfigMutation = () => const $t = await getFormatter(); toastManager.primary($t('settings_saved')); }, - onError: async (error) => { - const $t = await getFormatter(); - handleError(error, $t('errors.unable_to_save_settings')); - }, + meta: { error: { toast: 'errors.unable_to_save_settings' } }, }); diff --git a/web/src/lib/queries/user-admin.query.ts b/web/src/lib/queries/user-admin.query.ts new file mode 100644 index 000000000..bcf010e57 --- /dev/null +++ b/web/src/lib/queries/user-admin.query.ts @@ -0,0 +1,20 @@ +import { searchUsersAdmin } from '@immich/sdk'; +import { queryOptions } from '@tanstack/svelte-query'; + +export const searchUsersAdminQuery = () => + queryOptions({ + queryKey: ['user-admin'], + queryFn: ({ signal }) => searchUsersAdmin({ withDeleted: true }, { signal }), + }); + +export const getUserAdminQuery = (id: string) => + queryOptions({ + ...searchUsersAdminQuery(), + select: (users) => { + const user = users.find((user) => user.id === id); + if (!user) { + throw new Error(`User ${id} not found`); + } + return user; + }, + }); diff --git a/web/src/lib/services/library.service.ts b/web/src/lib/services/library.service.ts index 95a49147d..d2f95a561 100644 --- a/web/src/lib/services/library.service.ts +++ b/web/src/lib/services/library.service.ts @@ -1,33 +1,23 @@ -import { - createLibrary, - deleteLibrary, - QueueCommand, - QueueName, - runQueueCommandLegacy, - scanLibrary, - updateLibrary, - type CreateLibraryDto, - type LibraryResponseDto, - type UpdateLibraryDto, -} from '@immich/sdk'; -import { modalManager, toastManager, type ActionItem } from '@immich/ui'; +import type { LibraryResponseDto } from '@immich/sdk'; +import { modalManager, type ActionItem } from '@immich/ui'; import { mdiInformationOutline, mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js'; import type { MessageFormatter } from 'svelte-i18n'; import { goto } from '$app/navigation'; -import { eventManager } from '$lib/managers/event-manager.svelte'; import LibraryExclusionPatternAddModal from '$lib/modals/LibraryExclusionPatternAddModal.svelte'; import LibraryExclusionPatternEditModal from '$lib/modals/LibraryExclusionPatternEditModal.svelte'; import LibraryFolderAddModal from '$lib/modals/LibraryFolderAddModal.svelte'; import LibraryFolderEditModal from '$lib/modals/LibraryFolderEditModal.svelte'; +import { executeMutation } from '$lib/queries/client'; +import { deleteLibraryMutation, scanLibraryMutation, updateLibraryMutation } from '$lib/queries/library.query'; +import { scanAllLibrariesMutation } from '$lib/queries/queue.query'; import { Route } from '$lib/route'; -import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; export const getLibrariesActions = ($t: MessageFormatter) => { const ScanAll: ActionItem = { title: $t('scan_all_libraries'), icon: mdiSync, - onAction: () => handleScanAllLibraries(), + onAction: () => executeMutation(scanAllLibrariesMutation), shortcuts: { shift: true, key: 'r' }, }; @@ -79,7 +69,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse const Scan: ActionItem = { icon: mdiSync, title: $t('scan_library'), - onAction: () => handleScanLibrary(library), + onAction: () => executeMutation(scanLibraryMutation, [{ id: library.id }]), shortcuts: { shift: true, key: 'r' }, }; @@ -122,54 +112,6 @@ export const getLibraryExclusionPatternActions = ( return { Edit, Delete }; }; -const handleScanAllLibraries = async () => { - const $t = await getFormatter(); - - try { - await runQueueCommandLegacy({ name: QueueName.Library, queueCommandDto: { command: QueueCommand.Start } }); - toastManager.info($t('admin.refreshing_all_libraries')); - } catch (error) { - handleError(error, $t('errors.unable_to_scan_libraries')); - } -}; - -const handleScanLibrary = async (library: LibraryResponseDto) => { - const $t = await getFormatter(); - try { - await scanLibrary({ id: library.id }); - toastManager.info($t('admin.scanning_library')); - } catch (error) { - handleError(error, $t('errors.unable_to_scan_library')); - } -}; - -export const handleCreateLibrary = async (dto: CreateLibraryDto) => { - const $t = await getFormatter(); - - try { - const library = await createLibrary({ createLibraryDto: dto }); - eventManager.emit('LibraryCreate', library); - toastManager.primary($t('admin.library_created', { values: { library: library.name } })); - return library; - } catch (error) { - handleError(error, $t('errors.unable_to_create_library')); - } -}; - -export const handleUpdateLibrary = async (library: LibraryResponseDto, dto: UpdateLibraryDto) => { - const $t = await getFormatter(); - - try { - const updatedLibrary = await updateLibrary({ id: library.id, updateLibraryDto: dto }); - eventManager.emit('LibraryUpdate', updatedLibrary); - toastManager.primary($t('admin.library_updated')); - return true; - } catch (error) { - handleError(error, $t('errors.unable_to_update_library')); - return false; - } -}; - const handleDeleteLibrary = async (library: LibraryResponseDto) => { const $t = await getFormatter(); @@ -190,57 +132,9 @@ const handleDeleteLibrary = async (library: LibraryResponseDto) => { } } - try { - await deleteLibrary({ id: library.id }); - eventManager.emit('LibraryDelete', { id: library.id }); - toastManager.primary($t('admin.library_deleted')); - } catch (error) { - handleError(error, $t('errors.unable_to_remove_library')); - } -}; - -export const handleAddLibraryFolder = async (library: LibraryResponseDto, folder: string) => { - const $t = await getFormatter(); - - if (library.importPaths.includes(folder)) { - toastManager.danger($t('errors.library_folder_already_exists')); - return false; - } - - try { - const updatedLibrary = await updateLibrary({ - id: library.id, - updateLibraryDto: { importPaths: [...library.importPaths, folder] }, - }); - eventManager.emit('LibraryUpdate', updatedLibrary); - toastManager.primary($t('admin.library_updated')); - } catch (error) { - handleError(error, $t('errors.unable_to_update_library')); - return false; - } - - return true; -}; - -export const handleEditLibraryFolder = async (library: LibraryResponseDto, oldValue: string, newValue: string) => { - const $t = await getFormatter(); - - if (oldValue === newValue) { - return true; - } - - const importPaths = library.importPaths.map((path) => (path === oldValue ? newValue : path)); - - try { - const updatedLibrary = await updateLibrary({ id: library.id, updateLibraryDto: { importPaths } }); - eventManager.emit('LibraryUpdate', updatedLibrary); - toastManager.primary($t('admin.library_updated')); - } catch (error) { - handleError(error, $t('errors.unable_to_update_library')); - return false; - } - - return true; + await executeMutation(deleteLibraryMutation, [{ id: library.id }], { + onSuccess: () => void goto(Route.libraries()), + }); }; const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: string) => { @@ -255,60 +149,8 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st return; } - try { - const updatedLibrary = await updateLibrary({ - id: library.id, - updateLibraryDto: { importPaths: library.importPaths.filter((path) => path !== folder) }, - }); - eventManager.emit('LibraryUpdate', updatedLibrary); - toastManager.primary($t('admin.library_updated')); - } catch (error) { - handleError(error, $t('errors.unable_to_update_library')); - } -}; - -export const handleAddLibraryExclusionPattern = async (library: LibraryResponseDto, exclusionPattern: string) => { - const $t = await getFormatter(); - - if (library.exclusionPatterns.includes(exclusionPattern)) { - toastManager.danger($t('errors.exclusion_pattern_already_exists')); - return false; - } - - try { - const updatedLibrary = await updateLibrary({ - id: library.id, - updateLibraryDto: { exclusionPatterns: [...library.exclusionPatterns, exclusionPattern] }, - }); - eventManager.emit('LibraryUpdate', updatedLibrary); - toastManager.primary($t('admin.library_updated')); - } catch (error) { - handleError(error, $t('errors.unable_to_update_library')); - return false; - } - - return true; -}; - -export const handleEditExclusionPattern = async (library: LibraryResponseDto, oldValue: string, newValue: string) => { - const $t = await getFormatter(); - - if (oldValue === newValue) { - return true; - } - - const exclusionPatterns = library.exclusionPatterns.map((pattern) => (pattern === oldValue ? newValue : pattern)); - - try { - const updatedLibrary = await updateLibrary({ id: library.id, updateLibraryDto: { exclusionPatterns } }); - eventManager.emit('LibraryUpdate', updatedLibrary); - toastManager.primary($t('admin.library_updated')); - } catch (error) { - handleError(error, $t('errors.unable_to_update_library')); - return false; - } - - return true; + const importPaths = library.importPaths.filter((path) => path !== folder); + await executeMutation(updateLibraryMutation, [{ id: library.id, updateLibraryDto: { importPaths } }]); }; const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusionPattern: string) => { @@ -319,16 +161,6 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi return; } - try { - const updatedLibrary = await updateLibrary({ - id: library.id, - updateLibraryDto: { - exclusionPatterns: library.exclusionPatterns.filter((pattern) => pattern !== exclusionPattern), - }, - }); - eventManager.emit('LibraryUpdate', updatedLibrary); - toastManager.primary($t('admin.library_updated')); - } catch (error) { - handleError(error, $t('errors.unable_to_update_library')); - } + const exclusionPatterns = library.exclusionPatterns.filter((pattern) => pattern !== exclusionPattern); + await executeMutation(updateLibraryMutation, [{ id: library.id, updateLibraryDto: { exclusionPatterns } }]); }; diff --git a/web/src/lib/services/system-config.service.ts b/web/src/lib/services/system-config.service.ts index 568d59575..cfa616a49 100644 --- a/web/src/lib/services/system-config.service.ts +++ b/web/src/lib/services/system-config.service.ts @@ -55,7 +55,7 @@ const handleUploadConfig = () => { } const reader = async () => { const text = await file.text(); - await executeMutation(updateConfigMutation(), JSON.parse(text)); + await executeMutation(updateConfigMutation, JSON.parse(text)); }; reader() .catch((error) => console.error('Error handling JSON config upload', error)) diff --git a/web/src/routes/admin/library-management/(list)/+layout.svelte b/web/src/routes/admin/library-management/(list)/+layout.svelte index d41a1c99b..8a361b1fb 100644 --- a/web/src/routes/admin/library-management/(list)/+layout.svelte +++ b/web/src/routes/admin/library-management/(list)/+layout.svelte @@ -1,30 +1,18 @@ - - @@ -70,61 +35,16 @@ {#if libraries.length > 0} - {$t('name')} - {$t('owner')} - {$t('photos')} - {$t('videos')} - {$t('size')} - + {$t('name')} + {$t('owner')} + {$t('photos')} + {$t('videos')} + {$t('size')} + - {#each libraries as library (library.id + library.name)} - {@const owner = owners[library.id]} - - - {library.name} - - - {owner.name} - - {#await data.statisticsPromise} - - - - - - - - - - {:then loadedStats} - {@const stats = loadedStats[library.id]} - - {stats.photos.toLocaleString($locale)} - - - {stats.videos.toLocaleString($locale)} - - - {@const [diskUsage, diskUsageUnit] = getBytesWithUnit(stats.usage, 0)} - {diskUsage} - {diskUsageUnit} - - {:catch} - - - - - - - - - - {/await} - - - - + {#each libraries as library (library.id)} + {/each}
@@ -141,37 +61,3 @@
- - diff --git a/web/src/routes/admin/library-management/(list)/+layout.ts b/web/src/routes/admin/library-management/(list)/+layout.ts index 4721cf550..f0e4cdec4 100644 --- a/web/src/routes/admin/library-management/(list)/+layout.ts +++ b/web/src/routes/admin/library-management/(list)/+layout.ts @@ -1,27 +1,16 @@ -import { getAllLibraries, getLibraryStatistics, getUserAdmin, searchUsersAdmin } from '@immich/sdk'; +import { queryClient } from '$lib/queries/client'; +import { getAllLibrariesQuery } from '$lib/queries/library.query'; +import { searchUsersAdminQuery } from '$lib/queries/user-admin.query'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import type { LayoutLoad } from './$types'; -export const load = (async ({ url, depends }) => { - depends('app:libraries'); +export const load = (async ({ url }) => { await authenticate(url, { admin: true }); - const allUsers = await searchUsersAdmin({ withDeleted: false }); + await Promise.all([queryClient.query(getAllLibrariesQuery()), queryClient.query(searchUsersAdminQuery())]); const $t = await getFormatter(); - const libraries = await getAllLibraries(); - const statisticsPromise = Promise.all( - libraries.map(async ({ id }) => [id, await getLibraryStatistics({ id })] as const), - ); - const owners = await Promise.all( - libraries.map(async ({ id, ownerId }) => [id, await getUserAdmin({ id: ownerId })] as const), - ); - return { - allUsers, - libraries, - statisticsPromise: statisticsPromise.then((stats) => Object.fromEntries(stats)), - owners: Object.fromEntries(owners), meta: { title: $t('external_libraries'), }, diff --git a/web/src/routes/admin/library-management/(list)/LibraryTableRow.svelte b/web/src/routes/admin/library-management/(list)/LibraryTableRow.svelte new file mode 100644 index 000000000..faf25e32d --- /dev/null +++ b/web/src/routes/admin/library-management/(list)/LibraryTableRow.svelte @@ -0,0 +1,100 @@ + + + + + {library.name} + + + {owner.name} + + {#if stats} + {@const [diskUsage, diskUsageUnit] = getBytesWithUnit(stats.usage, 0)} + + {stats.photos.toLocaleString($locale)} + + + {stats.videos.toLocaleString($locale)} + + + {diskUsage} + {diskUsageUnit} + + {:else} + + + + + + + + + + {/if} + + + + + + diff --git a/web/src/routes/admin/library-management/(list)/new/+page.svelte b/web/src/routes/admin/library-management/(list)/new/+page.svelte index d5ace4a90..b8db1a86f 100644 --- a/web/src/routes/admin/library-management/(list)/new/+page.svelte +++ b/web/src/routes/admin/library-management/(list)/new/+page.svelte @@ -1,31 +1,26 @@ diff --git a/web/src/routes/admin/library-management/[id]/+layout.svelte b/web/src/routes/admin/library-management/[id]/+layout.svelte index 32b412275..690a01ea1 100644 --- a/web/src/routes/admin/library-management/[id]/+layout.svelte +++ b/web/src/routes/admin/library-management/[id]/+layout.svelte @@ -1,15 +1,13 @@ - - { - depends('app:library'); +export const load = (async ({ params: { id }, url }) => { await authenticate(url, { admin: true }); - let library: LibraryResponseDto; - - try { - library = await getLibrary({ id }); - } catch { + const libraries = await queryClient.query(getAllLibrariesQuery()); + if (libraries.every((library) => library.id !== id)) { redirect(307, Route.libraries()); } const $t = await getFormatter(); return { - library, meta: { title: $t('admin.library_details'), }, diff --git a/web/src/routes/admin/library-management/[id]/edit/+page.svelte b/web/src/routes/admin/library-management/[id]/edit/+page.svelte index 02c89e521..b668cac5b 100644 --- a/web/src/routes/admin/library-management/[id]/edit/+page.svelte +++ b/web/src/routes/admin/library-management/[id]/edit/+page.svelte @@ -1,30 +1,25 @@ diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index ab3b43ac4..51ffa1ea2 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -21,7 +21,7 @@ import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte'; import SearchBar from '$lib/elements/SearchBar.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; - import { createDefinedQuery } from '$lib/queries/client'; + import { createPreloadedQuery } from '$lib/queries/client'; import { getConfigQuery } from '$lib/queries/system-config.query'; import { getSystemConfigActions } from '$lib/services/system-config.service'; import { Alert, CommandPaletteDefaultProvider, Container } from '@immich/ui'; @@ -57,7 +57,7 @@ const { data }: Props = $props(); - const config = createDefinedQuery(getConfigQuery()); + const config = createPreloadedQuery(getConfigQuery); const settings: Array<{ component: Component; diff --git a/web/src/routes/admin/system-settings/AuthSettings.svelte b/web/src/routes/admin/system-settings/AuthSettings.svelte index 7fd23dc79..0e6f080c1 100644 --- a/web/src/routes/admin/system-settings/AuthSettings.svelte +++ b/web/src/routes/admin/system-settings/AuthSettings.svelte @@ -7,7 +7,7 @@ import FormatMessage from '$lib/elements/FormatMessage.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte'; - import { createDefinedQuery } from '$lib/queries/client'; + import { createPreloadedQuery } from '$lib/queries/client'; import { getConfigQuery } from '$lib/queries/system-config.query'; import { handleError } from '$lib/utils/handle-error'; import { OAuthTokenEndpointAuthMethod, unlinkAllOAuthAccountsAdmin } from '@immich/sdk'; @@ -18,7 +18,7 @@ import { fade } from 'svelte/transition'; import SettingSelect from './SettingSelect.svelte'; - const query = createDefinedQuery(getConfigQuery()); + const query = createPreloadedQuery(getConfigQuery); const config = $derived(query.data); const disabled = $derived(featureFlagsManager.value.configFile); diff --git a/web/src/routes/admin/system-settings/BackupSettings.svelte b/web/src/routes/admin/system-settings/BackupSettings.svelte index 4e147794a..b9da87d6d 100644 --- a/web/src/routes/admin/system-settings/BackupSettings.svelte +++ b/web/src/routes/admin/system-settings/BackupSettings.svelte @@ -1,5 +1,5 @@