Template Gallery: migration to 6.1.0

enable menu for editor redisign
move frontend code to modules
This commit is contained in:
yu-i-i
2026-02-02 17:05:02 +01:00
parent 6bf72f6373
commit 91e97dbdf2
54 changed files with 161 additions and 114 deletions

View File

@@ -32,8 +32,18 @@ const TemplatesManager = {
templateVersionId, templateVersionId,
userId, userId,
imageName, imageName,
language spellCheckLanguage
) { ) {
compiler = ProjectOptionsHandler.normalizeCompiler(compiler || 'pdflatex')
try {
imageName = ProjectOptionsHandler.normalizeImageName(imageName)
} catch {
logger.warn( { templateId, imageName }, 'cannot use the image required by the template, using the default image')
imageName = null
}
const zipUrl = `${settings.apis.filestore.url}/template/${templateId}/v/${templateVersionId}/zip` const zipUrl = `${settings.apis.filestore.url}/template/${templateId}/v/${templateVersionId}/zip`
const zipReq = await fetchStreamWithResponse(zipUrl, { const zipReq = await fetchStreamWithResponse(zipUrl, {
signal: AbortSignal.timeout(TIMEOUT), signal: AbortSignal.timeout(TIMEOUT),
@@ -42,8 +52,15 @@ const TemplatesManager = {
const projectName = ProjectDetailsHandler.fixProjectName(templateName) const projectName = ProjectDetailsHandler.fixProjectName(templateName)
const dumpPath = `${settings.path.dumpFolder}/${crypto.randomUUID()}_templates-manager` const dumpPath = `${settings.path.dumpFolder}/${crypto.randomUUID()}_templates-manager`
const writeStream = fs.createWriteStream(dumpPath) const writeStream = fs.createWriteStream(dumpPath)
try { try {
const attributes = {} const attributes = {
compiler,
imageName,
spellCheckLanguage,
}
if (brandVariationId) attributes.brandVariationId = brandVariationId
await pipeline(zipReq.stream, writeStream) await pipeline(zipReq.stream, writeStream)
if (zipReq.response.status !== 200) { if (zipReq.response.status !== 200) {
@@ -73,13 +90,22 @@ const TemplatesManager = {
return undefined return undefined
}) })
await TemplatesManager._setCompiler(project._id, compiler) await TemplatesManager._setMainFile(project, mainFile)
await TemplatesManager._setImage(project._id, imageName)
await TemplatesManager._setMainFile(project._id, mainFile)
await TemplatesManager._setSpellCheckLanguage(project._id, language)
await TemplatesManager._setBrandVariationId(project._id, brandVariationId)
await prepareClsiCacheInBackground const found = await prepareClsiCacheInBackground
if (found === false && project.rootDoc_id) {
ClsiCacheManager.createTemplateClsiCache({
templateVersionId,
project,
fileEntries,
docEntries,
}).catch(err => {
logger.error(
{ err, templateVersionId },
'failed to create template clsi-cache'
)
})
}
return project return project
} finally { } finally {
@@ -87,41 +113,15 @@ const TemplatesManager = {
} }
}, },
async _setCompiler(projectId, compiler) { async _setMainFile(project, mainFile) {
if (compiler == null) {
return
}
await ProjectOptionsHandler.setCompiler(projectId, compiler)
},
async _setImage(projectId, imageName) {
try {
await ProjectOptionsHandler.setImageName(projectId, imageName)
} catch {
logger.warn({ imageName: imageName }, 'not available')
await ProjectOptionsHandler.setImageName(projectId, settings.currentImageName)
}
},
async _setMainFile(projectId, mainFile) {
if (mainFile == null) { if (mainFile == null) {
return return
} }
await ProjectRootDocManager.setRootDocFromName(projectId, mainFile) const rootDocId = await ProjectRootDocManager.setRootDocFromName(
}, project._id,
mainFile
async _setSpellCheckLanguage(projectId, language) { )
if (language == null) { if (rootDocId) project.rootDoc_id = rootDocId
return
}
await ProjectOptionsHandler.setSpellCheckLanguage(projectId, language)
},
async _setBrandVariationId(projectId, brandVariationId) {
if (brandVariationId == null) {
return
}
await ProjectOptionsHandler.setBrandVariationId(projectId, brandVariationId)
}, },
async fetchFromV1(templateId) { async fetchFromV1(templateId) {

View File

@@ -309,6 +309,8 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
TokenAccessRouter.apply(webRouter) TokenAccessRouter.apply(webRouter)
HistoryRouter.apply(webRouter, privateApiRouter) HistoryRouter.apply(webRouter, privateApiRouter)
await Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
if (Settings.enableSubscriptions) { if (Settings.enableSubscriptions) {
webRouter.get( webRouter.get(
'/user/bonus', '/user/bonus',

View File

@@ -1,18 +0,0 @@
extends ../layout-react
block entrypointVar
- entrypoint = 'pages/template-gallery'
block vars
block vars
- const suppressNavContentLinks = true
- const suppressNavbar = true
- const suppressFooter = true
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
- isWebsiteRedesign = false
block append meta
meta(name="ol-templateCategory" data-type="string" content=category)
block content
#template-gallery-root

View File

@@ -1041,8 +1041,18 @@ module.exports = {
importProjectFromGithubModalWrapper: [], importProjectFromGithubModalWrapper: [],
importProjectFromGithubMenu: [], importProjectFromGithubMenu: [],
editorLeftMenuSync: [], editorLeftMenuSync: [],
editorLeftMenuManageTemplate: ['@/features/editor-left-menu/components/actions-manage-template'], editorLeftMenuManageTemplate: [
menubarExtraComponents: [], Path.resolve(
__dirname,
'../modules/template-gallery/frontend/js/features/template/components/actions-manage-template'
),
],
menubarExtraComponents: [
Path.resolve(
__dirname,
'../modules/template-gallery/frontend/js/features/template/components/menubar-manage-template'
),
],
oauth2Server: [], oauth2Server: [],
managedGroupSubscriptionEnrollmentNotification: [], managedGroupSubscriptionEnrollmentNotification: [],
managedGroupEnrollmentInvite: [], managedGroupEnrollmentInvite: [],

View File

@@ -32,7 +32,6 @@
padding-bottom: 0px; padding-bottom: 0px;
} }
.gallery-header-sort-btn { .gallery-header-sort-btn {
font-size: var(--font-size-02); font-size: var(--font-size-02);
border: 0; border: 0;

View File

@@ -1,12 +1,16 @@
import Path from 'node:path'
import { fileURLToPath } from 'node:url'
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import ErrorController from '../../../../app/src/Features/Errors/ErrorController.mjs' import ErrorController from '../../../../app/src/Features/Errors/ErrorController.mjs'
import Errors from '../../../../app/src/Features/Errors/Errors.js' import Errors from '../../../../app/src/Features/Errors/Errors.js'
import SessionManager from '../../../../app/src/Features/Authentication/SessionManager.js' import SessionManager from '../../../../app/src/Features/Authentication/SessionManager.mjs'
import TemplateGalleryManager from'./TemplateGalleryManager.mjs' import TemplateGalleryManager from'./TemplateGalleryManager.mjs'
import { getUserName } from './TemplateGalleryHelper.mjs' import { getUserName } from './TemplateGalleryHelper.mjs'
import { TemplateNameConflictError, RecompileRequiredError } from './TemplateErrors.mjs' import { TemplateNameConflictError, RecompileRequiredError } from './TemplateErrors.mjs'
import Settings from '@overleaf/settings' import Settings from '@overleaf/settings'
const __dirname = Path.dirname(fileURLToPath(import.meta.url))
async function createTemplateFromProject(req, res, next) { async function createTemplateFromProject(req, res, next) {
const t = req.i18n.translate const t = req.i18n.translate
try { try {
@@ -108,7 +112,7 @@ async function templatesCategoryPage(req, res, next) {
category = null category = null
title = t('templates_page_title') title = t('templates_page_title')
} }
res.render('template_gallery/template-gallery', { res.render(Path.resolve(__dirname, '../views/template_gallery/template-gallery'), {
title, title,
category, category,
}) })
@@ -121,7 +125,7 @@ async function templateDetailsPage(req, res, next) {
const t = req.i18n.translate const t = req.i18n.translate
try { try {
const template = await TemplateGalleryManager.getTemplate('_id', req.params.template_id) const template = await TemplateGalleryManager.getTemplate('_id', req.params.template_id)
res.render('template_gallery/template', { res.render(Path.resolve(__dirname, '../views/template_gallery/template'), {
title: `${t('template')}: ${template.name}`, title: `${t('template')}: ${template.name}`,
template: JSON.stringify(template), template: JSON.stringify(template),
languages: Settings.languages, languages: Settings.languages,

View File

@@ -9,9 +9,9 @@ import ProjectZipStreamManager from '../../../../app/src/Features/Downloads/Proj
import DocumentUpdaterHandler from '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.mjs' import DocumentUpdaterHandler from '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.mjs'
import ClsiManager from '../../../../app/src/Features/Compile/ClsiManager.mjs' import ClsiManager from '../../../../app/src/Features/Compile/ClsiManager.mjs'
import CompileManager from '../../../../app/src/Features/Compile/CompileManager.mjs' import CompileManager from '../../../../app/src/Features/Compile/CompileManager.mjs'
import UserGetter from '../../../../app/src/Features/User/UserGetter.js' import UserGetter from '../../../../app/src/Features/User/UserGetter.mjs'
import { fetchStreamWithResponse } from '@overleaf/fetch-utils' import { fetchStreamWithResponse } from '@overleaf/fetch-utils'
import { Template } from './models/Template.js' import { Template } from './models/Template.mjs'
import { RecompileRequiredError } from './TemplateErrors.mjs' import { RecompileRequiredError } from './TemplateErrors.mjs'
import { cleanHtml } from './CleanHtml.mjs' import { cleanHtml } from './CleanHtml.mjs'

View File

@@ -3,7 +3,7 @@ import logger from '@overleaf/logger'
import { Readable } from 'stream' import { Readable } from 'stream'
import settings from '@overleaf/settings' import settings from '@overleaf/settings'
import { OError } from '../../../../app/src/Features/Errors/Errors.js' import { OError } from '../../../../app/src/Features/Errors/Errors.js'
import { Template } from './models/Template.js' import { Template } from './models/Template.mjs'
import { import {
validateTemplateInput, validateTemplateInput,
renderTemplateHtmlFields, renderTemplateHtmlFields,

View File

@@ -2,7 +2,7 @@ import logger from '@overleaf/logger'
import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.mjs' import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.mjs'
import RateLimiterMiddleware from '../../../../app/src/Features/Security/RateLimiterMiddleware.mjs' import RateLimiterMiddleware from '../../../../app/src/Features/Security/RateLimiterMiddleware.mjs'
import { RateLimiter } from '../../../../app/src/infrastructure/RateLimiter.js' import { RateLimiter } from '../../../../app/src/infrastructure/RateLimiter.mjs'
import TemplateGalleryController from './TemplateGalleryController.mjs' import TemplateGalleryController from './TemplateGalleryController.mjs'
const rateLimiterNewTemplate = new RateLimiter('create-template-from-project', { const rateLimiterNewTemplate = new RateLimiter('create-template-from-project', {

View File

@@ -1,9 +1,9 @@
const mongoose = require('../../../../../app/src/infrastructure/Mongoose') import mongoose from '../../../../../app/src/infrastructure/Mongoose.mjs'
const { Schema } = mongoose const { Schema } = mongoose
const { ObjectId } = Schema const { ObjectId } = Schema
const TemplateSchema = new Schema( export const TemplateSchema = new Schema(
{ {
name: { type: String, required: true }, name: { type: String, required: true },
category: { type: String, required: true }, category: { type: String, required: true },
@@ -29,5 +29,4 @@ const TemplateSchema = new Schema(
{ minimize: false } { minimize: false }
) )
exports.Template = mongoose.model('Template', TemplateSchema) export const Template = mongoose.model('Template', TemplateSchema)
exports.TemplateSchema = TemplateSchema

View File

@@ -0,0 +1,15 @@
extends ../../../../../app/views/layout-react
block entrypointVar
- entrypoint = 'modules/template-gallery/pages/template-gallery'
block vars
- const suppressFooter = true
- const suppressPugCookieBanner = true
- isWebsiteRedesign = true
block append meta
meta(name="ol-templateCategory" data-type="string" content=category)
block content
#template-gallery-root

View File

@@ -1,14 +1,12 @@
extends ../layout-react extends ../../../../../app/views/layout-react
block entrypointVar block entrypointVar
- entrypoint = 'pages/template' - entrypoint = 'modules/template-gallery/pages/template'
block vars block vars
- const suppressNavContentLinks = true
- const suppressNavbar = true - const suppressNavbar = true
- const suppressFooter = true - const suppressFooter = true
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - isWebsiteRedesign = true
- isWebsiteRedesign = false
block append meta block append meta
meta(name="ol-template" data-type="json" content=template) meta(name="ol-template" data-type="json" content=template)

View File

@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { MergeAndOverride } from '../../../../../types/utils' import { MergeAndOverride } from '../../../../../../../types/utils'
import OLForm from '@/shared/components/ol/ol-form' import OLForm from '@/shared/components/ol/ol-form'
import OLFormControl from '@/shared/components/ol/ol-form-control' import OLFormControl from '@/shared/components/ol/ol-form-control'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'

View File

@@ -1,5 +1,5 @@
import { memo } from 'react' import { memo } from 'react'
import { cleanHtml } from '../../../../../modules/template-gallery/app/src/CleanHtml.mjs' import { cleanHtml } from '../../../../../app/src/CleanHtml.mjs'
function TemplateGalleryEntry({ template }) { function TemplateGalleryEntry({ template }) {
return ( return (

View File

@@ -1,7 +1,7 @@
import { TemplateGalleryProvider } from '../context/template-gallery-context' import { TemplateGalleryProvider } from '../context/template-gallery-context'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n' import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import withErrorBoundary from '../../../infrastructure/error-boundary' import withErrorBoundary from '@/infrastructure/error-boundary'
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback' import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
import getMeta from '@/utils/meta' import getMeta from '@/utils/meta'
import DefaultNavbar from '@/shared/components/navbar/default-navbar' import DefaultNavbar from '@/shared/components/navbar/default-navbar'

View File

@@ -9,8 +9,8 @@ import {
} from 'react' } from 'react'
import { Template } from '../../../../../types/template' import { Template } from '../../../../../types/template'
import { GetTemplatesResponseBody, Sort } from '../types/api' import { GetTemplatesResponseBody, Sort } from '../types/api'
import getMeta from '../../../utils/meta' import getMeta from '@/utils/meta'
import useAsync from '../../../shared/hooks/use-async' import useAsync from '@/shared/hooks/use-async'
import { getTemplates } from '../util/api' import { getTemplates } from '../util/api'
import sortTemplates from '../util/sort-templates' import sortTemplates from '../util/sort-templates'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'

View File

@@ -1,6 +1,6 @@
import { useTemplateGalleryContext } from '../context/template-gallery-context' import { useTemplateGalleryContext } from '../context/template-gallery-context'
import { Sort } from '../types/api' import { Sort } from '../types/api'
import { SortingOrder } from '../../../../../types/sorting-order' import { SortingOrder } from '../../../../../../../types/sorting-order'
const toggleSort = (order: SortingOrder): SortingOrder => { const toggleSort = (order: SortingOrder): SortingOrder => {
return order === 'asc' ? 'desc' : 'asc' return order === 'asc' ? 'desc' : 'asc'

View File

@@ -1,4 +1,4 @@
import { SortingOrder } from '../../../../../types/sorting-order' import { SortingOrder } from '../../../../../../../types/sorting-order'
import { Template } from '../../../../../types/template' import { Template } from '../../../../../types/template'
export type Sort = { export type Sort = {

View File

@@ -1,5 +1,5 @@
import { GetTemplatesResponseBody, Sort } from '../types/api' import { GetTemplatesResponseBody, Sort } from '../types/api'
import { getJSON } from '../../../infrastructure/fetch-json' import { getJSON } from '@/infrastructure/fetch-json'
export function getTemplates(sortBy: Sort, category: string): Promise<GetTemplatesResponseBody> { export function getTemplates(sortBy: Sort, category: string): Promise<GetTemplatesResponseBody> {
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({

View File

@@ -1,7 +1,7 @@
import { Sort } from '../types/api' import { Sort } from '../types/api'
import { Template } from '../../../../../types/template' import { Template } from '../../../../../types/template'
import { SortingOrder } from '../../../../../types/sorting-order' import { SortingOrder } from '../../../../../../../types/sorting-order'
import { Compare } from '../../../../../types/helpers/array/sort' import { Compare } from '../../../../../../../types/helpers/array/sort'
const order = (order: SortingOrder, templates: Template[]) => { const order = (order: SortingOrder, templates: Template[]) => {
return order === 'asc' ? [...templates] : templates.reverse() return order === 'asc' ? [...templates] : templates.reverse()

View File

@@ -1,11 +1,11 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '@/infrastructure/event-tracking'
import getMeta from '../../../utils/meta' import getMeta from '@/utils/meta'
import OLTooltip from '@/shared/components/ol/ol-tooltip' import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { useDetachCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext } from '@/shared/context/detach-compile-context'
import EditorManageTemplateModalWrapper from '../../template/components/manage-template-modal/editor-manage-template-modal-wrapper' import EditorManageTemplateModalWrapper from './manage-template-modal/editor-manage-template-modal-wrapper'
import LeftMenuButton from './left-menu-button' import LeftMenuButton from '@/features/editor-left-menu/components/left-menu-button'
type TemplateManageResponse = { type TemplateManageResponse = {
template_id: string template_id: string
@@ -31,7 +31,7 @@ export default function ActionsManageTemplate() {
({ template_id: templateId }: TemplateManageResponse) => { ({ template_id: templateId }: TemplateManageResponse) => {
location.assign(`/template/${templateId}`) location.assign(`/template/${templateId}`)
}, },
[location] []
) )
return ( return (

View File

@@ -0,0 +1,52 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
import EditorManageTemplateModalWrapper from './manage-template-modal/editor-manage-template-modal-wrapper'
type TemplateManageResponse = {
template_id: string
}
const MenubarManageTemplate = () => {
const { t } = useTranslation()
const { pdfUrl } = useCompileContext()
const [showManageTemplateModal, setShowManageTemplateModal] = useState(false)
const publishAsTemplateEnabled =
getMeta('ol-showTemplatesServerPro') && pdfUrl
useCommandProvider(
() => [
{
type: 'command',
id: 'manage-template',
label: t('publish_as_template'),
disabled: !publishAsTemplateEnabled,
handler: () => {
setShowManageTemplateModal(true)
},
},
],
[t, publishAsTemplateEnabled]
)
const openTemplate = useCallback(
({ template_id: templateId }: TemplateManageResponse) => {
location.assign(`/template/${templateId}`)
},
[]
)
return (
<EditorManageTemplateModalWrapper
show={showManageTemplateModal}
handleHide={() => setShowManageTemplateModal(false)}
openTemplate={openTemplate}
/>
)
}
export default MenubarManageTemplate

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import getMeta from '../../../../utils/meta' import getMeta from '@/utils/meta'
import SettingsMenuSelect from './settings-menu-select' import SettingsMenuSelect from './settings-menu-select'
import type { Optgroup } from './settings-menu-select' import type { Optgroup } from './settings-menu-select'

View File

@@ -3,8 +3,8 @@ import getMeta from '@/utils/meta'
import OLCol from '@/shared/components/ol/ol-col' import OLCol from '@/shared/components/ol/ol-col'
import OLRow from '@/shared/components/ol/ol-row' import OLRow from '@/shared/components/ol/ol-row'
import OLTooltip from '@/shared/components/ol/ol-tooltip' import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { formatDate, fromNowDate } from '../../../utils/dates' import { formatDate, fromNowDate } from '@/utils/dates'
import { cleanHtml } from '../../../../../modules/template-gallery/app/src/CleanHtml.mjs' import { cleanHtml } from '../../../../../app/src/CleanHtml.mjs'
import { useTemplateContext } from '../context/template-context' import { useTemplateContext } from '../context/template-context'
import DeleteTemplateButton from './delete-template-button' import DeleteTemplateButton from './delete-template-button'
import EditTemplateButton from './edit-template-button' import EditTemplateButton from './edit-template-button'

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n' import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import withErrorBoundary from '../../../infrastructure/error-boundary' import withErrorBoundary from '@/infrastructure/error-boundary'
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback' import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
import DefaultNavbar from '@/shared/components/navbar/default-navbar' import DefaultNavbar from '@/shared/components/navbar/default-navbar'
import Footer from '@/shared/components/footer/footer' import Footer from '@/shared/components/footer/footer'

View File

@@ -1,10 +1,3 @@
import './../utils/meta'
import '../utils/webpack-public-path'
import './../infrastructure/error-reporter'
import '@/i18n'
import '../features/event-tracking'
import '../features/cookie-banner'
import '../features/link-helpers/slow-link'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import TemplateGalleryRoot from '../features/template-gallery/components/template-gallery-root' import TemplateGalleryRoot from '../features/template-gallery/components/template-gallery-root'

View File

@@ -1,10 +1,3 @@
import './../utils/meta'
import '../utils/webpack-public-path'
import './../infrastructure/error-reporter'
import '@/i18n'
import '../features/event-tracking'
import '../features/cookie-banner'
import '../features/link-helpers/slow-link'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import TemplateRoot from '../features/template/components/template-root' import TemplateRoot from '../features/template/components/template-root'