fix: dirty tracking — prevent SWR revalidation from resetting form state
This commit is contained in:
@@ -23,11 +23,12 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
const { data: sessionsData, mutate: mutateSessions } = useSessions(siteId, botDateRange.start, botDateRange.end, botView === 'review' ? suspiciousOnly : false)
|
const { data: sessionsData, mutate: mutateSessions } = useSessions(siteId, botDateRange.start, botDateRange.end, botView === 'review' ? suspiciousOnly : false)
|
||||||
const sessions = sessionsData?.sessions
|
const sessions = sessionsData?.sessions
|
||||||
|
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (site) {
|
if (!site || hasInitialized.current) return
|
||||||
setFilterBots(site.filter_bots ?? false)
|
setFilterBots(site.filter_bots ?? false)
|
||||||
initialFilterRef.current = site.filter_bots ?? false
|
initialFilterRef.current = site.filter_bots ?? false
|
||||||
}
|
hasInitialized.current = true
|
||||||
}, [site])
|
}, [site])
|
||||||
|
|
||||||
// Track dirty state
|
// Track dirty state
|
||||||
|
|||||||
@@ -41,15 +41,16 @@ export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
|
|
||||||
const canEdit = user?.role === 'owner' || user?.role === 'admin'
|
const canEdit = user?.role === 'owner' || user?.role === 'admin'
|
||||||
const initialRef = useRef('')
|
const initialRef = useRef('')
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
const [isDirty, setIsDirty] = useState(false)
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (site) {
|
if (!site || hasInitialized.current) return
|
||||||
setName(site.name || '')
|
setName(site.name || '')
|
||||||
setTimezone(site.timezone || 'UTC')
|
setTimezone(site.timezone || 'UTC')
|
||||||
initialRef.current = JSON.stringify({ name: site.name || '', timezone: site.timezone || 'UTC' })
|
initialRef.current = JSON.stringify({ name: site.name || '', timezone: site.timezone || 'UTC' })
|
||||||
setIsDirty(false)
|
hasInitialized.current = true
|
||||||
}
|
setIsDirty(false)
|
||||||
}, [site])
|
}, [site])
|
||||||
|
|
||||||
// Track dirty state
|
// Track dirty state
|
||||||
|
|||||||
@@ -16,6 +16,18 @@ const GEO_OPTIONS = [
|
|||||||
{ value: 'none', label: 'Disabled' },
|
{ value: 'none', label: 'Disabled' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function PrivacyToggle({ label, desc, checked, onToggle }: { label: string; desc: string; checked: boolean; onToggle: () => void }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-3 px-4 rounded-xl hover:bg-neutral-800/20 transition-colors">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-white">{label}</p>
|
||||||
|
<p className="text-xs text-neutral-400">{desc}</p>
|
||||||
|
</div>
|
||||||
|
<Toggle checked={checked} onChange={onToggle} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) {
|
export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) {
|
||||||
const { data: site, mutate } = useSite(siteId)
|
const { data: site, mutate } = useSite(siteId)
|
||||||
const { data: subscription, error: subscriptionError, mutate: mutateSubscription } = useSubscription()
|
const { data: subscription, error: subscriptionError, mutate: mutateSubscription } = useSubscription()
|
||||||
@@ -33,28 +45,30 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
const [isDirty, setIsDirty] = useState(false)
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
const initialRef = useRef('')
|
const initialRef = useRef('')
|
||||||
|
|
||||||
|
// Sync form state from site data — only on first load, not on SWR revalidation
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (site) {
|
if (!site || hasInitialized.current) return
|
||||||
setCollectPagePaths(site.collect_page_paths ?? true)
|
setCollectPagePaths(site.collect_page_paths ?? true)
|
||||||
setCollectReferrers(site.collect_referrers ?? true)
|
setCollectReferrers(site.collect_referrers ?? true)
|
||||||
setCollectDeviceInfo(site.collect_device_info ?? true)
|
setCollectDeviceInfo(site.collect_device_info ?? true)
|
||||||
setCollectScreenRes(site.collect_screen_resolution ?? true)
|
setCollectScreenRes(site.collect_screen_resolution ?? true)
|
||||||
setCollectGeoData(site.collect_geo_data ?? 'full')
|
setCollectGeoData(site.collect_geo_data ?? 'full')
|
||||||
setHideUnknownLocations(site.hide_unknown_locations ?? false)
|
setHideUnknownLocations(site.hide_unknown_locations ?? false)
|
||||||
setDataRetention(site.data_retention_months ?? 6)
|
setDataRetention(site.data_retention_months ?? 6)
|
||||||
setExcludedPaths((site.excluded_paths || []).join('\n'))
|
setExcludedPaths((site.excluded_paths || []).join('\n'))
|
||||||
initialRef.current = JSON.stringify({
|
initialRef.current = JSON.stringify({
|
||||||
collectPagePaths: site.collect_page_paths ?? true,
|
collectPagePaths: site.collect_page_paths ?? true,
|
||||||
collectReferrers: site.collect_referrers ?? true,
|
collectReferrers: site.collect_referrers ?? true,
|
||||||
collectDeviceInfo: site.collect_device_info ?? true,
|
collectDeviceInfo: site.collect_device_info ?? true,
|
||||||
collectScreenRes: site.collect_screen_resolution ?? true,
|
collectScreenRes: site.collect_screen_resolution ?? true,
|
||||||
collectGeoData: site.collect_geo_data ?? 'full',
|
collectGeoData: site.collect_geo_data ?? 'full',
|
||||||
hideUnknownLocations: site.hide_unknown_locations ?? false,
|
hideUnknownLocations: site.hide_unknown_locations ?? false,
|
||||||
dataRetention: site.data_retention_months ?? 6,
|
dataRetention: site.data_retention_months ?? 6,
|
||||||
excludedPaths: (site.excluded_paths || []).join('\n'),
|
excludedPaths: (site.excluded_paths || []).join('\n'),
|
||||||
})
|
})
|
||||||
setIsDirty(false)
|
hasInitialized.current = true
|
||||||
}
|
setIsDirty(false)
|
||||||
}, [site])
|
}, [site])
|
||||||
|
|
||||||
// Track dirty state
|
// Track dirty state
|
||||||
@@ -101,21 +115,11 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{[
|
<PrivacyToggle label="Page paths" desc="Track which pages visitors view." checked={collectPagePaths} onToggle={() => setCollectPagePaths(v => !v)} />
|
||||||
{ label: 'Page paths', desc: 'Track which pages visitors view.', checked: collectPagePaths, onChange: setCollectPagePaths },
|
<PrivacyToggle label="Referrers" desc="Track where visitors come from." checked={collectReferrers} onToggle={() => setCollectReferrers(v => !v)} />
|
||||||
{ label: 'Referrers', desc: 'Track where visitors come from.', checked: collectReferrers, onChange: setCollectReferrers },
|
<PrivacyToggle label="Device info" desc="Track browser, OS, and device type." checked={collectDeviceInfo} onToggle={() => setCollectDeviceInfo(v => !v)} />
|
||||||
{ label: 'Device info', desc: 'Track browser, OS, and device type.', checked: collectDeviceInfo, onChange: setCollectDeviceInfo },
|
<PrivacyToggle label="Screen resolution" desc="Track visitor screen dimensions." checked={collectScreenRes} onToggle={() => setCollectScreenRes(v => !v)} />
|
||||||
{ label: 'Screen resolution', desc: 'Track visitor screen dimensions.', checked: collectScreenRes, onChange: setCollectScreenRes },
|
<PrivacyToggle label="Hide unknown locations" desc='Exclude "Unknown" from location stats.' checked={hideUnknownLocations} onToggle={() => setHideUnknownLocations(v => !v)} />
|
||||||
{ label: 'Hide unknown locations', desc: 'Exclude "Unknown" from location stats.', checked: hideUnknownLocations, onChange: setHideUnknownLocations },
|
|
||||||
].map(item => (
|
|
||||||
<div key={item.label} className="flex items-center justify-between py-3 px-4 rounded-xl hover:bg-neutral-800/20 transition-colors">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-white">{item.label}</p>
|
|
||||||
<p className="text-xs text-neutral-400">{item.desc}</p>
|
|
||||||
</div>
|
|
||||||
<Toggle checked={item.checked} onChange={() => item.onChange((p: boolean) => !p)} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s
|
|||||||
const [linkCopied, setLinkCopied] = useState(false)
|
const [linkCopied, setLinkCopied] = useState(false)
|
||||||
const [isDirty, setIsDirty] = useState(false)
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
const initialRef = useRef('')
|
const initialRef = useRef('')
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (site) {
|
if (!site || hasInitialized.current) return
|
||||||
setIsPublic(site.is_public ?? false)
|
setIsPublic(site.is_public ?? false)
|
||||||
setPasswordEnabled(site.has_password ?? false)
|
setPasswordEnabled(site.has_password ?? false)
|
||||||
initialRef.current = JSON.stringify({ isPublic: site.is_public ?? false, passwordEnabled: site.has_password ?? false })
|
initialRef.current = JSON.stringify({ isPublic: site.is_public ?? false, passwordEnabled: site.has_password ?? false })
|
||||||
setIsDirty(false)
|
hasInitialized.current = true
|
||||||
}
|
setIsDirty(false)
|
||||||
}, [site])
|
}, [site])
|
||||||
|
|
||||||
// Track dirty state
|
// Track dirty state
|
||||||
|
|||||||
Reference in New Issue
Block a user