fix(sdk): Auto-save company profile to SDK context and backend
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 51s
CI/CD / test-python-backend-compliance (push) Successful in 38s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 28s
CI/CD / deploy-hetzner (push) Failing after 6s
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 51s
CI/CD / test-python-backend-compliance (push) Successful in 38s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 28s
CI/CD / deploy-hetzner (push) Failing after 6s
Profile data was lost when navigating away because it was only saved to SDK context on explicit button click (Next/Save). Scope data persisted because it auto-synced on every change. Added two debounced auto-save mechanisms: - SDK context sync (500ms) — survives in-app navigation - Backend save (2s) — survives page reload/session change Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { useSDK } from '@/lib/sdk'
|
import { useSDK } from '@/lib/sdk'
|
||||||
import {
|
import {
|
||||||
CompanyProfile,
|
CompanyProfile,
|
||||||
@@ -2469,6 +2469,39 @@ export default function CompanyProfilePage() {
|
|||||||
setFormData(prev => ({ ...prev, ...updates }))
|
setFormData(prev => ({ ...prev, ...updates }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Auto-save: sync formData to SDK context (debounced) so data survives navigation.
|
||||||
|
// This mirrors the pattern used by compliance-scope/page.tsx.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const autoSaveRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
const initialLoadDone = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip the initial load — only auto-save after user has started editing
|
||||||
|
if (!initialLoadDone.current) {
|
||||||
|
// Mark initial load done after first formData update (from backend or SDK state)
|
||||||
|
if (formData.companyName !== undefined) {
|
||||||
|
initialLoadDone.current = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce: sync to SDK context after 500ms of inactivity
|
||||||
|
if (autoSaveRef.current) clearTimeout(autoSaveRef.current)
|
||||||
|
autoSaveRef.current = setTimeout(() => {
|
||||||
|
// Only sync if there's meaningful data (not just defaults)
|
||||||
|
const hasData = formData.companyName || (formData.industry && formData.industry.length > 0)
|
||||||
|
if (hasData) {
|
||||||
|
setCompanyProfile({ ...formData, isComplete: false, completedAt: null } as CompanyProfile)
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autoSaveRef.current) clearTimeout(autoSaveRef.current)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [formData])
|
||||||
|
|
||||||
// Shared payload builder for draft saves and final save (DRY)
|
// Shared payload builder for draft saves and final save (DRY)
|
||||||
const buildProfilePayload = (isComplete: boolean) => ({
|
const buildProfilePayload = (isComplete: boolean) => ({
|
||||||
project_id: projectId || null,
|
project_id: projectId || null,
|
||||||
@@ -2537,7 +2570,39 @@ export default function CompanyProfilePage() {
|
|||||||
} : {}),
|
} : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auto-save draft to backend (fire-and-forget, non-blocking)
|
// ---------------------------------------------------------------------------
|
||||||
|
// Auto-save draft to backend (debounced, 2s after last change)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const backendAutoSaveRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialLoadDone.current) return
|
||||||
|
|
||||||
|
const hasData = formData.companyName || (formData.industry && formData.industry.length > 0)
|
||||||
|
if (!hasData) return
|
||||||
|
|
||||||
|
if (backendAutoSaveRef.current) clearTimeout(backendAutoSaveRef.current)
|
||||||
|
backendAutoSaveRef.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await fetch(profileApiUrl(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(buildProfilePayload(false)),
|
||||||
|
})
|
||||||
|
setDraftSaveStatus('saved')
|
||||||
|
if (draftSaveTimerRef.current) clearTimeout(draftSaveTimerRef.current)
|
||||||
|
draftSaveTimerRef.current = setTimeout(() => setDraftSaveStatus('idle'), 3000)
|
||||||
|
} catch {
|
||||||
|
// Silent fail for auto-save — user can still manually save via Next
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (backendAutoSaveRef.current) clearTimeout(backendAutoSaveRef.current)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [formData])
|
||||||
|
|
||||||
const [draftSaveStatus, setDraftSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
|
const [draftSaveStatus, setDraftSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
|
||||||
const draftSaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
const draftSaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user