@communecter/cocolight-api-client 1.0.133 → 1.0.135
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/cocolight-api-client.browser.js +1 -1
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.mjs.js +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 +1 -1
- package/src/api/Answer.ts +391 -21
- package/src/api/BaseEntity.ts +54 -8
- package/src/api/serverDataType/Answer.ts +19 -0
- package/types/api/Answer.d.ts +195 -5
- package/types/api/BaseEntity.d.ts +45 -6
- package/types/api/serverDataType/Answer.d.ts +14 -0
package/types/api/Answer.d.ts
CHANGED
|
@@ -36,6 +36,25 @@ export interface ProcessUploadsOptions {
|
|
|
36
36
|
/** Nombre d'uploads parallèles par batch. Défaut : 4. */
|
|
37
37
|
batchSize?: number;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Options de `Answer.get()` — params optionnels du endpoint `COFORM_ANSWERS_BY_ID`.
|
|
41
|
+
*/
|
|
42
|
+
export interface AnswerGetOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Liste de champs à retourner côté backend (filtrage). Si absent, tous les
|
|
45
|
+
* champs sont renvoyés. Utile pour optimiser la taille de la réponse.
|
|
46
|
+
*/
|
|
47
|
+
fields?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* ID du formulaire parent. Utilisé côté backend pour calculer `canEdit` et
|
|
50
|
+
* `editDeniedReason` (droits d'édition contextuels). Si absent, **auto-injecté
|
|
51
|
+
* via `this.parent.id`** quand le parent est un `Form` (cas typique
|
|
52
|
+
* `form.answer({id})`).
|
|
53
|
+
*/
|
|
54
|
+
formId?: string;
|
|
55
|
+
/** Chemin Finder (rarement utilisé). */
|
|
56
|
+
finderPath?: string;
|
|
57
|
+
}
|
|
39
58
|
/**
|
|
40
59
|
* Options pour `Answer.uploadFile()`. Tous les champs sont optionnels — les
|
|
41
60
|
* défauts reproduisent les valeurs backend (`docType: "image"`, `contentKey: "slider"`).
|
|
@@ -72,6 +91,38 @@ export interface UploadAnswerFileResult {
|
|
|
72
91
|
/** ID de l'Answer (créée par cet upload si premier, sinon `= this.id`). */
|
|
73
92
|
answerId: string;
|
|
74
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Options de `Answer.getFiles()`.
|
|
96
|
+
*/
|
|
97
|
+
export interface GetFilesOptions {
|
|
98
|
+
/** Clé d'identification de l'input (ex: `"subFormId.inputId"`). Requise. */
|
|
99
|
+
subKey: string;
|
|
100
|
+
/** Type de document attendu. Défaut backend : `"file"`. */
|
|
101
|
+
docType?: "image" | "file";
|
|
102
|
+
/** Clé de contenu utilisée lors de l'upload. Défaut backend : `"presentation"`. */
|
|
103
|
+
contentKey?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Élément renvoyé par `Answer.getFiles()` — un document attaché à l'Answer.
|
|
107
|
+
* Les champs `imagePath`/`imageThumbPath`/`imageMediumPath` ne sont présents
|
|
108
|
+
* que pour `docType: "image"`.
|
|
109
|
+
*/
|
|
110
|
+
export interface AnswerFileItem {
|
|
111
|
+
/** ID MongoDB du document (utilisable pour suppression via `deleteDocumentById`). */
|
|
112
|
+
docId: string;
|
|
113
|
+
/** Chemin relatif du document côté backend. */
|
|
114
|
+
docPath: string;
|
|
115
|
+
/** Nom original du fichier. */
|
|
116
|
+
name?: string;
|
|
117
|
+
/** Taille en octets. */
|
|
118
|
+
size?: number;
|
|
119
|
+
/** Chemin de l'image (uniquement si `docType: "image"`). */
|
|
120
|
+
imagePath?: string | null;
|
|
121
|
+
/** Chemin du thumbnail (uniquement si `docType: "image"`). */
|
|
122
|
+
imageThumbPath?: string | null;
|
|
123
|
+
/** Chemin de la version medium (uniquement si `docType: "image"`). */
|
|
124
|
+
imageMediumPath?: string | null;
|
|
125
|
+
}
|
|
75
126
|
export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
76
127
|
static entityType: string;
|
|
77
128
|
static entityTag: string;
|
|
@@ -85,6 +136,34 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
85
136
|
* Les autres sont calculés/posés par le backend.
|
|
86
137
|
*/
|
|
87
138
|
removeFields: string[];
|
|
139
|
+
/**
|
|
140
|
+
* {@inheritDoc BaseEntity#isAuthor}
|
|
141
|
+
*
|
|
142
|
+
* Override Answer : le champ d'autorité est `serverData.user` (pas `creator`
|
|
143
|
+
* comme la version par défaut de BaseEntity). Le `user` peut être :
|
|
144
|
+
* - un `string` (ID MongoDB brut renvoyé par le backend)
|
|
145
|
+
* - une instance `User` (après `_transformServerData` qui le link en entité)
|
|
146
|
+
*
|
|
147
|
+
* Les pré-conditions (connexion, id, serverData) sont vérifiées comme dans
|
|
148
|
+
* la version de base — `silent: true` (défaut) renvoie `false` au lieu de throw.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* {@inheritDoc BaseEntity#_isAdminViaHierarchy}
|
|
152
|
+
*
|
|
153
|
+
* Override Answer : la hiérarchie d'Answer passe par son **parent instance
|
|
154
|
+
* Form** (pas par `serverData.parent` direct comme pour `events`/`projects`/`poi`).
|
|
155
|
+
*
|
|
156
|
+
* Chaîne : `Answer → parent (Form) → form.serverData.parent ({[orgId]: {type, name}}) → check userContext.links`.
|
|
157
|
+
*
|
|
158
|
+
* Si le parent n'est pas un Form (cas `api.answer({id})` direct sans contexte),
|
|
159
|
+
* renvoie `false` — impossible de vérifier la hiérarchie sans le Form.
|
|
160
|
+
*
|
|
161
|
+
* @protected
|
|
162
|
+
*/
|
|
163
|
+
protected _isAdminViaHierarchy(): boolean;
|
|
164
|
+
isAuthor(options?: {
|
|
165
|
+
silent?: boolean;
|
|
166
|
+
}): boolean;
|
|
88
167
|
/**
|
|
89
168
|
* Transforme les champs imbriqués (user, context etc.) en instances d'entités.
|
|
90
169
|
* @param data - Les données brutes du serveur.
|
|
@@ -93,10 +172,36 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
93
172
|
*/
|
|
94
173
|
protected _transformServerData(data: AnswerItemNormalized): AnswerItemNormalized;
|
|
95
174
|
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
175
|
+
* Rafraîchit les données de l'entité depuis l'API (`COFORM_ANSWERS_BY_ID`).
|
|
176
|
+
*
|
|
177
|
+
* **Auto-injection du `formId`** : si le parent est un `Form` (cas typique
|
|
178
|
+
* `form.answer({id})`), le param `formId` est automatiquement envoyé pour
|
|
179
|
+
* que le backend calcule `canEdit` et `editDeniedReason` dans la réponse.
|
|
180
|
+
* Le caller peut override via `opts.formId` ou couper avec `opts.formId: ""`.
|
|
181
|
+
*
|
|
182
|
+
* @param opts - Params optionnels :
|
|
183
|
+
* - `fields` : filtrage des champs côté backend
|
|
184
|
+
* - `formId` : override de l'auto-injection (utile pour disable ou forcer un autre form)
|
|
185
|
+
* - `finderPath` : chemin Finder (rare)
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* // Cas commun : form.answer({id}) → get() auto avec formId
|
|
189
|
+
* const answer = await form.answer({ id: answerId });
|
|
190
|
+
* answer.serverData.canEdit; // true/false calculé backend
|
|
191
|
+
* answer.serverData.editDeniedReason; // raison si false
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* // Cas avancé : filtrage de champs pour optim perf
|
|
195
|
+
* const answer = await form.answer();
|
|
196
|
+
* answer._id(answerId);
|
|
197
|
+
* await answer.get({ fields: ["answers", "canEdit", "editDeniedReason"] });
|
|
198
|
+
*/
|
|
199
|
+
get(opts?: AnswerGetOptions): Promise<Record<string, any>>;
|
|
200
|
+
/**
|
|
201
|
+
* Récupère l'ID du parent Form si applicable, pour l'auto-injection dans `get()`.
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
private _autoFormIdFromParent;
|
|
100
205
|
form(): Promise<never>;
|
|
101
206
|
/**
|
|
102
207
|
* Supprime cette Answer côté serveur via `DELETE_ELEMENT`
|
|
@@ -106,18 +211,91 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
106
211
|
* - `serverData` et `draftData` sont vidés (sans casser la réactivité)
|
|
107
212
|
* - `_isDeleted = true` → toute mutation ultérieure (`save`, `get`, etc.) throw
|
|
108
213
|
*
|
|
214
|
+
* **Guard** : autorisation côté client si l'utilisateur courant est :
|
|
215
|
+
* - l'**auteur** de l'Answer (`serverData.user === userId`), OU
|
|
216
|
+
* - **admin** d'un parent du Form (org/project) via la hiérarchie
|
|
217
|
+
* (`form.serverData.parent[orgId]` × `userContext.links.memberOf[orgId].isAdmin`).
|
|
218
|
+
*
|
|
219
|
+
* Le check utilise `isAuthorOrAdmin({checkHierarchy: true})` qui s'appuie sur
|
|
220
|
+
* l'override `Answer._isAdminViaHierarchy` (remonte via parent instance Form).
|
|
221
|
+
*
|
|
222
|
+
* **Pré-requis** :
|
|
223
|
+
* - `this.parent` doit être un Form (cas `form.answer({id})`)
|
|
224
|
+
* - `userContext` (User connecté) doit avoir `serverData.links` chargés (cas `api.me()`)
|
|
225
|
+
* Sans ces pré-requis, seul `isAuthor` peut autoriser.
|
|
226
|
+
*
|
|
109
227
|
* @param reason - Raison libre transmise au backend (audit). Défaut : `"delete coform answer"`.
|
|
110
228
|
* @throws {ApiError} 400 si l'Answer n'a pas d'id (jamais sauvegardée).
|
|
229
|
+
* @throws {ApiError} 403 si ni auteur ni admin du parent.
|
|
111
230
|
*
|
|
112
231
|
* @example
|
|
113
232
|
* const answer = await form.answer({ id: "..." });
|
|
114
|
-
* await answer.delete();
|
|
233
|
+
* await answer.delete(); // OK si auteur OU admin org/project parent du Form
|
|
115
234
|
* answer._isDeleted; // true
|
|
116
235
|
*/
|
|
117
236
|
delete(reason?: string): Promise<void>;
|
|
237
|
+
/**
|
|
238
|
+
* {@inheritDoc BaseEntity#deleteFile}
|
|
239
|
+
*
|
|
240
|
+
* Override avec **cleanup local** : après suppression backend, retire le
|
|
241
|
+
* `docId` des structures uploader normalisées dans `serverData.answers` et
|
|
242
|
+
* `_draftData.answers` (format `{updateDate, files: {docId: docPath}}`
|
|
243
|
+
* produit par `processUploads()` et `_transformServerData`).
|
|
244
|
+
*
|
|
245
|
+
* Le caller n'a pas besoin de refetch — `answer.serverData.answers[*][*].files`
|
|
246
|
+
* reflète directement l'état post-suppression.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
|
|
250
|
+
* await answer.deleteFile(files[0].docId);
|
|
251
|
+
* // answer.serverData.answers[sf][input].files[files[0].docId] === undefined
|
|
252
|
+
*/
|
|
253
|
+
deleteFile(docId: string): Promise<void>;
|
|
254
|
+
/**
|
|
255
|
+
* Walk `answers[subFormId][inputId]` et retire `docId` des structures
|
|
256
|
+
* `{updateDate, files: {docId: docPath}}` (format canonique uploader).
|
|
257
|
+
* Mutation in-place pour préserver la réactivité.
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
private _removeDocIdFromUploaderFields;
|
|
261
|
+
/**
|
|
262
|
+
* Récupère la liste des fichiers attachés à un input précis de cette Answer
|
|
263
|
+
* via `COFORM_GET_ANSWER_FILES`.
|
|
264
|
+
*
|
|
265
|
+
* **Cas d'usage principal** : récupérer les fichiers d'inputs uploader stockés
|
|
266
|
+
* en **format legacy** `{updateDate: [...]}` (sans la clé `files`). Le backend
|
|
267
|
+
* recherche alors les documents MongoDB liés à `{answerId, subKey, docType}`.
|
|
268
|
+
*
|
|
269
|
+
* Pour les Answer en format moderne `{updateDate, files: {<docId>: <path>}}`,
|
|
270
|
+
* les fichiers sont déjà dans `answer.serverData.answers[subFormId][inputId].files`
|
|
271
|
+
* — pas besoin d'appel réseau.
|
|
272
|
+
*
|
|
273
|
+
* @param opts - `subKey` (requis, ex: `"subFormId.inputId"`), `docType`, `contentKey`
|
|
274
|
+
* @returns Liste normalisée des fichiers `{docId, docPath, name?, size?, imagePath?, imageThumbPath?, imageMediumPath?}`.
|
|
275
|
+
* Tableau vide si aucun fichier trouvé.
|
|
276
|
+
* @throws {ApiError} 400 si pas d'id Answer. Throws aussi si le backend renvoie `result: false`.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* const form = await org.form({ id: formId });
|
|
280
|
+
* const answer = await form.answer({ id: answerId });
|
|
281
|
+
* const files = await answer.getFiles({
|
|
282
|
+
* subKey: `${subFormId}.${inputId}`,
|
|
283
|
+
* docType: "image",
|
|
284
|
+
* });
|
|
285
|
+
* files[0].docId; // ID du document (pour suppression future)
|
|
286
|
+
* files[0].docPath; // Chemin relatif (à concaténer avec baseURL pour affichage)
|
|
287
|
+
*/
|
|
288
|
+
getFiles(opts: GetFilesOptions): Promise<AnswerFileItem[]>;
|
|
118
289
|
/**
|
|
119
290
|
* Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
|
|
120
291
|
*
|
|
292
|
+
* **Building block du pipeline** : conçu pour être utilisé via
|
|
293
|
+
* `processUploads()` suivi de `save()`. L'Answer créée par le premier upload
|
|
294
|
+
* est en état "placeholder" côté backend (champ `user` non posé) — `save()`
|
|
295
|
+
* finalise. Appeler `get()` ou `delete()` (via l'entité) sur une Answer
|
|
296
|
+
* upload-only sans save échouera : le backend la considère comme orpheline
|
|
297
|
+
* (`editDeniedReason: "not_owner"`, stub sans `data.id`).
|
|
298
|
+
*
|
|
121
299
|
* **Cas premier upload (Answer sans id)** : le backend crée l'Answer et
|
|
122
300
|
* renvoie `answerId`. La méthode pose alors `this._draftData.id = answerId`
|
|
123
301
|
* — les uploads suivants utiliseront cet id, et un `save()` ultérieur passera
|
|
@@ -276,6 +454,18 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
276
454
|
private _getUploadKeys;
|
|
277
455
|
private _getValueAtPath;
|
|
278
456
|
private _setValueAtPath;
|
|
457
|
+
/**
|
|
458
|
+
* Walk les `answers[subFormId][inputId]` pour normaliser les valeurs des
|
|
459
|
+
* inputs `*.uploader` vers le format canonique `{updateDate, files: {docId: docPath}}`.
|
|
460
|
+
*
|
|
461
|
+
* Utilisé par `_transformServerData()` pour harmoniser les Answer en format
|
|
462
|
+
* legacy Array `[{docId, docPath}]` avec le format moderne (ce que
|
|
463
|
+
* `processUploads()` écrit).
|
|
464
|
+
*
|
|
465
|
+
* Skip si le schéma `formData` n'a pas l'inputType pour un champ donné.
|
|
466
|
+
* @private
|
|
467
|
+
*/
|
|
468
|
+
private _normalizeLegacyUploaderInAnswers;
|
|
279
469
|
/**
|
|
280
470
|
* Normalise une valeur d'input uploader vers le format legacy backend :
|
|
281
471
|
* `{ updateDate: ["DD/MM/YYYY"], files: { "<docId>": "<docPath>", ... } }`
|
|
@@ -562,6 +562,32 @@ export declare class BaseEntity<TServerData = any> {
|
|
|
562
562
|
qqfilename: string;
|
|
563
563
|
qqtotalfilesize: number;
|
|
564
564
|
}>;
|
|
565
|
+
/**
|
|
566
|
+
* Supprime un document (fichier ou image) par son `docId` MongoDB via
|
|
567
|
+
* `DELETE_DOCUMENT_BY_ID`.
|
|
568
|
+
*
|
|
569
|
+
* Méthode générique exposée sur toutes les entités — le endpoint backend
|
|
570
|
+
* opère par docId seul, sans contexte parent requis. Disponible ici pour
|
|
571
|
+
* être découvrable et **override-able** par les entités qui veulent un
|
|
572
|
+
* cleanup local après suppression (ex: `News.deleteFile` pourrait retirer
|
|
573
|
+
* l'id de `mediaImg.images` côté serverData).
|
|
574
|
+
*
|
|
575
|
+
* Ne met PAS à jour `serverData` après suppression — le caller doit
|
|
576
|
+
* invalider son cache local s'il en a un (cas typique : React Query).
|
|
577
|
+
*
|
|
578
|
+
* @param docId - ID MongoDB du document (24 hex)
|
|
579
|
+
* @throws {ApiError} 400 si `docId` invalide.
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* // Depuis une Answer (suppression d'un fichier uploader)
|
|
583
|
+
* const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
|
|
584
|
+
* await answer.deleteFile(files[0].docId);
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* // Depuis n'importe quelle entité qui possède un docId
|
|
588
|
+
* await news.deleteFile(imageDocId);
|
|
589
|
+
*/
|
|
590
|
+
deleteFile(docId: string): Promise<void>;
|
|
565
591
|
/**
|
|
566
592
|
* Valide les entrées d'upload de fichiers.
|
|
567
593
|
*
|
|
@@ -938,15 +964,24 @@ export declare class BaseEntity<TServerData = any> {
|
|
|
938
964
|
*/
|
|
939
965
|
private _extractUserContext;
|
|
940
966
|
/**
|
|
941
|
-
* Vérifie si l'utilisateur est admin via la hiérarchie parent (logique généraliste)
|
|
942
|
-
*
|
|
967
|
+
* Vérifie si l'utilisateur est admin via la hiérarchie parent (logique généraliste).
|
|
968
|
+
*
|
|
969
|
+
* Note : passé `protected` pour permettre l'override par les entités dont la
|
|
970
|
+
* hiérarchie n'est pas un simple champ `serverData.parent` ou `.organizer`
|
|
971
|
+
* (cf. `Answer._isAdminViaHierarchy` qui remonte via son parent instance Form).
|
|
972
|
+
*
|
|
973
|
+
* @protected
|
|
943
974
|
*/
|
|
944
|
-
|
|
975
|
+
protected _isAdminViaHierarchy(): boolean;
|
|
945
976
|
/**
|
|
946
|
-
* Vérifie si l'utilisateur est admin d'un parent spécifique
|
|
947
|
-
*
|
|
977
|
+
* Vérifie si l'utilisateur est admin d'un parent spécifique.
|
|
978
|
+
*
|
|
979
|
+
* Note : passé `protected` pour être utilisable par les overrides de
|
|
980
|
+
* `_isAdminViaHierarchy()` (cf. `Answer._isAdminViaHierarchy`).
|
|
981
|
+
*
|
|
982
|
+
* @protected
|
|
948
983
|
*/
|
|
949
|
-
|
|
984
|
+
protected _isAdminOfParent(parentId: string, parentType: string): boolean;
|
|
950
985
|
/**
|
|
951
986
|
* Construit dynamiquement des filtres sur un lien entre entités
|
|
952
987
|
*
|
|
@@ -1503,11 +1538,15 @@ export declare class BaseEntity<TServerData = any> {
|
|
|
1503
1538
|
*
|
|
1504
1539
|
* @param options - Options de vérification.
|
|
1505
1540
|
* @param options.silent - Si `true`, retourne `false` au lieu de lever une exception. Par défaut `true`.
|
|
1541
|
+
* @param options.checkHierarchy - Si `true`, propagé à `isAdmin` pour vérifier
|
|
1542
|
+
* également la hiérarchie parent (ex: `Answer.isAdmin({checkHierarchy: true})`
|
|
1543
|
+
* remonte vers org/project du Form parent).
|
|
1506
1544
|
* @returns - `true` si l'utilisateur est l'auteur ou administrateur, `false` sinon.
|
|
1507
1545
|
* @throws {ApiError} - Si `silent` est `false` et que les préconditions ne sont pas remplies.
|
|
1508
1546
|
*/
|
|
1509
1547
|
isAuthorOrAdmin(options?: {
|
|
1510
1548
|
silent?: boolean;
|
|
1549
|
+
checkHierarchy?: boolean;
|
|
1511
1550
|
}): boolean;
|
|
1512
1551
|
/**
|
|
1513
1552
|
* Vérifie si l'utilisateur est membre de l'entité.
|
|
@@ -55,6 +55,12 @@ export interface AnswerItemJson {
|
|
|
55
55
|
project?: AnswerProjectRef;
|
|
56
56
|
[key: string]: unknown;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Raisons de refus d'édition côté backend, calculées par `coformAnswersById`
|
|
60
|
+
* et présentes dans la réponse normalisée. Utilisé par les UI pour distinguer
|
|
61
|
+
* les modes readonly/edit et afficher des messages contextuels.
|
|
62
|
+
*/
|
|
63
|
+
export type AnswerEditDeniedReason = "not_logged_in" | "not_owner" | "form_inactive" | "form_closed";
|
|
58
64
|
export interface AnswerItemNormalized {
|
|
59
65
|
id: string;
|
|
60
66
|
_id: ObjectID;
|
|
@@ -64,6 +70,7 @@ export interface AnswerItemNormalized {
|
|
|
64
70
|
user?: string | User;
|
|
65
71
|
links?: AnswerLinksBlock;
|
|
66
72
|
draft?: boolean;
|
|
73
|
+
finished?: boolean;
|
|
67
74
|
answers?: Record<string, unknown>;
|
|
68
75
|
context?: ParentsMap | Record<string, Organization | Project>;
|
|
69
76
|
form?: string;
|
|
@@ -74,6 +81,13 @@ export interface AnswerItemNormalized {
|
|
|
74
81
|
date: Date;
|
|
75
82
|
}>;
|
|
76
83
|
project?: AnswerProjectRef;
|
|
84
|
+
/**
|
|
85
|
+
* L'utilisateur courant peut-il éditer cette réponse ? Calculé côté backend.
|
|
86
|
+
* Présent par défaut, ou enrichi avec le contexte si `formId` est fourni.
|
|
87
|
+
*/
|
|
88
|
+
canEdit?: boolean;
|
|
89
|
+
/** Raison si `canEdit === false`. `null` si `canEdit === true`. */
|
|
90
|
+
editDeniedReason?: AnswerEditDeniedReason | null;
|
|
77
91
|
[key: string]: unknown;
|
|
78
92
|
}
|
|
79
93
|
export {};
|