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>
This commit is contained in:
claude
2026-06-02 14:26:03 +00:00
parent c9727a26e4
commit 405c1d27c9
8 changed files with 172 additions and 2 deletions
@@ -0,0 +1,38 @@
import { expressify } from '@overleaf/promise-utils'
import SessionManager from '../Authentication/SessionManager.mjs'
import ProjectEntityHandler from '../Project/ProjectEntityHandler.mjs'
import EditorController from '../Editor/EditorController.mjs'
// The project's Python dependency list lives in a single Verso requirements
// file at the project root. It is hidden from the file tree and edited through
// the dedicated "Python packages" modal instead.
const REQUIREMENTS_PATH = '/requirements.vrf'
async function getRequirements(req, res) {
const projectId = req.params.Project_id
const docs = await ProjectEntityHandler.promises.getAllDocs(projectId)
const doc = docs[REQUIREMENTS_PATH]
res.json({ content: doc ? doc.lines.join('\n') : '' })
}
async function setRequirements(req, res) {
const projectId = req.params.Project_id
const userId = SessionManager.getLoggedInUserId(req.session)
const content = typeof req.body.content === 'string' ? req.body.content : ''
// Normalise line endings; an empty body still upserts an (empty) file, which
// is harmless and keeps the editor state simple.
const docLines = content.replace(/\r\n?/g, '\n').split('\n')
await EditorController.promises.upsertDocWithPath(
projectId,
REQUIREMENTS_PATH,
docLines,
'python-requirements',
userId
)
res.json({ content })
}
export default {
getRequirements: expressify(getRequirements),
setRequirements: expressify(setRequirements),
}
+14
View File
@@ -5,6 +5,7 @@ import ProjectController from './Features/Project/ProjectController.mjs'
import ProjectApiController from './Features/Project/ProjectApiController.mjs'
import PublishedPresentationController from './Features/PublishedPresentation/PublishedPresentationController.mjs'
import PresentationExportController from './Features/Compile/PresentationExportController.mjs'
import PythonRequirementsController from './Features/Compile/PythonRequirementsController.mjs'
import ProjectListController from './Features/Project/ProjectListController.mjs'
import SpellingController from './Features/Spelling/SpellingController.mjs'
import EditorRouter from './Features/Editor/EditorRouter.mjs'
@@ -700,6 +701,19 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
PresentationExportController.exportPresentation
)
// Read/write the project's Python requirements (requirements.vrf), edited via
// the "Python packages" modal rather than as a file in the tree.
webRouter.get(
'/project/:Project_id/python-requirements',
AuthorizationMiddleware.ensureUserCanReadProject,
PythonRequirementsController.getRequirements
)
webRouter.post(
'/project/:Project_id/python-requirements',
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
PythonRequirementsController.setRequirements
)
// Standalone viewer for a published presentation (no editor chrome).
// Visibility is enforced inside the handler: 'public' is anonymous,
// 'private' requires any logged-in Verso user.
@@ -1436,6 +1436,8 @@
"please_ask_the_project_owner_to_upgrade_to_track_changes": "",
"please_change_primary_to_remove": "",
"please_compile_pdf_before_download": "",
"python_packages": "",
"python_packages_help": "",
"please_confirm_primary_email_or_edit": "",
"please_confirm_secondary_email_or_edit": "",
"please_confirm_your_email_before_making_it_default": "",
@@ -2,11 +2,12 @@ import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useFileTreeActionable } from '@/features/file-tree/contexts/file-tree-actionable'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import React from 'react'
import React, { useState } from 'react'
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import FileTreeActionButton from './file-tree-action-button'
import { useRailContext } from '../../ide-react/context/rail-context'
import PythonRequirementsModal from './python-requirements-modal'
export default function FileTreeActionButtons({
fileTreeExpanded,
@@ -17,6 +18,7 @@ export default function FileTreeActionButtons({
const { fileTreeReadOnly } = useFileTreeData()
const { write } = usePermissionsContext()
const { handlePaneCollapse } = useRailContext()
const [showPythonModal, setShowPythonModal] = useState(false)
const {
canCreate,
@@ -110,6 +112,14 @@ export default function FileTreeActionButtons({
iconType="delete"
/>
)}
{write && (
<FileTreeActionButton
id="python-packages"
description={t('python_packages')}
onClick={() => setShowPythonModal(true)}
iconType="deployed_code"
/>
)}
</>
)}
<FileTreeActionButton
@@ -118,6 +128,10 @@ export default function FileTreeActionButtons({
onClick={handlePaneCollapse}
iconType="close"
/>
<PythonRequirementsModal
show={showPythonModal}
onHide={() => setShowPythonModal(false)}
/>
</div>
)
}
@@ -28,7 +28,11 @@ function FileTreeFolderList({
dataTestId?: string
}) {
files = files.map(file => ({ ...file, isFile: true }))
const docsAndFiles: (Doc | ExtendedFileRef)[] = [...docs, ...files]
// The Verso requirements file (requirements.vrf) is managed through the
// dedicated "Python packages" editor, so it is hidden from the file tree.
const docsAndFiles: (Doc | ExtendedFileRef)[] = [...docs, ...files].filter(
entity => !/\.vrf$/i.test(entity.name)
)
return (
<ul
@@ -0,0 +1,94 @@
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>
)
}
+2
View File
@@ -1904,6 +1904,8 @@
"please_ask_the_project_owner_to_upgrade_to_track_changes": "Please ask the project owner to upgrade to use track changes",
"please_change_primary_to_remove": "Please change your primary email in order to remove",
"please_compile_pdf_before_download": "Please compile your project before downloading the PDF",
"python_packages": "Python packages",
"python_packages_help": "One package per line, using pip syntax (e.g. openpyxl==3.1.5). These are installed for this project's Python code cells; the bundled scientific packages (numpy, pandas, matplotlib, …) are always available.",
"please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ",
"please_confirm_primary_email_or_edit": "Please confirm your primary email address __emailAddress__. To edit it, go to <0>Account settings</0>.",
"please_confirm_secondary_email_or_edit": "Please confirm your secondary email address __emailAddress__. To edit it, go to <0>Account settings</0>.",
+2
View File
@@ -824,6 +824,8 @@
"please_ask_the_project_owner_to_upgrade_to_track_changes": "Veuillez demander au propriétaire du projet de mettre à niveau son compte pour pouvoir suivre les modifications",
"please_change_primary_to_remove": "Veuillez changer votre adresse courriel principale pour pouvoir la retirer",
"please_compile_pdf_before_download": "Veuillez compiler votre projet avant de pouvoir télécharger le PDF",
"python_packages": "Paquets Python",
"python_packages_help": "Un paquet par ligne, en syntaxe pip (par ex. openpyxl==3.1.5). Ils sont installés pour les cellules de code Python de ce projet ; les paquets scientifiques fournis (numpy, pandas, matplotlib, …) sont toujours disponibles.",
"please_confirm_email": "Veuillez confirmer votre adresse courriel __emailAddress__ en cliquant sur le lien contenu dans le courriel de confirmation ",
"please_confirm_your_email_before_making_it_default": "Veuillez confirmer cette adresse courriel avant de pouvoir la rendre principale.",
"please_enter_email": "Veuillez indiquer votre adresse électronique",