0caa4ff144
Build and Deploy Verso / deploy (push) Successful in 12m35s
Branch: - New "Branch" text field (default: main) stored in gitSyncBranch on the project. Used for git fetch, symbolic-ref, and push target. Validated server-side: alphanumeric + / _ . - only, no .. First-push confirmation: - When pushFiles is enabled and the user hasn't confirmed for the current subPath, clicking "Push now" shows an inline warning: "The <subPath> directory will be completely replaced …" with "Yes, push and replace" / "Cancel" buttons. - Confirmation is stored in localStorage keyed by projectId+subPath, so it's shown again if the subPath is changed. - Auto-push bypasses the dialog (user already opted in explicitly). PDF-only replace: - Already correct with the fetch-first approach: only the specific file at pdfPath is written; other files in the same directory (e.g. output/old.pdf) are preserved from the fetched remote state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
329 lines
10 KiB
TypeScript
329 lines
10 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' | 'confirm'
|
|
|
|
const projectId = getMeta('ol-project_id')
|
|
const AUTO_PUSH_KEY = `gitSync.autoPushOnCompile.${projectId}`
|
|
// Tracks the last subPath the user confirmed clearing on push
|
|
const CONFIRMED_SUBPATH_KEY = `gitSync.confirmedSubPath.${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 [branch, setBranch] = useState(getMeta('ol-gitSyncBranch') ?? 'main')
|
|
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('')
|
|
|
|
// Pending push args while waiting for user to confirm subPath reset
|
|
const pendingPushRef = useRef<{ buildId?: string; serverId?: string } | null>(null)
|
|
|
|
const wasCompilingRef = useRef(false)
|
|
const isBusyRef = useRef(false)
|
|
|
|
useEffect(() => {
|
|
isBusyRef.current = status === 'busy'
|
|
}, [status])
|
|
|
|
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, branch, pushFiles, pushPdf },
|
|
})
|
|
}
|
|
|
|
function extractError(err: any): string {
|
|
return err?.data?.error ?? err?.message ?? String(err)
|
|
}
|
|
|
|
function needsConfirmation(): boolean {
|
|
if (!pushFiles) return false
|
|
const confirmed = localStorage.getItem(CONFIRMED_SUBPATH_KEY)
|
|
return confirmed !== subPath
|
|
}
|
|
|
|
function confirmPush() {
|
|
localStorage.setItem(CONFIRMED_SUBPATH_KEY, subPath)
|
|
const args = pendingPushRef.current ?? {}
|
|
pendingPushRef.current = null
|
|
setStatus('idle')
|
|
executePush(args.buildId, args.serverId)
|
|
}
|
|
|
|
function cancelConfirm() {
|
|
pendingPushRef.current = null
|
|
setStatus('idle')
|
|
}
|
|
|
|
async function executePush(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))
|
|
}
|
|
}
|
|
|
|
function doPush(buildId?: string, serverId?: string) {
|
|
if (needsConfirmation()) {
|
|
pendingPushRef.current = { buildId, serverId }
|
|
setStatus('confirm')
|
|
return
|
|
}
|
|
executePush(buildId, serverId)
|
|
}
|
|
|
|
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 isConfirm = status === 'confirm'
|
|
const nothingToPush = !pushFiles && (!pushPdf || !pdfPath.trim())
|
|
const managedArea = subPath.trim() || t('git_sync_repo_root')
|
|
|
|
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>
|
|
|
|
{isConfirm ? (
|
|
<div className="git-sync-confirm">
|
|
<OLNotification
|
|
type="warning"
|
|
content={t('git_sync_confirm_reset', { path: managedArea })}
|
|
/>
|
|
<div className="git-sync-widget-actions" style={{ marginTop: 'var(--spacing-04)' }}>
|
|
<OLButton
|
|
type="button"
|
|
variant="danger"
|
|
size="sm"
|
|
onClick={confirmPush}
|
|
>
|
|
{t('git_sync_confirm_push')}
|
|
</OLButton>
|
|
<OLButton
|
|
type="button"
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={cancelConfirm}
|
|
>
|
|
{t('cancel')}
|
|
</OLButton>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<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-branch">
|
|
<OLFormLabel>{t('git_sync_branch')}</OLFormLabel>
|
|
<OLFormControl
|
|
type="text"
|
|
value={branch}
|
|
onChange={e => setBranch(e.target.value)}
|
|
placeholder="main"
|
|
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>
|
|
|
|
{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} />
|
|
)}
|
|
</form>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|