Manual save button, file requirement labels, fix config revert bug
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

- Replace auto-save with manual floating save bar that appears when config
  has unsaved changes (Discard / Save Changes buttons). Fixes race condition
  where server sync overwrote local state after toggling switches.
- Show file requirement name (e.g. "Pitch Deck", "Presentation") above each
  document in the All Uploaded Files section on project detail page
- Pass requirement relation data through to FileViewer component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-17 16:43:47 +01:00
parent 1c6961355b
commit a4ff278db2
3 changed files with 65 additions and 31 deletions

View File

@@ -677,6 +677,13 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
detectedLang: f.detectedLang, detectedLang: f.detectedLang,
langConfidence: f.langConfidence, langConfidence: f.langConfidence,
analyzedAt: f.analyzedAt ? String(f.analyzedAt) : null, analyzedAt: f.analyzedAt ? String(f.analyzedAt) : null,
requirementId: f.requirementId,
requirement: f.requirement ? {
id: f.requirement.id,
name: f.requirement.name,
description: f.requirement.description,
isRequired: f.requirement.isRequired,
} : null,
}))} }))}
/> />
</div> </div>

View File

@@ -6,7 +6,6 @@ import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useDebouncedCallback } from 'use-debounce'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@@ -167,7 +166,6 @@ export default function RoundDetailPage() {
const [config, setConfig] = useState<Record<string, unknown>>({}) const [config, setConfig] = useState<Record<string, unknown>>({})
const [autosaveStatus, setAutosaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle') const [autosaveStatus, setAutosaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const pendingSaveRef = useRef(false)
const [activeTab, setActiveTab] = useState('overview') const [activeTab, setActiveTab] = useState('overview')
const [previewSheetOpen, setPreviewSheetOpen] = useState(false) const [previewSheetOpen, setPreviewSheetOpen] = useState(false)
@@ -239,28 +237,29 @@ export default function RoundDetailPage() {
{ enabled: round?.roundType === 'FILTERING', refetchInterval: 5_000 }, { enabled: round?.roundType === 'FILTERING', refetchInterval: 5_000 },
) )
// Sync config from server when no pending save // Initialize config from server once on load (or when round changes externally)
if (round && !pendingSaveRef.current) { const serverConfig = useMemo(() => (round?.configJson as Record<string, unknown>) ?? {}, [round?.configJson])
const configInitialized = useRef(false)
if (round && !configInitialized.current) {
const roundConfig = (round.configJson as Record<string, unknown>) ?? {} const roundConfig = (round.configJson as Record<string, unknown>) ?? {}
if (JSON.stringify(roundConfig) !== JSON.stringify(config)) { if (Object.keys(config).length === 0 || JSON.stringify(roundConfig) !== JSON.stringify(config)) {
setConfig(roundConfig) setConfig(roundConfig)
} }
configInitialized.current = true
} }
const hasUnsavedConfig = useMemo(
() => configInitialized.current && JSON.stringify(config) !== JSON.stringify(serverConfig),
[config, serverConfig],
)
// ── Mutations ────────────────────────────────────────────────────────── // ── Mutations ──────────────────────────────────────────────────────────
const updateMutation = trpc.round.update.useMutation({ const updateMutation = trpc.round.update.useMutation({
onSuccess: () => { onSuccess: () => {
utils.round.getById.invalidate({ id: roundId }) utils.round.getById.invalidate({ id: roundId })
setAutosaveStatus('saved') setAutosaveStatus('saved')
// Keep pendingSaveRef locked briefly so the sync block doesn't
// overwrite local config with stale cache before refetch completes
setTimeout(() => {
pendingSaveRef.current = false
}, 500)
setTimeout(() => setAutosaveStatus('idle'), 2000) setTimeout(() => setAutosaveStatus('idle'), 2000)
}, },
onError: (err) => { onError: (err) => {
pendingSaveRef.current = false
setAutosaveStatus('error') setAutosaveStatus('error')
toast.error(err.message) toast.error(err.message)
}, },
@@ -389,17 +388,14 @@ export default function RoundDetailPage() {
const isTransitioning = activateMutation.isPending || closeMutation.isPending || reopenMutation.isPending || archiveMutation.isPending const isTransitioning = activateMutation.isPending || closeMutation.isPending || reopenMutation.isPending || archiveMutation.isPending
const debouncedSave = useDebouncedCallback((newConfig: Record<string, unknown>) => {
setAutosaveStatus('saving')
updateMutation.mutate({ id: roundId, configJson: newConfig })
}, 1500)
const handleConfigChange = useCallback((newConfig: Record<string, unknown>) => { const handleConfigChange = useCallback((newConfig: Record<string, unknown>) => {
setConfig(newConfig) setConfig(newConfig)
pendingSaveRef.current = true }, [])
const saveConfig = useCallback(() => {
setAutosaveStatus('saving') setAutosaveStatus('saving')
debouncedSave(newConfig) updateMutation.mutate({ id: roundId, configJson: config })
}, [debouncedSave]) }, [config, roundId, updateMutation])
// ── Computed values ──────────────────────────────────────────────────── // ── Computed values ────────────────────────────────────────────────────
const projectCount = round?._count?.projectRoundStates ?? 0 const projectCount = round?._count?.projectRoundStates ?? 0
@@ -603,24 +599,12 @@ export default function RoundDetailPage() {
{/* Action buttons */} {/* Action buttons */}
<div className="flex items-center gap-2 shrink-0 flex-wrap"> <div className="flex items-center gap-2 shrink-0 flex-wrap">
{autosaveStatus === 'saving' && (
<span className="flex items-center gap-1.5 text-xs text-white/70">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Saving...
</span>
)}
{autosaveStatus === 'saved' && ( {autosaveStatus === 'saved' && (
<span className="flex items-center gap-1.5 text-xs text-emerald-300"> <span className="flex items-center gap-1.5 text-xs text-emerald-300">
<CheckCircle2 className="h-3.5 w-3.5" /> <CheckCircle2 className="h-3.5 w-3.5" />
Saved Saved
</span> </span>
)} )}
{autosaveStatus === 'error' && (
<span className="flex items-center gap-1.5 text-xs text-red-300">
<AlertTriangle className="h-3.5 w-3.5" />
Save failed
</span>
)}
<Link href={poolLink}> <Link href={poolLink}>
<Button variant="outline" size="sm" className="border-white/40 bg-white/15 text-white hover:bg-white/30 hover:text-white"> <Button variant="outline" size="sm" className="border-white/40 bg-white/15 text-white hover:bg-white/30 hover:text-white">
<Layers className="h-4 w-4 mr-1.5" /> <Layers className="h-4 w-4 mr-1.5" />
@@ -2009,6 +1993,44 @@ export default function RoundDetailPage() {
</TabsContent> </TabsContent>
)} )}
</Tabs> </Tabs>
{/* Floating save bar — appears when config has unsaved changes */}
{hasUnsavedConfig && (
<div className="fixed bottom-0 left-0 right-0 z-50 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 shadow-[0_-4px_12px_rgba(0,0,0,0.1)]">
<div className="container flex items-center justify-between py-3 px-4 max-w-5xl mx-auto">
<div className="flex items-center gap-2 text-sm">
<div className="h-2 w-2 rounded-full bg-amber-500 animate-pulse" />
<span className="text-muted-foreground">You have unsaved changes</span>
</div>
<div className="flex items-center gap-2">
{autosaveStatus === 'error' && (
<span className="text-xs text-red-500 mr-2">Save failed try again</span>
)}
<Button
size="sm"
variant="outline"
onClick={() => {
setConfig(serverConfig)
}}
>
Discard
</Button>
<Button
size="sm"
onClick={saveConfig}
disabled={updateMutation.isPending}
className="bg-[#de0f1e] hover:bg-[#c00d1a] text-white"
>
{updateMutation.isPending ? (
<><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Saving...</>
) : (
<><Save className="h-3.5 w-3.5 mr-1.5" />Save Changes</>
)}
</Button>
</div>
</div>
</div>
)}
</div> </div>
) )
} }

View File

@@ -258,6 +258,11 @@ function FileItem({ file }: { file: ProjectFile }) {
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{file.requirement && (
<p className="text-xs font-semibold text-primary/80 mb-0.5 uppercase tracking-wide">
{file.requirement.name}
</p>
)}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="font-medium truncate">{file.fileName}</p> <p className="font-medium truncate">{file.fileName}</p>
{file.version != null && file.version > 1 && ( {file.version != null && file.version > 1 && (