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:
@@ -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),
|
||||
}
|
||||
@@ -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": "",
|
||||
|
||||
+15
-1
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user