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,
userId,
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 zipReq = await fetchStreamWithResponse(zipUrl, {
signal: AbortSignal.timeout(TIMEOUT),
@@ -42,8 +52,15 @@ const TemplatesManager = {
const projectName = ProjectDetailsHandler.fixProjectName(templateName)
const dumpPath = `${settings.path.dumpFolder}/${crypto.randomUUID()}_templates-manager`
const writeStream = fs.createWriteStream(dumpPath)
try {
const attributes = {}
const attributes = {
compiler,
imageName,
spellCheckLanguage,
}
if (brandVariationId) attributes.brandVariationId = brandVariationId
await pipeline(zipReq.stream, writeStream)
if (zipReq.response.status !== 200) {
@@ -73,13 +90,22 @@ const TemplatesManager = {
return undefined
})
await TemplatesManager._setCompiler(project._id, compiler)
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 TemplatesManager._setMainFile(project, mainFile)
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
} finally {
@@ -87,41 +113,15 @@ const TemplatesManager = {
}
},
async _setCompiler(projectId, compiler) {
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) {
async _setMainFile(project, mainFile) {
if (mainFile == null) {
return
}
await ProjectRootDocManager.setRootDocFromName(projectId, mainFile)
},
async _setSpellCheckLanguage(projectId, language) {
if (language == null) {
return
}
await ProjectOptionsHandler.setSpellCheckLanguage(projectId, language)
},
async _setBrandVariationId(projectId, brandVariationId) {
if (brandVariationId == null) {
return
}
await ProjectOptionsHandler.setBrandVariationId(projectId, brandVariationId)
const rootDocId = await ProjectRootDocManager.setRootDocFromName(
project._id,
mainFile
)
if (rootDocId) project.rootDoc_id = rootDocId
},
async fetchFromV1(templateId) {

View File

@@ -309,6 +309,8 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
TokenAccessRouter.apply(webRouter)
HistoryRouter.apply(webRouter, privateApiRouter)
await Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
if (Settings.enableSubscriptions) {
webRouter.get(
'/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: [],
importProjectFromGithubMenu: [],
editorLeftMenuSync: [],
editorLeftMenuManageTemplate: ['@/features/editor-left-menu/components/actions-manage-template'],
menubarExtraComponents: [],
editorLeftMenuManageTemplate: [
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: [],
managedGroupSubscriptionEnrollmentNotification: [],
managedGroupEnrollmentInvite: [],

View File

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

View File

@@ -1,12 +1,16 @@
import Path from 'node:path'
import { fileURLToPath } from 'node:url'
import logger from '@overleaf/logger'
import ErrorController from '../../../../app/src/Features/Errors/ErrorController.mjs'
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 { getUserName } from './TemplateGalleryHelper.mjs'
import { TemplateNameConflictError, RecompileRequiredError } from './TemplateErrors.mjs'
import Settings from '@overleaf/settings'
const __dirname = Path.dirname(fileURLToPath(import.meta.url))
async function createTemplateFromProject(req, res, next) {
const t = req.i18n.translate
try {
@@ -108,7 +112,7 @@ async function templatesCategoryPage(req, res, next) {
category = null
title = t('templates_page_title')
}
res.render('template_gallery/template-gallery', {
res.render(Path.resolve(__dirname, '../views/template_gallery/template-gallery'), {
title,
category,
})
@@ -121,7 +125,7 @@ async function templateDetailsPage(req, res, next) {
const t = req.i18n.translate
try {
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}`,
template: JSON.stringify(template),
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 ClsiManager from '../../../../app/src/Features/Compile/ClsiManager.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 { Template } from './models/Template.js'
import { Template } from './models/Template.mjs'
import { RecompileRequiredError } from './TemplateErrors.mjs'
import { cleanHtml } from './CleanHtml.mjs'

View File

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

View File

@@ -2,7 +2,7 @@ import logger from '@overleaf/logger'
import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.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'
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 { ObjectId } = Schema
const TemplateSchema = new Schema(
export const TemplateSchema = new Schema(
{
name: { type: String, required: true },
category: { type: String, required: true },
@@ -29,5 +29,4 @@ const TemplateSchema = new Schema(
{ minimize: false }
)
exports.Template = mongoose.model('Template', TemplateSchema)
exports.TemplateSchema = TemplateSchema
export const Template = mongoose.model('Template', 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
- entrypoint = 'pages/template'
- entrypoint = 'modules/template-gallery/pages/template'
block vars
- const suppressNavContentLinks = true
- const suppressNavbar = true
- const suppressFooter = true
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
- isWebsiteRedesign = false
- isWebsiteRedesign = true
block append meta
meta(name="ol-template" data-type="json" content=template)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { useTemplateGalleryContext } from '../context/template-gallery-context'
import { Sort } from '../types/api'
import { SortingOrder } from '../../../../../types/sorting-order'
import { SortingOrder } from '../../../../../../../types/sorting-order'
const toggleSort = (order: SortingOrder): SortingOrder => {
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'
export type Sort = {

View File

@@ -1,5 +1,5 @@
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> {
const queryParams = new URLSearchParams({

View File

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

View File

@@ -1,11 +1,11 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import getMeta from '../../../utils/meta'
import * as eventTracking from '@/infrastructure/event-tracking'
import getMeta from '@/utils/meta'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { useDetachCompileContext } from '../../../shared/context/detach-compile-context'
import EditorManageTemplateModalWrapper from '../../template/components/manage-template-modal/editor-manage-template-modal-wrapper'
import LeftMenuButton from './left-menu-button'
import { useDetachCompileContext } from '@/shared/context/detach-compile-context'
import EditorManageTemplateModalWrapper from './manage-template-modal/editor-manage-template-modal-wrapper'
import LeftMenuButton from '@/features/editor-left-menu/components/left-menu-button'
type TemplateManageResponse = {
template_id: string
@@ -31,7 +31,7 @@ export default function ActionsManageTemplate() {
({ template_id: templateId }: TemplateManageResponse) => {
location.assign(`/template/${templateId}`)
},
[location]
[]
)
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 { useTranslation } from 'react-i18next'
import getMeta from '../../../../utils/meta'
import getMeta from '@/utils/meta'
import SettingsMenuSelect 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 OLRow from '@/shared/components/ol/ol-row'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { formatDate, fromNowDate } from '../../../utils/dates'
import { cleanHtml } from '../../../../../modules/template-gallery/app/src/CleanHtml.mjs'
import { formatDate, fromNowDate } from '@/utils/dates'
import { cleanHtml } from '../../../../../app/src/CleanHtml.mjs'
import { useTemplateContext } from '../context/template-context'
import DeleteTemplateButton from './delete-template-button'
import EditTemplateButton from './edit-template-button'

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
import withErrorBoundary from '../../../infrastructure/error-boundary'
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import withErrorBoundary from '@/infrastructure/error-boundary'
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
import DefaultNavbar from '@/shared/components/navbar/default-navbar'
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 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 TemplateRoot from '../features/template/components/template-root'