refactor: folder view queries (#32667)
(cherry picked from commit 6bf016964515ed65bb45cd1cf7b7bfadb405559a)
This commit is contained in:
@@ -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 }),
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user