refactor: folder view queries (#32667)

(cherry picked from commit 6bf016964515ed65bb45cd1cf7b7bfadb405559a)
This commit is contained in:
Jason Rasmussen
2026-10-22 20:43:53 +03:00
committed by Codex
parent 0991f506bb
commit 5f54700344
5 changed files with 40 additions and 109 deletions
+27
View File
@@ -0,0 +1,27 @@
import { getAssetsByOriginalPath, getUniqueOriginalPaths } from '@immich/sdk';
import { queryOptions } from '@tanstack/svelte-query';
import { TreeNode } from '$lib/utils/tree-utils';
const toTree = (paths: string[]) => {
const tree = TreeNode.fromPaths(paths);
tree.collapse();
return tree;
};
export const getUniqueOriginalPathsQuery = () =>
queryOptions({
queryKey: ['view', 'folder', 'unique-paths'],
queryFn: ({ signal }) => getUniqueOriginalPaths({ signal }),
});
export const getFolderTreeQuery = () =>
queryOptions({
...getUniqueOriginalPathsQuery(),
select: toTree,
});
export const getAssetsByOriginalPathQuery = (path: string) =>
queryOptions({
queryKey: ['view', 'folder', 'assets', path],
queryFn: ({ signal }) => getAssetsByOriginalPath({ path }, { signal }),
});
-42
View File
@@ -1,42 +0,0 @@
import { getUniqueOriginalPaths } from '@immich/sdk';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { foldersStore } from '$lib/stores/folders.svelte';
vi.mock('$lib/managers/event-manager.svelte', () => ({
eventManager: {
on: vi.fn(),
},
}));
vi.mock('@immich/sdk', () => ({
getAssetsByOriginalPath: vi.fn(),
getUniqueOriginalPaths: vi.fn(),
}));
describe('foldersStore', () => {
beforeEach(() => {
foldersStore.clearCache();
vi.clearAllMocks();
});
it('returns the same non-null tree for concurrent fetchTree calls', async () => {
let resolvePaths: (value: string[]) => void;
vi.mocked(getUniqueOriginalPaths).mockReturnValue(
new Promise<string[]>((resolve) => {
resolvePaths = resolve;
}),
);
const first = foldersStore.fetchTree();
const second = foldersStore.fetchTree();
resolvePaths!(['/photos/2026']);
const [firstTree, secondTree] = await Promise.all([first, second]);
expect(firstTree).not.toBeNull();
expect(secondTree).not.toBeNull();
expect(secondTree).toEqual(firstTree);
});
});
-56
View File
@@ -1,56 +0,0 @@
import {
getAssetsByOriginalPath,
getUniqueOriginalPaths,
/**
* TODO: Incorrect type
*/
type AssetResponseDto,
} from '@immich/sdk';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { TreeNode } from '$lib/utils/tree-utils';
type AssetCache = {
[path: string]: AssetResponseDto[];
};
class FoldersStore {
folders = $state.raw<TreeNode | null>(null);
private initialized = false;
private assets = $state<AssetCache>({});
constructor() {
eventManager.on({
AuthLogout: () => this.clearCache(),
});
}
async fetchTree(): Promise<TreeNode> {
if (this.initialized) {
return this.folders!;
}
this.folders = TreeNode.fromPaths(await getUniqueOriginalPaths());
this.folders.collapse();
this.initialized = true;
return this.folders;
}
bustAssetCache() {
this.assets = {};
}
async refreshAssetsByPath(path: string) {
return (this.assets[path] = await getAssetsByOriginalPath({ path }));
}
async fetchAssetsByPath(path: string) {
return (this.assets[path] ??= await getAssetsByOriginalPath({ path }));
}
clearCache() {
this.initialized = false;
this.assets = {};
this.folders = null;
}
}
export const foldersStore = new FoldersStore();
@@ -23,8 +23,9 @@
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import type { Viewport } from '$lib/managers/timeline-manager/types';
import { Route } from '$lib/route';
import { createPreloadedQuery, queryClient } from '$lib/queries/client';
import { getAssetsByOriginalPathQuery, getFolderTreeQuery } from '$lib/queries/view.query';
import { getAssetBulkActions } from '$lib/services/asset.service';
import { foldersStore } from '$lib/stores/folders.svelte';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { joinPaths } from '$lib/utils/tree-utils';
import { ActionButton, CommandPaletteDefaultProvider, IconButton, Text } from '@immich/ui';
@@ -38,6 +39,9 @@
let { data }: Props = $props();
const query = createPreloadedQuery(getFolderTreeQuery);
const root = $derived(query.data);
const viewport: Viewport = $state({ width: 0, height: 0 });
let folderScrollContainer: HTMLElement | undefined = $state();
let savedScrollTop = 0;
@@ -67,7 +71,7 @@
const triggerAssetUpdate = async () => {
assetMultiSelectManager.clear();
if (data.tree.path) {
await foldersStore.refreshAssetsByPath(data.tree.path);
await queryClient.invalidateQueries({ queryKey: getAssetsByOriginalPathQuery(data.tree.path).queryKey });
}
await invalidateAll();
};
@@ -94,7 +98,7 @@
<div class="h-full">
<TreeItems
icons={{ default: mdiFolderOutline, active: mdiFolder }}
tree={foldersStore.folders!}
tree={root}
active={data.tree.path}
getLink={getLinkForPath}
/>
@@ -1,25 +1,23 @@
import { QueryParameter } from '$lib/constants';
import { foldersStore } from '$lib/stores/folders.svelte';
import { queryClient } from '$lib/queries/client';
import { getAssetsByOriginalPathQuery, getFolderTreeQuery } from '$lib/queries/view.query';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url);
const [, $t] = await Promise.all([foldersStore.fetchTree(), getFormatter()]);
const [root, $t] = await Promise.all([queryClient.query(getFolderTreeQuery()), getFormatter()]);
let tree = foldersStore.folders!;
let tree = root;
const path = url.searchParams.get(QueryParameter.PATH);
if (path) {
tree = tree.traverse(path);
} else if (path === null) {
// If no path is provided, we've just navigated to the folders page.
// We should bust the asset cache of the folder store, to make sure we don't show stale data
foldersStore.bustAssetCache();
queryClient.removeQueries({ queryKey: ['view', 'folder', 'assets'] });
}
// only fetch assets if the folder has assets
const pathAssets = tree.hasAssets ? await foldersStore.fetchAssetsByPath(tree.path) : null;
const pathAssets = tree.hasAssets ? await queryClient.query(getAssetsByOriginalPathQuery(tree.path)) : null;
return {
tree,