Manual save button, file requirement labels, fix config revert bug
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user