Make Template Gallery optional; rename environment variables
Refactor Template Gallery; resolves#38 and #39:
- replace free-text license input with a select box
- improve visual presentation of modals and enhance keyboard interaction
Template Gallery: replace markdown-it with marked
Fix template publishing to align with upstream project context changes
Template gallery: ui components were moved to shared folder
Fix style of Templates button in navbar
Template gallery: Fix `OLModal` usage (see upstream commit 19b3834)
Template gallery: Fix import after upstream converted `CompileManager`, `ClsiManager` and `RateLimiterMiddleware` to ESM
Template gallery: Fix import after upstream converted `AuthenticationController`, `ProjectGetter`, ProjectLocator` and `DocumentUpdaterHandler` to ESM
Template Gallery: migration to 6.1.0
- enable menu for editor redisign
- move frontend code to modules
Template gallery: add template management authorization middleware
- middleware to enforce template access rules
- introduce template manager role
- fix minor issues
Template Gallery: fix for 6.2.0
Template Gallery: fix for 6.3.0 and show language in the details
* [editor-core] hold the one threshold that decides which store owns the file tree
HISTORY_FILE_TREE_STAGE is five literals across the services that consume this
library -- web backend, web editor, real-time, third-party-datastore,
github-sync -- linked by comments saying which other copies they are aligned
with. What the docstring here says is what goes wrong: this is the single thing
that decides which storage is authoritative, a project crosses it once with no
way back, and a copy that drifts sends a whole service's writes to the store
nothing reads any more. Silently, and for good.
overleaf-editor-core is where it goes: every one of the five already depends on
it, the editor included, and the library that defines what a file tree is can say
where one lives. The consumers adopt it separately; this is the definition they
adopt.
historyIsSourceOfTruth goes with it, and refuses a stage that is neither absent
nor a number rather than reading it as low -- web's copy coerced, which turned a
corrupt stage into a permanent mongo write. Absent stays "below the gate": a
project that never migrated has no stage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [editor-core] hold which doc a project opens on
History records it as `main: true` on the file, and nothing derives it: every
writer that creates a project has to record it, or the project compiles nothing
until the user picks a file. So the rule for choosing one, and the operations
that record it, go where a file tree is defined rather than with any one writer.
The rule is the one web has always applied, over a snapshot instead of a mongo
tree: of the docs that declare a document class, the one nearest the project
root, alphabetically among equals, so that the same files always give the same
answer. A project that already records a root doc keeps it -- that is a user's
choice and not something to re-derive.
Whether a doc could be the one is asked of content rather than of a store to read
it from: a writer creating a project has each doc's content in its hands as it
stores it, so reading them back would be a second read of every one. The choice
is then made from the pathnames alone.
The extension list joins the others in text_file_defaults for the reason they are
there: a writer holding a shorter copy leaves a project with nothing to compile.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [editor-core] say why a store built for a consumer does not need getBlob
ReadWriteBlobStore is BlobStore without the lookup by hash, and the type said so
without saying why a consumer can do without it. getBlob is reached only for a
file stored as a bare hash, and one of those does not leave history-v1:
chunk_store's lazyLoadHistoryFiles attaches blob metadata to every snapshot and
change it serves, so a file arrives already knowing its own lengths.
That is what the contract rests on, and a store that reimplemented the
classification rule to answer the lookup anyway would be a second opinion on
whether a file is editable. The reasoning was what was missing, rather than the
type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [editor-core] say what makes a doc a root doc candidate
The sentence naming the two halves of the check had lost the first one, and read
"named as a root doc may be named". It is the extension that is tested: one of
DEFAULT_ROOT_DOC_EXTENSIONS, and a document class declared in the content.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [editor-core] hold the doc flags a file's metadata can carry in one place
`main` is not the only flag a doc carries -- `mainBibliography` marks the file
references are added to -- and both live in the same metadata, which
SetFileMetadataOperation replaces wholesale rather than merges. So every writer
that sets one has to write the others out again, and every reader that asks
whether a file is a doc has to know the whole list.
root_doc was spelling all three of those for itself against `main` alone: it
tested the key by hand, refused a file whose metadata held anything else, and
cleared the metadata to `{}` when moving the root doc elsewhere. Against a file
that is also the main bibliography, the first two refuse a legitimate root doc
and the third silently drops the bibliography.
The helpers answer it once: which keys are doc flags, whether metadata holds only
those, and what metadata with one flag changed looks like. root_doc uses them
throughout, and the schema takes both flags on one file.
Co-Authored-By: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [editor-core] ask the snapshot which doc carries a flag
Finding the file a doc flag is set on is the same search whichever flag is being
asked about, and it belongs to the snapshot rather than to the caller: it walks
the snapshot's own pathnames, and it is the snapshot that knows a file's
metadata without its content being loaded.
root_doc's mainPathname is that search for `main`, and now delegates. It gains
the check it was missing: a file whose metadata holds anything besides doc flags
came from outside the editor, so a stale `main` on it does not make it the root
doc -- which is already what setMainPathnameOperations refuses to write.
Co-Authored-By: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [editor-core] drop mainPathname for the snapshot method behind it
It became a second name for `snapshot.getPathnameWithDocFlag('main')` and nothing
in the library was left calling it but chooseRootDoc, which asks the snapshot
directly now. Its two tests go with it: Snapshot's own cover the same two answers,
and more of them.
Callers outside the library ask the snapshot too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: b3f503aa3e94ea8b6524d83f910fe33dc381c0a5
* [web] Add the Back to School group sale banner to the plans page
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 596b23006fce7504fcdfd6341cf93429c08acc8b
* [monorepo] lint .mts/.cts files
ESLint's `files` globs listed `{js,jsx,mjs,cjs,ts,tsx}` but never the
module-scoped TypeScript extensions, so every `.mts` file in the repo was
silently skipped with "File ignored because no matching configuration was
supplied" -- 21 files today, all in services/web, and more as backend code
migrates to TypeScript (`.mts` is the extension runtime-loaded backend TS
has to use, since services/web has no `"type": "module"`).
Each extension list now carries the c/m variants of the languages it
already allowed, canonicalised to one of:
{js,cjs,mjs} JS only
{js,cjs,mjs,jsx} JS + JSX
{js,cjs,mjs,ts,cts,mts} JS + TS, no JSX
{ts,cts,mts,tsx} TS only
{js,cjs,mjs,jsx,ts,cts,mts,tsx} everything
No list gains a language it did not already have, and none gains JSX
where it was previously excluded, so backend globs keep excluding
jsx/tsx and TS-only rule blocks keep excluding JS.
Both the extension declaration and the plugin/parser block in
services/web/eslint.config.mjs are widened together: widening only the
rule blocks makes `@overleaf/*` rules match files the plugin
registration does not, which fails with "could not find plugin
@overleaf".
The root global-ignores shim (`services/**/*.{mjs,cjs,ts,tsx}` and
friends) is deliberately left alone -- it replicates ESLint v8's file
walker so pre-existing debt in never-linted files stays off the build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fix eslint errors
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 084d0d054db9db6b5e3dd26de56d14358ab5edf7
* [monorepo] reduce writes from chrome in cypress
Chrome puts its shared memory in /dev/shm, and docker's default 64MB is far
too small for it - which is why Cypress passes --disable-dev-shm-usage,
spilling those segments to files on the container's writable layer instead.
Under one spec that is a few hundred MB of write-then-delete churn; under the
CI fan-out it was most of the disk bandwidth on the whole box.
So every service built from dockerfiles/cypress gets shm_size 1gb and every
cypress config drops the flag again: the SaaS e2e service, web's CI and local
component-test services plus its standalone cypress one, and server-ce's e2e
and native cypress services. Both e2e-open variants inherit the setting
through `extends`; the native one is a service of its own, and would be left
running Chrome against 64MB once the flag is gone.
* [monorepo] adopt cypress-pnp-reporter
Materializing the reporter packages copied 267 packages / 3779 files
(59MB) into /tmp on every container start, once per spec. Resolving them
through PnP inside Cypress's Electron process instead drops that to
zero: the entrypoint shell went from 55.6MB of write_bytes to 0.
* [web] serve built assets from a prepared buffer between builds
Serving a chunk cost ~2ms whatever its size, and an editor page pulls ~144
of them, so webpack-dev-server burned 2-3 cores through an e2e run purely
answering requests for output that had not changed since the last build.
It is not bytes: cutting the bundle 2.6x with a cheaper devtool moved CPU
by 8%, and an empty 304 cost nearly as much as the full body. It is the
per-request work - reading the asset out of the in-memory filesystem and
re-hashing the whole body for a strong ETag, on every request.
Three things a build does that this has to respect, each found by tracing
the hooks rather than by reading them:
- `done` fires twice per build, for the compilation and for the manifest,
both with the same hash. A flag would have been cleared by the second
while the first was still running, so the gate is a counter.
- An incremental `done` reports only the assets it rewrote - 16, not 630 -
so asset names accumulate. Resetting them on `invalid` left the
manifest's two behind and nothing was served from here again.
- webpack-dev-middleware holds requests during a rebuild. This sits in
front of it, so it steps aside for that window and drops the cache when
a rebuild starts. Whether a response can be kept is then decided as it
ends rather than as it began: an epoch bumped on `invalid` names the
build its bytes were read from, and only a response from the current
build with no rebuild in flight is stored. A rebuild starting
mid-response can no longer write the previous build's bytes in behind
the cache being dropped, which eviction by asset name could not catch
anyway - an incremental build does not name the assets it left alone.
Measured on the editor entrypoint, 16 concurrent clients:
0.26-0.31s -> 0.045-0.050s of webpack CPU per page load, and 0.0455s
immediately after a rebuild. Same chunk graph, byte-identical output.
GitOrigin-RevId: 08b4ab2d665414069bc01b4763dd64faa9b4e5cb