Эх сурвалжийг харах

[web] Migrate IDE page loading screen to BS5 (#20896)

* [web] Add `.loading-screen` style

* [web] Add `.loading-screen-error` style

* [web] Nest styles in `.loading-screen`

* [web] Simplify code, make a more valuable Storybook

* [web] Add a reusable bootstrap-switcher argument to loading.stories.tsx

* [web] Make `isBootstrap5()` work in storybook

* [web] Revert unrelated changes around `ConnectionError` type

* [web] Remove comment about unhandled error codes

https://github.com/overleaf/internal/pull/20896/files#r1790572314

* [web] Don't repeat the `errorCode` prop type

* [web] Remove unused CSS and magic padding

* [web] Fixup SCSS division

* [storybook] Revert Storybook changes (moved to another branch)

* [web] Fixup SCSS division again (lint)

* [web] Render with `Boolean(errorCode) && ...` instead of `errorCode && ...`

* [web] Remove importants; use spacing var

Addresses Tim's comments

GitOrigin-RevId: e8b5623f4bb9aa72a255851f46b45b652a0dbb16
Antoine Clausse 1 жил өмнө
parent
commit
2e080a3a34

+ 16 - 18
services/web/frontend/js/features/ide-react/components/loading-error.tsx

@@ -2,25 +2,23 @@ import { FC } from 'react'
 import { ConnectionError } from '@/features/ide-react/connection/types/connection-state'
 import getMeta from '@/utils/meta'
 
-// NOTE: i18n translations might not be loaded in the client at this point,
-// so these translations have to be loaded from meta tags
-export const LoadingError: FC<{
-  connectionStateError: ConnectionError | ''
-  i18nError?: Error
-}> = ({ connectionStateError, i18nError }) => {
-  if (connectionStateError) {
-    switch (connectionStateError) {
-      case 'io-not-loaded':
-        return <>{getMeta('ol-translationIoNotLoaded')}</>
+const errorMessages = {
+  'io-not-loaded': 'ol-translationIoNotLoaded',
+  'unable-to-join': 'ol-translationUnableToJoin',
+  'i18n-error': 'ol-translationLoadErrorMessage',
+} as const
 
-      case 'unable-to-join':
-        return <>{getMeta('ol-translationUnableToJoin')}</>
-    }
-  }
+const isHandledCode = (key: string): key is keyof typeof errorMessages =>
+  key in errorMessages
 
-  if (i18nError) {
-    return <>{getMeta('ol-translationLoadErrorMessage')}</>
-  }
+export type LoadingErrorProps = {
+  errorCode: ConnectionError | 'i18n-error' | ''
+}
 
-  return null
+// NOTE: i18n translations might not be loaded in the client at this point,
+// so these translations have to be loaded from meta tags
+export const LoadingError: FC<LoadingErrorProps> = ({ errorCode }) => {
+  return isHandledCode(errorCode) ? (
+    <p className="loading-screen-error">{getMeta(errorMessages[errorCode])}</p>
+  ) : null
 }

+ 18 - 11
services/web/frontend/js/features/ide-react/components/loading.tsx

@@ -4,7 +4,7 @@ import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
 import getMeta from '@/utils/meta'
 import { useConnectionContext } from '../context/connection-context'
 import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
-import { LoadingError } from './loading-error'
+import { LoadingError, LoadingErrorProps } from './loading-error'
 
 type Part = 'initial' | 'render' | 'connection' | 'translations' | 'project'
 
@@ -58,23 +58,30 @@ export const Loading: FC<{
   // Use loading text from the server, because i18n will not be ready initially
   const label = getMeta('ol-loadingText')
 
-  const hasError = Boolean(connectionState.error || i18n.error)
+  const errorCode = connectionState.error ?? (i18n.error ? 'i18n-error' : '')
 
+  return <LoadingUI progress={progress} label={label} errorCode={errorCode} />
+}
+
+type LoadingUiProps = {
+  progress: number
+  label: string
+  errorCode: LoadingErrorProps['errorCode']
+}
+
+export const LoadingUI: FC<LoadingUiProps> = ({
+  progress,
+  label,
+  errorCode,
+}) => {
   return (
     <div className="loading-screen">
       <LoadingBranded
         loadProgress={progress}
         label={label}
-        hasError={hasError}
+        hasError={Boolean(errorCode)}
       />
-      {hasError && (
-        <p className="loading-screen-error">
-          <LoadingError
-            connectionStateError={connectionState.error}
-            i18nError={i18n.error}
-          />
-        </p>
-      )}
+      {Boolean(errorCode) && <LoadingError errorCode={errorCode} />}
     </div>
   )
 }

+ 0 - 33
services/web/frontend/stories/loading/loading-error.stories.tsx

@@ -1,33 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react'
-import { LoadingError } from '@/features/ide-react/components/loading-error'
-
-const meta: Meta<typeof LoadingError> = {
-  title: 'Loading Page / Loading Error',
-  component: LoadingError,
-}
-
-export default meta
-
-type Story = StoryObj<typeof LoadingError>
-
-export const IoNotLoaded: Story = {
-  render: () => {
-    window.metaAttributesCache.set(
-      'ol-translationIoNotLoaded',
-      'Could not connect to the WebSocket server'
-    )
-
-    return <LoadingError connectionStateError="io-not-loaded" />
-  },
-}
-
-export const UnableToJoin: Story = {
-  render: () => {
-    window.metaAttributesCache.set(
-      'ol-translationUnableToJoin',
-      'Could not connect to the collaboration server'
-    )
-
-    return <LoadingError connectionStateError="unable-to-join" />
-  },
-}

+ 31 - 10
services/web/frontend/stories/loading/loading.stories.tsx

@@ -1,25 +1,46 @@
 import type { Meta, StoryObj } from '@storybook/react'
-import { Loading } from '@/features/ide-react/components/loading'
+import { LoadingUI } from '@/features/ide-react/components/loading'
 import { EditorProviders } from '../../../test/frontend/helpers/editor-providers'
 import { bsVersionDecorator } from '../../../.storybook/utils/with-bootstrap-switcher'
 
-const meta: Meta<typeof Loading> = {
+const meta: Meta<typeof LoadingUI> = {
   title: 'Loading Page / Loading',
-  component: Loading,
+  component: LoadingUI,
   argTypes: {
-    setLoaded: { action: 'setLoaded' },
+    errorCode: {
+      control: 'select',
+      options: [
+        '',
+        'io-not-loaded',
+        'unable-to-join',
+        'i18n-error',
+        'unhandled-error-code',
+      ],
+    },
+    progress: { control: { type: 'range', min: 0, max: 100 } },
     ...bsVersionDecorator.argTypes,
   },
 }
 
 export default meta
 
-type Story = StoryObj<typeof Loading>
+type Story = StoryObj<typeof LoadingUI>
+
+const errorMessages = {
+  translationIoNotLoaded: 'Could not connect to the WebSocket server',
+  translationLoadErrorMessage: 'Could not load translations',
+  translationUnableToJoin: 'Could not connect to collaboration server',
+}
 
 export const LoadingPage: Story = {
-  render: args => (
-    <EditorProviders>
-      <Loading {...args} />
-    </EditorProviders>
-  ),
+  render: args => {
+    for (const [key, value] of Object.entries(errorMessages)) {
+      window.metaAttributesCache.set(`ol-${key}`, value)
+    }
+    return (
+      <EditorProviders>
+        <LoadingUI {...args} />
+      </EditorProviders>
+    )
+  },
 }

+ 52 - 37
services/web/frontend/stylesheets/bootstrap-5/pages/editor/loading-screen.scss

@@ -1,3 +1,5 @@
+@use 'sass:math';
+
 @keyframes blink {
   0% {
     opacity: 0.2;
@@ -12,49 +14,62 @@
   }
 }
 
-.loading-screen-brand-container {
-  width: 15%;
-  min-width: 200px;
-  text-align: center;
-}
-
-.loading-screen-brand {
-  position: relative;
+.loading-screen {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
   width: 100%;
-  padding-top: 115.44%;
-  height: 0;
-  background: url(../../../../../public/img/ol-brand/overleaf-o-grey.svg)
-    no-repeat bottom / 100%;
-
-  &::after {
-    content: '';
-    position: absolute;
-    height: inherit;
-    right: 0;
-    bottom: 0;
-    left: 0;
-    background: url(../../../../../public/img/ol-brand/overleaf-o.svg) no-repeat
-      bottom / 100%;
-    transition: height 0.5s;
+  height: 100%;
+  background-color: #fff;
+
+  .loading-screen-brand-container {
+    min-width: 200px;
   }
-}
 
-.loading-screen-label {
-  margin: 0 !important;
-  padding-top: var(--spacing-09);
-  font-family: $font-family-serif;
-  font-size: var(--font-size-07) !important;
-  color: var(--content-secondary);
-}
+  .loading-screen-brand {
+    position: relative;
+    padding-top: math.percentage(math.div(150, 130)); // dimensions of the SVG
+    height: 0;
+    background: url(../../../../../public/img/ol-brand/overleaf-o-grey.svg)
+      no-repeat bottom / 100%;
+
+    &::after {
+      content: '';
+      position: absolute;
+      height: inherit;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      background: url(../../../../../public/img/ol-brand/overleaf-o.svg)
+        no-repeat bottom / 100%;
+      transition: height 0.5s;
+    }
+  }
+
+  .loading-screen-label {
+    margin: 0;
+    padding-top: var(--spacing-09);
+    font-family: $font-family-serif;
+    font-size: var(--font-size-07);
+    color: var(--content-secondary);
+  }
+
+  .loading-screen-ellip {
+    animation: blink 1.4s both infinite;
 
-.loading-screen-ellip {
-  animation: blink 1.4s both infinite;
+    &:nth-child(2) {
+      animation-delay: 0.2s;
+    }
 
-  &:nth-child(2) {
-    animation-delay: 0.2s;
+    &:nth-child(3) {
+      animation-delay: 0.4s;
+    }
   }
 
-  &:nth-child(3) {
-    animation-delay: 0.4s;
+  .loading-screen-error {
+    margin: 0;
+    padding-top: var(--spacing-06);
+    color: var(--content-danger);
   }
 }