fix(web): use correct date field for shift-click range in Recently Added (#30071)

This commit is contained in:
okxint
2026-07-22 11:53:45 +02:00
committed by GitHub
parent 8435d219e4
commit 8a875f5978
3 changed files with 55 additions and 4 deletions
@@ -1,6 +1,6 @@
import { AssetOrder, type AssetResponseDto } from '@immich/sdk'; import { AssetOrder, AssetOrderBy, type AssetResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util'; import { getOrderingDate, plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
import { TimelineManager } from '../timeline-manager.svelte'; import { TimelineManager } from '../timeline-manager.svelte';
import type { TimelineMonth } from '../timeline-month.svelte'; import type { TimelineMonth } from '../timeline-month.svelte';
import type { AssetDescriptor, Direction, TimelineAsset } from '../types'; import type { AssetDescriptor, Direction, TimelineAsset } from '../types';
@@ -125,7 +125,14 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass
return []; return [];
} }
const assetOrder: AssetOrder = timelineManager.getAssetOrder(); const assetOrder: AssetOrder = timelineManager.getAssetOrder();
if (plainDateTimeCompare(assetOrder === AssetOrder.Desc, startAsset.localDateTime, endAsset.localDateTime) < 0) { const orderBy: AssetOrderBy = timelineManager.getOrderBy();
if (
plainDateTimeCompare(
assetOrder === AssetOrder.Desc,
getOrderingDate(startAsset, orderBy),
getOrderingDate(endAsset, orderBy),
) < 0
) {
[startAsset, endAsset] = [endAsset, startAsset]; [startAsset, endAsset] = [endAsset, startAsset];
// eslint-disable-next-line no-useless-assignment // eslint-disable-next-line no-useless-assignment
[startTimelineMonth, endTimelineMonth] = [endTimelineMonth, startTimelineMonth]; [startTimelineMonth, endTimelineMonth] = [endTimelineMonth, startTimelineMonth];
@@ -1,4 +1,4 @@
import { AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk'; import { AssetOrderBy, AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { sdkMock } from '$lib/__mocks__/sdk.mock';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
@@ -808,4 +808,44 @@ describe('TimelineManager', () => {
expect(b.showAssetOwners).toBe(true); expect(b.showAssetOwners).toBe(true);
}); });
}); });
describe('retrieveRange', () => {
it('uses createdAt ordering in the Recently Added view (orderBy=CreatedAt)', async () => {
// Simulate the "Recently Added" bug: two assets whose localDateTime order is
// the reverse of their createdAt (upload) order. Before the fix, retrieveRange
// compared localDateTime and selected the wrong range direction.
const timelineManager = new TimelineManager();
sdkMock.getTimeBuckets.mockResolvedValue([]);
await timelineManager.updateOptions({ orderBy: AssetOrderBy.CreatedAt });
// assetA was taken recently (2024) but uploaded first (2025-01)
const assetA = timelineAssetFactory.build({
localDateTime: fromISODateTimeUTCToObject('2024-06-01T00:00:00.000Z'),
createdAt: fromISODateTimeUTCToObject('2025-01-20T00:00:00.000Z'),
fileCreatedAt: fromISODateTimeUTCToObject('2025-01-20T00:00:00.000Z'),
});
// assetB was taken long ago (2018) but uploaded second (2025-02)
const assetB = timelineAssetFactory.build({
localDateTime: fromISODateTimeUTCToObject('2018-03-15T00:00:00.000Z'),
createdAt: fromISODateTimeUTCToObject('2025-02-10T00:00:00.000Z'),
fileCreatedAt: fromISODateTimeUTCToObject('2025-02-10T00:00:00.000Z'),
});
// assetC is an unrelated asset that should not appear in the selection
const assetC = timelineAssetFactory.build({
localDateTime: fromISODateTimeUTCToObject('2022-01-01T00:00:00.000Z'),
createdAt: fromISODateTimeUTCToObject('2025-03-01T00:00:00.000Z'),
fileCreatedAt: fromISODateTimeUTCToObject('2025-03-01T00:00:00.000Z'),
});
timelineManager.upsertAssets([assetA, assetB, assetC]);
// Shift-click from assetB (uploaded most recently) to assetA — the range
// between them in the "Recently Added" timeline should contain only those two.
const range = await timelineManager.retrieveRange({ id: assetB.id }, { id: assetA.id });
const ids = range.map((a) => a.id);
expect(ids).toContain(assetA.id);
expect(ids).toContain(assetB.id);
expect(ids).not.toContain(assetC.id);
});
});
}); });
@@ -625,6 +625,10 @@ export class TimelineManager extends VirtualScrollManager {
return this.#options.order ?? AssetOrder.Desc; return this.#options.order ?? AssetOrder.Desc;
} }
getOrderBy() {
return this.#options.orderBy ?? AssetOrderBy.TakenAt;
}
protected postCreateSegments(): void { protected postCreateSegments(): void {
this.months.sort((a, b) => { this.months.sort((a, b) => {
return a.yearMonth.year === b.yearMonth.year return a.yearMonth.year === b.yearMonth.year