diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 14821ace0..2c3e853ab 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 3.3.3-local.2+3030401 +version: 3.3.3-local.3+3030402 environment: sdk: '>=3.12.0 <4.0.0' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3bc68d2e..3da0a9d66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -824,7 +824,7 @@ importers: version: 5.15.1(@photo-sphere-viewer/core@5.15.1) '@tanstack/svelte-query': specifier: ^6.3.2 - version: 6.3.2(svelte@5.57.2(@typescript-eslint/types@8.71.2)) + version: 6.3.2(svelte@5.57.1(@typescript-eslint/types@8.70.1)) '@types/geojson': specifier: ^7946.0.16 version: 7946.0.16 @@ -862,8 +862,8 @@ importers: specifier: ^1.6.16 version: 1.7.3 intl-messageformat: - specifier: ^11.0.0 - version: 11.2.15 + specifier: ^12.0.0 + version: 12.1.3 justified-layout: specifier: ^4.1.0 version: 4.1.0 @@ -2734,18 +2734,15 @@ packages: '@formatjs/icu-messageformat-parser@2.11.4': resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} - '@formatjs/icu-messageformat-parser@3.5.18': - resolution: {integrity: sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA==} - '@formatjs/icu-messageformat-parser@3.5.20': resolution: {integrity: sha512-DUzCb2iskGDKvLU7P+nz/rjpmN59luoTRbWHFFNYNnZTxWCKvwPI94rNll99TGZaI6XoTbDF0QGegk21GFrQZQ==} + '@formatjs/icu-messageformat-parser@3.5.21': + resolution: {integrity: sha512-Vnl7IOSyMRaP4Pjv48etkT2nlcIwzh4QWIa3hewzGlZZPBbft+TbMir4MijpDViOLN/1hvkGC10oYMuilsG+fw==} + '@formatjs/icu-skeleton-parser@1.8.16': resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} - '@formatjs/icu-skeleton-parser@2.1.11': - resolution: {integrity: sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==} - '@formatjs/icu-skeleton-parser@2.1.12': resolution: {integrity: sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA==} @@ -8502,8 +8499,8 @@ packages: intl-messageformat@10.7.18: resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - intl-messageformat@11.2.15: - resolution: {integrity: sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ==} + intl-messageformat@12.1.3: + resolution: {integrity: sha512-JXi5wk8R/oSECKyxSIZ00VS8KG6MZkHM0qU4NS7RrvIWWXkiv3wVqGlCvEvyXiDvKlt2yqwZMiZGIv+I8rrnZQ==} invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -15743,21 +15740,19 @@ snapshots: '@formatjs/icu-skeleton-parser': 1.8.16 tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@3.5.18': - dependencies: - '@formatjs/icu-skeleton-parser': 2.1.11 - '@formatjs/icu-messageformat-parser@3.5.20': dependencies: '@formatjs/icu-skeleton-parser': 2.1.12 + '@formatjs/icu-messageformat-parser@3.5.21': + dependencies: + '@formatjs/icu-skeleton-parser': 2.1.12 + '@formatjs/icu-skeleton-parser@1.8.16': dependencies: '@formatjs/ecma402-abstract': 2.3.6 tslib: 2.8.1 - '@formatjs/icu-skeleton-parser@2.1.11': {} - '@formatjs/icu-skeleton-parser@2.1.12': {} '@formatjs/intl-localematcher@0.6.2': @@ -17636,10 +17631,10 @@ snapshots: '@tanstack/query-core@5.104.2': {} - '@tanstack/svelte-query@6.3.2(svelte@5.57.2(@typescript-eslint/types@8.71.2))': + '@tanstack/svelte-query@6.3.2(svelte@5.57.1(@typescript-eslint/types@8.70.1))': dependencies: '@tanstack/query-core': 5.104.2 - svelte: 5.57.2(@typescript-eslint/types@8.71.2) + svelte: 5.57.1(@typescript-eslint/types@8.70.1) '@testing-library/dom@10.4.1': dependencies: @@ -18251,7 +18246,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 24.19.0 + '@types/node': 24.19.2 '@types/trusted-types@2.0.7': optional: true @@ -18268,7 +18263,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.19.0 + '@types/node': 24.19.2 '@types/yargs-parser@21.0.3': {} @@ -19249,7 +19244,7 @@ snapshots: buffer-image-size@0.6.4: dependencies: - '@types/node': 24.19.0 + '@types/node': 24.19.2 buffer@5.7.1: dependencies: @@ -21566,7 +21561,7 @@ snapshots: happy-dom@20.14.5: dependencies: - '@types/node': 24.19.0 + '@types/node': 24.19.2 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 buffer-image-size: 0.6.4 @@ -22069,10 +22064,10 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.4 tslib: 2.8.1 - intl-messageformat@11.2.15: + intl-messageformat@12.1.3: dependencies: '@formatjs/fast-memoize': 3.1.7 - '@formatjs/icu-messageformat-parser': 3.5.18 + '@formatjs/icu-messageformat-parser': 3.5.21 invariant@2.2.4: dependencies: @@ -22382,7 +22377,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.19.0 + '@types/node': 24.19.2 merge-stream: 2.0.0 supports-color: 8.1.1 diff --git a/web/src/lib/queries/release-regressions.spec.ts b/web/src/lib/queries/release-regressions.spec.ts new file mode 100644 index 000000000..75e9688ab --- /dev/null +++ b/web/src/lib/queries/release-regressions.spec.ts @@ -0,0 +1,131 @@ +import { + deleteLibrary, + getConfig, + getStorage, + updateConfig, + updateLibrary, + type AdminConfigDto, + type LibraryResponseDto, +} from '@immich/sdk'; +import { eventManager } from '$lib/managers/event-manager.svelte'; +import { executeMutation, queryClient } from '$lib/queries/client'; +import { + deleteLibraryMutation, + getAllLibrariesQuery, + getLibraryStatisticsQuery, + updateLibraryMutation, +} from '$lib/queries/library.query'; +import { getStorageQuery } from '$lib/queries/server.query'; +import { getConfigQuery, updateConfigMutation } from '$lib/queries/system-config.query'; +import { handleError } from '$lib/utils/handle-error'; + +vi.mock('@immich/ui', () => ({ toastManager: { primary: vi.fn(), info: vi.fn() } })); +vi.mock('$lib/utils/handle-error', () => ({ handleError: vi.fn() })); +vi.mock('$lib/utils/i18n', () => ({ getFormatter: async () => (key: string) => key })); +vi.mock('@immich/sdk', () => ({ + createLibrary: vi.fn(), + deleteLibrary: vi.fn(), + getAllLibraries: vi.fn(), + getLibraryStatistics: vi.fn(), + scanLibrary: vi.fn(), + updateLibrary: vi.fn(), + getAboutInfo: vi.fn(), + getVersionHistory: vi.fn(), + getStorage: vi.fn(), + getServerStatistics: vi.fn(), + getConfig: vi.fn(), + getConfigDefaults: vi.fn(), + updateConfig: vi.fn(), +})); + +const first = { id: 'first', name: 'Photos' } as LibraryResponseDto; +const second = { id: 'second', name: 'Archive' } as LibraryResponseDto; + +describe('shared queries after the upstream migration', () => { + beforeEach(() => { + queryClient.clear(); + vi.resetAllMocks(); + }); + + afterEach(() => queryClient.clear()); + + it('clears account data on logout', () => { + queryClient.setQueryData(getAllLibrariesQuery().queryKey, [first]); + queryClient.setQueryData(getConfigQuery().queryKey, {} as AdminConfigDto); + eventManager.emit('AuthLogout'); + expect(queryClient.getQueryCache().getAll()).toHaveLength(0); + }); + + it('shares a storage request between consumers', async () => { + const storage = { diskSizeRaw: 100, diskUseRaw: 20 } as Awaited>; + vi.mocked(getStorage).mockResolvedValue(storage); + const values = await Promise.all([queryClient.query(getStorageQuery()), queryClient.query(getStorageQuery())]); + expect(values).toEqual([storage, storage]); + expect(getStorage).toHaveBeenCalledTimes(1); + }); + + it('preserves libraries and statistics when deletion fails', async () => { + queryClient.setQueryData(getAllLibrariesQuery().queryKey, [first, second]); + queryClient.setQueryData(getLibraryStatisticsQuery(first.id).queryKey, { + photos: 5, + videos: 0, + usage: 100, + total: 5, + }); + vi.mocked(deleteLibrary).mockRejectedValue(new Error('offline')); + await executeMutation(deleteLibraryMutation, [{ id: first.id }]); + expect(queryClient.getQueryData(getAllLibrariesQuery().queryKey)).toEqual([first, second]); + expect(queryClient.getQueryData(getLibraryStatisticsQuery(first.id).queryKey)?.photos).toBe(5); + expect(handleError).toHaveBeenCalledOnce(); + }); + + it('removes only the deleted library from the cache after success', async () => { + queryClient.setQueryData(getAllLibrariesQuery().queryKey, [first, second]); + queryClient.setQueryData(getLibraryStatisticsQuery(first.id).queryKey, { + photos: 5, + videos: 0, + usage: 100, + total: 5, + }); + vi.mocked(deleteLibrary).mockResolvedValue(undefined as never); + await executeMutation(deleteLibraryMutation, [{ id: first.id }]); + expect(queryClient.getQueryData(getAllLibrariesQuery().queryKey)).toEqual([second]); + expect(queryClient.getQueryData(getLibraryStatisticsQuery(first.id).queryKey)).toBeUndefined(); + }); + + it('uses the saved library response and refreshes its statistics', async () => { + queryClient.setQueryData(getAllLibrariesQuery().queryKey, [first, second]); + queryClient.setQueryData(getLibraryStatisticsQuery(first.id).queryKey, { + photos: 5, + videos: 0, + usage: 100, + total: 5, + }); + const saved = { ...first, name: 'Renamed' }; + vi.mocked(updateLibrary).mockResolvedValue(saved); + await executeMutation(updateLibraryMutation, [{ id: first.id, updateLibraryDto: { name: saved.name } }]); + expect(queryClient.getQueryData(getAllLibrariesQuery().queryKey)).toEqual([saved, second]); + expect(queryClient.getQueryState(getLibraryStatisticsQuery(first.id).queryKey)?.isInvalidated).toBe(true); + }); + + it('merges a settings edit with the latest server configuration', async () => { + const current = { server: { externalDomain: 'https://example.com' }, trash: { enabled: true, days: 30 } }; + const update = { trash: { enabled: true, days: 60 } }; + const saved = { ...current, ...update } as AdminConfigDto; + vi.mocked(getConfig).mockResolvedValue(current as AdminConfigDto); + vi.mocked(updateConfig).mockResolvedValue(saved); + await executeMutation(updateConfigMutation, update); + expect(updateConfig).toHaveBeenCalledWith({ adminConfigDto: saved }); + expect(queryClient.getQueryData(getConfigQuery().queryKey)).toEqual(saved); + }); + + it('keeps the previous settings if saving fails', async () => { + const current = { trash: { enabled: true, days: 30 } } as AdminConfigDto; + queryClient.setQueryData(getConfigQuery().queryKey, current); + vi.mocked(getConfig).mockResolvedValue(current); + vi.mocked(updateConfig).mockRejectedValue(new Error('offline')); + await executeMutation(updateConfigMutation, { trash: { enabled: true, days: 60 } }); + expect(queryClient.getQueryData(getConfigQuery().queryKey)).toEqual(current); + expect(handleError).toHaveBeenCalledOnce(); + }); +});