diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index dbc74c8a56..9b216ad82e 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", 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 98b4c07fe4..ee89f5eba5 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,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" /> ) } diff --git a/services/web/frontend/js/features/ide-settings/components/compiler-settings/optimize-compile-setting.tsx b/services/web/frontend/js/features/ide-settings/components/compiler-settings/optimize-compile-setting.tsx new file mode 100644 index 0000000000..b417525466 --- /dev/null +++ b/services/web/frontend/js/features/ide-settings/components/compiler-settings/optimize-compile-setting.tsx @@ -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 ? ( + + ) : null +} diff --git a/services/web/frontend/js/features/ide-settings/context/settings-modal-context.tsx b/services/web/frontend/js/features/ide-settings/context/settings-modal-context.tsx index 0b9bff7044..e490ba7392 100644 --- a/services/web/frontend/js/features/ide-settings/context/settings-modal-context.tsx +++ b/services/web/frontend/js/features/ide-settings/context/settings-modal-context.tsx @@ -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 = ({ key: 'autoCompile', component: , }, + { + key: 'optimizeCompiles', + component: , + }, ], }, ], 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 36bdc9e9df..ede4d98a2f 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,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() { setCompileMode('normal')} - trailingIcon={!draft && !png2pdf ? 'check' : null} + trailingIcon={!draft ? 'check' : null} > {t('normal')} - {png2pdfEnabled && ( -
  • - setCompileMode('png2pdf')} - trailingIcon={png2pdf ? 'check' : null} - > - {t('fast')}  - [optimize images] - -
  • - )}
  • = ({ setAnimateCompileDropdownArrow: _setAnimateCompileDropdownArrow, setAutoCompile: _setAutoCompile, setDraft: _setDraft, - setPng2pdf: _setPng2pdf, setError: _setError, setHasLintingError: _setHasLintingError, setHighlights: _setHighlights, @@ -268,12 +267,6 @@ export const DetachCompileProvider: FC = ({ 'detached', 'detacher' ) - const setPng2pdf = useDetachAction( - 'setPng2pdf', - _setPng2pdf, - 'detached', - 'detacher' - ) const setError = useDetachAction( 'setError', _setError, @@ -454,7 +447,6 @@ export const DetachCompileProvider: FC = ({ setAutoCompile, setCompiling, setDraft, - setPng2pdf, setError, setHasLintingError, setHighlights, @@ -513,7 +505,6 @@ export const DetachCompileProvider: FC = ({ setAutoCompile, setCompiling, setDraft, - setPng2pdf, setError, setHasLintingError, setHighlights, 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 ca4854cb78..a5f527b017 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -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 = ({ // 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 = ({ 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 = ({ setAutoCompile, setCompiling, setDraft, - setPng2pdf, setError, setHasLintingError, // only for stories setHighlights, @@ -877,7 +856,6 @@ export const LocalCompileProvider: FC = ({ setAnimateCompileDropdownArrow, setAutoCompile, setDraft, - setPng2pdf, setError, setHasLintingError, // only for stories setHighlights, diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 1bb9c3e634..0372e5d064 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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 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 __email__ __appName__ 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", 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 94f9159e42..100d1f0fe0 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 @@ -39,111 +39,12 @@ describe('', 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( - - - - - - ) - - 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( - - - - - - ) - - 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. + 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( diff --git a/services/web/test/frontend/features/ide-settings/settings/optimize-compile-setting.test.tsx b/services/web/test/frontend/features/ide-settings/settings/optimize-compile-setting.test.tsx new file mode 100644 index 0000000000..1c85a6083e --- /dev/null +++ b/services/web/test/frontend/features/ide-settings/settings/optimize-compile-setting.test.tsx @@ -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('', function () { + afterEach(function () { + window.metaAttributesCache.delete('ol-canUsePng2Pdf') + localStorage.clear() + fetchMock.removeRoutes().clearHistory() + }) + + function renderSetting( + png2pdf?: boolean, + permissionsLevel: PermissionsLevel = 'owner' + ) { + render( + + + + + + ) + } + + 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 + }) +}) diff --git a/services/web/test/frontend/helpers/editor-providers.tsx b/services/web/test/frontend/helpers/editor-providers.tsx index 81026a5dcb..2153164790 100644 --- a/services/web/test/frontend/helpers/editor-providers.tsx +++ b/services/web/test/frontend/helpers/editor-providers.tsx @@ -84,6 +84,7 @@ export type EditorProvidersProps = { rootDocId?: string imageName?: string compiler?: ProjectCompiler + png2pdf?: boolean socket?: Socket isRestrictedTokenMember?: boolean scope?: Record @@ -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: () => {},