From 2ad17039f12267bc9e3640e0db4c667a381ac898 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Tue, 16 Sep 2025 12:28:19 +0100 Subject: [PATCH] Merge pull request #28441 from overleaf/dp-4-way-test Initial setup for 4 way split test GitOrigin-RevId: c15496111b32e1b9bf0b7798d9d8ff79d229b1c8 --- .../web/frontend/extracted-translations.json | 5 + .../components/error-logs/error-logs.tsx | 34 +++- .../pdf-preview/pdf-logs-viewer.tsx | 19 ++ .../pdf-preview-hybrid-toolbar.tsx | 6 +- .../ide-redesign/components/rail/rail.tsx | 14 +- .../ide-redesign/utils/new-editor-utils.ts | 22 ++- .../components/pdf-preview-pane.tsx | 12 +- .../pdf-preview/hooks/use-log-events.ts | 167 ++++++++++-------- .../preview-logs-pane-max-entries.tsx | 8 +- .../stylesheets/pages/editor/logs.scss | 29 +-- services/web/locales/en.json | 3 + 11 files changed, 213 insertions(+), 106 deletions(-) create mode 100644 services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-logs-viewer.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 7d882c14dd..b2b53ebac0 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -121,8 +121,10 @@ "ai_feedback_the_suggestion_wasnt_the_best_fix_available": "", "ai_feedback_there_was_no_code_fix_suggested": "", "alignment": "", + "all": "", "all_borders": "", "all_features_in_group_standard_plus": "", + "all_logs": "", "all_premium_features": "", "all_premium_features_including": "", "all_projects": "", @@ -551,6 +553,7 @@ "error_opening_document_detail": "", "error_performing_request": "", "error_processing_file": "", + "errors": "", "essential_cookies_only": "", "example_project": "", "existing_plan_active_until_term_end": "", @@ -833,6 +836,7 @@ "included_as_part_of_your_writefull_subscription": "", "increase_indent": "", "increased_compile_timeout": "", + "info": "", "initiator": "", "inline": "", "inline_math": "", @@ -2091,6 +2095,7 @@ "visual_editor": "", "visual_editor_is_only_available_for_tex_files": "", "want_change_to_apply_before_plan_end": "", + "warnings": "", "we_are_unable_to_opt_you_into_this_experiment": "", "we_cant_find_any_sections_or_subsections_in_this_file": "", "we_do_not_share_personal_information": "", diff --git a/services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs.tsx b/services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs.tsx index be4ba09660..84f19a88fc 100644 --- a/services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs.tsx @@ -14,6 +14,9 @@ import LogEntry from './log-entry' import importOverleafModules from '../../../../../macros/import-overleaf-module.macro' import TimeoutUpgradePromptNew from '@/features/pdf-preview/components/timeout-upgrade-prompt-new' import getMeta from '@/utils/meta' +import PdfClearCacheButton from '@/features/pdf-preview/components/pdf-clear-cache-button' +import PdfDownloadFilesButton from '@/features/pdf-preview/components/pdf-download-files-button' +import { useIsNewErrorLogsPositionEnabled } from '../../utils/new-editor-utils' const logsComponents: Array<{ import: { default: ElementType } @@ -26,24 +29,32 @@ type ErrorLogTab = { entries: LogEntryData[] | undefined } -function ErrorLogs() { +function ErrorLogs({ + includeActionButtons, +}: { + includeActionButtons?: boolean +}) { const { error, logEntries, rawLog, validationIssues, stoppedOnFirstError } = useCompileContext() const { compileTimeout } = getMeta('ol-compileSettings') + const newLogsPosition = useIsNewErrorLogsPositionEnabled() + const { t } = useTranslation() const tabs = useMemo(() => { return [ - { key: 'all', label: 'All', entries: logEntries?.all }, - { key: 'errors', label: 'Errors', entries: logEntries?.errors }, - { key: 'warnings', label: 'Warnings', entries: logEntries?.warnings }, - { key: 'info', label: 'Info', entries: logEntries?.typesetting }, + { + key: 'all', + label: newLogsPosition ? t('all') : t('all_logs'), + entries: logEntries?.all, + }, + { key: 'errors', label: t('errors'), entries: logEntries?.errors }, + { key: 'warnings', label: t('warnings'), entries: logEntries?.warnings }, + { key: 'info', label: t('info'), entries: logEntries?.typesetting }, ] - }, [logEntries]) + }, [logEntries, newLogsPosition, t]) const { loadingError } = usePdfPreviewContext() - const { t } = useTranslation() - const [activeTab, setActiveTab] = useState('all') const entries = useMemo(() => { @@ -108,6 +119,13 @@ function ErrorLogs() { showSourceLocationLink={false} /> )} + + {includeActionButtons && ( +
+ + +
+ )} diff --git a/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-logs-viewer.tsx b/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-logs-viewer.tsx new file mode 100644 index 0000000000..32fa2d9c21 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-logs-viewer.tsx @@ -0,0 +1,19 @@ +import classnames from 'classnames' +import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context' +import ErrorLogs from '../error-logs/error-logs' +import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider' + +export default function PdfLogsViewer() { + const { showLogs } = useCompileContext() + const { loadingError } = usePdfPreviewContext() + + return ( +
+ +
+ ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-preview-hybrid-toolbar.tsx b/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-preview-hybrid-toolbar.tsx index fb02c93d09..3b4c2d5866 100644 --- a/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-preview-hybrid-toolbar.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-preview-hybrid-toolbar.tsx @@ -6,11 +6,11 @@ import PdfHybridDownloadButton from '@/features/pdf-preview/components/pdf-hybri import { DetachedSynctexControl } from '@/features/pdf-preview/components/detach-synctex-control' import SwitchToEditorButton from '@/features/pdf-preview/components/switch-to-editor-button' import PdfHybridLogsButton from '@/features/pdf-preview/components/pdf-hybrid-logs-button' -import { useAreNewErrorLogsEnabled } from '../../utils/new-editor-utils' +import { useIsNewErrorLogsPositionEnabled } from '../../utils/new-editor-utils' function PdfPreviewHybridToolbar() { const { t } = useTranslation() - const newErrorLogs = useAreNewErrorLogsEnabled() + const newErrorLogsPosition = useIsNewErrorLogsPositionEnabled() // TODO: add detached pdf logic return ( @@ -20,7 +20,7 @@ function PdfPreviewHybridToolbar() { >
- {!newErrorLogs && } + {!newErrorLogsPosition && }
diff --git a/services/web/frontend/js/features/ide-redesign/components/rail/rail.tsx b/services/web/frontend/js/features/ide-redesign/components/rail/rail.tsx index 7f9170cefb..a5d43cfd88 100644 --- a/services/web/frontend/js/features/ide-redesign/components/rail/rail.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/rail/rail.tsx @@ -29,7 +29,7 @@ import RailResizeHandle from './rail-resize-handle' import RailModals from './rail-modals' import RailOverflowDropdown from './rail-overflow-dropdown' import useRailOverflow from '../../hooks/use-rail-overflow' -import { useAreNewErrorLogsEnabled } from '../../utils/new-editor-utils' +import { useIsNewErrorLogsPositionEnabled } from '../../utils/new-editor-utils' export const RailLayout = () => { const { sendEvent } = useEditorAnalytics() @@ -45,7 +45,7 @@ export const RailLayout = () => { const isHistoryView = view === 'history' - const newErrorlogs = useAreNewErrorLogsEnabled() + const newErrorlogsPosition = useIsNewErrorLogsPositionEnabled() const railTabs: RailElement[] = useMemo( () => [ @@ -94,10 +94,16 @@ export const RailLayout = () => { component: , indicator: , disabled: errorLogsDisabled, - hide: !newErrorlogs, + hide: !newErrorlogsPosition, }, ], - [t, features.trackChangesVisible, newErrorlogs, errorLogsDisabled, view] + [ + t, + features.trackChangesVisible, + newErrorlogsPosition, + errorLogsDisabled, + view, + ] ) const railActions: RailAction[] = useMemo( diff --git a/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts b/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts index ac43a1ea27..9a89d795ac 100644 --- a/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts +++ b/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts @@ -25,7 +25,8 @@ export const canUseNewEditorViaNewUserFeatureFlag = () => { !canUseNewEditorViaPrimaryFeatureFlag() && isNewUser() && (newUserTestVariant === 'new-editor' || - newUserTestVariant === 'new-editor-old-logs') + newUserTestVariant === 'new-editor-old-logs' || + newUserTestVariant === 'new-editor-new-logs-old-position') ) } @@ -36,7 +37,7 @@ export const canUseNewEditor = () => { ) } -const canUseNewLogs = () => { +const canUseNewLogsPosition = () => { const newUserTestVariant = getSplitTestVariant('editor-redesign-new-users') const canUseNewLogsViaNewUserFeatureFlag = isNewUser() && newUserTestVariant === 'new-editor' @@ -46,6 +47,18 @@ const canUseNewLogs = () => { ) } +const canUseNewLogs = () => { + const newUserTestVariant = getSplitTestVariant('editor-redesign-new-users') + const canUseNewLogsViaNewUserFeatureFlag = + isNewUser() && + (newUserTestVariant === 'new-editor' || + newUserTestVariant === 'new-editor-new-logs-old-position') + + return ( + canUseNewEditorViaPrimaryFeatureFlag() || canUseNewLogsViaNewUserFeatureFlag + ) +} + export const useIsNewEditorEnabledViaPrimaryFeatureFlag = () => { const { userSettings } = useUserSettingsContext() const hasAccess = canUseNewEditorViaPrimaryFeatureFlag() @@ -60,6 +73,11 @@ export const useIsNewEditorEnabled = () => { return hasAccess && enabled } +export const useIsNewErrorLogsPositionEnabled = () => { + const newEditorEnabled = useIsNewEditorEnabled() + return newEditorEnabled && canUseNewLogsPosition() +} + export const useAreNewErrorLogsEnabled = () => { const newEditorEnabled = useIsNewEditorEnabled() return newEditorEnabled && canUseNewLogs() diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx index 00bdade03f..90f368960b 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx @@ -13,10 +13,12 @@ import PdfErrorState from '@/features/ide-redesign/components/pdf-preview/pdf-er import { useAreNewErrorLogsEnabled, useIsNewEditorEnabled, + useIsNewErrorLogsPositionEnabled, } from '@/features/ide-redesign/utils/new-editor-utils' import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import PdfCodeCheckFailedBanner from '@/features/ide-redesign/components/pdf-preview/pdf-code-check-failed-banner' import getMeta from '@/utils/meta' +import NewPdfLogsViewer from '@/features/ide-redesign/components/pdf-preview/pdf-logs-viewer' function PdfPreviewPane() { const { pdfUrl } = useCompileContext() @@ -26,6 +28,8 @@ function PdfPreviewPane() { }) const newEditor = useIsNewEditorEnabled() const newErrorLogs = useAreNewErrorLogsEnabled() + const newErrorLogsPosition = useIsNewErrorLogsPositionEnabled() + const pdfPromotions = importOverleafModules('pdfPreviewPromotions') as { import: { default: ElementType } path: string @@ -48,7 +52,13 @@ function PdfPreviewPane() {
- {newErrorLogs ? : } + {newErrorLogsPosition ? ( + + ) : newErrorLogs ? ( + + ) : ( + + )} {pdfPromotions.map(({ import: { default: Component }, path }) => ( ))} diff --git a/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts b/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts index 193c6c8c4f..ea64f1527c 100644 --- a/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts +++ b/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts @@ -1,79 +1,65 @@ -import { useCallback, useEffect } from 'react' +import { useCallback } from 'react' import { useLayoutContext } from '@/shared/context/layout-context' -import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils' +import { + useAreNewErrorLogsEnabled, + useIsNewErrorLogsPositionEnabled, +} from '@/features/ide-redesign/utils/new-editor-utils' import { useRailContext } from '@/features/ide-redesign/contexts/rail-context' import { useEditorContext } from '@/shared/context/editor-context' +import useEventListener from '@/shared/hooks/use-event-listener' /** * This hook adds an event listener for events dispatched from the editor to the compile logs pane */ export const useLogEvents = (setShowLogs: (show: boolean) => void) => { const { pdfLayout, setView } = useLayoutContext() - const newEditor = useIsNewEditorEnabled() + const newLogs = useAreNewErrorLogsEnabled() + const newLogsPosition = useIsNewErrorLogsPositionEnabled() const { openTab: openRailTab } = useRailContext() const { hasPremiumSuggestion } = useEditorContext() - const handleViewCompileLogEntryEventOldEditor = useCallback( - (event: Event) => { - const { id, suggestFix } = ( - event as CustomEvent<{ id: string; suggestFix?: boolean }> - ).detail + const selectLogOldLogs = useCallback((id: string, suggestFix: boolean) => { + window.setTimeout(() => { + const element = document.querySelector( + `.log-entry[data-log-entry-id="${id}"]` + ) - setShowLogs(true) + if (element) { + element.scrollIntoView({ + block: 'start', + inline: 'nearest', + }) - if (pdfLayout === 'flat') { - setView('pdf') - } + if (suggestFix) { + // if they are paywalled, click that instead + const paywall = document.querySelector( + 'button[data-action="assistant-paywall-show"]' + ) - window.setTimeout(() => { - const element = document.querySelector( - `.log-entry[data-log-entry-id="${id}"]` - ) - - if (element) { - element.scrollIntoView({ - block: 'start', - inline: 'nearest', - }) - - if (suggestFix) { - // if they are paywalled, click that instead - const paywall = document.querySelector( - 'button[data-action="assistant-paywall-show"]' - ) - - if (paywall) { - paywall.scrollIntoView({ - block: 'start', - inline: 'nearest', - }) - paywall.click() - } else { - element - .querySelector( - 'button[data-action="suggest-fix"]' - ) - ?.click() - } + if (paywall) { + paywall.scrollIntoView({ + block: 'start', + inline: 'nearest', + }) + paywall.click() + } else { + element + .querySelector( + 'button[data-action="suggest-fix"]' + ) + ?.click() } } - }) - }, - [pdfLayout, setView, setShowLogs] - ) - - const handleViewCompileLogEntryEventNewEditor = useCallback( - (event: Event) => { - const { id, suggestFix, showPaywallIfOutOfSuggestions } = ( - event as CustomEvent<{ - id: string - suggestFix?: boolean - showPaywallIfOutOfSuggestions?: boolean - }> - ).detail - - openRailTab('errors') + } + }) + }, []) + const selectLogNewLogs = useCallback( + ( + id: string, + suggestFix: boolean, + showPaywallIfOutOfSuggestions: boolean + ) => { window.setTimeout(() => { const logEntry = document.querySelector( `.log-entry[data-log-entry-id="${id}"]` @@ -114,26 +100,59 @@ export const useLogEvents = (setShowLogs: (show: boolean) => void) => { } }) }, - [openRailTab, hasPremiumSuggestion] + [hasPremiumSuggestion] ) - useEffect(() => { - const listener = (event: Event) => { - if (newEditor) { - handleViewCompileLogEntryEventNewEditor(event) + const openNewLogsPosition = useCallback(() => { + openRailTab('errors') + }, [openRailTab]) + + const openOldLogsPosition = useCallback(() => { + setShowLogs(true) + + if (pdfLayout === 'flat') { + setView('pdf') + } + }, [pdfLayout, setView, setShowLogs]) + + const handleViewCompileLogEntryEvent = useCallback( + (event: Event) => { + const { id, suggestFix, showPaywallIfOutOfSuggestions } = ( + event as CustomEvent<{ + id: string + suggestFix?: boolean + showPaywallIfOutOfSuggestions?: boolean + }> + ).detail + + if (newLogsPosition) { + openNewLogsPosition() } else { - handleViewCompileLogEntryEventOldEditor(event) + openOldLogsPosition() } - } - window.addEventListener('editor:view-compile-log-entry', listener) + if (newLogs) { + selectLogNewLogs( + id, + Boolean(suggestFix), + Boolean(showPaywallIfOutOfSuggestions) + ) + } else { + selectLogOldLogs(id, Boolean(suggestFix)) + } + }, + [ + openNewLogsPosition, + openOldLogsPosition, + selectLogNewLogs, + selectLogOldLogs, + newLogs, + newLogsPosition, + ] + ) - return () => { - window.removeEventListener('editor:view-compile-log-entry', listener) - } - }, [ - handleViewCompileLogEntryEventNewEditor, - handleViewCompileLogEntryEventOldEditor, - newEditor, - ]) + useEventListener( + 'editor:view-compile-log-entry', + handleViewCompileLogEntryEvent + ) } diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.tsx b/services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.tsx index b1f43ed3e9..a80c89e494 100644 --- a/services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.tsx +++ b/services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.tsx @@ -5,7 +5,7 @@ import { useDetachCompileContext as useCompileContext } from '../../../shared/co import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error' import MaterialIcon from '@/shared/components/material-icon' import PdfLogEntry from '@/features/pdf-preview/components/pdf-log-entry' -import { useAreNewErrorLogsEnabled } from '@/features/ide-redesign/utils/new-editor-utils' +import { useIsNewErrorLogsPositionEnabled } from '@/features/ide-redesign/utils/new-editor-utils' function PreviewLogsPaneMaxEntries({ totalEntries, @@ -40,7 +40,7 @@ function PreviewLogsPaneMaxEntriesContent({ }: { hasErrors?: boolean }) { - const newLogs = useAreNewErrorLogsEnabled() + const newLogsPosition = useIsNewErrorLogsPositionEnabled() const { t } = useTranslation() const { startCompile, stoppedOnFirstError, setAnimateCompileDropdownArrow } = useCompileContext() @@ -79,7 +79,7 @@ function PreviewLogsPaneMaxEntriesContent({ />{' '}

- {newLogs + {newLogsPosition ? t('log_entry_maximum_entries_see_full_logs_new') : t('log_entry_maximum_entries_see_full_logs')}

@@ -92,7 +92,7 @@ function PreviewLogsPaneMaxEntriesContent({   {t('tip')}: - {newLogs + {newLogsPosition ? t('log_entry_maximum_entries_see_full_logs_new') : t('log_entry_maximum_entries_see_full_logs')}

diff --git a/services/web/frontend/stylesheets/pages/editor/logs.scss b/services/web/frontend/stylesheets/pages/editor/logs.scss index 92b640a042..133e6add3e 100644 --- a/services/web/frontend/stylesheets/pages/editor/logs.scss +++ b/services/web/frontend/stylesheets/pages/editor/logs.scss @@ -177,6 +177,7 @@ .error-logs-tabs { display: flex; + background-color: var(--bg-primary-themed); } .error-logs-tab-header { @@ -204,6 +205,14 @@ } } +.new-logs-pane { + position: absolute; + inset: 0; + z-index: 11; // above the PDF viewer + controls + top: var(--toolbar-small-height); + background-color: var(--bg-secondary-themed); +} + .logs-pane { position: absolute; inset: 0; @@ -220,16 +229,6 @@ min-height: 100%; } - .logs-pane-actions { - display: flex; - flex-wrap: wrap; - place-content: flex-end flex-end; - padding: var(--spacing-03) 0; - flex-grow: 1; - align-items: flex-end; - gap: var(--spacing-04); - } - .log-entry { border-radius: var(--border-radius-base); overflow: hidden; @@ -340,6 +339,16 @@ } } +.logs-pane-actions { + display: flex; + flex-wrap: wrap; + place-content: flex-end flex-end; + padding: var(--spacing-03) 0; + flex-grow: 1; + align-items: flex-end; + gap: var(--spacing-04); +} + .log-entry-content { &:empty { display: none; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index b48af8ec54..4a40e2b7f2 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -149,6 +149,7 @@ "all": "All", "all_borders": "All borders", "all_features_in_group_standard_plus": "All features in Group Standard, plus:", + "all_logs": "All logs", "all_premium_features": "All premium features", "all_premium_features_including": "All premium features, including:", "all_prices_displayed_are_in_currency": "All prices displayed are in __recommendedCurrency__.", @@ -708,6 +709,7 @@ "error_opening_document_detail": "Sorry, something went wrong opening this document. Please try again.", "error_performing_request": "An error has occurred while performing your request.", "error_processing_file": "Sorry, something went wrong processing this file. Please try again.", + "errors": "Errors", "es": "Spanish", "essential_cookies_only": "Essential cookies only", "estimated_number_of_overleaf_users": "Estimated number of __appName__ users", @@ -2641,6 +2643,7 @@ "visual_editor_is_only_available_for_tex_files": "Visual Editor is only available for TeX files", "want_access_to_overleaf_premium_features_through_your_university": "Want access to __appName__ premium features through your university?", "want_change_to_apply_before_plan_end": "If you wish this change to apply before the end of your current billing period, please contact us.", + "warnings": "Warnings", "we_are_unable_to_opt_you_into_this_experiment": "We are unable to opt you into this experiment at this time, please ensure your organization has allowed this feature, or try again later.", "we_cant_confirm_this_email": "We can’t confirm this email", "we_cant_find_any_sections_or_subsections_in_this_file": "We can’t find any sections or subsections in this file",