From ba98367c5a9f55afc9d252b8cf97060d2de884d7 Mon Sep 17 00:00:00 2001 From: Xavier Jp Date: Wed, 5 Jun 2024 11:40:11 +0200 Subject: [PATCH 1/2] chore: migrate service public to the client (#1098) --- .../sections/service-public-responsables.tsx | 193 ++++++++++-------- .../dirigeants/[slug]/page.tsx | 18 +- .../entreprise/[slug]/page.tsx | 38 ++-- .../etablissement/[slug]/page.tsx | 11 +- .../carte-professionnelle-TP/[slug]/route.ts | 2 +- .../clients/annuaire-service-public/index.ts | 6 +- components/service-public-section/index.tsx | 33 ++- hooks/fetch/service-public.ts | 44 ++++ models/service-public/index.ts | 24 +-- 9 files changed, 211 insertions(+), 158 deletions(-) create mode 100644 hooks/fetch/service-public.ts diff --git a/app/(header-default)/dirigeants/[slug]/_component/sections/service-public-responsables.tsx b/app/(header-default)/dirigeants/[slug]/_component/sections/service-public-responsables.tsx index fe7b392b6..cb6b361d7 100644 --- a/app/(header-default)/dirigeants/[slug]/_component/sections/service-public-responsables.tsx +++ b/app/(header-default)/dirigeants/[slug]/_component/sections/service-public-responsables.tsx @@ -1,99 +1,132 @@ +'use client'; + import { DILA } from '#components/administrations'; import NonRenseigne from '#components/non-renseigne'; -import { DataSection } from '#components/section/data-section'; +import { DataSectionClient } from '#components/section/data-section'; import { FullTable } from '#components/table/full'; import { EAdministration } from '#models/administrations/EAdministration'; -import { IAPINotRespondingError } from '#models/api-not-responding'; -import { IServicePublic } from '#models/service-public'; +import { isAPILoading } from '#models/api-loading'; +import { IUniteLegale } from '#models/core/types'; +import { useFetchServicePublic } from 'hooks/fetch/service-public'; +import SubServicesSection from './service-public-subservices'; -type IProps = { servicePublic: IServicePublic | IAPINotRespondingError }; +export default function ResponsablesServicePublicSection({ + uniteLegale, +}: { + uniteLegale: IUniteLegale; +}) { + const servicePublic = useFetchServicePublic(uniteLegale); -export default function ResponsableSection({ servicePublic }: IProps) { return ( - } - data={servicePublic} - > - {(servicePublic) => ( - <> - {!servicePublic.affectationPersonne ? ( -

- Ce service public n’a pas de responsable enregistré auprès de la{' '} - . -

- ) : ( - <> + <> + } + data={servicePublic} + > + {(servicePublic) => ( + <> + {!servicePublic.affectationPersonne ? (

- Ce service public possède{' '} - {servicePublic.affectationPersonne.length} responsable(s) - enregistré(s) auprès de la - {servicePublic.liens.annuaireServicePublic && ( - <> - {' '} - sur{' '} - - l’Annuaire du service public - - - )} - {servicePublic.liens.organigramme && ( - <> - {' '} - et publie un{' '} - - organigramme - - - )} + Ce service public n’a pas de responsable enregistré dans l’ + + Annuaire de l’administration + .

+ ) : ( + <> +

+ Ce service public possède{' '} + {servicePublic.affectationPersonne.length} responsable(s) + enregistré(s) auprès de la + {servicePublic.liens.annuaireServicePublic && ( + <> + {' '} + sur{' '} + + l’Annuaire du service public + + + )} + {servicePublic.liens.organigramme && ( + <> + {' '} + et publie un{' '} + + organigramme + + + )} + . +

- [ - personne.fonction, - personne.nom ?? , - personne.lienTexteAffectation ? ( - - {personne.lienTexteAffectation.libelle || - 'Voir la nomination'} - - ) : ( - - ), - ])} - /> - - )} - + [ + personne.fonction, + personne.nom ?? , + personne.lienTexteAffectation ? ( + + {personne.lienTexteAffectation.libelle || + 'Voir la nomination'} + + ) : ( + + ), + ])} + /> + + )} + + )} +
+ {!isAPILoading(servicePublic) && ( + )} -
+ ); } const NotFoundInfo = () => (

- Ce service public n’est pas enregistré auprès de la . + Ce service public n’a pas été retrouvé dans l’ + + Annuaire de l’administration + + .

); diff --git a/app/(header-default)/dirigeants/[slug]/page.tsx b/app/(header-default)/dirigeants/[slug]/page.tsx index df671b611..8cd04d5fb 100644 --- a/app/(header-default)/dirigeants/[slug]/page.tsx +++ b/app/(header-default)/dirigeants/[slug]/page.tsx @@ -3,7 +3,7 @@ import { DonneesPriveesSection } from '#components/donnees-privees-section'; import Title from '#components/title-section'; import { FICHE } from '#components/title-section/tabs'; import { estDiffusible } from '#models/core/statut-diffusion'; -import { getServicePublicByUniteLegale } from '#models/service-public'; +import { isServicePublic } from '#models/core/types'; import { EScope, hasRights } from '#models/user/rights'; import { uniteLegalePageDescription, @@ -14,9 +14,8 @@ import extractParamsAppRouter, { AppRouterProps, } from '#utils/server-side-helper/app/extract-params'; import getSession from '#utils/server-side-helper/app/get-session'; -import ResponsableSection from 'app/(header-default)/dirigeants/[slug]/_component/sections/service-public-responsables'; import { DirigeantInformation } from './_component/sections'; -import SubServicesSection from './_component/sections/service-public-subservices'; +import ResponsablesServicePublicSection from './_component/sections/service-public-responsables'; export const generateMetadata = async ( props: AppRouterProps @@ -43,9 +42,6 @@ const DirigeantsPage = async (props: AppRouterProps) => { const { slug, isBot } = extractParamsAppRouter(props); const uniteLegale = await cachedGetUniteLegale(slug, isBot); - const servicePublic = await getServicePublicByUniteLegale(uniteLegale, { - isBot, - }); const session = await getSession(); @@ -57,14 +53,8 @@ const DirigeantsPage = async (props: AppRouterProps) => { ficheType={FICHE.DIRIGEANTS} session={session} /> - {servicePublic ? ( - <> - - - + {isServicePublic(uniteLegale) ? ( + ) : !estDiffusible(uniteLegale) && !hasRights(session, EScope.nonDiffusible) ? ( diff --git a/app/(header-default)/entreprise/[slug]/page.tsx b/app/(header-default)/entreprise/[slug]/page.tsx index e0e88a63a..e0123b9a9 100644 --- a/app/(header-default)/entreprise/[slug]/page.tsx +++ b/app/(header-default)/entreprise/[slug]/page.tsx @@ -1,6 +1,4 @@ import { Metadata } from 'next'; -import { HorizontalSeparator } from '#components-ui/horizontal-separator'; -import BreakPageForPrint from '#components-ui/print-break-page'; import AssociationSection from '#components/association-section'; import CollectiviteTerritorialeSection from '#components/collectivite-territoriale-section'; import { EspaceAgentSummarySection } from '#components/espace-agent-components/summary-section'; @@ -14,11 +12,13 @@ import Title from '#components/title-section'; import { FICHE } from '#components/title-section/tabs'; import UniteLegaleSection from '#components/unite-legale-section'; import UsefulShortcuts from '#components/useful-shortcuts'; -import { isAPINotResponding } from '#models/api-not-responding'; import { estNonDiffusible } from '#models/core/statut-diffusion'; -import { isAssociation, isCollectiviteTerritoriale } from '#models/core/types'; +import { + isAssociation, + isCollectiviteTerritoriale, + isServicePublic, +} from '#models/core/types'; import { getImmatriculationEORI } from '#models/espace-agent/immatriculation-eori'; -import { getServicePublicByUniteLegale } from '#models/service-public'; import { EScope, hasRights } from '#models/user/rights'; import { shouldNotIndex, @@ -55,14 +55,9 @@ export default async function UniteLegalePage(props: AppRouterProps) { const session = await getSession(); const uniteLegale = await cachedGetUniteLegale(slug, isBot, page); - const [servicePublic, immatriculationEORI] = await Promise.all([ - getServicePublicByUniteLegale(uniteLegale, { - isBot, - }), - hasRights(session, EScope.eori) - ? getImmatriculationEORI(uniteLegale.siege.siret, session?.user) - : null, - ]); + const immatriculationEORI = hasRights(session, EScope.eori) + ? await getImmatriculationEORI(uniteLegale.siege.siret, session?.user) + : null; return ( <> @@ -90,19 +85,10 @@ export default async function UniteLegalePage(props: AppRouterProps) { {isCollectiviteTerritoriale(uniteLegale) && ( )} - {servicePublic && ( - - )} - {(isCollectiviteTerritoriale(uniteLegale) || - (servicePublic && !isAPINotResponding(servicePublic))) && ( - <> - - - - )} + {isServicePublic(uniteLegale) && + !isCollectiviteTerritoriale(uniteLegale) && ( + + )} {!isBot && isAssociation(uniteLegale) && ( )} diff --git a/app/(header-default)/etablissement/[slug]/page.tsx b/app/(header-default)/etablissement/[slug]/page.tsx index 81a251eea..be4970a60 100644 --- a/app/(header-default)/etablissement/[slug]/page.tsx +++ b/app/(header-default)/etablissement/[slug]/page.tsx @@ -3,7 +3,7 @@ import { NonDiffusibleSection } from '#components/non-diffusible-section'; import ServicePublicSection from '#components/service-public-section'; import { TitleEtablissementWithDenomination } from '#components/title-section/etablissement'; import { estNonDiffusible } from '#models/core/statut-diffusion'; -import { getServicePublicByEtablissement } from '#models/service-public'; +import { isServicePublic } from '#models/core/types'; import { etablissementPageDescription, etablissementPageTitle, @@ -44,11 +44,6 @@ export default (async function EtablissementPage(props: AppRouterProps) { await cachedEtablissementWithUniteLegale(slug, isBot); const session = await getSession(); - const servicePublic = await getServicePublicByEtablissement( - uniteLegale, - etablissement, - { isBot } - ); return ( <> @@ -73,10 +68,10 @@ export default (async function EtablissementPage(props: AppRouterProps) { usedInEntreprisePage={false} /> )} - {servicePublic && ( + {!isBot && isServicePublic(uniteLegale) && ( )} diff --git a/app/api/data-fetching/espace-agent/carte-professionnelle-TP/[slug]/route.ts b/app/api/data-fetching/espace-agent/carte-professionnelle-TP/[slug]/route.ts index a0fcb9cce..19f04e208 100644 --- a/app/api/data-fetching/espace-agent/carte-professionnelle-TP/[slug]/route.ts +++ b/app/api/data-fetching/espace-agent/carte-professionnelle-TP/[slug]/route.ts @@ -13,7 +13,7 @@ export async function GET( 'CarteProfessionnelleTravauxPublics', slug, EAdministration.FNTP, - EScope.conformite, + EScope.carteProfessionnelleTravauxPublics, async (agentSiret: string) => { const siret = verifySiret(slug as string); const siren = extractSirenFromSiret(siret); diff --git a/clients/open-data-soft/clients/annuaire-service-public/index.ts b/clients/open-data-soft/clients/annuaire-service-public/index.ts index 28b57876c..72ab45100 100644 --- a/clients/open-data-soft/clients/annuaire-service-public/index.ts +++ b/clients/open-data-soft/clients/annuaire-service-public/index.ts @@ -47,7 +47,10 @@ function queryAnnuaireServicePublic(whereQuery: string) { return odsClient( { url: routes.annuaireServicePublic.ods.search, - config: { params: { where: whereQuery }, useCache: true }, + config: { + params: { where: whereQuery }, + useCache: true, + }, }, routes.annuaireServicePublic.ods.metadata ); @@ -82,7 +85,6 @@ const clientAnnuaireServicePublicByIds = async ( ids: string[] ): Promise => { const query = `id="${ids.join('" OR id="')}"`; - const useCache = false; let response = await queryAnnuaireServicePublic(query); if (!response.records.length) { diff --git a/components/service-public-section/index.tsx b/components/service-public-section/index.tsx index cad460914..bb4f6f335 100644 --- a/components/service-public-section/index.tsx +++ b/components/service-public-section/index.tsx @@ -1,28 +1,45 @@ +'use client'; + import { ReactNode } from 'react'; import NonRenseigne from '#components/non-renseigne'; -import { DataSection } from '#components/section/data-section'; +import { DataSectionClient } from '#components/section/data-section'; import { TwoColumnTable } from '#components/table/simple'; import { EAdministration } from '#models/administrations/EAdministration'; -import { IAPINotRespondingError } from '#models/api-not-responding'; -import { IUniteLegale } from '#models/core/types'; +import { IEtablissement, IUniteLegale } from '#models/core/types'; import { IServicePublic } from '#models/service-public'; +import { useFetchServicePublic } from 'hooks/fetch/service-public'; type IProps = { - servicePublic: IServicePublic | IAPINotRespondingError; uniteLegale: IUniteLegale; + etablissement?: IEtablissement; }; export default function ServicePublicSection({ - servicePublic, uniteLegale, + etablissement, }: IProps) { + const servicePublic = useFetchServicePublic(uniteLegale, etablissement); + return ( <> - + Ce service public n’a pas été retrouvé dans l’ + + Annuaire du service public + + . +

+ } > {(servicePublic) => ( <> @@ -44,7 +61,7 @@ export default function ServicePublicSection({ )} )} -
+ ); } diff --git a/hooks/fetch/service-public.ts b/hooks/fetch/service-public.ts new file mode 100644 index 000000000..5b5613d60 --- /dev/null +++ b/hooks/fetch/service-public.ts @@ -0,0 +1,44 @@ +import { EAdministration } from '#models/administrations/EAdministration'; +import { IEtablissement, IUniteLegale } from '#models/core/types'; +import { FetchRessourceException } from '#models/exceptions'; +import { + getServicePublicByEtablissement, + getServicePublicByUniteLegale, +} from '#models/service-public'; +import logErrorInSentry from '#utils/sentry'; +import { useFetchData } from './use-fetch-data'; + +export function useFetchServicePublic( + uniteLegale: IUniteLegale, + etablissement?: IEtablissement +) { + return useFetchData( + { + fetchData: async () => { + if (etablissement) { + return await getServicePublicByEtablissement(etablissement); + } else { + return await getServicePublicByUniteLegale(uniteLegale); + } + }, + administration: EAdministration.DILA, + logError: (e: any) => { + if (e.status) { + // We already log error server side + return; + } + logErrorInSentry( + new FetchRessourceException({ + ressource: 'AnnuaireServicePublic', + administration: EAdministration.DILA, + cause: e, + context: { + siren: uniteLegale.siren, + }, + }) + ); + }, + }, + [uniteLegale] + ); +} diff --git a/models/service-public/index.ts b/models/service-public/index.ts index f9758d94a..b4c444faf 100644 --- a/models/service-public/index.ts +++ b/models/service-public/index.ts @@ -8,11 +8,7 @@ import { APINotRespondingFactory, IAPINotRespondingError, } from '#models/api-not-responding'; -import { - IEtablissement, - IUniteLegale, - isServicePublic, -} from '#models/core/types'; +import { IEtablissement, IUniteLegale } from '#models/core/types'; import { FetchRessourceException } from '#models/exceptions'; import { Siret } from '#utils/helpers'; import logErrorInSentry from '#utils/sentry'; @@ -69,14 +65,9 @@ type IAffectationPersonne = Array<{ }>; export const getServicePublicByUniteLegale = async ( - uniteLegale: IUniteLegale, - options: { isBot: boolean } -): Promise => { + uniteLegale: IUniteLegale +): Promise => { try { - if (options.isBot || !isServicePublic(uniteLegale)) { - return null; - } - return await clientAnnuaireServicePublicBySiret(uniteLegale.siege.siret); } catch (eSiret: any) { try { @@ -91,14 +82,9 @@ export const getServicePublicByUniteLegale = async ( }; export const getServicePublicByEtablissement = async ( - uniteLegale: IUniteLegale, - etablissement: IEtablissement, - options: { isBot: boolean } -): Promise => { + etablissement: IEtablissement +): Promise => { try { - if (options.isBot || !isServicePublic(uniteLegale)) { - return null; - } return await clientAnnuaireServicePublicBySiret(etablissement.siret); } catch (e: any) { return mapToError(e, etablissement.siret); From 16cbff4b46732d0b33c20286cbcc07b23db9b086 Mon Sep 17 00:00:00 2001 From: Xavier Jp Date: Wed, 5 Jun 2024 11:54:34 +0200 Subject: [PATCH 2/2] feat: use Link in tabs (#1096) * feat: use Link in tabs * feat: improve loadbar behaviour --------- Co-authored-by: Johan Girod --- components/header/header-core/index.tsx | 164 +++++++++--------- components/load-bar/index.tsx | 40 +++-- components/load-bar/style.module.css | 9 + components/title-section/tabs/index.tsx | 21 ++- .../title-section/tabs/styles.module.css | 3 +- components/title-section/tabs/tab-link.tsx | 39 +++++ 6 files changed, 170 insertions(+), 106 deletions(-) create mode 100644 components/load-bar/style.module.css create mode 100644 components/title-section/tabs/tab-link.tsx diff --git a/components/header/header-core/index.tsx b/components/header/header-core/index.tsx index 15602f806..742a1ae61 100644 --- a/components/header/header-core/index.tsx +++ b/components/header/header-core/index.tsx @@ -30,99 +30,101 @@ export const HeaderCore: React.FC = ({ pathFrom, }) => { return ( -
+ <> - +
+ - -
-
-
-
-
-
- - {useSearchBar || useLogo ? ( -
- - - - - + + +
+
+
+
+ - {useSearchBar ? ( -
- -
- ) : null} -
-
-
-
    -
  • + {useSearchBar || useLogo ? ( + + ) : null} +
    -
  • -
+
+
+ {useSearchBar ? ( +
+ +
+ ) : null} +
+
+
+
    +
  • + +
  • +
+
-
- {plugin} - - -
+ {plugin} + + +
+ ); }; diff --git a/components/load-bar/index.tsx b/components/load-bar/index.tsx index eccc79e8b..1fbc0b235 100644 --- a/components/load-bar/index.tsx +++ b/components/load-bar/index.tsx @@ -4,27 +4,27 @@ import { useEffect } from 'react'; import constants from '#models/constants'; import { EScope, hasRights } from '#models/user/rights'; import { ISession } from '#models/user/session'; - +import styles from './style.module.css'; export default function LoadBar({ session }: { session: ISession | null }) { useEffect(() => { const loadBar = loadBarFactory(); if (typeof window !== 'undefined') { - window.addEventListener('beforeunload', loadBar.run); + window.addEventListener( + 'beforeunload', + loadBar.runWithDelay.bind(loadBar) + ); + window.addEventListener('runloadbar', loadBar.runImmediate.bind(loadBar)); + window.addEventListener('cancelloadbar', loadBar.cancel.bind(loadBar)); } }, []); return (
); @@ -71,8 +71,7 @@ const loadBarFactory = () => { return { _currentJobId: '', _loader: null as HTMLElement | null, - - run: async function () { + async _run(step: number) { const jobId = Math.random().toString(16).substring(7); this._currentJobId = jobId; @@ -84,14 +83,29 @@ const loadBarFactory = () => { return; } } - - for (let w of positions) { + this._loader.style.opacity = '1'; + for (let i = step; i < positions.length; i++) { // interrupt job if another job has been triggered by another beforeunload event if (this._currentJobId !== jobId) { return; } - this._loader.style.width = `${w}vw`; + this._loader.style.width = `${positions[i]}%`; + await wait(200); + } + }, + runImmediate() { + this._run(2); + }, + runWithDelay() { + this._run(0); + }, + async cancel() { + this._currentJobId = ''; + if (this._loader) { + this._loader.style.width = '100%'; + this._loader.style.opacity = '0'; await wait(200); + this._loader.style.width = '0'; } }, }; diff --git a/components/load-bar/style.module.css b/components/load-bar/style.module.css new file mode 100644 index 000000000..00f8fe94d --- /dev/null +++ b/components/load-bar/style.module.css @@ -0,0 +1,9 @@ +.load-bar { + transition: all 200ms ease-in-out; + height: 3px; + width: 0; + opacity: 0; + position: fixed; + top: 0; + z-index: 9999; +} diff --git a/components/title-section/tabs/index.tsx b/components/title-section/tabs/index.tsx index 1e648ddfe..d6c5af760 100644 --- a/components/title-section/tabs/index.tsx +++ b/components/title-section/tabs/index.tsx @@ -1,3 +1,4 @@ +import Link from 'next/link'; import { PrintNever } from '#components-ui/print-visibility'; import { checkHasLabelsAndCertificates, @@ -11,6 +12,7 @@ import { import { EScope, hasRights } from '#models/user/rights'; import { ISession } from '#models/user/session'; import styles from './styles.module.css'; +import TabLink from './tab-link'; export enum FICHE { INFORMATION = 'résumé', @@ -135,30 +137,27 @@ export const Tabs: React.FC<{ noFollow, width = 'auto', }) => ( - - {currentFicheType === ficheType ? label :

{label}

} -
+ width={width} + /> ) )} {currentFicheType === FICHE.ETABLISSEMENT && ( <> diff --git a/components/title-section/tabs/styles.module.css b/components/title-section/tabs/styles.module.css index 65c81c7bb..f338691c1 100644 --- a/components/title-section/tabs/styles.module.css +++ b/components/title-section/tabs/styles.module.css @@ -56,6 +56,7 @@ } } -.titleTabs > a:hover { +.titleTabs > a:hover, +.titleTabs > a:focus { background-color: var(--annuaire-colors-pastelBlue); } diff --git a/components/title-section/tabs/tab-link.tsx b/components/title-section/tabs/tab-link.tsx new file mode 100644 index 000000000..b5441bc4a --- /dev/null +++ b/components/title-section/tabs/tab-link.tsx @@ -0,0 +1,39 @@ +'use client'; + +import Link from 'next/link'; +import { useEffect } from 'react'; +import styles from './styles.module.css'; +type IProps = { + label: string; + href: string; + noFollow?: boolean; + width?: string; + active: boolean; +}; +export default function TabLink({ + active, + href, + label, + noFollow, + width, +}: IProps) { + useEffect(() => { + active === false && window.dispatchEvent(new Event('cancelloadbar')); + }, [active]); + useEffect(() => { + window.dispatchEvent(new Event('cancelloadbar')); + }, []); + return ( + window.dispatchEvent(new Event('runloadbar'))} + prefetch={false} + > + {active ? label :

{label}

} + + ); +}