diff --git a/services/web/.storybook/utils/with-split-tests.tsx b/services/web/.storybook/utils/with-split-tests.tsx index 0a7ee0bd72..0d85afc9a6 100644 --- a/services/web/.storybook/utils/with-split-tests.tsx +++ b/services/web/.storybook/utils/with-split-tests.tsx @@ -5,6 +5,13 @@ import { SplitTestContext } from '../../frontend/js/shared/context/split-test-co export const splitTestsArgTypes = { // to be able to use this utility, you need to add the argTypes for each split test in this object // Check the original implementation for an example: https://github.com/overleaf/internal/pull/17809 + 'editor-redesign': { + description: 'Enable the new editor redesign', + control: { + type: 'select' as const, + }, + options: ['enabled'], + }, } export const withSplitTests = ( diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index b0e1bd9cef..c7374f8b74 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -1342,9 +1342,11 @@ "project_search_result_count": "", "project_search_result_count_plural": "", "project_synchronisation": "", + "project_timed_out_common_causes": "", "project_timed_out_enable_stop_on_first_error": "", "project_timed_out_fatal_error": "", "project_timed_out_intro": "", + "project_timed_out_intro_short": "", "project_timed_out_learn_more": "", "project_timed_out_optimize_images": "", "project_title_options": "", diff --git a/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx b/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx index 680075ffef..4f2651e2df 100644 --- a/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx @@ -1,28 +1,61 @@ import OLButton from '@/features/ui/components/ol/ol-button' import MaterialIcon from '@/shared/components/material-icon' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { useRailContext } from '../../contexts/rail-context' import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider' import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context' import { useIsNewEditorEnabled } from '../../utils/new-editor-utils' import { upgradePlan } from '@/main/account-upgrade' import classNames from 'classnames' +import { useStopOnFirstError } from '@/shared/hooks/use-stop-on-first-error' +import { useCallback } from 'react' +// AvailableStates +// - rendering-error-expected +// - rendering-error +// - clsi-maintenance +// - clsi-unavailable +// - too-recently-compiled +// - terminated +// - rate-limited +// - compile-in-progress +// - autocompile-disabled +// - project-too-large +// - timedout +// - failure +// - clear-cache +// - pdf-viewer-loading-error +// - validation-problems function PdfErrorState() { const { loadingError } = usePdfPreviewContext() // TODO ide-redesign-cleanup: rename showLogs to something else and check usages const { hasShortCompileTimeout, error, showLogs } = useCompileContext() const newEditor = useIsNewEditorEnabled() + const { t } = useTranslation() if (!newEditor || (!loadingError && !showLogs)) { return null } - if (hasShortCompileTimeout && error === 'timedout') { - return + switch (error) { + case 'timedout': { + if (hasShortCompileTimeout) { + return + } else { + return + } + } + case 'compile-in-progress': + return ( + + ) + default: + return } - - return } const GeneralErrorState = () => { @@ -34,7 +67,6 @@ const GeneralErrorState = () => { title={t('pdf_couldnt_compile')} description={t('we_are_unable_to_generate_the_pdf_at_this_time')} iconType="warning" - iconClassName="pdf-error-state-warning-icon" actions={ { {t('why_might_this_happen')} -
    -
  • {t('there_is_an_unrecoverable_latex_error_check_logs')}
  • -
  • {t('the_document_environment_contains_no_content')}
  • -
  • {t('this_project_contains_a_file_called_output')}
  • -
+
+
    +
  • {t('there_is_an_unrecoverable_latex_error_check_logs')}
  • +
  • {t('the_document_environment_contains_no_content')}
  • +
  • {t('this_project_contains_a_file_called_output')}
  • +
+
} /> @@ -84,6 +118,85 @@ const CompileTimeoutErrorState = () => { ) } +const LongCompileTimeoutErrorState = () => { + const { t } = useTranslation() + + const { enableStopOnFirstError } = useStopOnFirstError({ + eventSource: 'timeout', + }) + const { startCompile, lastCompileOptions, setAnimateCompileDropdownArrow } = + useCompileContext() + + const handleEnableStopOnFirstErrorClick = useCallback(() => { + enableStopOnFirstError() + startCompile({ stopOnFirstError: true }) + setAnimateCompileDropdownArrow(true) + }, [enableStopOnFirstError, startCompile, setAnimateCompileDropdownArrow]) + + return ( + +
+ + {t('project_timed_out_common_causes')} +
+
+
    +
  • + , + ]} + /> +
  • +
  • + , + ]} + /> + {!lastCompileOptions.stopOnFirstError && ( + <> + {' '} + , + ]} + /> + + )} +
  • +
+

+ , + ]} + /> +

+
+ + } + /> + ) +} + const ErrorState = ({ title, description, @@ -95,14 +208,20 @@ const ErrorState = ({ title: string description: string iconType: string - actions: React.ReactNode + actions?: React.ReactNode iconClassName?: string extraContent?: React.ReactNode }) => { return (
-
+
diff --git a/services/web/frontend/stories/decorators/scope.tsx b/services/web/frontend/stories/decorators/scope.tsx index 9bde1eebfa..85785743d0 100644 --- a/services/web/frontend/stories/decorators/scope.tsx +++ b/services/web/frontend/stories/decorators/scope.tsx @@ -21,7 +21,7 @@ import { ReactContextRoot } from '@/features/ide-react/context/react-context-roo const scopeWatchers: [string, (value: any) => void][] = [] -const user: User = { +export const user: User = { id: 'story-user' as UserId, email: 'story-user@example.com', allowedFreeTrial: true, diff --git a/services/web/frontend/stories/pdf-error-state.stories.tsx b/services/web/frontend/stories/pdf-error-state.stories.tsx new file mode 100644 index 0000000000..4e61768788 --- /dev/null +++ b/services/web/frontend/stories/pdf-error-state.stories.tsx @@ -0,0 +1,90 @@ +import useFetchMock from './hooks/use-fetch-mock' +import { mockCompile } from './fixtures/compile' +import { ScopeDecorator, user } from './decorators/scope' +import PdfErrorState from '@/features/ide-redesign/components/pdf-preview/pdf-error-state' +import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context' +import { useEffect } from 'react' +import PdfPreview from '@/features/pdf-preview/components/pdf-preview' + +export default { + title: 'Editor / PDF Error States', + component: PdfErrorState, +} + +const compileErrors = [ + 'autocompile-backoff', + 'clear-cache', + 'clsi-maintenance', + 'compile-in-progress', + 'exited', + 'failure', + 'generic', + 'project-too-large', + 'rate-limited', + 'success', + 'terminated', + 'timedout', + 'too-recently-compiled', + 'unavailable', + 'validation-problems', + 'foo', +] as const + +const ErrorPane = ({ error }: { error: (typeof compileErrors)[number] }) => { + window.metaAttributesCache.set('ol-splitTestVariants', { + 'editor-redesign': 'enabled', + }) + useFetchMock(fetchMock => { + mockCompile(fetchMock) + }) + const { setError } = useCompileContext() + useEffect(() => { + setError(error) + }, [setError, error]) + + return ( +
+ {error} + +
+ ) +} + +const story = { + render: (args: { error: (typeof compileErrors)[number] }) => { + return + }, + argTypes: { + error: { + options: compileErrors, + control: { + type: 'select', + }, + }, + }, + args: { + error: compileErrors[0], + }, +} + +export const PremiumUser = { + ...story, + decorators: [ScopeDecorator], +} + +export const FreeUser = { + ...story, + decorators: [ + (Story: any) => + ScopeDecorator( + Story, + { mockCompileOnLoad: false }, + { + 'ol-user': { + ...user, + features: { ...user.features, compileTimeout: 20 }, + }, + } + ), + ], +} diff --git a/services/web/frontend/stylesheets/abstracts/mixins.scss b/services/web/frontend/stylesheets/abstracts/mixins.scss index 1a79a1221c..0b8ebff3e5 100644 --- a/services/web/frontend/stylesheets/abstracts/mixins.scss +++ b/services/web/frontend/stylesheets/abstracts/mixins.scss @@ -156,6 +156,12 @@ --link-visited-color: var(--link-visited-color-dark); } +@mixin themed-links { + --link-color: var(--link-ui-themed); + --link-hover-color: var(--link-ui-hover-themed); + --link-visited-color: var(--link-ui-visited-themed); +} + @mixin triangle($direction, $width, $height, $color) { position: absolute; border-color: transparent; diff --git a/services/web/frontend/stylesheets/pages/editor/pdf-error-state.scss b/services/web/frontend/stylesheets/pages/editor/pdf-error-state.scss index aee036f775..8b1566df0e 100644 --- a/services/web/frontend/stylesheets/pages/editor/pdf-error-state.scss +++ b/services/web/frontend/stylesheets/pages/editor/pdf-error-state.scss @@ -1,19 +1,3 @@ -:root { - --pdf-error-state-info-box-color: var(--content-primary-dark); - --pdf-error-state-info-box-background: var(--bg-dark-primary); - --pdf-error-state-info-box-border: var(--border-divider-dark); - --pdf-error-state-label-color: var(--content-primary-dark); - --pdf-error-state-description-color: var(--content-secondary-dark); -} - -@include theme('light') { - --pdf-error-state-info-box-color: var(--content-primary); - --pdf-error-state-info-box-background: var(--bg-light-primary); - --pdf-error-state-info-box-border: var(--border-divider); - --pdf-error-state-label-color: var(--content-primary); - --pdf-error-state-description-color: var(--content-secondary); -} - .pdf-error-state { position: absolute; inset: var(--toolbar-small-height) 0 0 0; @@ -34,6 +18,7 @@ } .pdf-error-state-icon { + color: var(--content-primary-themed); width: 80px; height: 80px; display: flex; @@ -44,14 +29,14 @@ .material-symbols { font-size: 80px; } -} -.pdf-error-state-warning-icon { - background-color: var(--bg-danger-03); - color: var(--content-danger); + &.pdf-error-state-warning-icon { + background-color: var(--bg-danger-03); + color: var(--content-danger); - .material-symbols { - font-size: 32px; + .material-symbols { + font-size: 32px; + } } } @@ -64,23 +49,25 @@ .pdf-error-state-label { font-size: var(--font-size-02); - color: var(--pdf-error-state-label-color); + color: var(--content-primary-themed); font-weight: 600; margin-bottom: 0; } .pdf-error-state-description { - color: var(--pdf-error-state-description-color); + color: var(--content-secondary-themed); font-size: var(--font-size-02); margin-bottom: 0; } .pdf-error-state-info-box { - background-color: var(--pdf-error-state-info-box-background); - color: var(--pdf-error-state-info-box-color); + background-color: var(--bg-primary-themed); + color: var(--content-primary-themed); padding: var(--spacing-06); - border: 1px solid var(--pdf-error-state-info-box-border); + border: 1px solid var(--border-divider-themed); border-radius: var(--border-radius-base); + + @include themed-links; } .pdf-error-state-info-box-title { @@ -94,8 +81,12 @@ .pdf-error-state-info-box-text { font-size: var(--font-size-02); - display: flex; - flex-direction: column; - gap: var(--spacing-02); - margin-bottom: 0; +} + +.pdf-error-state-info-box-list { + margin-bottom: 0; + + li + li { + margin-top: var(--spacing-02); + } } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 0f694d0572..67801974d7 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1753,9 +1753,11 @@ "project_search_result_count": "__count__ result", "project_search_result_count_plural": "__count__ results", "project_synchronisation": "Project synchronisation", + "project_timed_out_common_causes": "Most common causes of timeouts are:", "project_timed_out_enable_stop_on_first_error": "<0>Enable “Stop on first error” to help you find and fix errors right away.", "project_timed_out_fatal_error": "A <0>fatal compile error may be completely blocking compilation.", "project_timed_out_intro": "Sorry, your compile took too long to run and timed out. The most common causes of timeouts are:", + "project_timed_out_intro_short": "Sorry, your compile took too long to run and timed out", "project_timed_out_learn_more": "<0>Learn more about other causes of compile timeouts and how to fix them.", "project_timed_out_optimize_images": "Large or high-resolution images are taking too long to process. You may be able to <0>optimize them.", "project_too_large": "Project too large",