diff --git a/packages/api/src/api/rest/demarches.ts b/packages/api/src/api/rest/demarches.ts index c3de75315aad638db6a9dc1044ddafdade7554e2..1ba20b73e6fef22ccbb4910b2a68242b7dd1fcb7 100644 --- a/packages/api/src/api/rest/demarches.ts +++ b/packages/api/src/api/rest/demarches.ts @@ -25,7 +25,7 @@ import { isDemarcheTypeId, isTravaux } from 'camino-common/src/static/demarchesT import { titreGet } from '../../database/queries/titres' import { CaminoApiError } from '../../types' import { EffectDbQueryAndValidateErrors } from '../../pg-database' -import { callAndExit } from '../../tools/fp-tools' +import { callAndExit, filterOrFailFromValidWithError } from '../../tools/fp-tools' const canReadDemarcheError = 'impossible de savoir si on peut lire la démarche' as const @@ -192,14 +192,9 @@ export const getResultatEnConcurrence: RestNewGetCall<'/rest/demarches/:demarche return rootPipe.pipe( Effect.bind('etapes', ({ pool, params }) => getEtapesByDemarcheId(pool, params.demarcheId)), Effect.bind('demarche', ({ pool, params }) => getDemarcheByIdOrSlug(pool, params.demarcheId)), - Effect.tap(({ etapes, demarche, user }) => { - const result = canPublishResultatMiseEnConcurrence(user, demarche.titre_type_id, demarche.demarche_type_id, etapes, demarche.demarche_id) - if (result.valid) { - return Effect.succeed(null) - } else { - return Effect.fail({ message: 'droits insuffisants' as const, detail: result.error }) - } - }), + Effect.tap(({ etapes, demarche, user }) => + filterOrFailFromValidWithError(canPublishResultatMiseEnConcurrence(user, demarche.titre_type_id, demarche.demarche_type_id, etapes, demarche.demarche_id), 'droits insuffisants' as const) + ), Effect.flatMap(({ pool, params, user }) => getDemarchePivotEnConcurrence(pool, params.demarcheId, user)), Effect.mapError(caminoError => Match.value(caminoError.message).pipe( diff --git a/packages/api/src/api/rest/titres.test.integration.ts b/packages/api/src/api/rest/titres.test.integration.ts index 8701c1a8264e23fe4afb0e27c5ed4a2011801c93..d1193926ad68fca93d6bfb65d163a37aada57b90 100644 --- a/packages/api/src/api/rest/titres.test.integration.ts +++ b/packages/api/src/api/rest/titres.test.integration.ts @@ -141,7 +141,7 @@ async function createTitreWithEtapes( etapes: (Omit<ITitreEtape, 'id' | 'titreDemarcheId' | 'concurrence' | 'demarcheIdsConsentement' | 'hasTitreFrom'> & { demarcheIdsConsentement?: DemarcheId[] })[], entreprises: any ) { - const titreId = newTitreId() + const titreId = newTitreId(`id-${nomTitre}`) await insertTitreGraph({ id: titreId, nom: nomTitre, @@ -212,7 +212,7 @@ describe('titresLiaisons', () => { ) const titreId = getTitres.body[0].id - const axmId = newTitreId() + const axmId = newTitreId('titreIdTitreLie') const axmNom = 'mon axm simple' await insertTitreGraph({ id: axmId, @@ -222,7 +222,7 @@ describe('titresLiaisons', () => { propsTitreEtapesIds: {}, }) - const tested = await restNewPostCall( + let tested = await restNewPostCall( dbPool, '/rest/titres/:id/titreLiaisons', { id: axmId }, @@ -233,15 +233,19 @@ describe('titresLiaisons', () => { [titreId] ) - expect(tested.statusCode).toBe(200) - expect(tested.body.amont).toHaveLength(1) - expect(tested.body.aval).toHaveLength(0) - expect(tested.body.amont[0]).toStrictEqual({ - id: titreId, - nom: getTitres.body[0].nom, - }) + expect(tested.body).toMatchInlineSnapshot(` + { + "amont": [ + { + "id": "id-titre1", + "nom": "titre1", + }, + ], + "aval": [], + } + `) - const avalTested = await restNewCall( + let avalTested = await restNewCall( dbPool, '/rest/titres/:id/titreLiaisons', { id: titreId }, @@ -251,13 +255,52 @@ describe('titresLiaisons', () => { } ) - expect(avalTested.statusCode).toBe(200) - expect(avalTested.body.amont).toHaveLength(0) - expect(avalTested.body.aval).toHaveLength(1) - expect(avalTested.body.aval[0]).toStrictEqual({ - id: axmId, - nom: axmNom, - }) + expect(avalTested.body).toMatchInlineSnapshot(` + { + "amont": [], + "aval": [ + { + "id": "titreIdTitreLie", + "nom": "mon axm simple", + }, + ], + } + `) + + // On vérifie qu'on peut bien supprimer les liens + tested = await restNewPostCall( + dbPool, + '/rest/titres/:id/titreLiaisons', + { id: axmId }, + { + role: 'admin', + administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'], + }, + [] + ) + expect(tested.body).toMatchInlineSnapshot(` + { + "amont": [], + "aval": [], + } + `) + + avalTested = await restNewCall( + dbPool, + '/rest/titres/:id/titreLiaisons', + { id: titreId }, + { + role: 'admin', + administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'], + } + ) + + expect(avalTested.body).toMatchInlineSnapshot(` + { + "amont": [], + "aval": [], + } + `) }) }) diff --git a/packages/api/src/api/rest/titres.ts b/packages/api/src/api/rest/titres.ts index 61a0ded5431cae9b964e9771c129dabbcf587996..c44b988e06999763470ce3264f0959f1f12271ba 100644 --- a/packages/api/src/api/rest/titres.ts +++ b/packages/api/src/api/rest/titres.ts @@ -5,7 +5,7 @@ import { CaminoRequest, CustomResponse } from './express-type' import { userSuper } from '../../database/user-super' import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, onlyUnique } from 'camino-common/src/typescript-tools' import { CaminoApiError } from '../../types' -import { NotNullableKeys, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools' +import { isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools' import TitresTitres from '../../database/models/titres--titres' import { titreAdministrationsGet } from '../_format/titres' import { canDeleteTitre, canEditTitre, canLinkTitres } from 'camino-common/src/permissions/titres' @@ -31,6 +31,7 @@ import { CaminoMachines, machineFind } from '../../business/rules-demarches/mach import { demarcheEnregistrementDemandeDateFind } from 'camino-common/src/demarche' import { DBTitre } from '../../database/models/titres' import { AdministrationId } from 'camino-common/src/static/administrations' +import { filterOrFailFromValid } from '../../tools/fp-tools' const etapesAMasquer = [ETAPES_TYPES.classementSansSuite, ETAPES_TYPES.desistementDuDemandeur, ETAPES_TYPES.demandeDeComplements_RecevabiliteDeLaDemande_] @@ -232,15 +233,16 @@ export const postTitreLiaisons: RestNewPostCall<'/rest/titres/:id/titreLiaisons' user ), catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }), - }) - ), - Effect.filterOrFail( - (binded): binded is NotNullableKeys<typeof binded> => isNotNullNorUndefined(binded.titre), - () => ({ message: droitInsuffisant }) + }).pipe( + Effect.filterOrFail( + titre => isNotNullNorUndefined(titre), + () => ({ message: droitInsuffisant }) + ) + ) ), Effect.bind('administrations', ({ titre }) => - Effect.tryPromise({ - try: async () => titreAdministrationsGet(titre), + Effect.try({ + try: () => titreAdministrationsGet(titre), catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }), }) ), @@ -252,23 +254,16 @@ export const postTitreLiaisons: RestNewPostCall<'/rest/titres/:id/titreLiaisons' ({ titre }) => isNotNullNorUndefined(titre.demarches), () => ({ message: demarcheNonChargeesError }) ), - Effect.bind('titresFrom', ({ body, user }) => - Effect.tryPromise({ - try: async () => titresGet({ ids: [...body] }, { fields: { id: {} } }, user), - catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }), - }) - ), - Effect.tap(({ titre, titresFrom, body }) => { - const result = checkTitreLinks(titre.typeId, body, titresFrom, titre.demarches ?? []) - - if (result.valid) { - return Effect.succeed(null) - } else { - console.warn(result.errors) - - return Effect.fail({ message: result.errors[0], extra: result.errors }) + Effect.bind('titresFrom', ({ body, user }) => { + if (body.length > 0) { + return Effect.tryPromise({ + try: () => titresGet({ ids: [...body] }, { fields: { id: {} } }, user), + catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }), + }) } + return Effect.succeed([]) }), + Effect.tap(({ titre, titresFrom, body }) => filterOrFailFromValid(checkTitreLinks(titre.typeId, body, titresFrom, titre.demarches ?? []))), Effect.tap(({ pool, params, body }) => linkTitres(pool, { linkTo: params.id, linkFrom: body })), Effect.bind('amont', ({ params, user }) => titreLinksGet(params.id, 'titreFromId', user)), Effect.bind('aval', ({ params, user }) => titreLinksGet(params.id, 'titreToId', user)), diff --git a/packages/api/src/business/validations/titre-links-validate.ts b/packages/api/src/business/validations/titre-links-validate.ts index 867632301e883a44179fe3586246479ac115915d..5dd14fe26e4fd81cc526fdbbeea2f0ffb118fcfb 100644 --- a/packages/api/src/business/validations/titre-links-validate.ts +++ b/packages/api/src/business/validations/titre-links-validate.ts @@ -1,19 +1,14 @@ import { TitreId } from 'camino-common/src/validators/titres' import { ITitre, ITitreDemarche } from '../../types' import { getLinkConfig } from 'camino-common/src/permissions/titres' -import { NonEmptyArray, isNonEmptyArray, isNullOrUndefined } from 'camino-common/src/typescript-tools' +import { CaminoValid, isNonEmptyArray, isNullOrUndefined } from 'camino-common/src/typescript-tools' import { TitreTypeId } from 'camino-common/src/static/titresTypes' const linkImpossible = 'ce titre ne peut pas être lié à d’autres titres' as const const oneLinkTitre = 'ce titre peut avoir un seul titre lié' as const const droitsInsuffisants = 'droits insuffisants ou titre inexistant' as const export type CheckTitreLinksError = typeof linkImpossible | typeof oneLinkTitre | typeof droitsInsuffisants | 'lien incompatible entre ces types de titre' -export const checkTitreLinks = ( - titreTypeId: TitreTypeId, - titreFromIds: Readonly<TitreId[]>, - titresFrom: ITitre[], - demarches: ITitreDemarche[] -): { valid: true } | { valid: false; errors: NonEmptyArray<CheckTitreLinksError> } => { +export const checkTitreLinks = (titreTypeId: TitreTypeId, titreFromIds: Readonly<TitreId[]>, titresFrom: ITitre[], demarches: ITitreDemarche[]): CaminoValid<CheckTitreLinksError> => { const linkConfig = getLinkConfig( titreTypeId, demarches.map(({ typeId }) => ({ demarche_type_id: typeId })) @@ -39,5 +34,5 @@ export const checkTitreLinks = ( return { valid: false, errors } } - return { valid: true } + return { valid: true, errors: null } } diff --git a/packages/api/src/tools/fp-tools.ts b/packages/api/src/tools/fp-tools.ts index 5ced5c46047e0ea070a5d50959a484e4f78f453e..336794612f844ff176c45d4ffd9c0a185c668354 100644 --- a/packages/api/src/tools/fp-tools.ts +++ b/packages/api/src/tools/fp-tools.ts @@ -1,6 +1,6 @@ -import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools' +import { CaminoValid, isNotNullNorUndefined } from 'camino-common/src/typescript-tools' import { CaminoError, CaminoZodErrorReadableMessage, translateIssue } from 'camino-common/src/zod-tools' -import { Cause, Effect, Exit, pipe } from 'effect' +import { Cause, Effect, Exit, Option, pipe } from 'effect' import { ZodTypeAny } from 'zod' import { fromError, isZodErrorLike } from 'zod-validation-error' @@ -73,3 +73,9 @@ export const shortCircuitError = <T extends string>(value: T, ...stuff: unknown[ console.debug(`shortCircuit ${value}`, ...stuff) return { _tag: value } } + +export const filterOrFailFromValid = <T extends string>(value: CaminoValid<T>): Effect.Effect<void, CaminoError<T>> => + value.valid ? Effect.succeed(Option.none) : Effect.fail({ message: value.errors[0], detail: value.errors.join(', ') }) + +export const filterOrFailFromValidWithError = <ErrorMessage extends string>(value: CaminoValid<string>, errorMessage: ErrorMessage): Effect.Effect<void, CaminoError<ErrorMessage>> => + value.valid ? Effect.succeed(Option.none) : Effect.fail({ message: errorMessage, detail: value.errors.join(', ') }) diff --git a/packages/common/src/permissions/titres-demarches.test.ts b/packages/common/src/permissions/titres-demarches.test.ts index af81bbccd8e339f148d0dd8ae07258a96823fc1b..38749f47d24194052d9e4148101070c729fccc0a 100644 --- a/packages/common/src/permissions/titres-demarches.test.ts +++ b/packages/common/src/permissions/titres-demarches.test.ts @@ -118,11 +118,13 @@ describe('canPublishResultatMiseEnConcurrence', () => { test('machine non procédure spécifique', () => { expect(canPublishResultatMiseEnConcurrence({ ...testBlankUser, role: 'super' }, 'arm', 'oct', [{ date: toCaminoDate('2020-01-01'), etape_statut_id: 'fai', etape_type_id: 'mfr' }], demarcheId)) .toMatchInlineSnapshot(` - { - "error": "Cette démarche n'est pas une procédure spécifique", - "valid": false, - } - `) + { + "errors": [ + "Cette démarche n'est pas une procédure spécifique", + ], + "valid": false, + } + `) }) test('mise en concurrence non terminée', () => { expect( @@ -135,7 +137,9 @@ describe('canPublishResultatMiseEnConcurrence', () => { ) ).toMatchInlineSnapshot(` { - "error": "Cette démarche n'a pas terminé sa mise en concurrence", + "errors": [ + "Cette démarche n'a pas terminé sa mise en concurrence", + ], "valid": false, } `) @@ -158,7 +162,9 @@ describe('canPublishResultatMiseEnConcurrence', () => { ) ).toMatchInlineSnapshot(` { - "error": "L'utilisateur ne dispose pas des droits suffisants", + "errors": [ + "L'utilisateur ne dispose pas des droits suffisants", + ], "valid": false, } `) @@ -167,7 +173,9 @@ describe('canPublishResultatMiseEnConcurrence', () => { test("pas d'étapes", () => { expect(canPublishResultatMiseEnConcurrence({ ...testBlankUser, role: 'super' }, 'arm', 'oct', [], demarcheId)).toMatchInlineSnapshot(` { - "error": "Au moins une étape est nécessaire", + "errors": [ + "Au moins une étape est nécessaire", + ], "valid": false, } `) @@ -191,7 +199,7 @@ describe('canPublishResultatMiseEnConcurrence', () => { ) ).toMatchInlineSnapshot(` { - "error": null, + "errors": null, "valid": true, } `) @@ -221,7 +229,9 @@ describe('canPublishResultatMiseEnConcurrence', () => { ) ).toMatchInlineSnapshot(` { - "error": "Cette démarche a déja un résultat final de la mise en concurrence", + "errors": [ + "Cette démarche a déja un résultat final de la mise en concurrence", + ], "valid": false, } `) diff --git a/packages/common/src/permissions/titres-demarches.ts b/packages/common/src/permissions/titres-demarches.ts index 34ae82575c08d03da4c9a4cb7cf301c8223d0407..9e1c332345e77f5ee967a6730e58d13037d626ec 100644 --- a/packages/common/src/permissions/titres-demarches.ts +++ b/packages/common/src/permissions/titres-demarches.ts @@ -8,7 +8,7 @@ import { getEtapesTDE } from '../static/titresTypes_demarchesTypes_etapesTypes/i import { DemarcheTypeId } from '../static/demarchesTypes' import { canCreateEtape } from './titres-etapes' import { TitreGetDemarche } from '../titres' -import { isNotNullNorUndefinedNorEmpty, isNullOrUndefined } from '../typescript-tools' +import { CaminoValid, isNotNullNorUndefinedNorEmpty, isNullOrUndefined } from '../typescript-tools' import { ETAPE_IS_BROUILLON } from '../etape' import { demarcheEnregistrementDemandeDateFind, DemarcheEtape, DemarcheId } from '../demarche' import { machineIdFind } from '../machines' @@ -94,19 +94,19 @@ export const canPublishResultatMiseEnConcurrence = ( demarche_type_id: DemarcheTypeId, etapes: Pick<DemarcheEtape, 'etape_type_id' | 'demarche_id_en_concurrence' | 'date' | 'etape_statut_id'>[], id: DemarcheId -): { valid: true; error: null } | { valid: false; error: string } => { +): CaminoValid<string> => { if (!isSuper(user) && !isAdministrationAdmin(user) && !isAdministrationEditeur(user)) { - return { valid: false, error: "L'utilisateur ne dispose pas des droits suffisants" } + return { valid: false, errors: ["L'utilisateur ne dispose pas des droits suffisants"] } } const firstEtapeDate = demarcheEnregistrementDemandeDateFind(etapes.map(etape => ({ ...etape, typeId: etape.etape_type_id }))) if (isNullOrUndefined(firstEtapeDate)) { - return { valid: false, error: 'Au moins une étape est nécessaire' } + return { valid: false, errors: ['Au moins une étape est nécessaire'] } } const machineId = machineIdFind(titre_type_id, demarche_type_id, id, firstEtapeDate) if (machineId !== 'ProcedureSpecifique') { - return { valid: false, error: "Cette démarche n'est pas une procédure spécifique" } + return { valid: false, errors: ["Cette démarche n'est pas une procédure spécifique"] } } if ( @@ -115,12 +115,12 @@ export const canPublishResultatMiseEnConcurrence = ( etape_type_id === ETAPES_TYPES.avisDeMiseEnConcurrenceAuJORF && etape_statut_id === EtapesTypesEtapesStatuts.avisDeMiseEnConcurrenceAuJORF.TERMINE.etapeStatutId ) ) { - return { valid: false, error: "Cette démarche n'a pas terminé sa mise en concurrence" } + return { valid: false, errors: ["Cette démarche n'a pas terminé sa mise en concurrence"] } } if (etapes.some(({ etape_type_id }) => etape_type_id === ETAPES_TYPES.resultatMiseEnConcurrence)) { - return { valid: false, error: `Cette démarche a déja un ${EtapesTypes[ETAPES_TYPES.resultatMiseEnConcurrence].nom}` } + return { valid: false, errors: [`Cette démarche a déja un ${EtapesTypes[ETAPES_TYPES.resultatMiseEnConcurrence].nom}`] } } - return { valid: true, error: null } + return { valid: true, errors: null } } diff --git a/packages/common/src/permissions/titres.ts b/packages/common/src/permissions/titres.ts index fbf7a9368df304e53161dfd81ffe1dee1bb1e388..12f1348736a7871178c08c141501605cfd50e8e7 100644 --- a/packages/common/src/permissions/titres.ts +++ b/packages/common/src/permissions/titres.ts @@ -16,7 +16,8 @@ import { EntrepriseId } from '../entreprise' import { TITRES_TYPES_TYPES_IDS } from '../static/titresTypesTypes' export const canSeeTitreLastModifiedDate = (user: User): boolean => isSuper(user) || isAdministration(user) -export const getLinkConfig = (typeId: TitreTypeId, demarches: { demarche_type_id: DemarcheTypeId }[]): { count: 'single' | 'multiple'; typeId: TitreTypeId } | null => { +export type LinkConfig = { count: 'single' | 'multiple'; typeId: TitreTypeId } +export const getLinkConfig = (typeId: TitreTypeId, demarches: { demarche_type_id: DemarcheTypeId }[]): LinkConfig | null => { const titreType = TitresTypes[typeId] if (titreType.typeId === TITRES_TYPES_TYPES_IDS.CONCESSION && demarches.some(({ demarche_type_id }) => demarche_type_id === DEMARCHES_TYPES_IDS.Fusion)) { diff --git a/packages/common/src/static/__snapshots__/substancesLegales.test.ts.snap b/packages/common/src/static/__snapshots__/substancesLegales.test.ts.snap deleted file mode 100644 index e6df61632223177659479515ae8541c946bc2a81..0000000000000000000000000000000000000000 --- a/packages/common/src/static/__snapshots__/substancesLegales.test.ts.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`substancesFiscalesBySubstanceLegale 1`] = ` -[ - { - "calculFiscalite": { - "unite": "mkg", - }, - "description": "contenu dans les minerais", - "id": "auru", - "nom": "or", - "substanceLegaleId": "auru", - "uniteId": "mgr", - }, -] -`; - -exports[`substancesFiscalesBySubstanceLegale 2`] = ` -[ - { - "description": "bauxite nettes livrées", - "id": "aloh", - "nom": "bauxite", - "substanceLegaleId": "aloh", - "uniteId": "mtk", - }, - { - "description": "contenu dans les minerais", - "id": "cuiv", - "nom": "cuivre", - "substanceLegaleId": "cuiv", - "uniteId": "mtt", - }, - { - "description": "contenu dans les minerais", - "id": "etai", - "nom": "étain", - "substanceLegaleId": "etai", - "uniteId": "mtt", - }, - { - "description": "net livré", - "id": "fera", - "nom": "pyrite de fer", - "substanceLegaleId": "ferx", - "uniteId": "mtk", - }, - { - "description": "net livré", - "id": "ferb", - "nom": "minerais de fer", - "substanceLegaleId": "ferx", - "uniteId": "mtk", - }, - { - "description": "contenu dans les minerais", - "id": "mang", - "nom": "manganèse", - "substanceLegaleId": "mang", - "uniteId": "mtc", - }, - { - "description": "contenu dans les minerais", - "id": "plom", - "nom": "plomb", - "substanceLegaleId": "plom", - "uniteId": "mtc", - }, - { - "description": "contenu dans les minerais", - "id": "zinc", - "nom": "zinc", - "substanceLegaleId": "zinc", - "uniteId": "mtc", - }, -] -`; diff --git a/packages/common/src/typescript-tools.ts b/packages/common/src/typescript-tools.ts index 25d913ebb6146aa756cd7bb61d9cd838399015c9..94a4099e1e681bda1a05891563c110a15bc37c7b 100644 --- a/packages/common/src/typescript-tools.ts +++ b/packages/common/src/typescript-tools.ts @@ -75,6 +75,8 @@ export const exhaustiveCheck = (param: never): never => { throw new Error(`Unreachable case: ${JSON.stringify(param)}`) } +export type CaminoValid<T extends string> = { valid: true; errors: null } | { valid: false; errors: NonEmptyArray<T> } + export type NonEmptyArray<T> = [T, ...T[]] export const isNonEmptyArray = <T>(arr: T[]): arr is NonEmptyArray<T> => { return arr.length > 0 diff --git a/packages/ui/src/components/titre.stories_snapshots_AbattisKoticaOctroi.html b/packages/ui/src/components/titre.stories_snapshots_AbattisKoticaOctroi.html index 554912ee99bf156b9c6c17ece54f534275a191c9..4309bf9ac3ff58f710f43a1b79dbed2c6dc36ae0 100644 --- a/packages/ui/src/components/titre.stories_snapshots_AbattisKoticaOctroi.html +++ b/packages/ui/src/components/titre.stories_snapshots_AbattisKoticaOctroi.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_BasseManaMod.html b/packages/ui/src/components/titre.stories_snapshots_BasseManaMod.html index 503bd2710e9afd67d46e212a8954e061f9c75a42..2ac6e2f493d0f7188e9a4aae993758aab6adca4a 100644 --- a/packages/ui/src/components/titre.stories_snapshots_BasseManaMod.html +++ b/packages/ui/src/components/titre.stories_snapshots_BasseManaMod.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_BonEspoirOctroi.html b/packages/ui/src/components/titre.stories_snapshots_BonEspoirOctroi.html index 8e2db12e791c760b42f2e17b8b67f5333cd22848..910a4e7c13e0422297b1f9024e357cbead4d6c53 100644 --- a/packages/ui/src/components/titre.stories_snapshots_BonEspoirOctroi.html +++ b/packages/ui/src/components/titre.stories_snapshots_BonEspoirOctroi.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_BonEspoirProlongation2.html b/packages/ui/src/components/titre.stories_snapshots_BonEspoirProlongation2.html index 6c3e3960ed8a72f24b6b1a56914e65d48bbec050..fd0a69a2006e51e172fef850e39161937d5aa6fe 100644 --- a/packages/ui/src/components/titre.stories_snapshots_BonEspoirProlongation2.html +++ b/packages/ui/src/components/titre.stories_snapshots_BonEspoirProlongation2.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_BonEspoirTravaux.html b/packages/ui/src/components/titre.stories_snapshots_BonEspoirTravaux.html index 2f93b97b0b8998f38f158954eeca366951840508..3dbda53e32f3c07709b0266596d9752b79f443f0 100644 --- a/packages/ui/src/components/titre.stories_snapshots_BonEspoirTravaux.html +++ b/packages/ui/src/components/titre.stories_snapshots_BonEspoirTravaux.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html index ae95a59ba2722c7cb96824e5d4a2ea4cfa82a7fe..4a19fbb8e345545633d31abc0e2fb15521f7a399 100644 --- a/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html +++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html @@ -27,6 +27,10 @@ <p>Il manque 2 rapports d'activités. <a href="/mocked-href" title="Remplir les rapports d'activités" class="fr-link" aria-label="Remplir les rapports d'activités">Remplir les rapports d'activités</a></p> </div><a href="/mocked-href" title="Consulter les rapports d'activités" class="fr-mt-2w fr-btn fr-btn--secondary" aria-label="Consulter les rapports d'activités">Consulter les rapports d'activités</a> <div> + <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier un titre <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button"> + <!----> + </button></div> + </div> <!----> <!----> </div> diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html index 44f706b80836a562956795f556e54e2930bb084a..9173dcdef3e269deee296adf772189cd81490935 100644 --- a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html +++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html @@ -28,6 +28,10 @@ <p>Il manque 2 rapports d'activités. <a href="/mocked-href" title="Remplir les rapports d'activités" class="fr-link" aria-label="Remplir les rapports d'activités">Remplir les rapports d'activités</a></p> </div><a href="/mocked-href" title="Consulter les rapports d'activités" class="fr-mt-2w fr-btn fr-btn--secondary" aria-label="Consulter les rapports d'activités">Consulter les rapports d'activités</a> <div> + <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier un titre <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button"> + <!----> + </button></div> + </div> <!----> <!----> </div> diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html index fed6d71f874dfb9519acee1d7b1aab2ddd92a630..92eea09c403b786b662ada7553fa7f7414fa9ea9 100644 --- a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html +++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html @@ -30,6 +30,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_CriqueAdolpheOctroi.html b/packages/ui/src/components/titre.stories_snapshots_CriqueAdolpheOctroi.html index 98b672ca762576d96d27a8821e9165547fa8ca78..6841a78b953912f98cf9039264284888b61489f9 100644 --- a/packages/ui/src/components/titre.stories_snapshots_CriqueAdolpheOctroi.html +++ b/packages/ui/src/components/titre.stories_snapshots_CriqueAdolpheOctroi.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_Empty.html b/packages/ui/src/components/titre.stories_snapshots_Empty.html index b57faa16adba1f5e5024fe92a969fdb89fff6478..e0cd3adf42b4326e970f92680d58648177cce253 100644 --- a/packages/ui/src/components/titre.stories_snapshots_Empty.html +++ b/packages/ui/src/components/titre.stories_snapshots_Empty.html @@ -26,6 +26,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_Full.html b/packages/ui/src/components/titre.stories_snapshots_Full.html index 68081d2f080c9aba0312577c323a1140b40ab0cc..69fb573745dc604f7e240299a6f3276a47037e22 100644 --- a/packages/ui/src/components/titre.stories_snapshots_Full.html +++ b/packages/ui/src/components/titre.stories_snapshots_Full.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <div class="fr-alert fr-alert--warning fr-mt-2w"> diff --git a/packages/ui/src/components/titre.stories_snapshots_Lenoncourt.html b/packages/ui/src/components/titre.stories_snapshots_Lenoncourt.html index 4b73581da07de799253bbeab35c28c91f69cb218..74b0058378d8b3be38dabf6fef32f6b0ac07d9b8 100644 --- a/packages/ui/src/components/titre.stories_snapshots_Lenoncourt.html +++ b/packages/ui/src/components/titre.stories_snapshots_Lenoncourt.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_TitreAvecUnOctroiEnConstructionEtUnTravaux.html b/packages/ui/src/components/titre.stories_snapshots_TitreAvecUnOctroiEnConstructionEtUnTravaux.html index e56a8c733f1f1eb3c67dfcf3fddd0827e5107813..15ac8c0c2fe5a693e2d85bba09341f1cf6d6d029 100644 --- a/packages/ui/src/components/titre.stories_snapshots_TitreAvecUnOctroiEnConstructionEtUnTravaux.html +++ b/packages/ui/src/components/titre.stories_snapshots_TitreAvecUnOctroiEnConstructionEtUnTravaux.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <div class="fr-alert fr-alert--warning fr-mt-2w"> diff --git a/packages/ui/src/components/titre.stories_snapshots_TitreAvecUneSeuleDemarcheEnConstruction.html b/packages/ui/src/components/titre.stories_snapshots_TitreAvecUneSeuleDemarcheEnConstruction.html index 26405942646315725f325cbceba8534deae239da..e551ae8de184dd4aa636a2ce865905ea3198c4b8 100644 --- a/packages/ui/src/components/titre.stories_snapshots_TitreAvecUneSeuleDemarcheEnConstruction.html +++ b/packages/ui/src/components/titre.stories_snapshots_TitreAvecUneSeuleDemarcheEnConstruction.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <div class="fr-alert fr-alert--warning fr-mt-2w"> diff --git a/packages/ui/src/components/titre.stories_snapshots_WithDoublon.html b/packages/ui/src/components/titre.stories_snapshots_WithDoublon.html index e3e2e1c3362f39b8dc87cc06a8b26e4753ec7340..bb3aded12ba2136912368016417235f8a5e48637 100644 --- a/packages/ui/src/components/titre.stories_snapshots_WithDoublon.html +++ b/packages/ui/src/components/titre.stories_snapshots_WithDoublon.html @@ -28,6 +28,7 @@ <div> <!----> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAmont.html b/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAmont.html index 069802e97152b6551f0deb9b883e8c5907ce1224..0fa8b039a15a572191936a3ea1d494bd9fe3a833 100644 --- a/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAmont.html +++ b/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAmont.html @@ -29,6 +29,7 @@ </button></div> </div> <!----> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAval.html b/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAval.html index 5364209fe2fa2da959827ce8aae2f2cf8ba1ae90..d0e79253ef838d1a0fca9df7a51a998e3eb0e1b5 100644 --- a/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAval.html +++ b/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAval.html @@ -24,9 +24,13 @@ <!----> <!----> <div> - <!----> + <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier un titre <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button"> + <!----> + </button></div> + </div> <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titre issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="nom du titre en aval" class="fr-tag fr-tag--sm" aria-label="nom du titre en aval">nom du titre en aval</a></div> </div> + <!----> </div> </div> <!----> diff --git a/packages/ui/src/components/titre/titres-link-form.stories.tsx b/packages/ui/src/components/titre/titres-link-form.stories.tsx index 2b7e209327795003cdbdb27999b477117cbc08cf..483db4834215746dc16885f27332b2560580b97c 100644 --- a/packages/ui/src/components/titre/titres-link-form.stories.tsx +++ b/packages/ui/src/components/titre/titres-link-form.stories.tsx @@ -54,7 +54,7 @@ const titresFrom: TitreLink[] = [linkableTitres[0]] const apiClient: Props['apiClient'] = { loadLinkableTitres: () => () => Promise.resolve(linkableTitres), - loadTitreLinks: () => Promise.resolve({ aval: titresTo, amont: titresFrom }), + loadTitreLinks: async () => ({ aval: titresTo, amont: titresFrom }), linkTitres: () => new Promise<TitreLinks>(resolve => resolve({ aval: titresTo, amont: titresFrom })), } @@ -67,6 +67,15 @@ export const AxmWithAlreadySelectedTitre: StoryFn = () => ( /> ) +export const AxmWithoutSelectedTitre: StoryFn = () => ( + <TitresLinkForm + user={{ role: 'super', ...testBlankUser }} + titre={{ typeId: 'axm', administrations: [], id: titreIdValidator.parse('titreId'), demarches: [] }} + apiClient={{ ...apiClient, loadTitreLinks: async () => ({ aval: [], amont: [] }) }} + onTitresFromLoaded={() => {}} + /> +) + export const AxmWithAlreadySelectedTitreNotEditable: StoryFn = () => ( <TitresLinkForm user={{ role: 'defaut', ...testBlankUser }} @@ -101,6 +110,26 @@ export const FusionWithAlreadySelectedTitre: StoryFn = () => ( /> ) +export const FusionWithoutSelectedTitre: StoryFn = () => ( + <TitresLinkForm + user={{ role: 'super', ...testBlankUser }} + titre={{ + typeId: 'cxm', + administrations: [], + id: titreIdValidator.parse('titreId'), + demarches: [{ demarche_type_id: 'fus' }], + }} + apiClient={{ + ...apiClient, + loadTitreLinks: async () => ({ + aval: [], + amont: [], + }), + }} + onTitresFromLoaded={() => {}} + /> +) + export const TitreWithTitreLinksLoading: StoryFn = () => ( <TitresLinkForm user={{ role: 'super', ...testBlankUser }} diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitre.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitre.html index ce240850c42ed0c4c5d84a788874074ab8014f84..ffb573fa5e5ff691ea9e2f431be2b1253e4659a0 100644 --- a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitre.html +++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitre.html @@ -5,4 +5,5 @@ </div> <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titre issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a></div> </div> + <!----> </div> \ No newline at end of file diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitreNotEditable.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitreNotEditable.html index c94a248e0d5253d5295b9826485a440943c1d7ec..5cc9908db7cae978b6dd22693fec7b055f7e5b4d 100644 --- a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitreNotEditable.html +++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitreNotEditable.html @@ -5,4 +5,5 @@ </div> <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titre issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a></div> </div> + <!----> </div> \ No newline at end of file diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithoutSelectedTitre.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithoutSelectedTitre.html new file mode 100644 index 0000000000000000000000000000000000000000..efa67a367250470a3e549de223d61b7a3898e96e --- /dev/null +++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithoutSelectedTitre.html @@ -0,0 +1,8 @@ +<div> + <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier un titre <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button"> + <!----> + </button></div> + </div> + <!----> + <!----> +</div> \ No newline at end of file diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_DefautCantUpdateLinks.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_DefautCantUpdateLinks.html index c94a248e0d5253d5295b9826485a440943c1d7ec..5cc9908db7cae978b6dd22693fec7b055f7e5b4d 100644 --- a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_DefautCantUpdateLinks.html +++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_DefautCantUpdateLinks.html @@ -5,4 +5,5 @@ </div> <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titre issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a></div> </div> + <!----> </div> \ No newline at end of file diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithAlreadySelectedTitre.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithAlreadySelectedTitre.html index 45fdcc0494ff4887bce8a03e562f1cf8d17cb77b..dce627fb0a4ea837f0f254f77c982f767b53ea0c 100644 --- a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithAlreadySelectedTitre.html +++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithAlreadySelectedTitre.html @@ -3,6 +3,7 @@ <!----> </button></div> </div> - <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titres issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a><a href="/mocked-href" title="Titre fils 2" class="fr-tag fr-tag--sm" aria-label="Titre fils 2">Titre fils 2</a></div> + <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titres issus de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a><a href="/mocked-href" title="Titre fils 2" class="fr-tag fr-tag--sm" aria-label="Titre fils 2">Titre fils 2</a></div> </div> + <!----> </div> \ No newline at end of file diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithoutSelectedTitre.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithoutSelectedTitre.html new file mode 100644 index 0000000000000000000000000000000000000000..07ae89d8e3598fbb2cb296791b6e03a0462510ba --- /dev/null +++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithoutSelectedTitre.html @@ -0,0 +1,8 @@ +<div> + <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier plusieurs titres <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button"> + <!----> + </button></div> + </div> + <!----> + <!----> +</div> \ No newline at end of file diff --git a/packages/ui/src/components/titre/titres-link-form.tsx b/packages/ui/src/components/titre/titres-link-form.tsx index 30f8ad1ddfb4bf434c700e78f67264b9c1762e48..642a01b7b541cf78c29b45eaca9f561f0172c810 100644 --- a/packages/ui/src/components/titre/titres-link-form.tsx +++ b/packages/ui/src/components/titre/titres-link-form.tsx @@ -1,5 +1,5 @@ -import { canLinkTitres, getLinkConfig } from 'camino-common/src/permissions/titres' -import { computed, defineComponent, onMounted, ref, watch } from 'vue' +import { canLinkTitres, getLinkConfig, LinkConfig } from 'camino-common/src/permissions/titres' +import { computed, defineComponent, onMounted, watch } from 'vue' import { TitreTypeId } from 'camino-common/src/static/titresTypes' import { User } from 'camino-common/src/roles' import { AdministrationId } from 'camino-common/src/static/administrations' @@ -11,10 +11,12 @@ import { TitreLink, TitreLinks } from 'camino-common/src/titres' import { TitreId } from 'camino-common/src/validators/titres' import { ApiClient } from '@/api/api-client' import { TitresLinkConfig } from '@/components/titre/titres-link-form-api-client' -import { DsfrButton, DsfrButtonIcon } from '../_ui/dsfr-button' +import { DsfrButtonIcon } from '../_ui/dsfr-button' import { DsfrIcon } from '../_ui/icon' import { DsfrTag } from '../_ui/tag' import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty } from 'camino-common/src/typescript-tools' +import { useState } from '@/utils/vue-tsx-utils' +import { FunctionalPopup } from '../_ui/functional-popup' export interface Props { user: User @@ -28,9 +30,16 @@ export interface Props { onTitresFromLoaded: (hasTitresFrom: boolean) => void } export const TitresLinkForm = defineComponent<Props>(props => { - const mode = ref<'read' | 'edit'>('read') - const selectedTitres = ref<TitreLink[]>([]) - const titresLinks = ref<AsyncData<TitreLinks>>({ status: 'LOADING' }) + const [popupOpen, setPopupOpen] = useState<boolean>(false) + const [titresLinks, setTitresLinks] = useState<AsyncData<TitreLinks>>({ status: 'LOADING' }) + + const openEditPopup = () => { + setPopupOpen(true) + } + + const closeEditPopup = () => { + setPopupOpen(false) + } const linkConfig = computed(() => getLinkConfig(props.titre.typeId, props.titre.demarches)) @@ -46,22 +55,21 @@ export const TitresLinkForm = defineComponent<Props>(props => { ) const init = async () => { - titresLinks.value = { status: 'LOADING' } + setTitresLinks({ status: 'LOADING' }) const result = await props.apiClient.loadTitreLinks(props.titre.id) if ('message' in result) { - titresLinks.value = { + setTitresLinks({ status: 'NEW_ERROR', error: result, - } + }) } else { - titresLinks.value = { status: 'LOADED', value: result } + setTitresLinks({ status: 'LOADED', value: result }) props.onTitresFromLoaded(result.amont.length > 0) - selectedTitres.value = titresLinks.value.value.amont } } const canEditLink = computed<boolean>(() => { - // On ne peut pas lier si ce type de titre n’accepte pas de liaison + // On ne peut pas lier si ce type de titre n'accepte pas de liaison if (!linkConfig.value) { return false } @@ -69,94 +77,45 @@ export const TitresLinkForm = defineComponent<Props>(props => { return canLinkTitres(props.user, props.titre.administrations ?? []) }) - const titreLinkConfig = computed<TitresLinkConfig | null>(() => { - if (titresLinks.value.status !== 'LOADED') { - return null - } - - const titreFromIds = titresLinks.value.value.amont.map(({ id }) => id) - if (linkConfig.value?.count === 'single') { - return { - type: 'single', - selectedTitreId: titreFromIds.length === 1 ? titreFromIds[0] : null, - } - } - - return { - type: 'multiple', - selectedTitreIds: titreFromIds, - } - }) - - const onSelectedTitres = (titres: TitreLink[]) => { - selectedTitres.value = titres - } - - const saveLink = async () => { - titresLinks.value = { status: 'LOADING' } - try { - const links = await props.apiClient.linkTitres( - props.titre.id, - selectedTitres.value.map(({ id }) => id) - ) - if ('message' in links) { - titresLinks.value = { - status: 'NEW_ERROR', - error: links, - } - } else { - mode.value = 'read' - titresLinks.value = { + const myApiClient = { + ...props.apiClient, + linkTitres: async (titreId: TitreId, titreFromIds: TitreId[]) => { + const links = await props.apiClient.linkTitres(titreId, titreFromIds) + if (!('message' in links)) { + setTitresLinks({ status: 'LOADED', value: links, - } + }) } - } catch (e: any) { - titresLinks.value = { - status: 'ERROR', - message: e.message ?? 'something wrong happened', - } - } + return links + }, } - const closeForm = () => (mode.value = 'read') - return () => ( <div> <LoadingElement data={titresLinks.value} renderItem={item => ( <> - {isNotNullNorUndefinedNorEmpty(item.amont) && isNotNullNorUndefined(linkConfig.value) ? ( + {(isNotNullNorUndefinedNorEmpty(item.amont) || canEditLink.value) && isNotNullNorUndefined(linkConfig.value) ? ( <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}> <DsfrIcon name="fr-icon-link" size="sm" color="text-title-blue-france" aria-hidden="true" /> - Titre{item.amont.length > 1 ? 's' : ''} à l'origine de ce titre : - {mode.value === 'edit' ? ( - <> - {titreLinkConfig.value ? ( - <TitresLink config={titreLinkConfig.value} loadLinkableTitres={props.apiClient.loadLinkableTitres(props.titre.typeId, props.titre.demarches)} onSelectTitres={onSelectedTitres} /> - ) : null} - <> - <DsfrButton buttonType="primary" title="Enregistrer" onClick={saveLink} /> - <DsfrButton buttonType="secondary" title="Annuler" onClick={closeForm} /> - </> - </> - ) : ( - <div class="flex flex-center" style={{ gap: '0.5rem' }}> - {item.amont.map(titreFrom => ( - <DsfrTag key={titreFrom.id} tagSize="sm" to={{ name: 'titre', params: { id: titreFrom.id } }} ariaLabel={titreFrom.nom} /> - ))} - - {canEditLink.value ? <DsfrButtonIcon buttonType="tertiary-no-outline" title="modifier les titres liés" onClick={() => (mode.value = 'edit')} icon="fr-icon-pencil-line" /> : null} - </div> - )} + {item.amont.length === 0 ? <>Lier {linkConfig.value.count === 'single' ? 'un titre' : 'plusieurs titres'} </> : <>Titre{item.amont.length > 1 ? 's' : ''} à l'origine de ce titre :</>} + + <div class="flex flex-center" style={{ gap: '0.5rem' }}> + {item.amont.map(titreFrom => ( + <DsfrTag key={titreFrom.id} tagSize="sm" to={{ name: 'titre', params: { id: titreFrom.id } }} ariaLabel={titreFrom.nom} /> + ))} + + {canEditLink.value ? <DsfrButtonIcon buttonType="tertiary-no-outline" title="modifier les titres liés" onClick={openEditPopup} icon="fr-icon-pencil-line" /> : null} + </div> </div> ) : null} {item.aval.length ? ( <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }} class="fr-mt-1w"> <DsfrIcon name="fr-icon-link" size="sm" color="text-title-blue-france" aria-hidden="true" /> - Titre{item.aval.length > 1 ? 's' : ''} issu de ce titre : + {item.aval.length > 1 ? 'Titres issus ' : 'Titre issu'} de ce titre : <div class="flex flex-center" style={{ gap: '0.5rem' }}> {item.aval.map(titreTo => ( <DsfrTag key={titreTo.id} tagSize="sm" to={{ name: 'titre', params: { id: titreTo.id } }} ariaLabel={titreTo.nom} /> @@ -164,6 +123,9 @@ export const TitresLinkForm = defineComponent<Props>(props => { </div> </div> ) : null} + {popupOpen.value && isNotNullNorUndefined(linkConfig.value) ? ( + <EditPopup titreLinks={item} linkConfig={linkConfig.value} close={closeEditPopup} apiClient={myApiClient} titre={props.titre} /> + ) : null} </> )} /> @@ -171,5 +133,65 @@ export const TitresLinkForm = defineComponent<Props>(props => { ) }) +interface EditPopupProps { + titreLinks: TitreLinks + linkConfig: LinkConfig + titre: { + id: TitreId + typeId: TitreTypeId + administrations: AdministrationId[] + demarches: { demarche_type_id: DemarcheTypeId }[] + } + close: () => void + apiClient: Pick<ApiClient, 'loadTitreLinks' | 'loadLinkableTitres' | 'linkTitres'> +} + +const EditPopup = defineComponent<EditPopupProps>(props => { + const [selectedTitres, setSelectedTitres] = useState<TitreLink[]>([]) + const onSelectedTitres = (titres: TitreLink[]) => { + setSelectedTitres(titres) + } + const titreLinkConfig = computed<TitresLinkConfig>(() => { + const titreFromIds = props.titreLinks.amont.map(({ id }) => id) + if (props.linkConfig.count === 'single') { + return { + type: 'single', + selectedTitreId: titreFromIds.length === 1 ? titreFromIds[0] : null, + } + } + + return { + type: 'multiple', + selectedTitreIds: titreFromIds, + } + }) + + const content = () => ( + <form> + <TitresLink config={titreLinkConfig.value} loadLinkableTitres={props.apiClient.loadLinkableTitres(props.titre.typeId, props.titre.demarches)} onSelectTitres={onSelectedTitres} /> + </form> + ) + + return () => ( + <FunctionalPopup + title="Modification des liens entre les titres" + content={content} + close={props.close} + validate={{ + action: () => + props.apiClient.linkTitres( + props.titre.id, + selectedTitres.value.map(({ id }) => id) + ), + text: 'Enregistrer', + }} + canValidate={true} + /> + ) +}) + // @ts-ignore waiting for https://github.com/vuejs/core/issues/7833 TitresLinkForm.props = ['apiClient', 'titre', 'user', 'onTitresFromLoaded'] + +// @ts-ignore waiting for https://github.com/vuejs/core/issues/7833 +EditPopup.props = ['apiClient', 'titre', 'titreLinks', 'linkConfig', 'close']