fix: regression: sort day by fileCreatedAt again (#18732)

* fix: regression: sort day by fileCreatedAt again

* lint

* e2e test

* inline function

* e2e

* Address comments. Drop dayGroup and timezone in favor of localOffsetMinutes

* lint and some api-doc

* lint, more api-doc

* format

* Move minutes to fractional hours

* make sql

* merge/conflict

* merge fallout, review comments

* spelling

* drop offset from returned date

* move description into decorator where possible, regen api
This commit is contained in:
Min Idzelis
2025-06-05 21:56:32 -04:00
committed by GitHub
parent 81423420c8
commit 55f4e93456
23 changed files with 687 additions and 247 deletions
+157 -85
View File
@@ -2,7 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock';
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { AbortError } from '$lib/utils';
import { fromLocalDateTimeToObject } from '$lib/utils/timeline-util';
import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util';
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
@@ -14,6 +14,13 @@ async function getAssets(store: AssetStore) {
return assets;
}
function deriveLocalDateTimeFromFileCreatedAt(arg: TimelineAsset): TimelineAsset {
return {
...arg,
localDateTime: arg.fileCreatedAt,
};
}
describe('AssetStore', () => {
beforeEach(() => {
vi.resetAllMocks();
@@ -22,15 +29,24 @@ describe('AssetStore', () => {
describe('init', () => {
let assetStore: AssetStore;
const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-03-01T00:00:00.000Z': timelineAssetFactory
.buildList(1)
.map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-03-01T00:00:00.000Z') })),
'2024-02-01T00:00:00.000Z': timelineAssetFactory
.buildList(100)
.map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-02-01T00:00:00.000Z') })),
'2024-01-01T00:00:00.000Z': timelineAssetFactory
.buildList(3)
.map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-01-01T00:00:00.000Z') })),
'2024-03-01T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) =>
deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-03-01T00:00:00.000Z'),
}),
),
'2024-02-01T00:00:00.000Z': timelineAssetFactory.buildList(100).map((asset) =>
deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-01T00:00:00.000Z'),
}),
),
'2024-01-01T00:00:00.000Z': timelineAssetFactory.buildList(3).map((asset) =>
deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-01T00:00:00.000Z'),
}),
),
};
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
@@ -40,9 +56,9 @@ describe('AssetStore', () => {
beforeEach(async () => {
assetStore = new AssetStore();
sdkMock.getTimeBuckets.mockResolvedValue([
{ count: 1, timeBucket: '2024-03-01T00:00:00.000Z' },
{ count: 100, timeBucket: '2024-02-01T00:00:00.000Z' },
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
{ count: 1, timeBucket: '2024-03-01' },
{ count: 100, timeBucket: '2024-02-01' },
{ count: 3, timeBucket: '2024-01-01' },
]);
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
@@ -78,12 +94,18 @@ describe('AssetStore', () => {
describe('loadBucket', () => {
let assetStore: AssetStore;
const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-01-03T00:00:00.000Z': timelineAssetFactory
.buildList(1)
.map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-03-01T00:00:00.000Z') })),
'2024-01-01T00:00:00.000Z': timelineAssetFactory
.buildList(3)
.map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-01-01T00:00:00.000Z') })),
'2024-01-03T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) =>
deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-03-01T00:00:00.000Z'),
}),
),
'2024-01-01T00:00:00.000Z': timelineAssetFactory.buildList(3).map((asset) =>
deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-01T00:00:00.000Z'),
}),
),
};
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
@@ -166,9 +188,11 @@ describe('AssetStore', () => {
});
it('adds assets to new bucket', () => {
const asset = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const asset = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);
assetStore.addAssets([asset]);
expect(assetStore.buckets.length).toEqual(1);
@@ -180,9 +204,11 @@ describe('AssetStore', () => {
});
it('adds assets to existing bucket', () => {
const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, {
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const [assetOne, assetTwo] = timelineAssetFactory
.buildList(2, {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
})
.map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset));
assetStore.addAssets([assetOne]);
assetStore.addAssets([assetTwo]);
@@ -194,15 +220,21 @@ describe('AssetStore', () => {
});
it('orders assets in buckets by descending date', () => {
const assetOne = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const assetTwo = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-15T12:00:00.000Z'),
});
const assetThree = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-16T12:00:00.000Z'),
});
const assetOne = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);
const assetTwo = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-15T12:00:00.000Z'),
}),
);
const assetThree = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-16T12:00:00.000Z'),
}),
);
assetStore.addAssets([assetOne, assetTwo, assetThree]);
const bucket = assetStore.getBucketByDate({ year: 2024, month: 1 });
@@ -214,15 +246,21 @@ describe('AssetStore', () => {
});
it('orders buckets by descending date', () => {
const assetOne = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const assetTwo = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-04-20T12:00:00.000Z'),
});
const assetThree = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2023-01-20T12:00:00.000Z'),
});
const assetOne = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);
const assetTwo = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-04-20T12:00:00.000Z'),
}),
);
const assetThree = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2023-01-20T12:00:00.000Z'),
}),
);
assetStore.addAssets([assetOne, assetTwo, assetThree]);
expect(assetStore.buckets.length).toEqual(3);
@@ -238,7 +276,7 @@ describe('AssetStore', () => {
it('updates existing asset', () => {
const updateAssetsSpy = vi.spyOn(assetStore, 'updateAssets');
const asset = timelineAssetFactory.build();
const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build());
assetStore.addAssets([asset]);
assetStore.addAssets([asset]);
@@ -248,8 +286,8 @@ describe('AssetStore', () => {
// disabled due to the wasm Justified Layout import
it('ignores trashed assets when isTrashed is true', async () => {
const asset = timelineAssetFactory.build({ isTrashed: false });
const trashedAsset = timelineAssetFactory.build({ isTrashed: true });
const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ isTrashed: false }));
const trashedAsset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ isTrashed: true }));
const assetStore = new AssetStore();
await assetStore.updateOptions({ isTrashed: true });
@@ -269,14 +307,14 @@ describe('AssetStore', () => {
});
it('ignores non-existing assets', () => {
assetStore.updateAssets([timelineAssetFactory.build()]);
assetStore.updateAssets([deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build())]);
expect(assetStore.buckets.length).toEqual(0);
expect(assetStore.count).toEqual(0);
});
it('updates an asset', () => {
const asset = timelineAssetFactory.build({ isFavorite: false });
const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ isFavorite: false }));
const updatedAsset = { ...asset, isFavorite: true };
assetStore.addAssets([asset]);
@@ -289,10 +327,15 @@ describe('AssetStore', () => {
});
it('asset moves buckets when asset date changes', () => {
const asset = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
const asset = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);
const updatedAsset = deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-03-20T12:00:00.000Z'),
});
const updatedAsset = { ...asset, localDateTime: fromLocalDateTimeToObject('2024-03-20T12:00:00.000Z') };
assetStore.addAssets([asset]);
expect(assetStore.buckets.length).toEqual(1);
@@ -320,7 +363,11 @@ describe('AssetStore', () => {
it('ignores invalid IDs', () => {
assetStore.addAssets(
timelineAssetFactory.buildList(2, { localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z') }),
timelineAssetFactory
.buildList(2, {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
})
.map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset)),
);
assetStore.removeAssets(['', 'invalid', '4c7d9acc']);
@@ -330,9 +377,11 @@ describe('AssetStore', () => {
});
it('removes asset from bucket', () => {
const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, {
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const [assetOne, assetTwo] = timelineAssetFactory
.buildList(2, {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
})
.map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset));
assetStore.addAssets([assetOne, assetTwo]);
assetStore.removeAssets([assetOne.id]);
@@ -342,9 +391,11 @@ describe('AssetStore', () => {
});
it('does not remove bucket when empty', () => {
const assets = timelineAssetFactory.buildList(2, {
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const assets = timelineAssetFactory
.buildList(2, {
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
})
.map((asset) => deriveLocalDateTimeFromFileCreatedAt(asset));
assetStore.addAssets(assets);
assetStore.removeAssets(assets.map((asset) => asset.id));
@@ -367,12 +418,16 @@ describe('AssetStore', () => {
});
it('populated store returns first asset', () => {
const assetOne = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const assetTwo = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-15T12:00:00.000Z'),
});
const assetOne = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);
const assetTwo = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-15T12:00:00.000Z'),
}),
);
assetStore.addAssets([assetOne, assetTwo]);
expect(assetStore.getFirstAsset()).toEqual(assetOne);
});
@@ -381,15 +436,24 @@ describe('AssetStore', () => {
describe('getLaterAsset', () => {
let assetStore: AssetStore;
const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-03-01T00:00:00.000Z': timelineAssetFactory
.buildList(1)
.map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-03-01T00:00:00.000Z') })),
'2024-02-01T00:00:00.000Z': timelineAssetFactory
.buildList(6)
.map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-02-01T00:00:00.000Z') })),
'2024-01-01T00:00:00.000Z': timelineAssetFactory
.buildList(3)
.map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-01-01T00:00:00.000Z') })),
'2024-03-01T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) =>
deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-03-01T00:00:00.000Z'),
}),
),
'2024-02-01T00:00:00.000Z': timelineAssetFactory.buildList(6).map((asset) =>
deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-01T00:00:00.000Z'),
}),
),
'2024-01-01T00:00:00.000Z': timelineAssetFactory.buildList(3).map((asset) =>
deriveLocalDateTimeFromFileCreatedAt({
...asset,
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-01T00:00:00.000Z'),
}),
),
};
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
@@ -479,12 +543,16 @@ describe('AssetStore', () => {
});
it('returns the bucket index', () => {
const assetOne = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const assetTwo = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-02-15T12:00:00.000Z'),
});
const assetOne = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);
const assetTwo = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-15T12:00:00.000Z'),
}),
);
assetStore.addAssets([assetOne, assetTwo]);
expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024);
@@ -494,12 +562,16 @@ describe('AssetStore', () => {
});
it('ignores removed buckets', () => {
const assetOne = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'),
});
const assetTwo = timelineAssetFactory.build({
localDateTime: fromLocalDateTimeToObject('2024-02-15T12:00:00.000Z'),
});
const assetOne = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);
const assetTwo = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-15T12:00:00.000Z'),
}),
);
assetStore.addAssets([assetOne, assetTwo]);
assetStore.removeAssets([assetTwo.id]);