fix: preserve stack primary asset selection (#31380)

* fix: preserve stack primary asset selection

* test: select stacked assets by id

* chore: limit dart formatting changes

* fix: address stack viewer review feedback

* fix: align stack ordering with server

* fix: keep stack primary asset first

---------

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
(cherry picked from commit 5a5d7be11084969c3c00a294ce6c0527042f88e7)
This commit is contained in:
Kairui Liu
2026-10-27 23:12:13 +03:00
committed by Codex
parent 75a1d7c5c8
commit c482c1e53e
7 changed files with 41 additions and 10 deletions
@@ -100,7 +100,7 @@ test.describe('OCR with stacked assets', () => {
const stackThumbnails = page.locator('#stack-slideshow [data-asset]');
await expect(stackThumbnails).toHaveCount(2);
await stackThumbnails.nth(1).click();
await page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`).click();
// refreshOcr() clears showOverlay when switching assets, so re-enable it
await expect(ocrBoxes).toHaveCount(0);
@@ -75,8 +75,8 @@ test.describe('asset-viewer stack', () => {
await page.goto(`/photos/${fixture.primaryAsset.id}`);
await ensureDetailPanelVisible(page);
const stackAssets = page.locator('#stack-slideshow [data-asset]');
await stackAssets.nth(1).click();
const secondAsset = page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`);
await secondAsset.click();
const tags = page.getByTestId('detail-panel-tags').getByRole('link');
await expect(tags.first()).toHaveText('test/2');
@@ -60,7 +60,6 @@ class AssetService {
}
final stack = await _remoteRepository.getStackChildren(asset);
// Include the primary asset in the stack as the first item
return [asset, ...stack];
}
@@ -76,7 +76,7 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
final query = _db.remoteAssetEntity.select()
..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
..orderBy([(row) => OrderingTerm.asc(row.createdAt)]);
return query.map((row) => row.toDto()).get();
}
@@ -18,6 +18,37 @@ void main() {
await ctx.dispose();
});
group('getStackChildren', () {
test('returns stack children from oldest to newest', () async {
final user = await ctx.newUser();
const stackId = 'stack';
final primary = await ctx.newRemoteAsset(
id: 'primary',
ownerId: user.id,
stackId: stackId,
createdAt: DateTime(2026, 1, 2),
);
final beforePrimary = await ctx.newRemoteAsset(
id: 'before-primary',
ownerId: user.id,
stackId: stackId,
createdAt: DateTime(2026, 1, 1),
);
final afterPrimary = await ctx.newRemoteAsset(
id: 'after-primary',
ownerId: user.id,
stackId: stackId,
createdAt: DateTime(2026, 1, 3),
);
await ctx.newRemoteAsset(ownerId: user.id, stackId: 'other-stack');
final primaryAsset = await sut.get(primary.id);
final stack = await sut.getStackChildren(primaryAsset!);
expect(stack.map((asset) => asset.id), [beforePrimary.id, afterPrimary.id]);
});
});
group('getByChecksum', () {
late String userId;
@@ -291,16 +291,15 @@
if (!stack || !withStacked || assetViewerManager.isShowEditor) {
return;
}
const assets = stack.assets;
const currentIndex = assets.findIndex(({ id }) => id === asset.id);
const currentIndex = stack.assets.findIndex(({ id }) => id === asset.id);
if (currentIndex === -1) {
return;
}
const nextIndex = direction === 'previous' ? currentIndex - 1 : currentIndex + 1;
if (nextIndex < 0 || nextIndex >= assets.length) {
if (nextIndex < 0 || nextIndex >= stack.assets.length) {
return;
}
cursor.current = assets[nextIndex];
cursor.current = stack.assets[nextIndex];
};
/**
@@ -94,7 +94,9 @@
};
const handleStack = () => {
onStack(assets);
const selectedAssets = assets.filter(({ id }) => selectedAssetIds.has(id));
const unselectedAssets = assets.filter(({ id }) => !selectedAssetIds.has(id));
onStack([...selectedAssets, ...unselectedAssets]);
};
const assetCursor = $derived({