diff --git a/front/public/locales/en/stdcm.json b/front/public/locales/en/stdcm.json index c71d841e08a..87f37df24fd 100644 --- a/front/public/locales/en/stdcm.json +++ b/front/public/locales/en/stdcm.json @@ -26,7 +26,6 @@ "indicateAnteriorPath": "Indicate anterior path", "indicatePosteriorPath": "Indicate posterior path", "leaveAt": "Leave at {{ time }}", - "loaderImageLegend": "Morning view of the locomotive shed at Le Bourget depot", "noConfigurationFound": { "title": "A configuration problem prevents you from performing a search", "text": "Please contact the maintenance team so they can get you back on track." @@ -35,9 +34,9 @@ "notificationTitle": "Phase 1: from D-7 to D-1 5pm, on the Perrigny-Miramas axis.", "pleaseWait": "Please wait…", "simulation": { - "averageRequestTime": "For your request, the time required is generally 90 seconds.", "calculatingSimulation": "Calculation in progress...", "getSimulation": "Get the simulation", + "infoMessage": "This simulation doesn't ensure the availability of the path.", "modifySearchCriteria": "You can modify your search criteria to find a solution.", "pendingSimulation": "Simulation in progress", "results": { @@ -67,8 +66,8 @@ "startNewQuery": "Start a new query", "status": { "completed": "Calculation completed", - "errorMessage": "We are sorry that we could not process your request. The STDCM team has been notified and will be aware of the situation soon.", - "failed": "Calculation failed" + "errorMessage": "We are sorry that we could not process your request. The LMR team has been notified and will be aware of the situation as soon as possible.", + "failed": "A technical problem occurred" }, "upgrade": { "arrivalIncompatible": "Arrival time incompatible", @@ -79,7 +78,7 @@ "unqualifiedDriver": "Unqualified driver" } }, - "stopCalculation": "Stop calculation" + "stopCalculation": "Stop" }, "spaceTimeGraphic": "Space-Time graph", "speedSpaceChart": "Speed Space Chart", diff --git a/front/public/locales/fr/stdcm.json b/front/public/locales/fr/stdcm.json index ea79d09d196..c655098cfc2 100644 --- a/front/public/locales/fr/stdcm.json +++ b/front/public/locales/fr/stdcm.json @@ -26,7 +26,6 @@ "indicateAnteriorPath": "Indiquer le sillon antérieur", "indicatePosteriorPath": "Indiquer le sillon postérieur", "leaveAt": "Partir à {{ time }}", - "loaderImageLegend": "Vue matinale du relais de locomotives du dépôt du Bourget", "noConfigurationFound": { "title": "Un problème de configuration vous empêche de faire une recherche", "text": "Veuillez contacter l'équipe de maintenance pour qu'elle puisse vous remettre sur les rails." @@ -35,9 +34,9 @@ "notificationTitle": "Phase 1 : de J-7 à J-1 17h, sur l’axe Perrigny—Miramas.", "pleaseWait": "Veuillez patientez…", "simulation": { - "averageRequestTime": "Pour votre demande, le temps nécessaire est généralement de 90 secondes.", "calculatingSimulation": "Calcul en cours...", "getSimulation": "Obtenir la simulation", + "infoMessage": "Cette simulation n'offre pas la garantie de disponibilité du sillon.", "modifySearchCriteria": "Vous pouvez modifier vos critères de recherche pour trouver une solution.", "pendingSimulation": "simulation en cours", "results": { @@ -67,8 +66,8 @@ "startNewQuery": "Démarrez une nouvelle requête", "status": { "completed": "Calcul terminé", - "errorMessage": "Nous sommes désolé de ne pas avoir pu traiter votre demande. L’équipe STDCM a été notifiée et prendra connaissance de la situation prochainement.", - "failed": "Le calcul n'a pas abouti" + "errorMessage": "Nous sommes désolés de ne pas avoir pu traiter votre demande. L’équipe LMR a été notifiée et prendra connaissance de la situation dès que possible.", + "failed": "Un problème technique est survenu" }, "upgrade": { "arrivalIncompatible": "Heure arrivée incompatible", @@ -79,7 +78,7 @@ "unqualifiedDriver": "Conducteur non habilité" } }, - "stopCalculation": "Arrêter le calcul" + "stopCalculation": "Arrêter" }, "spaceTimeGraphic": "Graphique Espace-Temps", "speedSpaceChart": "Graphique Espace Vitesse", diff --git a/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx b/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx index d564c7d9a23..9b10c0193e0 100644 --- a/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx +++ b/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Button } from '@osrd-project/ui-core'; import { ArrowDown, ArrowUp } from '@osrd-project/ui-icons'; @@ -24,6 +24,7 @@ import StdcmSimulationParams from '../StdcmSimulationParams'; import StdcmVias from './StdcmVias'; import { ArrivalTimeTypes, StdcmConfigErrorTypes } from '../../types'; import checkStdcmConfigErrors from '../../utils/checkStdcmConfigErrors'; +import StdcmLoader from '../StdcmLoader'; import StdcmWarningBox from '../StdcmWarningBox'; /** @@ -36,6 +37,7 @@ type StdcmConfigProps = { launchStdcmRequest: () => Promise; retainedSimulationIndex: number; showBtnToLaunchSimulation: boolean; + cancelStdcmRequest: () => void; }; const StdcmConfig = ({ @@ -44,8 +46,10 @@ const StdcmConfig = ({ launchStdcmRequest, retainedSimulationIndex, showBtnToLaunchSimulation, + cancelStdcmRequest, }: StdcmConfigProps) => { const { t } = useTranslation('stdcm'); + const launchButtonRef = useRef(null); const { infra } = useInfraStatus(); const dispatch = useAppDispatch(); @@ -72,6 +76,7 @@ const StdcmConfig = ({ const scenarioID = useSelector(getScenarioID); const pathfinding = useStaticPathfinding(infra); + const formRef = useRef(null); const [formErrors, setFormErrors] = useState(); @@ -156,7 +161,7 @@ const StdcmConfig = ({
-
+
@@ -168,18 +173,22 @@ const StdcmConfig = ({ className="posterior-linked-path" linkedOp={{ extremityType: 'origin', id: destination.id }} /> +
- {showBtnToLaunchSimulation && ( -
+ + {isPending && ( + + )}
diff --git a/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx b/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx index 7806c2e0334..52d72786fb3 100644 --- a/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx +++ b/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx @@ -160,6 +160,7 @@ const StdcmConsist = ({ disabled = false }: StdcmConfigCardProps) => { min={0} value={totalMass ?? ''} onChange={onTotalMassChange} + disabled={disabled} /> { min={0} value={totalLength ?? ''} onChange={onTotalLengthChange} + disabled={disabled} />
@@ -186,6 +188,7 @@ const StdcmConsist = ({ disabled = false }: StdcmConfigCardProps) => { min={0} value={maxSpeed ?? ''} onChange={onMaxSpeedChange} + disabled={disabled} />
diff --git a/front/src/applications/stdcm/components/StdcmLoader.tsx b/front/src/applications/stdcm/components/StdcmLoader.tsx index 7c0f63322c1..fa05b4ae41c 100644 --- a/front/src/applications/stdcm/components/StdcmLoader.tsx +++ b/front/src/applications/stdcm/components/StdcmLoader.tsx @@ -1,42 +1,105 @@ -import { forwardRef } from 'react'; +import { useEffect, useRef, useState, type RefObject } from 'react'; import { Button } from '@osrd-project/ui-core'; +import cx from 'classnames'; import { useTranslation } from 'react-i18next'; -import stdcmLoaderImg from 'assets/pictures/views/stdcm_loader.jpg'; +import type { LoaderStatus } from '../types'; + +const LOADER_HEIGHT = 176; +const LOADER_OFFSET = 32; type StdcmLoaderProps = { cancelStdcmRequest: () => void; + launchButtonRef: RefObject; + formRef: RefObject; }; -const StdcmLoader = forwardRef( - ({ cancelStdcmRequest }: StdcmLoaderProps, ref: React.Ref) => { - const { t } = useTranslation('stdcm'); - return ( -
-
-
-

{t('simulation.calculatingSimulation')}

-

{t('simulation.averageRequestTime')}

-
-
-
- {t('simulation.pendingSimulation')} -

{t('loaderImageLegend')}

+const StdcmLoader = ({ cancelStdcmRequest, launchButtonRef, formRef }: StdcmLoaderProps) => { + const { t } = useTranslation('stdcm'); + const loaderRef = useRef(null); + + const { top } = launchButtonRef.current!.getBoundingClientRect(); + const windowHeight = window.innerHeight; + + const [loaderStatus, setLoaderStatus] = useState({ + status: windowHeight - top - 32 > LOADER_HEIGHT ? 'loader-absolute' : 'loader-fixed-bottom', + firstLaunch: true, + }); + + useEffect(() => { + // Depending on the scroll, change the position of the loader between fixed, sticky or absolute + const handleScroll = () => { + if (!loaderRef.current || !launchButtonRef.current || !formRef.current) return; + + const { scrollY, innerHeight } = window; + + const isLoaderFitting = + innerHeight - launchButtonRef.current.getBoundingClientRect().top > + LOADER_HEIGHT + LOADER_OFFSET; + + // Loader doesn't fit between the bottom of the form and bottom of the viewport + if (!isLoaderFitting) { + setLoaderStatus({ + firstLaunch: false, + status: 'loader-fixed-bottom', + }); + return; + } + + const currentFormHeight = formRef.current.clientHeight; + const topFormPosition = formRef.current.getBoundingClientRect().top; + const launchButtonHeight = launchButtonRef.current.clientHeight; + const shouldLoaderStickTop = + scrollY > + currentFormHeight + scrollY + topFormPosition - launchButtonHeight - LOADER_OFFSET; + + // Loader reaches the top of the screen minus its top offset + if (shouldLoaderStickTop) { + setLoaderStatus({ + firstLaunch: false, + status: 'loader-fixed-top', + }); + return; + } + + setLoaderStatus({ + firstLaunch: false, + status: 'loader-absolute', + }); + }; + + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + return ( +
+
+

{t('simulation.calculatingSimulation')}

+
+
- ); - } -); +

{t('simulation.infoMessage')}

+
+ ); +}; export default StdcmLoader; diff --git a/front/src/applications/stdcm/types.ts b/front/src/applications/stdcm/types.ts index 12ddb30cb7e..dee90bc8087 100644 --- a/front/src/applications/stdcm/types.ts +++ b/front/src/applications/stdcm/types.ts @@ -200,3 +200,8 @@ export type StdcmLinkedPathResult = { }; export type ExtremityPathStepType = 'origin' | 'destination'; + +export type LoaderStatus = { + status: 'loader-fixed-bottom' | 'loader-fixed-top' | 'loader-absolute'; + firstLaunch: boolean; +}; diff --git a/front/src/applications/stdcm/views/StdcmView.tsx b/front/src/applications/stdcm/views/StdcmView.tsx index 041b4e1a568..a0097351b17 100644 --- a/front/src/applications/stdcm/views/StdcmView.tsx +++ b/front/src/applications/stdcm/views/StdcmView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState } from 'react'; import { isEqual, isNil } from 'lodash'; @@ -12,7 +12,6 @@ import { replaceElementAtIndex } from 'utils/array'; import StdcmEmptyConfigError from '../components/StdcmEmptyConfigError'; import StdcmConfig from '../components/StdcmForm/StdcmConfig'; import StdcmHeader from '../components/StdcmHeader'; -import StdcmLoader from '../components/StdcmLoader'; import StdcmResults from '../components/StdcmResults'; import StdcmStatusBanner from '../components/StdcmStatusBanner'; import useStdcmEnvironment, { NO_CONFIG_FOUND_MSG } from '../hooks/useStdcmEnv'; @@ -48,9 +47,7 @@ const StdcmView = () => { useOsrdConfActions() as StdcmConfSliceActions; const selectedSimulation = simulationsList[selectedSimulationIndex]; - const showResults = - !isPending && (showStatusBanner || simulationsList.length > 0 || hasConflicts); - const loaderRef = useRef(null); + const showResults = showStatusBanner || simulationsList.length > 0 || hasConflicts; const handleRetainSimulation = () => setRetainedSimulationIndex(selectedSimulationIndex); @@ -168,12 +165,6 @@ const StdcmView = () => { } }, [simulationsList]); - useEffect(() => { - if (isPending) { - loaderRef?.current?.scrollIntoView({ behavior: 'smooth' }); - } - }, [isPending]); - // If we've got an error during the loading of the stdcm env which is not the "no config error" message, // we let the error boundary manage it if (error && error.message !== NO_CONFIG_FOUND_MSG) throw error; @@ -192,9 +183,9 @@ const StdcmView = () => { showBtnToLaunchSimulation={showBtnToLaunchSimulation} retainedSimulationIndex={retainedSimulationIndex} launchStdcmRequest={launchStdcmRequest} + cancelStdcmRequest={cancelStdcmRequest} /> - {isPending && } {showStatusBanner && } {showResults && ( diff --git a/front/src/assets/pictures/views/stdcm_loader.jpg b/front/src/assets/pictures/views/stdcm_loader.jpg deleted file mode 100644 index 18ee18772a7..00000000000 Binary files a/front/src/assets/pictures/views/stdcm_loader.jpg and /dev/null differ diff --git a/front/src/styles/scss/applications/stdcm/_home.scss b/front/src/styles/scss/applications/stdcm/_home.scss index 84c15be6ad9..b72c839200e 100644 --- a/front/src/styles/scss/applications/stdcm/_home.scss +++ b/front/src/styles/scss/applications/stdcm/_home.scss @@ -10,7 +10,7 @@ cursor: default !important; .stdcm__body { - padding: 32px 32px 48px; + padding: 32px; background-color: rgb(239, 243, 245); display: flex; flex-direction: column; @@ -94,7 +94,7 @@ position: relative; align-items: center; - > div { + > div:not(.stdcm-loader) { width: 100%; } @@ -120,7 +120,11 @@ justify-content: center; width: 100%; font-weight: 500; - margin-top: 32px; + + &.fade-out { + opacity: 0; + transition: opacity 0.3s ease-out; + } } .stdcm-warning-buttons { @@ -221,10 +225,10 @@ .simulation-status-banner { .banner-content { - padding: 0 0 16px 338px; + padding: 0 0 16px 378px; background: linear-gradient(180deg, rgba(239, 243, 245) 40px, rgba(233, 239, 242) 40px); .status { - width: 546px; + width: 466px; display: flex; font-weight: 400; letter-spacing: 0px; diff --git a/front/src/styles/scss/applications/stdcm/_linkedPath.scss b/front/src/styles/scss/applications/stdcm/_linkedPath.scss index 3bd3eb80b3e..a153d60bbd2 100644 --- a/front/src/styles/scss/applications/stdcm/_linkedPath.scss +++ b/front/src/styles/scss/applications/stdcm/_linkedPath.scss @@ -6,7 +6,7 @@ } &.posterior-linked-path { - margin-top: 18px; + margin-block: 18px 32px; } .stdcm-card__header { diff --git a/front/src/styles/scss/applications/stdcm/_loader.scss b/front/src/styles/scss/applications/stdcm/_loader.scss index 3c11e50021f..ac78b57db6f 100644 --- a/front/src/styles/scss/applications/stdcm/_loader.scss +++ b/front/src/styles/scss/applications/stdcm/_loader.scss @@ -1,61 +1,100 @@ -.stdcm-loader-background { - padding: 0 0 48px 338px; - background-color: #eff3f5; -} .stdcm-loader { - padding: 48px 48px 19px 48px; - border-radius: 6px; - background-color: rgba(255, 255, 255, 1); - width: 546px; + z-index: 2; + padding: 16px 16px 18px; + border-radius: 24px; + background-color: var(--white100); + width: 466px; + box-shadow: + 0 6px 21px -5px rgba(24, 68, 239, 0.26), + 0 16px 30px -5px rgba(0, 0, 0, 0.16), + 0 3px 5px -2px rgba(0, 0, 0, 0.1); - .stdcm-loader__wrapper { - padding: 0 67.2px; - border-radius: 6px; - background-color: rgba(230, 247, 255, 1); - width: 100%; - h1 { + &.loader-fixed-bottom { + position: fixed; + bottom: 32px; + top: unset; + + &.with-slide-animation { + animation: slideUp 0.8s ease-in-out; + } + } + + &.loader-absolute { + position: absolute; + bottom: calc(-176px + 40px); // height of the loader minus the height of the launch button + + &.with-fade-in-animation { + animation: fade-in 0.5s ease-in-out; + } + } + + &.loader-fixed-top { + position: fixed; + top: 32px; + bottom: unset; + } + + @keyframes fade-in { + 0% { + opacity: 0.1; + } + 100% { opacity: 1; - color: rgba(33, 100, 130, 1); + } + } + + @keyframes slideUp { + 0% { + transform: translateY(100%); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } + } + + .stdcm-loader__wrapper { + display: flex; + flex-direction: column; + align-items: center; + border-radius: 8px; + background-color: var(--info5); + + h2 { + color: var(--info60); font-size: 1.5rem; font-weight: 400; - font-style: Regular; - letter-spacing: 0; - text-align: center; line-height: 32px; - margin-top: 24px; + margin-block: 15px 0; + animation: fade 1.6s infinite; } - p { - opacity: 1; - color: rgba(33, 100, 130, 1); - font-size: 1rem; - font-weight: 400; - font-style: Regular; - letter-spacing: 0; - text-align: center; - line-height: 24px; - margin-bottom: 24px; + @keyframes fade { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.25; + } } } .stdcm-loader__cancel-btn { - margin: 24px 0; - } + margin-block: 18px 23px; - img { - border-radius: 4px; - width: 100%; + button { + font-size: 0.875rem; + font-weight: 500; + } } - .stdcm-loader__img-signature { - opacity: 1; - color: rgba(121, 118, 113, 1); + .stdcm-loader__info-message { + color: var(--grey80); font-size: 0.75rem; font-weight: 400; - font-style: Italic; - letter-spacing: 0; - text-align: left; - margin-top: 12px; - margin-bottom: 0; + margin-block: 14px 0; + text-align: center; } } diff --git a/front/tests/pages/stdcm-page-model.ts b/front/tests/pages/stdcm-page-model.ts index e19b1c3b2d4..d9eae4272e7 100644 --- a/front/tests/pages/stdcm-page-model.ts +++ b/front/tests/pages/stdcm-page-model.ts @@ -379,7 +379,8 @@ class STDCMPage { await toleranceField.click(); await this.page.getByRole('button', { name: minusValue, exact: true }).click(); await this.page.getByRole('button', { name: plusValue, exact: true }).click(); - // TODO : Add a click on the close button when #693 is done + // TODO : Add a click on the close button instead of clicking on the map when #693 is done + await this.mapContainer.click(); } async fillAndVerifyViaDetails(viaNumber: number, viaSearch: string, selectedLanguage?: string) {