mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
v3.0.22 Bug Fixes & Improvements (#1041)
Bundles the v3.0.22 bug fixes and improvements. See the release notes for the full list.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// FE-STORE-JOURNEY-001 to FE-STORE-JOURNEY-015
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../../tests/helpers/msw/server';
|
||||
import { journeyApi } from '../api/client';
|
||||
import { useJourneyStore } from './journeyStore';
|
||||
import type { JourneyDetail, JourneyEntry, JourneyPhoto } from './journeyStore';
|
||||
|
||||
@@ -282,16 +283,64 @@ describe('journeyStore', () => {
|
||||
useJourneyStore.setState({ current: detail });
|
||||
|
||||
const newPhoto = buildPhoto({ id: 91, entry_id: 100 });
|
||||
server.use(
|
||||
http.post('/api/journeys/entries/100/photos', () =>
|
||||
HttpResponse.json({ photos: [newPhoto] })
|
||||
)
|
||||
);
|
||||
const result = await useJourneyStore.getState().uploadPhotos(100, new FormData());
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(91);
|
||||
// MSW's XHR interceptor calls request.arrayBuffer() on FormData bodies to
|
||||
// emit upload progress events, which hangs in jsdom+Node. Spy on the API
|
||||
// layer directly so this test exercises store state management only.
|
||||
const spy = vi.spyOn(journeyApi, 'uploadPhotos').mockResolvedValue({ photos: [newPhoto] } as any);
|
||||
const file = new File(['x'], 'photo.jpg', { type: 'image/jpeg' });
|
||||
const result = await useJourneyStore.getState().uploadPhotos(100, [file]);
|
||||
expect(result.succeeded).toHaveLength(1);
|
||||
expect(result.succeeded[0].id).toBe(91);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
const storedEntry = useJourneyStore.getState().current?.entries.find(e => e.id === 100);
|
||||
expect(storedEntry?.photos).toHaveLength(2);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('FE-STORE-JOURNEY-017: uploadPhotos returns failed files and merges only succeeded on network error', async () => {
|
||||
const entry = buildEntry({ id: 100, photos: [] });
|
||||
const detail = buildJourneyDetail({ id: 50, entries: [entry] });
|
||||
useJourneyStore.setState({ current: detail });
|
||||
|
||||
server.use(
|
||||
http.post('/api/journeys/entries/100/photos', () =>
|
||||
HttpResponse.error()
|
||||
)
|
||||
);
|
||||
const file = new File(['x'], 'fail.jpg', { type: 'image/jpeg' });
|
||||
const result = await useJourneyStore.getState().uploadPhotos(100, [file]);
|
||||
expect(result.succeeded).toHaveLength(0);
|
||||
expect(result.failed).toHaveLength(1);
|
||||
expect(result.failed[0]).toBe(file);
|
||||
const storedEntry = useJourneyStore.getState().current?.entries.find(e => e.id === 100);
|
||||
expect(storedEntry?.photos).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('FE-STORE-JOURNEY-018: uploadPhotos merges each file result incrementally on partial success', async () => {
|
||||
const entry = buildEntry({ id: 100, photos: [] });
|
||||
const detail = buildJourneyDetail({ id: 50, entries: [entry] });
|
||||
useJourneyStore.setState({ current: detail });
|
||||
|
||||
const photo1 = buildPhoto({ id: 91, entry_id: 100 });
|
||||
const photo2 = buildPhoto({ id: 92, entry_id: 100 });
|
||||
let callCount = 0;
|
||||
// Spy on the API layer to avoid MSW's FormData body hang (see FE-STORE-JOURNEY-013).
|
||||
// Use a 4xx-shaped error for file2 so isRetryable returns false and the test runs instantly.
|
||||
const spy = vi.spyOn(journeyApi, 'uploadPhotos').mockImplementation(async () => {
|
||||
callCount++;
|
||||
if (callCount === 1) return { photos: [photo1] } as any;
|
||||
throw Object.assign(new Error('Bad Request'), { response: { status: 400 } });
|
||||
});
|
||||
const file1 = new File(['a'], 'ok.jpg', { type: 'image/jpeg' });
|
||||
const file2 = new File(['b'], 'fail.jpg', { type: 'image/jpeg' });
|
||||
const result = await useJourneyStore.getState().uploadPhotos(100, [file1, file2], undefined);
|
||||
expect(result.succeeded).toHaveLength(1);
|
||||
expect(result.succeeded[0].id).toBe(photo1.id);
|
||||
expect(result.failed).toHaveLength(1);
|
||||
const storedEntry = useJourneyStore.getState().current?.entries.find(e => e.id === 100);
|
||||
expect(storedEntry?.photos).toHaveLength(1);
|
||||
void photo2; // referenced to avoid lint warning
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
// ── deletePhoto ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import { journeyApi } from '../api/client'
|
||||
import { uploadFilesResilient, type ResilientResult, type UploadProgress } from '../utils/uploadQueue'
|
||||
|
||||
export interface Journey {
|
||||
id: number
|
||||
@@ -121,8 +122,8 @@ interface JourneyState {
|
||||
deleteEntry: (entryId: number) => Promise<void>
|
||||
reorderEntries: (journeyId: number, orderedIds: number[]) => Promise<void>
|
||||
|
||||
uploadPhotos: (entryId: number, formData: FormData) => Promise<JourneyPhoto[]>
|
||||
uploadGalleryPhotos: (journeyId: number, formData: FormData) => Promise<GalleryPhoto[]>
|
||||
uploadPhotos: (entryId: number, files: File[], cbs?: { onProgress?: (p: UploadProgress) => void }) => Promise<ResilientResult<JourneyPhoto>>
|
||||
uploadGalleryPhotos: (journeyId: number, files: File[], cbs?: { onProgress?: (p: UploadProgress) => void }) => Promise<ResilientResult<GalleryPhoto>>
|
||||
unlinkPhoto: (entryId: number, journeyPhotoId: number) => Promise<void>
|
||||
deleteGalleryPhoto: (journeyId: number, journeyPhotoId: number) => Promise<void>
|
||||
deletePhoto: (photoId: number) => Promise<void>
|
||||
@@ -237,32 +238,49 @@ export const useJourneyStore = create<JourneyState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
uploadPhotos: async (entryId, formData) => {
|
||||
const data = await journeyApi.uploadPhotos(entryId, formData)
|
||||
const photos = data.photos || []
|
||||
set(s => {
|
||||
if (!s.current) return s
|
||||
return {
|
||||
current: {
|
||||
...s.current,
|
||||
entries: s.current.entries.map(e =>
|
||||
e.id === entryId ? { ...e, photos: [...(e.photos || []), ...photos] } : e
|
||||
),
|
||||
gallery: [...(s.current.gallery || []), ...(data.gallery || [])],
|
||||
},
|
||||
}
|
||||
})
|
||||
return photos
|
||||
uploadPhotos: async (entryId, files, cbs) => {
|
||||
return uploadFilesResilient<JourneyPhoto>(
|
||||
files,
|
||||
async (file, opts) => {
|
||||
const fd = new FormData()
|
||||
fd.append('photos', file)
|
||||
const data = await journeyApi.uploadPhotos(entryId, fd, opts)
|
||||
const photos: JourneyPhoto[] = data.photos || []
|
||||
const gallery: GalleryPhoto[] = data.gallery || []
|
||||
set(s => {
|
||||
if (!s.current) return s
|
||||
return {
|
||||
current: {
|
||||
...s.current,
|
||||
entries: s.current.entries.map(e =>
|
||||
e.id === entryId ? { ...e, photos: [...(e.photos || []), ...photos] } : e
|
||||
),
|
||||
gallery: [...(s.current.gallery || []), ...gallery],
|
||||
},
|
||||
}
|
||||
})
|
||||
return photos
|
||||
},
|
||||
{ onProgress: cbs?.onProgress },
|
||||
)
|
||||
},
|
||||
|
||||
uploadGalleryPhotos: async (journeyId, formData) => {
|
||||
const data = await journeyApi.uploadGalleryPhotos(journeyId, formData)
|
||||
const photos: GalleryPhoto[] = data.photos || []
|
||||
set(s => {
|
||||
if (!s.current || s.current.id !== journeyId) return s
|
||||
return { current: { ...s.current, gallery: [...(s.current.gallery || []), ...photos] } }
|
||||
})
|
||||
return photos
|
||||
uploadGalleryPhotos: async (journeyId, files, cbs) => {
|
||||
return uploadFilesResilient<GalleryPhoto>(
|
||||
files,
|
||||
async (file, opts) => {
|
||||
const fd = new FormData()
|
||||
fd.append('photos', file)
|
||||
const data = await journeyApi.uploadGalleryPhotos(journeyId, fd, opts)
|
||||
const photos: GalleryPhoto[] = data.photos || []
|
||||
set(s => {
|
||||
if (!s.current || s.current.id !== journeyId) return s
|
||||
return { current: { ...s.current, gallery: [...(s.current.gallery || []), ...photos] } }
|
||||
})
|
||||
return photos
|
||||
},
|
||||
{ onProgress: cbs?.onProgress },
|
||||
)
|
||||
},
|
||||
|
||||
unlinkPhoto: async (entryId, journeyPhotoId) => {
|
||||
|
||||
Reference in New Issue
Block a user