diff --git a/packages/api/src/api/rest/titre-demande.ts b/packages/api/src/api/rest/titre-demande.ts index 5f5102a0998713bbad71141d801fda6b87fadbf4..6cfa6427f7db62dd8db1a142486cc523f494761b 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 5849ddd8da9644f14dddcb2e18ada6de1eb346cd..2ba314256f6473be76423c66dc6a2d6d146d88ca 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 d19930efc06169cc3e03a390e960f8c588d8cb08..6852ce711a76133cec23c6e870b5b84214112e6b 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 9803250238f0373e55f6a6ef42047714fedf7009..950248e4829cc2955bcda19504c2d62b2b94cd8f 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 3f31cf708289ffeba701ef26c4ad20d61d3c60be..61a0ded5431cae9b964e9771c129dabbcf587996 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 08df0387cf7ed7c480bfa12f9d0a5a3a6052fc5e..0000000000000000000000000000000000000000 --- 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 eb749b763cef75c0c83f5247a8fc4682a142a254..0000000000000000000000000000000000000000 --- 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 a31c24625feb2ac7f64133b31cb43b45902ed201..b3f80fe99e6350ef158da007b90ba27846bf8ce3 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 9d82737a4d4b19c5e9d47ce933942fa76a02c510..353fd1af841d3d879059818e4aad36b49b63643b 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 0381df06dea89e69f7e0e68683879990cbf5698b..daa20429336353f9121301bc8c2a2b5103dc74f8 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 93eba7dd11e9f1f9213e4a785d7d15b6578f314e..702a8a7f4a4ab8a00f8b550e7c889c0db3179d56 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 78f248921e440a7869745da1ce84bc58b8dd4adf..b2117f08ea3113e7fdc41df42a2371a110941686 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 1128d1874bc7deed26eb4d8d6c6dd991e653ce2c..e6f4d1a9b335edce47447abafa6347af9ae2000a 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 d36f31c9a384988cb1f4a7969a7eb832782f500d..4ea689d83953680e8da401a34001c8085b40b35b 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 dd1f3f99a5a845276d617003d13ebe4c0ca6d50c..8127e6df8671ea7f5c0c5e3b38caeb7f8fda8cc9 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 623c22f7028478f24b0a797cdf4aa02406e1bba6..9cbb892ba320871d4313c040aebc8a4da2b1c703 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 3fd0efc376ddf048801234f29ff339d47a07a69b..750b4927d90fcf06b320272f45dd919cb0a8bb53 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 8a7dc5ccbd9a09f545562f9f1230a263bf766649..bc9bd047a4a6b0913c049efdac9f6b9d07573cc0 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 077a716d813210ebe809ca633b9ae8b954b31b5b..5d2823f48eb401020c6d4f39755686117b1c4844 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 421e2ad43b6220ecf14d9024b761179cf7f58d1f..403a416bef650adaed33a762cfca8836808d65d5 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 4faa39c54b1687c4f6430aa0e97c8b37f6137c1c..ab1b2c34bae5364d7a4441aa943685a46e281504 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 5cd6a49518801e7d551131667e88a40de522a282..96ab73995c352d5866bce731b0fffe7f6fcbc61a 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 fea7499039afd80ca0af795d0a37d8f232e0a8ec..7a9f19c4999190919658731c3468b02eea700bc1 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 1595e19107e99c49fda1b44b4cd4377b66cef8b0..b34c9827bc81d789de659581a865abb014a424f5 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 })