From 405c1d27c9feb6a75a0447498f8bfcbbbb32cd36 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 14:26:03 +0000 Subject: [PATCH] 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 --- .../Compile/PythonRequirementsController.mjs | 38 ++++++++ services/web/app/src/router.mjs | 14 +++ .../web/frontend/extracted-translations.json | 2 + .../components/file-tree-action-buttons.tsx | 16 +++- .../components/file-tree-folder-list.tsx | 6 +- .../components/python-requirements-modal.tsx | 94 +++++++++++++++++++ services/web/locales/en.json | 2 + services/web/locales/fr.json | 2 + 8 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 services/web/app/src/Features/Compile/PythonRequirementsController.mjs create mode 100644 services/web/frontend/js/features/file-tree/components/python-requirements-modal.tsx diff --git a/services/web/app/src/Features/Compile/PythonRequirementsController.mjs b/services/web/app/src/Features/Compile/PythonRequirementsController.mjs new file mode 100644 index 0000000000..b4dcb7d2af --- /dev/null +++ b/services/web/app/src/Features/Compile/PythonRequirementsController.mjs @@ -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), +} diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs index 3c7a03fece..c67ab996cb 100644 --- a/services/web/app/src/router.mjs +++ b/services/web/app/src/router.mjs @@ -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. diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index b7fbe1c833..12af4c8280 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-action-buttons.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-action-buttons.tsx index 8ade6fcfa3..4d633caece 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-action-buttons.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-action-buttons.tsx @@ -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 && ( + setShowPythonModal(true)} + iconType="deployed_code" + /> + )} )} + setShowPythonModal(false)} + /> ) } diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.tsx index 1706554960..219719ffa9 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.tsx @@ -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 (