diff --git a/services/web/app/src/Features/Compile/ClsiManager.mjs b/services/web/app/src/Features/Compile/ClsiManager.mjs index fe815129c4..57fa48e473 100644 --- a/services/web/app/src/Features/Compile/ClsiManager.mjs +++ b/services/web/app/src/Features/Compile/ClsiManager.mjs @@ -1190,6 +1190,9 @@ function _finaliseRequest(projectId, options, project, docs, files) { if (options.fileLineErrors) { flags = ['-file-line-error'] } + const hasPremiumCompiles = ['alpha', 'priority'].includes( + options.compileGroup + ) return { compile: { @@ -1201,7 +1204,8 @@ function _finaliseRequest(projectId, options, project, docs, files) { timeout: options.timeout, imageName: project.imageName, draft: Boolean(options.draft), - png2pdf: Boolean(options.png2pdf), + // enable for premium compiles only + png2pdf: Boolean(options.png2pdf) && hasPremiumCompiles, stopOnFirstError: Boolean(options.stopOnFirstError), check: options.check, syncType: options.syncType, @@ -1210,7 +1214,7 @@ function _finaliseRequest(projectId, options, project, docs, files) { // Overleaf alpha/staff users get compileGroup=alpha (via getProjectCompileLimits in CompileManager), enroll them into the premium rollout of clsi-cache. compileFromClsiCache: // enable for premium compiles - (['alpha', 'priority'].includes(options.compileGroup) || + (hasPremiumCompiles || // enable for free for short period when we saw low capacity enableCompileFromCacheUntil > Date.now()) && options.compileFromClsiCache, diff --git a/services/web/app/src/Features/Compile/CompileController.mjs b/services/web/app/src/Features/Compile/CompileController.mjs index 5fd42bca0d..34a7c39fd7 100644 --- a/services/web/app/src/Features/Compile/CompileController.mjs +++ b/services/web/app/src/Features/Compile/CompileController.mjs @@ -55,18 +55,14 @@ async function _getSplitTestOptions(req, res) { const pdfDownloadDomain = Settings.pdfDownloadDomain const enablePdfCaching = Settings.enablePdfCaching - - if (!enablePdfCaching || !req.query.enable_pdf_caching) { - // The frontend does not want to do pdf caching. - return { - compileFromHistory, - pdfDownloadDomain, - enablePdfCaching: false, - } - } - const pdfCachingMinChunkSize = Settings.pdfCachingMinChunkSize + const pdfCachingOptions = + !enablePdfCaching || !req.query.enable_pdf_caching + ? // The frontend does not want to do pdf caching. + { enablePdfCaching: false } + : { enablePdfCaching, pdfCachingMinChunkSize } + const enableCheckpoint = await SplitTestHandler.promises.featureFlagEnabled( req, res, @@ -74,12 +70,24 @@ async function _getSplitTestOptions(req, res) { { includeReferer: true } ) + const enablePng2Pdf = await SplitTestHandler.promises.featureFlagEnabled( + req, + res, + 'png2pdf', + { includeReferer: true } + ) + + const png2PdfOptions = + !enablePng2Pdf || !req.body.png2pdf + ? { enablePng2Pdf: false } + : { enablePng2Pdf } + return { compileFromHistory, pdfDownloadDomain, - enablePdfCaching, - pdfCachingMinChunkSize, + ...pdfCachingOptions, enableCheckpoint, + ...png2PdfOptions, } } @@ -202,9 +210,6 @@ const _CompileController = { if (req.body.draft) { options.draft = req.body.draft } - if (req.body.png2pdf) { - options.png2pdf = req.body.png2pdf - } if (['validate', 'error', 'silent'].includes(req.body.check)) { options.check = req.body.check } @@ -218,6 +223,7 @@ const _CompileController = { pdfDownloadDomain, compileFromHistory, enableCheckpoint, + enablePng2Pdf, } = await _getSplitTestOptions(req, res) if (Features.hasFeature('saas')) { options.compileFromClsiCache = true @@ -226,6 +232,9 @@ const _CompileController = { if (enableCheckpoint) { options.enableCheckpoint = enableCheckpoint } + if (enablePng2Pdf) { + options.png2pdf = enablePng2Pdf + } } options.enablePdfCaching = enablePdfCaching if (enablePdfCaching) { diff --git a/services/web/app/src/Features/Editor/EditorController.mjs b/services/web/app/src/Features/Editor/EditorController.mjs index 57ddc2cf59..b9987a3f15 100644 --- a/services/web/app/src/Features/Editor/EditorController.mjs +++ b/services/web/app/src/Features/Editor/EditorController.mjs @@ -576,6 +576,16 @@ const EditorController = { }) }, + setPng2pdf(projectId, png2pdf, callback) { + ProjectOptionsHandler.setPng2pdf(projectId, png2pdf, function (err) { + if (err) { + return callback(err) + } + EditorRealTimeController.emitToRoom(projectId, 'png2pdfUpdated', png2pdf) + callback() + }) + }, + setSpellCheckLanguage(projectId, languageCode, callback) { ProjectOptionsHandler.setSpellCheckLanguage( projectId, @@ -717,6 +727,7 @@ EditorController.promises = { renameProject: promisify(EditorController.renameProject), setCompiler: promisify(EditorController.setCompiler), setImageName: promisify(EditorController.setImageName), + setPng2pdf: promisify(EditorController.setPng2pdf), setSpellCheckLanguage: promisify(EditorController.setSpellCheckLanguage), setPublicAccessLevel: promisify(EditorController.setPublicAccessLevel), setRootDoc: promisify(EditorController.setRootDoc), diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index e09055a2f2..f2ded0314d 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -87,6 +87,7 @@ const updateProjectSettingsSchema = z.object({ body: z.object({ compiler: z.string().optional(), imageName: z.string().optional(), + png2pdf: z.boolean().optional(), mainBibliographyDocId: zz.objectId().optional(), name: z.string().optional(), rootDocId: zz.objectId().optional(), @@ -117,6 +118,10 @@ const _ProjectController = { await EditorController.promises.setImageName(projectId, body.imageName) } + if (body.png2pdf != null) { + await EditorController.promises.setPng2pdf(projectId, body.png2pdf) + } + if (body.name != null) { await EditorController.promises.renameProject(projectId, body.name) } @@ -576,6 +581,7 @@ const _ProjectController = { track_changes: 1, owner_ref: 1, brandVariationId: 1, + png2pdf: 1, overleaf: 1, tokens: 1, }), @@ -975,6 +981,10 @@ const _ProjectController = { canUseClsiCache: Features.hasFeature('saas') && ownerFeatures?.compileGroup === 'priority', + canUsePng2Pdf: + Features.hasFeature('saas') && + ownerFeatures?.compileGroup === 'priority' && + splitTestAssignments['png2pdf']?.variant === 'enabled', user: { id: userId, email: user.email, diff --git a/services/web/app/src/Features/Project/ProjectEditorHandler.mjs b/services/web/app/src/Features/Project/ProjectEditorHandler.mjs index c85381431d..dcf3cd80f5 100644 --- a/services/web/app/src/Features/Project/ProjectEditorHandler.mjs +++ b/services/web/app/src/Features/Project/ProjectEditorHandler.mjs @@ -24,6 +24,7 @@ export default ProjectEditorHandler = { compiler: project.compiler, description: project.description, spellCheckLanguage: project.spellCheckLanguage, + png2pdf: project.png2pdf, deletedByExternalDataSource: project.deletedByExternalDataSource || false, imageName: project.imageName != null diff --git a/services/web/app/src/Features/Project/ProjectOptionsHandler.mjs b/services/web/app/src/Features/Project/ProjectOptionsHandler.mjs index e1d8830fa4..accb3144df 100644 --- a/services/web/app/src/Features/Project/ProjectOptionsHandler.mjs +++ b/services/web/app/src/Features/Project/ProjectOptionsHandler.mjs @@ -59,6 +59,12 @@ const ProjectOptionsHandler = { return Project.updateOne(conditions, update, {}) }, + async setPng2pdf(projectId, png2pdf) { + const conditions = { _id: projectId } + const update = { png2pdf: Boolean(png2pdf) } + return Project.updateOne(conditions, update, {}) + }, + async setSpellCheckLanguage(projectId, languageCode) { if (!Array.isArray(settings.languages)) { return @@ -118,6 +124,7 @@ const ProjectOptionsHandler = { export default { setCompiler: callbackify(ProjectOptionsHandler.setCompiler), setImageName: callbackify(ProjectOptionsHandler.setImageName), + setPng2pdf: callbackify(ProjectOptionsHandler.setPng2pdf), setSpellCheckLanguage: callbackify( ProjectOptionsHandler.setSpellCheckLanguage ), diff --git a/services/web/app/src/models/Project.mjs b/services/web/app/src/models/Project.mjs index 0ac5683850..9ca3cd2431 100644 --- a/services/web/app/src/models/Project.mjs +++ b/services/web/app/src/models/Project.mjs @@ -52,6 +52,7 @@ export const ProjectSchema = new Schema( trashed: [{ type: ObjectId, ref: 'User' }], deletedDocs: [DeletedDocSchema], imageName: { type: String }, + png2pdf: { type: Boolean }, brandVariationId: { type: String }, track_changes: { type: Object }, tokens: { diff --git a/services/web/app/views/project/editor/_meta.pug b/services/web/app/views/project/editor/_meta.pug index fb0a637c0e..39ab408cdf 100644 --- a/services/web/app/views/project/editor/_meta.pug +++ b/services/web/app/views/project/editor/_meta.pug @@ -1,6 +1,7 @@ meta(name="ol-project_id" content=project_id) meta(name="ol-projectName" content=projectName) meta(name="ol-canUseClsiCache" data-type="boolean" content=canUseClsiCache) +meta(name="ol-canUsePng2Pdf" data-type="boolean" content=canUsePng2Pdf) meta(name="ol-userSettings" data-type="json" content=userSettings) meta(name="ol-user" data-type="json" content=user) meta(name="ol-learnedWords" data-type="json" content=learnedWords) diff --git a/services/web/frontend/js/features/ide-settings/components/compiler-settings/draft-setting.tsx b/services/web/frontend/js/features/ide-settings/components/compiler-settings/draft-setting.tsx index 658dbd7bdd..98b4c07fe4 100644 --- a/services/web/frontend/js/features/ide-settings/components/compiler-settings/draft-setting.tsx +++ b/services/web/frontend/js/features/ide-settings/components/compiler-settings/draft-setting.tsx @@ -1,17 +1,19 @@ +import getMeta from '../../../../utils/meta' import { useTranslation } from 'react-i18next' import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context' import { useCallback, useMemo } from 'react' import DropdownSetting from '../dropdown-setting' import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics' -import { useFeatureFlag } from '@/shared/context/split-test-context' +import { useProjectSettingsContext } from '@/features/ide-settings/context/project-settings-context' type CompileMode = 'normal' | 'png2pdf' | 'fast_draft' export default function DraftSetting() { const { draft, setDraft, png2pdf, setPng2pdf } = useCompileContext() + const { setPng2pdf: persistPng2pdf } = useProjectSettingsContext() const { t } = useTranslation() const { sendEvent } = useEditorAnalytics() - const png2pdfEnabled = useFeatureFlag('png2pdf') + const png2pdfEnabled = getMeta('ol-canUsePng2Pdf') // The three modes are mutually exclusive, derived from the two underlying flags. const mode: CompileMode = png2pdf @@ -26,10 +28,18 @@ export default function DraftSetting() { setting: 'compile-mode', settingVal: value, }) + const enablePng2pdf = value === 'png2pdf' setDraft(value === 'fast_draft') - setPng2pdf(value === 'png2pdf') + setPng2pdf(enablePng2pdf) + // Draft mode is a local-only setting, so don't overwrite the project-wide + // png2pdf preference (shared with all collaborators) when a user just + // toggles draft for themselves. Only persist when the user explicitly + // picks Normal or Fast [optimize images]. + if (value !== 'fast_draft') { + persistPng2pdf(enablePng2pdf) + } }, - [sendEvent, setDraft, setPng2pdf] + [sendEvent, setDraft, setPng2pdf, persistPng2pdf] ) const options = useMemo( diff --git a/services/web/frontend/js/features/ide-settings/context/project-settings-context.tsx b/services/web/frontend/js/features/ide-settings/context/project-settings-context.tsx index c9b9016a2f..4b88b8f5fe 100644 --- a/services/web/frontend/js/features/ide-settings/context/project-settings-context.tsx +++ b/services/web/frontend/js/features/ide-settings/context/project-settings-context.tsx @@ -8,6 +8,7 @@ import { UserSettings } from '../../../../../types/user-settings' type ProjectSettingsSetterContextValue = { setCompiler: (compiler: ProjectSettings['compiler']) => Promise setImageName: (imageName: ProjectSettings['imageName']) => Promise + setPng2pdf: (png2pdf: ProjectSettings['png2pdf']) => Promise setRootDocId: (rootDocId: ProjectSettings['rootDocId']) => void setSpellCheckLanguage: ( spellCheckLanguage: ProjectSettings['spellCheckLanguage'] @@ -60,6 +61,8 @@ export const ProjectSettingsProvider: FC = ({ setCompiler, imageName, setImageName, + png2pdf, + setPng2pdf, rootDocId, setRootDocId, spellCheckLanguage, @@ -121,6 +124,8 @@ export const ProjectSettingsProvider: FC = ({ setCompiler, imageName, setImageName, + png2pdf, + setPng2pdf, rootDocId, setRootDocId, spellCheckLanguage, @@ -175,6 +180,8 @@ export const ProjectSettingsProvider: FC = ({ setCompiler, imageName, setImageName, + png2pdf, + setPng2pdf, rootDocId, setRootDocId, spellCheckLanguage, diff --git a/services/web/frontend/js/features/ide-settings/hooks/use-project-wide-settings-socket-listener.tsx b/services/web/frontend/js/features/ide-settings/hooks/use-project-wide-settings-socket-listener.tsx index 953084bd25..88393a15b5 100644 --- a/services/web/frontend/js/features/ide-settings/hooks/use-project-wide-settings-socket-listener.tsx +++ b/services/web/frontend/js/features/ide-settings/hooks/use-project-wide-settings-socket-listener.tsx @@ -26,6 +26,15 @@ export default function useProjectWideSettingsSocketListener() { [project, updateProject] ) + const setPng2pdf = useCallback( + (png2pdf: ProjectSettings['png2pdf']) => { + if (project) { + updateProject({ png2pdf }) + } + }, + [project, updateProject] + ) + const setSpellCheckLanguage = useCallback( (spellCheckLanguage: ProjectSettings['spellCheckLanguage']) => { if (project) { @@ -42,15 +51,24 @@ export default function useProjectWideSettingsSocketListener() { if (dataAvailable && socket) { socket.on('compilerUpdated', setCompiler) socket.on('imageNameUpdated', setImageName) + socket.on('png2pdfUpdated', setPng2pdf) socket.on('spellCheckLanguageUpdated', setSpellCheckLanguage) return () => { socket.removeListener('compilerUpdated', setCompiler) socket.removeListener('imageNameUpdated', setImageName) + socket.removeListener('png2pdfUpdated', setPng2pdf) socket.removeListener( 'spellCheckLanguageUpdated', setSpellCheckLanguage ) } } - }, [socket, project, setCompiler, setImageName, setSpellCheckLanguage]) + }, [ + socket, + project, + setCompiler, + setImageName, + setPng2pdf, + setSpellCheckLanguage, + ]) } diff --git a/services/web/frontend/js/features/ide-settings/hooks/use-project-wide-settings.tsx b/services/web/frontend/js/features/ide-settings/hooks/use-project-wide-settings.tsx index 52f18c0e08..faa2c614b2 100644 --- a/services/web/frontend/js/features/ide-settings/hooks/use-project-wide-settings.tsx +++ b/services/web/frontend/js/features/ide-settings/hooks/use-project-wide-settings.tsx @@ -5,10 +5,12 @@ import useSaveProjectSettings from './use-save-project-settings' import useSetSpellCheckLanguage from './use-set-spell-check-language' import { debugConsole } from '@/utils/debugging' import { useProjectContext } from '@/shared/context/project-context' +import getMeta from '@/utils/meta' export default function useProjectWideSettings() { // The value will be undefined on mount const { project } = useProjectContext() + const png2pdfEnabled = Boolean(getMeta('ol-canUsePng2Pdf')) const saveProjectSettings = useSaveProjectSettings() const setCompiler = useCallback( @@ -29,6 +31,13 @@ export default function useProjectWideSettings() { [saveProjectSettings] ) + const setPng2pdf = useCallback( + async (newPng2pdf: ProjectSettings['png2pdf']) => { + await saveProjectSettings('png2pdf', newPng2pdf).catch(debugConsole.error) + }, + [saveProjectSettings] + ) + const { setRootDocId, rootDocId } = useRootDocId() const setSpellCheckLanguage = useSetSpellCheckLanguage() @@ -37,6 +46,8 @@ export default function useProjectWideSettings() { setCompiler, imageName: project?.imageName, setImageName, + png2pdf: project?.png2pdf ?? png2pdfEnabled, + setPng2pdf, rootDocId, setRootDocId, spellCheckLanguage: project?.spellCheckLanguage, diff --git a/services/web/frontend/js/features/ide-settings/utils/api.ts b/services/web/frontend/js/features/ide-settings/utils/api.ts index 122df91f8c..21c2db2665 100644 --- a/services/web/frontend/js/features/ide-settings/utils/api.ts +++ b/services/web/frontend/js/features/ide-settings/utils/api.ts @@ -7,6 +7,9 @@ import { UserSettings } from '../../../../../types/user-settings' export interface ProjectSettings { compiler: ProjectCompiler imageName: string + // Optional: a project that has never set png2pdf sends no value, which the + // compile context treats as "no preference, use the local default". + png2pdf?: boolean rootDocId: string spellCheckLanguage: string name: string diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx index fea4e9be83..36bdc9e9df 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx @@ -1,3 +1,4 @@ +import getMeta from '../../../utils/meta' import { useTranslation } from 'react-i18next' import { memo, useCallback } from 'react' import classNames from 'classnames' @@ -18,7 +19,6 @@ import OLButton from '@/shared/components/ol/ol-button' import OLButtonGroup from '@/shared/components/ol/ol-button-group' import { useLayoutContext } from '@/shared/context/layout-context' import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider' -import { useFeatureFlag } from '@/shared/context/split-test-context' const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl' @@ -58,7 +58,7 @@ function PdfCompileButton() { const { t } = useTranslation() - const png2pdfEnabled = useFeatureFlag('png2pdf') + const png2pdfEnabled = getMeta('ol-canUsePng2Pdf') // The three compile modes (Normal / Fast [optimize images] / Fast [draft]) are // mutually exclusive, so each selection sets both underlying flags. diff --git a/services/web/frontend/js/shared/context/local-compile-context.tsx b/services/web/frontend/js/shared/context/local-compile-context.tsx index c75fd14ae1..ca4854cb78 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -276,15 +276,27 @@ export const LocalCompileProvider: FC = ({ listen: true, }) + // ol-canUsePng2Pdf is the single source of truth from the backend: it already + // accounts for both the split-test rollout and the premium entitlement. + const canUsePng2pdf = Boolean(getMeta('ol-canUsePng2Pdf')) + const png2pdfDefault = canUsePng2pdf && !draft + // whether the compile should optimize images (png2pdf conversion) - const [png2pdf, setPng2pdf] = usePersistedState( + const [png2pdfSetting, setPng2pdf] = usePersistedState( `png2pdf:${projectId}`, - false, + png2pdfDefault, { listen: true, } ) + // The compile modes (Normal / optimize-images / draft) are mutually + // exclusive. Draft takes precedence, so png2pdf is never sent alongside draft + // even when a stale local or server-persisted setting has it enabled (e.g. a + // socket update applying project.png2pdf=true while the user is in local draft + // mode, or a setting left over after a downgrade). + const png2pdf = canUsePng2pdf && png2pdfSetting && !draft + // whether compiling should stop on first error const [stopOnFirstError, setStopOnFirstError] = usePersistedState( `stop_on_first_error:${projectId}`, @@ -367,6 +379,15 @@ export const LocalCompileProvider: FC = ({ compiler.setOption('png2pdf', png2pdf) }, [compiler, png2pdf]) + // Apply the server-persisted png2pdf setting when the project has one. When + // it is unset, leave the locally-derived default (which accounts for draft + // mode) in place rather than forcing it on. + useEffect(() => { + if (project?.png2pdf !== undefined) { + setPng2pdf(project.png2pdf) + } + }, [project?.png2pdf, setPng2pdf]) + // keep stop on first error setting in sync with the compiler useEffect(() => { compiler.setOption('stopOnFirstError', stopOnFirstError) diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index 7bda0d5dc0..1f152369f2 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -87,6 +87,7 @@ export interface Meta { 'ol-canUseAddSeatsFeature': boolean 'ol-canUseClsiCache': boolean 'ol-canUseFlexibleLicensing': boolean + 'ol-canUsePng2Pdf': boolean // dynamic keys based on permissions 'ol-cannot-add-secondary-email': boolean diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx index 16389878c0..2e5dc19dea 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx @@ -55,6 +55,7 @@ describe('', function () { window.metaAttributesCache.set('ol-compileSettings', { compileTimeout: 240, }) + window.metaAttributesCache.set('ol-splitTestVariants', {}) cy.interceptEvents() }) @@ -222,6 +223,15 @@ describe('', function () { }, props: {}, }, + 'ignores the compile from cache when png2pdf is available by default': { + cached: false, + setup: () => { + cy.window().then(w => + w.metaAttributesCache.set('ol-canUsePng2Pdf', true) + ) + }, + props: {}, + }, 'ignores the compile from cache when rootDoc changed': { cached: false, setup: () => {}, diff --git a/services/web/test/frontend/features/ide-settings/settings/draft-setting.test.tsx b/services/web/test/frontend/features/ide-settings/settings/draft-setting.test.tsx index 2cf52fe8f1..94f9159e42 100644 --- a/services/web/test/frontend/features/ide-settings/settings/draft-setting.test.tsx +++ b/services/web/test/frontend/features/ide-settings/settings/draft-setting.test.tsx @@ -1,5 +1,6 @@ import { screen, within, render } from '@testing-library/react' import { expect } from 'chai' +import fetchMock from 'fetch-mock' import { SettingsModalProvider } from '@/features/ide-settings/context/settings-modal-context' import { EditorProviders, @@ -11,7 +12,9 @@ import DraftSetting from '@/features/ide-settings/components/compiler-settings/d describe('', function () { afterEach(function () { window.metaAttributesCache.delete('ol-splitTestVariants') + window.metaAttributesCache.delete('ol-canUsePng2Pdf') localStorage.clear() + fetchMock.removeRoutes().clearHistory() }) it('each option is shown and can be selected', async function () { @@ -42,10 +45,8 @@ describe('', function () { } }) - it('offers the optimize-images option behind the png2pdf split test', async function () { - window.metaAttributesCache.set('ol-splitTestVariants', { - png2pdf: 'enabled', - }) + it('offers the optimize-images option when png2pdf is available', async function () { + window.metaAttributesCache.set('ol-canUsePng2Pdf', true) render( @@ -59,11 +60,118 @@ describe('', function () { expect(optionElement.getAttribute('value')).to.equal('png2pdf') await userEvent.selectOptions(select, [optionElement]) - expect(!!localStorage.getItem(`png2pdf:${projectDefaults._id}`)).to.equal( - true + expect((select as HTMLSelectElement).value).to.equal('png2pdf') + expect(localStorage.getItem(`png2pdf:${projectDefaults._id}`)).to.equal( + null ) expect(!!localStorage.getItem(`draft:${projectDefaults._id}`)).to.equal( false ) }) + + it('defaults to optimize-images mode when png2pdf is available', function () { + window.metaAttributesCache.set('ol-canUsePng2Pdf', true) + + render( + + + + + + ) + + const select = screen.getByLabelText('Compile mode') as HTMLSelectElement + expect(select.value).to.equal('png2pdf') + expect(localStorage.getItem(`png2pdf:${projectDefaults._id}`)).to.equal( + null + ) + expect(localStorage.getItem(`draft:${projectDefaults._id}`)).to.equal(null) + }) + + it('respects a previous draft setting over the png2pdf default', function () { + window.metaAttributesCache.set('ol-canUsePng2Pdf', true) + localStorage.setItem(`draft:${projectDefaults._id}`, 'true') + + render( + + + + + + ) + + const select = screen.getByLabelText('Compile mode') as HTMLSelectElement + expect(select.value).to.equal('fast_draft') + }) + + it('persists the png2pdf setting to the server when changed', async function () { + window.metaAttributesCache.set('ol-canUsePng2Pdf', true) + render( + + + + + + ) + + const saveSettingsMock = fetchMock.post( + `express:/project/:projectId/settings`, + { status: 200 }, + { delay: 0 } + ) + + const select = screen.getByLabelText('Compile mode') + + await userEvent.selectOptions(select, [ + within(select).getByText('Fast [optimize images]'), + ]) + expect( + saveSettingsMock.callHistory.called( + `/project/${projectDefaults._id}/settings`, + { body: { png2pdf: true } } + ) + ).to.be.true + + await userEvent.selectOptions(select, [within(select).getByText('Normal')]) + expect( + saveSettingsMock.callHistory.called( + `/project/${projectDefaults._id}/settings`, + { body: { png2pdf: false } } + ) + ).to.be.true + }) + + it('does not persist png2pdf when switching to draft mode', async function () { + // Draft is a local-only setting; toggling it must not overwrite the shared + // project-wide png2pdf preference. + window.metaAttributesCache.set('ol-canUsePng2Pdf', true) + render( + + + + + + ) + + const saveSettingsMock = fetchMock.post( + `express:/project/:projectId/settings`, + { status: 200 }, + { delay: 0 } + ) + + const select = screen.getByLabelText('Compile mode') + await userEvent.selectOptions(select, [ + within(select).getByText('Fast [draft]'), + ]) + + expect((select as HTMLSelectElement).value).to.equal('fast_draft') + expect(localStorage.getItem(`draft:${projectDefaults._id}`)).to.equal( + 'true' + ) + expect( + saveSettingsMock.callHistory.called( + `/project/${projectDefaults._id}/settings` + ) + ).to.be.false + }) }) diff --git a/services/web/test/unit/src/Compile/ClsiManager.test.mjs b/services/web/test/unit/src/Compile/ClsiManager.test.mjs index 7b04be3520..d50ffd30d0 100644 --- a/services/web/test/unit/src/Compile/ClsiManager.test.mjs +++ b/services/web/test/unit/src/Compile/ClsiManager.test.mjs @@ -1096,6 +1096,7 @@ describe('ClsiManager', function () { ctx.user_id, { timeout: 100, + compileGroup: 'priority', png2pdf: true, } ) @@ -1111,6 +1112,37 @@ describe('ClsiManager', function () { }) }) + describe('with the png2pdf option and standard compileGroup', function () { + beforeEach(async function (ctx) { + await ctx.ClsiManager.promises.sendRequest( + null, + ctx.project._id, + ctx.user_id, + { + timeout: 100, + compileGroup: 'standard', + png2pdf: true, + } + ) + }) + + it('should force the png2pdf option to false in the request', function (ctx) { + expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith( + sinon.match.any, + sinon.match({ + json: { + compile: { + options: { + compileGroup: 'standard', + png2pdf: false, + }, + }, + }, + }) + ) + }) + }) + describe('with a failed compile', function () { beforeEach(async function (ctx) { ctx.responseBody.compile.status = 'failure' diff --git a/services/web/test/unit/src/Project/ProjectEditorHandler.test.mjs b/services/web/test/unit/src/Project/ProjectEditorHandler.test.mjs index 5ea0126809..651424befd 100644 --- a/services/web/test/unit/src/Project/ProjectEditorHandler.test.mjs +++ b/services/web/test/unit/src/Project/ProjectEditorHandler.test.mjs @@ -12,6 +12,7 @@ describe('ProjectEditorHandler', function () { rootDoc_id: 'file-id', publicAccesLevel: 'private', deletedByExternalDataSource: false, + png2pdf: true, rootFolder: [ { _id: 'root-folder-id', @@ -126,6 +127,10 @@ describe('ProjectEditorHandler', function () { ctx.result.publicAccesLevel.should.equal('private') }) + it('should include the png2pdf setting', function (ctx) { + ctx.result.png2pdf.should.equal(true) + }) + it('should include the owner', function (ctx) { expect(ctx.result.owner).to.exist ctx.result.owner._id.should.equal('owner-id')