udp-stencil-component-library 25.18.2-beta.7 → 25.18.2-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/stencil-library.cjs.js +1 -1
- package/dist/cjs/udp-forms-renderer.cjs.entry.js +591 -603
- package/dist/cjs/udp-forms-renderer.entry.cjs.js.map +1 -1
- package/dist/cjs/udp-forms-ui.cjs.entry.js +1 -4
- package/dist/cjs/udp-forms-ui.entry.cjs.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-ui/udp-forms-ui.js +1 -4
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-ui/udp-forms-ui.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.js +153 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.js.map +1 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.js +104 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.js.map +1 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/{udp-forms-renderer-utils.js → udp-forms-renderer-utils/utils.js} +48 -2
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/utils.js.map +1 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.js +187 -310
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.js +13 -13
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.js +142 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.js.map +1 -0
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.js +3 -10
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/IFormSubmissionHandler.js.map +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/types.js.map +1 -1
- package/dist/components/udp-forms-renderer.js +593 -608
- package/dist/components/udp-forms-renderer.js.map +1 -1
- package/dist/components/udp-forms-ui2.js +1 -4
- package/dist/components/udp-forms-ui2.js.map +1 -1
- package/dist/docs.json +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/stencil-library.js +1 -1
- package/dist/esm/udp-forms-renderer.entry.js +592 -604
- package/dist/esm/udp-forms-renderer.entry.js.map +1 -1
- package/dist/esm/udp-forms-ui.entry.js +1 -4
- package/dist/esm/udp-forms-ui.entry.js.map +1 -1
- package/dist/stencil-library/stencil-library.esm.js +1 -1
- package/dist/stencil-library/udp-forms-renderer.entry.esm.js.map +1 -1
- package/dist/stencil-library/udp-forms-renderer.entry.js +1 -1
- package/dist/stencil-library/udp-forms-renderer.entry.js.map +1 -1
- package/dist/stencil-library/udp-forms-ui.entry.esm.js.map +1 -1
- package/dist/stencil-library/udp-forms-ui.entry.js +1 -1
- package/dist/stencil-library/udp-forms-ui.entry.js.map +1 -1
- package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/comments-crud-utils.d.ts +31 -0
- package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils/repeated-section-utils.d.ts +17 -0
- package/dist/types/components/forms/udp-forms/udp-forms-renderer/{udp-forms-renderer-utils.d.ts → udp-forms-renderer-utils/utils.d.ts} +4 -0
- package/dist/types/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer.d.ts +6 -10
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-handler/UdpFormHandler.d.ts +5 -6
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandler.d.ts +42 -0
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/FormSubmissionHandlerFactory.d.ts +1 -1
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/IFormSubmissionHandler.d.ts +44 -5
- package/dist/types/components/forms/udp-forms/udp-forms-utils/types.d.ts +16 -0
- package/package.json +1 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-renderer/udp-forms-renderer-utils.js.map +0 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.js +0 -264
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.js.map +0 -1
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PublicFormSubmissionHandler.js +0 -63
- package/dist/collection/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PublicFormSubmissionHandler.js.map +0 -1
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PrivateFormSubmissionHandler.d.ts +0 -131
- package/dist/types/components/forms/udp-forms/udp-forms-utils/form-submission-handler/PublicFormSubmissionHandler.d.ts +0 -15
|
@@ -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,
|
|
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
|
-
*
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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:
|
|
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
|
|
431
|
-
const
|
|
432
|
-
|
|
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('
|
|
441
|
-
|
|
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
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
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('
|
|
471
|
-
|
|
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
|
|
481
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
838
|
-
this.currentValues = Object.assign({},
|
|
839
|
-
this.udpFormSubmission.data.submissionResponseData = Object.assign({},
|
|
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.
|
|
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
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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 =
|
|
894
|
-
this.currentValues = Object.assign({},
|
|
895
|
-
this.udpFormSubmission.data.submissionResponseData = Object.assign({},
|
|
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.
|
|
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
|
-
|
|
909
|
-
|
|
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
|
-
|
|
912
|
-
this.
|
|
913
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
961
|
-
|
|
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
|
-
|
|
969
|
-
this.
|
|
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
|
-
|
|
995
|
+
replaceUrlWithSubmissionId(this.udpFormSubmission.id, this.history);
|
|
974
996
|
}
|
|
975
|
-
|
|
976
|
-
variant: 'success',
|
|
977
|
-
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
|
978
|
-
});
|
|
997
|
+
enqueueSnackbarSuccess('Form saved successfully.', this.enqueueSnackbar);
|
|
979
998
|
}
|
|
980
999
|
catch (error) {
|
|
981
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
this.clientUserInfo.
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1158
|
+
await this.formSubmissionHandler.finalizeFormSubmissionState(values, this.udpFormSubmission);
|
|
1159
|
+
await this.refreshSubmissionAndSyncState();
|
|
1136
1160
|
this.submitSuccessful = true;
|
|
1137
1161
|
}
|
|
1138
1162
|
catch (error) {
|
|
1139
|
-
|
|
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
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
enqueueSnackbarError('Failed to delete comment.', this.enqueueSnackbar);
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1271
1253
|
}
|
|
1272
|
-
|
|
1273
|
-
//
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
-
|
|
1332
|
-
|
|
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: '
|
|
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
|
}
|