@communecter/cocolight-api-client 1.0.155 → 1.0.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/1035.cocolight-api-client.browser.js +1 -1
- package/dist/1035.cocolight-api-client.cjs +1 -1
- package/dist/1483.cocolight-api-client.browser.js +1 -1
- package/dist/1483.cocolight-api-client.cjs +1 -1
- package/dist/1491.cocolight-api-client.browser.js +1 -1
- package/dist/1491.cocolight-api-client.cjs +1 -1
- package/dist/1494.cocolight-api-client.browser.js +1 -1
- package/dist/1494.cocolight-api-client.cjs +1 -1
- package/dist/1641.cocolight-api-client.browser.js +1 -1
- package/dist/1641.cocolight-api-client.cjs +1 -1
- package/dist/1678.cocolight-api-client.browser.js +1 -1
- package/dist/1678.cocolight-api-client.cjs +1 -1
- package/dist/2041.cocolight-api-client.browser.js +1 -1
- package/dist/2041.cocolight-api-client.cjs +1 -1
- package/dist/2302.cocolight-api-client.browser.js +1 -1
- package/dist/2302.cocolight-api-client.cjs +1 -1
- package/dist/2313.cocolight-api-client.browser.js +1 -1
- package/dist/2313.cocolight-api-client.cjs +1 -1
- package/dist/2379.cocolight-api-client.browser.js +1 -1
- package/dist/2379.cocolight-api-client.cjs +1 -1
- package/dist/2439.cocolight-api-client.browser.js +1 -1
- package/dist/2439.cocolight-api-client.cjs +1 -1
- package/dist/2594.cocolight-api-client.browser.js +1 -1
- package/dist/2594.cocolight-api-client.cjs +1 -1
- package/dist/2766.cocolight-api-client.browser.js +1 -1
- package/dist/2766.cocolight-api-client.cjs +1 -1
- package/dist/2812.cocolight-api-client.browser.js +1 -1
- package/dist/2812.cocolight-api-client.cjs +1 -1
- package/dist/2918.cocolight-api-client.browser.js +1 -1
- package/dist/2918.cocolight-api-client.cjs +1 -1
- package/dist/3028.cocolight-api-client.browser.js +1 -1
- package/dist/3028.cocolight-api-client.cjs +1 -1
- package/dist/3330.cocolight-api-client.browser.js +1 -1
- package/dist/3330.cocolight-api-client.cjs +1 -1
- package/dist/3371.cocolight-api-client.browser.js +1 -1
- package/dist/3371.cocolight-api-client.cjs +1 -1
- package/dist/3401.cocolight-api-client.browser.js +1 -1
- package/dist/3401.cocolight-api-client.cjs +1 -1
- package/dist/3516.cocolight-api-client.browser.js +1 -1
- package/dist/3516.cocolight-api-client.cjs +1 -1
- package/dist/3593.cocolight-api-client.browser.js +1 -1
- package/dist/3593.cocolight-api-client.cjs +1 -1
- package/dist/3764.cocolight-api-client.browser.js +1 -1
- package/dist/3764.cocolight-api-client.cjs +1 -1
- package/dist/3873.cocolight-api-client.browser.js +1 -1
- package/dist/3873.cocolight-api-client.cjs +1 -1
- package/dist/3911.cocolight-api-client.browser.js +1 -1
- package/dist/3911.cocolight-api-client.cjs +1 -1
- package/dist/4031.cocolight-api-client.browser.js +1 -1
- package/dist/4031.cocolight-api-client.cjs +1 -1
- package/dist/4152.cocolight-api-client.browser.js +1 -1
- package/dist/4152.cocolight-api-client.cjs +1 -1
- package/dist/4203.cocolight-api-client.browser.js +1 -1
- package/dist/4203.cocolight-api-client.cjs +1 -1
- package/dist/424.cocolight-api-client.browser.js +1 -1
- package/dist/424.cocolight-api-client.cjs +1 -1
- package/dist/435.cocolight-api-client.browser.js +1 -1
- package/dist/435.cocolight-api-client.cjs +1 -1
- package/dist/4402.cocolight-api-client.browser.js +1 -1
- package/dist/4402.cocolight-api-client.cjs +1 -1
- package/dist/4425.cocolight-api-client.browser.js +1 -1
- package/dist/4425.cocolight-api-client.cjs +1 -1
- package/dist/4472.cocolight-api-client.browser.js +1 -1
- package/dist/4472.cocolight-api-client.cjs +1 -1
- package/dist/4914.cocolight-api-client.browser.js +1 -1
- package/dist/4914.cocolight-api-client.cjs +1 -1
- package/dist/4959.cocolight-api-client.browser.js +1 -1
- package/dist/4959.cocolight-api-client.cjs +1 -1
- package/dist/5100.cocolight-api-client.browser.js +1 -1
- package/dist/5100.cocolight-api-client.cjs +1 -1
- package/dist/5111.cocolight-api-client.browser.js +1 -1
- package/dist/5111.cocolight-api-client.cjs +1 -1
- package/dist/5286.cocolight-api-client.browser.js +1 -1
- package/dist/5286.cocolight-api-client.cjs +1 -1
- package/dist/5380.cocolight-api-client.browser.js +1 -1
- package/dist/5380.cocolight-api-client.cjs +1 -1
- package/dist/5383.cocolight-api-client.browser.js +1 -1
- package/dist/5383.cocolight-api-client.cjs +1 -1
- package/dist/5480.cocolight-api-client.browser.js +1 -1
- package/dist/5480.cocolight-api-client.cjs +1 -1
- package/dist/5541.cocolight-api-client.browser.js +1 -1
- package/dist/5541.cocolight-api-client.cjs +1 -1
- package/dist/5839.cocolight-api-client.browser.js +1 -1
- package/dist/5839.cocolight-api-client.cjs +1 -1
- package/dist/5862.cocolight-api-client.browser.js +1 -1
- package/dist/5862.cocolight-api-client.cjs +1 -1
- package/dist/5869.cocolight-api-client.browser.js +1 -1
- package/dist/5869.cocolight-api-client.cjs +1 -1
- package/dist/588.cocolight-api-client.browser.js +1 -1
- package/dist/588.cocolight-api-client.cjs +1 -1
- package/dist/601.cocolight-api-client.browser.js +1 -1
- package/dist/601.cocolight-api-client.cjs +1 -1
- package/dist/6012.cocolight-api-client.browser.js +1 -1
- package/dist/6012.cocolight-api-client.cjs +1 -1
- package/dist/6200.cocolight-api-client.browser.js +1 -1
- package/dist/6200.cocolight-api-client.cjs +1 -1
- package/dist/6369.cocolight-api-client.browser.js +1 -1
- package/dist/6369.cocolight-api-client.cjs +1 -1
- package/dist/7009.cocolight-api-client.browser.js +1 -1
- package/dist/7009.cocolight-api-client.cjs +1 -1
- package/dist/725.cocolight-api-client.browser.js +1 -1
- package/dist/725.cocolight-api-client.cjs +1 -1
- package/dist/7320.cocolight-api-client.browser.js +1 -1
- package/dist/7339.cocolight-api-client.browser.js +1 -1
- package/dist/7339.cocolight-api-client.cjs +1 -1
- package/dist/7398.cocolight-api-client.browser.js +1 -1
- package/dist/7398.cocolight-api-client.cjs +1 -1
- package/dist/7888.cocolight-api-client.browser.js +1 -1
- package/dist/7888.cocolight-api-client.cjs +1 -1
- package/dist/7975.cocolight-api-client.browser.js +1 -1
- package/dist/7975.cocolight-api-client.cjs +1 -1
- package/dist/809.cocolight-api-client.browser.js +1 -1
- package/dist/809.cocolight-api-client.cjs +1 -1
- package/dist/8300.cocolight-api-client.browser.js +1 -1
- package/dist/8300.cocolight-api-client.cjs +1 -1
- package/dist/8317.cocolight-api-client.browser.js +1 -1
- package/dist/8317.cocolight-api-client.cjs +1 -1
- package/dist/8525.cocolight-api-client.browser.js +1 -1
- package/dist/8525.cocolight-api-client.cjs +1 -1
- package/dist/8894.cocolight-api-client.browser.js +1 -1
- package/dist/8894.cocolight-api-client.cjs +1 -1
- package/dist/9301.cocolight-api-client.browser.js +1 -1
- package/dist/9301.cocolight-api-client.cjs +1 -1
- package/dist/9438.cocolight-api-client.browser.js +1 -1
- package/dist/9438.cocolight-api-client.cjs +1 -1
- package/dist/9478.cocolight-api-client.browser.js +1 -1
- package/dist/9478.cocolight-api-client.cjs +1 -1
- package/dist/9568.cocolight-api-client.browser.js +1 -1
- package/dist/9568.cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.browser.js +6 -6
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.vite.mjs.js +1 -1
- package/dist/cocolight-api-client.vite.mjs.js.map +1 -1
- package/package.json +7 -17
- package/types/Api.d.ts +1 -0
- package/types/ApiClient.d.ts +10 -1
- package/types/api/Answer.d.ts +27 -9
- package/types/api/BaseEntity.d.ts +77 -0
- package/types/api/EndpointApi.d.ts +51 -3
- package/types/api/EndpointApi.types.d.ts +112 -1
- package/types/api/User.d.ts +20 -0
- package/types/costum/types.d.ts +0 -2
- package/types/endpoints.module.d.ts +3039 -1009
- package/types/index.d.ts +1 -0
- package/types/utils/OfflineClientManager.d.ts +8 -0
- package/types/utils/adminImportExport.d.ts +29 -0
- package/dist/1035.cocolight-api-client.mjs.js +0 -1
- package/dist/1483.cocolight-api-client.mjs.js +0 -1
- package/dist/1491.cocolight-api-client.mjs.js +0 -1
- package/dist/1494.cocolight-api-client.mjs.js +0 -1
- package/dist/1641.cocolight-api-client.mjs.js +0 -1
- package/dist/1678.cocolight-api-client.mjs.js +0 -1
- package/dist/2041.cocolight-api-client.mjs.js +0 -1
- package/dist/2302.cocolight-api-client.mjs.js +0 -1
- package/dist/2313.cocolight-api-client.mjs.js +0 -1
- package/dist/2379.cocolight-api-client.mjs.js +0 -1
- package/dist/2439.cocolight-api-client.mjs.js +0 -1
- package/dist/2594.cocolight-api-client.mjs.js +0 -1
- package/dist/2766.cocolight-api-client.mjs.js +0 -1
- package/dist/2812.cocolight-api-client.mjs.js +0 -1
- package/dist/2918.cocolight-api-client.mjs.js +0 -1
- package/dist/3028.cocolight-api-client.mjs.js +0 -1
- package/dist/3330.cocolight-api-client.mjs.js +0 -1
- package/dist/3371.cocolight-api-client.mjs.js +0 -1
- package/dist/3401.cocolight-api-client.mjs.js +0 -1
- package/dist/3516.cocolight-api-client.mjs.js +0 -1
- package/dist/3593.cocolight-api-client.mjs.js +0 -1
- package/dist/3764.cocolight-api-client.mjs.js +0 -1
- package/dist/3873.cocolight-api-client.mjs.js +0 -1
- package/dist/3911.cocolight-api-client.mjs.js +0 -1
- package/dist/4031.cocolight-api-client.mjs.js +0 -1
- package/dist/4152.cocolight-api-client.mjs.js +0 -1
- package/dist/4203.cocolight-api-client.mjs.js +0 -1
- package/dist/424.cocolight-api-client.mjs.js +0 -1
- package/dist/435.cocolight-api-client.mjs.js +0 -1
- package/dist/4402.cocolight-api-client.mjs.js +0 -1
- package/dist/4425.cocolight-api-client.mjs.js +0 -1
- package/dist/4472.cocolight-api-client.mjs.js +0 -1
- package/dist/4914.cocolight-api-client.mjs.js +0 -1
- package/dist/4959.cocolight-api-client.mjs.js +0 -1
- package/dist/5100.cocolight-api-client.mjs.js +0 -1
- package/dist/5111.cocolight-api-client.mjs.js +0 -1
- package/dist/5286.cocolight-api-client.mjs.js +0 -1
- package/dist/5380.cocolight-api-client.mjs.js +0 -1
- package/dist/5383.cocolight-api-client.mjs.js +0 -1
- package/dist/5480.cocolight-api-client.mjs.js +0 -1
- package/dist/5541.cocolight-api-client.mjs.js +0 -1
- package/dist/5839.cocolight-api-client.mjs.js +0 -1
- package/dist/5862.cocolight-api-client.mjs.js +0 -1
- package/dist/5869.cocolight-api-client.mjs.js +0 -1
- package/dist/588.cocolight-api-client.mjs.js +0 -1
- package/dist/601.cocolight-api-client.mjs.js +0 -1
- package/dist/6012.cocolight-api-client.mjs.js +0 -1
- package/dist/6200.cocolight-api-client.mjs.js +0 -1
- package/dist/6369.cocolight-api-client.mjs.js +0 -1
- package/dist/7009.cocolight-api-client.mjs.js +0 -1
- package/dist/725.cocolight-api-client.mjs.js +0 -1
- package/dist/7339.cocolight-api-client.mjs.js +0 -1
- package/dist/7398.cocolight-api-client.mjs.js +0 -1
- package/dist/7888.cocolight-api-client.mjs.js +0 -1
- package/dist/7975.cocolight-api-client.mjs.js +0 -1
- package/dist/809.cocolight-api-client.mjs.js +0 -1
- package/dist/8300.cocolight-api-client.mjs.js +0 -1
- package/dist/8317.cocolight-api-client.mjs.js +0 -1
- package/dist/8525.cocolight-api-client.mjs.js +0 -1
- package/dist/8894.cocolight-api-client.mjs.js +0 -1
- package/dist/9301.cocolight-api-client.mjs.js +0 -1
- package/dist/9438.cocolight-api-client.mjs.js +0 -1
- package/dist/9478.cocolight-api-client.mjs.js +0 -1
- package/dist/9568.cocolight-api-client.mjs.js +0 -1
- package/dist/cocolight-api-client.mjs.js +0 -1
- package/src/Api.ts +0 -348
- package/src/ApiClient.ts +0 -1994
- package/src/EJSONType.ts +0 -103
- package/src/api/Action.ts +0 -665
- package/src/api/Answer.ts +0 -1392
- package/src/api/Badge.ts +0 -143
- package/src/api/BaseEntity.ts +0 -6384
- package/src/api/Classified.ts +0 -175
- package/src/api/Comment.ts +0 -280
- package/src/api/EndpointApi.ts +0 -2270
- package/src/api/EndpointApi.types.ts +0 -6566
- package/src/api/EntityRegistry.ts +0 -324
- package/src/api/Event.ts +0 -360
- package/src/api/Form.ts +0 -282
- package/src/api/News.ts +0 -547
- package/src/api/Notifications.ts +0 -196
- package/src/api/Organization.ts +0 -771
- package/src/api/Poi.ts +0 -242
- package/src/api/Project.ts +0 -545
- package/src/api/User.ts +0 -1889
- package/src/api/UserApi.ts +0 -192
- package/src/api/serverDataType/Action.ts +0 -177
- package/src/api/serverDataType/Answer.ts +0 -106
- package/src/api/serverDataType/Classified.ts +0 -152
- package/src/api/serverDataType/Comment.ts +0 -88
- package/src/api/serverDataType/Country.ts +0 -1
- package/src/api/serverDataType/Event.ts +0 -112
- package/src/api/serverDataType/Form.ts +0 -157
- package/src/api/serverDataType/News.ts +0 -138
- package/src/api/serverDataType/Notification.ts +0 -81
- package/src/api/serverDataType/Organization.ts +0 -111
- package/src/api/serverDataType/Poi.ts +0 -75
- package/src/api/serverDataType/Project.ts +0 -115
- package/src/api/serverDataType/User.ts +0 -112
- package/src/api/serverDataType/Zone.ts +0 -33
- package/src/api/serverDataType/common.ts +0 -84
- package/src/costum/normalize.ts +0 -7
- package/src/costum/registry.ts +0 -138
- package/src/costum/runtime-map.ts +0 -221
- package/src/costum/runtime.ts +0 -274
- package/src/costum/schemas/CercleTravailleursSociauxLiberauxLaPlumeSociale.ts +0 -19
- package/src/costum/schemas/FabEn.ts +0 -19
- package/src/costum/schemas/LivingLabInnovationSocialeBraspanon.ts +0 -19
- package/src/costum/schemas/alfph.ts +0 -27
- package/src/costum/schemas/campagne.ts +0 -19
- package/src/costum/schemas/chtitube.ts +0 -19
- package/src/costum/schemas/circuitCourt.ts +0 -19
- package/src/costum/schemas/cmcasHauteBretagne.ts +0 -19
- package/src/costum/schemas/coFinancementTierslieux.ts +0 -27
- package/src/costum/schemas/coeurNumerique.ts +0 -19
- package/src/costum/schemas/communLundi.ts +0 -19
- package/src/costum/schemas/cressReunion.ts +0 -35
- package/src/costum/schemas/cressReunionNetwork.ts +0 -35
- package/src/costum/schemas/ctenat.ts +0 -43
- package/src/costum/schemas/cyberReunion.ts +0 -19
- package/src/costum/schemas/ekisphere.ts +0 -35
- package/src/costum/schemas/equipementsSportifs974.ts +0 -19
- package/src/costum/schemas/federationDesCae.ts +0 -19
- package/src/costum/schemas/formationGenerique.ts +0 -35
- package/src/costum/schemas/franceTierslieux.ts +0 -19
- package/src/costum/schemas/hinaura.ts +0 -27
- package/src/costum/schemas/hubmayotte.ts +0 -19
- package/src/costum/schemas/hubnouvellecaledonie.ts +0 -19
- package/src/costum/schemas/hubpolynesiefrancaise.ts +0 -19
- package/src/costum/schemas/hubreunion.ts +0 -19
- package/src/costum/schemas/hubterritoires.ts +0 -19
- package/src/costum/schemas/hubwallisetfutuna.ts +0 -19
- package/src/costum/schemas/hva.ts +0 -27
- package/src/costum/schemas/ibv2.ts +0 -19
- package/src/costum/schemas/institutBleu.ts +0 -19
- package/src/costum/schemas/journalInsoumisChambery.ts +0 -27
- package/src/costum/schemas/kpacite.ts +0 -19
- package/src/costum/schemas/laCompagnieDesTierslieux.ts +0 -43
- package/src/costum/schemas/laMaisonDesFaiseurs.ts +0 -19
- package/src/costum/schemas/laReunionPourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/schemas/laSemaineDesTierslieux.ts +0 -19
- package/src/costum/schemas/labEnCommuns.ts +0 -19
- package/src/costum/schemas/lakou.ts +0 -27
- package/src/costum/schemas/lesCommunsDesTierslieux.ts +0 -19
- package/src/costum/schemas/mayottePourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/schemas/meir.ts +0 -27
- package/src/costum/schemas/navigatorDesTierslieux.ts +0 -19
- package/src/costum/schemas/notragora.ts +0 -27
- package/src/costum/schemas/nouvellecaledoniePourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/schemas/openAtlas.ts +0 -19
- package/src/costum/schemas/polynesieFrancaisePourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/schemas/prdr.ts +0 -19
- package/src/costum/schemas/reseauTierslieux.ts +0 -19
- package/src/costum/schemas/rfflabs.ts +0 -19
- package/src/costum/schemas/ries.ts +0 -19
- package/src/costum/schemas/siteDuPactePourLaTransition.ts +0 -27
- package/src/costum/schemas/sportSanteBienetre.ts +0 -43
- package/src/costum/schemas/suppa.ts +0 -19
- package/src/costum/schemas/terramies.ts +0 -19
- package/src/costum/schemas/testeTplFormation.ts +0 -35
- package/src/costum/schemas/tierslieuxbelgique.ts +0 -19
- package/src/costum/schemas/tierslieuxorg1.ts +0 -35
- package/src/costum/schemas/transiter.ts +0 -35
- package/src/costum/schemas/vhelioPeiTour2025.ts +0 -19
- package/src/costum/schemas/wallisetfutunaPourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/symbol.ts +0 -26
- package/src/costum/types.ts +0 -895
- package/src/endpoints.module.ts +0 -4253
- package/src/error.ts +0 -86
- package/src/forms/entityForm.ts +0 -142
- package/src/index.ts +0 -161
- package/src/mixin/UserMixin.ts +0 -4
- package/src/types/api-responses.ts +0 -217
- package/src/types/entities.ts +0 -58
- package/src/types/error-guards.ts +0 -230
- package/src/types/index.ts +0 -41
- package/src/types/payloads.ts +0 -21
- package/src/types/transforms.ts +0 -119
- package/src/utils/FileOfflineStorageStrategy.node.ts +0 -50
- package/src/utils/FileStorageStrategy.node.ts +0 -63
- package/src/utils/MultiServerFileStorageStrategy.node.ts +0 -67
- package/src/utils/MultiServerTokenStorageStrategy.ts +0 -139
- package/src/utils/OfflineClientManager.ts +0 -216
- package/src/utils/OfflineQueueStorageStrategy.ts +0 -47
- package/src/utils/TokenStorage.ts +0 -77
- package/src/utils/compat.ts +0 -12
- package/src/utils/createDefaultMultiServerTokenStorageStrategy.ts +0 -35
- package/src/utils/createDefaultOfflineStrategy.ts +0 -34
- package/src/utils/createDefaultTokenStorageStrategy.ts +0 -33
- package/src/utils/reactive.ts +0 -243
- package/src/utils/stream-utils.node.ts +0 -12
package/src/api/Answer.ts
DELETED
|
@@ -1,1392 +0,0 @@
|
|
|
1
|
-
import { BaseEntity } from "./BaseEntity.js";
|
|
2
|
-
import { ApiError } from "../error.js";
|
|
3
|
-
|
|
4
|
-
import type { CoformAnswersByIdData, CoformGetAnswerFilesData, CoformUploadAnswerFileData, DeleteElementData, GetCoformAnswerHistoryData, GetCoformMultievalDataData, SaveCoformAnswerData } from "./EndpointApi.types.js";
|
|
5
|
-
import type { AnswerItemNormalized } from "./serverDataType/Answer.js";
|
|
6
|
-
import type { FormItemNormalized } from "./serverDataType/Form.js";
|
|
7
|
-
|
|
8
|
-
type UploadInput = File | Blob | Buffer | import("stream").Readable;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Format des données utilisateur d'un CoForm — `{ subFormId: { inputId: value } }`.
|
|
12
|
-
* Générique : la lib ne fait aucune supposition sur le type des valeurs.
|
|
13
|
-
*/
|
|
14
|
-
export type AllStepsData = Record<string, Record<string, unknown>>;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Une entrée de l'historique d'audit d'une réponse (réponse de
|
|
18
|
-
* `GET_COFORM_ANSWER_HISTORY`). `userName`/`userSlug` sont dénormalisés
|
|
19
|
-
* au moment de la modification : survivent à une suppression de l'user.
|
|
20
|
-
*/
|
|
21
|
-
export interface AnswerChange {
|
|
22
|
-
userId: string;
|
|
23
|
-
userName: string;
|
|
24
|
-
userSlug: string;
|
|
25
|
-
/** Timestamp unix en SECONDES (pas ms) — `new Date(at * 1000)`. */
|
|
26
|
-
at: number;
|
|
27
|
-
/** `""` possible : cast PHP `(string)` avec défaut vide si champ absent. */
|
|
28
|
-
mutationType: "create" | "update" | "";
|
|
29
|
-
changedFields: string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Un axe du radar multi-eval (= un input radioNew d'une step donnée). */
|
|
33
|
-
export interface MultiEvalAxis {
|
|
34
|
-
key: string;
|
|
35
|
-
label: string;
|
|
36
|
-
options: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Un dataset = la contribution d'un user à un step (1 dataset = 1 user). */
|
|
40
|
-
export interface MultiEvalDataset {
|
|
41
|
-
userId: string;
|
|
42
|
-
userName: string;
|
|
43
|
-
userSlug: string;
|
|
44
|
-
evaluatedAt: number | null;
|
|
45
|
-
values: Record<string, number>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Une step avec ses axes et ses contributeurs (élément de `getMultiEvalData`). */
|
|
49
|
-
export interface MultiEvalStepData {
|
|
50
|
-
stepKey: string;
|
|
51
|
-
stepName: string;
|
|
52
|
-
axes: MultiEvalAxis[];
|
|
53
|
-
datasets: MultiEvalDataset[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Réponse de `Answer.getMultiEvalData()`. */
|
|
57
|
-
export interface MultiEvalDataResponse {
|
|
58
|
-
steps: MultiEvalStepData[];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Valeur d'un upload en attente — objet `{ name?, data: "data:...;base64,..." }`
|
|
63
|
-
* inséré par les inputs uploader avant le pré-upload backend.
|
|
64
|
-
*/
|
|
65
|
-
export interface PendingUploadValue {
|
|
66
|
-
name?: string;
|
|
67
|
-
data: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Un pending upload détecté dans la structure `allData` — chemin imbriqué +
|
|
72
|
-
* type d'input dérivé du schéma (`formData.inputs[subFormId].inputs[inputId].type`).
|
|
73
|
-
*/
|
|
74
|
-
export interface PendingUpload {
|
|
75
|
-
value: PendingUploadValue;
|
|
76
|
-
path: (string | number)[];
|
|
77
|
-
inputType?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Options de `Answer.processUploads()`.
|
|
82
|
-
*/
|
|
83
|
-
export interface ProcessUploadsOptions {
|
|
84
|
-
/**
|
|
85
|
-
* Schéma du Form (pour déduire `inputType` des champs uploader/simpleTable).
|
|
86
|
-
* Auto-récupéré via `this.parent.serverData` si parent est un Form.
|
|
87
|
-
*/
|
|
88
|
-
formData?: FormItemNormalized;
|
|
89
|
-
/** Nombre d'uploads parallèles par batch. Défaut : 4. */
|
|
90
|
-
batchSize?: number;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Options de `Answer.get()` — params optionnels du endpoint `COFORM_ANSWERS_BY_ID`.
|
|
95
|
-
*/
|
|
96
|
-
export interface AnswerGetOptions {
|
|
97
|
-
/**
|
|
98
|
-
* Liste de champs à retourner côté backend (filtrage). Si absent, tous les
|
|
99
|
-
* champs sont renvoyés. Utile pour optimiser la taille de la réponse.
|
|
100
|
-
*/
|
|
101
|
-
fields?: string[];
|
|
102
|
-
/**
|
|
103
|
-
* ID du formulaire parent. Utilisé côté backend pour calculer `canEdit` et
|
|
104
|
-
* `editDeniedReason` (droits d'édition contextuels). Si absent, **auto-injecté
|
|
105
|
-
* via `this.parent.id`** quand le parent est un `Form` (cas typique
|
|
106
|
-
* `form.answer({id})`).
|
|
107
|
-
*/
|
|
108
|
-
formId?: string;
|
|
109
|
-
/** Chemin Finder (rarement utilisé). */
|
|
110
|
-
finderPath?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Options pour `Answer.uploadFile()`. Tous les champs sont optionnels — les
|
|
115
|
-
* défauts reproduisent les valeurs backend (`docType: "image"`, `contentKey: "slider"`).
|
|
116
|
-
*/
|
|
117
|
-
export interface UploadAnswerFileOptions {
|
|
118
|
-
/** Type de document : `image` (défaut) ou `file` (PDF, DOCX, etc.). */
|
|
119
|
-
docType?: "image" | "file";
|
|
120
|
-
/** Clé de contenu côté backend. `"slider"` (défaut) pour images, `"presentation"` pour inputs uploader. */
|
|
121
|
-
contentKey?: string;
|
|
122
|
-
/** Sous-clé optionnelle (ex: `"subFormId.inputId"` pour les inputs `*.uploader`). */
|
|
123
|
-
subKey?: string;
|
|
124
|
-
/** UUID client (compat fine-uploader). Auto-généré si absent. */
|
|
125
|
-
qquuid?: string;
|
|
126
|
-
/** Nom du fichier. Auto-déduit (File.name, stream.path, ou fallback) si absent. */
|
|
127
|
-
qqfilename?: string;
|
|
128
|
-
/**
|
|
129
|
-
* Taille du fichier en octets. Auto-déduite pour `File` (`.size`), `Blob`
|
|
130
|
-
* (`.size`), `Buffer` (`.length`), ou `Readable` annoté (`.size`).
|
|
131
|
-
*
|
|
132
|
-
* **À fournir explicitement pour un `Readable` non annoté** (ex:
|
|
133
|
-
* `fs.createReadStream(path)` → faire `fs.statSync(path).size` au préalable),
|
|
134
|
-
* sinon `0` est envoyé et le backend peut rejeter le multipart.
|
|
135
|
-
*/
|
|
136
|
-
qqtotalfilesize?: number;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Retour normalisé de `Answer.uploadFile()`.
|
|
141
|
-
*/
|
|
142
|
-
export interface UploadAnswerFileResult {
|
|
143
|
-
/** ID du document MongoDB (à stocker dans `{docId, docPath}` pour les inputs uploader). */
|
|
144
|
-
docId: string;
|
|
145
|
-
/** Chemin du document. URL absolue côté backend, à nettoyer en chemin relatif si besoin (cf. `cleanUploaderUrls` site-json). */
|
|
146
|
-
docPath: string;
|
|
147
|
-
/** ID de l'Answer (créée par cet upload si premier, sinon `= this.id`). */
|
|
148
|
-
answerId: string;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Options de `Answer.getFiles()`.
|
|
153
|
-
*/
|
|
154
|
-
export interface GetFilesOptions {
|
|
155
|
-
/** Clé d'identification de l'input (ex: `"subFormId.inputId"`). Requise. */
|
|
156
|
-
subKey: string;
|
|
157
|
-
/** Type de document attendu. Défaut backend : `"file"`. */
|
|
158
|
-
docType?: "image" | "file";
|
|
159
|
-
/** Clé de contenu utilisée lors de l'upload. Défaut backend : `"presentation"`. */
|
|
160
|
-
contentKey?: string;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Élément renvoyé par `Answer.getFiles()` — un document attaché à l'Answer.
|
|
165
|
-
* Les champs `imagePath`/`imageThumbPath`/`imageMediumPath` ne sont présents
|
|
166
|
-
* que pour `docType: "image"`.
|
|
167
|
-
*/
|
|
168
|
-
export interface AnswerFileItem {
|
|
169
|
-
/** ID MongoDB du document (utilisable pour suppression via `deleteDocumentById`). */
|
|
170
|
-
docId: string;
|
|
171
|
-
/** Chemin relatif du document côté backend. */
|
|
172
|
-
docPath: string;
|
|
173
|
-
/** Nom original du fichier. */
|
|
174
|
-
name?: string;
|
|
175
|
-
/** Taille en octets. */
|
|
176
|
-
size?: number;
|
|
177
|
-
/** Chemin de l'image (uniquement si `docType: "image"`). */
|
|
178
|
-
imagePath?: string | null;
|
|
179
|
-
/** Chemin du thumbnail (uniquement si `docType: "image"`). */
|
|
180
|
-
imageThumbPath?: string | null;
|
|
181
|
-
/** Chemin de la version medium (uniquement si `docType: "image"`). */
|
|
182
|
-
imageMediumPath?: string | null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
186
|
-
static override entityType = "answers";
|
|
187
|
-
|
|
188
|
-
static override entityTag = "Answer";
|
|
189
|
-
static override SCHEMA_CONSTANTS: string[] = [
|
|
190
|
-
"SAVE_COFORM_ANSWER"
|
|
191
|
-
];
|
|
192
|
-
|
|
193
|
-
static ADD_BLOCKS = new Map([
|
|
194
|
-
["SAVE_COFORM_ANSWER", "saveCoformAnswer"]
|
|
195
|
-
] as const);
|
|
196
|
-
|
|
197
|
-
static UPDATE_BLOCKS = new Map([
|
|
198
|
-
["SAVE_COFORM_ANSWER", "saveCoformAnswer"]
|
|
199
|
-
] as const);
|
|
200
|
-
|
|
201
|
-
override defaultFields: Record<string, any> = {
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Champs `serverData` à ne pas renvoyer au serveur dans le payload de save.
|
|
206
|
-
* `answers`, `addedOptions`, `links`, `form` sont conservés (envoyés au serveur).
|
|
207
|
-
* Les autres sont calculés/posés par le backend.
|
|
208
|
-
*/
|
|
209
|
-
override removeFields: string[] = [
|
|
210
|
-
"_id", "collection", "user", "created", "updated",
|
|
211
|
-
"modified", "draft", "finished", "context",
|
|
212
|
-
"voteCount", "vote", "project"
|
|
213
|
-
];
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* {@inheritDoc BaseEntity#_isAdminViaHierarchy}
|
|
217
|
-
*
|
|
218
|
-
* Override Answer : la hiérarchie d'Answer passe par son **parent instance
|
|
219
|
-
* Form** (pas par `serverData.parent` direct comme pour `events`/`projects`/`poi`).
|
|
220
|
-
*
|
|
221
|
-
* Chaîne : `Answer → parent (Form) → form.serverData.parent ({[orgId]: {type, name}}) → check userContext.links`.
|
|
222
|
-
*
|
|
223
|
-
* Si le parent n'est pas un Form (cas `api.answer({id})` direct sans contexte),
|
|
224
|
-
* renvoie `false` — impossible de vérifier la hiérarchie sans le Form.
|
|
225
|
-
*
|
|
226
|
-
* @protected
|
|
227
|
-
*/
|
|
228
|
-
protected override _isAdminViaHierarchy(): boolean {
|
|
229
|
-
const form = this.parent;
|
|
230
|
-
if (!form || typeof (form as any).getEntityType !== "function"
|
|
231
|
-
|| (form as any).getEntityType() !== "forms") {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const formParents = (form as { serverData?: { parent?: Record<string, unknown> } }).serverData?.parent;
|
|
236
|
-
if (!formParents || typeof formParents !== "object") {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const [parentId, parentRef] of Object.entries(formParents)) {
|
|
241
|
-
if (!parentRef || typeof parentRef !== "object") continue;
|
|
242
|
-
const ref = parentRef as { type?: string };
|
|
243
|
-
if (!ref.type) continue;
|
|
244
|
-
if (this._isAdminOfParent(parentId, ref.type)) {
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* {@inheritDoc BaseEntity#isAuthor}
|
|
253
|
-
*
|
|
254
|
-
* Override Answer : le champ d'autorité est `serverData.user` (pas `creator`
|
|
255
|
-
* comme la version par défaut de BaseEntity). Le `user` peut être :
|
|
256
|
-
* - un `string` (ID MongoDB brut renvoyé par le backend)
|
|
257
|
-
* - une instance `User` (après `_transformServerData` qui le link en entité)
|
|
258
|
-
*
|
|
259
|
-
* Les pré-conditions (connexion, id, serverData) sont vérifiées comme dans
|
|
260
|
-
* la version de base — `silent: true` (défaut) renvoie `false` au lieu de throw.
|
|
261
|
-
*/
|
|
262
|
-
override isAuthor(options?: { silent?: boolean }): boolean {
|
|
263
|
-
const silent = options?.silent ?? true;
|
|
264
|
-
try {
|
|
265
|
-
// Note : on utilise `isConnected` (user logged in) au lieu de `isMe`
|
|
266
|
-
// (l'entité IS le user logged in) qui est sémantiquement faux sur Answer.
|
|
267
|
-
// `_checkAccess` du parent est private et utilise isMe — non réutilisable ici.
|
|
268
|
-
if (!this.isConnected) throw new ApiError("Vous devez être connecté pour vérifier l'auteur.", 401);
|
|
269
|
-
if (!this.id) throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
270
|
-
if (!this.serverData) throw new ApiError("Aucune donnée serveur disponible.", 404);
|
|
271
|
-
if (!this.userId) throw new ApiError("Utilisateur non connecté.", 401);
|
|
272
|
-
} catch (e) {
|
|
273
|
-
if (silent) return false;
|
|
274
|
-
throw e;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const user = this._serverData.user;
|
|
278
|
-
if (!user) return false;
|
|
279
|
-
|
|
280
|
-
const authorId = typeof user === "string" ? user : (user as { id?: string }).id;
|
|
281
|
-
return Boolean(
|
|
282
|
-
this.userId
|
|
283
|
-
&& typeof authorId === "string"
|
|
284
|
-
&& authorId === this.userId
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Transforme les champs imbriqués (user, context etc.) en instances d'entités.
|
|
290
|
-
* @param data - Les données brutes du serveur.
|
|
291
|
-
* @returns Les données transformées.
|
|
292
|
-
* @protected
|
|
293
|
-
*/
|
|
294
|
-
protected override _transformServerData(data: AnswerItemNormalized): AnswerItemNormalized {
|
|
295
|
-
|
|
296
|
-
// Transformer user en instance User
|
|
297
|
-
if(data.user && typeof data.user === "object" && "id" in data.user) {
|
|
298
|
-
data.user = this._linkNestedEntity({ type: "citoyens", collection: "citoyens", ...data.user });
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Normalisation legacy uploader : si parent Form connu, convertit les valeurs
|
|
302
|
-
// d'inputs `*.uploader` du format Array `[{docId, docPath}]` vers le format
|
|
303
|
-
// canonique `{updateDate, files: {docId: docPath}}`. Symétrique de ce que
|
|
304
|
-
// `processUploads()` écrit. Si parent pas Form → skip (pas de schéma pour
|
|
305
|
-
// identifier les inputs uploader).
|
|
306
|
-
const formData = this._resolveFormData();
|
|
307
|
-
if (formData && data.answers && typeof data.answers === "object") {
|
|
308
|
-
data.answers = this._normalizeLegacyUploaderInAnswers(
|
|
309
|
-
data.answers as Record<string, Record<string, unknown>>,
|
|
310
|
-
formData,
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return data;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Rafraîchit les données de l'entité depuis l'API (`COFORM_ANSWERS_BY_ID`).
|
|
319
|
-
*
|
|
320
|
-
* **Auto-injection du `formId`** : si le parent est un `Form` (cas typique
|
|
321
|
-
* `form.answer({id})`), le param `formId` est automatiquement envoyé pour
|
|
322
|
-
* que le backend calcule `canEdit` et `editDeniedReason` dans la réponse.
|
|
323
|
-
* Le caller peut override via `opts.formId` ou couper avec `opts.formId: ""`.
|
|
324
|
-
*
|
|
325
|
-
* @param opts - Params optionnels :
|
|
326
|
-
* - `fields` : filtrage des champs côté backend
|
|
327
|
-
* - `formId` : override de l'auto-injection (utile pour disable ou forcer un autre form)
|
|
328
|
-
* - `finderPath` : chemin Finder (rare)
|
|
329
|
-
*
|
|
330
|
-
* @example
|
|
331
|
-
* // Cas commun : form.answer({id}) → get() auto avec formId
|
|
332
|
-
* const answer = await form.answer({ id: answerId });
|
|
333
|
-
* answer.serverData.canEdit; // true/false calculé backend
|
|
334
|
-
* answer.serverData.editDeniedReason; // raison si false
|
|
335
|
-
*
|
|
336
|
-
* @example
|
|
337
|
-
* // Cas avancé : filtrage de champs pour optim perf
|
|
338
|
-
* const answer = await form.answer();
|
|
339
|
-
* answer._id(answerId);
|
|
340
|
-
* await answer.get({ fields: ["answers", "canEdit", "editDeniedReason"] });
|
|
341
|
-
*/
|
|
342
|
-
override async get(opts: AnswerGetOptions = {}): Promise<Record<string, any>> {
|
|
343
|
-
if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.", 400);
|
|
344
|
-
const answerId = this.id; // Type narrowing
|
|
345
|
-
|
|
346
|
-
// Auto-injection du formId via parent Form (sauf si caller a explicitement
|
|
347
|
-
// passé opts.formId — même chaîne vide pour disable).
|
|
348
|
-
const formId = "formId" in opts ? opts.formId : this._autoFormIdFromParent();
|
|
349
|
-
|
|
350
|
-
const payload: CoformAnswersByIdData = { answerId };
|
|
351
|
-
if (opts.fields && opts.fields.length > 0) payload.fields = opts.fields;
|
|
352
|
-
if (typeof formId === "string" && formId.length > 0) payload.formId = formId;
|
|
353
|
-
if (opts.finderPath) payload.finderPath = opts.finderPath;
|
|
354
|
-
|
|
355
|
-
const answer = await this.endpointApi.coformAnswersById(payload);
|
|
356
|
-
|
|
357
|
-
if (answer?.data?.id) {
|
|
358
|
-
this._setData(answer.data as AnswerItemNormalized, { forceInitialDraftReset: true });
|
|
359
|
-
return this.serverData;
|
|
360
|
-
}
|
|
361
|
-
throw new ApiError(`Aucune réponse trouvée pour l'ID ${this.id}`, 404);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Récupère l'ID du parent Form si applicable, pour l'auto-injection dans `get()`.
|
|
366
|
-
* @private
|
|
367
|
-
*/
|
|
368
|
-
private _autoFormIdFromParent(): string | undefined {
|
|
369
|
-
const parent = this.parent;
|
|
370
|
-
if (parent && typeof (parent as any).getEntityType === "function"
|
|
371
|
-
&& (parent as any).getEntityType() === "forms"
|
|
372
|
-
&& typeof (parent as any).id === "string") {
|
|
373
|
-
return (parent as any).id as string;
|
|
374
|
-
}
|
|
375
|
-
return undefined;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
override async form(): Promise<never> {
|
|
379
|
-
throw new ApiError(`form n'existe pas dans ${this.constructor.name}`, 501);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Supprime cette Answer côté serveur via `DELETE_ELEMENT`
|
|
384
|
-
* (`type=answers`, `id=this.id`).
|
|
385
|
-
*
|
|
386
|
-
* Après succès :
|
|
387
|
-
* - `serverData` et `draftData` sont vidés (sans casser la réactivité)
|
|
388
|
-
* - `_isDeleted = true` → toute mutation ultérieure (`save`, `get`, etc.) throw
|
|
389
|
-
*
|
|
390
|
-
* **Guard** : autorisation côté client si l'utilisateur courant est :
|
|
391
|
-
* - l'**auteur** de l'Answer (`serverData.user === userId`), OU
|
|
392
|
-
* - **admin** d'un parent du Form (org/project) via la hiérarchie
|
|
393
|
-
* (`form.serverData.parent[orgId]` × `userContext.links.memberOf[orgId].isAdmin`).
|
|
394
|
-
*
|
|
395
|
-
* Le check utilise `isAuthorOrAdmin({checkHierarchy: true})` qui s'appuie sur
|
|
396
|
-
* l'override `Answer._isAdminViaHierarchy` (remonte via parent instance Form).
|
|
397
|
-
*
|
|
398
|
-
* **Pré-requis** :
|
|
399
|
-
* - `this.parent` doit être un Form (cas `form.answer({id})`)
|
|
400
|
-
* - `userContext` (User connecté) doit avoir `serverData.links` chargés (cas `api.me()`)
|
|
401
|
-
* Sans ces pré-requis, seul `isAuthor` peut autoriser.
|
|
402
|
-
*
|
|
403
|
-
* @param reason - Raison libre transmise au backend (audit). Défaut : `"delete coform answer"`.
|
|
404
|
-
* @throws {ApiError} 400 si l'Answer n'a pas d'id (jamais sauvegardée).
|
|
405
|
-
* @throws {ApiError} 403 si ni auteur ni admin du parent.
|
|
406
|
-
*
|
|
407
|
-
* @example
|
|
408
|
-
* const answer = await form.answer({ id: "..." });
|
|
409
|
-
* await answer.delete(); // OK si auteur OU admin org/project parent du Form
|
|
410
|
-
* answer._isDeleted; // true
|
|
411
|
-
*/
|
|
412
|
-
async delete(reason: string = "delete coform answer"): Promise<void> {
|
|
413
|
-
if (!this.id) {
|
|
414
|
-
throw new ApiError("Vous devez fournir un id pour supprimer une Answer.", 400);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Guard : auteur OU admin du parent Form (org/project).
|
|
418
|
-
if (!this.isAuthorOrAdmin({ checkHierarchy: true })) {
|
|
419
|
-
throw new ApiError(
|
|
420
|
-
"Suppression refusée — vous devez être l'auteur de l'Answer "
|
|
421
|
-
+ "ou administrateur du parent (organisation/projet) du Form.",
|
|
422
|
-
403
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const data: DeleteElementData = {
|
|
427
|
-
reason,
|
|
428
|
-
pathParams: { type: "answers", id: this.id },
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
await this.callIsConnected(() => this.endpointApi.deleteElement(data));
|
|
432
|
-
|
|
433
|
-
// Vider les objets réactifs sans casser la réactivité (cf. Comment.delete)
|
|
434
|
-
Object.keys(this._draftData).forEach(key => delete this._draftData[key]);
|
|
435
|
-
Object.keys(this._serverData).forEach(key => delete (this._serverData as any)[key]);
|
|
436
|
-
|
|
437
|
-
this._isDeleted = true;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* {@inheritDoc BaseEntity#deleteFile}
|
|
442
|
-
*
|
|
443
|
-
* Override avec **cleanup local** : après suppression backend, retire le
|
|
444
|
-
* `docId` des structures uploader normalisées dans `serverData.answers` et
|
|
445
|
-
* `_draftData.answers` (format `{updateDate, files: {docId: docPath}}`
|
|
446
|
-
* produit par `processUploads()` et `_transformServerData`).
|
|
447
|
-
*
|
|
448
|
-
* Le caller n'a pas besoin de refetch — `answer.serverData.answers[*][*].files`
|
|
449
|
-
* reflète directement l'état post-suppression.
|
|
450
|
-
*
|
|
451
|
-
* @example
|
|
452
|
-
* const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
|
|
453
|
-
* await answer.deleteFile(files[0].docId);
|
|
454
|
-
* // answer.serverData.answers[sf][input].files[files[0].docId] === undefined
|
|
455
|
-
*/
|
|
456
|
-
override async deleteFile(docId: string): Promise<void> {
|
|
457
|
-
await super.deleteFile(docId);
|
|
458
|
-
|
|
459
|
-
// Cleanup local : retire le docId des structures uploader normalisées
|
|
460
|
-
this._removeDocIdFromUploaderFields(this._serverData.answers, docId);
|
|
461
|
-
this._removeDocIdFromUploaderFields(this._draftData.answers as Record<string, unknown> | undefined, docId);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Walk `answers[subFormId][inputId]` et retire `docId` des structures
|
|
466
|
-
* `{updateDate, files: {docId: docPath}}` (format canonique uploader).
|
|
467
|
-
* Mutation in-place pour préserver la réactivité.
|
|
468
|
-
* @private
|
|
469
|
-
*/
|
|
470
|
-
private _removeDocIdFromUploaderFields(
|
|
471
|
-
answers: Record<string, unknown> | undefined,
|
|
472
|
-
docId: string,
|
|
473
|
-
): void {
|
|
474
|
-
if (!answers || typeof answers !== "object") return;
|
|
475
|
-
|
|
476
|
-
for (const subFormData of Object.values(answers)) {
|
|
477
|
-
if (!this._isObjectRecord(subFormData)) continue;
|
|
478
|
-
for (const inputValue of Object.values(subFormData)) {
|
|
479
|
-
if (!this._isObjectRecord(inputValue)) continue;
|
|
480
|
-
const files = (inputValue as { files?: unknown }).files;
|
|
481
|
-
if (this._isObjectRecord(files) && docId in files) {
|
|
482
|
-
delete (files as Record<string, unknown>)[docId];
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Récupère la liste des fichiers attachés à un input précis de cette Answer
|
|
490
|
-
* via `COFORM_GET_ANSWER_FILES`.
|
|
491
|
-
*
|
|
492
|
-
* **Cas d'usage principal** : récupérer les fichiers d'inputs uploader stockés
|
|
493
|
-
* en **format legacy** `{updateDate: [...]}` (sans la clé `files`). Le backend
|
|
494
|
-
* recherche alors les documents MongoDB liés à `{answerId, subKey, docType}`.
|
|
495
|
-
*
|
|
496
|
-
* Pour les Answer en format moderne `{updateDate, files: {<docId>: <path>}}`,
|
|
497
|
-
* les fichiers sont déjà dans `answer.serverData.answers[subFormId][inputId].files`
|
|
498
|
-
* — pas besoin d'appel réseau.
|
|
499
|
-
*
|
|
500
|
-
* @param opts - `subKey` (requis, ex: `"subFormId.inputId"`), `docType`, `contentKey`
|
|
501
|
-
* @returns Liste normalisée des fichiers `{docId, docPath, name?, size?, imagePath?, imageThumbPath?, imageMediumPath?}`.
|
|
502
|
-
* Tableau vide si aucun fichier trouvé.
|
|
503
|
-
* @throws {ApiError} 400 si pas d'id Answer. Throws aussi si le backend renvoie `result: false`.
|
|
504
|
-
*
|
|
505
|
-
* @example
|
|
506
|
-
* const form = await org.form({ id: formId });
|
|
507
|
-
* const answer = await form.answer({ id: answerId });
|
|
508
|
-
* const files = await answer.getFiles({
|
|
509
|
-
* subKey: `${subFormId}.${inputId}`,
|
|
510
|
-
* docType: "image",
|
|
511
|
-
* });
|
|
512
|
-
* files[0].docId; // ID du document (pour suppression future)
|
|
513
|
-
* files[0].docPath; // Chemin relatif (à concaténer avec baseURL pour affichage)
|
|
514
|
-
*/
|
|
515
|
-
async getFiles(opts: GetFilesOptions): Promise<AnswerFileItem[]> {
|
|
516
|
-
if (!this.id) {
|
|
517
|
-
throw new ApiError("Answer sans id, impossible de lister ses fichiers.", 400);
|
|
518
|
-
}
|
|
519
|
-
if (!opts.subKey || opts.subKey.length === 0) {
|
|
520
|
-
throw new ApiError("`subKey` requis pour lister les fichiers d'un input.", 400);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const payload: CoformGetAnswerFilesData = {
|
|
524
|
-
answerId: this.id,
|
|
525
|
-
subKey: opts.subKey,
|
|
526
|
-
};
|
|
527
|
-
if (opts.docType) payload.docType = opts.docType;
|
|
528
|
-
if (opts.contentKey) payload.contentKey = opts.contentKey;
|
|
529
|
-
|
|
530
|
-
// Forme observée du backend (cf. site-json useCoFormAnswerFiles.tsx
|
|
531
|
-
// et schema COFORM_GET_ANSWER_FILES) :
|
|
532
|
-
// { result, answerId, subKey, contentKey, count, files: [{ id, name, size,
|
|
533
|
-
// docPath, imagePath, imageThumbPath, imageMediumPath }] }
|
|
534
|
-
const response = await this.callIsConnected(() =>
|
|
535
|
-
this.endpointApi.coformGetAnswerFiles(payload)
|
|
536
|
-
) as {
|
|
537
|
-
result?: boolean;
|
|
538
|
-
msg?: string;
|
|
539
|
-
count?: number;
|
|
540
|
-
files?: Array<{
|
|
541
|
-
id?: string | null;
|
|
542
|
-
name?: string | null;
|
|
543
|
-
size?: number | null;
|
|
544
|
-
docPath?: string;
|
|
545
|
-
imagePath?: string | null;
|
|
546
|
-
imageThumbPath?: string | null;
|
|
547
|
-
imageMediumPath?: string | null;
|
|
548
|
-
}>;
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
// Filtre les entrées invalides (id + docPath requis) et normalise.
|
|
552
|
-
return (response.files ?? [])
|
|
553
|
-
.filter(f => typeof f.id === "string" && typeof f.docPath === "string")
|
|
554
|
-
.map(f => ({
|
|
555
|
-
docId: f.id!,
|
|
556
|
-
docPath: f.docPath!,
|
|
557
|
-
name: f.name ?? undefined,
|
|
558
|
-
size: f.size ?? undefined,
|
|
559
|
-
imagePath: f.imagePath ?? undefined,
|
|
560
|
-
imageThumbPath: f.imageThumbPath ?? undefined,
|
|
561
|
-
imageMediumPath: f.imageMediumPath ?? undefined,
|
|
562
|
-
}));
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Récupère l'historique d'audit de cette Answer.
|
|
567
|
-
*
|
|
568
|
-
* Trace `create` + `update` triée du plus récent au plus ancien, max 200
|
|
569
|
-
* entrées (cap backend). Auth côté serveur : owner OU canAdminAnswer
|
|
570
|
-
* (admin du parent du form, admin/membre finder selon
|
|
571
|
-
* `membersCanEditSharedAnswer`, ou form `publicCanEditSharedAnswer`).
|
|
572
|
-
*
|
|
573
|
-
* Constant : `GET_COFORM_ANSWER_HISTORY` (POST
|
|
574
|
-
* `/survey/coform/getanswerhistory`, auth bearer).
|
|
575
|
-
*
|
|
576
|
-
* @returns Liste des changements ; tableau vide si l'historique est vide.
|
|
577
|
-
* @throws {ApiError} 400 si Answer sans id.
|
|
578
|
-
* @throws {ApiAuthenticationError} si non connecté.
|
|
579
|
-
*
|
|
580
|
-
* @example
|
|
581
|
-
* const form = await org.form({ id: formId });
|
|
582
|
-
* const answer = await form.answer({ id: answerId });
|
|
583
|
-
* const history = await answer.getHistory();
|
|
584
|
-
* new Date(history[0].at * 1000); // date de la dernière modif (at en SECONDES)
|
|
585
|
-
*/
|
|
586
|
-
async getHistory(): Promise<AnswerChange[]> {
|
|
587
|
-
if (!this.id) {
|
|
588
|
-
throw new ApiError("Answer sans id, impossible de récupérer son historique.", 400);
|
|
589
|
-
}
|
|
590
|
-
const payload: GetCoformAnswerHistoryData = { answerId: this.id };
|
|
591
|
-
// EndpointApi.call retourne le body HTTP ; succès serveur = {result: true, data: {history}}.
|
|
592
|
-
const body = await this.callIsConnected(() =>
|
|
593
|
-
this.endpointApi.getCoformAnswerHistory(payload)
|
|
594
|
-
) as { result?: boolean; data?: { history?: AnswerChange[] } };
|
|
595
|
-
const list = body?.data?.history;
|
|
596
|
-
return Array.isArray(list) ? list : [];
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Récupère les datasets agrégés des évaluations multiples (radar) de cette
|
|
601
|
-
* Answer. Pour une réponse partagée contenant des inputs `radioNew` avec
|
|
602
|
-
* `activeMultieval: true`, retourne un dataset par contributeur pour
|
|
603
|
-
* chaque step.
|
|
604
|
-
*
|
|
605
|
-
* Constant : `GET_COFORM_MULTIEVAL_DATA` (POST
|
|
606
|
-
* `/survey/coform/getmultievaldata`, auth bearer). Auth côté serveur :
|
|
607
|
-
* owner OU canAdminAnswer.
|
|
608
|
-
*
|
|
609
|
-
* @param opts.stepKey - Optionnel : filtrer aux datasets d'une seule
|
|
610
|
-
* step. Sinon retourne toutes les steps.
|
|
611
|
-
* @throws {ApiError} 400 si Answer sans id.
|
|
612
|
-
* @throws {ApiAuthenticationError} si non connecté.
|
|
613
|
-
*
|
|
614
|
-
* @example
|
|
615
|
-
* const data = await answer.getMultiEvalData();
|
|
616
|
-
* data.steps.forEach(s => renderRadar(s));
|
|
617
|
-
*/
|
|
618
|
-
async getMultiEvalData(opts: { stepKey?: string | null } = {}): Promise<MultiEvalDataResponse> {
|
|
619
|
-
if (!this.id) {
|
|
620
|
-
throw new ApiError("Answer sans id, impossible de récupérer ses évaluations multiples.", 400);
|
|
621
|
-
}
|
|
622
|
-
const payload: GetCoformMultievalDataData = { answerId: this.id };
|
|
623
|
-
if (opts.stepKey) payload.stepKey = opts.stepKey;
|
|
624
|
-
|
|
625
|
-
// EndpointApi.call retourne le body HTTP ; succès serveur = {result: true, data: {steps}}.
|
|
626
|
-
const body = await this.callIsConnected(() =>
|
|
627
|
-
this.endpointApi.getCoformMultievalData(payload)
|
|
628
|
-
) as { result?: boolean; data?: { steps?: MultiEvalStepData[] } };
|
|
629
|
-
const steps = body?.data?.steps;
|
|
630
|
-
return { steps: Array.isArray(steps) ? steps : [] };
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
|
|
635
|
-
*
|
|
636
|
-
* **Building block du pipeline** : conçu pour être utilisé via
|
|
637
|
-
* `processUploads()` suivi de `save()`. L'Answer créée par le premier upload
|
|
638
|
-
* est en état "placeholder" côté backend (champ `user` non posé) — `save()`
|
|
639
|
-
* finalise. Appeler `get()` ou `delete()` (via l'entité) sur une Answer
|
|
640
|
-
* upload-only sans save échouera : le backend la considère comme orpheline
|
|
641
|
-
* (`editDeniedReason: "not_owner"`, stub sans `data.id`).
|
|
642
|
-
*
|
|
643
|
-
* **Cas premier upload (Answer sans id)** : le backend crée l'Answer et
|
|
644
|
-
* renvoie `answerId`. La méthode pose alors `this._draftData.id = answerId`
|
|
645
|
-
* — les uploads suivants utiliseront cet id, et un `save()` ultérieur passera
|
|
646
|
-
* en update.
|
|
647
|
-
*
|
|
648
|
-
* **Cas uploads suivants (Answer avec id)** : `answerId: this.id` est envoyé.
|
|
649
|
-
*
|
|
650
|
-
* Compatible browser (File/Blob) et Node (Buffer/Readable) via
|
|
651
|
-
* `BaseEntity._prepareUploadFile()` — le multipart encoder du package
|
|
652
|
-
* `form-data` (utilisé en Node) ignore les Buffer bruts, donc conversion en
|
|
653
|
-
* stream annoté.
|
|
654
|
-
*
|
|
655
|
-
* @param file - Fichier à uploader (File/Blob côté browser, Buffer/Readable côté Node)
|
|
656
|
-
* @param opts - `docType`, `contentKey`, `subKey`, `qquuid`, `qqfilename`
|
|
657
|
-
* @returns `{ docId, docPath, answerId }` — toujours fournis par le backend.
|
|
658
|
-
* @throws {ApiError} si `formId` introuvable, ou réponse invalide.
|
|
659
|
-
*
|
|
660
|
-
* @example
|
|
661
|
-
* // Browser (depuis un <input type="file">)
|
|
662
|
-
* const form = await org.form({ id: formId });
|
|
663
|
-
* const answer = await form.answer(); // draft
|
|
664
|
-
* const { docId, docPath } = await answer.uploadFile(htmlFile, {
|
|
665
|
-
* contentKey: "presentation",
|
|
666
|
-
* subKey: `${subFormId}.${inputId}`,
|
|
667
|
-
* });
|
|
668
|
-
* // answer.id désormais défini → uploads suivants utilisent cet id.
|
|
669
|
-
*
|
|
670
|
-
* @example
|
|
671
|
-
* // Node (depuis un Buffer)
|
|
672
|
-
* const buffer = await fs.promises.readFile("./photo.jpg");
|
|
673
|
-
* const result = await answer.uploadFile(buffer, { docType: "image" });
|
|
674
|
-
*/
|
|
675
|
-
async uploadFile(
|
|
676
|
-
file: UploadInput,
|
|
677
|
-
opts: UploadAnswerFileOptions = {},
|
|
678
|
-
): Promise<UploadAnswerFileResult> {
|
|
679
|
-
const formId = this._resolveFormId({});
|
|
680
|
-
|
|
681
|
-
const fallbackName = opts.qqfilename ?? `upload-${Date.now()}`;
|
|
682
|
-
const { qqfile, qqfilename, qqtotalfilesize } =
|
|
683
|
-
await this._prepareUploadFile(file, fallbackName);
|
|
684
|
-
|
|
685
|
-
const payload: CoformUploadAnswerFileData = {
|
|
686
|
-
formId,
|
|
687
|
-
docType: opts.docType ?? "image",
|
|
688
|
-
contentKey: opts.contentKey ?? "slider",
|
|
689
|
-
qquuid: opts.qquuid ?? this._generateUuid(),
|
|
690
|
-
qqfilename: opts.qqfilename ?? qqfilename,
|
|
691
|
-
// Priorité : override caller > taille déduite (File/Blob/Buffer/Readable.size)
|
|
692
|
-
qqtotalfilesize: opts.qqtotalfilesize ?? qqtotalfilesize,
|
|
693
|
-
qqfile: qqfile as unknown as Record<string, unknown>,
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
if (opts.subKey) payload.subKey = opts.subKey;
|
|
697
|
-
if (this.id) payload.answerId = this.id;
|
|
698
|
-
|
|
699
|
-
const response = await this.callIsConnected(() =>
|
|
700
|
-
this.endpointApi.coformUploadAnswerFile(payload)
|
|
701
|
-
) as {
|
|
702
|
-
result?: boolean;
|
|
703
|
-
success?: boolean;
|
|
704
|
-
msg?: string;
|
|
705
|
-
answerId?: string;
|
|
706
|
-
docPath?: string;
|
|
707
|
-
id?: string;
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
// Forme observée (cf. upload-answer-shape.test.ts) :
|
|
711
|
-
// { result, success, answerId, docPath, id, name, size, msg, _id }
|
|
712
|
-
if (response?.result !== true && response?.success !== true) {
|
|
713
|
-
throw new ApiError(response?.msg || "Échec de l'upload coform.", 500);
|
|
714
|
-
}
|
|
715
|
-
if (!response.answerId || !response.docPath) {
|
|
716
|
-
throw new ApiError("Réponse d'upload incomplète (answerId ou docPath manquant).", 500);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Pose this.id si premier upload (Answer auto-créée par le backend)
|
|
720
|
-
if (!this.id) {
|
|
721
|
-
this._draftData.id = response.answerId;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
return {
|
|
725
|
-
docId: response.id ?? "",
|
|
726
|
-
docPath: response.docPath,
|
|
727
|
-
answerId: response.answerId,
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Orchestre l'upload de tous les fichiers pendants (`data:URI`) trouvés dans
|
|
733
|
-
* `allData`, puis renvoie la structure transformée prête pour `answer.save()`.
|
|
734
|
-
*
|
|
735
|
-
* **Pipeline complet** :
|
|
736
|
-
* 1. Walk récursif de `allData` → collecte les `data:URI` avec leur path + inputType
|
|
737
|
-
* 2. Premier upload (si Answer sans id) → backend crée l'Answer + pose `this.id`
|
|
738
|
-
* 3. Uploads restants en batches parallèles (défaut 4)
|
|
739
|
-
* 4. Remplacement dans la structure :
|
|
740
|
-
* - inputs `*.uploader` → `{ docId, docPath }`
|
|
741
|
-
* - autres → `docPath` string
|
|
742
|
-
* 5. Normalisation legacy uploader : `Array<{docId, docPath}>` → `{ updateDate, files }`
|
|
743
|
-
* 6. Nettoyage URLs absolues → chemins relatifs (uploader/simpleTable uniquement)
|
|
744
|
-
*
|
|
745
|
-
* Ne touche PAS `addedOptions` ni `links` — caller les pose séparément avant `save()`.
|
|
746
|
-
*
|
|
747
|
-
* @param allData - Données utilisateur `{ subFormId: { inputId: value } }`
|
|
748
|
-
* @param options - `formData` (auto via parent Form), `batchSize` (défaut 4)
|
|
749
|
-
* @returns `allData` transformé, prêt à être affecté à `answer.data.answers`
|
|
750
|
-
*
|
|
751
|
-
* @example
|
|
752
|
-
* const form = await org.form({ id: formId });
|
|
753
|
-
* const answer = await form.answer(); // ou form.answer({ id }) en édition
|
|
754
|
-
*
|
|
755
|
-
* const prepared = await answer.processUploads(allStepsDataFromForm);
|
|
756
|
-
* answer.data.answers = prepared;
|
|
757
|
-
* answer.data.addedOptions = addedOptions; // optionnel
|
|
758
|
-
* answer.data.links = finderLinks; // optionnel
|
|
759
|
-
* await answer.save();
|
|
760
|
-
*/
|
|
761
|
-
async processUploads(
|
|
762
|
-
allData: AllStepsData,
|
|
763
|
-
options: ProcessUploadsOptions = {},
|
|
764
|
-
): Promise<AllStepsData> {
|
|
765
|
-
// Résoudre formData : argument > parent Form
|
|
766
|
-
const formData = options.formData ?? this._resolveFormData();
|
|
767
|
-
|
|
768
|
-
// 1. Collecter tous les pending uploads
|
|
769
|
-
const pending = this._collectPendingUploads(allData, [], formData);
|
|
770
|
-
|
|
771
|
-
if (pending.length === 0) {
|
|
772
|
-
// Pas de fichiers : on nettoie quand même les URLs absolues côté uploader
|
|
773
|
-
return this._cleanUploaderUrls(allData, formData);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
let prepared = allData;
|
|
777
|
-
let startIndex = 0;
|
|
778
|
-
|
|
779
|
-
// 2. Premier upload (séparé) si pas d'id — pour récupérer answerId
|
|
780
|
-
if (!this.id) {
|
|
781
|
-
const first = pending[0];
|
|
782
|
-
const replaced = await this._uploadAndReplace(first, prepared, 1);
|
|
783
|
-
prepared = replaced;
|
|
784
|
-
startIndex = 1;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// 3. Uploads restants en batches parallèles
|
|
788
|
-
if (startIndex < pending.length) {
|
|
789
|
-
const remaining = pending.slice(startIndex);
|
|
790
|
-
const batchSize = options.batchSize ?? 4;
|
|
791
|
-
|
|
792
|
-
const replacements = await this._uploadInBatches(
|
|
793
|
-
remaining,
|
|
794
|
-
(item, idx) => this._uploadPendingItem(item, startIndex + idx + 1),
|
|
795
|
-
batchSize,
|
|
796
|
-
);
|
|
797
|
-
|
|
798
|
-
remaining.forEach((item, idx) => {
|
|
799
|
-
prepared = this._setValueAtPath(prepared, item.path, replacements[idx]) as AllStepsData;
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// 4. Normalisation legacy uploader (Array → {updateDate, files})
|
|
804
|
-
const uploaderFieldPaths = new Map<string, (string | number)[]>();
|
|
805
|
-
for (const item of pending) {
|
|
806
|
-
if (item.inputType?.endsWith(".uploader") && item.path.length >= 2) {
|
|
807
|
-
const key = JSON.stringify(item.path.slice(0, 2));
|
|
808
|
-
if (!uploaderFieldPaths.has(key)) {
|
|
809
|
-
uploaderFieldPaths.set(key, item.path.slice(0, 2));
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
for (const fieldPath of uploaderFieldPaths.values()) {
|
|
814
|
-
const value = this._getValueAtPath(prepared, fieldPath);
|
|
815
|
-
prepared = this._setValueAtPath(
|
|
816
|
-
prepared,
|
|
817
|
-
fieldPath,
|
|
818
|
-
this._normalizeUploaderValue(value),
|
|
819
|
-
) as AllStepsData;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// 5. Clean URLs absolues → chemins relatifs (uploader/simpleTable)
|
|
823
|
-
return this._cleanUploaderUrls(prepared, formData);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* UUID v4 compatible browser + Node.
|
|
828
|
-
* Utilise `crypto.randomUUID()` si dispo, sinon fallback Math.random.
|
|
829
|
-
* @private
|
|
830
|
-
*/
|
|
831
|
-
private _generateUuid(): string {
|
|
832
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
833
|
-
return crypto.randomUUID();
|
|
834
|
-
}
|
|
835
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
|
|
836
|
-
const r = Math.random() * 16 | 0;
|
|
837
|
-
const v = c === "x" ? r : (r & 0x3 | 0x8);
|
|
838
|
-
return v.toString(16);
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* Sauvegarde une nouvelle Answer (sans `id`) via `SAVE_COFORM_ANSWER`.
|
|
844
|
-
*
|
|
845
|
-
* Le payload caller-side attendu (dans `draftData`) :
|
|
846
|
-
* - `answers` (requis) — objet `{ subFormId: { inputId: value } }`
|
|
847
|
-
* - `form` ou parent Form (requis indirectement) — pour résoudre `formId`
|
|
848
|
-
* - `addedOptions` (optionnel) — multiCheckboxPlus dynamiques
|
|
849
|
-
* - `links` (optionnel) — sélections Finder à attacher à l'answer
|
|
850
|
-
*
|
|
851
|
-
* Sérialise automatiquement `answers`/`addedOptions`/`links` en JSON
|
|
852
|
-
* (le wire-format `application/x-www-form-urlencoded` exige des strings).
|
|
853
|
-
*
|
|
854
|
-
* Après succès, l'ID retourné par le serveur est posé dans `_draftData.id`
|
|
855
|
-
* → le `refresh()` de `BaseEntity.save()` peut alors récupérer l'Answer canonique.
|
|
856
|
-
*
|
|
857
|
-
* @protected — utiliser `answer.save()` (BaseEntity dispatch automatiquement).
|
|
858
|
-
*/
|
|
859
|
-
override _add = async (payload: Record<string, any>): Promise<void> => {
|
|
860
|
-
if (!this._calledFromSave) {
|
|
861
|
-
throw new Error("utilisation invalide de _add, utilisez save");
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const formId = this._resolveFormId(payload);
|
|
865
|
-
const requestData = this._buildSavePayload(formId, payload, /* withAnswerId */ false);
|
|
866
|
-
|
|
867
|
-
const response = await this.callIsConnected(() =>
|
|
868
|
-
this.endpointApi.saveCoformAnswer(requestData)
|
|
869
|
-
) as { result?: boolean; msg?: string; data?: { id?: string } };
|
|
870
|
-
|
|
871
|
-
// Forme observée du backend (cf. save-answer-shape.test.ts) :
|
|
872
|
-
// { result: true, msg, data: { id: "<24hex>", _id: {_str}, collection, ... } }
|
|
873
|
-
const newId = response?.data?.id;
|
|
874
|
-
if (!this.id && typeof newId === "string" && newId.length === 24) {
|
|
875
|
-
this._draftData.id = newId;
|
|
876
|
-
}
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
/**
|
|
880
|
-
* Met à jour une Answer existante via `SAVE_COFORM_ANSWER` (avec `answerId`).
|
|
881
|
-
*
|
|
882
|
-
* Si `payload.answers` est `undefined` (le caller a modifié uniquement
|
|
883
|
-
* `addedOptions` ou `links`), on retombe sur `serverData.answers` pour ne
|
|
884
|
-
* pas envoyer un payload vide qui écraserait les réponses existantes.
|
|
885
|
-
*
|
|
886
|
-
* @protected — utiliser `answer.save()`.
|
|
887
|
-
*/
|
|
888
|
-
override _update = async (payload: Record<string, any>): Promise<boolean> => {
|
|
889
|
-
if (!this._calledFromSave) {
|
|
890
|
-
throw new Error("utilisation invalide de _update, utilisez save");
|
|
891
|
-
}
|
|
892
|
-
if (!this.id) {
|
|
893
|
-
throw new ApiError("Answer sans id, impossible de mettre à jour.", 400);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const formId = this._resolveFormId(payload);
|
|
897
|
-
const requestData = this._buildSavePayload(formId, payload, /* withAnswerId */ true);
|
|
898
|
-
|
|
899
|
-
await this.callIsConnected(() => this.endpointApi.saveCoformAnswer(requestData));
|
|
900
|
-
return true;
|
|
901
|
-
};
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Résout le `formId` à envoyer à `saveCoformAnswer`.
|
|
905
|
-
*
|
|
906
|
-
* Priorité :
|
|
907
|
-
* 1. `payload.form` (posé via `draftData.form` par `form.answer()`)
|
|
908
|
-
* 2. `serverData.form` (Answer chargée via `get()`)
|
|
909
|
-
* 3. `this.parent.id` si le parent est un `Form`
|
|
910
|
-
*
|
|
911
|
-
* @throws {ApiError} 400 si aucune source ne fournit un formId valide.
|
|
912
|
-
* @private
|
|
913
|
-
*/
|
|
914
|
-
private _resolveFormId(payload: Record<string, any>): string {
|
|
915
|
-
const fromDraftOrPayload = typeof payload.form === "string" ? payload.form : undefined;
|
|
916
|
-
const fromServer = typeof this._serverData.form === "string" ? this._serverData.form : undefined;
|
|
917
|
-
const parent = this.parent;
|
|
918
|
-
const fromParent =
|
|
919
|
-
parent && typeof (parent as any).getEntityType === "function"
|
|
920
|
-
&& (parent as any).getEntityType() === "forms"
|
|
921
|
-
&& typeof (parent as any).id === "string"
|
|
922
|
-
? (parent as any).id as string
|
|
923
|
-
: undefined;
|
|
924
|
-
|
|
925
|
-
const formId = fromDraftOrPayload ?? fromServer ?? fromParent;
|
|
926
|
-
if (typeof formId !== "string" || formId.length === 0) {
|
|
927
|
-
throw new ApiError(
|
|
928
|
-
"formId introuvable pour sauvegarder l'Answer. "
|
|
929
|
-
+ "Créez l'Answer via `form.answer()` ou définissez `answer.data.form` manuellement.",
|
|
930
|
-
400
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
return formId;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* Construit le payload `SaveCoformAnswerData` en sérialisant les champs JSON
|
|
938
|
-
* requis par le wire-format `application/x-www-form-urlencoded`.
|
|
939
|
-
*
|
|
940
|
-
* Si `payload.answers` est absent en update, retombe sur `serverData.answers`
|
|
941
|
-
* (cf. design point 6.5 — indulgent envers le caller).
|
|
942
|
-
*
|
|
943
|
-
* @private
|
|
944
|
-
*/
|
|
945
|
-
private _buildSavePayload(
|
|
946
|
-
formId: string,
|
|
947
|
-
payload: Record<string, any>,
|
|
948
|
-
withAnswerId: boolean
|
|
949
|
-
): SaveCoformAnswerData {
|
|
950
|
-
const answersValue = payload.answers ?? (withAnswerId ? this._serverData.answers : undefined);
|
|
951
|
-
if (answersValue === undefined || answersValue === null) {
|
|
952
|
-
throw new ApiError(
|
|
953
|
-
"answer.data.answers est requis (objet `{ subFormId: { inputId: value } }`).",
|
|
954
|
-
400
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const data: SaveCoformAnswerData = {
|
|
959
|
-
formId,
|
|
960
|
-
answers: JSON.stringify(answersValue),
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
if (withAnswerId) {
|
|
964
|
-
data.answerId = this.id!;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
if (payload.addedOptions && typeof payload.addedOptions === "object"
|
|
968
|
-
&& Object.keys(payload.addedOptions).length > 0) {
|
|
969
|
-
data.addedOptions = JSON.stringify(payload.addedOptions);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
if (payload.links && typeof payload.links === "object"
|
|
973
|
-
&& Object.keys(payload.links).length > 0) {
|
|
974
|
-
data.links = JSON.stringify(payload.links);
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
return data;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// ===========================================================================
|
|
981
|
-
// Helpers internes pour processUploads — équivalents portés depuis
|
|
982
|
-
// site-json/coform/actions/mutations/uploadHelpers.ts (Sprint 3).
|
|
983
|
-
// ===========================================================================
|
|
984
|
-
|
|
985
|
-
/**
|
|
986
|
-
* Récupère `formData` (schéma du Form) depuis le parent si c'est un Form.
|
|
987
|
-
* Renvoie `undefined` si pas exploitable — `processUploads` continuera mais
|
|
988
|
-
* sans détection automatique d'`inputType` (pas de normalisation uploader,
|
|
989
|
-
* pas de clean URLs).
|
|
990
|
-
* @private
|
|
991
|
-
*/
|
|
992
|
-
private _resolveFormData(): FormItemNormalized | undefined {
|
|
993
|
-
const parent = this.parent;
|
|
994
|
-
if (parent && typeof (parent as any).getEntityType === "function"
|
|
995
|
-
&& (parent as any).getEntityType() === "forms") {
|
|
996
|
-
return (parent as any).serverData as FormItemNormalized;
|
|
997
|
-
}
|
|
998
|
-
return undefined;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
private _isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
1002
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
private _isDataUri(value: string): boolean {
|
|
1006
|
-
return /^data:[^;]+;base64,/.test(value);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
private _isPendingUploadValue(value: unknown): value is PendingUploadValue {
|
|
1010
|
-
if (!this._isObjectRecord(value)) return false;
|
|
1011
|
-
return typeof value.data === "string" && this._isDataUri(value.data);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
private _parseMimeType(dataUri: string): string {
|
|
1015
|
-
const match = dataUri.match(/^data:([^;]+);base64,/i);
|
|
1016
|
-
return match?.[1]?.toLowerCase() ?? "application/octet-stream";
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
private _inferExtensionFromMimeType(mimeType: string): string {
|
|
1020
|
-
const map: Record<string, string> = {
|
|
1021
|
-
"image/jpeg": "jpg",
|
|
1022
|
-
"image/jpg": "jpg",
|
|
1023
|
-
"image/png": "png",
|
|
1024
|
-
"image/gif": "gif",
|
|
1025
|
-
"image/webp": "webp",
|
|
1026
|
-
"image/svg+xml": "svg",
|
|
1027
|
-
"application/pdf": "pdf",
|
|
1028
|
-
"text/plain": "txt",
|
|
1029
|
-
"text/csv": "csv",
|
|
1030
|
-
"application/msword": "doc",
|
|
1031
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
1032
|
-
"application/vnd.ms-excel": "xls",
|
|
1033
|
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
|
1034
|
-
"application/vnd.ms-powerpoint": "ppt",
|
|
1035
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
|
1036
|
-
"application/zip": "zip",
|
|
1037
|
-
"application/x-rar-compressed": "rar",
|
|
1038
|
-
};
|
|
1039
|
-
return map[mimeType] ?? "bin";
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
private _sanitizeBaseName(fileName: string): string {
|
|
1043
|
-
const raw = fileName.replace(/\.[^.]+$/, "");
|
|
1044
|
-
const cleaned = raw.replace(/[^a-zA-Z0-9_. -]+/g, "-").trim();
|
|
1045
|
-
return cleaned || "upload";
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
/**
|
|
1049
|
-
* Convertit un `PendingUploadValue` (data:URI) en `{file, docType, filename}`
|
|
1050
|
-
* exploitable par `uploadFile()`. Compatible browser + Node :
|
|
1051
|
-
* - Browser → `File` natif
|
|
1052
|
-
* - Node → `Buffer` (sera converti en stream par `_prepareUploadFile()`)
|
|
1053
|
-
* @private
|
|
1054
|
-
*/
|
|
1055
|
-
private async _dataUriToFile(
|
|
1056
|
-
value: PendingUploadValue,
|
|
1057
|
-
fallbackName: string,
|
|
1058
|
-
): Promise<{ file: UploadInput; docType: "image" | "file"; filename: string }> {
|
|
1059
|
-
const mime = this._parseMimeType(value.data);
|
|
1060
|
-
const docType: "image" | "file" = mime.startsWith("image/") ? "image" : "file";
|
|
1061
|
-
|
|
1062
|
-
const extension = this._inferExtensionFromMimeType(mime);
|
|
1063
|
-
const sourceName = value.name && value.name.trim() !== "" ? value.name : fallbackName;
|
|
1064
|
-
const hasExtension = /\.[a-zA-Z0-9]+$/.test(sourceName);
|
|
1065
|
-
const filename = hasExtension ? sourceName : `${this._sanitizeBaseName(sourceName)}.${extension}`;
|
|
1066
|
-
|
|
1067
|
-
const isBrowser = typeof window !== "undefined" && typeof File !== "undefined";
|
|
1068
|
-
|
|
1069
|
-
if (isBrowser) {
|
|
1070
|
-
const blob = await (await fetch(value.data)).blob();
|
|
1071
|
-
return { file: new File([blob], filename, { type: mime }), docType, filename };
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// Node : Buffer (sera converti en stream par uploadFile → _prepareUploadFile)
|
|
1075
|
-
const base64Match = value.data.match(/^data:[^;]+;base64,(.*)$/);
|
|
1076
|
-
if (!base64Match) {
|
|
1077
|
-
throw new ApiError("data:URI invalide (base64 attendu).", 400);
|
|
1078
|
-
}
|
|
1079
|
-
const buffer = Buffer.from(base64Match[1], "base64");
|
|
1080
|
-
return { file: buffer, docType, filename };
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/**
|
|
1084
|
-
* Parcourt récursivement `allData` pour trouver tous les `data:URI` à uploader.
|
|
1085
|
-
* Renvoie chaque pending avec son `path` (chemin imbriqué) et `inputType`
|
|
1086
|
-
* (déduit du schéma `formData` si fourni).
|
|
1087
|
-
* @private
|
|
1088
|
-
*/
|
|
1089
|
-
private _collectPendingUploads(
|
|
1090
|
-
node: unknown,
|
|
1091
|
-
path: (string | number)[] = [],
|
|
1092
|
-
formData?: FormItemNormalized,
|
|
1093
|
-
): PendingUpload[] {
|
|
1094
|
-
const uploads: PendingUpload[] = [];
|
|
1095
|
-
|
|
1096
|
-
const getInputType = (): string | undefined => {
|
|
1097
|
-
if (!formData || path.length < 2) return undefined;
|
|
1098
|
-
const [subFormId, inputId] = path;
|
|
1099
|
-
if (typeof subFormId !== "string" || typeof inputId !== "string") return undefined;
|
|
1100
|
-
const subForm = formData.inputs?.[subFormId];
|
|
1101
|
-
if (!subForm) return undefined;
|
|
1102
|
-
const input = subForm.inputs?.[inputId];
|
|
1103
|
-
return input && typeof input === "object" && "type" in input
|
|
1104
|
-
? (input.type as string | undefined)
|
|
1105
|
-
: undefined;
|
|
1106
|
-
};
|
|
1107
|
-
|
|
1108
|
-
if (typeof node === "string") {
|
|
1109
|
-
if (this._isDataUri(node)) {
|
|
1110
|
-
uploads.push({ value: { data: node }, path, inputType: getInputType() });
|
|
1111
|
-
}
|
|
1112
|
-
return uploads;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
if (this._isPendingUploadValue(node)) {
|
|
1116
|
-
uploads.push({ value: node, path, inputType: getInputType() });
|
|
1117
|
-
return uploads;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
if (Array.isArray(node)) {
|
|
1121
|
-
node.forEach((item, index) => {
|
|
1122
|
-
uploads.push(...this._collectPendingUploads(item, [...path, index], formData));
|
|
1123
|
-
});
|
|
1124
|
-
return uploads;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
if (this._isObjectRecord(node)) {
|
|
1128
|
-
Object.entries(node).forEach(([key, value]) => {
|
|
1129
|
-
uploads.push(...this._collectPendingUploads(value, [...path, key], formData));
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
return uploads;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Détermine `contentKey` et `subKey` pour un upload selon le type d'input :
|
|
1138
|
-
* - `*.uploader` → `presentation` + subKey `"subFormId.inputId"`
|
|
1139
|
-
* - autres → `slider` sans subKey
|
|
1140
|
-
* @private
|
|
1141
|
-
*/
|
|
1142
|
-
private _getUploadKeys(
|
|
1143
|
-
inputType: string | undefined,
|
|
1144
|
-
path: (string | number)[],
|
|
1145
|
-
): { contentKey: string; subKey?: string } {
|
|
1146
|
-
if (inputType && (inputType === "uploader" || inputType.endsWith(".uploader"))) {
|
|
1147
|
-
const subKey = path
|
|
1148
|
-
.slice(0, 2)
|
|
1149
|
-
.filter(p => typeof p === "string")
|
|
1150
|
-
.join(".");
|
|
1151
|
-
return { contentKey: "presentation", subKey: subKey || undefined };
|
|
1152
|
-
}
|
|
1153
|
-
return { contentKey: "slider" };
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
private _getValueAtPath(obj: unknown, path: (string | number)[]): unknown {
|
|
1157
|
-
if (path.length === 0) return obj;
|
|
1158
|
-
const [first, ...rest] = path;
|
|
1159
|
-
if (Array.isArray(obj)) return this._getValueAtPath(obj[first as number], rest);
|
|
1160
|
-
if (this._isObjectRecord(obj)) return this._getValueAtPath(obj[first as string], rest);
|
|
1161
|
-
return undefined;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
private _setValueAtPath(obj: unknown, path: (string | number)[], value: unknown): unknown {
|
|
1165
|
-
if (path.length === 0) return value;
|
|
1166
|
-
const [first, ...rest] = path;
|
|
1167
|
-
if (Array.isArray(obj)) {
|
|
1168
|
-
const next = [...obj];
|
|
1169
|
-
next[first as number] = this._setValueAtPath(next[first as number], rest, value);
|
|
1170
|
-
return next;
|
|
1171
|
-
}
|
|
1172
|
-
if (this._isObjectRecord(obj)) {
|
|
1173
|
-
return { ...obj, [first]: this._setValueAtPath(obj[first as string], rest, value) };
|
|
1174
|
-
}
|
|
1175
|
-
return obj;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
/**
|
|
1179
|
-
* Walk les `answers[subFormId][inputId]` pour normaliser les valeurs des
|
|
1180
|
-
* inputs `*.uploader` vers le format canonique `{updateDate, files: {docId: docPath}}`.
|
|
1181
|
-
*
|
|
1182
|
-
* Utilisé par `_transformServerData()` pour harmoniser les Answer en format
|
|
1183
|
-
* legacy Array `[{docId, docPath}]` avec le format moderne (ce que
|
|
1184
|
-
* `processUploads()` écrit).
|
|
1185
|
-
*
|
|
1186
|
-
* Skip si le schéma `formData` n'a pas l'inputType pour un champ donné.
|
|
1187
|
-
* @private
|
|
1188
|
-
*/
|
|
1189
|
-
private _normalizeLegacyUploaderInAnswers(
|
|
1190
|
-
answers: Record<string, Record<string, unknown>>,
|
|
1191
|
-
formData: FormItemNormalized,
|
|
1192
|
-
): Record<string, Record<string, unknown>> {
|
|
1193
|
-
const normalized: Record<string, Record<string, unknown>> = {};
|
|
1194
|
-
|
|
1195
|
-
for (const [subFormId, subFormData] of Object.entries(answers)) {
|
|
1196
|
-
if (!this._isObjectRecord(subFormData)) {
|
|
1197
|
-
normalized[subFormId] = subFormData as any;
|
|
1198
|
-
continue;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
const subForm = formData.inputs?.[subFormId];
|
|
1202
|
-
if (!subForm) {
|
|
1203
|
-
normalized[subFormId] = subFormData;
|
|
1204
|
-
continue;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
const cleanedSubForm: Record<string, unknown> = {};
|
|
1208
|
-
for (const [inputId, inputValue] of Object.entries(subFormData)) {
|
|
1209
|
-
const input = subForm.inputs?.[inputId];
|
|
1210
|
-
const inputType = input && typeof input === "object" && "type" in input
|
|
1211
|
-
? (input.type as string | undefined)
|
|
1212
|
-
: undefined;
|
|
1213
|
-
|
|
1214
|
-
cleanedSubForm[inputId] = inputType?.endsWith(".uploader")
|
|
1215
|
-
? this._normalizeUploaderValue(inputValue)
|
|
1216
|
-
: inputValue;
|
|
1217
|
-
}
|
|
1218
|
-
normalized[subFormId] = cleanedSubForm;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
return normalized;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
/**
|
|
1225
|
-
* Normalise une valeur d'input uploader vers le format legacy backend :
|
|
1226
|
-
* `{ updateDate: ["DD/MM/YYYY"], files: { "<docId>": "<docPath>", ... } }`
|
|
1227
|
-
* @private
|
|
1228
|
-
*/
|
|
1229
|
-
private _normalizeUploaderValue(value: unknown): unknown {
|
|
1230
|
-
const today = new Date().toLocaleDateString("fr-FR");
|
|
1231
|
-
|
|
1232
|
-
const toFilesObject = (arr: unknown[]): Record<string, string> => {
|
|
1233
|
-
const obj: Record<string, string> = {};
|
|
1234
|
-
for (const item of arr) {
|
|
1235
|
-
if (this._isObjectRecord(item)
|
|
1236
|
-
&& typeof item.docId === "string"
|
|
1237
|
-
&& typeof item.docPath === "string") {
|
|
1238
|
-
obj[item.docId as string] = item.docPath as string;
|
|
1239
|
-
} else if (typeof item === "string") {
|
|
1240
|
-
obj[item] = item;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
return obj;
|
|
1244
|
-
};
|
|
1245
|
-
|
|
1246
|
-
if (this._isObjectRecord(value) && "updateDate" in value && Array.isArray(value.updateDate)) {
|
|
1247
|
-
if (this._isObjectRecord(value.files)) return value;
|
|
1248
|
-
if (Array.isArray(value.files)) return { ...value, files: toFilesObject(value.files) };
|
|
1249
|
-
return value;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
if (Array.isArray(value)) {
|
|
1253
|
-
return { updateDate: [today], files: toFilesObject(value) };
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
return value;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
private _cleanUrlToRelativePath(value: unknown): unknown {
|
|
1260
|
-
if (typeof value === "string") {
|
|
1261
|
-
if (this._isDataUri(value)) return value;
|
|
1262
|
-
const urlMatch = value.match(/^https?:\/\/[^/]+(\/.*)/i);
|
|
1263
|
-
if (urlMatch) return urlMatch[1];
|
|
1264
|
-
return value;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
if (Array.isArray(value)) return value.map(v => this._cleanUrlToRelativePath(v));
|
|
1268
|
-
|
|
1269
|
-
if (this._isObjectRecord(value)) {
|
|
1270
|
-
if ("data" in value && typeof value.data === "string") {
|
|
1271
|
-
const cleanedData = !this._isDataUri(value.data)
|
|
1272
|
-
? this._cleanUrlToRelativePath(value.data)
|
|
1273
|
-
: value.data;
|
|
1274
|
-
return { ...value, data: cleanedData };
|
|
1275
|
-
}
|
|
1276
|
-
const cleaned: Record<string, unknown> = {};
|
|
1277
|
-
for (const [k, v] of Object.entries(value)) {
|
|
1278
|
-
cleaned[k] = this._cleanUrlToRelativePath(v);
|
|
1279
|
-
}
|
|
1280
|
-
return cleaned;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
return value;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
/**
|
|
1287
|
-
* Suffixes de types d'inputs dont les valeurs contiennent des URLs de fichiers
|
|
1288
|
-
* à nettoyer avant envoi au serveur.
|
|
1289
|
-
* @private
|
|
1290
|
-
*/
|
|
1291
|
-
private static readonly INPUT_TYPES_TO_CLEAN_URLS = [".uploader", ".simpleTable"] as const;
|
|
1292
|
-
|
|
1293
|
-
/**
|
|
1294
|
-
* Nettoie les URLs absolues → chemins relatifs **uniquement** sur les champs
|
|
1295
|
-
* uploader/simpleTable. Les autres champs (finder, text…) peuvent contenir
|
|
1296
|
-
* des URLs légitimes qu'il ne faut pas tronquer.
|
|
1297
|
-
* @private
|
|
1298
|
-
*/
|
|
1299
|
-
private _cleanUploaderUrls(
|
|
1300
|
-
data: AllStepsData,
|
|
1301
|
-
formData: FormItemNormalized | undefined,
|
|
1302
|
-
): AllStepsData {
|
|
1303
|
-
if (!formData) return data;
|
|
1304
|
-
const result: Record<string, unknown> = {};
|
|
1305
|
-
|
|
1306
|
-
for (const [subFormId, subFormData] of Object.entries(data)) {
|
|
1307
|
-
if (!this._isObjectRecord(subFormData)) {
|
|
1308
|
-
result[subFormId] = subFormData;
|
|
1309
|
-
continue;
|
|
1310
|
-
}
|
|
1311
|
-
const subForm = formData.inputs?.[subFormId];
|
|
1312
|
-
if (!subForm) {
|
|
1313
|
-
result[subFormId] = subFormData;
|
|
1314
|
-
continue;
|
|
1315
|
-
}
|
|
1316
|
-
const cleaned: Record<string, unknown> = {};
|
|
1317
|
-
for (const [inputId, inputValue] of Object.entries(subFormData as Record<string, unknown>)) {
|
|
1318
|
-
const input = subForm.inputs?.[inputId];
|
|
1319
|
-
const inputType = input && typeof input === "object" && "type" in input
|
|
1320
|
-
? (input.type as string | undefined)
|
|
1321
|
-
: undefined;
|
|
1322
|
-
const shouldClean = !!inputType
|
|
1323
|
-
&& Answer.INPUT_TYPES_TO_CLEAN_URLS.some(suffix => inputType.endsWith(suffix));
|
|
1324
|
-
cleaned[inputId] = shouldClean
|
|
1325
|
-
? this._cleanUrlToRelativePath(inputValue)
|
|
1326
|
-
: inputValue;
|
|
1327
|
-
}
|
|
1328
|
-
result[subFormId] = cleaned;
|
|
1329
|
-
}
|
|
1330
|
-
return result as AllStepsData;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
/**
|
|
1334
|
-
* Upload par batches parallèles (défaut : 4 fichiers simultanés).
|
|
1335
|
-
* @private
|
|
1336
|
-
*/
|
|
1337
|
-
private async _uploadInBatches<T, R>(
|
|
1338
|
-
items: T[],
|
|
1339
|
-
uploadFn: (item: T, index: number) => Promise<R>,
|
|
1340
|
-
batchSize: number = 4,
|
|
1341
|
-
): Promise<R[]> {
|
|
1342
|
-
const results: R[] = [];
|
|
1343
|
-
for (let i = 0; i < items.length; i += batchSize) {
|
|
1344
|
-
const batch = items.slice(i, i + batchSize);
|
|
1345
|
-
const batchResults = await Promise.all(
|
|
1346
|
-
batch.map((item, batchIndex) => uploadFn(item, i + batchIndex)),
|
|
1347
|
-
);
|
|
1348
|
-
results.push(...batchResults);
|
|
1349
|
-
}
|
|
1350
|
-
return results;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
/**
|
|
1354
|
-
* Upload un PendingUpload, retourne la valeur de remplacement à insérer
|
|
1355
|
-
* dans `allData` (`{docId, docPath}` pour uploader, `docPath` string sinon).
|
|
1356
|
-
* @private
|
|
1357
|
-
*/
|
|
1358
|
-
private async _uploadPendingItem(
|
|
1359
|
-
item: PendingUpload,
|
|
1360
|
-
index: number,
|
|
1361
|
-
): Promise<{ docId: string; docPath: string } | string> {
|
|
1362
|
-
const fallbackName = `upload-${index}`;
|
|
1363
|
-
const { file, docType, filename } = await this._dataUriToFile(item.value, fallbackName);
|
|
1364
|
-
const { contentKey, subKey } = this._getUploadKeys(item.inputType, item.path);
|
|
1365
|
-
|
|
1366
|
-
const result = await this.uploadFile(file, {
|
|
1367
|
-
docType,
|
|
1368
|
-
contentKey,
|
|
1369
|
-
subKey,
|
|
1370
|
-
qqfilename: filename,
|
|
1371
|
-
});
|
|
1372
|
-
|
|
1373
|
-
const isUploader = item.inputType?.endsWith(".uploader") ?? false;
|
|
1374
|
-
return isUploader
|
|
1375
|
-
? { docId: result.docId, docPath: result.docPath }
|
|
1376
|
-
: result.docPath;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
/**
|
|
1380
|
-
* Upload un PendingUpload + remplace immédiatement dans `prepared` à son path.
|
|
1381
|
-
* Utilisé pour le premier upload (séquentiel) qui crée l'Answer.
|
|
1382
|
-
* @private
|
|
1383
|
-
*/
|
|
1384
|
-
private async _uploadAndReplace(
|
|
1385
|
-
item: PendingUpload,
|
|
1386
|
-
prepared: AllStepsData,
|
|
1387
|
-
index: number,
|
|
1388
|
-
): Promise<AllStepsData> {
|
|
1389
|
-
const replacement = await this._uploadPendingItem(item, index);
|
|
1390
|
-
return this._setValueAtPath(prepared, item.path, replacement) as AllStepsData;
|
|
1391
|
-
}
|
|
1392
|
-
}
|