mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(client): sort the picker grid in a memo again, not inside the fetches
Sorting inside searchPhotos and loadAlbumPhotos looks equivalent and is not. contextLocation is live editor state. The place search and "use my location" write it while the picker stays mounted, and the picker key is only provider plus date, so nothing remounts it. Open an entry with no location, let the photos load, then search for Rome: the caption flips to nearest-first and the grid does not move. The appending search made it worse than that. It sorted only the page it had just fetched before appending, so from page two on the grid was a sequence of sorted blocks rather than a sorted list, and a photo from page two landed under a day heading page one had already closed. FE-JRN-PICKER-021 pins it and is red without the memo. The video badge also gets z-10 and pointer-events-none. It shares a stacking context with the city gradient that follows it, so the later sibling was winning on the geotagged tiles; pointer-events-none is what the eight existing video overlays in the journey code all set.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-JRN-PICKER-001 to FE-JRN-PICKER-020
|
||||
// FE-JRN-PICKER-001 to FE-JRN-PICKER-021
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { http, HttpResponse, delay } from 'msw'
|
||||
@@ -324,6 +324,39 @@ describe('ProviderPicker', () => {
|
||||
expect(range.to.endsWith('-09')).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-JRN-PICKER-021: a location picked after the photos loaded reorders the grid', async () => {
|
||||
// contextLocation is live editor state: the place search and "use my location"
|
||||
// write it while this picker stays mounted, and the key is only provider plus
|
||||
// date. Sorting inside the fetch instead of in a memo leaves the grid frozen
|
||||
// while the caption above it already claims nearest-first.
|
||||
searchReturns([
|
||||
asset('rome', { lat: 41.9, lng: 12.5, takenAt: '2026-03-15T09:00:00.000Z' }),
|
||||
asset('helsinki', { lat: 60.17, lng: 24.94, takenAt: '2026-03-15T11:00:00.000Z' }),
|
||||
])
|
||||
const props = {
|
||||
provider: 'immich',
|
||||
userId: 42,
|
||||
entries,
|
||||
trips,
|
||||
existingAssetIds: new Set<string>(),
|
||||
onClose: vi.fn(),
|
||||
onAdd: vi.fn(async () => {}),
|
||||
} as React.ComponentProps<typeof ProviderPicker>
|
||||
const { container, rerender } = render(<ProviderPicker {...props} />)
|
||||
await screen.findByText('March 15, 2026')
|
||||
|
||||
const order = () => Array.from(container.querySelectorAll('img'))
|
||||
.map(img => (img.getAttribute('src') || '').split('/assets/0/')[1]?.split('/')[0])
|
||||
.filter(Boolean)
|
||||
|
||||
// Nothing to be near yet, so newest first.
|
||||
expect(order()).toEqual(['helsinki', 'rome'])
|
||||
|
||||
rerender(<ProviderPicker {...props} contextLocation={{ lat: 41.9, lng: 12.5, name: 'Rome' }} />)
|
||||
|
||||
await waitFor(() => expect(order()).toEqual(['rome', 'helsinki']))
|
||||
})
|
||||
|
||||
it('FE-JRN-PICKER-020: closes through the header button and the backdrop', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { container, onClose } = mountPicker()
|
||||
|
||||
@@ -95,7 +95,7 @@ export function ProviderPicker({
|
||||
setSearchPage(page);
|
||||
try {
|
||||
const data = await memoriesApi.search(provider, { from, to, page, size: 50 }, signal);
|
||||
const assets = sortProviderPhotos(data.assets || [], contextLocation);
|
||||
const assets = data.assets || [];
|
||||
setPhotos((prev) => (append ? [...prev, ...assets] : assets));
|
||||
setHasMore(!!data.hasMore);
|
||||
} catch {
|
||||
@@ -121,7 +121,7 @@ export function ProviderPicker({
|
||||
setHasMore(false);
|
||||
try {
|
||||
const assets = (await memoriesApi.albumPhotos(provider, album.id, album.passphrase, signal)).assets || [];
|
||||
setPhotos(sortProviderPhotos(assets, contextLocation));
|
||||
setPhotos(assets);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -151,6 +151,17 @@ export function ProviderPicker({
|
||||
if (customFrom && customTo) searchPhotos(customFrom, customTo);
|
||||
};
|
||||
|
||||
// Sorted here rather than in the two fetches above. contextLocation is live
|
||||
// editor state: the place search and "use my location" write it while this
|
||||
// picker stays mounted, and its key is only provider plus date, so a fetch-time
|
||||
// sort would leave the grid frozen while the caption already says "near Rome".
|
||||
// It also keeps the ranking global instead of per page, which a sort inside
|
||||
// the appending search cannot do.
|
||||
const sortedPhotos = useMemo(
|
||||
() => sortProviderPhotos(photos, contextLocation),
|
||||
[photos, contextLocation?.lat, contextLocation?.lng],
|
||||
);
|
||||
|
||||
const toggleAsset = (id: string) => {
|
||||
setSelected((prev) => {
|
||||
const next = new Map(prev);
|
||||
@@ -411,9 +422,9 @@ export function ProviderPicker({
|
||||
|
||||
{/* Select all bar — sticky above grid */}
|
||||
{!loading &&
|
||||
photos.length > 0 &&
|
||||
sortedPhotos.length > 0 &&
|
||||
(() => {
|
||||
const selectable = photos.filter((a: any) => !existingAssetIds.has(a.id));
|
||||
const selectable = sortedPhotos.filter((a: any) => !existingAssetIds.has(a.id));
|
||||
const allSelected = selectable.length > 0 && selectable.every((a: any) => selected.has(a.id));
|
||||
if (selectable.length === 0) return null;
|
||||
return (
|
||||
@@ -461,7 +472,7 @@ export function ProviderPicker({
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-zinc-300 border-t-zinc-900" />
|
||||
</div>
|
||||
) : photos.length === 0 ? (
|
||||
) : sortedPhotos.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<p className="text-[13px] text-zinc-500">
|
||||
{filter === 'trip' && !tripRange.from
|
||||
@@ -471,7 +482,7 @@ export function ProviderPicker({
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{groupPhotosByDate(photos).map((group) => (
|
||||
{groupPhotosByDate(sortedPhotos).map((group) => (
|
||||
<div key={group.date}>
|
||||
{(!embedded || filter !== 'day') && (
|
||||
<p className="mt-4 mb-2 text-[11px] font-medium text-zinc-500 first:mt-0 dark:text-zinc-400">
|
||||
@@ -520,7 +531,7 @@ export function ProviderPicker({
|
||||
</div>
|
||||
)}
|
||||
{asset.mediaType === 'video' && (
|
||||
<div className="absolute bottom-1.5 left-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-black/60">
|
||||
<div className="pointer-events-none absolute bottom-1.5 left-1.5 z-10 flex h-4 w-4 items-center justify-center rounded-full bg-black/60">
|
||||
<Play size={8} className="ml-px text-white" fill="currentColor" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user