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