[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
This commit is contained in:
Antoine Clausse 2024-10-10 09:38:08 +02:00 committed by Copybot
parent b054342ddb
commit 2e080a3a34
5 changed files with 121 additions and 113 deletions

View file

@ -2,25 +2,23 @@ import { FC } from 'react'
import { ConnectionError } from '@/features/ide-react/connection/types/connection-state'
import getMeta from '@/utils/meta'
const errorMessages = {
'io-not-loaded': 'ol-translationIoNotLoaded',
'unable-to-join': 'ol-translationUnableToJoin',
'i18n-error': 'ol-translationLoadErrorMessage',
} as const
const isHandledCode = (key: string): key is keyof typeof errorMessages =>
key in errorMessages
export type LoadingErrorProps = {
errorCode: ConnectionError | 'i18n-error' | ''
}
// 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')}</>
case 'unable-to-join':
return <>{getMeta('ol-translationUnableToJoin')}</>
}
}
if (i18nError) {
return <>{getMeta('ol-translationLoadErrorMessage')}</>
}
return null
export const LoadingError: FC<LoadingErrorProps> = ({ errorCode }) => {
return isHandledCode(errorCode) ? (
<p className="loading-screen-error">{getMeta(errorMessages[errorCode])}</p>
) : null
}

View file

@ -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>
)
}

View file

@ -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" />
},
}

View file

@ -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>
)
},
}

View file

@ -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%;
height: 100%;
background-color: #fff;
&::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 !important;
padding-top: var(--spacing-09);
font-family: $font-family-serif;
font-size: var(--font-size-07) !important;
color: var(--content-secondary);
}
.loading-screen-ellip {
animation: blink 1.4s both infinite;
&:nth-child(2) {
animation-delay: 0.2s;
.loading-screen-brand-container {
min-width: 200px;
}
&:nth-child(3) {
animation-delay: 0.4s;
.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;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
.loading-screen-error {
margin: 0;
padding-top: var(--spacing-06);
color: var(--content-danger);
}
}