فهرست منبع

Merge pull request #28441 from overleaf/dp-4-way-test

Initial setup for 4 way split test

GitOrigin-RevId: c15496111b32e1b9bf0b7798d9d8ff79d229b1c8
Mathias Jakobsen 1 سال پیش
والد
کامیت
959823515a

+ 5 - 0
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": "",

+ 26 - 8
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<string | null>('all')
 
   const entries = useMemo(() => {
@@ -108,6 +119,13 @@ function ErrorLogs() {
               showSourceLocationLink={false}
             />
           )}
+
+          {includeActionButtons && (
+            <div className="logs-pane-actions">
+              <PdfClearCacheButton />
+              <PdfDownloadFilesButton />
+            </div>
+          )}
         </div>
       </TabContent>
     </TabContainer>

+ 19 - 0
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 (
+    <div
+      className={classnames('new-logs-pane', {
+        hidden: !showLogs && !loadingError,
+      })}
+    >
+      <ErrorLogs includeActionButtons />
+    </div>
+  )
+}

+ 3 - 3
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() {
     >
       <div className="toolbar-pdf-left">
         <PdfCompileButton />
-        {!newErrorLogs && <PdfHybridLogsButton />}
+        {!newErrorLogsPosition && <PdfHybridLogsButton />}
         <PdfHybridDownloadButton />
       </div>
       <div className="toolbar-pdf-right">

+ 10 - 4
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: <ErrorLogsPanel />,
         indicator: <ErrorIndicator />,
         disabled: errorLogsDisabled,
-        hide: !newErrorlogs,
+        hide: !newErrorlogsPosition,
       },
     ],
-    [t, features.trackChangesVisible, newErrorlogs, errorLogsDisabled, view]
+    [
+      t,
+      features.trackChangesVisible,
+      newErrorlogsPosition,
+      errorLogsDisabled,
+      view,
+    ]
   )
 
   const railActions: RailAction[] = useMemo(

+ 20 - 2
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()

+ 11 - 1
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() {
             <PdfViewer />
           </div>
         </Suspense>
-        {newErrorLogs ? <PdfErrorState /> : <PdfLogsViewer />}
+        {newErrorLogsPosition ? (
+          <PdfErrorState />
+        ) : newErrorLogs ? (
+          <NewPdfLogsViewer />
+        ) : (
+          <PdfLogsViewer />
+        )}
         {pdfPromotions.map(({ import: { default: Component }, path }) => (
           <Component key={path} />
         ))}

+ 97 - 78
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
-
-      setShowLogs(true)
-
-      if (pdfLayout === 'flat') {
-        setView('pdf')
-      }
-
-      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<HTMLButtonElement>(
-              'button[data-action="assistant-paywall-show"]'
-            )
-
-            if (paywall) {
-              paywall.scrollIntoView({
-                block: 'start',
-                inline: 'nearest',
-              })
-              paywall.click()
-            } else {
-              element
-                .querySelector<HTMLButtonElement>(
-                  'button[data-action="suggest-fix"]'
-                )
-                ?.click()
-            }
+  const selectLogOldLogs = useCallback((id: string, suggestFix: boolean) => {
+    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<HTMLButtonElement>(
+            'button[data-action="assistant-paywall-show"]'
+          )
+
+          if (paywall) {
+            paywall.scrollIntoView({
+              block: 'start',
+              inline: 'nearest',
+            })
+            paywall.click()
+          } else {
+            element
+              .querySelector<HTMLButtonElement>(
+                '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
+  )
 }

+ 4 - 4
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({
           />{' '}
         </p>
         <p>
-          {newLogs
+          {newLogsPosition
             ? t('log_entry_maximum_entries_see_full_logs_new')
             : t('log_entry_maximum_entries_see_full_logs')}
         </p>
@@ -92,7 +92,7 @@ function PreviewLogsPaneMaxEntriesContent({
       <MaterialIcon type="lightbulb" className="align-middle" />
       &nbsp;
       <strong>{t('tip')}: </strong>
-      {newLogs
+      {newLogsPosition
         ? t('log_entry_maximum_entries_see_full_logs_new')
         : t('log_entry_maximum_entries_see_full_logs')}
     </p>

+ 19 - 10
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;

+ 3 - 0
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",