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,
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,6 +258,11 @@ function FileItem({ file }: { file: ProjectFile }) {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<p className="font-medium truncate">{file.fileName}</p>
|
||||
{file.version != null && file.version > 1 && (
|
||||
|
||||
Reference in New Issue
Block a user