fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab
Surgical fixes for the 7 UAT blockers that prevent productive forward
testing. Each item has a corresponding entry in alpha-uat-master.md.
- supplemental-info route relocated out of (portal) so it bypasses the
isPortalDisabledGlobally() kill-switch. URL unchanged.
- file upload service derives client_id/company_id/yacht_id from
(entityType, entityId) when not explicitly passed, so interest-tab
uploads no longer land with client_id=NULL and stay visible in the
Attachments list.
- triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils
attach the anchor to the DOM before click so Chromium honours the
download attribute; 7 sites refactored, file-named downloads stop
arriving as bare UUIDs.
- search-nav-catalog dedupes by href at the result-collection layer so
the same href can no longer surface twice in the command-K dropdown
(kills the React duplicate-key warning); /admin/templates entries
merged into a single richer-keyword variant.
- NotesList gains a parentInvalidateKey prop, wired through all five
callers (interest, client, yacht, company, residential client/
interest) so the Overview "Latest note" teaser refreshes when a note
is added in the Notes tab.
- expense-form-dialog: setValue('receiptFileIds') / setValue(
'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level
refine sees the field and Create stops silently no-op'ing on submit.
- bulk-add-berths-wizard: side-pontoon dropdown now reads through
useVocabulary('berth_side_pontoon_options') instead of a wrong local
enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches
the rest of the platform + honours admin-editable per-port overrides.
tsc clean. 1419/1419 vitest. lint clean on touched files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient, type QueryKey } from '@tanstack/react-query';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Lock, Pencil, Trash2, Send, Loader2 } from 'lucide-react';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
@@ -100,6 +100,14 @@ interface NotesListProps {
|
||||
* residential_clients}. Ignored for interests / residential_interests.
|
||||
*/
|
||||
aggregate?: boolean;
|
||||
/**
|
||||
* Optional parent-entity query key to invalidate alongside the notes
|
||||
* query on create/update/delete. The parent entity detail typically
|
||||
* hydrates a `recentNote` / `notesCount` teaser that goes stale after
|
||||
* a note mutation; passing the detail's query key here keeps it in
|
||||
* sync without a hard refresh.
|
||||
*/
|
||||
parentInvalidateKey?: QueryKey;
|
||||
}
|
||||
|
||||
const NOTE_EDIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
||||
@@ -126,8 +134,20 @@ function sortByGroup(notes: Note[]): Note[] {
|
||||
});
|
||||
}
|
||||
|
||||
export function NotesList({ entityType, entityId, currentUserId, aggregate }: NotesListProps) {
|
||||
export function NotesList({
|
||||
entityType,
|
||||
entityId,
|
||||
currentUserId,
|
||||
aggregate,
|
||||
parentInvalidateKey,
|
||||
}: NotesListProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const invalidateAll = () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
if (parentInvalidateKey) {
|
||||
queryClient.invalidateQueries({ queryKey: parentInvalidateKey });
|
||||
}
|
||||
};
|
||||
const [newNote, setNewNote] = useState('');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editContent, setEditContent] = useState('');
|
||||
@@ -164,7 +184,7 @@ export function NotesList({ entityType, entityId, currentUserId, aggregate }: No
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (content: string) => apiFetch(baseEndpoint, { method: 'POST', body: { content } }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
invalidateAll();
|
||||
setNewNote('');
|
||||
},
|
||||
});
|
||||
@@ -173,14 +193,14 @@ export function NotesList({ entityType, entityId, currentUserId, aggregate }: No
|
||||
mutationFn: ({ noteId, content }: { noteId: string; content: string }) =>
|
||||
apiFetch(`${baseEndpoint}/${noteId}`, { method: 'PATCH', body: { content } }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
invalidateAll();
|
||||
setEditingId(null);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (noteId: string) => apiFetch(`${baseEndpoint}/${noteId}`, { method: 'DELETE' }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey }),
|
||||
onSuccess: () => invalidateAll(),
|
||||
});
|
||||
|
||||
function canEdit(note: Note): boolean {
|
||||
|
||||
Reference in New Issue
Block a user