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,
langConfidence: f.langConfidence,
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>

View File

@@ -6,7 +6,6 @@ import Link from 'next/link'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
import { useDebouncedCallback } from 'use-debounce'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
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 [autosaveStatus, setAutosaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const pendingSaveRef = useRef(false)
const [activeTab, setActiveTab] = useState('overview')
const [previewSheetOpen, setPreviewSheetOpen] = useState(false)
@@ -239,28 +237,29 @@ export default function RoundDetailPage() {
{ enabled: round?.roundType === 'FILTERING', refetchInterval: 5_000 },
)
// Sync config from server when no pending save
if (round && !pendingSaveRef.current) {
// Initialize config from server once on load (or when round changes externally)
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>) ?? {}
if (JSON.stringify(roundConfig) !== JSON.stringify(config)) {
if (Object.keys(config).length === 0 || JSON.stringify(roundConfig) !== JSON.stringify(config)) {
setConfig(roundConfig)
}
configInitialized.current = true
}
const hasUnsavedConfig = useMemo(
() => configInitialized.current && JSON.stringify(config) !== JSON.stringify(serverConfig),
[config, serverConfig],
)
// ── Mutations ──────────────────────────────────────────────────────────
const updateMutation = trpc.round.update.useMutation({
onSuccess: () => {
utils.round.getById.invalidate({ id: roundId })
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)
},
onError: (err) => {
pendingSaveRef.current = false
setAutosaveStatus('error')
toast.error(err.message)
},
@@ -389,17 +388,14 @@ export default function RoundDetailPage() {
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>) => {
setConfig(newConfig)
pendingSaveRef.current = true
}, [])
const saveConfig = useCallback(() => {
setAutosaveStatus('saving')
debouncedSave(newConfig)
}, [debouncedSave])
updateMutation.mutate({ id: roundId, configJson: config })
}, [config, roundId, updateMutation])
// ── Computed values ────────────────────────────────────────────────────
const projectCount = round?._count?.projectRoundStates ?? 0
@@ -603,24 +599,12 @@ export default function RoundDetailPage() {
{/* Action buttons */}
<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' && (
<span className="flex items-center gap-1.5 text-xs text-emerald-300">
<CheckCircle2 className="h-3.5 w-3.5" />
Saved
</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}>
<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" />
@@ -2009,6 +1993,44 @@ export default function RoundDetailPage() {
</TabsContent>
)}
</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>
)
}