udp-stencil-component-library 25.18.2-beta.6 → 25.18.2-beta.8
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/cjs/index.cjs.js +138 -0
- package/dist/cjs/index.cjs.js.map +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/stencil-library.cjs.js +1 -1
- package/dist/cjs/udp-forms-renderer.cjs.entry.js +591 -603
- package/dist/cjs/udp-forms-renderer.entry.cjs.js.map +1 -1
- package/dist/cjs/udp-forms-ui.cjs.entry.js +1 -4
- package/dist/cjs/udp-forms-ui.entry.cjs.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-ui/udp-forms-ui.js +1 -4
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-ui/udp-forms-ui.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.js +153 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.js.map +1 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.js +104 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.js.map +1 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/{udp-forms-renderer-utils.js → udp-forms-renderer-utils/utils.js} +48 -2
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/utils.js.map +1 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.js +187 -310
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.js +13 -13
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.js +142 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.js.map +1 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.js +3 -10
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/IFormSubmissionHandler.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/types.js.map +1 -1
- package/dist/collection/index.js +1 -0
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/udp-utilities/udp-websocket-client/udp-websocket-client.js +137 -0
- package/dist/collection/udp-utilities/udp-websocket-client/udp-websocket-client.js.map +1 -0
- package/dist/components/index.js +138 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/udp-forms-renderer.js +593 -608
- package/dist/components/udp-forms-renderer.js.map +1 -1
- package/dist/components/udp-forms-ui2.js +1 -4
- package/dist/components/udp-forms-ui2.js.map +1 -1
- package/dist/docs.json +1 -1
- package/dist/esm/index.js +138 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/stencil-library.js +1 -1
- package/dist/esm/udp-forms-renderer.entry.js +592 -604
- package/dist/esm/udp-forms-renderer.entry.js.map +1 -1
- package/dist/esm/udp-forms-ui.entry.js +1 -4
- package/dist/esm/udp-forms-ui.entry.js.map +1 -1
- package/dist/stencil-library/index.esm.js +1 -1
- package/dist/stencil-library/index.esm.js.map +1 -1
- package/dist/stencil-library/stencil-library.esm.js +1 -1
- package/dist/stencil-library/udp-forms-renderer.entry.esm.js.map +1 -1
- package/dist/stencil-library/udp-forms-renderer.entry.js +1 -1
- package/dist/stencil-library/udp-forms-renderer.entry.js.map +1 -1
- package/dist/stencil-library/udp-forms-ui.entry.esm.js.map +1 -1
- package/dist/stencil-library/udp-forms-ui.entry.js +1 -1
- package/dist/stencil-library/udp-forms-ui.entry.js.map +1 -1
- package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.d.ts +31 -0
- package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.d.ts +17 -0
- package/dist/types/components/forms/udp-forms/udp-forms-renderer/{udp-forms-renderer-utils.d.ts → udp-forms-renderer-utils/utils.d.ts} +4 -0
- package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.d.ts +6 -10
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.d.ts +5 -6
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.d.ts +42 -0
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.d.ts +1 -1
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/IFormSubmissionHandler.d.ts +44 -5
- package/dist/types/components/forms/udp-forms/udp-forms-utils/types.d.ts +16 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/udp-utilities/udp-websocket-client/udp-websocket-client.d.ts +34 -0
- package/package.json +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils.js.map +0 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.js +0 -264
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.js.map +0 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PublicFormSubmissionHandler.js +0 -63
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PublicFormSubmissionHandler.js.map +0 -1
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.d.ts +0 -131
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PublicFormSubmissionHandler.d.ts +0 -15
|
@@ -24,67 +24,6 @@ function isFileArray(array) {
|
|
|
24
24
|
return array.every(item => item instanceof File);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// TODO: This handler needs to be updated with new public form logic.
|
|
28
|
-
// ******* THIS HANDLER IS OUTDATED, AND WILL LIKELY NOT WORK *********
|
|
29
|
-
/**
|
|
30
|
-
* Handles submission for *public* (unauthenticated) forms.
|
|
31
|
-
*/
|
|
32
|
-
class PublicFormSubmissionHandler {
|
|
33
|
-
constructor(formId, formVersion, tenantId) { }
|
|
34
|
-
async fetchAndPopulateUdpFormSubmissionObj(submission) {
|
|
35
|
-
return submission;
|
|
36
|
-
}
|
|
37
|
-
objectToFormData(obj) {
|
|
38
|
-
var _a, _b;
|
|
39
|
-
const files = [];
|
|
40
|
-
const formObj = {};
|
|
41
|
-
for (const key in obj) {
|
|
42
|
-
let value = (_b = (_a = obj[key]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : obj[key];
|
|
43
|
-
if (isFileArray(value)) {
|
|
44
|
-
if (value.length > 0) {
|
|
45
|
-
files.push(value[0]);
|
|
46
|
-
formObj[key] = value[0].name;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
else if (Array.isArray(value)) {
|
|
50
|
-
formObj[key] = value.map(v => (typeof v === 'string' ? v : v.value)).join(',');
|
|
51
|
-
}
|
|
52
|
-
else if (value !== null && value !== undefined && value !== '') {
|
|
53
|
-
formObj[key] = value;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return makeApiCall.createFormData({
|
|
57
|
-
formData: JSON.stringify(formObj),
|
|
58
|
-
formFiles: files[0] || null
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
async saveCurrentFormSubmissionState(values, submission) {
|
|
62
|
-
return submission;
|
|
63
|
-
}
|
|
64
|
-
async saveFormSubmissionComments(values, submission) {
|
|
65
|
-
return submission;
|
|
66
|
-
}
|
|
67
|
-
async createNewLinkedFollowUpFormSubmission(submission) {
|
|
68
|
-
return submission;
|
|
69
|
-
}
|
|
70
|
-
async finalizeFormSubmissionState(values, submission) {
|
|
71
|
-
try {
|
|
72
|
-
// const formData = this.objectToFormData(values);
|
|
73
|
-
// await makeApiCall(
|
|
74
|
-
// 'POST',
|
|
75
|
-
// `${ConfigService.productV1ApiUrl}/UdpForm/${this.formId}/${this.formVersion}/submit/public?tenantId=${this.tenantId}`,
|
|
76
|
-
// formData,
|
|
77
|
-
// true
|
|
78
|
-
// );
|
|
79
|
-
return submission;
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
console.error('Public form submission failed:', error);
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
27
|
class UdpFormSubmission {
|
|
89
28
|
constructor(initialData = {}) {
|
|
90
29
|
this.id = null;
|
|
@@ -217,260 +156,138 @@ class UdpFormSubmission {
|
|
|
217
156
|
}
|
|
218
157
|
|
|
219
158
|
/**
|
|
220
|
-
*
|
|
159
|
+
* Single handler for UdpFormSubmission operations.
|
|
160
|
+
*
|
|
161
|
+
* Behavior changes by access mode:
|
|
162
|
+
* - private: supports draft save and follow-ups; comments are saved via standard draft-save until submitted,
|
|
163
|
+
* and via dedicated comment endpoints when submitted or viewed by a non-owner.
|
|
164
|
+
* - public : no draft save, no follow-ups; submit uses the `/UdpFormSubmission/public` endpoint;
|
|
165
|
+
* comments use dedicated comment endpoints once a submission exists.
|
|
221
166
|
*/
|
|
222
|
-
class
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
* Fetch and refresh an existing UdpFormSubmission instance.
|
|
228
|
-
*
|
|
229
|
-
* Attempts to load the latest server representation of the provided
|
|
230
|
-
* udpFormSubmission. Uses generic fields (generic1/2/3) when present to
|
|
231
|
-
* disambiguate records, otherwise falls back to a simple id lookup.
|
|
232
|
-
*
|
|
233
|
-
* @param udpFormSubmission - local UdpFormSubmission object to refresh
|
|
234
|
-
* @returns a new UdpFormSubmission merged with server data, or the original on failure
|
|
235
|
-
* @throws network or unexpected errors
|
|
236
|
-
*/
|
|
237
|
-
async fetchAndPopulateUdpFormSubmissionObj(udpFormSubmission) {
|
|
167
|
+
class FormSubmissionHandler {
|
|
168
|
+
constructor(accessMode) {
|
|
169
|
+
this.accessMode = accessMode;
|
|
170
|
+
}
|
|
171
|
+
async addComment(udpFormSubmission, comment) {
|
|
238
172
|
if (!(udpFormSubmission === null || udpFormSubmission === void 0 ? void 0 : udpFormSubmission.id)) {
|
|
239
|
-
|
|
240
|
-
return udpFormSubmission;
|
|
241
|
-
}
|
|
242
|
-
try {
|
|
243
|
-
const { id, generic1, generic2, generic3 } = udpFormSubmission;
|
|
244
|
-
let response = null;
|
|
245
|
-
// Prefer the generics-aware lookup if any generic values are present
|
|
246
|
-
if (generic1 || generic2 || generic3) {
|
|
247
|
-
response = await this.getFormSubmissionByIdAndGenerics(id, generic1 || undefined, generic2 || undefined, generic3 || undefined);
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
response = await this.getFormSubmissionById(id);
|
|
251
|
-
}
|
|
252
|
-
if (!response) {
|
|
253
|
-
console.warn('No form submission found for the provided object.');
|
|
254
|
-
return udpFormSubmission;
|
|
255
|
-
}
|
|
256
|
-
return new UdpFormSubmission(Object.assign(Object.assign({}, udpFormSubmission), response));
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
console.error('Error fetching form submission:', error);
|
|
260
|
-
throw error;
|
|
173
|
+
throw new Error('Cannot add a comment without a submission ID.');
|
|
261
174
|
}
|
|
175
|
+
// Backend contract:
|
|
176
|
+
// - public : POST /UdpFormSubmission/{submissionId}/comments/public
|
|
177
|
+
// - private: POST /UdpFormSubmission/{submissionId}/comments/private
|
|
178
|
+
const url = `${configService.ConfigService.productV1ApiUrl}/UdpFormSubmission/${udpFormSubmission.id}/comments/${this.accessMode}`;
|
|
179
|
+
await makeApiCall.makeApiCall('POST', url, comment, true);
|
|
262
180
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
* Performs a minimal search (page size 1) for the given id and returns
|
|
267
|
-
* the first match or null if not found. Logs and returns null on errors.
|
|
268
|
-
*
|
|
269
|
-
* @param id - submission id to query
|
|
270
|
-
* @returns UdpFormSubmission or null
|
|
271
|
-
*/
|
|
272
|
-
async getFormSubmissionById(id) {
|
|
273
|
-
var _a;
|
|
274
|
-
try {
|
|
275
|
-
const search = new SearchBuilder.SearchBuilder(1, 1)
|
|
276
|
-
.addFilter('id', id, SearchBuilder.SearchOperators.EQUALS);
|
|
277
|
-
const response = await search.execute('UdpFormSubmission');
|
|
278
|
-
return ((_a = response === null || response === void 0 ? void 0 : response.pageList) === null || _a === void 0 ? void 0 : _a[0]) || null;
|
|
181
|
+
async updateComment(udpFormSubmission, args) {
|
|
182
|
+
if (!(udpFormSubmission === null || udpFormSubmission === void 0 ? void 0 : udpFormSubmission.id)) {
|
|
183
|
+
throw new Error('Cannot update a comment without a submission ID.');
|
|
279
184
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
185
|
+
const { sectionKey, questionKey, commentId, value } = args;
|
|
186
|
+
// Backend contract: PUT /UdpFormSubmission/{submissionId}/comments/{commentId}
|
|
187
|
+
// Body: { value, sectionKey, questionKey }
|
|
188
|
+
const url = `${configService.ConfigService.productV1ApiUrl}/UdpFormSubmission/${udpFormSubmission.id}/comments/${commentId}`;
|
|
189
|
+
await makeApiCall.makeApiCall('PUT', url, { value, sectionKey, questionKey }, true);
|
|
190
|
+
}
|
|
191
|
+
async deleteComment(udpFormSubmission, args) {
|
|
192
|
+
if (!(udpFormSubmission === null || udpFormSubmission === void 0 ? void 0 : udpFormSubmission.id)) {
|
|
193
|
+
throw new Error('Cannot delete a comment without a submission ID.');
|
|
283
194
|
}
|
|
195
|
+
const { commentId } = args;
|
|
196
|
+
// Backend contract: DELETE /UdpFormSubmission/{submissionId}/comments/{commentId}
|
|
197
|
+
const url = `${configService.ConfigService.productV1ApiUrl}/UdpFormSubmission/${udpFormSubmission.id}/comments/${commentId}`;
|
|
198
|
+
await makeApiCall.makeApiCall('DELETE', url, null, true);
|
|
284
199
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
* then executes a minimal search and returns the first match or null.
|
|
290
|
-
*
|
|
291
|
-
* @param id - submission id to query
|
|
292
|
-
* @param generic1 - optional generic1 value to filter by
|
|
293
|
-
* @param generic2 - optional generic2 value to filter by
|
|
294
|
-
* @param generic3 - optional generic3 value to filter by
|
|
295
|
-
* @returns UdpFormSubmission or null
|
|
296
|
-
*/
|
|
297
|
-
async getFormSubmissionByIdAndGenerics(id, generic1, generic2, generic3) {
|
|
298
|
-
var _a;
|
|
299
|
-
try {
|
|
300
|
-
const search = new SearchBuilder.SearchBuilder(1, 1)
|
|
301
|
-
.addFilter('id', id, SearchBuilder.SearchOperators.EQUALS);
|
|
302
|
-
if (generic1) {
|
|
303
|
-
search.addFilter('generic1', generic1, SearchBuilder.SearchOperators.EQUALS);
|
|
304
|
-
}
|
|
305
|
-
if (generic2) {
|
|
306
|
-
search.addFilter('generic2', generic2, SearchBuilder.SearchOperators.EQUALS);
|
|
307
|
-
}
|
|
308
|
-
if (generic3) {
|
|
309
|
-
search.addFilter('generic3', generic3, SearchBuilder.SearchOperators.EQUALS);
|
|
310
|
-
}
|
|
311
|
-
const response = await search.execute('UdpFormSubmission');
|
|
312
|
-
return ((_a = response === null || response === void 0 ? void 0 : response.pageList) === null || _a === void 0 ? void 0 : _a[0]) || null;
|
|
200
|
+
async fetchAndPopulateUdpFormSubmissionObj(udpFormSubmission) {
|
|
201
|
+
if (!(udpFormSubmission === null || udpFormSubmission === void 0 ? void 0 : udpFormSubmission.id)) {
|
|
202
|
+
console.warn('Cannot fetch form submission without an ID.');
|
|
203
|
+
return udpFormSubmission;
|
|
313
204
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
205
|
+
// Public reads should not leak other users' submissions.
|
|
206
|
+
if (this.accessMode === 'public') {
|
|
207
|
+
const response = await this.getFormSubmissionByIdAndUser(udpFormSubmission.id, udpFormSubmission.unityUserId);
|
|
208
|
+
return response ? new UdpFormSubmission(Object.assign(Object.assign({}, udpFormSubmission), response)) : udpFormSubmission;
|
|
317
209
|
}
|
|
210
|
+
// Private: fetch by id.
|
|
211
|
+
const { id } = udpFormSubmission;
|
|
212
|
+
const response = await this.getFormSubmissionById(id);
|
|
213
|
+
return response ? new UdpFormSubmission(Object.assign(Object.assign({}, udpFormSubmission), response)) : udpFormSubmission;
|
|
318
214
|
}
|
|
319
|
-
// =============================
|
|
320
|
-
// Save & Submit Methods
|
|
321
|
-
// =============================
|
|
322
|
-
/**
|
|
323
|
-
* Create a new follow-up form submission that is linked to an existing submission.
|
|
324
|
-
*
|
|
325
|
-
* Saves a new UdpFormSubmission in 'in-progress' state without populating field values
|
|
326
|
-
* (only metadata/links), and returns the created submission object.
|
|
327
|
-
*
|
|
328
|
-
* @param udpFormSubmission - template/parent submission to base the new follow-up on
|
|
329
|
-
* @returns newly created UdpFormSubmission
|
|
330
|
-
*/
|
|
331
215
|
async createNewLinkedFollowUpFormSubmission(udpFormSubmission) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Persist submission metadata (without full field values).
|
|
337
|
-
*
|
|
338
|
-
* Converts provided udpFormSubmission into formData excluding field values,
|
|
339
|
-
* determines whether to POST or PUT based on presence of id, and merges the
|
|
340
|
-
* server response into a new UdpFormSubmission instance.
|
|
341
|
-
*
|
|
342
|
-
* @param udpFormSubmission - the submission to save
|
|
343
|
-
* @param status - desired status to save (e.g., 'in-progress' or 'submitted')
|
|
344
|
-
* @returns saved UdpFormSubmission (refetches if server returns empty PUT response)
|
|
345
|
-
*/
|
|
346
|
-
async saveFormDataWithoutValues(udpFormSubmission, status) {
|
|
347
|
-
try {
|
|
348
|
-
const formData = udpFormSubmission.processSubmissionIntoFormDataWithoutValues(status);
|
|
349
|
-
const { method, url } = this.getApiRequestInfo(udpFormSubmission);
|
|
350
|
-
const response = await makeApiCall.makeApiCall(method, url, formData, true);
|
|
351
|
-
// PUT returns an empty string; refetch to refresh local data
|
|
352
|
-
if (response === '') {
|
|
353
|
-
return await this.fetchAndPopulateUdpFormSubmissionObj(udpFormSubmission);
|
|
354
|
-
}
|
|
355
|
-
return new UdpFormSubmission(Object.assign(Object.assign({}, udpFormSubmission), response));
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
console.error('Failed to save form udpFormSubmission:', error);
|
|
359
|
-
throw error;
|
|
216
|
+
if (this.accessMode === 'public') {
|
|
217
|
+
throw new Error('Follow-up form submissions are not supported for public forms.');
|
|
360
218
|
}
|
|
219
|
+
return this.saveFormDataWithoutValues(udpFormSubmission, enums.UdpFormsSubmissionStatusEnum.InProgress);
|
|
361
220
|
}
|
|
362
|
-
/**
|
|
363
|
-
* Save only comment-related fields for a submission.
|
|
364
|
-
*
|
|
365
|
-
* Delegates to the lower-level saveCommentsToDB helper to persist comment changes
|
|
366
|
-
* while preserving existing submission state and metadata.
|
|
367
|
-
*
|
|
368
|
-
* @param values - comment values to persist
|
|
369
|
-
* @param udpFormSubmission - target submission object
|
|
370
|
-
* @returns updated UdpFormSubmission
|
|
371
|
-
*/
|
|
372
|
-
async saveFormSubmissionComments(values, udpFormSubmission) {
|
|
373
|
-
return this.saveCommentsToDB(values, udpFormSubmission);
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Save the current state of the form's data as a draft ('in-progress').
|
|
377
|
-
*
|
|
378
|
-
* Converts the provided values into the required form payload and persists them.
|
|
379
|
-
* Determines POST vs PUT automatically and refreshes local object on empty PUT responses.
|
|
380
|
-
*
|
|
381
|
-
* @param values - field data to save
|
|
382
|
-
* @param udpFormSubmission - submission being updated
|
|
383
|
-
* @returns updated UdpFormSubmission
|
|
384
|
-
*/
|
|
385
221
|
async saveCurrentFormSubmissionState(values, udpFormSubmission) {
|
|
222
|
+
if (this.accessMode === 'public') {
|
|
223
|
+
// Public forms do not support draft save / return-later.
|
|
224
|
+
return udpFormSubmission;
|
|
225
|
+
}
|
|
386
226
|
return this.saveSubmissionToDB(values, udpFormSubmission, enums.UdpFormsSubmissionStatusEnum.InProgress);
|
|
387
227
|
}
|
|
388
|
-
/**
|
|
389
|
-
* Finalize and submit the form (set state to 'submitted').
|
|
390
|
-
*
|
|
391
|
-
* Persists the provided values and marks the submission as 'submitted'.
|
|
392
|
-
*
|
|
393
|
-
* @param values - final form values to submit
|
|
394
|
-
* @param udpFormSubmission - submission being finalized
|
|
395
|
-
* @returns updated UdpFormSubmission
|
|
396
|
-
*/
|
|
397
228
|
async finalizeFormSubmissionState(values, udpFormSubmission) {
|
|
398
229
|
return this.saveSubmissionToDB(values, udpFormSubmission, enums.UdpFormsSubmissionStatusEnum.Submitted);
|
|
399
230
|
}
|
|
400
231
|
// =============================
|
|
401
232
|
// Helpers
|
|
402
233
|
// =============================
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
234
|
+
async saveFormDataWithoutValues(udpFormSubmission, status) {
|
|
235
|
+
const formData = udpFormSubmission.processSubmissionIntoFormDataWithoutValues(status);
|
|
236
|
+
const { method, url } = this.getApiRequestInfo(udpFormSubmission);
|
|
237
|
+
const response = await makeApiCall.makeApiCall(method, url, formData, true);
|
|
238
|
+
if (response === '') {
|
|
239
|
+
return await this.fetchAndPopulateUdpFormSubmissionObj(udpFormSubmission);
|
|
240
|
+
}
|
|
241
|
+
return new UdpFormSubmission(Object.assign(Object.assign({}, udpFormSubmission), response));
|
|
242
|
+
}
|
|
412
243
|
getApiRequestInfo(udpFormSubmission) {
|
|
413
|
-
const baseUrl = `${configService.ConfigService.productV1ApiUrl}/UdpFormSubmission`;
|
|
244
|
+
const baseUrl = `${configService.ConfigService.productV1ApiUrl}/UdpFormSubmission/${this.accessMode}`;
|
|
245
|
+
// Public mode does not support draft saves or updates; only a single submit (POST /public).
|
|
246
|
+
// Private mode supports POST (create) and PUT (update).
|
|
247
|
+
if (this.accessMode === 'public') {
|
|
248
|
+
return { method: 'POST', url: baseUrl };
|
|
249
|
+
}
|
|
414
250
|
return udpFormSubmission.id
|
|
415
|
-
? { method: 'PUT', url:
|
|
251
|
+
? { method: 'PUT', url: baseUrl }
|
|
416
252
|
: { method: 'POST', url: baseUrl };
|
|
417
253
|
}
|
|
418
|
-
/**
|
|
419
|
-
* Persist form values and metadata to the backend.
|
|
420
|
-
*
|
|
421
|
-
* Builds the form payload using udpFormSubmission.processSubmissionIntoFormData,
|
|
422
|
-
* resolves POST vs PUT using getApiRequestInfo, and returns a merged UdpFormSubmission.
|
|
423
|
-
* If the server returns an empty string for PUT, the method refetches the record.
|
|
424
|
-
*
|
|
425
|
-
* @param values - field values to be saved
|
|
426
|
-
* @param udpFormSubmission - submission being updated
|
|
427
|
-
* @param status - status to persist (e.g., 'in-progress' | 'submitted')
|
|
428
|
-
* @returns updated UdpFormSubmission
|
|
429
|
-
*/
|
|
430
254
|
async saveSubmissionToDB(values, udpFormSubmission, status) {
|
|
255
|
+
const formData = udpFormSubmission.processSubmissionIntoFormData(values, status);
|
|
256
|
+
const { method, url } = this.getApiRequestInfo(udpFormSubmission);
|
|
257
|
+
const response = await makeApiCall.makeApiCall(method, url, formData, true);
|
|
258
|
+
if (response === '') {
|
|
259
|
+
return await this.fetchAndPopulateUdpFormSubmissionObj(udpFormSubmission);
|
|
260
|
+
}
|
|
261
|
+
const updated = new UdpFormSubmission(Object.assign(Object.assign({}, udpFormSubmission), response));
|
|
262
|
+
// IMPORTANT: callers often pass a shared `udpFormSubmission` instance and do not use the return value.
|
|
263
|
+
// Ensure the original instance gets the new server-assigned id/status when the first save/submit creates it.
|
|
264
|
+
Object.assign(udpFormSubmission, updated);
|
|
265
|
+
return updated;
|
|
266
|
+
}
|
|
267
|
+
async getFormSubmissionById(id) {
|
|
268
|
+
var _a;
|
|
431
269
|
try {
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
// PUT returns an empty string; refetch to refresh local data
|
|
436
|
-
if (response === '') {
|
|
437
|
-
return await this.fetchAndPopulateUdpFormSubmissionObj(udpFormSubmission);
|
|
438
|
-
}
|
|
439
|
-
return new UdpFormSubmission(Object.assign(Object.assign({}, udpFormSubmission), response));
|
|
270
|
+
const search = new SearchBuilder.SearchBuilder(1, 1).addFilter('id', id, SearchBuilder.SearchOperators.EQUALS);
|
|
271
|
+
const response = await search.execute('UdpFormSubmission');
|
|
272
|
+
return ((_a = response === null || response === void 0 ? void 0 : response.pageList) === null || _a === void 0 ? void 0 : _a[0]) || null;
|
|
440
273
|
}
|
|
441
274
|
catch (error) {
|
|
442
|
-
console.error('
|
|
443
|
-
|
|
275
|
+
console.error('Error fetching form submission by ID:', error);
|
|
276
|
+
return null;
|
|
444
277
|
}
|
|
445
278
|
}
|
|
446
|
-
|
|
447
|
-
* Persist only comment changes for an existing submission via PUT.
|
|
448
|
-
*
|
|
449
|
-
* Forces a PUT to the specific submission id, builds a form payload containing
|
|
450
|
-
* the provided comment values, and returns the updated submission. If the API
|
|
451
|
-
* returns an empty string (PUT shorthand), the method refetches the record.
|
|
452
|
-
*
|
|
453
|
-
* @param values - comment fields to persist
|
|
454
|
-
* @param udpFormSubmission - target submission (must have id)
|
|
455
|
-
* @returns updated UdpFormSubmission
|
|
456
|
-
*/
|
|
457
|
-
async saveCommentsToDB(values, udpFormSubmission) {
|
|
279
|
+
async getFormSubmissionByIdAndUser(id, unityUserId) {
|
|
458
280
|
var _a;
|
|
459
281
|
try {
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
// PUT returns an empty string; refetch to refresh local data
|
|
466
|
-
if (response === '') {
|
|
467
|
-
return await this.fetchAndPopulateUdpFormSubmissionObj(udpFormSubmission);
|
|
468
|
-
}
|
|
469
|
-
return new UdpFormSubmission(Object.assign(Object.assign({}, udpFormSubmission), response));
|
|
282
|
+
const search = new SearchBuilder.SearchBuilder(1, 1)
|
|
283
|
+
.addFilter('id', id, SearchBuilder.SearchOperators.EQUALS)
|
|
284
|
+
.addFilter('unityUserId', unityUserId, SearchBuilder.SearchOperators.EQUALS);
|
|
285
|
+
const response = await search.execute('UdpFormSubmission');
|
|
286
|
+
return ((_a = response === null || response === void 0 ? void 0 : response.pageList) === null || _a === void 0 ? void 0 : _a[0]) || null;
|
|
470
287
|
}
|
|
471
288
|
catch (error) {
|
|
472
|
-
console.error('
|
|
473
|
-
|
|
289
|
+
console.error('Error fetching form submission by ID:', error);
|
|
290
|
+
return null;
|
|
474
291
|
}
|
|
475
292
|
}
|
|
476
293
|
}
|
|
@@ -479,36 +296,28 @@ class PrivateFormSubmissionHandler {
|
|
|
479
296
|
* Factory for creating form submission handlers.
|
|
480
297
|
*/
|
|
481
298
|
class FormSubmissionHandlerFactory {
|
|
482
|
-
static create(isPublic, userId
|
|
483
|
-
|
|
484
|
-
return new PublicFormSubmissionHandler(formId, version, tenantId);
|
|
485
|
-
}
|
|
486
|
-
if (!userId) {
|
|
487
|
-
throw new Error('User ID is required for private forms');
|
|
488
|
-
}
|
|
489
|
-
return new PrivateFormSubmissionHandler();
|
|
299
|
+
static create(isPublic, userId) {
|
|
300
|
+
return new FormSubmissionHandler(isPublic ? 'public' : 'private');
|
|
490
301
|
}
|
|
491
302
|
}
|
|
492
303
|
|
|
493
|
-
/**
|
|
494
|
-
* UDP Form handler (new UdpFormSubmission endpoint)
|
|
495
|
-
*/
|
|
496
304
|
class UdpFormHandler {
|
|
497
|
-
constructor(tenantId) {
|
|
305
|
+
constructor(tenantId, isPublic = false) {
|
|
498
306
|
this.tenantId = tenantId;
|
|
307
|
+
this.isPublic = isPublic;
|
|
499
308
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
309
|
+
buildDescribeUrl(formId, formVersion) {
|
|
310
|
+
const base = configService.ConfigService.productV1ApiUrl;
|
|
311
|
+
if (this.isPublic) {
|
|
312
|
+
return `${base}/UdpForm/${formId}/${formVersion}/describe/public`;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
return `${base}/UdpForm/${formId}/${formVersion}/describe`;
|
|
507
316
|
}
|
|
508
|
-
return formData;
|
|
509
317
|
}
|
|
510
318
|
async getFormByFormIdAndFormVersion(formId, formVersion) {
|
|
511
|
-
const
|
|
319
|
+
const url = this.buildDescribeUrl(formId, formVersion);
|
|
320
|
+
const formData = await makeApiCall.makeApiCall('GET', url);
|
|
512
321
|
if (formData.styleOverrides && typeof formData.styleOverrides === 'string') {
|
|
513
322
|
formData.styleOverrides = JSON.parse(formData.styleOverrides);
|
|
514
323
|
}
|
|
@@ -717,6 +526,306 @@ const applyUrlSeedValuesForAll = (dynamicSections, urlContext) => {
|
|
|
717
526
|
});
|
|
718
527
|
return merged;
|
|
719
528
|
};
|
|
529
|
+
const replaceUrlWithSubmissionId = (submissionId, history) => {
|
|
530
|
+
// build a URL that preserves the current pathname + hash but replaces the query string
|
|
531
|
+
const pathname = typeof window !== 'undefined' ? window.location.pathname : `/page/${enums.UdpFormsPageIdEnum.FormRendererPageId}`;
|
|
532
|
+
const hash = typeof window !== 'undefined' ? window.location.hash : '';
|
|
533
|
+
const newUrl = `${pathname}?udpf_submissionId=${submissionId}${hash}`;
|
|
534
|
+
const h = history;
|
|
535
|
+
// Prefer history.replace when available, handle react-router v6 navigate function, fallback to push or native replaceState
|
|
536
|
+
if (h) {
|
|
537
|
+
if (typeof h.replace === 'function') {
|
|
538
|
+
h.replace(newUrl);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (typeof h.push === 'function') {
|
|
542
|
+
h.push(newUrl);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// react-router v6 exposes a navigate function
|
|
546
|
+
if (typeof h === 'function') {
|
|
547
|
+
try {
|
|
548
|
+
h(newUrl, { replace: true });
|
|
549
|
+
}
|
|
550
|
+
catch (_a) {
|
|
551
|
+
h(newUrl);
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (typeof window !== 'undefined' && window.history && typeof window.history.replaceState === 'function') {
|
|
557
|
+
window.history.replaceState({}, '', newUrl);
|
|
558
|
+
}
|
|
559
|
+
else if (typeof window !== 'undefined') {
|
|
560
|
+
window.location.href = newUrl;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
const enqueueSnackbarSuccess = (message, enqueueSnackbar) => {
|
|
564
|
+
enqueueSnackbar === null || enqueueSnackbar === void 0 ? void 0 : enqueueSnackbar(message, {
|
|
565
|
+
variant: 'success',
|
|
566
|
+
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
const enqueueSnackbarError = (message, enqueueSnackbar) => {
|
|
570
|
+
enqueueSnackbar === null || enqueueSnackbar === void 0 ? void 0 : enqueueSnackbar(message, {
|
|
571
|
+
variant: 'error',
|
|
572
|
+
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const getSectionKey = (section) => {
|
|
577
|
+
return section.isOriginalSection ? section.name : `${section.name}_${section.sectionPositionSuffix}`;
|
|
578
|
+
};
|
|
579
|
+
const safeParseFieldProperties = (fieldProperties) => {
|
|
580
|
+
if (!fieldProperties)
|
|
581
|
+
return {};
|
|
582
|
+
if (typeof fieldProperties === 'object')
|
|
583
|
+
return fieldProperties;
|
|
584
|
+
if (typeof fieldProperties !== 'string')
|
|
585
|
+
return {};
|
|
586
|
+
try {
|
|
587
|
+
return JSON.parse(fieldProperties || '{}');
|
|
588
|
+
}
|
|
589
|
+
catch (_a) {
|
|
590
|
+
return {};
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const computeDuplicateRepeatableSection = (params) => {
|
|
594
|
+
var _a, _b, _c;
|
|
595
|
+
const { dynamicSections, values, index } = params;
|
|
596
|
+
const sectionToClone = dynamicSections[index];
|
|
597
|
+
if (!sectionToClone)
|
|
598
|
+
return { nextDynamicSections: dynamicSections, nextValues: values };
|
|
599
|
+
const cloningSectionName = sectionToClone.name;
|
|
600
|
+
// Find existing repeat group indices
|
|
601
|
+
const repeatKeys = findRepeatGroupKeys(cloningSectionName, values);
|
|
602
|
+
const nextRepeatIndex = repeatKeys.length > 0 ? Math.max(...repeatKeys) + 1 : 2;
|
|
603
|
+
const clonedSection = Object.assign(Object.assign({}, structuredClone(sectionToClone)), { formQuestions: (sectionToClone.formQuestions || []).map(q => {
|
|
604
|
+
const newQuestionObj = structuredClone(q);
|
|
605
|
+
newQuestionObj.questionIdentifierKey = `${cloningSectionName}_${nextRepeatIndex}.${q.name}`;
|
|
606
|
+
return newQuestionObj;
|
|
607
|
+
}), isOriginalSection: false, sectionPositionSuffix: nextRepeatIndex });
|
|
608
|
+
// Find the last index of this section group
|
|
609
|
+
let insertAtIndex = index;
|
|
610
|
+
for (let i = index + 1; i < dynamicSections.length; i++) {
|
|
611
|
+
const s = dynamicSections[i];
|
|
612
|
+
if (s.name === cloningSectionName)
|
|
613
|
+
insertAtIndex = i;
|
|
614
|
+
else
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
const nextDynamicSections = [
|
|
618
|
+
...dynamicSections.slice(0, insertAtIndex + 1),
|
|
619
|
+
clonedSection,
|
|
620
|
+
...dynamicSections.slice(insertAtIndex + 1),
|
|
621
|
+
];
|
|
622
|
+
const nextValues = structuredClone(values || {});
|
|
623
|
+
const newSectionKey = `${cloningSectionName}_${nextRepeatIndex}`;
|
|
624
|
+
const sourceSectionKey = getSectionKey(sectionToClone);
|
|
625
|
+
for (const q of clonedSection.formQuestions || []) {
|
|
626
|
+
if (!nextValues[newSectionKey])
|
|
627
|
+
nextValues[newSectionKey] = {};
|
|
628
|
+
let value = '';
|
|
629
|
+
if (q.fieldTypeId === enums.UdpFormsFieldTypeEnum.Paragraph) {
|
|
630
|
+
const sourceVal = (_b = (_a = values === null || values === void 0 ? void 0 : values[sourceSectionKey]) === null || _a === void 0 ? void 0 : _a[q.name]) === null || _b === void 0 ? void 0 : _b.value;
|
|
631
|
+
const props = safeParseFieldProperties(q.fieldProperties);
|
|
632
|
+
const paragraphDefault = (_c = props === null || props === void 0 ? void 0 : props.paragraphText) !== null && _c !== void 0 ? _c : '';
|
|
633
|
+
value = sourceVal !== null && sourceVal !== void 0 ? sourceVal : paragraphDefault;
|
|
634
|
+
}
|
|
635
|
+
nextValues[newSectionKey][q.name] = { value, comments: [], metadata: {} };
|
|
636
|
+
}
|
|
637
|
+
return { nextDynamicSections, nextValues };
|
|
638
|
+
};
|
|
639
|
+
const computeDeleteRepeatableSection = (params) => {
|
|
640
|
+
const { dynamicSections, values, index } = params;
|
|
641
|
+
const sectionToDelete = dynamicSections[index];
|
|
642
|
+
if (!sectionToDelete || sectionToDelete.isOriginalSection) {
|
|
643
|
+
return { nextDynamicSections: dynamicSections, nextValues: values };
|
|
644
|
+
}
|
|
645
|
+
const deleteSectionName = sectionToDelete.name;
|
|
646
|
+
const deleteSuffix = sectionToDelete.sectionPositionSuffix;
|
|
647
|
+
const sectionKeyToDelete = `${deleteSectionName}_${deleteSuffix}`;
|
|
648
|
+
const nextValues = Object.assign({}, (values || {}));
|
|
649
|
+
delete nextValues[sectionKeyToDelete];
|
|
650
|
+
const updatedSections = structuredClone(dynamicSections);
|
|
651
|
+
updatedSections.splice(index, 1);
|
|
652
|
+
// Shift all later repeatable sections backward by 1
|
|
653
|
+
updatedSections.forEach(section => {
|
|
654
|
+
var _a;
|
|
655
|
+
if (section.name !== deleteSectionName ||
|
|
656
|
+
section.isOriginalSection ||
|
|
657
|
+
((_a = section.sectionPositionSuffix) !== null && _a !== void 0 ? _a : 0) <= (deleteSuffix !== null && deleteSuffix !== void 0 ? deleteSuffix : 0)) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const oldSuffix = section.sectionPositionSuffix;
|
|
661
|
+
const oldSectionKey = `${deleteSectionName}_${oldSuffix}`;
|
|
662
|
+
const newSuffix = oldSuffix - 1;
|
|
663
|
+
const newSectionKey = `${deleteSectionName}_${newSuffix}`;
|
|
664
|
+
section.sectionPositionSuffix = newSuffix;
|
|
665
|
+
section.formQuestions = (section.formQuestions || []).map(q => {
|
|
666
|
+
const newSectionQuestion = structuredClone(q);
|
|
667
|
+
newSectionQuestion.questionIdentifierKey = `${newSectionKey}.${q.name}`;
|
|
668
|
+
return newSectionQuestion;
|
|
669
|
+
});
|
|
670
|
+
if (nextValues[oldSectionKey]) {
|
|
671
|
+
nextValues[newSectionKey] = structuredClone(nextValues[oldSectionKey]);
|
|
672
|
+
delete nextValues[oldSectionKey];
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return { nextDynamicSections: updatedSections, nextValues };
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Pure comment CRUD logic.
|
|
680
|
+
*
|
|
681
|
+
* - Updates submissionResponseData structure for comments/draftComments.
|
|
682
|
+
* - Does NOT perform any network calls.
|
|
683
|
+
* - Returns persistence intent for caller.
|
|
684
|
+
*/
|
|
685
|
+
function applyQuestionCommentCrud(params) {
|
|
686
|
+
var _a, _b, _c, _d, _e;
|
|
687
|
+
const { actionType, questionIdentifierKey, commentId, currentSubmissionResponseData, clientUserInfo } = params;
|
|
688
|
+
const parts = (questionIdentifierKey || '').split('.');
|
|
689
|
+
const sectionKey = parts[0] || '';
|
|
690
|
+
const questionKey = parts[1] || '';
|
|
691
|
+
// Defensive: if malformed key, do nothing.
|
|
692
|
+
if (!sectionKey || !questionKey) {
|
|
693
|
+
return {
|
|
694
|
+
sectionKey,
|
|
695
|
+
questionKey,
|
|
696
|
+
nextSubmissionResponseData: currentSubmissionResponseData || {},
|
|
697
|
+
didMutate: false,
|
|
698
|
+
shouldPersist: false,
|
|
699
|
+
persistIntent: 'none',
|
|
700
|
+
commentId,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const next = structuredClone(currentSubmissionResponseData || {});
|
|
704
|
+
// ensure structure exists WITHOUT overwriting existing data
|
|
705
|
+
if (!next[sectionKey])
|
|
706
|
+
next[sectionKey] = {};
|
|
707
|
+
if (!next[sectionKey][questionKey]) {
|
|
708
|
+
// create defaults but do not clobber any existing saved shape from submissionResponseData
|
|
709
|
+
next[sectionKey][questionKey] = {
|
|
710
|
+
value: '',
|
|
711
|
+
comments: [],
|
|
712
|
+
draftComments: [],
|
|
713
|
+
metadata: {},
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
// ensure arrays/objects exist so later code can safely push/filter
|
|
718
|
+
next[sectionKey][questionKey].comments = (_a = next[sectionKey][questionKey].comments) !== null && _a !== void 0 ? _a : [];
|
|
719
|
+
next[sectionKey][questionKey].draftComments = (_b = next[sectionKey][questionKey].draftComments) !== null && _b !== void 0 ? _b : [];
|
|
720
|
+
next[sectionKey][questionKey].metadata = (_c = next[sectionKey][questionKey].metadata) !== null && _c !== void 0 ? _c : {};
|
|
721
|
+
}
|
|
722
|
+
// normalize draftComments to array if needed (back-compat)
|
|
723
|
+
const maybeDraft = next[sectionKey][questionKey].draftComments;
|
|
724
|
+
if (maybeDraft && !Array.isArray(maybeDraft)) {
|
|
725
|
+
next[sectionKey][questionKey].draftComments = [maybeDraft];
|
|
726
|
+
}
|
|
727
|
+
else if (!maybeDraft) {
|
|
728
|
+
next[sectionKey][questionKey].draftComments = [];
|
|
729
|
+
}
|
|
730
|
+
const commentsArr = next[sectionKey][questionKey].comments || [];
|
|
731
|
+
const draftsArr = next[sectionKey][questionKey].draftComments || [];
|
|
732
|
+
let didMutate = false;
|
|
733
|
+
switch (actionType) {
|
|
734
|
+
case 'add': {
|
|
735
|
+
const newDraft = {
|
|
736
|
+
value: '',
|
|
737
|
+
commentId: uuid.v4(),
|
|
738
|
+
isTempComment: true,
|
|
739
|
+
timestamp: null,
|
|
740
|
+
};
|
|
741
|
+
// put new draft first so activeDraft === draftComments[0] matches UX
|
|
742
|
+
next[sectionKey][questionKey].draftComments = [newDraft, ...draftsArr];
|
|
743
|
+
didMutate = true;
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
case 'save': {
|
|
747
|
+
const draftIdx = draftsArr.findIndex((d) => d.commentId === commentId);
|
|
748
|
+
if (draftIdx === -1) {
|
|
749
|
+
return {
|
|
750
|
+
sectionKey,
|
|
751
|
+
questionKey,
|
|
752
|
+
nextSubmissionResponseData: next,
|
|
753
|
+
didMutate: false,
|
|
754
|
+
shouldPersist: false,
|
|
755
|
+
persistIntent: 'none',
|
|
756
|
+
commentId,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const draft = draftsArr[draftIdx];
|
|
760
|
+
const saveTimestamp = draft.timestamp || new Date().toISOString();
|
|
761
|
+
const savedComment = Object.assign(Object.assign({}, draft), { timestamp: saveTimestamp, editedTimestamp: new Date().toISOString(), userId: clientUserInfo === null || clientUserInfo === void 0 ? void 0 : clientUserInfo.id, userDisplayName: clientUserInfo === null || clientUserInfo === void 0 ? void 0 : clientUserInfo.displayName, isDraftComment: false });
|
|
762
|
+
const existingIdx = commentsArr.findIndex((c) => c.commentId === commentId);
|
|
763
|
+
if (existingIdx !== -1) {
|
|
764
|
+
const newComments = [...commentsArr];
|
|
765
|
+
newComments[existingIdx] = Object.assign({}, savedComment);
|
|
766
|
+
next[sectionKey][questionKey].comments = newComments;
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
next[sectionKey][questionKey].comments = [...commentsArr, savedComment];
|
|
770
|
+
}
|
|
771
|
+
next[sectionKey][questionKey].draftComments = draftsArr.filter((d) => d.commentId !== commentId);
|
|
772
|
+
didMutate = true;
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
case 'edit': {
|
|
776
|
+
const saved = commentsArr.find((c) => c.commentId === commentId);
|
|
777
|
+
if (!saved) {
|
|
778
|
+
return {
|
|
779
|
+
sectionKey,
|
|
780
|
+
questionKey,
|
|
781
|
+
nextSubmissionResponseData: next,
|
|
782
|
+
didMutate: false,
|
|
783
|
+
shouldPersist: false,
|
|
784
|
+
persistIntent: 'none',
|
|
785
|
+
commentId,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
const draftFromSaved = Object.assign(Object.assign({}, saved), { isDraftComment: true, timestamp: null });
|
|
789
|
+
next[sectionKey][questionKey].draftComments = [draftFromSaved, ...draftsArr];
|
|
790
|
+
next[sectionKey][questionKey].comments = commentsArr;
|
|
791
|
+
didMutate = true;
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
case 'delete': {
|
|
795
|
+
next[sectionKey][questionKey].comments = commentsArr.map((c) => {
|
|
796
|
+
if (c.commentId === commentId) {
|
|
797
|
+
return Object.assign(Object.assign({}, c), { isDeleted: true, value: '', editedTimestamp: new Date().toISOString() });
|
|
798
|
+
}
|
|
799
|
+
return c;
|
|
800
|
+
});
|
|
801
|
+
didMutate = true;
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
case 'editClose': {
|
|
805
|
+
next[sectionKey][questionKey].draftComments = draftsArr.filter((d) => d.commentId !== commentId);
|
|
806
|
+
didMutate = true;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const shouldPersist = actionType === 'save' || actionType === 'delete';
|
|
811
|
+
// For the dedicated comments API we only ever need the saved comment value.
|
|
812
|
+
let valueToPersist;
|
|
813
|
+
if (actionType === 'save') {
|
|
814
|
+
const saved = (((_e = (_d = next === null || next === void 0 ? void 0 : next[sectionKey]) === null || _d === void 0 ? void 0 : _d[questionKey]) === null || _e === void 0 ? void 0 : _e.comments) || []).find((c) => c.commentId === commentId);
|
|
815
|
+
valueToPersist = saved === null || saved === void 0 ? void 0 : saved.value;
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
sectionKey,
|
|
819
|
+
questionKey,
|
|
820
|
+
nextSubmissionResponseData: next,
|
|
821
|
+
didMutate,
|
|
822
|
+
shouldPersist,
|
|
823
|
+
// Caller decides if this becomes public-add vs private-update.
|
|
824
|
+
persistIntent: 'none',
|
|
825
|
+
commentId,
|
|
826
|
+
valueToPersist,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
720
829
|
|
|
721
830
|
const UdpFormsRenderer = class {
|
|
722
831
|
constructor(hostRef) {
|
|
@@ -725,17 +834,14 @@ const UdpFormsRenderer = class {
|
|
|
725
834
|
this.autoSaveDelay = 2000; // Debounce delay for auto-save in milliseconds (currently disabled)
|
|
726
835
|
this.urlContext = {}; // additional context from URL if needed, eg. generic1, gernic2, generic3, or any initial prepopulated values for form inputs. eg section1.question1 = 'some value'
|
|
727
836
|
this.currentValues = {}; // values of the current form state
|
|
728
|
-
this.formInputSeedValues = {}; // support for a initial set of values to seed the form with
|
|
729
837
|
this.submitSuccessful = false;
|
|
730
838
|
this.isLoading = false;
|
|
731
839
|
this.isSaving = false;
|
|
732
840
|
this.isSubmitted = false;
|
|
733
|
-
this.saveErrorMessage = null;
|
|
734
841
|
this.dynamicSections = [];
|
|
735
842
|
this.isUpdatingSections = false;
|
|
736
843
|
this.reRenderKey = 0;
|
|
737
|
-
this.
|
|
738
|
-
this.isUserUpdatedSections = false;
|
|
844
|
+
this.hasUnsavedChanges = false;
|
|
739
845
|
this.sideSheetFollowUpFormsList = []; // used for follow up forms.
|
|
740
846
|
this.followUpSideSheetTotalItems = 0; // used for follow up forms.
|
|
741
847
|
this.isFollowUpFormsSideSheetOpen = false; // used for follow up forms.
|
|
@@ -755,9 +861,19 @@ const UdpFormsRenderer = class {
|
|
|
755
861
|
};
|
|
756
862
|
this.followUpParentSectionKey = ''; // used for follow up forms.
|
|
757
863
|
this.followUpParentQuestionKey = ''; // used for follow up forms.
|
|
864
|
+
// Feature flags for public mode. Keep capabilities in the handlers,
|
|
865
|
+
// but disable UX / automatic flows for now.
|
|
866
|
+
this.publicFeatures = {
|
|
867
|
+
allowDraftSave: false, // public cannot save and return later
|
|
868
|
+
allowComments: true, // public can add comments locally; persistence handled by backend rules
|
|
869
|
+
allowFollowUpForms: false, // public does not support follow-up forms
|
|
870
|
+
};
|
|
758
871
|
this.handleLaunchFollowUpFormSideSheet = async (e) => {
|
|
759
872
|
try {
|
|
760
|
-
|
|
873
|
+
// Public forms do not support follow-up forms.
|
|
874
|
+
if (this.isPublic) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
761
877
|
this.followUpParentSectionKey = e.detail.sectionKey;
|
|
762
878
|
this.followUpParentQuestionKey = e.detail.questionKey;
|
|
763
879
|
await this.loadFollowUpForms(this.followUpParentSectionKey, this.followUpParentQuestionKey);
|
|
@@ -776,75 +892,20 @@ const UdpFormsRenderer = class {
|
|
|
776
892
|
this.isUpdatingSections = true;
|
|
777
893
|
this.isLoading = true;
|
|
778
894
|
try {
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
// Find existing repeat group indices
|
|
784
|
-
const repeatKeys = findRepeatGroupKeys(cloningSectionName, this.currentValues);
|
|
785
|
-
const nextRepeatIndex = repeatKeys.length > 0 ? Math.max(...repeatKeys) + 1 : 2;
|
|
786
|
-
const clonedSection = Object.assign(Object.assign({}, structuredClone(sectionToClone)), { formQuestions: sectionToClone.formQuestions.map(q => {
|
|
787
|
-
const newQuestionObj = structuredClone(q);
|
|
788
|
-
newQuestionObj.questionIdentifierKey = `${cloningSectionName}_${nextRepeatIndex}.${q.name}`;
|
|
789
|
-
return newQuestionObj;
|
|
790
|
-
}), isOriginalSection: false, sectionPositionSuffix: nextRepeatIndex });
|
|
791
|
-
// Find the last index of this section group
|
|
792
|
-
let insertAtIndex = index;
|
|
793
|
-
for (let i = index + 1; i < this.dynamicSections.length; i++) {
|
|
794
|
-
const s = this.dynamicSections[i];
|
|
795
|
-
if (s.name === cloningSectionName) {
|
|
796
|
-
insertAtIndex = i;
|
|
797
|
-
}
|
|
798
|
-
else {
|
|
799
|
-
break;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
// Insert after the last repeat of the section group
|
|
803
|
-
this.dynamicSections = [
|
|
804
|
-
...this.dynamicSections.slice(0, insertAtIndex + 1),
|
|
805
|
-
clonedSection,
|
|
806
|
-
...this.dynamicSections.slice(insertAtIndex + 1),
|
|
807
|
-
];
|
|
808
|
-
// Create new initial values structure
|
|
809
|
-
const newCurrentValues = structuredClone(this.udpFormSubmission.data.submissionResponseData);
|
|
810
|
-
clonedSection.formQuestions.forEach(q => {
|
|
811
|
-
var _a, _b, _c, _d;
|
|
812
|
-
const newSectionNameWithSuffix = `${cloningSectionName}_${nextRepeatIndex}`;
|
|
813
|
-
const newQuestionName = q.name;
|
|
814
|
-
if (!newCurrentValues[newSectionNameWithSuffix]) {
|
|
815
|
-
newCurrentValues[newSectionNameWithSuffix] = {};
|
|
816
|
-
}
|
|
817
|
-
// Preserve only Paragraph values from the source section; clear others
|
|
818
|
-
let value = '';
|
|
819
|
-
if (q.fieldTypeId === enums.UdpFormsFieldTypeEnum.Paragraph) {
|
|
820
|
-
// Determine source section key (the section being duplicated)
|
|
821
|
-
const sourceSectionKey = sectionToClone.isOriginalSection
|
|
822
|
-
? cloningSectionName
|
|
823
|
-
: `${cloningSectionName}_${sectionToClone.sectionPositionSuffix}`;
|
|
824
|
-
const sourceVal = (_c = (_b = (_a = this.udpFormSubmission.data.submissionResponseData) === null || _a === void 0 ? void 0 : _a[sourceSectionKey]) === null || _b === void 0 ? void 0 : _b[newQuestionName]) === null || _c === void 0 ? void 0 : _c.value;
|
|
825
|
-
let fieldProps = q === null || q === void 0 ? void 0 : q.fieldProperties;
|
|
826
|
-
if (typeof fieldProps === 'string') {
|
|
827
|
-
try {
|
|
828
|
-
fieldProps = JSON.parse(fieldProps || '{}');
|
|
829
|
-
}
|
|
830
|
-
catch (_e) {
|
|
831
|
-
fieldProps = {};
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
const paragraphDefault = (_d = fieldProps === null || fieldProps === void 0 ? void 0 : fieldProps.paragraphText) !== null && _d !== void 0 ? _d : '';
|
|
835
|
-
value = sourceVal !== null && sourceVal !== void 0 ? sourceVal : paragraphDefault;
|
|
836
|
-
}
|
|
837
|
-
newCurrentValues[newSectionNameWithSuffix][newQuestionName] = { value, comments: [], metadata: {} };
|
|
895
|
+
const { nextDynamicSections, nextValues } = computeDuplicateRepeatableSection({
|
|
896
|
+
dynamicSections: this.dynamicSections,
|
|
897
|
+
values: structuredClone(this.udpFormSubmission.data.submissionResponseData || {}),
|
|
898
|
+
index,
|
|
838
899
|
});
|
|
839
|
-
|
|
840
|
-
this.currentValues = Object.assign({},
|
|
841
|
-
this.udpFormSubmission.data.submissionResponseData = Object.assign({},
|
|
900
|
+
this.dynamicSections = nextDynamicSections;
|
|
901
|
+
this.currentValues = Object.assign({}, nextValues);
|
|
902
|
+
this.udpFormSubmission.data.submissionResponseData = Object.assign({}, nextValues);
|
|
842
903
|
this.triggerFormRerender();
|
|
843
904
|
}
|
|
844
905
|
finally {
|
|
845
906
|
this.isUpdatingSections = false;
|
|
846
907
|
this.isLoading = false;
|
|
847
|
-
this.
|
|
908
|
+
this.hasUnsavedChanges = true;
|
|
848
909
|
}
|
|
849
910
|
};
|
|
850
911
|
/**
|
|
@@ -859,137 +920,90 @@ const UdpFormsRenderer = class {
|
|
|
859
920
|
this.isUpdatingSections = true;
|
|
860
921
|
this.isLoading = true;
|
|
861
922
|
try {
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
delete newCurrentValues[sectionKeyToDelete];
|
|
867
|
-
// Remove the section from dynamicSections
|
|
868
|
-
const updatedSections = structuredClone(this.dynamicSections);
|
|
869
|
-
updatedSections.splice(index, 1);
|
|
870
|
-
// Shift all later repeatable sections backward by 1
|
|
871
|
-
updatedSections.forEach(section => {
|
|
872
|
-
if (section.name !== deleteSectionName ||
|
|
873
|
-
section.isOriginalSection ||
|
|
874
|
-
section.sectionPositionSuffix <= deleteSuffix) {
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const oldSuffix = section.sectionPositionSuffix;
|
|
878
|
-
const oldSectionKey = `${deleteSectionName}_${oldSuffix}`;
|
|
879
|
-
const newSuffix = oldSuffix - 1;
|
|
880
|
-
const newSectionKey = `${deleteSectionName}_${newSuffix}`;
|
|
881
|
-
// Update suffix
|
|
882
|
-
section.sectionPositionSuffix = newSuffix;
|
|
883
|
-
// Update questionIdentifierKeys
|
|
884
|
-
section.formQuestions = section.formQuestions.map(q => {
|
|
885
|
-
const newSectionQuestion = structuredClone(q);
|
|
886
|
-
newSectionQuestion.questionIdentifierKey = `${newSectionKey}.${q.name}`;
|
|
887
|
-
return newSectionQuestion;
|
|
888
|
-
});
|
|
889
|
-
// Move data in initial values
|
|
890
|
-
if (newCurrentValues[oldSectionKey]) {
|
|
891
|
-
newCurrentValues[newSectionKey] = structuredClone(newCurrentValues[oldSectionKey]);
|
|
892
|
-
delete newCurrentValues[oldSectionKey];
|
|
893
|
-
}
|
|
923
|
+
const { nextDynamicSections, nextValues } = computeDeleteRepeatableSection({
|
|
924
|
+
dynamicSections: this.dynamicSections,
|
|
925
|
+
values: Object.assign({}, (this.udpFormSubmission.data.submissionResponseData || {})),
|
|
926
|
+
index,
|
|
894
927
|
});
|
|
895
|
-
this.dynamicSections =
|
|
896
|
-
this.currentValues = Object.assign({},
|
|
897
|
-
this.udpFormSubmission.data.submissionResponseData = Object.assign({},
|
|
928
|
+
this.dynamicSections = nextDynamicSections;
|
|
929
|
+
this.currentValues = Object.assign({}, nextValues);
|
|
930
|
+
this.udpFormSubmission.data.submissionResponseData = Object.assign({}, nextValues);
|
|
898
931
|
this.triggerFormRerender();
|
|
899
932
|
}
|
|
900
933
|
finally {
|
|
901
934
|
this.isUpdatingSections = false;
|
|
902
935
|
this.isLoading = false;
|
|
903
|
-
this.
|
|
936
|
+
this.hasUnsavedChanges = true;
|
|
904
937
|
}
|
|
905
938
|
};
|
|
906
939
|
/**
|
|
907
940
|
* Auto save (background save)
|
|
908
941
|
*/
|
|
909
942
|
this.performBackgroundSaveAndUpdateLocalSubmissionState = async (values) => {
|
|
910
|
-
|
|
911
|
-
|
|
943
|
+
return this.performBackgroundSaveAndUpdateLocalSubmissionStateInternal(values);
|
|
944
|
+
};
|
|
945
|
+
this.performBackgroundSaveAndUpdateLocalSubmissionStateInternal = async (values, opts) => {
|
|
946
|
+
var _a, _b;
|
|
947
|
+
const allowUrlReplace = (_a = opts === null || opts === void 0 ? void 0 : opts.allowUrlReplace) !== null && _a !== void 0 ? _a : !this.isPublic;
|
|
948
|
+
const forceCreateDraftIfMissingId = (_b = opts === null || opts === void 0 ? void 0 : opts.forceCreateDraftIfMissingId) !== null && _b !== void 0 ? _b : false;
|
|
949
|
+
// Public mode: disable draft save flows by default
|
|
950
|
+
if (this.isPublic && !this.publicFeatures.allowDraftSave && !forceCreateDraftIfMissingId) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
912
953
|
try {
|
|
913
|
-
|
|
914
|
-
this.
|
|
915
|
-
|
|
916
|
-
if (!new URLSearchParams(window.location.search).has('udpf_submissionId')) {
|
|
954
|
+
await this.formSubmissionHandler.saveCurrentFormSubmissionState(values, this.udpFormSubmission);
|
|
955
|
+
await this.refreshSubmissionAndSyncState();
|
|
956
|
+
if (allowUrlReplace && !new URLSearchParams(window.location.search).has('udpf_submissionId')) {
|
|
917
957
|
// replace the current entry's query string with udpf_submissionId without needing to know the path
|
|
918
|
-
|
|
958
|
+
replaceUrlWithSubmissionId(this.udpFormSubmission.id, this.history);
|
|
919
959
|
}
|
|
920
960
|
}
|
|
921
961
|
catch (error) {
|
|
922
|
-
this.saveErrorMessage = 'Failed to save form data';
|
|
923
962
|
}
|
|
924
963
|
finally {
|
|
925
964
|
this.isSaving = false;
|
|
926
|
-
this.
|
|
927
|
-
this.isFormDirty = false;
|
|
965
|
+
this.hasUnsavedChanges = false;
|
|
928
966
|
}
|
|
929
967
|
};
|
|
968
|
+
this.refreshSubmissionAndSyncState = async () => {
|
|
969
|
+
var _a, _b;
|
|
970
|
+
if (!((_a = this.udpFormSubmission) === null || _a === void 0 ? void 0 : _a.id))
|
|
971
|
+
return;
|
|
972
|
+
const updated = await this.formSubmissionHandler.fetchAndPopulateUdpFormSubmissionObj(this.udpFormSubmission);
|
|
973
|
+
this.udpFormSubmission = updated;
|
|
974
|
+
this.currentValues = Object.assign({}, (((_b = updated.data) === null || _b === void 0 ? void 0 : _b.submissionResponseData) || {}));
|
|
975
|
+
this.isSubmitted = updated.status === enums.UdpFormsSubmissionStatusEnum.Submitted;
|
|
976
|
+
};
|
|
930
977
|
/**
|
|
931
|
-
* Handle the user saving or deleting a comment
|
|
978
|
+
* Handle the user saving or deleting a comment (pre-submit, private forms only)
|
|
932
979
|
*/
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const updatedUdpFormSubmission = await this.formSubmissionHandler.saveFormSubmissionComments(values, this.udpFormSubmission);
|
|
939
|
-
this.udpFormSubmission = updatedUdpFormSubmission;
|
|
940
|
-
this.currentValues = Object.assign({}, (((_a = updatedUdpFormSubmission.data) === null || _a === void 0 ? void 0 : _a.submissionResponseData) || {}));
|
|
941
|
-
this.enqueueSnackbar('Saved sucessfully.', {
|
|
942
|
-
variant: 'success',
|
|
943
|
-
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
catch (error) {
|
|
947
|
-
this.enqueueSnackbar('There was an error saving.', {
|
|
948
|
-
variant: 'error',
|
|
949
|
-
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
finally {
|
|
953
|
-
this.isSaving = false;
|
|
954
|
-
this.isUserUpdatedSections = false;
|
|
955
|
-
this.isFormDirty = false;
|
|
956
|
-
}
|
|
957
|
-
};
|
|
980
|
+
// NOTE: comment saves for non-submitted owner flows are now handled by the standard
|
|
981
|
+
// draft save path (manual/background save). Dedicated comment endpoints are used
|
|
982
|
+
// only for:
|
|
983
|
+
// 1) submitted submissions, OR
|
|
984
|
+
// 2) non-owner viewers adding/updating/deleting comments.
|
|
958
985
|
/**
|
|
959
|
-
* Manual save function - debounced to 5 seconds
|
|
986
|
+
* Manual save function - debounced to 5 seconds (private forms only)
|
|
960
987
|
*/
|
|
961
988
|
this.handleManualSave = async (values) => {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
// return;
|
|
965
|
-
// }
|
|
966
|
-
var _a;
|
|
989
|
+
if (this.isPublic && !this.publicFeatures.allowDraftSave)
|
|
990
|
+
return;
|
|
967
991
|
this.isSaving = true;
|
|
968
|
-
this.saveErrorMessage = null;
|
|
969
992
|
try {
|
|
970
|
-
|
|
971
|
-
this.
|
|
972
|
-
this.currentValues = Object.assign({}, (((_a = updatedUdpFormSubmission.data) === null || _a === void 0 ? void 0 : _a.submissionResponseData) || {}));
|
|
993
|
+
await this.formSubmissionHandler.saveCurrentFormSubmissionState(values, this.udpFormSubmission);
|
|
994
|
+
await this.refreshSubmissionAndSyncState();
|
|
973
995
|
if (!new URLSearchParams(window.location.search).has('udpf_submissionId')) {
|
|
974
996
|
// replace the current entry's query string with udpf_submissionId without needing to know the path
|
|
975
|
-
|
|
997
|
+
replaceUrlWithSubmissionId(this.udpFormSubmission.id, this.history);
|
|
976
998
|
}
|
|
977
|
-
|
|
978
|
-
variant: 'success',
|
|
979
|
-
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
|
980
|
-
});
|
|
999
|
+
enqueueSnackbarSuccess('Form saved successfully.', this.enqueueSnackbar);
|
|
981
1000
|
}
|
|
982
1001
|
catch (error) {
|
|
983
|
-
|
|
984
|
-
variant: 'error',
|
|
985
|
-
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
|
986
|
-
});
|
|
987
|
-
this.saveErrorMessage = 'Failed to save form data';
|
|
1002
|
+
enqueueSnackbarError('There was an error saving this form', this.enqueueSnackbar);
|
|
988
1003
|
}
|
|
989
1004
|
finally {
|
|
990
1005
|
this.isSaving = false;
|
|
991
|
-
this.
|
|
992
|
-
this.isFormDirty = false;
|
|
1006
|
+
this.hasUnsavedChanges = false;
|
|
993
1007
|
}
|
|
994
1008
|
};
|
|
995
1009
|
this.handleFormChange = (values) => {
|
|
@@ -1056,7 +1070,9 @@ const UdpFormsRenderer = class {
|
|
|
1056
1070
|
// Listen for launchFollowUpFormSideSheet event from udp-question
|
|
1057
1071
|
this.el.addEventListener('launchFollowUpFormSideSheet', (e) => this.handleLaunchFollowUpFormSideSheet(e));
|
|
1058
1072
|
this.el.addEventListener('formDirtyChange', (e) => {
|
|
1059
|
-
|
|
1073
|
+
// Preserve any previously-detected unsaved changes (e.g. repeatable section add/delete)
|
|
1074
|
+
// so a later false emission doesn't hide the save icon.
|
|
1075
|
+
this.hasUnsavedChanges = this.hasUnsavedChanges || e.detail;
|
|
1060
1076
|
});
|
|
1061
1077
|
}
|
|
1062
1078
|
}
|
|
@@ -1065,17 +1081,24 @@ const UdpFormsRenderer = class {
|
|
|
1065
1081
|
this.isLoading = true;
|
|
1066
1082
|
try {
|
|
1067
1083
|
// Get client user info if available
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
this.clientUserInfo.
|
|
1071
|
-
|
|
1072
|
-
|
|
1084
|
+
if (this.isPublic) {
|
|
1085
|
+
this.clientUserInfo.id = '00000000-0000-0000-0000-000000000001';
|
|
1086
|
+
this.clientUserInfo.displayName = 'Anonymous';
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
const user = (_a = this.getUserCallback) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
1090
|
+
if (user) {
|
|
1091
|
+
this.clientUserInfo.id = user.id || null;
|
|
1092
|
+
this.clientUserInfo.displayName = user.name || null;
|
|
1093
|
+
this.clientUserInfo.email = user.email || null;
|
|
1094
|
+
}
|
|
1073
1095
|
}
|
|
1074
1096
|
this.formSubmissionHandler = FormSubmissionHandlerFactory.create(this.isPublic, this.clientUserInfo.id);
|
|
1075
1097
|
this.udpFormSubmission = new UdpFormSubmission({ id: this.submissionId, formId: this.formId, formVersion: this.version, unityUserId: this.clientUserInfo.id, generic1: this.urlContext.generic1, generic2: this.urlContext.generic2, generic3: this.urlContext.generic3 });
|
|
1076
1098
|
// fetch existing submission from Udp.FormSubmission if submissionId is provided
|
|
1077
1099
|
// take exisitng object, and populate with db values, and return new obj with updated values
|
|
1078
1100
|
if (this.submissionId) {
|
|
1101
|
+
// Public users should not open private forms; we rely on backend auth to block.
|
|
1079
1102
|
this.udpFormSubmission = await this.formSubmissionHandler.fetchAndPopulateUdpFormSubmissionObj(this.udpFormSubmission);
|
|
1080
1103
|
}
|
|
1081
1104
|
// get the master form from Udp.Form
|
|
@@ -1134,157 +1157,117 @@ const UdpFormsRenderer = class {
|
|
|
1134
1157
|
async handleSubmit(values) {
|
|
1135
1158
|
this.isLoading = true;
|
|
1136
1159
|
try {
|
|
1137
|
-
|
|
1160
|
+
await this.formSubmissionHandler.finalizeFormSubmissionState(values, this.udpFormSubmission);
|
|
1161
|
+
await this.refreshSubmissionAndSyncState();
|
|
1138
1162
|
this.submitSuccessful = true;
|
|
1139
1163
|
}
|
|
1140
1164
|
catch (error) {
|
|
1141
|
-
|
|
1142
|
-
variant: 'error',
|
|
1143
|
-
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
|
1144
|
-
});
|
|
1165
|
+
enqueueSnackbarError('There was an error submitting this form', this.enqueueSnackbar);
|
|
1145
1166
|
throw error;
|
|
1146
1167
|
}
|
|
1147
1168
|
finally {
|
|
1148
1169
|
this.isLoading = false;
|
|
1149
1170
|
}
|
|
1150
1171
|
}
|
|
1151
|
-
replaceUrlWithSubmissionId(submissionId) {
|
|
1152
|
-
// build a URL that preserves the current pathname + hash but replaces the query string
|
|
1153
|
-
const pathname = typeof window !== 'undefined' ? window.location.pathname : `/page/${enums.UdpFormsPageIdEnum.FormRendererPageId}`;
|
|
1154
|
-
const hash = typeof window !== 'undefined' ? window.location.hash : '';
|
|
1155
|
-
const newUrl = `${pathname}?udpf_submissionId=${submissionId}${hash}`;
|
|
1156
|
-
const h = this.history;
|
|
1157
|
-
// Prefer history.replace when available, handle react-router v6 navigate function, fallback to push or native replaceState
|
|
1158
|
-
if (h) {
|
|
1159
|
-
if (typeof h.replace === 'function') {
|
|
1160
|
-
h.replace(newUrl);
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
if (typeof h.push === 'function') {
|
|
1164
|
-
h.push(newUrl);
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
// react-router v6 exposes a navigate function
|
|
1168
|
-
if (typeof h === 'function') {
|
|
1169
|
-
try {
|
|
1170
|
-
h(newUrl, { replace: true });
|
|
1171
|
-
}
|
|
1172
|
-
catch (_a) {
|
|
1173
|
-
h(newUrl);
|
|
1174
|
-
}
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
if (typeof window !== 'undefined' && window.history && typeof window.history.replaceState === 'function') {
|
|
1179
|
-
window.history.replaceState({}, '', newUrl);
|
|
1180
|
-
}
|
|
1181
|
-
else if (typeof window !== 'undefined') {
|
|
1182
|
-
window.location.href = newUrl;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
1172
|
async handleQuestionCommentLiveCRUD(e) {
|
|
1186
|
-
var _a, _b, _c;
|
|
1173
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1174
|
+
if (this.isPublic && !this.publicFeatures.allowComments)
|
|
1175
|
+
return;
|
|
1187
1176
|
try {
|
|
1188
1177
|
const { type, questionIdentifierKey, commentId } = e.detail;
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
else {
|
|
1204
|
-
// ensure arrays/objects exist so later code can safely push/filter
|
|
1205
|
-
newCurrentValues[sectionName][questionName].comments = (_a = newCurrentValues[sectionName][questionName].comments) !== null && _a !== void 0 ? _a : [];
|
|
1206
|
-
newCurrentValues[sectionName][questionName].draftComments = (_b = newCurrentValues[sectionName][questionName].draftComments) !== null && _b !== void 0 ? _b : [];
|
|
1207
|
-
newCurrentValues[sectionName][questionName].metadata = (_c = newCurrentValues[sectionName][questionName].metadata) !== null && _c !== void 0 ? _c : {};
|
|
1208
|
-
}
|
|
1209
|
-
// normalize draftComments to array if needed (back-compat)
|
|
1210
|
-
const maybeDraft = newCurrentValues[sectionName][questionName].draftComments;
|
|
1211
|
-
if (maybeDraft && !Array.isArray(maybeDraft)) {
|
|
1212
|
-
newCurrentValues[sectionName][questionName].draftComments = [maybeDraft];
|
|
1213
|
-
}
|
|
1214
|
-
else if (!maybeDraft) {
|
|
1215
|
-
newCurrentValues[sectionName][questionName].draftComments = [];
|
|
1178
|
+
const result = applyQuestionCommentCrud({
|
|
1179
|
+
actionType: type,
|
|
1180
|
+
questionIdentifierKey,
|
|
1181
|
+
commentId,
|
|
1182
|
+
currentSubmissionResponseData: this.udpFormSubmission.data.submissionResponseData || {},
|
|
1183
|
+
clientUserInfo: this.clientUserInfo,
|
|
1184
|
+
});
|
|
1185
|
+
const newCurrentValues = result.nextSubmissionResponseData;
|
|
1186
|
+
// keep submission state in sync
|
|
1187
|
+
this.udpFormSubmission.data.submissionResponseData = Object.assign({}, newCurrentValues);
|
|
1188
|
+
// Edit-ish actions are UI-only (open/close draft editor). They must update currentValues
|
|
1189
|
+
// even though they don't persist.
|
|
1190
|
+
if (type === 'edit' || type === 'editClose' || type === 'add') {
|
|
1191
|
+
this.currentValues = Object.assign({}, newCurrentValues);
|
|
1216
1192
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
const draftIdx = draftsArr.findIndex(d => d.commentId === commentId);
|
|
1234
|
-
if (draftIdx === -1)
|
|
1235
|
-
return;
|
|
1236
|
-
const draft = draftsArr[draftIdx];
|
|
1237
|
-
const saveTimestamp = draft.timestamp || new Date().toISOString();
|
|
1238
|
-
const savedComment = Object.assign(Object.assign({}, draft), { timestamp: saveTimestamp, editedTimestamp: new Date().toISOString(), userId: this.clientUserInfo.id, userDisplayName: this.clientUserInfo.displayName, isDraftComment: false });
|
|
1239
|
-
// If a saved comment with same id exists, replace it. Otherwise append.
|
|
1240
|
-
const existingIdx = commentsArr.findIndex(c => c.commentId === commentId);
|
|
1241
|
-
if (existingIdx !== -1) {
|
|
1242
|
-
const newComments = [...commentsArr];
|
|
1243
|
-
newComments[existingIdx] = Object.assign({}, savedComment);
|
|
1244
|
-
newCurrentValues[sectionName][questionName].comments = newComments;
|
|
1193
|
+
// if it's a save or delete action, persist immediately
|
|
1194
|
+
if (result.shouldPersist) {
|
|
1195
|
+
const submissionIsSubmitted = ((_a = this.udpFormSubmission) === null || _a === void 0 ? void 0 : _a.status) === enums.UdpFormsSubmissionStatusEnum.Submitted;
|
|
1196
|
+
const submissionIsOwnedByCurrentUser = ((_b = this.udpFormSubmission) === null || _b === void 0 ? void 0 : _b.unityUserId) === this.clientUserInfo.id;
|
|
1197
|
+
// Detect whether this save is updating an existing saved comment vs creating a new one.
|
|
1198
|
+
// IMPORTANT: check against the *previous* persisted state (before applyQuestionCommentCrud),
|
|
1199
|
+
// because once we apply 'save' locally the comment will exist locally even if it's new.
|
|
1200
|
+
const existingSavedComment = (((_e = (_d = (_c = ((this.currentValues || {}))) === null || _c === void 0 ? void 0 : _c[result.sectionKey]) === null || _d === void 0 ? void 0 : _d[result.questionKey]) === null || _e === void 0 ? void 0 : _e.comments) || [])
|
|
1201
|
+
.find((c) => (c === null || c === void 0 ? void 0 : c.commentId) === result.commentId && !(c === null || c === void 0 ? void 0 : c.isDeleted));
|
|
1202
|
+
// Use dedicated comment APIs in 2 scenarios:
|
|
1203
|
+
// 1) Once submitted (public+private) to avoid overwriting submission data.
|
|
1204
|
+
// 2) When viewed by a non-owner (read-only mode), since they can't save the whole submission.
|
|
1205
|
+
if (submissionIsSubmitted || !submissionIsOwnedByCurrentUser) {
|
|
1206
|
+
if (!((_f = this.udpFormSubmission) === null || _f === void 0 ? void 0 : _f.id)) {
|
|
1207
|
+
// can't persist without an ID; keep local only
|
|
1208
|
+
this.currentValues = Object.assign({}, newCurrentValues);
|
|
1245
1209
|
}
|
|
1246
|
-
else {
|
|
1247
|
-
|
|
1210
|
+
else if (type === 'save') {
|
|
1211
|
+
const valueToPersist = result.valueToPersist;
|
|
1212
|
+
if (valueToPersist) {
|
|
1213
|
+
try {
|
|
1214
|
+
// If this comment already exists, update it; otherwise add it.
|
|
1215
|
+
if (existingSavedComment) {
|
|
1216
|
+
await this.formSubmissionHandler.updateComment(this.udpFormSubmission, {
|
|
1217
|
+
sectionKey: result.sectionKey,
|
|
1218
|
+
questionKey: result.questionKey,
|
|
1219
|
+
value: valueToPersist,
|
|
1220
|
+
commentId: result.commentId,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
await this.formSubmissionHandler.addComment(this.udpFormSubmission, {
|
|
1225
|
+
sectionKey: result.sectionKey,
|
|
1226
|
+
questionKey: result.questionKey,
|
|
1227
|
+
value: valueToPersist,
|
|
1228
|
+
commentId: result.commentId,
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
await this.refreshSubmissionAndSyncState();
|
|
1232
|
+
enqueueSnackbarSuccess(existingSavedComment ? 'Comment updated.' : 'Comment added.', this.enqueueSnackbar);
|
|
1233
|
+
}
|
|
1234
|
+
catch (error) {
|
|
1235
|
+
enqueueSnackbarError(existingSavedComment ? 'Failed to update comment.' : 'Failed to add comment.', this.enqueueSnackbar);
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1248
1239
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
const draftFromSaved = Object.assign(Object.assign({}, saved), { isDraftComment: true, timestamp: null });
|
|
1259
|
-
// Add this draft at the front; keep saved comment intact so content doesn't disappear.
|
|
1260
|
-
newCurrentValues[sectionName][questionName].draftComments = [draftFromSaved, ...draftsArr];
|
|
1261
|
-
newCurrentValues[sectionName][questionName].comments = commentsArr;
|
|
1262
|
-
break;
|
|
1263
|
-
}
|
|
1264
|
-
case 'delete': {
|
|
1265
|
-
// mark the comment as deleted and clear its value, then persist.
|
|
1266
|
-
newCurrentValues[sectionName][questionName].comments = commentsArr.map(c => {
|
|
1267
|
-
if (c.commentId === commentId) {
|
|
1268
|
-
return Object.assign(Object.assign({}, c), { isDeleted: true, value: '', editedTimestamp: new Date().toISOString() });
|
|
1240
|
+
else if (type === 'delete') {
|
|
1241
|
+
try {
|
|
1242
|
+
await this.formSubmissionHandler.deleteComment(this.udpFormSubmission, {
|
|
1243
|
+
sectionKey: result.sectionKey,
|
|
1244
|
+
questionKey: result.questionKey,
|
|
1245
|
+
commentId: result.commentId,
|
|
1246
|
+
});
|
|
1247
|
+
await this.refreshSubmissionAndSyncState();
|
|
1248
|
+
enqueueSnackbarSuccess('Comment deleted.', this.enqueueSnackbar);
|
|
1269
1249
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1250
|
+
catch (error) {
|
|
1251
|
+
enqueueSnackbarError('Failed to delete comment.', this.enqueueSnackbar);
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1273
1255
|
}
|
|
1274
|
-
|
|
1275
|
-
//
|
|
1276
|
-
|
|
1277
|
-
|
|
1256
|
+
else {
|
|
1257
|
+
// Not submitted yet:
|
|
1258
|
+
// - public: persist nothing beyond local state (submission is created on submit)
|
|
1259
|
+
// - private + owner: comment edits are persisted via standard submission save
|
|
1260
|
+
if (this.isPublic) {
|
|
1261
|
+
this.currentValues = Object.assign({}, newCurrentValues);
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
await this.handleManualSave(newCurrentValues);
|
|
1265
|
+
}
|
|
1278
1266
|
}
|
|
1279
1267
|
}
|
|
1280
|
-
// if it's a save or delete action, persist immediately
|
|
1281
|
-
if (type === 'save' || type === 'delete') {
|
|
1282
|
-
this.udpFormSubmission.data.submissionResponseData = Object.assign({}, newCurrentValues);
|
|
1283
|
-
await this.handleCommmentUpdate(newCurrentValues);
|
|
1284
|
-
}
|
|
1285
1268
|
else {
|
|
1269
|
+
// Non-persisting actions update UI state only
|
|
1286
1270
|
this.currentValues = Object.assign({}, newCurrentValues);
|
|
1287
|
-
this.udpFormSubmission.data.submissionResponseData = Object.assign({}, newCurrentValues);
|
|
1288
1271
|
}
|
|
1289
1272
|
}
|
|
1290
1273
|
catch (error) {
|
|
@@ -1297,7 +1280,6 @@ const UdpFormsRenderer = class {
|
|
|
1297
1280
|
async loadFollowUpForms(sectionKey, questionKey) {
|
|
1298
1281
|
var _a;
|
|
1299
1282
|
this.isLoading = true;
|
|
1300
|
-
this.saveErrorMessage = '';
|
|
1301
1283
|
try {
|
|
1302
1284
|
const response = await udpFormApiUtils.fetchLatestForms(this.followUpSideSheetListPageNumber, this.FOLLOW_UP_SIDE_SHEET_PAGE_SIZE, [{ searchField: 'type', searchOperator: '=', searchValue: enums.UdpFormsTypeEnum.FollowUp }], { sortDirection: 'DESC', sortColumn: 'lastModifiedOn' });
|
|
1303
1285
|
const data = response.data;
|
|
@@ -1309,7 +1291,6 @@ const UdpFormsRenderer = class {
|
|
|
1309
1291
|
}
|
|
1310
1292
|
catch (err) {
|
|
1311
1293
|
console.error('Failed to follow up form.', err);
|
|
1312
|
-
this.saveErrorMessage = 'Failed to follow up forms.';
|
|
1313
1294
|
}
|
|
1314
1295
|
finally {
|
|
1315
1296
|
this.isLoading = false;
|
|
@@ -1327,11 +1308,17 @@ const UdpFormsRenderer = class {
|
|
|
1327
1308
|
await this.loadFollowUpForms(this.followUpParentSectionKey, this.followUpParentQuestionKey);
|
|
1328
1309
|
}
|
|
1329
1310
|
renderFollowUpSideSheet() {
|
|
1311
|
+
if (this.isPublic)
|
|
1312
|
+
return null;
|
|
1330
1313
|
return (index.h("udp-side-sheet", { title: "Link a Follow Up Form", open: this.isFollowUpFormsSideSheetOpen, onUdpSideSheetClose: () => this.handleSideSheetClose(), position: "right", width: "md" }, this.isLoading && this.isFollowUpFormsSideSheetOpen && index.h("udp-linear-loader", null), index.h("udp-list-renderer", { itemComponent: "udp-forms-follow-up-list-card", data: this.sideSheetFollowUpFormsList, pagination: true, isServerSide: true, isLoading: this.isLoading, itemsPerPage: this.FOLLOW_UP_SIDE_SHEET_PAGE_SIZE, currentPage: this.followUpSideSheetListPageNumber, totalItems: this.followUpSideSheetTotalItems, onPageChange: e => this.handleSideSheetPageChange(e.detail), componentDataMap: this.componentMap, spacing: 'md' })));
|
|
1331
1314
|
}
|
|
1332
1315
|
isShowManualSaveIcon() {
|
|
1333
|
-
|
|
1334
|
-
|
|
1316
|
+
if (this.isPublic)
|
|
1317
|
+
return false; // cannot save a form in public mode. can only submit.
|
|
1318
|
+
// NOTE: repeatable section add/delete sets hasUnsavedChanges=true, but udp-forms-ui likely
|
|
1319
|
+
// also emits a formDirtyChange event that can override the value back to false.
|
|
1320
|
+
// Keep the save icon sticky once we've detected any unsaved change.
|
|
1321
|
+
const showSaveIcon = !!this.clientUserInfo.id && (this.hasUnsavedChanges);
|
|
1335
1322
|
return showSaveIcon;
|
|
1336
1323
|
}
|
|
1337
1324
|
;
|
|
@@ -1339,7 +1326,8 @@ const UdpFormsRenderer = class {
|
|
|
1339
1326
|
return this.isSubmitted || this.udpFormSubmission.unityUserId !== this.clientUserInfo.id;
|
|
1340
1327
|
}
|
|
1341
1328
|
render() {
|
|
1342
|
-
return (index.h("div", { key: '
|
|
1329
|
+
return (index.h("div", { key: '37ec47fc7f3ecec70ba7ca3e12717789285be5d8', class: "forms-renderer-container", style: this.isLoading ? { minHeight: '100vh' } : {} }, this.renderFollowUpSideSheet(), index.h("udp-forms-ui", { udpForm: this.udpForm, currentValues: this.currentValues, udpFormSubmission: this.udpFormSubmission, submitSuccessful: this.submitSuccessful, isSaving: this.isSaving,
|
|
1330
|
+
// saveErrorMessage={this.saveErrorMessage}
|
|
1343
1331
|
// showAutoSaveStatus={this.formSubmissionHandler.supportsAutoSave() && !!this.userId}
|
|
1344
1332
|
readonly: this.isReadOnlyMode, handleSubmit: this.handleSubmit.bind(this), handleSave: values => Promise.resolve(this.debouncedManualSave(values)), handleChange: this.handleFormChange, handleAction: this.triggerAction, handleFinish: this.handleFinish, clientUserInfo: this.clientUserInfo, isSubmitted: this.isSubmitted, dynamicSections: this.dynamicSections, duplicateRepeatableSection: this.duplicateRepeatableSection, deleteRepeatableSection: this.deleteRepeatableSection, key: `form-rerender-key-${this.reRenderKey}`, isShowManualSaveIcon: this.isShowManualSaveIcon(), isLoading: this.isLoading, performBackgroundSaveAndUpdateLocalSubmissionState: this.performBackgroundSaveAndUpdateLocalSubmissionState })));
|
|
1345
1333
|
}
|