From a90177630547f32a1c37cbd4ce308259475bba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BITARD=20Micha=C3=ABl?= <michael.bitard@beta.gouv.fr> Date: Tue, 22 Apr 2025 08:34:00 +0000 Subject: [PATCH] =?UTF-8?q?chore(api):=20passe=20en=20effect=20les=20route?= =?UTF-8?q?s=20pour=20=C3=A9diter=20un=20titre=20et=20s'abonner=20=C3=A0?= =?UTF-8?q?=20un=20titre=20(pub/pnm-public/camino!1695)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/src/api/rest/titre-demande.ts | 15 +- packages/api/src/api/rest/titres.queries.ts | 18 +- .../api/src/api/rest/titres.queries.types.ts | 30 ++++ .../src/api/rest/titres.test.integration.ts | 59 +++++-- packages/api/src/api/rest/titres.ts | 167 ++++++++++-------- .../database/models/utilisateurs--titres.ts | 35 ---- .../database/queries/utilisateurs-titres.ts | 9 - .../queries/utilisateurs.test.integration.ts | 4 +- packages/api/src/server/rest.ts | 12 +- packages/api/src/types.ts | 5 - packages/api/tests/_utils/index.ts | 12 -- packages/common/src/rest.ts | 15 +- packages/ui/src/api/client-rest.ts | 10 -- packages/ui/src/components/titre.stories.tsx | 10 +- ...e.stories_snapshots_ChantepieMutation.html | 2 +- ...tre.stories_snapshots_ChantepieOctroi.html | 2 +- ...snapshots_ChantepieOctroiAsEntreprise.html | 2 +- .../components/titre/edit-popup.stories.tsx | 8 +- .../ui/src/components/titre/edit-popup.tsx | 6 +- .../titre/titre-abonner-button.stories.tsx | 4 +- ...er-button.stories_snapshots_NotAbonne.html | 2 +- ...er-button.stories_snapshots_WithError.html | 4 +- .../components/titre/titre-abonner-button.tsx | 23 ++- .../src/components/titre/titre-api-client.ts | 14 +- 24 files changed, 239 insertions(+), 229 deletions(-) delete mode 100644 packages/api/src/database/models/utilisateurs--titres.ts delete mode 100644 packages/api/src/database/queries/utilisateurs-titres.ts diff --git a/packages/api/src/api/rest/titre-demande.ts b/packages/api/src/api/rest/titre-demande.ts index 5f5102a09..6cfa6427f 100644 --- a/packages/api/src/api/rest/titre-demande.ts +++ b/packages/api/src/api/rest/titre-demande.ts @@ -12,30 +12,27 @@ import { ETAPE_IS_BROUILLON } from 'camino-common/src/etape' import { Effect, pipe, Match } from 'effect' import { capitalize } from 'effect/String' import { titreDemarcheUpdateTask } from '../../business/titre-demarche-update' -import { ZodUnparseable } from '../../tools/fp-tools' import { CaminoApiError, ITitreEtape } from '../../types' import { CreateTitreErrors, CreateDemarcheErrors, createTitre, createDemarche } from './titre-demande.queries' import { titreEtapeUpsert } from '../../database/queries/titres-etapes' import { getCurrent } from 'camino-common/src/date' import { titreEtapeUpdateTask } from '../../business/titre-etape-update' -import { utilisateurTitreCreate } from '../../database/queries/utilisateurs-titres' import { RestNewPostCall } from '../../server/rest' import { ETAPES_TYPES } from 'camino-common/src/static/etapesTypes' import { ETAPES_STATUTS } from 'camino-common/src/static/etapesStatuts' import { TITRES_TYPES_IDS } from 'camino-common/src/static/titresTypes' +import { updateUtilisateurTitre } from './titres.queries' type TitreDemandeCreerErrors = | 'Accès interdit' | 'Permissions insuffisantes' | 'Problème lors du lien des titres' - | ZodUnparseable | CreateTitreErrors | 'Problème lors de la mise à jour des taches du titre' | CreateDemarcheErrors | 'Problème lors de la mise à jour des taches de la démarche' | "Problème lors de la création de l'étape" | "Problème lors de la mise à jour des tâches de l'étape" - | "Problème lors de l'abonnement de l'utilisateur au titre" | "L'entreprise est obligatoire" | "L'entreprise ne doit pas être présente" export const titreDemandeCreer: RestNewPostCall<'/rest/titres'> = (rootPipe): Effect.Effect<TitreDemandeOutput, CaminoApiError<TitreDemandeCreerErrors>> => { @@ -150,14 +147,7 @@ export const titreDemandeCreer: RestNewPostCall<'/rest/titres'> = (rootPipe): Ef }, }) }), - Effect.tap(() => { - return Effect.tryPromise({ - try: () => utilisateurTitreCreate({ utilisateurId: user.id, titreId }), - catch: unknown => { - return { message: "Problème lors de l'abonnement de l'utilisateur au titre" as const, extra: unknown } - }, - }) - }), + Effect.tap(() => updateUtilisateurTitre(pool, true, titreId, user.id)), Effect.map(etapeId => ({ titreId, etapeId })) ) } @@ -175,7 +165,6 @@ export const titreDemandeCreer: RestNewPostCall<'/rest/titres'> = (rootPipe): Ef 'Problème lors de la mise à jour des taches de la démarche', "Problème lors de la création de l'étape", "Problème lors de la mise à jour des tâches de l'étape", - "Problème lors de l'abonnement de l'utilisateur au titre", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({ ...caminoError, diff --git a/packages/api/src/api/rest/titres.queries.ts b/packages/api/src/api/rest/titres.queries.ts index 5849ddd8d..2ba314256 100644 --- a/packages/api/src/api/rest/titres.queries.ts +++ b/packages/api/src/api/rest/titres.queries.ts @@ -2,6 +2,8 @@ import { sql } from '@pgtyped/runtime' import { getMostRecentValuePropFromEtapeFondamentaleValide, TitreGet, TitreGetDemarche } from 'camino-common/src/titres' import { EffectDbQueryAndValidateErrors, Redefine, dbQueryAndValidate, effectDbQueryAndValidate } from '../../pg-database' import { + IAddUtilisateurTitreDbQuery, + IDeleteUtilisateurTitreDbQuery, IGetAdministrationsLocalesByTitreIdDbQuery, IGetDemarchesByTitreIdQueryDbQuery, IGetDoublonsByTitreIdDbQuery, @@ -15,7 +17,7 @@ import { z } from 'zod' import { Commune, communeIdValidator } from 'camino-common/src/static/communes' import { Pool } from 'pg' import { titreTypeIdValidator } from 'camino-common/src/static/titresTypes' -import { isAdministration, isSuper, User } from 'camino-common/src/roles' +import { isAdministration, isSuper, User, UtilisateurId } from 'camino-common/src/roles' import { titreStatutIdValidator } from 'camino-common/src/static/titresStatuts' import { titreReferenceValidator } from 'camino-common/src/titres-references' import { @@ -514,3 +516,17 @@ export const getTitres = (pool: Pool): Effect.Effect<GetTitre[], CaminoError<Eff const getTitresDb = sql<Redefine<IGetTitresDbQuery, {}, GetTitre>>` select t.id from titres t where t.archive is false` + +export const updateUtilisateurTitre = (pool: Pool, abonne: boolean, titreId: TitreId, utilisateurId: UtilisateurId): Effect.Effect<void, CaminoError<EffectDbQueryAndValidateErrors>> => + Effect.if(abonne, { + onTrue: () => effectDbQueryAndValidate(addUtilisateurTitreDb, { titreId, utilisateurId }, pool, z.void()), + onFalse: () => effectDbQueryAndValidate(deleteUtilisateurTitreDb, { titreId, utilisateurId }, pool, z.void()), + }) + +const addUtilisateurTitreDb = sql< + Redefine<IAddUtilisateurTitreDbQuery, { utilisateurId: UtilisateurId; titreId: TitreId }, void> +>`INSERT INTO utilisateurs__titres (utilisateur_id, titre_id) VALUES ($utilisateurId!, $titreId)` + +const deleteUtilisateurTitreDb = sql< + Redefine<IDeleteUtilisateurTitreDbQuery, { utilisateurId: UtilisateurId; titreId: TitreId }, void> +>`DELETE FROM utilisateurs__titres WHERE utilisateur_id=$utilisateurId! and titre_id=$titreId` diff --git a/packages/api/src/api/rest/titres.queries.types.ts b/packages/api/src/api/rest/titres.queries.types.ts index d19930efc..6852ce711 100644 --- a/packages/api/src/api/rest/titres.queries.types.ts +++ b/packages/api/src/api/rest/titres.queries.types.ts @@ -132,3 +132,33 @@ export interface IGetTitresDbQuery { result: IGetTitresDbResult; } +/** 'AddUtilisateurTitreDb' parameters type */ +export interface IAddUtilisateurTitreDbParams { + titreId?: string | null | void; + utilisateurId: string; +} + +/** 'AddUtilisateurTitreDb' return type */ +export type IAddUtilisateurTitreDbResult = void; + +/** 'AddUtilisateurTitreDb' query type */ +export interface IAddUtilisateurTitreDbQuery { + params: IAddUtilisateurTitreDbParams; + result: IAddUtilisateurTitreDbResult; +} + +/** 'DeleteUtilisateurTitreDb' parameters type */ +export interface IDeleteUtilisateurTitreDbParams { + titreId?: string | null | void; + utilisateurId: string; +} + +/** 'DeleteUtilisateurTitreDb' return type */ +export type IDeleteUtilisateurTitreDbResult = void; + +/** 'DeleteUtilisateurTitreDb' query type */ +export interface IDeleteUtilisateurTitreDbQuery { + params: IDeleteUtilisateurTitreDbParams; + result: IDeleteUtilisateurTitreDbResult; +} + diff --git a/packages/api/src/api/rest/titres.test.integration.ts b/packages/api/src/api/rest/titres.test.integration.ts index 980325023..950248e48 100644 --- a/packages/api/src/api/rest/titres.test.integration.ts +++ b/packages/api/src/api/rest/titres.test.integration.ts @@ -3,7 +3,7 @@ import { titreUpdate } from '../../database/queries/titres' import { titreDemarcheCreate } from '../../database/queries/titres-demarches' import { titreEtapeCreate } from '../../database/queries/titres-etapes' import { userSuper } from '../../database/user-super' -import { restCall, restDeleteCall, restNewCall, restNewPostCall, restPostCall } from '../../../tests/_utils/index' +import { restCall, restDeleteCall, restNewCall, restNewPostCall } from '../../../tests/_utils/index' import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations' import { ITitreDemarche, ITitreEtape } from '../../types' import { entreprisesUpsert } from '../../database/queries/entreprises' @@ -276,24 +276,34 @@ describe('titreModifier', () => { }) test('ne peut pas modifier un titre (utilisateur anonyme)', async () => { - const tested = await restPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, undefined, { id, nom: 'mon titre modifié', references: [] }) + const tested = await restNewPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, undefined, { id, nom: 'mon titre modifié', references: [] }) - expect(tested.statusCode).toBe(403) + expect(tested.body).toMatchInlineSnapshot(` + { + "message": "Accès interdit", + "status": 403, + } + `) }) test("ne peut pas modifier un titre (un utilisateur 'entreprise')", async () => { - const tested = await restPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'entreprise', entrepriseIds: [] }, { id, nom: 'mon titre modifié', references: [] }) + const tested = await restNewPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'entreprise', entrepriseIds: [] }, { id, nom: 'mon titre modifié', references: [] }) - expect(tested.statusCode).toBe(404) + expect(tested.body).toMatchInlineSnapshot(` + { + "message": "Titre non trouvé", + "status": 404, + } + `) }) test('modifie un titre (un utilisateur userSuper)', async () => { - const tested = await restPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'super' }, { id, nom: 'mon titre modifié', references: [] }) - expect(tested.statusCode).toBe(204) + const tested = await restNewPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'super' }, { id, nom: 'mon titre modifié', references: [] }) + expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.OK) }) test("modifie un titre ARM (un utilisateur 'admin' PTMG)", async () => { - const tested = await restPostCall( + const tested = await restNewPostCall( dbPool, '/rest/titres/:titreId', { titreId: id }, @@ -303,14 +313,19 @@ describe('titreModifier', () => { }, { id, nom: 'mon titre modifié', references: [] } ) - expect(tested.statusCode).toBe(404) + expect(tested.body).toMatchInlineSnapshot(` + { + "message": "Titre non trouvé", + "status": 404, + } + `) }) test("ne peut pas modifier un titre ARM échu (un utilisateur 'admin' PTMG)", async () => { const id = newTitreId() await insertTitreGraph({ id, nom: 'mon titre échu', typeId: 'arm', titreStatutId: 'ech', propsTitreEtapesIds: {} }) - const tested = await restPostCall( + const tested = await restNewPostCall( dbPool, '/rest/titres/:titreId', { titreId: id }, @@ -320,18 +335,28 @@ describe('titreModifier', () => { }, { id, nom: 'mon titre modifié', references: [] } ) - expect(tested.statusCode).toBe(404) + expect(tested.body).toMatchInlineSnapshot(` + { + "message": "Titre non trouvé", + "status": 404, + } + `) }) test("ne peut pas modifier un titre ARM (un utilisateur 'admin' DGCL/SDFLAE/FL1)", async () => { - const tested = await restPostCall( + const tested = await restNewPostCall( dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'admin', administrationId: ADMINISTRATION_IDS['DGCL/SDFLAE/FL1'] }, { id, nom: 'mon titre modifié', references: [] } ) - expect(tested.statusCode).toBe(403) + expect(tested.body).toMatchInlineSnapshot(` + { + "message": "Droits insuffisants pour modifier le titre", + "status": 400, + } + `) }) }) @@ -549,9 +574,9 @@ test('utilisateurTitreAbonner', async () => { propsTitreEtapesIds: {}, }) - const tested = await restPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true }) + const tested = await restNewPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true }) - expect(tested.statusCode).toBe(HTTP_STATUS.NO_CONTENT) + expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.OK) }) test('getUtilisateurTitreAbonner', async () => { @@ -567,9 +592,9 @@ test('getUtilisateurTitreAbonner', async () => { let tested = await restNewCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper) expect(tested.body).toBe(false) - const abonnement = await restPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true }) + const abonnement = await restNewPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true }) - expect(abonnement.statusCode).toBe(HTTP_STATUS.NO_CONTENT) + expect(abonnement.statusCode, JSON.stringify(abonnement.body)).toBe(HTTP_STATUS.OK) tested = await restNewCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper) diff --git a/packages/api/src/api/rest/titres.ts b/packages/api/src/api/rest/titres.ts index 3f31cf708..61a0ded54 100644 --- a/packages/api/src/api/rest/titres.ts +++ b/packages/api/src/api/rest/titres.ts @@ -1,9 +1,9 @@ import { titreArchive, titresGet, titreGet, titreUpdate } from '../../database/queries/titres' import { HTTP_STATUS } from 'camino-common/src/http' -import { CommonTitreAdministration, editableTitreValidator, TitreLink, TitreLinks, TitreGet, utilisateurTitreAbonneValidator, titreGetValidator, SuperTitre } from 'camino-common/src/titres' +import { CommonTitreAdministration, TitreLink, TitreLinks, TitreGet, titreGetValidator, SuperTitre } from 'camino-common/src/titres' import { CaminoRequest, CustomResponse } from './express-type' import { userSuper } from '../../database/user-super' -import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, onlyUnique } from 'camino-common/src/typescript-tools' +import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, onlyUnique } from 'camino-common/src/typescript-tools' import { CaminoApiError } from '../../types' import { NotNullableKeys, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools' import TitresTitres from '../../database/models/titres--titres' @@ -17,9 +17,8 @@ import { ETAPES_TYPES, EtapeTypeId } from 'camino-common/src/static/etapesTypes' import { CaminoDate, getCurrent } from 'camino-common/src/date' import { isAdministration, isSuper, User, UserNotNull } from 'camino-common/src/roles' import { canEditDemarche, canCreateTravaux } from 'camino-common/src/permissions/titres-demarches' -import { utilisateurTitreCreate, utilisateurTitreDelete } from '../../database/queries/utilisateurs-titres' import { titreUpdateTask } from '../../business/titre-update' -import { getDoublonsByTitreId, getTitre as getTitreDb } from './titres.queries' +import { getDoublonsByTitreId, getTitre as getTitreDb, updateUtilisateurTitre } from './titres.queries' import type { Pool } from 'pg' import { TitresStatutIds } from 'camino-common/src/static/titresStatuts' import { accesSuperSeulementError, getTitresWithBrouillons, GetTitresWithBrouillonsErrors, getTitreUtilisateur } from '../../database/queries/titres-utilisateurs.queries' @@ -30,6 +29,8 @@ import { CaminoError } from 'camino-common/src/zod-tools' import { EffectDbQueryAndValidateErrors } from '../../pg-database' import { CaminoMachines, machineFind } from '../../business/rules-demarches/machines' import { demarcheEnregistrementDemandeDateFind } from 'camino-common/src/demarche' +import { DBTitre } from '../../database/models/titres' +import { AdministrationId } from 'camino-common/src/static/administrations' const etapesAMasquer = [ETAPES_TYPES.classementSansSuite, ETAPES_TYPES.desistementDuDemandeur, ETAPES_TYPES.demandeDeComplements_RecevabiliteDeLaDemande_] @@ -395,41 +396,28 @@ export const removeTitre = } } -export const utilisateurTitreAbonner = - (_pool: Pool) => - async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => { - const user = req.auth - const parsedBody = utilisateurTitreAbonneValidator.safeParse(req.body) - const titreId: TitreId | undefined | null = titreIdValidator.nullable().optional().parse(req.params.titreId) - if (isNullOrUndefined(titreId)) { - res.sendStatus(HTTP_STATUS.BAD_REQUEST) - } else if (!parsedBody.success) { - res.sendStatus(HTTP_STATUS.BAD_REQUEST) - } else { - try { - if (!user) { - res.sendStatus(HTTP_STATUS.BAD_REQUEST) - } else { - const titre = await titreGet(titreId, { fields: { id: {} } }, user) - - if (!titre) { - res.sendStatus(HTTP_STATUS.FORBIDDEN) - } else { - if (parsedBody.data.abonne) { - await utilisateurTitreCreate({ utilisateurId: user.id, titreId }) - } else { - await utilisateurTitreDelete(user.id, titreId) - } - } - res.sendStatus(HTTP_STATUS.NO_CONTENT) - } - } catch (e) { - console.error(e) - - res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR) - } - } - } +const erreurLorsDeLaRecuperationDuTitre = 'Erreur lors de la récupération du titre' as const +type UtilisateurTitreAbonnerErrors = typeof erreurLorsDeLaRecuperationDuTitre | EffectDbQueryAndValidateErrors +export const utilisateurTitreAbonner: RestNewPostCall<'/rest/titres/:titreId/abonne'> = (rootPipe): Effect.Effect<{ id: TitreId; abonne: boolean }, CaminoApiError<UtilisateurTitreAbonnerErrors>> => + rootPipe.pipe( + Effect.bind('titre', ({ user, params }) => + Effect.tryPromise({ + try: () => titreGet(params.titreId, { fields: { id: {} } }, user), + catch: e => ({ message: erreurLorsDeLaRecuperationDuTitre, extra: e }), + }) + ), + Effect.tap(({ pool, params, body, user }) => updateUtilisateurTitre(pool, body.abonne, params.titreId, user.id)), + Effect.map(({ body, params }) => ({ id: params.titreId, abonne: body.abonne })), + Effect.mapError(caminoError => + Match.value(caminoError.message).pipe( + Match.whenOr('Erreur lors de la récupération du titre', "Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({ + ...caminoError, + status: HTTP_STATUS.INTERNAL_SERVER_ERROR, + })), + Match.exhaustive + ) + ) + ) type GetUtilisateurTitreAbonnerError = EffectDbQueryAndValidateErrors | typeof droitInsuffisant export const getUtilisateurTitreAbonner: RestNewGetCall<'/rest/titres/:titreId/abonne'> = (rootPipe): Effect.Effect<boolean, CaminoApiError<GetUtilisateurTitreAbonnerError>> => @@ -451,45 +439,70 @@ export const getUtilisateurTitreAbonner: RestNewGetCall<'/rest/titres/:titreId/a ) ) -export const updateTitre = - (pool: Pool) => - async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => { - const titreId: TitreId | undefined | null = titreIdValidator.optional().nullable().parse(req.params.titreId) - const user = req.auth - const parsedBody = editableTitreValidator.safeParse(req.body) - if (!titreId) { - res.sendStatus(HTTP_STATUS.BAD_REQUEST) - } else if (!parsedBody.success) { - res.sendStatus(HTTP_STATUS.BAD_REQUEST) - } else if (titreId !== parsedBody.data.id) { - res.sendStatus(HTTP_STATUS.BAD_REQUEST) - } else { - try { - const titreOld = await titreGet(titreId, { fields: { pointsEtape: { id: {} } } }, user) - - if (isNullOrUndefined(titreOld)) { - res.sendStatus(HTTP_STATUS.NOT_FOUND) - } else { - if (isNullOrUndefined(titreOld.administrationsLocales)) { - throw new Error("pas d'administrations locales chargées") - } - - if (!canEditTitre(user, titreOld.typeId, titreOld.titreStatutId, titreOld.administrationsLocales ?? [])) { - res.sendStatus(HTTP_STATUS.FORBIDDEN) - } else { - await titreUpdate(titreId, parsedBody.data) - - await titreUpdateTask(pool, titreId) - res.sendStatus(HTTP_STATUS.NO_CONTENT) - } - } - } catch (e) { - console.error(e) +const incoherenceTitreId = "l'id du titre doit être le même que sa route" as const +const recuperationTitreImpossible = 'Impossible de récupérer le titre' as const +const titreNonTrouve = 'Titre non trouvé' as const +const administrationsNonChargees = 'Administrations non chargées' as const +const droitsInsuffisantsPourModifierLeTitre = 'Droits insuffisants pour modifier le titre' as const +const erreurPendantLaMiseAJourDuTitre = 'Erreur durant la mise à jour du titre' as const +type UpdateTitreErrors = + | typeof incoherenceTitreId + | typeof recuperationTitreImpossible + | typeof titreNonTrouve + | typeof administrationsNonChargees + | typeof droitsInsuffisantsPourModifierLeTitre + | typeof erreurPendantLaMiseAJourDuTitre +export const updateTitre: RestNewPostCall<'/rest/titres/:titreId'> = (rootPipe): Effect.Effect<{ id: TitreId }, CaminoApiError<UpdateTitreErrors>> => + rootPipe.pipe( + Effect.filterOrFail( + ({ body, params }) => params.titreId === body.id, + () => ({ message: incoherenceTitreId }) + ), + Effect.bind('titreOld', ({ user, params }) => + Effect.tryPromise({ + try: () => titreGet(params.titreId, { fields: { pointsEtape: { id: {} } } }, user), + catch: e => ({ message: recuperationTitreImpossible, extra: e }), + }).pipe( + Effect.filterOrFail( + titreOld => isNotNullNorUndefined(titreOld), + () => ({ message: titreNonTrouve }) + ), + Effect.filterOrFail( + (titreOld): titreOld is DBTitre & { administrationsLocales: AdministrationId[] } => isNotNullNorUndefined(titreOld.administrationsLocales), + () => ({ message: administrationsNonChargees }) + ) + ) + ), + Effect.filterOrFail( + ({ user, titreOld }) => canEditTitre(user, titreOld.typeId, titreOld.titreStatutId, titreOld.administrationsLocales), + () => ({ message: droitsInsuffisantsPourModifierLeTitre }) + ), + Effect.tap(({ body, pool }) => + Effect.tryPromise({ + try: async () => { + await titreUpdate(body.id, body) - res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR) - } - } - } + await titreUpdateTask(pool, body.id) + }, + catch: e => ({ message: erreurPendantLaMiseAJourDuTitre, extra: e }), + }) + ), + Effect.map(({ body }) => ({ id: body.id })), + Effect.mapError(caminoError => + Match.value(caminoError.message).pipe( + Match.when('Titre non trouvé', () => ({ ...caminoError, status: HTTP_STATUS.NOT_FOUND })), + Match.whenOr('Impossible de récupérer le titre', 'Erreur durant la mise à jour du titre', () => ({ + ...caminoError, + status: HTTP_STATUS.INTERNAL_SERVER_ERROR, + })), + Match.whenOr("l'id du titre doit être le même que sa route", 'Administrations non chargées', 'Droits insuffisants pour modifier le titre', () => ({ + ...caminoError, + status: HTTP_STATUS.BAD_REQUEST, + })), + Match.exhaustive + ) + ) + ) export const getTitre = (pool: Pool) => diff --git a/packages/api/src/database/models/utilisateurs--titres.ts b/packages/api/src/database/models/utilisateurs--titres.ts deleted file mode 100644 index 08df0387c..000000000 --- a/packages/api/src/database/models/utilisateurs--titres.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Model } from 'objection' -import { IUtilisateurTitre } from '../../types' -import Utilisateurs from './utilisateurs' - -interface UtilisateursTitres extends IUtilisateurTitre {} - -class UtilisateursTitres extends Model { - public static override tableName = 'utilisateurs__titres' - - public static override jsonSchema = { - type: 'object', - required: ['utilisateurId', 'titreId'], - - properties: { - utilisateurId: { type: 'string' }, - titreId: { type: 'string' }, - }, - } - - public static override idColumn = ['utilisateurId', 'titreId'] - - static override relationMappings = () => ({ - utilisateur: { - relation: Model.BelongsToOneRelation, - modelClass: Utilisateurs, - join: { - from: 'utilisateurs__titres.utilisateurId', - to: 'utilisateurs.id', - }, - }, - }) -} - -export default UtilisateursTitres diff --git a/packages/api/src/database/queries/utilisateurs-titres.ts b/packages/api/src/database/queries/utilisateurs-titres.ts deleted file mode 100644 index eb749b763..000000000 --- a/packages/api/src/database/queries/utilisateurs-titres.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UtilisateurId } from 'camino-common/src/roles' -import { IUtilisateurTitre } from '../../types' - -import UtilisateursTitres from '../models/utilisateurs--titres' -import { TitreId } from 'camino-common/src/validators/titres' - -export const utilisateurTitreCreate = async (utilisateurTitre: IUtilisateurTitre): Promise<UtilisateursTitres> => UtilisateursTitres.query().insert(utilisateurTitre) - -export const utilisateurTitreDelete = async (utilisateurId: UtilisateurId, titreId: TitreId): Promise<number> => UtilisateursTitres.query().deleteById([utilisateurId, titreId]) diff --git a/packages/api/src/database/queries/utilisateurs.test.integration.ts b/packages/api/src/database/queries/utilisateurs.test.integration.ts index a31c24625..b3f80fe99 100644 --- a/packages/api/src/database/queries/utilisateurs.test.integration.ts +++ b/packages/api/src/database/queries/utilisateurs.test.integration.ts @@ -9,7 +9,7 @@ import { getCurrent } from 'camino-common/src/date' import { callAndExit } from '../../tools/fp-tools' import { userSuper } from '../user-super' import { createTitre } from '../../api/rest/titre-demande.queries' -import { utilisateurTitreCreate } from './utilisateurs-titres' +import { updateUtilisateurTitre } from '../../api/rest/titres.queries' console.info = vi.fn() console.error = vi.fn() @@ -191,7 +191,7 @@ describe('getUtilisateursByTitreId', () => { let result = await getUtilisateursByTitreId(dbPool, titreId) expect(result).toHaveLength(0) - await utilisateurTitreCreate({ titreId, utilisateurId }) + await callAndExit(updateUtilisateurTitre(dbPool, true, titreId, utilisateurId)) result = await getUtilisateursByTitreId(dbPool, titreId) expect(result[0]).toMatchObject({ diff --git a/packages/api/src/server/rest.ts b/packages/api/src/server/rest.ts index 9d82737a4..353fd1af8 100644 --- a/packages/api/src/server/rest.ts +++ b/packages/api/src/server/rest.ts @@ -27,7 +27,6 @@ import { DownloadFormat, contentTypes, GetRestRoutes, - PostRestRoutes, DeleteRestRoutes, isCaminoRestRoute, DownloadRestRoutes, @@ -139,13 +138,11 @@ export type RestNewDeleteCall<Route extends NewDeleteRestRoutes> = ( > ) => Effect.Effect<Option.Option<never>, CaminoApiError<string>> -type RestPostCall<Route extends PostRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['post']['output']>>) => Promise<void> type RestDeleteCall = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<void | Error>) => Promise<void> type RestDownloadCall = (pool: Pool) => IRestResolver type Transform<Route> = (Route extends GetRestRoutes ? { getCall: RestGetCall<Route> } : {}) & (Route extends NewGetRestRoutes ? { newGetCall: RestNewGetCall<Route> } : {}) & - (Route extends PostRestRoutes ? { postCall: RestPostCall<Route> } : {}) & (Route extends NewPostRestRoutes ? { newPostCall: RestNewPostCall<Route> } : {}) & (Route extends NewPutRestRoutes ? { newPutCall: RestNewPutCall<Route> } : {}) & (Route extends DeleteRestRoutes ? { deleteCall: RestDeleteCall } : {}) & @@ -192,8 +189,8 @@ const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<k '/rest/etapesTypes/:demarcheId/:date': { newGetCall: getEtapesTypesEtapesStatusWithMainStep, ...CaminoRestRoutes['/rest/etapesTypes/:demarcheId/:date'] }, '/rest/quickAccess': { newGetCall: quickAccessSearch, ...CaminoRestRoutes['/rest/quickAccess'] }, '/rest/titres': { newPostCall: titreDemandeCreer, ...CaminoRestRoutes['/rest/titres'] }, - '/rest/titres/:titreId': { deleteCall: removeTitre, postCall: updateTitre, getCall: getTitre, ...CaminoRestRoutes['/rest/titres/:titreId'] }, - '/rest/titres/:titreId/abonne': { postCall: utilisateurTitreAbonner, newGetCall: getUtilisateurTitreAbonner, ...CaminoRestRoutes['/rest/titres/:titreId/abonne'] }, + '/rest/titres/:titreId': { deleteCall: removeTitre, newPostCall: updateTitre, getCall: getTitre, ...CaminoRestRoutes['/rest/titres/:titreId'] }, + '/rest/titres/:titreId/abonne': { newPostCall: utilisateurTitreAbonner, newGetCall: getUtilisateurTitreAbonner, ...CaminoRestRoutes['/rest/titres/:titreId/abonne'] }, '/rest/titresAdministrations': { getCall: titresAdministrations, ...CaminoRestRoutes['/rest/titresAdministrations'] }, '/rest/titresSuper': { newGetCall: titresSuper, ...CaminoRestRoutes['/rest/titresSuper'] }, '/rest/statistiques/minerauxMetauxMetropole': { getCall: getMinerauxMetauxMetropolesStats, ...CaminoRestRoutes['/rest/statistiques/minerauxMetauxMetropole'] }, // UNTESTED YET @@ -328,11 +325,6 @@ export const restWithPool = (dbPool: Pool): Router => { } }) } - if ('postCall' in maRoute) { - console.info(`POST ${route}`) - rest.post(route, restCatcherWithMutation('post', maRoute.postCall(dbPool), dbPool)) // eslint-disable-line @typescript-eslint/no-misused-promises - } - if ('newPostCall' in maRoute) { console.info(`POST ${route}`) // eslint-disable-next-line @typescript-eslint/no-misused-promises diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 0381df06d..daa204293 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -271,11 +271,6 @@ export interface ITitreEtapeFiltre { dateFin?: string } -export interface IUtilisateurTitre { - utilisateurId: string - titreId: string -} - export type Context = { user: User; pool: Pool } export interface IJournaux { diff --git a/packages/api/tests/_utils/index.ts b/packages/api/tests/_utils/index.ts index 93eba7dd1..702a8a7f4 100644 --- a/packages/api/tests/_utils/index.ts +++ b/packages/api/tests/_utils/index.ts @@ -15,7 +15,6 @@ import { DeleteRestRoutes, getRestRoute, GetRestRoutes, - PostRestRoutes, CaminoRestParams, DownloadRestRoutes, NewPostRestRoutes, @@ -83,17 +82,6 @@ export const restNewCall = async <Route extends NewGetRestRoutes>( return jwtSet(pool, req, user) } -export const restPostCall = async <Route extends PostRestRoutes>( - pool: Pool, - caminoRestRoute: Route, - params: CaminoRestParams<Route>, - user: TestUser | undefined, - body: z.infer<(typeof CaminoRestRoutes)[Route]['post']['input']> -): Promise<request.Test> => { - const req = request(app(pool)).post(getRestRoute(caminoRestRoute, params)).send(body) - - return jwtSet(pool, req, user) -} export const restNewPostCall = async <Route extends NewPostRestRoutes>( pool: Pool, caminoRestRoute: Route, diff --git a/packages/common/src/rest.ts b/packages/common/src/rest.ts index 78f248921..b2117f08e 100644 --- a/packages/common/src/rest.ts +++ b/packages/common/src/rest.ts @@ -72,7 +72,6 @@ import { flattenEtapeValidator, restEtapeCreationValidator, restEtapeModificatio type CaminoRoute<T extends string> = { params: ZodObjectParsUrlParams<T> } & { get?: { output: ZodType; searchParams?: ZodType } newGet?: { output: ZodType; searchParams?: ZodType } - post?: { input: ZodType; output: ZodType } newPost?: { input: ZodType; output: ZodType } newPut?: { input: ZodType; output: ZodType } newDelete?: true @@ -176,8 +175,17 @@ export const CaminoRestRoutes = { '/rest/statistiques/datagouv': { params: noParamsValidator, get: { output: z.array(statistiquesDataGouvValidator) } }, '/rest/quickAccess': { params: noParamsValidator, newGet: { searchParams: quickAccessSearchParamsValidator, output: quickAccessArrayResultValidator } }, '/rest/titres': { params: noParamsValidator, newPost: { input: titreDemandeValidator, output: titreDemandeOutputValidator } }, - '/rest/titres/:titreId': { params: z.object({ titreId: titreIdOrSlugValidator }), get: { output: titreGetValidator }, delete: true, post: { output: z.void(), input: editableTitreValidator } }, - '/rest/titres/:titreId/abonne': { params: z.object({ titreId: titreIdValidator }), post: { input: utilisateurTitreAbonneValidator, output: z.void() }, newGet: { output: z.boolean() } }, + '/rest/titres/:titreId': { + params: z.object({ titreId: titreIdOrSlugValidator }), + get: { output: titreGetValidator }, + delete: true, + newPost: { output: z.object({ id: titreIdValidator }), input: editableTitreValidator }, + }, + '/rest/titres/:titreId/abonne': { + params: z.object({ titreId: titreIdValidator }), + newPost: { input: utilisateurTitreAbonneValidator, output: z.object({ id: titreIdValidator, abonne: z.boolean() }) }, + newGet: { output: z.boolean() }, + }, '/rest/titresAdministrations': { params: noParamsValidator, get: { output: z.array(titreAdministrationValidator) } }, '/rest/titresSuper': { params: noParamsValidator, newGet: { output: z.array(superTitreValidator) } }, '/rest/titres/:id/titreLiaisons': { params: z.object({ id: titreIdValidator }), newGet: { output: titreLinksValidator }, newPost: { input: z.array(titreIdValidator), output: titreLinksValidator } }, @@ -310,7 +318,6 @@ type CaminoRestRouteList<Route, Method extends MethodVerb> = Route extends reado export type GetRestRoutes = CaminoRestRouteList<typeof IDS, 'get'>[number] export type NewGetRestRoutes = CaminoRestRouteList<typeof IDS, 'newGet'>[number] -export type PostRestRoutes = CaminoRestRouteList<typeof IDS, 'post'>[number] export type NewPostRestRoutes = CaminoRestRouteList<typeof IDS, 'newPost'>[number] export type NewPutRestRoutes = CaminoRestRouteList<typeof IDS, 'newPut'>[number] export type DeleteRestRoutes = CaminoRestRouteList<typeof IDS, 'delete'>[number] diff --git a/packages/ui/src/api/client-rest.ts b/packages/ui/src/api/client-rest.ts index 1128d1874..e6f4d1a9b 100644 --- a/packages/ui/src/api/client-rest.ts +++ b/packages/ui/src/api/client-rest.ts @@ -8,7 +8,6 @@ import { NewDownloadRestRoutes, getRestRoute, GetRestRoutes, - PostRestRoutes, NewPostRestRoutes, NewGetRestRoutes, NewPutRestRoutes, @@ -194,15 +193,6 @@ export const newDeleteWithJson = async <T extends NewDeleteRestRoutes>(path: T, export const deleteWithJson = async <T extends DeleteRestRoutes>(path: T, params: CaminoRestParams<T>, searchParams: Record<string, string | string[]> = {}): Promise<void> => callFetch(path, params, 'delete', searchParams) -/** - * @deprecated use newPostWithJson - **/ -export const postWithJson = async <T extends PostRestRoutes>( - path: T, - params: CaminoRestParams<T>, - body: z.infer<(typeof CaminoRestRoutes)[T]['post']['input']> -): Promise<z.infer<(typeof CaminoRestRoutes)[T]['post']['output']>> => callFetch(path, params, 'post', {}, body) - export const newPostWithJson = async <T extends NewPostRestRoutes>( path: T, params: CaminoRestParams<T>, diff --git a/packages/ui/src/components/titre.stories.tsx b/packages/ui/src/components/titre.stories.tsx index d36f31c9a..4ea689d83 100644 --- a/packages/ui/src/components/titre.stories.tsx +++ b/packages/ui/src/components/titre.stories.tsx @@ -216,10 +216,10 @@ const titre = { } as const satisfies TitreGet const apiClient: PropsApiClient = { - editTitre: (...params) => { + editTitre: params => { editTitreAction(params) - return Promise.resolve() + return Promise.resolve({ id: params.id }) }, deleteDemarche: (...params) => { deleteDemarcheAction(params) @@ -246,10 +246,10 @@ const apiClient: PropsApiClient = { return Promise.resolve(true) }, - titreUtilisateurAbonne: (...params) => { - titreUtilisateurAbonneAction(params) + titreUtilisateurAbonne: (params, abonne) => { + titreUtilisateurAbonneAction(params, abonne) - return Promise.resolve() + return Promise.resolve({ id: params, abonne }) }, loadLinkableTitres: (...params) => diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html index dd1f3f99a..8127e6df8 100644 --- a/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html +++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html @@ -7,7 +7,7 @@ <!----> </button><button class="fr-btn fr-btn--secondary fr-btn--md fr-icon-delete-bin-line fr-ml-2w" title="Supprimer le titre" aria-label="Supprimer le titre" type="button"> <!----> - </button><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S’abonner" aria-label="S’abonner" type="button" style="margin-right: 0px;">S’abonner</button></div> + </button><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S'abonner" aria-label="S'abonner" type="button" style="margin-right: 0px;">S'abonner</button></div> </div> <div class="fr-grid-row fr-grid-row--middle fr-mt-1w"> <p class="fr-h3 fr-m-0">Concession</p> diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html index 623c22f70..9cbb892ba 100644 --- a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html +++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html @@ -6,7 +6,7 @@ <div class="fr-m-0" style="margin-left: auto; display: flex;"><a class="fr-btn fr-btn--secondary fr-pl-2w fr-pr-2w fr-link" title="Signaler une erreur" href="mailto:camino@beta.gouv.fr?subject=Erreur m-cx-chantepie-1988&body=Bonjour, j'ai repéré une erreur sur le titre http://localhost:3000/ :" target="_blank" style="margin-left: auto; display: flex;" rel="noopener external">Signaler une erreur</a><button class="fr-btn fr-btn--secondary fr-btn--md fr-icon-pencil-line fr-ml-2w" title="Éditer le titre" aria-label="Éditer le titre" type="button"> <!----> </button> - <!----><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S’abonner" aria-label="S’abonner" type="button" style="margin-right: 0px;">S’abonner</button> + <!----><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S'abonner" aria-label="S'abonner" type="button" style="margin-right: 0px;">S'abonner</button> </div> </div> <div class="fr-grid-row fr-grid-row--middle fr-mt-1w"> diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html index 3fd0efc37..750b4927d 100644 --- a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html +++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html @@ -5,7 +5,7 @@ <p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; align-self: center;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="valide" aria-label="valide">valide</p> <div class="fr-m-0" style="margin-left: auto; display: flex;"><a class="fr-btn fr-btn--secondary fr-pl-2w fr-pr-2w fr-link" title="Signaler une erreur" href="mailto:camino@beta.gouv.fr?subject=Erreur m-cx-chantepie-1988&body=Bonjour, j'ai repéré une erreur sur le titre http://localhost:3000/ :" target="_blank" style="margin-left: auto; display: flex;" rel="noopener external">Signaler une erreur</a> <!----> - <!----><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S’abonner" aria-label="S’abonner" type="button" style="margin-right: 0px;">S’abonner</button> + <!----><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S'abonner" aria-label="S'abonner" type="button" style="margin-right: 0px;">S'abonner</button> </div> </div> <div class="fr-grid-row fr-grid-row--middle fr-mt-1w"> diff --git a/packages/ui/src/components/titre/edit-popup.stories.tsx b/packages/ui/src/components/titre/edit-popup.stories.tsx index 8a7dc5ccb..bc9bd047a 100644 --- a/packages/ui/src/components/titre/edit-popup.stories.tsx +++ b/packages/ui/src/components/titre/edit-popup.stories.tsx @@ -29,10 +29,10 @@ export const DefaultNoReference: StoryFn = () => ( }} close={close} apiClient={{ - editTitre: (...params) => { + editTitre: params => { editTitreAction(params) - return Promise.resolve() + return Promise.resolve({ id: params.id }) }, }} /> @@ -48,10 +48,10 @@ export const OneReference: StoryFn = () => ( }} close={close} apiClient={{ - editTitre: (...params) => { + editTitre: params => { editTitreAction(params) - return Promise.resolve() + return Promise.resolve({ id: params.id }) }, }} /> diff --git a/packages/ui/src/components/titre/edit-popup.tsx b/packages/ui/src/components/titre/edit-popup.tsx index 077a716d8..5d2823f48 100644 --- a/packages/ui/src/components/titre/edit-popup.tsx +++ b/packages/ui/src/components/titre/edit-popup.tsx @@ -38,12 +38,16 @@ export const EditPopup = defineComponent<Props>(props => { close={props.close} validate={{ action: async () => { - await props.apiClient.editTitre({ + const value = await props.apiClient.editTitre({ id: props.titre.id, nom: nom.value, references: references.value, }) + if ('message' in value) { + return value + } props.reload() + return value }, text: 'Enregistrer', }} diff --git a/packages/ui/src/components/titre/titre-abonner-button.stories.tsx b/packages/ui/src/components/titre/titre-abonner-button.stories.tsx index 421e2ad43..403a416be 100644 --- a/packages/ui/src/components/titre/titre-abonner-button.stories.tsx +++ b/packages/ui/src/components/titre/titre-abonner-button.stories.tsx @@ -24,7 +24,7 @@ const apiClient: Pick<TitreApiClient, 'getTitreUtilisateurAbonne' | 'titreUtilis titreUtilisateurAbonne: (...params) => { titreUtilisateurAbonneAction(params) - return Promise.resolve() + return Promise.resolve({ id: params[0], abonne: params[1] }) }, } const titreId = titreIdValidator.parse('titreId') @@ -55,6 +55,6 @@ export const WithError: StoryFn = () => ( <TitreAbonnerButton user={{ ...testBlankUser, role: 'super' }} titreId={titreId} - apiClient={{ ...apiClient, getTitreUtilisateurAbonne: () => Promise.reject(new Error('Une erreur est survenue')) }} + apiClient={{ ...apiClient, getTitreUtilisateurAbonne: () => Promise.resolve({ message: 'Une erreur est survenue' }) }} /> ) diff --git a/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_NotAbonne.html b/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_NotAbonne.html index 4faa39c54..ab1b2c34b 100644 --- a/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_NotAbonne.html +++ b/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_NotAbonne.html @@ -1 +1 @@ -<button class="fr-btn fr-btn--primary fr-btn--md" title="S’abonner" aria-label="S’abonner" type="button" style="margin-right: 0px;">S’abonner</button> \ No newline at end of file +<button class="fr-btn fr-btn--primary fr-btn--md" title="S'abonner" aria-label="S'abonner" type="button" style="margin-right: 0px;">S'abonner</button> \ No newline at end of file diff --git a/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_WithError.html b/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_WithError.html index 5cd6a4951..96ab73995 100644 --- a/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_WithError.html +++ b/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_WithError.html @@ -1,7 +1,7 @@ <div class="" style="display: flex; justify-content: center;"> - <div class="fr-alert fr-alert--error fr-alert--sm"> + <!----> + <div class="fr-alert fr-alert--error fr-alert--sm" role="alert"> <p>Une erreur est survenue</p> </div> <!----> - <!----> </div> \ No newline at end of file diff --git a/packages/ui/src/components/titre/titre-abonner-button.tsx b/packages/ui/src/components/titre/titre-abonner-button.tsx index fea749903..7a9f19c49 100644 --- a/packages/ui/src/components/titre/titre-abonner-button.tsx +++ b/packages/ui/src/components/titre/titre-abonner-button.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, defineComponent, onMounted, ref } from 'vue' +import { HTMLAttributes, defineComponent, onMounted } from 'vue' import { TitreId } from 'camino-common/src/validators/titres' import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools' import { User } from 'camino-common/src/roles' @@ -6,6 +6,7 @@ import { DsfrButton } from '../_ui/dsfr-button' import { LoadingElement } from '../_ui/functional-loader' import { TitreApiClient } from './titre-api-client' import { AsyncData } from '../../api/client-rest' +import { useState } from '@/utils/vue-tsx-utils' type Props = { apiClient: Pick<TitreApiClient, 'getTitreUtilisateurAbonne' | 'titreUtilisateurAbonne'> @@ -15,12 +16,16 @@ type Props = { } export const TitreAbonnerButton = defineComponent<Props>(props => { - const data = ref<AsyncData<boolean>>({ status: 'LOADING' }) + const [data, setData] = useState<AsyncData<boolean>>({ status: 'LOADING' }) const toggleAbonner = async () => { if (data.value.status === 'LOADED') { - await props.apiClient.titreUtilisateurAbonne(props.titreId, !data.value.value) - data.value.value = !data.value.value + const result = await props.apiClient.titreUtilisateurAbonne(props.titreId, !data.value.value) + if ('message' in result) { + setData({ status: 'NEW_ERROR', error: result }) + } else { + setData({ status: 'LOADED', value: !data.value.value }) + } } } @@ -29,17 +34,17 @@ export const TitreAbonnerButton = defineComponent<Props>(props => { if (isNotNullNorUndefined(props.user)) { const abonne = await props.apiClient.getTitreUtilisateurAbonne(props.titreId) if (typeof abonne !== 'boolean') { - data.value = { status: 'NEW_ERROR', error: abonne } + setData({ status: 'NEW_ERROR', error: abonne }) } else { - data.value = { status: 'LOADED', value: abonne } + setData({ status: 'LOADED', value: abonne }) } } } catch (e: any) { console.error('error', e) - data.value = { + setData({ status: 'ERROR', message: e.message ?? 'something wrong happened', - } + }) } }) @@ -49,7 +54,7 @@ export const TitreAbonnerButton = defineComponent<Props>(props => { <LoadingElement data={data.value} renderItem={isAbonne => ( - <DsfrButton class={props.class} style={{ marginRight: 0 }} buttonType={isAbonne ? 'secondary' : 'primary'} title={isAbonne ? 'Se désabonner' : 'S’abonner'} onClick={toggleAbonner} /> + <DsfrButton class={props.class} style={{ marginRight: 0 }} buttonType={isAbonne ? 'secondary' : 'primary'} title={isAbonne ? 'Se désabonner' : "S'abonner"} onClick={toggleAbonner} /> )} /> ) : null} diff --git a/packages/ui/src/components/titre/titre-api-client.ts b/packages/ui/src/components/titre/titre-api-client.ts index 1595e1910..b34c9827b 100644 --- a/packages/ui/src/components/titre/titre-api-client.ts +++ b/packages/ui/src/components/titre/titre-api-client.ts @@ -1,6 +1,6 @@ import { EditableTitre, QuickAccessResult, TitreDemande, TitreDemandeOutput, TitreGet } from 'camino-common/src/titres' import { TitreId, TitreIdOrSlug } from 'camino-common/src/validators/titres' -import { deleteWithJson, getWithJson, newGetWithJson, newPostWithJson, postWithJson } from '../../api/client-rest' +import { deleteWithJson, getWithJson, newGetWithJson, newPostWithJson } from '../../api/client-rest' import { CaminoDate } from 'camino-common/src/date' import { CommuneId } from 'camino-common/src/static/communes' import { EntrepriseId } from 'camino-common/src/entreprise' @@ -42,9 +42,9 @@ type TitreForTitresRerchercherByNom = { export interface TitreApiClient { removeTitre: (titreId: TitreId) => Promise<void> - titreUtilisateurAbonne: (titreId: TitreId, abonne: boolean) => Promise<void> + titreUtilisateurAbonne: (titreId: TitreId, abonne: boolean) => Promise<{ id: TitreId; abonne: boolean } | CaminoError<string>> getTitreUtilisateurAbonne: (titreId: TitreId) => Promise<boolean | CaminoError<string>> - editTitre: (titre: EditableTitre) => Promise<void> + editTitre: (titre: EditableTitre) => Promise<{ id: TitreId } | CaminoError<string>> getTitreById: (titreId: TitreIdOrSlug) => Promise<TitreGet> getTitresForTable: (params: { page?: number @@ -111,14 +111,14 @@ export const titreApiClient: TitreApiClient = { removeTitre: async (titreId: TitreId): Promise<void> => { return deleteWithJson('/rest/titres/:titreId', { titreId }) }, - titreUtilisateurAbonne: async (titreId: TitreId, abonne: boolean): Promise<void> => { - return postWithJson('/rest/titres/:titreId/abonne', { titreId }, { abonne }) + titreUtilisateurAbonne: async (titreId: TitreId, abonne: boolean): Promise<{ id: TitreId; abonne: boolean } | CaminoError<string>> => { + return newPostWithJson('/rest/titres/:titreId/abonne', { titreId }, { abonne }) }, getTitreUtilisateurAbonne: async (titreId: TitreId): Promise<boolean | CaminoError<string>> => { return newGetWithJson('/rest/titres/:titreId/abonne', { titreId }) }, - editTitre: (titre: EditableTitre): Promise<void> => { - return postWithJson('/rest/titres/:titreId', { titreId: titre.id }, titre) + editTitre: (titre: EditableTitre): Promise<{ id: TitreId } | CaminoError<string>> => { + return newPostWithJson('/rest/titres/:titreId', { titreId: titre.id }, titre) }, getTitreById: (titreId: TitreIdOrSlug): Promise<TitreGet> => { return getWithJson('/rest/titres/:titreId', { titreId }) -- GitLab