@form-engine-ts/react 4.4.0 → 4.6.0
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 +11 -0
- package/dist/index.cjs +452 -52
- package/dist/index.d.cts +77 -5
- package/dist/index.d.ts +77 -5
- package/dist/index.js +454 -49
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -38,7 +38,8 @@ __export(index_exports, {
|
|
|
38
38
|
useField: () => useField,
|
|
39
39
|
useForm: () => useForm,
|
|
40
40
|
useFormBuilder: () => useFormBuilder,
|
|
41
|
-
useSubmissionReceipts: () => useSubmissionReceipts
|
|
41
|
+
useSubmissionReceipts: () => useSubmissionReceipts,
|
|
42
|
+
useTranslationWorkspace: () => useTranslationWorkspace
|
|
42
43
|
});
|
|
43
44
|
module.exports = __toCommonJS(index_exports);
|
|
44
45
|
|
|
@@ -234,8 +235,20 @@ function move(items, sourceIndex, targetIndex) {
|
|
|
234
235
|
result.splice(targetIndex, 0, item);
|
|
235
236
|
return result;
|
|
236
237
|
}
|
|
238
|
+
function displayRuleSourceIds(field) {
|
|
239
|
+
if (field.displayRule === void 0) return [];
|
|
240
|
+
const ids = [];
|
|
241
|
+
const visit = (group) => {
|
|
242
|
+
for (const condition of group.conditions) {
|
|
243
|
+
if ("logic" in condition) visit(condition);
|
|
244
|
+
else ids.push(condition.fieldId);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
visit(field.displayRule.condition);
|
|
248
|
+
return ids;
|
|
249
|
+
}
|
|
237
250
|
function withoutDisplayCondition(field) {
|
|
238
|
-
const { displayCondition: _displayCondition, ...rest } = field;
|
|
251
|
+
const { displayCondition: _displayCondition, displayRule: _displayRule, ...rest } = field;
|
|
239
252
|
return rest;
|
|
240
253
|
}
|
|
241
254
|
function removeLocalizedProperty(translations, locale, property) {
|
|
@@ -258,8 +271,23 @@ function useFormBuilder({
|
|
|
258
271
|
onChange,
|
|
259
272
|
policy,
|
|
260
273
|
idFactory = defaultIdFactory,
|
|
261
|
-
factories = {}
|
|
274
|
+
factories = {},
|
|
275
|
+
fieldEditorMode = "all",
|
|
276
|
+
activeFieldId: controlledActiveFieldId,
|
|
277
|
+
defaultActiveFieldId,
|
|
278
|
+
onActiveFieldChange
|
|
262
279
|
}) {
|
|
280
|
+
const [internalActiveFieldId, setInternalActiveFieldId] = (0, import_react.useState)(
|
|
281
|
+
defaultActiveFieldId ?? (fieldEditorMode === "single" ? schema.fields[0]?.id : void 0)
|
|
282
|
+
);
|
|
283
|
+
const activeFieldId = controlledActiveFieldId ?? internalActiveFieldId;
|
|
284
|
+
const setActiveFieldId = (0, import_react.useCallback)(
|
|
285
|
+
(fieldId) => {
|
|
286
|
+
if (controlledActiveFieldId === void 0) setInternalActiveFieldId(fieldId);
|
|
287
|
+
onActiveFieldChange?.(fieldId);
|
|
288
|
+
},
|
|
289
|
+
[controlledActiveFieldId, onActiveFieldChange]
|
|
290
|
+
);
|
|
263
291
|
const createId = (0, import_react.useCallback)(
|
|
264
292
|
(kind, existingIds) => {
|
|
265
293
|
const rawId = idFactory(kind, existingIds);
|
|
@@ -372,9 +400,10 @@ function useFormBuilder({
|
|
|
372
400
|
questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
|
|
373
401
|
}));
|
|
374
402
|
onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
|
|
403
|
+
setActiveFieldId(field.id);
|
|
375
404
|
return { success: true };
|
|
376
405
|
},
|
|
377
|
-
[createId, factories, onChange, policy, schema]
|
|
406
|
+
[createId, factories, onChange, policy, schema, setActiveFieldId]
|
|
378
407
|
);
|
|
379
408
|
const removeField = (0, import_react.useCallback)(
|
|
380
409
|
(fieldId) => {
|
|
@@ -382,15 +411,19 @@ function useFormBuilder({
|
|
|
382
411
|
return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
|
|
383
412
|
if (schema.fields.length <= 1)
|
|
384
413
|
return { success: false, error: { type: "invalid_operation", message: "A form must contain one field." } };
|
|
385
|
-
const
|
|
414
|
+
const removedIndex = schema.fields.findIndex((field) => field.id === fieldId);
|
|
415
|
+
const fields = schema.fields.filter((field) => field.id !== fieldId).map(
|
|
416
|
+
(field) => field.displayCondition?.questionId === fieldId || displayRuleSourceIds(field).includes(fieldId) ? withoutDisplayCondition(field) : field
|
|
417
|
+
);
|
|
386
418
|
const pages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
|
|
387
419
|
if (schema.pages !== void 0 && pages?.length === 0) {
|
|
388
420
|
const { pages: _pages, ...single } = schema;
|
|
389
421
|
onChange({ ...single, fields });
|
|
390
422
|
} else onChange({ ...schema, fields, ...pages === void 0 ? {} : { pages } });
|
|
423
|
+
if (activeFieldId === fieldId) setActiveFieldId(fields[removedIndex]?.id ?? fields.at(-1)?.id);
|
|
391
424
|
return { success: true };
|
|
392
425
|
},
|
|
393
|
-
[onChange, schema]
|
|
426
|
+
[activeFieldId, onChange, schema, setActiveFieldId]
|
|
394
427
|
);
|
|
395
428
|
const moveField = (0, import_react.useCallback)(
|
|
396
429
|
(fieldId, targetIndex) => {
|
|
@@ -403,8 +436,8 @@ function useFormBuilder({
|
|
|
403
436
|
onChange({
|
|
404
437
|
...schema,
|
|
405
438
|
fields: fields.map((field, index) => {
|
|
406
|
-
const
|
|
407
|
-
return source
|
|
439
|
+
const sources = field.displayCondition?.questionId === void 0 ? displayRuleSourceIds(field) : [field.displayCondition.questionId];
|
|
440
|
+
return sources.every((source) => (indexById.get(source) ?? index) < index) ? field : withoutDisplayCondition(field);
|
|
408
441
|
})
|
|
409
442
|
});
|
|
410
443
|
return { success: true };
|
|
@@ -828,6 +861,14 @@ function useFormBuilder({
|
|
|
828
861
|
const result = (0, import_core.validateFormSchema)(schema, policy === void 0 ? {} : { policy });
|
|
829
862
|
return result.valid ? [] : result.issues;
|
|
830
863
|
}, [policy, schema]);
|
|
864
|
+
const getFieldEditorProps = (0, import_react.useCallback)(
|
|
865
|
+
(fieldId) => ({
|
|
866
|
+
isActive: activeFieldId === fieldId,
|
|
867
|
+
isVisible: fieldEditorMode === "all" || activeFieldId === fieldId,
|
|
868
|
+
onSelect: () => setActiveFieldId(fieldId)
|
|
869
|
+
}),
|
|
870
|
+
[activeFieldId, fieldEditorMode, setActiveFieldId]
|
|
871
|
+
);
|
|
831
872
|
return {
|
|
832
873
|
schema,
|
|
833
874
|
addField,
|
|
@@ -849,7 +890,10 @@ function useFormBuilder({
|
|
|
849
890
|
setLocaleTranslation,
|
|
850
891
|
addLocale,
|
|
851
892
|
setDefaultLocale,
|
|
852
|
-
validationIssues
|
|
893
|
+
validationIssues,
|
|
894
|
+
...activeFieldId === void 0 ? {} : { activeFieldId },
|
|
895
|
+
setActiveFieldId,
|
|
896
|
+
getFieldEditorProps
|
|
853
897
|
};
|
|
854
898
|
}
|
|
855
899
|
|
|
@@ -1502,7 +1546,10 @@ var BUILDER_DEFAULTS = {
|
|
|
1502
1546
|
"builder.operator.equals": "equals",
|
|
1503
1547
|
"builder.operator.not_equals": "does not equal",
|
|
1504
1548
|
"builder.operator.contains": "contains",
|
|
1505
|
-
"builder.operator.not_empty": "is not empty"
|
|
1549
|
+
"builder.operator.not_empty": "is not empty",
|
|
1550
|
+
"builder.submissionSettings": "Submission settings",
|
|
1551
|
+
"builder.showConfirmationBeforeSubmit": "Show confirmation before submit",
|
|
1552
|
+
"builder.confirmationRenderMode": "Confirmation display mode"
|
|
1506
1553
|
};
|
|
1507
1554
|
function BuilderSectionGroup({ children }) {
|
|
1508
1555
|
return children;
|
|
@@ -1679,7 +1726,12 @@ function FormBuilder({
|
|
|
1679
1726
|
slots,
|
|
1680
1727
|
sectionOrder,
|
|
1681
1728
|
disableDefaultStyles = false,
|
|
1682
|
-
unstyled = false
|
|
1729
|
+
unstyled = false,
|
|
1730
|
+
fieldEditorMode = "all",
|
|
1731
|
+
activeFieldId,
|
|
1732
|
+
defaultActiveFieldId,
|
|
1733
|
+
onActiveFieldChange,
|
|
1734
|
+
submissionSettingsOptions
|
|
1683
1735
|
}) {
|
|
1684
1736
|
const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
|
|
1685
1737
|
const components = {
|
|
@@ -1702,7 +1754,11 @@ function FormBuilder({
|
|
|
1702
1754
|
onChange,
|
|
1703
1755
|
...policy === void 0 ? {} : { policy },
|
|
1704
1756
|
...idFactory === void 0 ? {} : { idFactory },
|
|
1705
|
-
...factories === void 0 ? {} : { factories }
|
|
1757
|
+
...factories === void 0 ? {} : { factories },
|
|
1758
|
+
fieldEditorMode,
|
|
1759
|
+
...activeFieldId === void 0 ? {} : { activeFieldId },
|
|
1760
|
+
...defaultActiveFieldId === void 0 ? {} : { defaultActiveFieldId },
|
|
1761
|
+
...onActiveFieldChange === void 0 ? {} : { onActiveFieldChange }
|
|
1706
1762
|
});
|
|
1707
1763
|
const [newPageQuestionId, setNewPageQuestionId] = (0, import_react2.useState)("");
|
|
1708
1764
|
const [newLocale, setNewLocale] = (0, import_react2.useState)("");
|
|
@@ -2042,6 +2098,47 @@ function FormBuilder({
|
|
|
2042
2098
|
] })
|
|
2043
2099
|
}
|
|
2044
2100
|
) }),
|
|
2101
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "submissionSettings", children: submissionSettingsOptions?.enabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2102
|
+
Section,
|
|
2103
|
+
{
|
|
2104
|
+
className: builderClass("form-engine-builder__submission-settings"),
|
|
2105
|
+
title: translate("builder.submissionSettings"),
|
|
2106
|
+
children: [
|
|
2107
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2108
|
+
Checkbox,
|
|
2109
|
+
{
|
|
2110
|
+
id: "builder-show-confirmation",
|
|
2111
|
+
label: translate("builder.showConfirmationBeforeSubmit"),
|
|
2112
|
+
checked: schema.submissionSettings?.showConfirmationBeforeSubmit === true,
|
|
2113
|
+
onChange: (checked) => onChange({
|
|
2114
|
+
...schema,
|
|
2115
|
+
submissionSettings: { ...schema.submissionSettings, showConfirmationBeforeSubmit: checked }
|
|
2116
|
+
})
|
|
2117
|
+
}
|
|
2118
|
+
),
|
|
2119
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2120
|
+
Select,
|
|
2121
|
+
{
|
|
2122
|
+
id: "builder-confirmation-render-mode",
|
|
2123
|
+
label: translate("builder.confirmationRenderMode"),
|
|
2124
|
+
value: schema.submissionSettings?.confirmationRenderMode ?? "inline",
|
|
2125
|
+
options: [
|
|
2126
|
+
{ value: "dialog", label: "Dialog" },
|
|
2127
|
+
{ value: "inline", label: "Inline" },
|
|
2128
|
+
{ value: "replace", label: "Replace" }
|
|
2129
|
+
],
|
|
2130
|
+
onChange: (value) => {
|
|
2131
|
+
if (value !== "dialog" && value !== "inline" && value !== "replace") return;
|
|
2132
|
+
onChange({
|
|
2133
|
+
...schema,
|
|
2134
|
+
submissionSettings: { ...schema.submissionSettings, confirmationRenderMode: value }
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
)
|
|
2139
|
+
]
|
|
2140
|
+
}
|
|
2141
|
+
) : null }),
|
|
2045
2142
|
resolvedSectionOrder === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "completionMessage", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { headingId: "builder-completion-message-heading", title: translate("builder.completionMessage"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2046
2143
|
TextInput,
|
|
2047
2144
|
{
|
|
@@ -2440,9 +2537,25 @@ function FormBuilder({
|
|
|
2440
2537
|
) : null }),
|
|
2441
2538
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "questions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__list"), children: schema.fields.map((field, index) => {
|
|
2442
2539
|
const controls = resolveFieldEditorControls(fieldEditorControls);
|
|
2540
|
+
const editorState = headless.getFieldEditorProps?.(field.id) ?? {
|
|
2541
|
+
isActive: true,
|
|
2542
|
+
isVisible: true,
|
|
2543
|
+
onSelect: () => void 0
|
|
2544
|
+
};
|
|
2443
2545
|
const condition = field.displayCondition;
|
|
2444
2546
|
const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
|
|
2445
2547
|
const availableSources = schema.fields.slice(0, index);
|
|
2548
|
+
if (!editorState.isVisible) {
|
|
2549
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2550
|
+
"div",
|
|
2551
|
+
{
|
|
2552
|
+
className: builderClass("form-engine-builder__question-preview"),
|
|
2553
|
+
"data-field-id": field.id,
|
|
2554
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: editorState.onSelect, children: field.title })
|
|
2555
|
+
},
|
|
2556
|
+
field.id
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2446
2559
|
if (FieldEditorSlot !== void 0) {
|
|
2447
2560
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2448
2561
|
FieldEditorSlot,
|
|
@@ -3150,8 +3263,294 @@ function useField(fieldId) {
|
|
|
3150
3263
|
return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
|
|
3151
3264
|
}
|
|
3152
3265
|
|
|
3153
|
-
// src/
|
|
3266
|
+
// src/hooks/useTranslationWorkspace.ts
|
|
3267
|
+
var import_core4 = require("@form-engine-ts/core");
|
|
3154
3268
|
var import_react4 = require("react");
|
|
3269
|
+
function updateTranslationMap(translations, locale, property, text) {
|
|
3270
|
+
if (property === "label") return translations ?? {};
|
|
3271
|
+
const current = translations?.[locale];
|
|
3272
|
+
const next = { ...current, [property]: text };
|
|
3273
|
+
return { ...translations, [locale]: next };
|
|
3274
|
+
}
|
|
3275
|
+
function asAsyncAdapter(adapter) {
|
|
3276
|
+
if ("translateBatch" in adapter) return adapter;
|
|
3277
|
+
return {
|
|
3278
|
+
translateText: async (text, locale, sourceLocale) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text,
|
|
3279
|
+
translateBatch: async (texts, locale, sourceLocale) => texts.map(
|
|
3280
|
+
(text) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text
|
|
3281
|
+
)
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
function validateLocaleByPolicy(locale, currentLocales, policy) {
|
|
3285
|
+
if (locale.length === 0) {
|
|
3286
|
+
return {
|
|
3287
|
+
valid: false,
|
|
3288
|
+
error: { type: "invalid_locale_format", message: "Locale must not be empty." }
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
try {
|
|
3292
|
+
Intl.getCanonicalLocales(locale);
|
|
3293
|
+
} catch {
|
|
3294
|
+
return {
|
|
3295
|
+
valid: false,
|
|
3296
|
+
error: { type: "invalid_locale_format", message: `Locale "${locale}" is not a valid BCP 47 locale.` }
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
|
|
3300
|
+
return {
|
|
3301
|
+
valid: false,
|
|
3302
|
+
error: { type: "locale_not_allowed", message: `Locale "${locale}" is not allowed by the form policy.` }
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
if (policy?.maxLocales !== void 0 && !currentLocales.includes(locale) && currentLocales.length >= policy.maxLocales) {
|
|
3306
|
+
return {
|
|
3307
|
+
valid: false,
|
|
3308
|
+
error: {
|
|
3309
|
+
type: "max_locales_exceeded",
|
|
3310
|
+
message: `At most ${policy.maxLocales} locales are allowed by the form policy.`
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
return { valid: true };
|
|
3315
|
+
}
|
|
3316
|
+
function manualMetadata(sourceText, sourceLocale) {
|
|
3317
|
+
return {
|
|
3318
|
+
sourceLocale,
|
|
3319
|
+
sourceTextHash: (0, import_core4.computeSourceTextHash)(sourceText),
|
|
3320
|
+
translationSource: "manual",
|
|
3321
|
+
editedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
function setNodeMetadata(node, slot, text, sourceLocale) {
|
|
3325
|
+
const localeMetadata = node.translationMetadata?.[slot.locale];
|
|
3326
|
+
const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: manualMetadata(slot.sourceText, sourceLocale) };
|
|
3327
|
+
return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
|
|
3328
|
+
}
|
|
3329
|
+
function updateSchemaTranslation(schema, slot, text, sourceLocale) {
|
|
3330
|
+
if (slot.kind === "form") {
|
|
3331
|
+
return setNodeMetadata(
|
|
3332
|
+
{ ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
|
|
3333
|
+
slot,
|
|
3334
|
+
text,
|
|
3335
|
+
sourceLocale
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
if (slot.kind === "field") {
|
|
3339
|
+
return {
|
|
3340
|
+
...schema,
|
|
3341
|
+
fields: schema.fields.map((field) => {
|
|
3342
|
+
if (field.id !== slot.nodeId) return field;
|
|
3343
|
+
const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
|
|
3344
|
+
return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
|
|
3345
|
+
})
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
if (slot.kind === "option") {
|
|
3349
|
+
return {
|
|
3350
|
+
...schema,
|
|
3351
|
+
fields: schema.fields.map((field) => {
|
|
3352
|
+
if (!("options" in field)) return field;
|
|
3353
|
+
return {
|
|
3354
|
+
...field,
|
|
3355
|
+
options: field.options.map((option) => {
|
|
3356
|
+
if (option.id !== slot.nodeId) return option;
|
|
3357
|
+
const translations = text.trim().length === 0 ? Object.fromEntries(
|
|
3358
|
+
Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
|
|
3359
|
+
) : { ...option.translations, [slot.locale]: text };
|
|
3360
|
+
return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
|
|
3361
|
+
})
|
|
3362
|
+
};
|
|
3363
|
+
})
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
return {
|
|
3367
|
+
...schema,
|
|
3368
|
+
...schema.pages === void 0 ? {} : {
|
|
3369
|
+
pages: schema.pages.map((page) => {
|
|
3370
|
+
if (page.id !== slot.nodeId) return page;
|
|
3371
|
+
const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
|
|
3372
|
+
return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
|
|
3373
|
+
})
|
|
3374
|
+
}
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
function useTranslationWorkspace({
|
|
3378
|
+
schema,
|
|
3379
|
+
onChange,
|
|
3380
|
+
sourceLocale = schema.defaultLocale ?? "en",
|
|
3381
|
+
targetLocale,
|
|
3382
|
+
translationAdapter,
|
|
3383
|
+
readOnly = false,
|
|
3384
|
+
policy,
|
|
3385
|
+
validateLocale
|
|
3386
|
+
}) {
|
|
3387
|
+
const [draftSchema, setDraftSchema] = (0, import_react4.useState)(schema);
|
|
3388
|
+
const [selectedLocale, setSelectedLocale] = (0, import_react4.useState)(targetLocale ?? "");
|
|
3389
|
+
const [isTranslating, setIsTranslating] = (0, import_react4.useState)(false);
|
|
3390
|
+
const [error, setError] = (0, import_react4.useState)();
|
|
3391
|
+
const currentSchema = onChange === void 0 ? draftSchema : schema;
|
|
3392
|
+
const targetLocales = (0, import_react4.useMemo)(() => {
|
|
3393
|
+
const locales = (0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales;
|
|
3394
|
+
return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
|
|
3395
|
+
(locale) => locale !== sourceLocale
|
|
3396
|
+
);
|
|
3397
|
+
}, [currentSchema, sourceLocale]);
|
|
3398
|
+
const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
|
|
3399
|
+
const slots = (0, import_react4.useMemo)(
|
|
3400
|
+
() => activeLocale.length === 0 ? [] : (0, import_core4.collectTranslationSlots)(currentSchema, activeLocale),
|
|
3401
|
+
[activeLocale, currentSchema]
|
|
3402
|
+
);
|
|
3403
|
+
const summary = (0, import_react4.useMemo)(() => {
|
|
3404
|
+
const counts = {
|
|
3405
|
+
missing: 0,
|
|
3406
|
+
translated: 0,
|
|
3407
|
+
stale: 0,
|
|
3408
|
+
manual: 0,
|
|
3409
|
+
"manual-stale": 0
|
|
3410
|
+
};
|
|
3411
|
+
for (const slot of slots) counts[slot.status ?? "missing"] += 1;
|
|
3412
|
+
const complete = counts.translated + counts.manual;
|
|
3413
|
+
return {
|
|
3414
|
+
totalSlots: slots.length,
|
|
3415
|
+
translatedCount: complete,
|
|
3416
|
+
missingCount: counts.missing,
|
|
3417
|
+
staleCount: counts.stale + counts["manual-stale"],
|
|
3418
|
+
manualCount: counts.manual + counts["manual-stale"],
|
|
3419
|
+
completionPercentage: slots.length === 0 ? 100 : Math.round(complete / slots.length * 100)
|
|
3420
|
+
};
|
|
3421
|
+
}, [slots]);
|
|
3422
|
+
const commit = (0, import_react4.useCallback)(
|
|
3423
|
+
(next) => {
|
|
3424
|
+
setDraftSchema(next);
|
|
3425
|
+
onChange?.(next);
|
|
3426
|
+
},
|
|
3427
|
+
[onChange]
|
|
3428
|
+
);
|
|
3429
|
+
const setTranslation = (0, import_react4.useCallback)(
|
|
3430
|
+
(slot, text) => {
|
|
3431
|
+
if (readOnly) return;
|
|
3432
|
+
commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
|
|
3433
|
+
},
|
|
3434
|
+
[commit, currentSchema, readOnly, sourceLocale]
|
|
3435
|
+
);
|
|
3436
|
+
const addLocale = (0, import_react4.useCallback)(
|
|
3437
|
+
(locale) => {
|
|
3438
|
+
if (readOnly) return { success: false, error: "Workspace is read-only." };
|
|
3439
|
+
const normalized = locale.trim();
|
|
3440
|
+
const currentLocales = [
|
|
3441
|
+
.../* @__PURE__ */ new Set([
|
|
3442
|
+
...currentSchema.defaultLocale === void 0 ? [] : [currentSchema.defaultLocale],
|
|
3443
|
+
sourceLocale,
|
|
3444
|
+
...(0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales
|
|
3445
|
+
])
|
|
3446
|
+
];
|
|
3447
|
+
const validation = validateLocale?.(normalized, currentLocales) ?? validateLocaleByPolicy(normalized, currentLocales, policy);
|
|
3448
|
+
if (!validation.valid) return { success: false, error: validation.error?.message ?? "Locale is not valid." };
|
|
3449
|
+
const alreadyRegistered = normalized === sourceLocale || normalized === currentSchema.defaultLocale || (currentSchema.supportedLocales ?? []).includes(normalized);
|
|
3450
|
+
if (alreadyRegistered) {
|
|
3451
|
+
setSelectedLocale(normalized);
|
|
3452
|
+
return { success: true };
|
|
3453
|
+
}
|
|
3454
|
+
commit({
|
|
3455
|
+
...currentSchema,
|
|
3456
|
+
supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
|
|
3457
|
+
});
|
|
3458
|
+
setSelectedLocale(normalized);
|
|
3459
|
+
return { success: true };
|
|
3460
|
+
},
|
|
3461
|
+
[commit, currentSchema, policy, readOnly, sourceLocale, validateLocale]
|
|
3462
|
+
);
|
|
3463
|
+
const isAddLocaleAllowed = (0, import_react4.useCallback)(
|
|
3464
|
+
(locale) => {
|
|
3465
|
+
if (readOnly) return false;
|
|
3466
|
+
const normalized = locale.trim();
|
|
3467
|
+
const currentLocales = [
|
|
3468
|
+
.../* @__PURE__ */ new Set([
|
|
3469
|
+
...currentSchema.defaultLocale === void 0 ? [] : [currentSchema.defaultLocale],
|
|
3470
|
+
sourceLocale,
|
|
3471
|
+
...(0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales
|
|
3472
|
+
])
|
|
3473
|
+
];
|
|
3474
|
+
return (validateLocale?.(normalized, currentLocales) ?? validateLocaleByPolicy(normalized, currentLocales, policy)).valid;
|
|
3475
|
+
},
|
|
3476
|
+
[currentSchema, policy, readOnly, sourceLocale, validateLocale]
|
|
3477
|
+
);
|
|
3478
|
+
const removeLocale = (0, import_react4.useCallback)(
|
|
3479
|
+
(locale) => {
|
|
3480
|
+
if (readOnly || locale === sourceLocale || locale === currentSchema.defaultLocale) return;
|
|
3481
|
+
commit((0, import_core4.removeLocaleFromSchema)(currentSchema, locale));
|
|
3482
|
+
if (activeLocale === locale) setSelectedLocale("");
|
|
3483
|
+
},
|
|
3484
|
+
[activeLocale, commit, currentSchema, readOnly, sourceLocale]
|
|
3485
|
+
);
|
|
3486
|
+
const translateAll = (0, import_react4.useCallback)(
|
|
3487
|
+
async (options = {}) => {
|
|
3488
|
+
if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
|
|
3489
|
+
if (activeLocale.length === 0) throw new Error("A target locale is required.");
|
|
3490
|
+
setIsTranslating(true);
|
|
3491
|
+
setError(void 0);
|
|
3492
|
+
try {
|
|
3493
|
+
const populated = await (0, import_core4.populateSchemaTranslations)(
|
|
3494
|
+
currentSchema,
|
|
3495
|
+
[activeLocale],
|
|
3496
|
+
asAsyncAdapter(translationAdapter),
|
|
3497
|
+
{
|
|
3498
|
+
overwrite: "stale-and-missing",
|
|
3499
|
+
preserveManualTranslations: true,
|
|
3500
|
+
...options
|
|
3501
|
+
}
|
|
3502
|
+
);
|
|
3503
|
+
commit(populated.schema);
|
|
3504
|
+
return populated.report;
|
|
3505
|
+
} catch (cause) {
|
|
3506
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3507
|
+
setError(message);
|
|
3508
|
+
throw cause;
|
|
3509
|
+
} finally {
|
|
3510
|
+
setIsTranslating(false);
|
|
3511
|
+
}
|
|
3512
|
+
},
|
|
3513
|
+
[activeLocale, commit, currentSchema, translationAdapter]
|
|
3514
|
+
);
|
|
3515
|
+
const translateSlot = (0, import_react4.useCallback)(
|
|
3516
|
+
async (slot) => {
|
|
3517
|
+
if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
|
|
3518
|
+
if (readOnly) return;
|
|
3519
|
+
setIsTranslating(true);
|
|
3520
|
+
setError(void 0);
|
|
3521
|
+
try {
|
|
3522
|
+
const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
|
|
3523
|
+
commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
|
|
3524
|
+
} catch (cause) {
|
|
3525
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3526
|
+
setError(message);
|
|
3527
|
+
throw cause;
|
|
3528
|
+
} finally {
|
|
3529
|
+
setIsTranslating(false);
|
|
3530
|
+
}
|
|
3531
|
+
},
|
|
3532
|
+
[commit, currentSchema, readOnly, sourceLocale, translationAdapter]
|
|
3533
|
+
);
|
|
3534
|
+
return {
|
|
3535
|
+
sourceLocale,
|
|
3536
|
+
targetLocale: activeLocale,
|
|
3537
|
+
targetLocales,
|
|
3538
|
+
setTargetLocale: setSelectedLocale,
|
|
3539
|
+
slots,
|
|
3540
|
+
summary,
|
|
3541
|
+
addLocale,
|
|
3542
|
+
isAddLocaleAllowed,
|
|
3543
|
+
removeLocale,
|
|
3544
|
+
setTranslation,
|
|
3545
|
+
translateAll,
|
|
3546
|
+
translateSlot,
|
|
3547
|
+
isTranslating,
|
|
3548
|
+
...error === void 0 ? {} : { error }
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
// src/receipt.ts
|
|
3553
|
+
var import_react5 = require("react");
|
|
3155
3554
|
function submissionReceiptQueryKey(formId, formVersion) {
|
|
3156
3555
|
return `${formId}:v${formVersion}`;
|
|
3157
3556
|
}
|
|
@@ -3222,19 +3621,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
|
3222
3621
|
}
|
|
3223
3622
|
function useSubmissionReceipts(store, queries) {
|
|
3224
3623
|
const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
|
|
3225
|
-
const stableQueries = (0,
|
|
3624
|
+
const stableQueries = (0, import_react5.useMemo)(() => {
|
|
3226
3625
|
const parsed = JSON.parse(querySignature);
|
|
3227
3626
|
if (!Array.isArray(parsed)) return [];
|
|
3228
3627
|
return parsed.flatMap(
|
|
3229
3628
|
(entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
|
|
3230
3629
|
);
|
|
3231
3630
|
}, [querySignature]);
|
|
3232
|
-
const [state, setState] = (0,
|
|
3631
|
+
const [state, setState] = (0, import_react5.useState)({
|
|
3233
3632
|
receipts: /* @__PURE__ */ new Map(),
|
|
3234
3633
|
isLoading: stableQueries.length > 0,
|
|
3235
3634
|
error: null
|
|
3236
3635
|
});
|
|
3237
|
-
(0,
|
|
3636
|
+
(0, import_react5.useEffect)(() => {
|
|
3238
3637
|
let active = true;
|
|
3239
3638
|
if (stableQueries.length === 0) {
|
|
3240
3639
|
setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
|
|
@@ -3268,8 +3667,8 @@ function useSubmissionReceipts(store, queries) {
|
|
|
3268
3667
|
}
|
|
3269
3668
|
|
|
3270
3669
|
// src/renderer.tsx
|
|
3271
|
-
var
|
|
3272
|
-
var
|
|
3670
|
+
var import_core5 = require("@form-engine-ts/core");
|
|
3671
|
+
var import_react6 = require("react");
|
|
3273
3672
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
3274
3673
|
var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
|
|
3275
3674
|
function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
|
|
@@ -3780,41 +4179,41 @@ function ContextFormRenderer({
|
|
|
3780
4179
|
slots = {}
|
|
3781
4180
|
}) {
|
|
3782
4181
|
const form = useForm();
|
|
3783
|
-
const prefix = (0,
|
|
3784
|
-
const formRef = (0,
|
|
3785
|
-
const loadedDraftKey = (0,
|
|
3786
|
-
const [draftRestored, setDraftRestored] = (0,
|
|
3787
|
-
const [currentPageIndex, setCurrentPageIndex] = (0,
|
|
3788
|
-
const [focusFieldId, setFocusFieldId] = (0,
|
|
3789
|
-
const [confirmation, setConfirmation] = (0,
|
|
3790
|
-
const [guardMessage, setGuardMessage] = (0,
|
|
3791
|
-
const [guardsPending, setGuardsPending] = (0,
|
|
3792
|
-
const [receipt, setReceipt] = (0,
|
|
3793
|
-
const [completionData, setCompletionData] = (0,
|
|
3794
|
-
const [receiptLoaded, setReceiptLoaded] = (0,
|
|
3795
|
-
const rendererSubmissionInFlight = (0,
|
|
3796
|
-
const fallbackAttemptId = (0,
|
|
3797
|
-
const completionRef = (0,
|
|
3798
|
-
const confirmationRef = (0,
|
|
4182
|
+
const prefix = (0, import_react6.useId)().replace(/:/g, "");
|
|
4183
|
+
const formRef = (0, import_react6.useRef)(null);
|
|
4184
|
+
const loadedDraftKey = (0, import_react6.useRef)(null);
|
|
4185
|
+
const [draftRestored, setDraftRestored] = (0, import_react6.useState)(false);
|
|
4186
|
+
const [currentPageIndex, setCurrentPageIndex] = (0, import_react6.useState)(0);
|
|
4187
|
+
const [focusFieldId, setFocusFieldId] = (0, import_react6.useState)(null);
|
|
4188
|
+
const [confirmation, setConfirmation] = (0, import_react6.useState)(null);
|
|
4189
|
+
const [guardMessage, setGuardMessage] = (0, import_react6.useState)(null);
|
|
4190
|
+
const [guardsPending, setGuardsPending] = (0, import_react6.useState)(false);
|
|
4191
|
+
const [receipt, setReceipt] = (0, import_react6.useState)(null);
|
|
4192
|
+
const [completionData, setCompletionData] = (0, import_react6.useState)(null);
|
|
4193
|
+
const [receiptLoaded, setReceiptLoaded] = (0, import_react6.useState)(receiptStore === void 0);
|
|
4194
|
+
const rendererSubmissionInFlight = (0, import_react6.useRef)(false);
|
|
4195
|
+
const fallbackAttemptId = (0, import_react6.useRef)(null);
|
|
4196
|
+
const completionRef = (0, import_react6.useRef)(null);
|
|
4197
|
+
const confirmationRef = (0, import_react6.useRef)(null);
|
|
3799
4198
|
const pages = form.schema.pages;
|
|
3800
|
-
const visiblePageIndexes = (0,
|
|
4199
|
+
const visiblePageIndexes = (0, import_react6.useMemo)(
|
|
3801
4200
|
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
3802
4201
|
[form.pageVisibility, pages]
|
|
3803
4202
|
);
|
|
3804
4203
|
const activePage = pages?.[currentPageIndex];
|
|
3805
4204
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
3806
4205
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
3807
|
-
const visibleValues = (0,
|
|
3808
|
-
const visibleItems = (0,
|
|
4206
|
+
const visibleValues = (0, import_react6.useMemo)(() => (0, import_core5.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
|
|
4207
|
+
const visibleItems = (0, import_react6.useMemo)(
|
|
3809
4208
|
() => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
|
|
3810
4209
|
[form.schema, form.translate, form.values, form.visibility]
|
|
3811
4210
|
);
|
|
3812
|
-
const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? "inline";
|
|
3813
|
-
const confirmationEnabled = submissionConfirmation?.enabled
|
|
4211
|
+
const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? form.schema.submissionSettings?.confirmationRenderMode ?? "inline";
|
|
4212
|
+
const confirmationEnabled = submissionConfirmation?.enabled ?? form.schema.submissionSettings?.showConfirmationBeforeSubmit ?? false;
|
|
3814
4213
|
const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
|
|
3815
4214
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
3816
4215
|
const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
|
|
3817
|
-
const resolveMessage = (0,
|
|
4216
|
+
const resolveMessage = (0, import_react6.useCallback)(
|
|
3818
4217
|
(key, fallback) => {
|
|
3819
4218
|
const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
|
|
3820
4219
|
const configured = messages[key];
|
|
@@ -3822,15 +4221,15 @@ function ContextFormRenderer({
|
|
|
3822
4221
|
},
|
|
3823
4222
|
[form.locale, messageResolver, messages]
|
|
3824
4223
|
);
|
|
3825
|
-
const fieldTranslate = (0,
|
|
4224
|
+
const fieldTranslate = (0, import_react6.useCallback)(
|
|
3826
4225
|
(key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
|
|
3827
4226
|
[form.translate, messageResolver, messages.requiredField, resolveMessage]
|
|
3828
4227
|
);
|
|
3829
|
-
const focusSubmitButton = (0,
|
|
4228
|
+
const focusSubmitButton = (0, import_react6.useCallback)(() => {
|
|
3830
4229
|
const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
|
|
3831
4230
|
button?.focus();
|
|
3832
4231
|
}, []);
|
|
3833
|
-
(0,
|
|
4232
|
+
(0, import_react6.useEffect)(() => {
|
|
3834
4233
|
let active = true;
|
|
3835
4234
|
if (receiptStore === void 0) {
|
|
3836
4235
|
setReceipt(null);
|
|
@@ -3851,14 +4250,14 @@ function ContextFormRenderer({
|
|
|
3851
4250
|
active = false;
|
|
3852
4251
|
};
|
|
3853
4252
|
}, [form.schema.id, form.schema.version, receiptStore]);
|
|
3854
|
-
(0,
|
|
4253
|
+
(0, import_react6.useEffect)(() => {
|
|
3855
4254
|
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
3856
4255
|
setCurrentPageIndex(0);
|
|
3857
4256
|
return;
|
|
3858
4257
|
}
|
|
3859
4258
|
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
3860
4259
|
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
3861
|
-
(0,
|
|
4260
|
+
(0, import_react6.useEffect)(() => {
|
|
3862
4261
|
if (focusFieldId === null) return;
|
|
3863
4262
|
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
3864
4263
|
(element) => element.dataset.fieldId === focusFieldId
|
|
@@ -3870,11 +4269,11 @@ function ContextFormRenderer({
|
|
|
3870
4269
|
setFocusFieldId(null);
|
|
3871
4270
|
}
|
|
3872
4271
|
}, [focusFieldId]);
|
|
3873
|
-
(0,
|
|
4272
|
+
(0, import_react6.useEffect)(() => {
|
|
3874
4273
|
if (!isReplaceMode || form.submitStatus !== "success") return;
|
|
3875
4274
|
completionRef.current?.focus();
|
|
3876
4275
|
}, [form.submitStatus, isReplaceMode]);
|
|
3877
|
-
(0,
|
|
4276
|
+
(0, import_react6.useEffect)(() => {
|
|
3878
4277
|
if (confirmation === null) return;
|
|
3879
4278
|
const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
|
|
3880
4279
|
confirmButton?.focus();
|
|
@@ -3906,7 +4305,7 @@ function ContextFormRenderer({
|
|
|
3906
4305
|
globalThis.addEventListener("keydown", onKeyDown);
|
|
3907
4306
|
return () => globalThis.removeEventListener("keydown", onKeyDown);
|
|
3908
4307
|
}, [confirmation, confirmationRenderMode, focusSubmitButton]);
|
|
3909
|
-
(0,
|
|
4308
|
+
(0, import_react6.useEffect)(() => {
|
|
3910
4309
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
3911
4310
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
3912
4311
|
if (loadedDraftKey.current === loadIdentity) return;
|
|
@@ -3918,7 +4317,7 @@ function ContextFormRenderer({
|
|
|
3918
4317
|
form.restoreValues(draft.values);
|
|
3919
4318
|
setDraftRestored(true);
|
|
3920
4319
|
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
3921
|
-
(0,
|
|
4320
|
+
(0, import_react6.useEffect)(() => {
|
|
3922
4321
|
if (form.submitStatus === "success") return;
|
|
3923
4322
|
const timeout = globalThis.setTimeout(() => {
|
|
3924
4323
|
onDraftSave?.(form.values);
|
|
@@ -3974,7 +4373,7 @@ function ContextFormRenderer({
|
|
|
3974
4373
|
if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
|
|
3975
4374
|
return { status: "cancelled" };
|
|
3976
4375
|
}
|
|
3977
|
-
const validation = (0,
|
|
4376
|
+
const validation = (0, import_core5.validateAnswers)(form.schema, form.values);
|
|
3978
4377
|
const firstInvalidFieldId = validation.issues[0]?.fieldId;
|
|
3979
4378
|
if (validation.valid && !guardsConfirmed) {
|
|
3980
4379
|
rendererSubmissionInFlight.current = true;
|
|
@@ -4276,7 +4675,7 @@ function ContextFormRenderer({
|
|
|
4276
4675
|
...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
|
|
4277
4676
|
};
|
|
4278
4677
|
if (slots.renderField !== void 0) {
|
|
4279
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4678
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react6.Fragment, { children: slots.renderField({
|
|
4280
4679
|
question: field,
|
|
4281
4680
|
value: form.values[field.id],
|
|
4282
4681
|
onChange: (value) => {
|
|
@@ -4446,5 +4845,6 @@ function FormRenderer(props) {
|
|
|
4446
4845
|
useField,
|
|
4447
4846
|
useForm,
|
|
4448
4847
|
useFormBuilder,
|
|
4449
|
-
useSubmissionReceipts
|
|
4848
|
+
useSubmissionReceipts,
|
|
4849
|
+
useTranslationWorkspace
|
|
4450
4850
|
});
|