Files
Verso/services/web/frontend/js/features/file-tree/components/python-requirements-modal.tsx
T
claude 405c1d27c9 Bundle Python requirements into a dedicated editor; hide requirements.vrf
Add a 'Python packages' button to the file-tree toolbar that opens a modal to
edit the project's requirements.vrf (one package per line, pip syntax), backed
by GET/POST /project/:id/python-requirements (read via ProjectEntityHandler,
write via EditorController.upsertDocWithPath, write-gated). The .vrf file is now
hidden from the file tree, so it is managed only through this editor rather than
appearing as a loose file. Adds python_packages / python_packages_help i18n.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 14:26:03 +00:00

95 lines
3.0 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
OLModal,
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/shared/components/ol/ol-modal'
import OLButton from '@/shared/components/ol/ol-button'
import OLNotification from '@/shared/components/ol/ol-notification'
import LoadingSpinner from '@/shared/components/loading-spinner'
import { useProjectContext } from '@/shared/context/project-context'
import { getJSON, postJSON } from '@/infrastructure/fetch-json'
// Editor for the project's Python dependencies (requirements.vrf), reached from
// the file-tree toolbar. The file itself is hidden from the tree; this modal is
// the only entry point. One package per line, pip syntax (e.g. `openpyxl==3.1.5`).
export default function PythonRequirementsModal({
show,
onHide,
}: {
show: boolean
onHide: () => void
}) {
const { t } = useTranslation()
const { projectId } = useProjectContext()
const [content, setContent] = useState('')
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!show) return
setError(null)
setLoading(true)
getJSON<{ content: string }>(`/project/${projectId}/python-requirements`)
.then(data => setContent(data.content || ''))
.catch(() => setError(t('generic_something_went_wrong')))
.finally(() => setLoading(false))
}, [show, projectId, t])
const handleSave = useCallback(() => {
setSaving(true)
setError(null)
postJSON(`/project/${projectId}/python-requirements`, {
body: { content },
})
.then(() => onHide())
.catch(() => setError(t('generic_something_went_wrong')))
.finally(() => setSaving(false))
}, [projectId, content, onHide, t])
return (
<OLModal show={show} onHide={onHide}>
<OLModalHeader closeButton>
<OLModalTitle>{t('python_packages')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p className="text-muted">{t('python_packages_help')}</p>
{error && (
<OLNotification type="error" content={error} className="mb-3" />
)}
{loading ? (
<LoadingSpinner />
) : (
<textarea
className="form-control"
rows={10}
spellCheck={false}
style={{ fontFamily: 'monospace' }}
value={content}
onChange={e => setContent(e.target.value)}
placeholder={'openpyxl==3.1.5\nrequests'}
aria-label={t('python_packages')}
/>
)}
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={onHide} disabled={saving}>
{t('cancel')}
</OLButton>
<OLButton
variant="primary"
onClick={handleSave}
disabled={loading || saving}
isLoading={saving}
>
{t('save')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}