From 8a875f59787a90817ef4300a79d64cc49d6cbf3f Mon Sep 17 00:00:00 2001 From: okxint <130782884+okxint@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:23:45 +0530 Subject: [PATCH] fix(web): use correct date field for shift-click range in Recently Added (#30071) --- .../internal/search-support.svelte.ts | 13 ++++-- .../timeline-manager.svelte.spec.ts | 42 ++++++++++++++++++- .../timeline-manager.svelte.ts | 4 ++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts index 8a73bcce7d..d6104f4051 100644 --- a/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/search-support.svelte.ts @@ -1,6 +1,6 @@ -import { AssetOrder, type AssetResponseDto } from '@immich/sdk'; +import { AssetOrder, AssetOrderBy, type AssetResponseDto } from '@immich/sdk'; 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 type { TimelineMonth } from '../timeline-month.svelte'; import type { AssetDescriptor, Direction, TimelineAsset } from '../types'; @@ -125,7 +125,14 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass return []; } 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]; // eslint-disable-next-line no-useless-assignment [startTimelineMonth, endTimelineMonth] = [endTimelineMonth, startTimelineMonth]; diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts index 1762553897..f44f8a3046 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts @@ -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 { sdkMock } from '$lib/__mocks__/sdk.mock'; import { eventManager } from '$lib/managers/event-manager.svelte'; @@ -808,4 +808,44 @@ describe('TimelineManager', () => { 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); + }); + }); }); diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 0b9c6d4b07..3f8277fbc4 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -625,6 +625,10 @@ export class TimelineManager extends VirtualScrollManager { return this.#options.order ?? AssetOrder.Desc; } + getOrderBy() { + return this.#options.orderBy ?? AssetOrderBy.TakenAt; + } + protected postCreateSegments(): void { this.months.sort((a, b) => { return a.yearMonth.year === b.yearMonth.year