refactor(web): asset interaction (#14662)

* refactor(web): asset interaction

* feedback
This commit is contained in:
Michel Heusschen
2024-12-14 19:30:33 +01:00
committed by GitHub
parent 525840b040
commit b5022d80d6
21 changed files with 375 additions and 367 deletions

View File

@@ -1,86 +0,0 @@
import type { AssetResponseDto } from '@immich/sdk';
import { derived, readonly, writable } from 'svelte/store';
export type AssetInteractionStore = ReturnType<typeof createAssetInteractionStore>;
export function createAssetInteractionStore() {
const selectedAssets = writable(new Set<AssetResponseDto>());
const selectedGroup = writable(new Set<string>());
const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
// Candidates for the range selection. This set includes only loaded assets, so it improves highlight
// performance. From the user's perspective, range is highlighted almost immediately
const assetSelectionCandidates = writable(new Set<AssetResponseDto>());
// The beginning of the selection range
const assetSelectionStart = writable<AssetResponseDto | null>(null);
const selectAsset = (asset: AssetResponseDto) => {
selectedAssets.update(($selectedAssets) => $selectedAssets.add(asset));
};
const selectAssets = (assets: AssetResponseDto[]) => {
selectedAssets.update(($selectedAssets) => {
for (const asset of assets) {
$selectedAssets.add(asset);
}
return $selectedAssets;
});
};
const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => {
selectedAssets.update(($selectedAssets) => {
$selectedAssets.delete(asset);
return $selectedAssets;
});
};
const addGroupToMultiselectGroup = (group: string) => {
selectedGroup.update(($selectedGroup) => $selectedGroup.add(group));
};
const removeGroupFromMultiselectGroup = (group: string) => {
selectedGroup.update(($selectedGroup) => {
$selectedGroup.delete(group);
return $selectedGroup;
});
};
const setAssetSelectionStart = (asset: AssetResponseDto | null) => {
assetSelectionStart.set(asset);
};
const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => {
assetSelectionCandidates.set(new Set(assets));
};
const clearAssetSelectionCandidates = () => {
assetSelectionCandidates.set(new Set());
};
const clearMultiselect = () => {
// Multi-selection
selectedAssets.set(new Set());
selectedGroup.set(new Set());
// Range selection
assetSelectionCandidates.set(new Set());
assetSelectionStart.set(null);
};
return {
selectAsset,
selectAssets,
removeAssetFromMultiselectGroup,
addGroupToMultiselectGroup,
removeGroupFromMultiselectGroup,
setAssetSelectionCandidates,
clearAssetSelectionCandidates,
setAssetSelectionStart,
clearMultiselect,
isMultiSelectState: readonly(isMultiSelectStoreState),
selectedAssets: readonly(selectedAssets),
selectedGroup: readonly(selectedGroup),
assetSelectionCandidates: readonly(assetSelectionCandidates),
assetSelectionStart: readonly(assetSelectionStart),
};
}

View File

@@ -0,0 +1,40 @@
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { resetSavedUser, user } from '$lib/stores/user.store';
import { assetFactory } from '@test-data/factories/asset-factory';
import { userAdminFactory } from '@test-data/factories/user-factory';
describe('AssetInteraction', () => {
let assetInteraction: AssetInteraction;
beforeEach(() => {
assetInteraction = new AssetInteraction();
});
it('calculates derived values from selection', () => {
assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: true, isTrashed: true }));
assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: false, isTrashed: false }));
expect(assetInteraction.selectionActive).toBe(true);
expect(assetInteraction.isAllTrashed).toBe(false);
expect(assetInteraction.isAllArchived).toBe(false);
expect(assetInteraction.isAllFavorite).toBe(true);
});
it('updates isAllUserOwned when the active user changes', () => {
const [user1, user2] = userAdminFactory.buildList(2);
assetInteraction.selectAsset(assetFactory.build({ ownerId: user1.id }));
const cleanup = $effect.root(() => {
expect(assetInteraction.isAllUserOwned).toBe(false);
user.set(user1);
expect(assetInteraction.isAllUserOwned).toBe(true);
user.set(user2);
expect(assetInteraction.isAllUserOwned).toBe(false);
});
cleanup();
resetSavedUser();
});
});

View File

@@ -0,0 +1,66 @@
import { user } from '$lib/stores/user.store';
import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk';
import { SvelteSet } from 'svelte/reactivity';
import { fromStore } from 'svelte/store';
export class AssetInteraction {
readonly selectedAssets = new SvelteSet<AssetResponseDto>();
readonly selectedGroup = new SvelteSet<string>();
assetSelectionCandidates = $state(new SvelteSet<AssetResponseDto>());
assetSelectionStart = $state<AssetResponseDto | null>(null);
selectionActive = $derived(this.selectedAssets.size > 0);
selectedAssetsArray = $derived([...this.selectedAssets]);
private user = fromStore<UserAdminResponseDto | undefined>(user);
private userId = $derived(this.user.current?.id);
isAllTrashed = $derived(this.selectedAssetsArray.every((asset) => asset.isTrashed));
isAllArchived = $derived(this.selectedAssetsArray.every((asset) => asset.isArchived));
isAllFavorite = $derived(this.selectedAssetsArray.every((asset) => asset.isFavorite));
isAllUserOwned = $derived(this.selectedAssetsArray.every((asset) => asset.ownerId === this.userId));
selectAsset(asset: AssetResponseDto) {
this.selectedAssets.add(asset);
}
selectAssets(assets: AssetResponseDto[]) {
for (const asset of assets) {
this.selectedAssets.add(asset);
}
}
removeAssetFromMultiselectGroup(asset: AssetResponseDto) {
this.selectedAssets.delete(asset);
}
addGroupToMultiselectGroup(group: string) {
this.selectedGroup.add(group);
}
removeGroupFromMultiselectGroup(group: string) {
this.selectedGroup.delete(group);
}
setAssetSelectionStart(asset: AssetResponseDto | null) {
this.assetSelectionStart = asset;
}
setAssetSelectionCandidates(assets: AssetResponseDto[]) {
this.assetSelectionCandidates = new SvelteSet(assets);
}
clearAssetSelectionCandidates() {
this.assetSelectionCandidates.clear();
}
clearMultiselect() {
// Multi-selection
this.selectedAssets.clear();
this.selectedGroup.clear();
// Range selection
this.assetSelectionCandidates.clear();
this.assetSelectionStart = null;
}
}