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.
Files changed (73) hide show
  1. package/dist/cjs/index.cjs.js +138 -0
  2. package/dist/cjs/index.cjs.js.map +1 -1
  3. package/dist/cjs/loader.cjs.js +1 -1
  4. package/dist/cjs/stencil-library.cjs.js +1 -1
  5. package/dist/cjs/udp-forms-renderer.cjs.entry.js +591 -603
  6. package/dist/cjs/udp-forms-renderer.entry.cjs.js.map +1 -1
  7. package/dist/cjs/udp-forms-ui.cjs.entry.js +1 -4
  8. package/dist/cjs/udp-forms-ui.entry.cjs.js.map +1 -1
  9. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-ui/udp-forms-ui.js +1 -4
  10. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-ui/udp-forms-ui.js.map +1 -1
  11. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.js +153 -0
  12. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.js.map +1 -0
  13. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.js +104 -0
  14. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.js.map +1 -0
  15. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/{udp-forms-renderer-utils.js → udp-forms-renderer-utils/utils.js} +48 -2
  16. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/utils.js.map +1 -0
  17. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.js +187 -310
  18. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.js.map +1 -1
  19. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.js +13 -13
  20. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.js.map +1 -1
  21. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.js +142 -0
  22. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.js.map +1 -0
  23. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.js +3 -10
  24. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.js.map +1 -1
  25. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/IFormSubmissionHandler.js.map +1 -1
  26. package/dist/collection/components/forms/udp-forms/udp-forms-utils/types.js.map +1 -1
  27. package/dist/collection/index.js +1 -0
  28. package/dist/collection/index.js.map +1 -1
  29. package/dist/collection/udp-utilities/udp-websocket-client/udp-websocket-client.js +137 -0
  30. package/dist/collection/udp-utilities/udp-websocket-client/udp-websocket-client.js.map +1 -0
  31. package/dist/components/index.js +138 -1
  32. package/dist/components/index.js.map +1 -1
  33. package/dist/components/udp-forms-renderer.js +593 -608
  34. package/dist/components/udp-forms-renderer.js.map +1 -1
  35. package/dist/components/udp-forms-ui2.js +1 -4
  36. package/dist/components/udp-forms-ui2.js.map +1 -1
  37. package/dist/docs.json +1 -1
  38. package/dist/esm/index.js +138 -1
  39. package/dist/esm/index.js.map +1 -1
  40. package/dist/esm/loader.js +1 -1
  41. package/dist/esm/stencil-library.js +1 -1
  42. package/dist/esm/udp-forms-renderer.entry.js +592 -604
  43. package/dist/esm/udp-forms-renderer.entry.js.map +1 -1
  44. package/dist/esm/udp-forms-ui.entry.js +1 -4
  45. package/dist/esm/udp-forms-ui.entry.js.map +1 -1
  46. package/dist/stencil-library/index.esm.js +1 -1
  47. package/dist/stencil-library/index.esm.js.map +1 -1
  48. package/dist/stencil-library/stencil-library.esm.js +1 -1
  49. package/dist/stencil-library/udp-forms-renderer.entry.esm.js.map +1 -1
  50. package/dist/stencil-library/udp-forms-renderer.entry.js +1 -1
  51. package/dist/stencil-library/udp-forms-renderer.entry.js.map +1 -1
  52. package/dist/stencil-library/udp-forms-ui.entry.esm.js.map +1 -1
  53. package/dist/stencil-library/udp-forms-ui.entry.js +1 -1
  54. package/dist/stencil-library/udp-forms-ui.entry.js.map +1 -1
  55. package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.d.ts +31 -0
  56. package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.d.ts +17 -0
  57. 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
  58. package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.d.ts +6 -10
  59. package/dist/types/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.d.ts +5 -6
  60. package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.d.ts +42 -0
  61. package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.d.ts +1 -1
  62. package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/IFormSubmissionHandler.d.ts +44 -5
  63. package/dist/types/components/forms/udp-forms/udp-forms-utils/types.d.ts +16 -0
  64. package/dist/types/index.d.ts +1 -0
  65. package/dist/types/udp-utilities/udp-websocket-client/udp-websocket-client.d.ts +34 -0
  66. package/package.json +1 -1
  67. package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils.js.map +0 -1
  68. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.js +0 -264
  69. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.js.map +0 -1
  70. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PublicFormSubmissionHandler.js +0 -63
  71. package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PublicFormSubmissionHandler.js.map +0 -1
  72. package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.d.ts +0 -131
  73. 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
- * Handles CRUD and submission operations for private UdpFormSubmission records.
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 PrivateFormSubmissionHandler {
223
- // =============================
224
- // Fetch Methods
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
- console.warn('Cannot fetch form submission without an ID.');
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
- * Internal helper lookup a form submission by its unique id.
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
- catch (error) {
281
- console.error('Error fetching form submission by ID:', error);
282
- return null;
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
- * Internal helper lookup a form submission by id and generic fields.
287
- *
288
- * Adds optional equality filters for generic1/generic2/generic3 when provided,
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
- catch (error) {
315
- console.error('Error fetching form submission by ID:', error);
316
- return null;
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
- const formData = await this.saveFormDataWithoutValues(udpFormSubmission, enums.UdpFormsSubmissionStatusEnum.InProgress);
333
- return formData;
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
- * Determine correct HTTP verb and target URL for saving a UdpFormSubmission.
405
- *
406
- * Uses productV1ApiUrl from ConfigService and returns POST for new submissions
407
- * (no id) or PUT with the resource id for existing submissions.
408
- *
409
- * @param udpFormSubmission - submission to be saved
410
- * @returns object with method ('POST'|'PUT') and full url
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: `${baseUrl}/${udpFormSubmission.id}` }
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 formData = udpFormSubmission.processSubmissionIntoFormData(values, status);
433
- const { method, url } = this.getApiRequestInfo(udpFormSubmission);
434
- const response = await makeApiCall.makeApiCall(method, url, formData, true);
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('Failed to save form udpFormSubmission:', error);
443
- throw error;
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 method = 'PUT';
461
- const status = (_a = udpFormSubmission.status) !== null && _a !== void 0 ? _a : enums.UdpFormsSubmissionStatusEnum.Submitted;
462
- const url = `${configService.ConfigService.productV1ApiUrl}/UdpFormSubmission/${udpFormSubmission.id}`;
463
- const formData = udpFormSubmission.processSubmissionIntoFormData(values, status);
464
- const response = await makeApiCall.makeApiCall(method, url, formData, true);
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('Failed to save form udpFormSubmission:', error);
473
- throw error;
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, formId, version, tenantId) {
483
- if (isPublic) {
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
- // Fetching Form
502
- // =============================
503
- async getFormByFormId(formId) {
504
- const formData = await makeApiCall.makeApiCall('GET', `${configService.ConfigService.productV1ApiUrl}/UdpForm/${formId}/describe`);
505
- if (formData.styleOverrides && typeof formData.styleOverrides === 'string') {
506
- formData.styleOverrides = JSON.parse(formData.styleOverrides);
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 formData = await makeApiCall.makeApiCall('GET', `${configService.ConfigService.productV1ApiUrl}/UdpForm/${formId}/${formVersion}/describe`);
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.isFormDirty = false;
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
- await this.performBackgroundSaveAndUpdateLocalSubmissionState(this.udpFormSubmission.data.submissionResponseData);
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 sectionToClone = this.dynamicSections[index];
780
- if (!sectionToClone)
781
- return;
782
- const cloningSectionName = sectionToClone.name;
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
- // This will trigger a complete form re-render
840
- this.currentValues = Object.assign({}, newCurrentValues);
841
- this.udpFormSubmission.data.submissionResponseData = Object.assign({}, newCurrentValues);
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.isUserUpdatedSections = true;
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 deleteSectionName = sectionToDelete.name;
863
- const deleteSuffix = sectionToDelete.sectionPositionSuffix;
864
- const sectionKeyToDelete = `${deleteSectionName}_${deleteSuffix}`;
865
- const newCurrentValues = Object.assign({}, this.udpFormSubmission.data.submissionResponseData);
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 = updatedSections;
896
- this.currentValues = Object.assign({}, newCurrentValues);
897
- this.udpFormSubmission.data.submissionResponseData = Object.assign({}, newCurrentValues);
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.isUserUpdatedSections = true;
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
- var _a;
911
- this.saveErrorMessage = null;
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
- const updatedUdpFormSubmission = await this.formSubmissionHandler.saveCurrentFormSubmissionState(values, this.udpFormSubmission);
914
- this.udpFormSubmission = updatedUdpFormSubmission;
915
- this.currentValues = Object.assign({}, (((_a = updatedUdpFormSubmission.data) === null || _a === void 0 ? void 0 : _a.submissionResponseData) || {}));
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
- this.replaceUrlWithSubmissionId(updatedUdpFormSubmission.id);
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.isUserUpdatedSections = false;
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
- this.handleCommmentUpdate = async (values) => {
934
- var _a;
935
- this.isSaving = true;
936
- this.saveErrorMessage = null;
937
- try {
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
- // Only save if handler supports it and user is authenticated
963
- // if (!this.formSubmissionHandler.supportsAutoSave() || !this.userId || !this.formSubmissionHandler.saveCurrentFormSubmissionState) {
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
- const updatedUdpFormSubmission = await this.formSubmissionHandler.saveCurrentFormSubmissionState(values, this.udpFormSubmission);
971
- this.udpFormSubmission = updatedUdpFormSubmission;
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
- this.replaceUrlWithSubmissionId(updatedUdpFormSubmission.id);
997
+ replaceUrlWithSubmissionId(this.udpFormSubmission.id, this.history);
976
998
  }
977
- this.enqueueSnackbar('Form saved successfully.', {
978
- variant: 'success',
979
- anchorOrigin: { vertical: 'top', horizontal: 'center' },
980
- });
999
+ enqueueSnackbarSuccess('Form saved successfully.', this.enqueueSnackbar);
981
1000
  }
982
1001
  catch (error) {
983
- this.enqueueSnackbar('There was an error saving this form', {
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.isUserUpdatedSections = false;
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
- this.isFormDirty = e.detail;
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
- const user = (_a = this.getUserCallback) === null || _a === void 0 ? void 0 : _a.call(this);
1069
- if (user) {
1070
- this.clientUserInfo.id = user.id || null;
1071
- this.clientUserInfo.displayName = user.name || null;
1072
- this.clientUserInfo.email = user.email || null;
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
- this.udpFormSubmission = await this.formSubmissionHandler.finalizeFormSubmissionState(values, this.udpFormSubmission);
1160
+ await this.formSubmissionHandler.finalizeFormSubmissionState(values, this.udpFormSubmission);
1161
+ await this.refreshSubmissionAndSyncState();
1138
1162
  this.submitSuccessful = true;
1139
1163
  }
1140
1164
  catch (error) {
1141
- this.enqueueSnackbar('There was an error submitting this form', {
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 [sectionName, questionName] = questionIdentifierKey.split('.');
1190
- let newCurrentValues = structuredClone(this.udpFormSubmission.data.submissionResponseData || {});
1191
- // ensure structure exists WITHOUT overwriting existing data
1192
- if (!newCurrentValues[sectionName])
1193
- newCurrentValues[sectionName] = {};
1194
- if (!newCurrentValues[sectionName][questionName]) {
1195
- // create defaults but do not clobber any existing saved shape from submissionResponseData
1196
- newCurrentValues[sectionName][questionName] = {
1197
- value: '',
1198
- comments: [],
1199
- draftComments: [],
1200
- metadata: {},
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
- const commentsArr = newCurrentValues[sectionName][questionName].comments || [];
1218
- const draftsArr = newCurrentValues[sectionName][questionName].draftComments || [];
1219
- switch (type) {
1220
- case 'add': {
1221
- const newDraft = {
1222
- value: '',
1223
- commentId: uuid.v4(),
1224
- isTempComment: true,
1225
- timestamp: null,
1226
- };
1227
- // put new draft first so activeDraft === draftComments[0] matches UX
1228
- newCurrentValues[sectionName][questionName].draftComments = [newDraft, ...draftsArr];
1229
- break;
1230
- }
1231
- case 'save': {
1232
- // find draft by id
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
- newCurrentValues[sectionName][questionName].comments = [...commentsArr, savedComment];
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
- // remove that draft
1250
- newCurrentValues[sectionName][questionName].draftComments = draftsArr.filter(d => d.commentId !== commentId);
1251
- break;
1252
- }
1253
- case 'edit': {
1254
- // create a draft copy of the saved comment but DO NOT remove the saved comment from the list.
1255
- const saved = commentsArr.find(c => c.commentId === commentId);
1256
- if (!saved)
1257
- return;
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
- return c;
1271
- });
1272
- break;
1250
+ catch (error) {
1251
+ enqueueSnackbarError('Failed to delete comment.', this.enqueueSnackbar);
1252
+ throw error;
1253
+ }
1254
+ }
1273
1255
  }
1274
- case 'editClose': {
1275
- // Cancel edit: remove the draft only, do NOT restore/alter saved comments.
1276
- newCurrentValues[sectionName][questionName].draftComments = draftsArr.filter(d => d.commentId !== commentId);
1277
- break;
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
- // return (this.isFormDirty || this.isUserUpdatedSections) && !this.isSubmitted && !!this.userId;
1334
- const showSaveIcon = (this.isFormDirty || this.isUserUpdatedSections) && !!this.clientUserInfo.id;
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: 'd073bfa6145e449c59dc722723ac82d09730d885', 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, saveErrorMessage: this.saveErrorMessage,
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
  }