Transition optimized images to project-level toggle (#36093)

* feat: transitioning optimized images from a compile mode to a project level toggle. We still persist it for the proect and between users, and ensure that draft mode does not interact with it (silently disabled while draft mode is on)

* feat: cleaning up unused var, tests, and locales

* feat: updating copy

GitOrigin-RevId: 8010edfab164980f14125739e481d8b512f6b3df
This commit is contained in:
Jimmy Domagala-Tang
2026-09-17 11:47:49 +00:00
committed by Copybot
parent 49747e61c9
commit 560b7a209a
11 changed files with 143 additions and 192 deletions
@@ -690,7 +690,6 @@
"fair_usage_policy_applies": "",
"fast": "",
"fast_draft": "",
"fast_optimize_images": "",
"feature_controls": "",
"feature_disabled_by_organization_contact_support": "",
"feature_enabled_or_disabled": "",
@@ -1399,6 +1398,7 @@
"open_project": "",
"open_settings": "",
"open_target": "",
"optimise_images_recommended": "",
"optional": "",
"or": "",
"or_enable_clipboard_access_in_your_browser_settings": "",
@@ -1937,6 +1937,7 @@
"sort_projects": "",
"speak": "",
"speech_input_not_available": "",
"speed_up_compiles_by_reusing_images": "",
"spellcheck": "",
"spellcheck_language": "",
"spelling_and_language": "",
@@ -1,26 +1,17 @@
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 { useProjectSettingsContext } from '@/features/ide-settings/context/project-settings-context'
type CompileMode = 'normal' | 'png2pdf' | 'fast_draft'
type CompileMode = 'normal' | 'fast_draft'
export default function DraftSetting() {
const { draft, setDraft, png2pdf, setPng2pdf } = useCompileContext()
const { setPng2pdf: persistPng2pdf } = useProjectSettingsContext()
const { draft, setDraft } = useCompileContext()
const { t } = useTranslation()
const { sendEvent } = useEditorAnalytics()
const png2pdfEnabled = getMeta('ol-canUsePng2Pdf')
// The three modes are mutually exclusive, derived from the two underlying flags.
const mode: CompileMode = png2pdf
? 'png2pdf'
: draft
? 'fast_draft'
: 'normal'
const mode: CompileMode = draft ? 'fast_draft' : 'normal'
const changeMode = useCallback(
(value: CompileMode) => {
@@ -28,29 +19,17 @@ export default function DraftSetting() {
setting: 'compile-mode',
settingVal: value,
})
const enablePng2pdf = value === 'png2pdf'
setDraft(value === 'fast_draft')
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, persistPng2pdf]
[sendEvent, setDraft]
)
const options = useMemo(
() => [
{ label: t('normal'), value: 'normal' as const },
...(png2pdfEnabled
? [{ label: t('fast_optimize_images'), value: 'png2pdf' as const }]
: []),
{ label: t('fast_draft'), value: 'fast_draft' as const },
],
[t, png2pdfEnabled]
[t]
)
return (
@@ -61,7 +40,7 @@ export default function DraftSetting() {
description={t('switch_compile_mode_for_faster_draft_compilation')}
value={mode}
onChange={changeMode}
width={png2pdfEnabled ? 'wide' : 'default'}
width="default"
/>
)
}
@@ -0,0 +1,28 @@
import ToggleSetting from '../toggle-setting'
import getMeta from '../../../../utils/meta'
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '@/features/ide-settings/context/project-settings-context'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { useSetCompilationSettingWithEvent } from '@/features/ide-settings/hooks/use-set-compilation-setting'
export default function OptimizeCompilesSetting() {
const { t } = useTranslation()
const png2pdfEnabled = getMeta('ol-canUsePng2Pdf')
const { png2pdf, setPng2pdf } = useProjectSettingsContext()
const { write } = usePermissionsContext()
const changePng2pdf = useSetCompilationSettingWithEvent(
'optimize-compiles',
setPng2pdf
)
return png2pdfEnabled ? (
<ToggleSetting
id="optimizeCompiles"
label={t('optimise_images_recommended')}
description={t('speed_up_compiles_by_reusing_images')}
disabled={!write}
checked={png2pdf}
onChange={changePng2pdf}
/>
) : null
}
@@ -47,6 +47,7 @@ import type {
import EditorTabsSetting from '../components/editor-settings/editor-tabs-setting'
import FloatingMenuSetting from '../components/editor-settings/floating-menu-setting'
import useEventListener from '@/shared/hooks/use-event-listener'
import OptimizeCompileSetting from '../components/compiler-settings/optimize-compile-setting'
const [referenceSearchSettingModule] = importOverleafModules(
'referenceSearchSetting'
@@ -229,6 +230,10 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
key: 'autoCompile',
component: <AutoCompileSetting />,
},
{
key: 'optimizeCompiles',
component: <OptimizeCompileSetting />,
},
],
},
],
@@ -1,4 +1,3 @@
import getMeta from '../../../utils/meta'
import { useTranslation } from 'react-i18next'
import { memo, useCallback } from 'react'
import classNames from 'classnames'
@@ -40,11 +39,9 @@ function PdfCompileButton() {
autoCompile,
compiling,
draft,
png2pdf,
hasChanges,
setAutoCompile,
setDraft,
setPng2pdf,
setStopOnValidationError,
stopOnFirstError,
stopOnValidationError,
@@ -58,20 +55,16 @@ function PdfCompileButton() {
const { t } = useTranslation()
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.
// The two compile modes (Normal / Fast [draft]) are mutually exclusive.
const setCompileMode = useCallback(
(mode: 'normal' | 'png2pdf' | 'draft') => {
(mode: 'normal' | 'draft') => {
eventTracking.sendMB('recompile-setting-changed', {
setting: 'compile-mode',
settingVal: mode,
})
setDraft(mode === 'draft')
setPng2pdf(mode === 'png2pdf')
},
[setDraft, setPng2pdf]
[setDraft]
)
const { detachRole } = useLayoutContext()
@@ -205,23 +198,11 @@ function PdfCompileButton() {
<OLDropdownItem
as="button"
onClick={() => setCompileMode('normal')}
trailingIcon={!draft && !png2pdf ? 'check' : null}
trailingIcon={!draft ? 'check' : null}
>
{t('normal')}
</OLDropdownItem>
</li>
{png2pdfEnabled && (
<li role="none">
<OLDropdownItem
as="button"
onClick={() => setCompileMode('png2pdf')}
trailingIcon={png2pdf ? 'check' : null}
>
{t('fast')}&nbsp;
<span className="subdued">[optimize images]</span>
</OLDropdownItem>
</li>
)}
<li role="none">
<OLDropdownItem
as="button"
@@ -46,7 +46,6 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
setAnimateCompileDropdownArrow: _setAnimateCompileDropdownArrow,
setAutoCompile: _setAutoCompile,
setDraft: _setDraft,
setPng2pdf: _setPng2pdf,
setError: _setError,
setHasLintingError: _setHasLintingError,
setHighlights: _setHighlights,
@@ -268,12 +267,6 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
'detached',
'detacher'
)
const setPng2pdf = useDetachAction(
'setPng2pdf',
_setPng2pdf,
'detached',
'detacher'
)
const setError = useDetachAction(
'setError',
_setError,
@@ -454,7 +447,6 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
setAutoCompile,
setCompiling,
setDraft,
setPng2pdf,
setError,
setHasLintingError,
setHighlights,
@@ -513,7 +505,6 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
setAutoCompile,
setCompiling,
setDraft,
setPng2pdf,
setError,
setHasLintingError,
setHighlights,
@@ -95,7 +95,6 @@ export type CompileContext = {
rawLog?: string
setAutoCompile: (value: boolean) => void
setDraft: (value: any) => void
setPng2pdf: (value: any) => void
setError: (value: any) => void
setHasLintingError: (value: boolean) => void // only for storybook
setHighlights: (value: any) => void
@@ -279,23 +278,13 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
// 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 [png2pdfSetting, setPng2pdf] = usePersistedState(
`png2pdf:${projectId}`,
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
// Optimise images is a project-wide setting, so it is read straight off the
// project rather than persisted per user. It is on by default for anyone who
// can use it and has not turned it off. Draft mode takes precedence: an
// optimized compile is never requested while draft is on, but the project
// setting is left alone so it resumes when draft is turned off again.
const png2pdf = canUsePng2pdf && (project?.png2pdf ?? true) && !draft
// whether compiling should stop on first error
const [stopOnFirstError, setStopOnFirstError] = usePersistedState(
@@ -379,15 +368,6 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
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)
@@ -819,7 +799,6 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
setAutoCompile,
setCompiling,
setDraft,
setPng2pdf,
setError,
setHasLintingError, // only for stories
setHighlights,
@@ -877,7 +856,6 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
setAnimateCompileDropdownArrow,
setAutoCompile,
setDraft,
setPng2pdf,
setError,
setHasLintingError, // only for stories
setHighlights,
+2 -1
View File
@@ -890,7 +890,6 @@
"faq_how_does_free_trial_works_answer": "You get full access to your chosen __appName__ plan during your __len__-day free trial. There is no obligation to continue beyond the trial. Your card will be charged at the end of your __len__ day trial unless you cancel before then. You can cancel via your subscription settings.",
"fast": "Fast",
"fast_draft": "Fast [draft]",
"fast_optimize_images": "Fast [optimize images]",
"fastest": "Fastest",
"feature_controls": "Feature controls",
"feature_disabled_by_organization_contact_support": "This feature isn’t available for your group. <0>Contact Overleaf support</0> to enable it.",
@@ -1817,6 +1816,7 @@
"opens_in_new_tab": "opens in new tab",
"operation": "Operation",
"opted_out_linking": "You’ve opted out from linking your <b>__email__</b> <b>__appName__</b> account to your institutional account.",
"optimise_images_recommended": "Optimise images (recommended)",
"optional": "Optional",
"or": "or",
"or_enable_clipboard_access_in_your_browser_settings": "Or enable clipboard access in your browser settings.",
@@ -2497,6 +2497,7 @@
"source": "Source",
"speak": "Speak",
"speech_input_not_available": "Speech input is not yet available in this browser",
"speed_up_compiles_by_reusing_images": "Speed up compiles by reusing already-processed PNGs, with no loss in quality.",
"spellcheck": "Spellcheck",
"spellcheck_language": "Spellcheck language",
"spelling_and_language": "Spelling and language",
@@ -39,111 +39,12 @@ describe('<DraftSetting />', function () {
expect(!!localStorage.getItem(`draft:${projectDefaults._id}`)).to.equal(
option.draft
)
expect(!!localStorage.getItem(`png2pdf:${projectDefaults._id}`)).to.equal(
false
)
}
})
it('offers the optimize-images option when png2pdf is available', async function () {
window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
render(
<EditorProviders>
<SettingsModalProvider>
<DraftSetting />
</SettingsModalProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Compile mode')
const optionElement = within(select).getByText('Fast [optimize images]')
expect(optionElement.getAttribute('value')).to.equal('png2pdf')
await userEvent.selectOptions(select, [optionElement])
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(
<EditorProviders>
<SettingsModalProvider>
<DraftSetting />
</SettingsModalProvider>
</EditorProviders>
)
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(
<EditorProviders>
<SettingsModalProvider>
<DraftSetting />
</SettingsModalProvider>
</EditorProviders>
)
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(
<EditorProviders>
<SettingsModalProvider>
<DraftSetting />
</SettingsModalProvider>
</EditorProviders>
)
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.
it('does not persist a project setting when switching to draft mode', async function () {
// Draft is a local-only setting; toggling it must not touch the shared
// project-wide Optimise images (recommended) preference.
window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
render(
<EditorProviders>
@@ -0,0 +1,83 @@
import { screen, 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,
projectDefaults,
} from '../../../helpers/editor-providers'
import userEvent from '@testing-library/user-event'
import OptimizeCompilesSetting from '@/features/ide-settings/components/compiler-settings/optimize-compile-setting'
import type { PermissionsLevel } from '@/features/ide-react/types/permissions'
describe('<OptimizeCompilesSetting />', function () {
afterEach(function () {
window.metaAttributesCache.delete('ol-canUsePng2Pdf')
localStorage.clear()
fetchMock.removeRoutes().clearHistory()
})
function renderSetting(
png2pdf?: boolean,
permissionsLevel: PermissionsLevel = 'owner'
) {
render(
<EditorProviders png2pdf={png2pdf} permissionsLevel={permissionsLevel}>
<SettingsModalProvider>
<OptimizeCompilesSetting />
</SettingsModalProvider>
</EditorProviders>
)
}
it('is not rendered when png2pdf is unavailable', function () {
renderSetting()
expect(screen.queryByLabelText('Optimise images (recommended)')).to.be.null
})
it('is on by default when the project has no stored preference', function () {
window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
renderSetting()
const toggle = screen.getByLabelText('Optimise images (recommended)')
expect((toggle as HTMLInputElement).checked).to.be.true
})
it('is disabled for a collaborator without write access', function () {
window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
renderSetting(undefined, 'readOnly')
const toggle = screen.getByLabelText('Optimise images (recommended)')
expect((toggle as HTMLInputElement).disabled).to.be.true
})
it('reflects the stored project preference when it is off', function () {
window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
renderSetting(false)
const toggle = screen.getByLabelText('Optimise images (recommended)')
expect((toggle as HTMLInputElement).checked).to.be.false
})
it('persists the setting to the project when toggled', async function () {
window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
renderSetting()
const saveSettingsMock = fetchMock.post(
`express:/project/:projectId/settings`,
{ status: 200 },
{ delay: 0 }
)
const toggle = screen.getByLabelText('Optimise images (recommended)')
await userEvent.click(toggle)
expect(
saveSettingsMock.callHistory.called(
`/project/${projectDefaults._id}/settings`,
{ body: { png2pdf: false } }
)
).to.be.true
})
})
@@ -84,6 +84,7 @@ export type EditorProvidersProps = {
rootDocId?: string
imageName?: string
compiler?: ProjectCompiler
png2pdf?: boolean
socket?: Socket
isRestrictedTokenMember?: boolean
scope?: Record<string, any>
@@ -164,6 +165,8 @@ export function EditorProviders({
rootDocId = projectDefaults.rootDocId,
imageName = projectDefaults.imageName,
compiler = projectDefaults.compiler,
// left unset by default so the "never chosen" path is the one under test
png2pdf,
socket = new SocketIOMock() as any as Socket,
isRestrictedTokenMember = false,
scope: defaultScope = {},
@@ -231,6 +234,7 @@ export function EditorProviders({
rootFolder,
imageName,
compiler,
png2pdf,
members: [],
invites: [],
trackChangesState: {} as TrackChangesStateData,
@@ -730,7 +734,6 @@ const BASE_COMPILE_CONTEXT_MOCK = {
rawLog: undefined,
recompileFromScratch: () => {},
setAnimateCompileDropdownArrow: () => {},
setPng2pdf: () => {},
setHasLintingError: () => {},
setHighlights: () => {},
setPosition: () => {},