'use client'; import { useEffect, useRef, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Field, FieldDescription, FieldGroup, FieldLabel, FieldTitle, } from '@/components/ui/field'; import { Input } from '@/components/ui/input'; import { Spinner } from '@/components/ui/spinner'; import { Switch } from '@/components/ui/switch'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { type PagesSource, type PagesSourceActionReceipt, type PagesSourceUpdatePayload, PagesService, } from '@/lib/services/openflare'; import { deploymentsQueryKey, projectQueryKey, projectsQueryKey, sourceQueryKey, } from '../../components/pages-utils'; import { type PagesGitHubSourceFormErrors, type PagesGitHubSourceFormValue, PagesSourceGitHubFields, } from './pages-source-github-fields'; import { validGitHubAssetName, validGitHubReleaseTag, validGitHubRepositoryURL, } from './pages-source-validation'; export type PagesSourceMode = 'manual' | 'remote_url' | 'github_release'; type Confirmation = 'manual' | null; interface PagesSourceDialogProps { open: boolean; onOpenChange: (open: boolean) => void; projectId: number; source: PagesSource; initialMode?: PagesSourceMode; onActionDispatched?: (receipt: PagesSourceActionReceipt) => void; } const DEFAULT_GITHUB_ASSET = 'dist.zip'; const DEFAULT_GITHUB_CHECK_INTERVAL = 1440; const EMPTY_GITHUB_ERRORS: PagesGitHubSourceFormErrors = { repository: '', releaseTag: '', assetName: '', checkInterval: '', }; function githubRepositoryURL(repository: string) { const value = repository.trim(); return value ? `https://github.com/${value}` : ''; } export function PagesSourceDialog({ open, onOpenChange, projectId, source, initialMode, onActionDispatched, }: PagesSourceDialogProps) { const t = useTranslations('pages.source'); const tCommon = useTranslations('common'); const queryClient = useQueryClient(); const [mode, setMode] = useState('manual'); const [allowInsecure, setAllowInsecure] = useState(false); const [remoteURL, setRemoteURL] = useState(''); const [urlError, setURLError] = useState(''); const [githubForm, setGitHubForm] = useState({ repositoryURL: '', releaseSelector: 'latest', releaseTag: '', assetName: DEFAULT_GITHUB_ASSET, autoUpdateEnabled: false, checkIntervalMinutes: String(DEFAULT_GITHUB_CHECK_INTERVAL), }); const [githubErrors, setGitHubErrors] = useState(EMPTY_GITHUB_ERRORS); const [confirmation, setConfirmation] = useState(null); const initializedForOpen = useRef(false); useEffect(() => { if (!open) { initializedForOpen.current = false; return; } // Runtime polling may replace the source view while the dialog is open. // Initialize only on the open edge so it cannot overwrite an unsaved draft. if (initializedForOpen.current) return; initializedForOpen.current = true; const nextMode = initialMode ?? source.source_type; setMode(nextMode); setAllowInsecure( source.source_type === 'remote_url' && Boolean(source.allow_insecure), ); setRemoteURL( source.source_type === 'remote_url' ? (source.remote_url ?? '') : '', ); setURLError(''); setGitHubForm({ repositoryURL: source.source_type === 'github_release' ? githubRepositoryURL(source.github_repository) : '', releaseSelector: source.source_type === 'github_release' ? source.release_selector : 'latest', releaseTag: source.source_type === 'github_release' ? (source.release_tag ?? '') : '', assetName: source.source_type === 'github_release' ? source.asset_name : DEFAULT_GITHUB_ASSET, autoUpdateEnabled: source.source_type === 'github_release' && source.release_selector === 'latest' ? source.auto_update_enabled : false, checkIntervalMinutes: source.source_type === 'github_release' && source.release_selector === 'latest' ? String( source.check_interval_minutes || DEFAULT_GITHUB_CHECK_INTERVAL, ) : String(DEFAULT_GITHUB_CHECK_INTERVAL), }); setGitHubErrors(EMPTY_GITHUB_ERRORS); setConfirmation(null); }, [initialMode, open, source]); const invalidateSourceState = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: sourceQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(projectId), }), queryClient.invalidateQueries({ queryKey: projectsQueryKey }), ]); }; const updateMutation = useMutation({ mutationFn: (payload: PagesSourceUpdatePayload) => PagesService.updateSource(projectId, payload), onSuccess: async (result) => { queryClient.setQueryData(sourceQueryKey(projectId), result.source); if (result.check_task) onActionDispatched?.(result.check_task); await invalidateSourceState(); toast.success(t('updated')); if (result.warning) toast.warning(result.warning); setConfirmation(null); onOpenChange(false); }, onError: (error) => { toast.error(error instanceof Error ? error.message : t('updateFailed')); }, }); const deleteMutation = useMutation({ mutationFn: () => PagesService.deleteSource(projectId), onSuccess: async (manualSource) => { queryClient.setQueryData(sourceQueryKey(projectId), manualSource); await invalidateSourceState(); toast.success(t('switchedManual')); setConfirmation(null); onOpenChange(false); }, onError: (error) => { toast.error(error instanceof Error ? error.message : t('switchFailed')); }, }); const isPending = updateMutation.isPending || deleteMutation.isPending; const remotePayload = (): PagesSourceUpdatePayload => ({ source_type: 'remote_url', remote_url: remoteURL.trim(), allow_insecure: allowInsecure, }); const submitRemote = () => { const value = remoteURL.trim(); if (!value) { setURLError(t('urlRequired')); return; } try { const parsed = new URL(value); if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(); } catch { setURLError(t('urlInvalid')); return; } setURLError(''); updateMutation.mutate(remotePayload()); }; const submitGitHub = () => { const normalizedRepositoryURL = githubForm.repositoryURL.trim(); const nextRepositoryError = validGitHubRepositoryURL( normalizedRepositoryURL, ) ? '' : t('githubUrlInvalid'); const nextReleaseTagError = githubForm.releaseSelector === 'tag' && !validGitHubReleaseTag(githubForm.releaseTag) ? t('tagInvalid') : ''; const nextAssetNameError = validGitHubAssetName(githubForm.assetName) ? '' : t('assetInvalid'); const checkIntervalMinutes = Number(githubForm.checkIntervalMinutes); const nextCheckIntervalError = githubForm.releaseSelector === 'latest' && (!Number.isInteger(checkIntervalMinutes) || checkIntervalMinutes < 5 || checkIntervalMinutes > 1440) ? t('intervalInvalid') : ''; setGitHubErrors({ repository: nextRepositoryError, releaseTag: nextReleaseTagError, assetName: nextAssetNameError, checkInterval: nextCheckIntervalError, }); if ( nextRepositoryError || nextReleaseTagError || nextAssetNameError || nextCheckIntervalError ) { return; } const payload: PagesSourceUpdatePayload = githubForm.releaseSelector === 'latest' ? { source_type: 'github_release', repository_url: normalizedRepositoryURL, release_selector: 'latest', release_tag: '', asset_name: githubForm.assetName, auto_update_enabled: githubForm.autoUpdateEnabled, check_interval_minutes: checkIntervalMinutes, } : { source_type: 'github_release', repository_url: normalizedRepositoryURL, release_selector: 'tag', release_tag: githubForm.releaseTag, asset_name: githubForm.assetName, auto_update_enabled: false, check_interval_minutes: 0, }; updateMutation.mutate(payload); }; const handleSubmit = () => { switch (mode) { case 'manual': if (source.source_type === 'manual') { onOpenChange(false); } else { setConfirmation('manual'); } return; case 'remote_url': submitRemote(); return; case 'github_release': submitGitHub(); } }; const submitLabel = mode === 'manual' ? t('useManual') : mode === 'remote_url' ? t('saveRemote') : t('saveGithub'); return ( <> { if (!isPending) onOpenChange(nextOpen); }} > {t('settingsTitle')} {t('settingsDesc')} {t('mode')} { if ( value === 'manual' || value === 'remote_url' || value === 'github_release' ) { setMode(value); if (value !== 'remote_url') { setURLError(''); } else if (source.source_type === 'remote_url') { setRemoteURL(source.remote_url ?? ''); } } }} > {t('manual')} Remote URL GitHub Release {mode === 'manual' ? ( {t('manual')}
{t('manualHint')}
) : mode === 'remote_url' ? ( <> Remote URL { setRemoteURL(event.target.value); setURLError(''); }} /> {urlError || t('urlHint')}

{t('allowInsecure')}

{t('allowInsecureDesc')}

) : ( )}
{ if (!nextOpen && !isPending) setConfirmation(null); }} > {t('switchManualTitle')} {t('switchManualDesc')} {tCommon('cancel')} { event.preventDefault(); if (confirmation === 'manual') { deleteMutation.mutate(); } }} > {isPending ? : null} {tCommon('confirm')} ); }