Files
Verso/services/web/frontend/js/features/integrations-panel/git-sync-widget.tsx
T
claudeandClaude Sonnet 4.6 be8aef44fe
Build and Deploy Verso / deploy (push) Successful in 12m48s
feat(git-sync): independent toggles for project files and PDF push
Two new boolean fields on the project (gitSyncPushFiles, gitSyncPushPdf,
both default true) let users control what gets pushed independently:
- "Push project files" switch — skip all docs/binary files when off
- "Push compiled PDF" switch — grayed out when no pdfPath is set

The push button and auto-push are disabled when both switches would
result in nothing being pushed. Config is stored in MongoDB so settings
persist per-project.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:21:37 +00:00

256 lines
7.9 KiB
TypeScript

import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
import { postJSON } from '@/infrastructure/fetch-json'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import OLButton from '@/shared/components/ol/ol-button'
import OLFormControl from '@/shared/components/ol/ol-form-control'
import OLFormGroup from '@/shared/components/ol/ol-form-group'
import OLFormLabel from '@/shared/components/ol/ol-form-label'
import { FormCheck } from 'react-bootstrap'
import MaterialIcon from '@/shared/components/material-icon'
import OLNotification from '@/shared/components/ol/ol-notification'
type Action = 'save' | 'push' | 'pull'
type Status = 'idle' | 'busy' | 'success' | 'error'
const projectId = getMeta('ol-project_id')
const AUTO_PUSH_KEY = `gitSync.autoPushOnCompile.${projectId}`
export default function GitSyncWidget() {
const { t } = useTranslation()
const { compiling, error: compileError, pdfFile, clsiServerId } =
useCompileContext()
const [remoteUrl, setRemoteUrl] = useState(getMeta('ol-gitRemote') ?? '')
const [subPath, setSubPath] = useState(getMeta('ol-gitSyncPath') ?? '')
const [pdfPath, setPdfPath] = useState(getMeta('ol-gitSyncPdfPath') ?? '')
const [pushFiles, setPushFiles] = useState(getMeta('ol-gitSyncPushFiles') ?? true)
const [pushPdf, setPushPdf] = useState(getMeta('ol-gitSyncPushPdf') ?? true)
const [autoPush, setAutoPush] = useState(
() => localStorage.getItem(AUTO_PUSH_KEY) === 'true'
)
const [lastAction, setLastAction] = useState<Action>('save')
const [status, setStatus] = useState<Status>('idle')
const [errorMsg, setErrorMsg] = useState('')
const wasCompilingRef = useRef(false)
const isBusyRef = useRef(false)
useEffect(() => {
isBusyRef.current = status === 'busy'
}, [status])
// Auto-push when compile finishes successfully
useEffect(() => {
const wasCompiling = wasCompilingRef.current
wasCompilingRef.current = compiling
if (
wasCompiling &&
!compiling &&
!compileError &&
autoPush &&
!isBusyRef.current
) {
doPush(pdfFile?.build, clsiServerId)
}
}, [compiling]) // eslint-disable-line react-hooks/exhaustive-deps
async function saveConfig() {
await postJSON(`/project/${projectId}/git-sync`, {
body: { remoteUrl, subPath, pdfPath, pushFiles, pushPdf },
})
}
function extractError(err: any): string {
return err?.data?.error ?? err?.message ?? String(err)
}
async function doPush(buildId?: string, serverId?: string) {
setLastAction('push')
setStatus('busy')
setErrorMsg('')
try {
await saveConfig()
await postJSON(`/project/${projectId}/git-sync/push`, {
body: { buildId, clsiServerId: serverId },
})
setStatus('success')
} catch (err: any) {
setStatus('error')
setErrorMsg(extractError(err))
}
}
async function handleSave(e: React.FormEvent) {
e.preventDefault()
setLastAction('save')
setStatus('busy')
setErrorMsg('')
try {
await saveConfig()
setStatus('success')
} catch (err: any) {
setStatus('error')
setErrorMsg(extractError(err))
}
}
function handlePush() {
doPush(pdfFile?.build, clsiServerId)
}
async function handlePull() {
setLastAction('pull')
setStatus('busy')
setErrorMsg('')
try {
await saveConfig()
await postJSON(`/project/${projectId}/git-sync/pull`, { body: {} })
setStatus('success')
} catch (err: any) {
setStatus('error')
setErrorMsg(extractError(err))
}
}
function handleAutoPushToggle(e: React.ChangeEvent<HTMLInputElement>) {
const checked = e.target.checked
setAutoPush(checked)
localStorage.setItem(AUTO_PUSH_KEY, String(checked))
}
const isBusy = status === 'busy'
const nothingToPush = !pushFiles && (!pushPdf || !pdfPath.trim())
return (
<div className="git-sync-widget">
<div className="git-sync-widget-header">
<MaterialIcon type="merge" />
<span className="git-sync-widget-title">{t('git_sync')}</span>
</div>
<p className="git-sync-widget-description">{t('git_sync_description')}</p>
<form onSubmit={handleSave}>
<OLFormGroup controlId="git-sync-remote-url">
<OLFormLabel>{t('git_sync_remote_url')}</OLFormLabel>
<OLFormControl
type="text"
value={remoteUrl}
onChange={e => setRemoteUrl(e.target.value)}
placeholder="https://user:token@github.com/org/repo.git"
disabled={isBusy}
/>
</OLFormGroup>
<OLFormGroup controlId="git-sync-sub-path">
<OLFormLabel>{t('git_sync_sub_path')}</OLFormLabel>
<OLFormControl
type="text"
value={subPath}
onChange={e => setSubPath(e.target.value)}
placeholder={t('git_sync_sub_path_placeholder')}
disabled={isBusy}
/>
</OLFormGroup>
<OLFormGroup controlId="git-sync-push-files">
<FormCheck
type="switch"
id="git-sync-push-files"
label={t('git_sync_push_files')}
checked={pushFiles}
onChange={e => setPushFiles(e.target.checked)}
disabled={isBusy}
/>
</OLFormGroup>
<OLFormGroup controlId="git-sync-pdf-path">
<OLFormLabel>{t('git_sync_pdf_path')}</OLFormLabel>
<OLFormControl
type="text"
value={pdfPath}
onChange={e => setPdfPath(e.target.value)}
placeholder={t('git_sync_pdf_path_placeholder')}
disabled={isBusy}
/>
</OLFormGroup>
<OLFormGroup controlId="git-sync-push-pdf">
<FormCheck
type="switch"
id="git-sync-push-pdf"
label={t('git_sync_push_pdf')}
checked={pushPdf}
onChange={e => setPushPdf(e.target.checked)}
disabled={isBusy || !pdfPath.trim()}
/>
</OLFormGroup>
<OLFormGroup controlId="git-sync-auto-push">
<FormCheck
type="switch"
id="git-sync-auto-push"
label={t('git_sync_auto_push_on_compile')}
checked={autoPush}
onChange={handleAutoPushToggle}
disabled={isBusy || !remoteUrl.trim() || nothingToPush}
/>
</OLFormGroup>
<div className="git-sync-widget-actions">
<OLButton
type="submit"
variant="secondary"
size="sm"
disabled={isBusy || !remoteUrl.trim()}
isLoading={isBusy && lastAction === 'save'}
loadingLabel={t('saving')}
>
{t('save')}
</OLButton>
<OLButton
type="button"
variant="primary"
size="sm"
onClick={handlePush}
disabled={isBusy || !remoteUrl.trim() || nothingToPush}
isLoading={isBusy && lastAction === 'push'}
loadingLabel={t('git_sync_pushing')}
>
{t('git_sync_push_now')}
</OLButton>
<OLButton
type="button"
variant="secondary"
size="sm"
onClick={handlePull}
disabled={isBusy || !remoteUrl.trim()}
isLoading={isBusy && lastAction === 'pull'}
loadingLabel={t('git_sync_pulling')}
>
{t('git_sync_pull_now')}
</OLButton>
</div>
</form>
{status === 'success' && (
<OLNotification
type="success"
content={
lastAction === 'push'
? t('git_sync_push_success')
: lastAction === 'pull'
? t('git_sync_pull_success')
: t('git_sync_saved')
}
/>
)}
{status === 'error' && (
<OLNotification type="error" content={errorMsg} />
)}
</div>
)
}