udp-stencil-component-library 25.18.2-beta.7 → 25.18.2-beta.9

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