mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
fix(collab): note attachments added or removed on the phone show in the Files tab at once
On the phone, attaching a file to a note, detaching one or deleting a note with attachments left the trip's Files tab on the old list until a reload. The desktop panel fires the local collab-files-changed event that useTripWebSocket listens for, because the server leaves the writer's own socket out of its broadcast; the phone notes tab never fired it. The event now lives in the shared useCollabNotesData hook: uploadNoteFiles announces it once after a batch, and announceNoteFilesChanged is what both shells call after deleting a note or a note file. The desktop no longer fires it by hand after an upload. Red before the fix: tests/unit/mobile/trip/MCollabNotes.test.tsx FE-MOB-CNOTE-034 failed with "expected 0 to be 1" on the event count after detaching a file. FE-COLLAB-NOTESDATA-009 covers the hook. No existing assertions changed.
This commit is contained in:
@@ -16,7 +16,7 @@ import ConfirmDialog from '../shared/ConfirmDialog'
|
||||
import EmptyState from '../shared/EmptyState'
|
||||
import type { User } from '../../types'
|
||||
import type { CollabNote } from './CollabNotes.types'
|
||||
import { useCollabNotesData } from './useCollabNotesData'
|
||||
import { announceNoteFilesChanged, useCollabNotesData } from './useCollabNotesData'
|
||||
import { FONT, NOTE_COLORS } from './CollabNotes.constants'
|
||||
import { NoteFormModal } from './CollabNotesFormModal'
|
||||
import { CategorySettingsModal } from './CollabNotesCategorySettingsModal'
|
||||
@@ -101,7 +101,6 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
|
||||
// Reload note with attachments
|
||||
const fresh = await collabApi.getNotes(tripId)
|
||||
if (fresh?.notes) setNotes(fresh.notes)
|
||||
window.dispatchEvent(new Event('collab-files-changed'))
|
||||
return
|
||||
}
|
||||
setNotes(prev => {
|
||||
@@ -185,13 +184,12 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
|
||||
await uploadNoteFiles(editingNote.id, pendingFiles)
|
||||
const fresh = await collabApi.getNotes(tripId)
|
||||
if (fresh?.notes) setNotes(fresh.notes)
|
||||
window.dispatchEvent(new Event('collab-files-changed'))
|
||||
}
|
||||
}, [editingNote, tripId, handleUpdateNote, setNotes, uploadNoteFiles])
|
||||
|
||||
const handleDeleteNoteFile = useCallback(async (noteId, fileId) => {
|
||||
try { await collabApi.deleteNoteFile(tripId, noteId, fileId) } catch { toast.error(t('common.error')) }
|
||||
window.dispatchEvent(new Event('collab-files-changed'))
|
||||
announceNoteFilesChanged()
|
||||
}, [tripId, toast, t])
|
||||
|
||||
const handleDeleteNote = useCallback(async (noteId) => {
|
||||
@@ -202,7 +200,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
|
||||
throw err
|
||||
}
|
||||
setNotes(prev => prev.filter(n => n.id !== noteId))
|
||||
window.dispatchEvent(new Event('collab-files-changed'))
|
||||
announceNoteFilesChanged()
|
||||
}, [tripId, toast, t, setNotes])
|
||||
|
||||
// ── Derived data ──
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FE-COLLAB-NOTESDATA-001 to FE-COLLAB-NOTESDATA-008: the note list behind the desktop
|
||||
// FE-COLLAB-NOTESDATA-001 to FE-COLLAB-NOTESDATA-009: the note list behind the desktop
|
||||
// Collab panel (empties on a failed load, waits for a trip id) and the phone's notes tab
|
||||
// (keeps the list, always loads), with
|
||||
// the live note events and the attachment upload both run after saving a note.
|
||||
@@ -170,4 +170,23 @@ describe('useCollabNotesData', () => {
|
||||
act(() => result.current.setNotes([note(9)]));
|
||||
expect(result.current.notes.map((n) => n.id)).toEqual([9]);
|
||||
});
|
||||
|
||||
it('FE-COLLAB-NOTESDATA-009: an upload batch tells the Files tab to reload once, an empty one does not', async () => {
|
||||
const onFilesChanged = vi.fn();
|
||||
window.addEventListener('collab-files-changed', onFilesChanged);
|
||||
try {
|
||||
vi.mocked(collabApi.uploadNoteFile).mockRejectedValueOnce(new Error('too big'));
|
||||
const { result } = await loaded();
|
||||
await act(async () => {
|
||||
await result.current.uploadNoteFiles(5, []);
|
||||
});
|
||||
expect(onFilesChanged).not.toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
await result.current.uploadNoteFiles(5, [new File(['a'], 'a.pdf'), new File(['b'], 'b.pdf')]);
|
||||
});
|
||||
expect(onFilesChanged).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
window.removeEventListener('collab-files-changed', onFilesChanged);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,15 @@ export interface CollabNotesDataOptions {
|
||||
waitForTripId?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the trip's Files tab that note attachments changed. The server leaves the writer's
|
||||
* own socket out of its broadcast, so the tab that made the change has to say it itself;
|
||||
* useTripWebSocket listens and reloads the files.
|
||||
*/
|
||||
export function announceNoteFilesChanged(): void {
|
||||
window.dispatchEvent(new Event('collab-files-changed'));
|
||||
}
|
||||
|
||||
/**
|
||||
* The trip notes behind the desktop Collab panel and the phone's notes tab, each reading
|
||||
* the rows through its own note type: the list as the server has it, kept current by the
|
||||
@@ -76,7 +85,8 @@ export function useCollabNotesData<N extends { id: number } = CollabNoteData>({
|
||||
|
||||
/**
|
||||
* Uploads a saved note's attachments one by one; a file the server refuses is toasted
|
||||
* and the rest still go. `onFileError` sees each refusal before the toast.
|
||||
* and the rest still go. `onFileError` sees each refusal before the toast. Once the
|
||||
* batch is through, the Files tab is told to reload.
|
||||
*/
|
||||
const uploadNoteFiles = useCallback(
|
||||
async (noteId: number, files: File[], onFileError?: (err: unknown) => void) => {
|
||||
@@ -90,6 +100,7 @@ export function useCollabNotesData<N extends { id: number } = CollabNoteData>({
|
||||
toast.error(t('common.error'));
|
||||
}
|
||||
}
|
||||
if (files.length > 0) announceNoteFilesChanged();
|
||||
},
|
||||
[tripId, toast, t]
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { sanitizedMarkdownPlugins, sanitizedMarkdownComponents } from '../../../
|
||||
import { Check, ExternalLink, FileText, Paperclip, Pin, PinOff, Plus, StickyNote, Trash2, X } from 'lucide-react'
|
||||
import MDancingTrek from '../../../components/MDancingTrek'
|
||||
import { collabApi } from '../../../../api/client'
|
||||
import { useCollabNotesData } from '../../../../components/Collab/useCollabNotesData'
|
||||
import { announceNoteFilesChanged, useCollabNotesData } from '../../../../components/Collab/useCollabNotesData'
|
||||
import { openFile } from '../../../../utils/fileDownload'
|
||||
import { safeExternalHref } from '../../../../utils/safeUrl'
|
||||
import MSheet from '../../../components/MSheet'
|
||||
@@ -115,6 +115,7 @@ export default function MCollabNotes({ planner }: MCollabNotesProps) {
|
||||
try {
|
||||
await collabApi.deleteNote(tripId, noteId)
|
||||
setNotes(prev => prev.filter(n => n.id !== noteId))
|
||||
announceNoteFilesChanged()
|
||||
} catch {
|
||||
toast.error(t('common.error'))
|
||||
}
|
||||
@@ -128,6 +129,7 @@ export default function MCollabNotes({ planner }: MCollabNotesProps) {
|
||||
setNotes(prev => prev.map(n => (
|
||||
n.id === noteId ? { ...n, attachments: n.attachments.filter(f => f.id !== fileId) } : n
|
||||
)))
|
||||
announceNoteFilesChanged()
|
||||
return true
|
||||
} catch {
|
||||
toast.error(t('common.error'))
|
||||
|
||||
@@ -607,4 +607,35 @@ describe('MCollabNotes', () => {
|
||||
expect(links).toHaveLength(1)
|
||||
expect(links[0]).toHaveAttribute('href', 'https://visa.test')
|
||||
})
|
||||
|
||||
it('FE-MOB-CNOTE-034: attaching, detaching and deleting tell the Files tab to reload', async () => {
|
||||
let filesChanged = 0
|
||||
const onFilesChanged = () => { filesChanged += 1 }
|
||||
window.addEventListener('collab-files-changed', onFilesChanged)
|
||||
try {
|
||||
serveNotes([note(1, {
|
||||
title: 'Visa',
|
||||
attachments: [{ id: 5, filename: 'p', original_name: 'passport.pdf', url: '/uploads/p' }],
|
||||
})])
|
||||
serveWrites()
|
||||
await renderNotes()
|
||||
|
||||
fireEvent.click(screen.getByText('Visa'))
|
||||
fireEvent.click(within(screen.getByText('passport.pdf')).getByRole('button'))
|
||||
await waitFor(() => expect(deletedFiles).toEqual(['1/5']))
|
||||
await waitFor(() => expect(filesChanged).toBe(1))
|
||||
|
||||
fireEvent.change(fileInput(), { target: { files: [new File(['a'], 'x.pdf')] } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'collab.notes.save' }))
|
||||
await waitFor(() => expect(uploadedTo).toEqual(['1']))
|
||||
await waitFor(() => expect(filesChanged).toBe(2))
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'collab.notes.delete' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.delete' }))
|
||||
await waitFor(() => expect(deletedIds).toEqual(['1']))
|
||||
await waitFor(() => expect(filesChanged).toBe(3))
|
||||
} finally {
|
||||
window.removeEventListener('collab-files-changed', onFilesChanged)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user