@form-engine-ts/react 4.1.0 → 4.3.1
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/README.md +8 -1
- package/dist/index.cjs +120 -27
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +120 -27
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -168,8 +168,15 @@ state. Text controls forward schema `minLength`, `maxLength`, and `pattern` cons
|
|
|
168
168
|
`renderCharacterCount` can replace the default count. Guard evaluation, confirmation, receipt persistence, and provider
|
|
169
169
|
submission share an in-flight lock so rapid clicks cannot submit twice.
|
|
170
170
|
|
|
171
|
+
Set `submissionConfirmation={{ enabled: true, renderMode: "replace" }}` to show a standard answer review before
|
|
172
|
+
submission even when no submission guard is configured. The default `inline` mode keeps the form visible; `replace` and
|
|
173
|
+
`dialog` provide alternate presentations. The standard review lists visible answers with their resolved labels and
|
|
174
|
+
formatted display values. `renderSubmissionConfirmation` receives these as `visibleItems`; guard confirmations continue
|
|
175
|
+
to receive their findings, while generic confirmations provide an empty findings array.
|
|
176
|
+
|
|
171
177
|
The Builder basic-settings section edits source `title` and `description` through the same policy-aware action pipeline.
|
|
172
|
-
Submission confirmation slots receive the effective message, localized schema,
|
|
178
|
+
Submission confirmation slots receive the effective message, localized schema, visible answers, and formatted
|
|
179
|
+
`visibleItems`. An `onSubmit` result
|
|
173
180
|
may provide `submissionId` and `submittedAt`, which Renderer copies into its receipt. Receipt stores support `getBatch`,
|
|
174
181
|
and `useSubmissionReceipts` loads multiple form/version receipts for list and dashboard surfaces.
|
|
175
182
|
|
package/dist/index.cjs
CHANGED
|
@@ -169,6 +169,61 @@ function defaultCreateOption(field, id) {
|
|
|
169
169
|
function defaultCreatePage(id, questionIds) {
|
|
170
170
|
return { id, title: "New page", questionIds };
|
|
171
171
|
}
|
|
172
|
+
function applyFieldConstraintDefaults(field, policy) {
|
|
173
|
+
const constraint = policy?.fieldConstraints?.[field.type];
|
|
174
|
+
if (constraint === void 0) return field;
|
|
175
|
+
const required = constraint.fixedRequired ?? constraint.defaultRequired ?? field.required;
|
|
176
|
+
if (field.type === "rating") {
|
|
177
|
+
const ratingConstraint = "defaultMin" in constraint || "defaultMax" in constraint || "fixedMin" in constraint || "fixedMax" in constraint ? constraint : void 0;
|
|
178
|
+
const min = ratingConstraint !== void 0 && "fixedMin" in ratingConstraint ? ratingConstraint.fixedMin : ratingConstraint !== void 0 && "defaultMin" in ratingConstraint ? ratingConstraint.defaultMin : field.min;
|
|
179
|
+
const max = ratingConstraint !== void 0 && "fixedMax" in ratingConstraint ? ratingConstraint.fixedMax : ratingConstraint !== void 0 && "defaultMax" in ratingConstraint ? ratingConstraint.defaultMax : field.max;
|
|
180
|
+
return {
|
|
181
|
+
...field,
|
|
182
|
+
required,
|
|
183
|
+
...min === void 0 ? {} : { min },
|
|
184
|
+
...max === void 0 ? {} : { max }
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if ((field.type === "text" || field.type === "textarea") && "defaultMaxLength" in constraint) {
|
|
188
|
+
return {
|
|
189
|
+
...field,
|
|
190
|
+
required,
|
|
191
|
+
...constraint.defaultMaxLength === void 0 ? {} : { maxLength: constraint.defaultMaxLength }
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { ...field, required };
|
|
195
|
+
}
|
|
196
|
+
function fieldConstraintError(updated, policy) {
|
|
197
|
+
const constraint = policy?.fieldConstraints?.[updated.type];
|
|
198
|
+
if (constraint === void 0) return void 0;
|
|
199
|
+
if (constraint.fixedRequired !== void 0 && updated.required !== constraint.fixedRequired) {
|
|
200
|
+
return { type: "field_constraint_immutable" };
|
|
201
|
+
}
|
|
202
|
+
if (updated.type === "rating") {
|
|
203
|
+
if ("fixedMin" in constraint && constraint.fixedMin !== void 0 && updated.min !== constraint.fixedMin) {
|
|
204
|
+
return { type: "field_constraint_immutable" };
|
|
205
|
+
}
|
|
206
|
+
if ("fixedMax" in constraint && constraint.fixedMax !== void 0 && updated.max !== constraint.fixedMax) {
|
|
207
|
+
return { type: "field_constraint_immutable" };
|
|
208
|
+
}
|
|
209
|
+
if ("allowedMinRange" in constraint && constraint.allowedMinRange !== void 0 && typeof updated.min === "number" && (updated.min < constraint.allowedMinRange[0] || updated.min > constraint.allowedMinRange[1])) {
|
|
210
|
+
return { type: "field_constraint_violation", property: "min", expected: constraint.allowedMinRange[0] };
|
|
211
|
+
}
|
|
212
|
+
if ("allowedMaxRange" in constraint && constraint.allowedMaxRange !== void 0 && typeof updated.max === "number" && (updated.max < constraint.allowedMaxRange[0] || updated.max > constraint.allowedMaxRange[1])) {
|
|
213
|
+
return { type: "field_constraint_violation", property: "max", expected: constraint.allowedMaxRange[1] };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if ((updated.type === "text" || updated.type === "textarea") && "maxMaxLength" in constraint && constraint.maxMaxLength !== void 0 && updated.maxLength !== void 0 && updated.maxLength > constraint.maxMaxLength) {
|
|
217
|
+
return { type: "field_constraint_violation", property: "maxLength", expected: constraint.maxMaxLength };
|
|
218
|
+
}
|
|
219
|
+
if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "minOptions" in constraint && constraint.minOptions !== void 0 && updated.options.length < constraint.minOptions) {
|
|
220
|
+
return { type: "field_constraint_violation", property: "options", expected: constraint.minOptions };
|
|
221
|
+
}
|
|
222
|
+
if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "maxOptions" in constraint && constraint.maxOptions !== void 0 && updated.options.length > constraint.maxOptions) {
|
|
223
|
+
return { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions };
|
|
224
|
+
}
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
172
227
|
function move(items, sourceIndex, targetIndex) {
|
|
173
228
|
if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
|
|
174
229
|
return void 0;
|
|
@@ -224,6 +279,8 @@ function useFormBuilder({
|
|
|
224
279
|
const updated = updater(current);
|
|
225
280
|
if (updated.id !== fieldId)
|
|
226
281
|
return { success: false, error: { type: "invalid_id", kind: "field", id: updated.id } };
|
|
282
|
+
const constraintError = fieldConstraintError(updated, policy);
|
|
283
|
+
if (constraintError !== void 0) return { success: false, error: constraintError };
|
|
227
284
|
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type))
|
|
228
285
|
return { success: false, error: { type: "disallowed_field_type", fieldType: updated.type } };
|
|
229
286
|
for (const text of [updated.title, updated.description]) {
|
|
@@ -235,7 +292,7 @@ function useFormBuilder({
|
|
|
235
292
|
onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
|
|
236
293
|
return { success: true };
|
|
237
294
|
},
|
|
238
|
-
[onChange, policy
|
|
295
|
+
[onChange, policy, schema, textPolicyError]
|
|
239
296
|
);
|
|
240
297
|
const updateOption = (0, import_react.useCallback)(
|
|
241
298
|
(fieldId, optionId, updater) => {
|
|
@@ -308,6 +365,7 @@ function useFormBuilder({
|
|
|
308
365
|
return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
|
|
309
366
|
field = { ...field, options: [option] };
|
|
310
367
|
}
|
|
368
|
+
field = applyFieldConstraintDefaults(field, policy);
|
|
311
369
|
const pages = schema.pages?.map((page, index) => ({
|
|
312
370
|
...page,
|
|
313
371
|
questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
|
|
@@ -360,6 +418,12 @@ function useFormBuilder({
|
|
|
360
418
|
return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
|
|
361
419
|
if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField)
|
|
362
420
|
return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
|
|
421
|
+
const constraint = policy?.fieldConstraints?.[field.type];
|
|
422
|
+
if (constraint !== void 0 && "maxOptions" in constraint && constraint.maxOptions !== void 0 && field.options.length >= constraint.maxOptions)
|
|
423
|
+
return {
|
|
424
|
+
success: false,
|
|
425
|
+
error: { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions }
|
|
426
|
+
};
|
|
363
427
|
const ids = new Set(
|
|
364
428
|
schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
|
|
365
429
|
);
|
|
@@ -376,7 +440,7 @@ function useFormBuilder({
|
|
|
376
440
|
});
|
|
377
441
|
return { success: true };
|
|
378
442
|
},
|
|
379
|
-
[createId, factories.createOption, onChange, policy?.maxOptionsPerField, schema]
|
|
443
|
+
[createId, factories.createOption, onChange, policy, policy?.maxOptionsPerField, schema]
|
|
380
444
|
);
|
|
381
445
|
const removeOption = (0, import_react.useCallback)(
|
|
382
446
|
(fieldId, optionId) => {
|
|
@@ -386,6 +450,12 @@ function useFormBuilder({
|
|
|
386
450
|
return { success: false, error: { type: "node_not_found", kind: "option", id: optionId } };
|
|
387
451
|
if (field.options.length <= 1)
|
|
388
452
|
return { success: false, error: { type: "invalid_operation", message: "A choice field needs one option." } };
|
|
453
|
+
const constraint = policy?.fieldConstraints?.[field.type];
|
|
454
|
+
if (constraint !== void 0 && "minOptions" in constraint && constraint.minOptions !== void 0 && field.options.length <= constraint.minOptions)
|
|
455
|
+
return {
|
|
456
|
+
success: false,
|
|
457
|
+
error: { type: "field_constraint_violation", property: "options", expected: constraint.minOptions }
|
|
458
|
+
};
|
|
389
459
|
onChange({
|
|
390
460
|
...schema,
|
|
391
461
|
fields: schema.fields.map(
|
|
@@ -394,7 +464,7 @@ function useFormBuilder({
|
|
|
394
464
|
});
|
|
395
465
|
return { success: true };
|
|
396
466
|
},
|
|
397
|
-
[onChange, schema]
|
|
467
|
+
[onChange, policy, schema]
|
|
398
468
|
);
|
|
399
469
|
const moveOption = (0, import_react.useCallback)(
|
|
400
470
|
(fieldId, optionId, targetIndex) => {
|
|
@@ -425,7 +495,9 @@ function useFormBuilder({
|
|
|
425
495
|
if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
|
|
426
496
|
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type))
|
|
427
497
|
return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
|
|
428
|
-
let transformed = (0, import_core.transformFieldType)(field, type);
|
|
498
|
+
let transformed = applyFieldConstraintDefaults((0, import_core.transformFieldType)(field, type), policy);
|
|
499
|
+
const constraintError = fieldConstraintError(transformed, policy);
|
|
500
|
+
if (constraintError !== void 0) return { success: false, error: constraintError };
|
|
429
501
|
if (CHOICE_TYPES.includes(type) && !("options" in field) && "options" in transformed) {
|
|
430
502
|
const ids = new Set(
|
|
431
503
|
schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
|
|
@@ -440,7 +512,7 @@ function useFormBuilder({
|
|
|
440
512
|
onChange({ ...schema, fields: schema.fields.map((item) => item.id === fieldId ? transformed : item) });
|
|
441
513
|
return { success: true };
|
|
442
514
|
},
|
|
443
|
-
[createId, factories.createOption, onChange, policy?.allowedFieldTypes, schema]
|
|
515
|
+
[createId, factories.createOption, onChange, policy, policy?.allowedFieldTypes, schema]
|
|
444
516
|
);
|
|
445
517
|
const addPage = (0, import_react.useCallback)(
|
|
446
518
|
(questionId) => {
|
|
@@ -3495,7 +3567,8 @@ function ContextFormRenderer({
|
|
|
3495
3567
|
beforeSubmit,
|
|
3496
3568
|
onDraftSave,
|
|
3497
3569
|
successRenderMode = "append",
|
|
3498
|
-
|
|
3570
|
+
submissionConfirmation,
|
|
3571
|
+
submissionConfirmationRenderMode,
|
|
3499
3572
|
showHiddenFieldsInSummary = false,
|
|
3500
3573
|
fieldsClassName,
|
|
3501
3574
|
hideFormOnSuccess = false,
|
|
@@ -3531,6 +3604,12 @@ function ContextFormRenderer({
|
|
|
3531
3604
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
3532
3605
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
3533
3606
|
const visibleValues = (0, import_react5.useMemo)(() => (0, import_core4.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
|
|
3607
|
+
const visibleItems = (0, import_react5.useMemo)(
|
|
3608
|
+
() => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
|
|
3609
|
+
[form.schema, form.translate, form.values, form.visibility]
|
|
3610
|
+
);
|
|
3611
|
+
const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? "inline";
|
|
3612
|
+
const confirmationEnabled = submissionConfirmation?.enabled === true;
|
|
3534
3613
|
const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
|
|
3535
3614
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
3536
3615
|
const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
|
|
@@ -3598,7 +3677,7 @@ function ContextFormRenderer({
|
|
|
3598
3677
|
if (confirmation === null) return;
|
|
3599
3678
|
const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
|
|
3600
3679
|
confirmButton?.focus();
|
|
3601
|
-
if (
|
|
3680
|
+
if (confirmationRenderMode !== "dialog") return;
|
|
3602
3681
|
const onKeyDown = (event) => {
|
|
3603
3682
|
if (event.key === "Escape") {
|
|
3604
3683
|
event.preventDefault();
|
|
@@ -3625,7 +3704,7 @@ function ContextFormRenderer({
|
|
|
3625
3704
|
};
|
|
3626
3705
|
globalThis.addEventListener("keydown", onKeyDown);
|
|
3627
3706
|
return () => globalThis.removeEventListener("keydown", onKeyDown);
|
|
3628
|
-
}, [confirmation,
|
|
3707
|
+
}, [confirmation, confirmationRenderMode, focusSubmitButton]);
|
|
3629
3708
|
(0, import_react5.useEffect)(() => {
|
|
3630
3709
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
3631
3710
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
@@ -3696,21 +3775,29 @@ function ContextFormRenderer({
|
|
|
3696
3775
|
}
|
|
3697
3776
|
const validation = (0, import_core4.validateAnswers)(form.schema, form.values);
|
|
3698
3777
|
const firstInvalidFieldId = validation.issues[0]?.fieldId;
|
|
3699
|
-
if (validation.valid && !guardsConfirmed
|
|
3778
|
+
if (validation.valid && !guardsConfirmed) {
|
|
3700
3779
|
rendererSubmissionInFlight.current = true;
|
|
3701
|
-
setGuardsPending(
|
|
3780
|
+
setGuardsPending(submissionGuards.length > 0);
|
|
3702
3781
|
try {
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3782
|
+
if (submissionGuards.length > 0) {
|
|
3783
|
+
const guardResult = await runSubmissionGuards(submissionGuards);
|
|
3784
|
+
if (guardResult.status === "block") {
|
|
3785
|
+
setGuardMessage(guardResult.message ?? form.translate("form.submissionBlocked"));
|
|
3786
|
+
return { status: "cancelled" };
|
|
3787
|
+
}
|
|
3788
|
+
if (guardResult.status === "confirm") {
|
|
3789
|
+
setGuardMessage(null);
|
|
3790
|
+
setConfirmation({
|
|
3791
|
+
findings: guardResult.findings,
|
|
3792
|
+
generic: false,
|
|
3793
|
+
...guardResult.message === void 0 ? {} : { message: guardResult.message }
|
|
3794
|
+
});
|
|
3795
|
+
return { status: "cancelled" };
|
|
3796
|
+
}
|
|
3707
3797
|
}
|
|
3708
|
-
if (
|
|
3798
|
+
if (confirmationEnabled) {
|
|
3709
3799
|
setGuardMessage(null);
|
|
3710
|
-
setConfirmation({
|
|
3711
|
-
findings: guardResult.findings,
|
|
3712
|
-
...guardResult.message === void 0 ? {} : { message: guardResult.message }
|
|
3713
|
-
});
|
|
3800
|
+
setConfirmation({ findings: [], generic: true });
|
|
3714
3801
|
return { status: "cancelled" };
|
|
3715
3802
|
}
|
|
3716
3803
|
} finally {
|
|
@@ -3873,17 +3960,18 @@ function ContextFormRenderer({
|
|
|
3873
3960
|
{
|
|
3874
3961
|
ref: confirmationRef,
|
|
3875
3962
|
className: "fe-submission-confirmation",
|
|
3876
|
-
role:
|
|
3963
|
+
role: confirmationRenderMode === "dialog" ? void 0 : "dialog",
|
|
3877
3964
|
children: slots.renderSubmissionConfirmation?.({
|
|
3878
3965
|
findings: confirmation?.findings ?? [],
|
|
3879
|
-
message: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")),
|
|
3966
|
+
message: confirmation?.message ?? (confirmation?.generic === true ? form.locale.toLowerCase().startsWith("ja") ? "\u56DE\u7B54\u5185\u5BB9\u3092\u3054\u78BA\u8A8D\u306E\u3046\u3048\u3001\u9001\u4FE1\u3057\u3066\u304F\u3060\u3055\u3044\u3002" : "Please review your answers before submitting." : resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData"))),
|
|
3880
3967
|
schema: form.schema,
|
|
3881
3968
|
visibleValues,
|
|
3969
|
+
visibleItems,
|
|
3882
3970
|
onConfirm: confirmSubmission,
|
|
3883
3971
|
onCancel: cancelSubmission
|
|
3884
3972
|
}) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
3885
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { children: resolveMessage("confirmSensitiveDataTitle") }),
|
|
3886
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")) }),
|
|
3973
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { children: confirmation?.generic === true ? form.locale.toLowerCase().startsWith("ja") ? "\u56DE\u7B54\u5185\u5BB9\u306E\u78BA\u8A8D" : "Review your answers" : resolveMessage("confirmSensitiveDataTitle") }),
|
|
3974
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation?.message ?? (confirmation?.generic === true ? form.locale.toLowerCase().startsWith("ja") ? "\u56DE\u7B54\u5185\u5BB9\u3092\u3054\u78BA\u8A8D\u306E\u3046\u3048\u3001\u9001\u4FE1\u3057\u3066\u304F\u3060\u3055\u3044\u3002" : "Please review your answers before submitting." : resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData"))) }),
|
|
3887
3975
|
(confirmation?.findings ?? []).length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { children: (confirmation?.findings ?? []).map((finding, index) => {
|
|
3888
3976
|
const field = form.schema.fields.find((candidate) => candidate.id === finding.fieldId);
|
|
3889
3977
|
const typeLabels = form.locale.toLowerCase().startsWith("ja") ? { email: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9", phone: "\u96FB\u8A71\u756A\u53F7", url: "URL", postal_code: "\u90F5\u4FBF\u756A\u53F7" } : { email: "Email address", phone: "Phone number", url: "URL", postal_code: "Postal code" };
|
|
@@ -3899,6 +3987,11 @@ function ContextFormRenderer({
|
|
|
3899
3987
|
] })
|
|
3900
3988
|
] }, `${finding.fieldId}-${finding.type}-${finding.start ?? index}`);
|
|
3901
3989
|
}) }),
|
|
3990
|
+
confirmation?.generic === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: "fe-submission-summary", children: visibleItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { children: [
|
|
3991
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: item.title }),
|
|
3992
|
+
": ",
|
|
3993
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: item.displayValue })
|
|
3994
|
+
] }, item.fieldId)) }) : null,
|
|
3902
3995
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: resolveMessage("confirmButton", form.translate("form.confirmSubmission")) }),
|
|
3903
3996
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: resolveMessage("cancelButton", form.translate("form.cancelSubmission")) })
|
|
3904
3997
|
] })
|
|
@@ -3918,7 +4011,7 @@ function ContextFormRenderer({
|
|
|
3918
4011
|
if (form.submitStatus === "success" && isReplaceMode) {
|
|
3919
4012
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `fe-form ${className}`.trim(), children: completionRegion });
|
|
3920
4013
|
}
|
|
3921
|
-
if (confirmation !== null &&
|
|
4014
|
+
if (confirmation !== null && confirmationRenderMode === "replace") {
|
|
3922
4015
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `fe-form ${className}`.trim(), children: confirmationContent });
|
|
3923
4016
|
}
|
|
3924
4017
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
@@ -3929,7 +4022,7 @@ function ContextFormRenderer({
|
|
|
3929
4022
|
className: `fe-form ${className}`.trim(),
|
|
3930
4023
|
noValidate: true,
|
|
3931
4024
|
onSubmit: handleSubmit,
|
|
3932
|
-
"aria-hidden": confirmation !== null &&
|
|
4025
|
+
"aria-hidden": confirmation !== null && confirmationRenderMode === "dialog" ? true : void 0,
|
|
3933
4026
|
children: [
|
|
3934
4027
|
slots.renderHeader?.({
|
|
3935
4028
|
title: form.schema.title,
|
|
@@ -3998,7 +4091,7 @@ function ContextFormRenderer({
|
|
|
3998
4091
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: fieldClassName, children: fieldChildren });
|
|
3999
4092
|
})(),
|
|
4000
4093
|
guardMessage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: guardMessage }),
|
|
4001
|
-
confirmation !== null &&
|
|
4094
|
+
confirmation !== null && confirmationRenderMode === "inline" ? confirmationContent : null,
|
|
4002
4095
|
validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-validation-summary", role: "alert", children: [
|
|
4003
4096
|
validationIssues.length,
|
|
4004
4097
|
" validation error",
|
|
@@ -4053,7 +4146,7 @@ function ContextFormRenderer({
|
|
|
4053
4146
|
]
|
|
4054
4147
|
}
|
|
4055
4148
|
),
|
|
4056
|
-
confirmation !== null &&
|
|
4149
|
+
confirmation !== null && confirmationRenderMode === "dialog" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-confirmation-dialog-backdrop", role: "dialog", "aria-modal": "true", children: confirmationContent }) : null
|
|
4057
4150
|
] });
|
|
4058
4151
|
}
|
|
4059
4152
|
var RENDERER_MESSAGES = {
|
package/dist/index.d.cts
CHANGED
|
@@ -60,6 +60,12 @@ type BuilderActionError = {
|
|
|
60
60
|
} | {
|
|
61
61
|
readonly type: "max_locales_exceeded";
|
|
62
62
|
readonly max: number;
|
|
63
|
+
} | {
|
|
64
|
+
readonly type: "field_constraint_immutable";
|
|
65
|
+
} | {
|
|
66
|
+
readonly type: "field_constraint_violation";
|
|
67
|
+
readonly property: string;
|
|
68
|
+
readonly expected: number;
|
|
63
69
|
} | {
|
|
64
70
|
readonly type: "node_not_found";
|
|
65
71
|
readonly kind: BuilderTextTarget["kind"];
|
|
@@ -472,6 +478,10 @@ type SubmissionGuardResult = {
|
|
|
472
478
|
type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
|
|
473
479
|
type FormSuccessRenderMode = "append" | "replace";
|
|
474
480
|
type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
|
|
481
|
+
interface SubmissionConfirmationOptions {
|
|
482
|
+
readonly enabled?: boolean;
|
|
483
|
+
readonly renderMode?: SubmissionConfirmationRenderMode;
|
|
484
|
+
}
|
|
475
485
|
type FormSubmitStatus = "idle" | "submitting" | "confirming" | "success" | "error";
|
|
476
486
|
/** @deprecated Use FormSubmitStatus instead. */
|
|
477
487
|
type FormSubmitState = "idle" | "submitting" | "confirming" | "success" | "error";
|
|
@@ -482,10 +492,11 @@ interface RenderSubmitButtonProps {
|
|
|
482
492
|
readonly onSubmit: () => void;
|
|
483
493
|
}
|
|
484
494
|
interface SubmissionConfirmationSlotProps {
|
|
485
|
-
readonly findings
|
|
495
|
+
readonly findings?: readonly SensitiveDataFinding[];
|
|
486
496
|
readonly message?: string;
|
|
487
497
|
readonly schema: FormSchema;
|
|
488
498
|
readonly visibleValues: Record<string, unknown>;
|
|
499
|
+
readonly visibleItems?: readonly FormSubmittedAnswerItem[];
|
|
489
500
|
readonly onConfirm: () => void;
|
|
490
501
|
readonly onCancel: () => void;
|
|
491
502
|
}
|
|
@@ -665,6 +676,8 @@ interface FormRendererPresentationProps extends SubmissionProtectionProps {
|
|
|
665
676
|
* Defaults to "append" for backwards compatibility.
|
|
666
677
|
*/
|
|
667
678
|
readonly successRenderMode?: FormSuccessRenderMode;
|
|
679
|
+
readonly submissionConfirmation?: SubmissionConfirmationOptions;
|
|
680
|
+
/** @deprecated Use submissionConfirmation.renderMode instead. */
|
|
668
681
|
readonly submissionConfirmationRenderMode?: SubmissionConfirmationRenderMode;
|
|
669
682
|
readonly showHiddenFieldsInSummary?: boolean;
|
|
670
683
|
readonly fieldsClassName?: string;
|
|
@@ -691,4 +704,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
691
704
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
692
705
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
693
706
|
|
|
694
|
-
export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
|
707
|
+
export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ type BuilderActionError = {
|
|
|
60
60
|
} | {
|
|
61
61
|
readonly type: "max_locales_exceeded";
|
|
62
62
|
readonly max: number;
|
|
63
|
+
} | {
|
|
64
|
+
readonly type: "field_constraint_immutable";
|
|
65
|
+
} | {
|
|
66
|
+
readonly type: "field_constraint_violation";
|
|
67
|
+
readonly property: string;
|
|
68
|
+
readonly expected: number;
|
|
63
69
|
} | {
|
|
64
70
|
readonly type: "node_not_found";
|
|
65
71
|
readonly kind: BuilderTextTarget["kind"];
|
|
@@ -472,6 +478,10 @@ type SubmissionGuardResult = {
|
|
|
472
478
|
type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
|
|
473
479
|
type FormSuccessRenderMode = "append" | "replace";
|
|
474
480
|
type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
|
|
481
|
+
interface SubmissionConfirmationOptions {
|
|
482
|
+
readonly enabled?: boolean;
|
|
483
|
+
readonly renderMode?: SubmissionConfirmationRenderMode;
|
|
484
|
+
}
|
|
475
485
|
type FormSubmitStatus = "idle" | "submitting" | "confirming" | "success" | "error";
|
|
476
486
|
/** @deprecated Use FormSubmitStatus instead. */
|
|
477
487
|
type FormSubmitState = "idle" | "submitting" | "confirming" | "success" | "error";
|
|
@@ -482,10 +492,11 @@ interface RenderSubmitButtonProps {
|
|
|
482
492
|
readonly onSubmit: () => void;
|
|
483
493
|
}
|
|
484
494
|
interface SubmissionConfirmationSlotProps {
|
|
485
|
-
readonly findings
|
|
495
|
+
readonly findings?: readonly SensitiveDataFinding[];
|
|
486
496
|
readonly message?: string;
|
|
487
497
|
readonly schema: FormSchema;
|
|
488
498
|
readonly visibleValues: Record<string, unknown>;
|
|
499
|
+
readonly visibleItems?: readonly FormSubmittedAnswerItem[];
|
|
489
500
|
readonly onConfirm: () => void;
|
|
490
501
|
readonly onCancel: () => void;
|
|
491
502
|
}
|
|
@@ -665,6 +676,8 @@ interface FormRendererPresentationProps extends SubmissionProtectionProps {
|
|
|
665
676
|
* Defaults to "append" for backwards compatibility.
|
|
666
677
|
*/
|
|
667
678
|
readonly successRenderMode?: FormSuccessRenderMode;
|
|
679
|
+
readonly submissionConfirmation?: SubmissionConfirmationOptions;
|
|
680
|
+
/** @deprecated Use submissionConfirmation.renderMode instead. */
|
|
668
681
|
readonly submissionConfirmationRenderMode?: SubmissionConfirmationRenderMode;
|
|
669
682
|
readonly showHiddenFieldsInSummary?: boolean;
|
|
670
683
|
readonly fieldsClassName?: string;
|
|
@@ -691,4 +704,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
691
704
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
692
705
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
693
706
|
|
|
694
|
-
export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
|
707
|
+
export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
package/dist/index.js
CHANGED
|
@@ -133,6 +133,61 @@ function defaultCreateOption(field, id) {
|
|
|
133
133
|
function defaultCreatePage(id, questionIds) {
|
|
134
134
|
return { id, title: "New page", questionIds };
|
|
135
135
|
}
|
|
136
|
+
function applyFieldConstraintDefaults(field, policy) {
|
|
137
|
+
const constraint = policy?.fieldConstraints?.[field.type];
|
|
138
|
+
if (constraint === void 0) return field;
|
|
139
|
+
const required = constraint.fixedRequired ?? constraint.defaultRequired ?? field.required;
|
|
140
|
+
if (field.type === "rating") {
|
|
141
|
+
const ratingConstraint = "defaultMin" in constraint || "defaultMax" in constraint || "fixedMin" in constraint || "fixedMax" in constraint ? constraint : void 0;
|
|
142
|
+
const min = ratingConstraint !== void 0 && "fixedMin" in ratingConstraint ? ratingConstraint.fixedMin : ratingConstraint !== void 0 && "defaultMin" in ratingConstraint ? ratingConstraint.defaultMin : field.min;
|
|
143
|
+
const max = ratingConstraint !== void 0 && "fixedMax" in ratingConstraint ? ratingConstraint.fixedMax : ratingConstraint !== void 0 && "defaultMax" in ratingConstraint ? ratingConstraint.defaultMax : field.max;
|
|
144
|
+
return {
|
|
145
|
+
...field,
|
|
146
|
+
required,
|
|
147
|
+
...min === void 0 ? {} : { min },
|
|
148
|
+
...max === void 0 ? {} : { max }
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if ((field.type === "text" || field.type === "textarea") && "defaultMaxLength" in constraint) {
|
|
152
|
+
return {
|
|
153
|
+
...field,
|
|
154
|
+
required,
|
|
155
|
+
...constraint.defaultMaxLength === void 0 ? {} : { maxLength: constraint.defaultMaxLength }
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { ...field, required };
|
|
159
|
+
}
|
|
160
|
+
function fieldConstraintError(updated, policy) {
|
|
161
|
+
const constraint = policy?.fieldConstraints?.[updated.type];
|
|
162
|
+
if (constraint === void 0) return void 0;
|
|
163
|
+
if (constraint.fixedRequired !== void 0 && updated.required !== constraint.fixedRequired) {
|
|
164
|
+
return { type: "field_constraint_immutable" };
|
|
165
|
+
}
|
|
166
|
+
if (updated.type === "rating") {
|
|
167
|
+
if ("fixedMin" in constraint && constraint.fixedMin !== void 0 && updated.min !== constraint.fixedMin) {
|
|
168
|
+
return { type: "field_constraint_immutable" };
|
|
169
|
+
}
|
|
170
|
+
if ("fixedMax" in constraint && constraint.fixedMax !== void 0 && updated.max !== constraint.fixedMax) {
|
|
171
|
+
return { type: "field_constraint_immutable" };
|
|
172
|
+
}
|
|
173
|
+
if ("allowedMinRange" in constraint && constraint.allowedMinRange !== void 0 && typeof updated.min === "number" && (updated.min < constraint.allowedMinRange[0] || updated.min > constraint.allowedMinRange[1])) {
|
|
174
|
+
return { type: "field_constraint_violation", property: "min", expected: constraint.allowedMinRange[0] };
|
|
175
|
+
}
|
|
176
|
+
if ("allowedMaxRange" in constraint && constraint.allowedMaxRange !== void 0 && typeof updated.max === "number" && (updated.max < constraint.allowedMaxRange[0] || updated.max > constraint.allowedMaxRange[1])) {
|
|
177
|
+
return { type: "field_constraint_violation", property: "max", expected: constraint.allowedMaxRange[1] };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if ((updated.type === "text" || updated.type === "textarea") && "maxMaxLength" in constraint && constraint.maxMaxLength !== void 0 && updated.maxLength !== void 0 && updated.maxLength > constraint.maxMaxLength) {
|
|
181
|
+
return { type: "field_constraint_violation", property: "maxLength", expected: constraint.maxMaxLength };
|
|
182
|
+
}
|
|
183
|
+
if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "minOptions" in constraint && constraint.minOptions !== void 0 && updated.options.length < constraint.minOptions) {
|
|
184
|
+
return { type: "field_constraint_violation", property: "options", expected: constraint.minOptions };
|
|
185
|
+
}
|
|
186
|
+
if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "maxOptions" in constraint && constraint.maxOptions !== void 0 && updated.options.length > constraint.maxOptions) {
|
|
187
|
+
return { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions };
|
|
188
|
+
}
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
136
191
|
function move(items, sourceIndex, targetIndex) {
|
|
137
192
|
if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
|
|
138
193
|
return void 0;
|
|
@@ -188,6 +243,8 @@ function useFormBuilder({
|
|
|
188
243
|
const updated = updater(current);
|
|
189
244
|
if (updated.id !== fieldId)
|
|
190
245
|
return { success: false, error: { type: "invalid_id", kind: "field", id: updated.id } };
|
|
246
|
+
const constraintError = fieldConstraintError(updated, policy);
|
|
247
|
+
if (constraintError !== void 0) return { success: false, error: constraintError };
|
|
191
248
|
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type))
|
|
192
249
|
return { success: false, error: { type: "disallowed_field_type", fieldType: updated.type } };
|
|
193
250
|
for (const text of [updated.title, updated.description]) {
|
|
@@ -199,7 +256,7 @@ function useFormBuilder({
|
|
|
199
256
|
onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
|
|
200
257
|
return { success: true };
|
|
201
258
|
},
|
|
202
|
-
[onChange, policy
|
|
259
|
+
[onChange, policy, schema, textPolicyError]
|
|
203
260
|
);
|
|
204
261
|
const updateOption = useCallback(
|
|
205
262
|
(fieldId, optionId, updater) => {
|
|
@@ -272,6 +329,7 @@ function useFormBuilder({
|
|
|
272
329
|
return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
|
|
273
330
|
field = { ...field, options: [option] };
|
|
274
331
|
}
|
|
332
|
+
field = applyFieldConstraintDefaults(field, policy);
|
|
275
333
|
const pages = schema.pages?.map((page, index) => ({
|
|
276
334
|
...page,
|
|
277
335
|
questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
|
|
@@ -324,6 +382,12 @@ function useFormBuilder({
|
|
|
324
382
|
return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
|
|
325
383
|
if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField)
|
|
326
384
|
return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
|
|
385
|
+
const constraint = policy?.fieldConstraints?.[field.type];
|
|
386
|
+
if (constraint !== void 0 && "maxOptions" in constraint && constraint.maxOptions !== void 0 && field.options.length >= constraint.maxOptions)
|
|
387
|
+
return {
|
|
388
|
+
success: false,
|
|
389
|
+
error: { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions }
|
|
390
|
+
};
|
|
327
391
|
const ids = new Set(
|
|
328
392
|
schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
|
|
329
393
|
);
|
|
@@ -340,7 +404,7 @@ function useFormBuilder({
|
|
|
340
404
|
});
|
|
341
405
|
return { success: true };
|
|
342
406
|
},
|
|
343
|
-
[createId, factories.createOption, onChange, policy?.maxOptionsPerField, schema]
|
|
407
|
+
[createId, factories.createOption, onChange, policy, policy?.maxOptionsPerField, schema]
|
|
344
408
|
);
|
|
345
409
|
const removeOption = useCallback(
|
|
346
410
|
(fieldId, optionId) => {
|
|
@@ -350,6 +414,12 @@ function useFormBuilder({
|
|
|
350
414
|
return { success: false, error: { type: "node_not_found", kind: "option", id: optionId } };
|
|
351
415
|
if (field.options.length <= 1)
|
|
352
416
|
return { success: false, error: { type: "invalid_operation", message: "A choice field needs one option." } };
|
|
417
|
+
const constraint = policy?.fieldConstraints?.[field.type];
|
|
418
|
+
if (constraint !== void 0 && "minOptions" in constraint && constraint.minOptions !== void 0 && field.options.length <= constraint.minOptions)
|
|
419
|
+
return {
|
|
420
|
+
success: false,
|
|
421
|
+
error: { type: "field_constraint_violation", property: "options", expected: constraint.minOptions }
|
|
422
|
+
};
|
|
353
423
|
onChange({
|
|
354
424
|
...schema,
|
|
355
425
|
fields: schema.fields.map(
|
|
@@ -358,7 +428,7 @@ function useFormBuilder({
|
|
|
358
428
|
});
|
|
359
429
|
return { success: true };
|
|
360
430
|
},
|
|
361
|
-
[onChange, schema]
|
|
431
|
+
[onChange, policy, schema]
|
|
362
432
|
);
|
|
363
433
|
const moveOption = useCallback(
|
|
364
434
|
(fieldId, optionId, targetIndex) => {
|
|
@@ -389,7 +459,9 @@ function useFormBuilder({
|
|
|
389
459
|
if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
|
|
390
460
|
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type))
|
|
391
461
|
return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
|
|
392
|
-
let transformed = transformFieldType(field, type);
|
|
462
|
+
let transformed = applyFieldConstraintDefaults(transformFieldType(field, type), policy);
|
|
463
|
+
const constraintError = fieldConstraintError(transformed, policy);
|
|
464
|
+
if (constraintError !== void 0) return { success: false, error: constraintError };
|
|
393
465
|
if (CHOICE_TYPES.includes(type) && !("options" in field) && "options" in transformed) {
|
|
394
466
|
const ids = new Set(
|
|
395
467
|
schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
|
|
@@ -404,7 +476,7 @@ function useFormBuilder({
|
|
|
404
476
|
onChange({ ...schema, fields: schema.fields.map((item) => item.id === fieldId ? transformed : item) });
|
|
405
477
|
return { success: true };
|
|
406
478
|
},
|
|
407
|
-
[createId, factories.createOption, onChange, policy?.allowedFieldTypes, schema]
|
|
479
|
+
[createId, factories.createOption, onChange, policy, policy?.allowedFieldTypes, schema]
|
|
408
480
|
);
|
|
409
481
|
const addPage = useCallback(
|
|
410
482
|
(questionId) => {
|
|
@@ -3478,7 +3550,8 @@ function ContextFormRenderer({
|
|
|
3478
3550
|
beforeSubmit,
|
|
3479
3551
|
onDraftSave,
|
|
3480
3552
|
successRenderMode = "append",
|
|
3481
|
-
|
|
3553
|
+
submissionConfirmation,
|
|
3554
|
+
submissionConfirmationRenderMode,
|
|
3482
3555
|
showHiddenFieldsInSummary = false,
|
|
3483
3556
|
fieldsClassName,
|
|
3484
3557
|
hideFormOnSuccess = false,
|
|
@@ -3514,6 +3587,12 @@ function ContextFormRenderer({
|
|
|
3514
3587
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
3515
3588
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
3516
3589
|
const visibleValues = useMemo4(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
|
|
3590
|
+
const visibleItems = useMemo4(
|
|
3591
|
+
() => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
|
|
3592
|
+
[form.schema, form.translate, form.values, form.visibility]
|
|
3593
|
+
);
|
|
3594
|
+
const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? "inline";
|
|
3595
|
+
const confirmationEnabled = submissionConfirmation?.enabled === true;
|
|
3517
3596
|
const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
|
|
3518
3597
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
3519
3598
|
const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
|
|
@@ -3581,7 +3660,7 @@ function ContextFormRenderer({
|
|
|
3581
3660
|
if (confirmation === null) return;
|
|
3582
3661
|
const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
|
|
3583
3662
|
confirmButton?.focus();
|
|
3584
|
-
if (
|
|
3663
|
+
if (confirmationRenderMode !== "dialog") return;
|
|
3585
3664
|
const onKeyDown = (event) => {
|
|
3586
3665
|
if (event.key === "Escape") {
|
|
3587
3666
|
event.preventDefault();
|
|
@@ -3608,7 +3687,7 @@ function ContextFormRenderer({
|
|
|
3608
3687
|
};
|
|
3609
3688
|
globalThis.addEventListener("keydown", onKeyDown);
|
|
3610
3689
|
return () => globalThis.removeEventListener("keydown", onKeyDown);
|
|
3611
|
-
}, [confirmation,
|
|
3690
|
+
}, [confirmation, confirmationRenderMode, focusSubmitButton]);
|
|
3612
3691
|
useEffect3(() => {
|
|
3613
3692
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
3614
3693
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
@@ -3679,21 +3758,29 @@ function ContextFormRenderer({
|
|
|
3679
3758
|
}
|
|
3680
3759
|
const validation = validateAnswers2(form.schema, form.values);
|
|
3681
3760
|
const firstInvalidFieldId = validation.issues[0]?.fieldId;
|
|
3682
|
-
if (validation.valid && !guardsConfirmed
|
|
3761
|
+
if (validation.valid && !guardsConfirmed) {
|
|
3683
3762
|
rendererSubmissionInFlight.current = true;
|
|
3684
|
-
setGuardsPending(
|
|
3763
|
+
setGuardsPending(submissionGuards.length > 0);
|
|
3685
3764
|
try {
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3765
|
+
if (submissionGuards.length > 0) {
|
|
3766
|
+
const guardResult = await runSubmissionGuards(submissionGuards);
|
|
3767
|
+
if (guardResult.status === "block") {
|
|
3768
|
+
setGuardMessage(guardResult.message ?? form.translate("form.submissionBlocked"));
|
|
3769
|
+
return { status: "cancelled" };
|
|
3770
|
+
}
|
|
3771
|
+
if (guardResult.status === "confirm") {
|
|
3772
|
+
setGuardMessage(null);
|
|
3773
|
+
setConfirmation({
|
|
3774
|
+
findings: guardResult.findings,
|
|
3775
|
+
generic: false,
|
|
3776
|
+
...guardResult.message === void 0 ? {} : { message: guardResult.message }
|
|
3777
|
+
});
|
|
3778
|
+
return { status: "cancelled" };
|
|
3779
|
+
}
|
|
3690
3780
|
}
|
|
3691
|
-
if (
|
|
3781
|
+
if (confirmationEnabled) {
|
|
3692
3782
|
setGuardMessage(null);
|
|
3693
|
-
setConfirmation({
|
|
3694
|
-
findings: guardResult.findings,
|
|
3695
|
-
...guardResult.message === void 0 ? {} : { message: guardResult.message }
|
|
3696
|
-
});
|
|
3783
|
+
setConfirmation({ findings: [], generic: true });
|
|
3697
3784
|
return { status: "cancelled" };
|
|
3698
3785
|
}
|
|
3699
3786
|
} finally {
|
|
@@ -3856,17 +3943,18 @@ function ContextFormRenderer({
|
|
|
3856
3943
|
{
|
|
3857
3944
|
ref: confirmationRef,
|
|
3858
3945
|
className: "fe-submission-confirmation",
|
|
3859
|
-
role:
|
|
3946
|
+
role: confirmationRenderMode === "dialog" ? void 0 : "dialog",
|
|
3860
3947
|
children: slots.renderSubmissionConfirmation?.({
|
|
3861
3948
|
findings: confirmation?.findings ?? [],
|
|
3862
|
-
message: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")),
|
|
3949
|
+
message: confirmation?.message ?? (confirmation?.generic === true ? form.locale.toLowerCase().startsWith("ja") ? "\u56DE\u7B54\u5185\u5BB9\u3092\u3054\u78BA\u8A8D\u306E\u3046\u3048\u3001\u9001\u4FE1\u3057\u3066\u304F\u3060\u3055\u3044\u3002" : "Please review your answers before submitting." : resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData"))),
|
|
3863
3950
|
schema: form.schema,
|
|
3864
3951
|
visibleValues,
|
|
3952
|
+
visibleItems,
|
|
3865
3953
|
onConfirm: confirmSubmission,
|
|
3866
3954
|
onCancel: cancelSubmission
|
|
3867
3955
|
}) ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
|
|
3868
|
-
/* @__PURE__ */ jsx3("h2", { children: resolveMessage("confirmSensitiveDataTitle") }),
|
|
3869
|
-
/* @__PURE__ */ jsx3("p", { children: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")) }),
|
|
3956
|
+
/* @__PURE__ */ jsx3("h2", { children: confirmation?.generic === true ? form.locale.toLowerCase().startsWith("ja") ? "\u56DE\u7B54\u5185\u5BB9\u306E\u78BA\u8A8D" : "Review your answers" : resolveMessage("confirmSensitiveDataTitle") }),
|
|
3957
|
+
/* @__PURE__ */ jsx3("p", { children: confirmation?.message ?? (confirmation?.generic === true ? form.locale.toLowerCase().startsWith("ja") ? "\u56DE\u7B54\u5185\u5BB9\u3092\u3054\u78BA\u8A8D\u306E\u3046\u3048\u3001\u9001\u4FE1\u3057\u3066\u304F\u3060\u3055\u3044\u3002" : "Please review your answers before submitting." : resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData"))) }),
|
|
3870
3958
|
(confirmation?.findings ?? []).length === 0 ? null : /* @__PURE__ */ jsx3("ul", { children: (confirmation?.findings ?? []).map((finding, index) => {
|
|
3871
3959
|
const field = form.schema.fields.find((candidate) => candidate.id === finding.fieldId);
|
|
3872
3960
|
const typeLabels = form.locale.toLowerCase().startsWith("ja") ? { email: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9", phone: "\u96FB\u8A71\u756A\u53F7", url: "URL", postal_code: "\u90F5\u4FBF\u756A\u53F7" } : { email: "Email address", phone: "Phone number", url: "URL", postal_code: "Postal code" };
|
|
@@ -3882,6 +3970,11 @@ function ContextFormRenderer({
|
|
|
3882
3970
|
] })
|
|
3883
3971
|
] }, `${finding.fieldId}-${finding.type}-${finding.start ?? index}`);
|
|
3884
3972
|
}) }),
|
|
3973
|
+
confirmation?.generic === true ? /* @__PURE__ */ jsx3("ul", { className: "fe-submission-summary", children: visibleItems.map((item) => /* @__PURE__ */ jsxs2("li", { children: [
|
|
3974
|
+
/* @__PURE__ */ jsx3("span", { children: item.title }),
|
|
3975
|
+
": ",
|
|
3976
|
+
/* @__PURE__ */ jsx3("span", { children: item.displayValue })
|
|
3977
|
+
] }, item.fieldId)) }) : null,
|
|
3885
3978
|
/* @__PURE__ */ jsx3("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: resolveMessage("confirmButton", form.translate("form.confirmSubmission")) }),
|
|
3886
3979
|
/* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: resolveMessage("cancelButton", form.translate("form.cancelSubmission")) })
|
|
3887
3980
|
] })
|
|
@@ -3901,7 +3994,7 @@ function ContextFormRenderer({
|
|
|
3901
3994
|
if (form.submitStatus === "success" && isReplaceMode) {
|
|
3902
3995
|
return /* @__PURE__ */ jsx3("div", { className: `fe-form ${className}`.trim(), children: completionRegion });
|
|
3903
3996
|
}
|
|
3904
|
-
if (confirmation !== null &&
|
|
3997
|
+
if (confirmation !== null && confirmationRenderMode === "replace") {
|
|
3905
3998
|
return /* @__PURE__ */ jsx3("div", { className: `fe-form ${className}`.trim(), children: confirmationContent });
|
|
3906
3999
|
}
|
|
3907
4000
|
return /* @__PURE__ */ jsxs2(Fragment3, { children: [
|
|
@@ -3912,7 +4005,7 @@ function ContextFormRenderer({
|
|
|
3912
4005
|
className: `fe-form ${className}`.trim(),
|
|
3913
4006
|
noValidate: true,
|
|
3914
4007
|
onSubmit: handleSubmit,
|
|
3915
|
-
"aria-hidden": confirmation !== null &&
|
|
4008
|
+
"aria-hidden": confirmation !== null && confirmationRenderMode === "dialog" ? true : void 0,
|
|
3916
4009
|
children: [
|
|
3917
4010
|
slots.renderHeader?.({
|
|
3918
4011
|
title: form.schema.title,
|
|
@@ -3981,7 +4074,7 @@ function ContextFormRenderer({
|
|
|
3981
4074
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ jsx3("div", { className: fieldClassName, children: fieldChildren });
|
|
3982
4075
|
})(),
|
|
3983
4076
|
guardMessage === null ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: guardMessage }),
|
|
3984
|
-
confirmation !== null &&
|
|
4077
|
+
confirmation !== null && confirmationRenderMode === "inline" ? confirmationContent : null,
|
|
3985
4078
|
validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-validation-summary", role: "alert", children: [
|
|
3986
4079
|
validationIssues.length,
|
|
3987
4080
|
" validation error",
|
|
@@ -4036,7 +4129,7 @@ function ContextFormRenderer({
|
|
|
4036
4129
|
]
|
|
4037
4130
|
}
|
|
4038
4131
|
),
|
|
4039
|
-
confirmation !== null &&
|
|
4132
|
+
confirmation !== null && confirmationRenderMode === "dialog" ? /* @__PURE__ */ jsx3("div", { className: "fe-confirmation-dialog-backdrop", role: "dialog", "aria-modal": "true", children: confirmationContent }) : null
|
|
4040
4133
|
] });
|
|
4041
4134
|
}
|
|
4042
4135
|
var RENDERER_MESSAGES = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/react",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"typescript"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@form-engine-ts/core": "4.1
|
|
46
|
-
"@form-engine-ts/privacy": "4.1
|
|
45
|
+
"@form-engine-ts/core": "4.3.1",
|
|
46
|
+
"@form-engine-ts/privacy": "4.3.1"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react": ">=18.2 <20",
|