@form-engine-ts/react 4.4.0 → 4.5.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 +6 -0
- package/dist/index.cjs +390 -52
- package/dist/index.d.cts +64 -5
- package/dist/index.d.ts +64 -5
- package/dist/index.js +391 -49
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -73,6 +73,12 @@ on the next mount:
|
|
|
73
73
|
membership controls and localization editors; pass an `AsyncTranslationAdapter` as `translationAdapter` to enable its
|
|
74
74
|
batch-translation action.
|
|
75
75
|
|
|
76
|
+
`useTranslationWorkspace` exposes locale management, translation slots, completion summaries, manual editing, and
|
|
77
|
+
single-slot or batch translation with stale/manual status handling. The MUI package exports a ready-made
|
|
78
|
+
`TranslationWorkspace` surface. For large forms, set `fieldEditorMode="single"` and use `activeFieldId` or the hook's
|
|
79
|
+
`setActiveFieldId` to keep one field editor open at a time. Set `submissionSettingsOptions={{ enabled: true }}` to expose
|
|
80
|
+
schema-driven pre-submit confirmation controls in the builder.
|
|
81
|
+
|
|
76
82
|
## Headless builder and renderer lifecycle
|
|
77
83
|
|
|
78
84
|
`useFormBuilder({ schema, onChange, policy, idFactory, factories })` exposes controlled field, option, page, condition,
|
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,232 @@ 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 manualMetadata(sourceText, sourceLocale) {
|
|
3285
|
+
return {
|
|
3286
|
+
sourceLocale,
|
|
3287
|
+
sourceTextHash: (0, import_core4.computeSourceTextHash)(sourceText),
|
|
3288
|
+
translationSource: "manual",
|
|
3289
|
+
editedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
function setNodeMetadata(node, slot, text, sourceLocale) {
|
|
3293
|
+
const localeMetadata = node.translationMetadata?.[slot.locale];
|
|
3294
|
+
const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: manualMetadata(slot.sourceText, sourceLocale) };
|
|
3295
|
+
return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
|
|
3296
|
+
}
|
|
3297
|
+
function updateSchemaTranslation(schema, slot, text, sourceLocale) {
|
|
3298
|
+
if (slot.kind === "form") {
|
|
3299
|
+
return setNodeMetadata(
|
|
3300
|
+
{ ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
|
|
3301
|
+
slot,
|
|
3302
|
+
text,
|
|
3303
|
+
sourceLocale
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
if (slot.kind === "field") {
|
|
3307
|
+
return {
|
|
3308
|
+
...schema,
|
|
3309
|
+
fields: schema.fields.map((field) => {
|
|
3310
|
+
if (field.id !== slot.nodeId) return field;
|
|
3311
|
+
const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
|
|
3312
|
+
return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
|
|
3313
|
+
})
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
if (slot.kind === "option") {
|
|
3317
|
+
return {
|
|
3318
|
+
...schema,
|
|
3319
|
+
fields: schema.fields.map((field) => {
|
|
3320
|
+
if (!("options" in field)) return field;
|
|
3321
|
+
return {
|
|
3322
|
+
...field,
|
|
3323
|
+
options: field.options.map((option) => {
|
|
3324
|
+
if (option.id !== slot.nodeId) return option;
|
|
3325
|
+
const translations = text.trim().length === 0 ? Object.fromEntries(
|
|
3326
|
+
Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
|
|
3327
|
+
) : { ...option.translations, [slot.locale]: text };
|
|
3328
|
+
return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
|
|
3329
|
+
})
|
|
3330
|
+
};
|
|
3331
|
+
})
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
return {
|
|
3335
|
+
...schema,
|
|
3336
|
+
...schema.pages === void 0 ? {} : {
|
|
3337
|
+
pages: schema.pages.map((page) => {
|
|
3338
|
+
if (page.id !== slot.nodeId) return page;
|
|
3339
|
+
const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
|
|
3340
|
+
return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
|
|
3341
|
+
})
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
function useTranslationWorkspace({
|
|
3346
|
+
schema,
|
|
3347
|
+
onChange,
|
|
3348
|
+
sourceLocale = schema.defaultLocale ?? "en",
|
|
3349
|
+
targetLocale,
|
|
3350
|
+
translationAdapter,
|
|
3351
|
+
readOnly = false
|
|
3352
|
+
}) {
|
|
3353
|
+
const [draftSchema, setDraftSchema] = (0, import_react4.useState)(schema);
|
|
3354
|
+
const [selectedLocale, setSelectedLocale] = (0, import_react4.useState)(targetLocale ?? "");
|
|
3355
|
+
const [isTranslating, setIsTranslating] = (0, import_react4.useState)(false);
|
|
3356
|
+
const [error, setError] = (0, import_react4.useState)();
|
|
3357
|
+
const currentSchema = onChange === void 0 ? draftSchema : schema;
|
|
3358
|
+
const targetLocales = (0, import_react4.useMemo)(() => {
|
|
3359
|
+
const locales = (0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales;
|
|
3360
|
+
return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
|
|
3361
|
+
(locale) => locale !== sourceLocale
|
|
3362
|
+
);
|
|
3363
|
+
}, [currentSchema, sourceLocale]);
|
|
3364
|
+
const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
|
|
3365
|
+
const slots = (0, import_react4.useMemo)(
|
|
3366
|
+
() => activeLocale.length === 0 ? [] : (0, import_core4.collectTranslationSlots)(currentSchema, activeLocale),
|
|
3367
|
+
[activeLocale, currentSchema]
|
|
3368
|
+
);
|
|
3369
|
+
const summary = (0, import_react4.useMemo)(() => {
|
|
3370
|
+
const counts = {
|
|
3371
|
+
missing: 0,
|
|
3372
|
+
translated: 0,
|
|
3373
|
+
stale: 0,
|
|
3374
|
+
manual: 0,
|
|
3375
|
+
"manual-stale": 0
|
|
3376
|
+
};
|
|
3377
|
+
for (const slot of slots) counts[slot.status ?? "missing"] += 1;
|
|
3378
|
+
const complete = counts.translated + counts.manual;
|
|
3379
|
+
return {
|
|
3380
|
+
totalSlots: slots.length,
|
|
3381
|
+
translatedCount: complete,
|
|
3382
|
+
missingCount: counts.missing,
|
|
3383
|
+
staleCount: counts.stale + counts["manual-stale"],
|
|
3384
|
+
manualCount: counts.manual + counts["manual-stale"],
|
|
3385
|
+
completionPercentage: slots.length === 0 ? 100 : Math.round(complete / slots.length * 100)
|
|
3386
|
+
};
|
|
3387
|
+
}, [slots]);
|
|
3388
|
+
const commit = (0, import_react4.useCallback)(
|
|
3389
|
+
(next) => {
|
|
3390
|
+
setDraftSchema(next);
|
|
3391
|
+
onChange?.(next);
|
|
3392
|
+
},
|
|
3393
|
+
[onChange]
|
|
3394
|
+
);
|
|
3395
|
+
const setTranslation = (0, import_react4.useCallback)(
|
|
3396
|
+
(slot, text) => {
|
|
3397
|
+
if (readOnly) return;
|
|
3398
|
+
commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
|
|
3399
|
+
},
|
|
3400
|
+
[commit, currentSchema, readOnly, sourceLocale]
|
|
3401
|
+
);
|
|
3402
|
+
const addLocale = (0, import_react4.useCallback)(
|
|
3403
|
+
(locale) => {
|
|
3404
|
+
if (readOnly || locale.trim().length === 0) return;
|
|
3405
|
+
const normalized = locale.trim();
|
|
3406
|
+
commit({
|
|
3407
|
+
...currentSchema,
|
|
3408
|
+
supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
|
|
3409
|
+
});
|
|
3410
|
+
setSelectedLocale(normalized);
|
|
3411
|
+
},
|
|
3412
|
+
[commit, currentSchema, readOnly]
|
|
3413
|
+
);
|
|
3414
|
+
const removeLocale = (0, import_react4.useCallback)(
|
|
3415
|
+
(locale) => {
|
|
3416
|
+
if (readOnly || locale === sourceLocale) return;
|
|
3417
|
+
commit({
|
|
3418
|
+
...currentSchema,
|
|
3419
|
+
supportedLocales: (currentSchema.supportedLocales ?? []).filter((candidate) => candidate !== locale)
|
|
3420
|
+
});
|
|
3421
|
+
if (activeLocale === locale) setSelectedLocale("");
|
|
3422
|
+
},
|
|
3423
|
+
[activeLocale, commit, currentSchema, readOnly, sourceLocale]
|
|
3424
|
+
);
|
|
3425
|
+
const translateAll = (0, import_react4.useCallback)(
|
|
3426
|
+
async (options = {}) => {
|
|
3427
|
+
if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
|
|
3428
|
+
if (activeLocale.length === 0) throw new Error("A target locale is required.");
|
|
3429
|
+
setIsTranslating(true);
|
|
3430
|
+
setError(void 0);
|
|
3431
|
+
try {
|
|
3432
|
+
const populated = await (0, import_core4.populateSchemaTranslations)(
|
|
3433
|
+
currentSchema,
|
|
3434
|
+
[activeLocale],
|
|
3435
|
+
asAsyncAdapter(translationAdapter),
|
|
3436
|
+
{
|
|
3437
|
+
overwrite: "stale-and-missing",
|
|
3438
|
+
preserveManualTranslations: true,
|
|
3439
|
+
...options
|
|
3440
|
+
}
|
|
3441
|
+
);
|
|
3442
|
+
commit(populated.schema);
|
|
3443
|
+
return populated.report;
|
|
3444
|
+
} catch (cause) {
|
|
3445
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3446
|
+
setError(message);
|
|
3447
|
+
throw cause;
|
|
3448
|
+
} finally {
|
|
3449
|
+
setIsTranslating(false);
|
|
3450
|
+
}
|
|
3451
|
+
},
|
|
3452
|
+
[activeLocale, commit, currentSchema, translationAdapter]
|
|
3453
|
+
);
|
|
3454
|
+
const translateSlot = (0, import_react4.useCallback)(
|
|
3455
|
+
async (slot) => {
|
|
3456
|
+
if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
|
|
3457
|
+
if (readOnly) return;
|
|
3458
|
+
setIsTranslating(true);
|
|
3459
|
+
setError(void 0);
|
|
3460
|
+
try {
|
|
3461
|
+
const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
|
|
3462
|
+
commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
|
|
3463
|
+
} catch (cause) {
|
|
3464
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3465
|
+
setError(message);
|
|
3466
|
+
throw cause;
|
|
3467
|
+
} finally {
|
|
3468
|
+
setIsTranslating(false);
|
|
3469
|
+
}
|
|
3470
|
+
},
|
|
3471
|
+
[commit, currentSchema, readOnly, sourceLocale, translationAdapter]
|
|
3472
|
+
);
|
|
3473
|
+
return {
|
|
3474
|
+
sourceLocale,
|
|
3475
|
+
targetLocale: activeLocale,
|
|
3476
|
+
targetLocales,
|
|
3477
|
+
setTargetLocale: setSelectedLocale,
|
|
3478
|
+
slots,
|
|
3479
|
+
summary,
|
|
3480
|
+
addLocale,
|
|
3481
|
+
removeLocale,
|
|
3482
|
+
setTranslation,
|
|
3483
|
+
translateAll,
|
|
3484
|
+
translateSlot,
|
|
3485
|
+
isTranslating,
|
|
3486
|
+
...error === void 0 ? {} : { error }
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// src/receipt.ts
|
|
3491
|
+
var import_react5 = require("react");
|
|
3155
3492
|
function submissionReceiptQueryKey(formId, formVersion) {
|
|
3156
3493
|
return `${formId}:v${formVersion}`;
|
|
3157
3494
|
}
|
|
@@ -3222,19 +3559,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
|
3222
3559
|
}
|
|
3223
3560
|
function useSubmissionReceipts(store, queries) {
|
|
3224
3561
|
const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
|
|
3225
|
-
const stableQueries = (0,
|
|
3562
|
+
const stableQueries = (0, import_react5.useMemo)(() => {
|
|
3226
3563
|
const parsed = JSON.parse(querySignature);
|
|
3227
3564
|
if (!Array.isArray(parsed)) return [];
|
|
3228
3565
|
return parsed.flatMap(
|
|
3229
3566
|
(entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
|
|
3230
3567
|
);
|
|
3231
3568
|
}, [querySignature]);
|
|
3232
|
-
const [state, setState] = (0,
|
|
3569
|
+
const [state, setState] = (0, import_react5.useState)({
|
|
3233
3570
|
receipts: /* @__PURE__ */ new Map(),
|
|
3234
3571
|
isLoading: stableQueries.length > 0,
|
|
3235
3572
|
error: null
|
|
3236
3573
|
});
|
|
3237
|
-
(0,
|
|
3574
|
+
(0, import_react5.useEffect)(() => {
|
|
3238
3575
|
let active = true;
|
|
3239
3576
|
if (stableQueries.length === 0) {
|
|
3240
3577
|
setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
|
|
@@ -3268,8 +3605,8 @@ function useSubmissionReceipts(store, queries) {
|
|
|
3268
3605
|
}
|
|
3269
3606
|
|
|
3270
3607
|
// src/renderer.tsx
|
|
3271
|
-
var
|
|
3272
|
-
var
|
|
3608
|
+
var import_core5 = require("@form-engine-ts/core");
|
|
3609
|
+
var import_react6 = require("react");
|
|
3273
3610
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
3274
3611
|
var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
|
|
3275
3612
|
function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
|
|
@@ -3780,41 +4117,41 @@ function ContextFormRenderer({
|
|
|
3780
4117
|
slots = {}
|
|
3781
4118
|
}) {
|
|
3782
4119
|
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,
|
|
4120
|
+
const prefix = (0, import_react6.useId)().replace(/:/g, "");
|
|
4121
|
+
const formRef = (0, import_react6.useRef)(null);
|
|
4122
|
+
const loadedDraftKey = (0, import_react6.useRef)(null);
|
|
4123
|
+
const [draftRestored, setDraftRestored] = (0, import_react6.useState)(false);
|
|
4124
|
+
const [currentPageIndex, setCurrentPageIndex] = (0, import_react6.useState)(0);
|
|
4125
|
+
const [focusFieldId, setFocusFieldId] = (0, import_react6.useState)(null);
|
|
4126
|
+
const [confirmation, setConfirmation] = (0, import_react6.useState)(null);
|
|
4127
|
+
const [guardMessage, setGuardMessage] = (0, import_react6.useState)(null);
|
|
4128
|
+
const [guardsPending, setGuardsPending] = (0, import_react6.useState)(false);
|
|
4129
|
+
const [receipt, setReceipt] = (0, import_react6.useState)(null);
|
|
4130
|
+
const [completionData, setCompletionData] = (0, import_react6.useState)(null);
|
|
4131
|
+
const [receiptLoaded, setReceiptLoaded] = (0, import_react6.useState)(receiptStore === void 0);
|
|
4132
|
+
const rendererSubmissionInFlight = (0, import_react6.useRef)(false);
|
|
4133
|
+
const fallbackAttemptId = (0, import_react6.useRef)(null);
|
|
4134
|
+
const completionRef = (0, import_react6.useRef)(null);
|
|
4135
|
+
const confirmationRef = (0, import_react6.useRef)(null);
|
|
3799
4136
|
const pages = form.schema.pages;
|
|
3800
|
-
const visiblePageIndexes = (0,
|
|
4137
|
+
const visiblePageIndexes = (0, import_react6.useMemo)(
|
|
3801
4138
|
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
3802
4139
|
[form.pageVisibility, pages]
|
|
3803
4140
|
);
|
|
3804
4141
|
const activePage = pages?.[currentPageIndex];
|
|
3805
4142
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
3806
4143
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
3807
|
-
const visibleValues = (0,
|
|
3808
|
-
const visibleItems = (0,
|
|
4144
|
+
const visibleValues = (0, import_react6.useMemo)(() => (0, import_core5.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
|
|
4145
|
+
const visibleItems = (0, import_react6.useMemo)(
|
|
3809
4146
|
() => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
|
|
3810
4147
|
[form.schema, form.translate, form.values, form.visibility]
|
|
3811
4148
|
);
|
|
3812
|
-
const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? "inline";
|
|
3813
|
-
const confirmationEnabled = submissionConfirmation?.enabled
|
|
4149
|
+
const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? form.schema.submissionSettings?.confirmationRenderMode ?? "inline";
|
|
4150
|
+
const confirmationEnabled = submissionConfirmation?.enabled ?? form.schema.submissionSettings?.showConfirmationBeforeSubmit ?? false;
|
|
3814
4151
|
const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
|
|
3815
4152
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
3816
4153
|
const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
|
|
3817
|
-
const resolveMessage = (0,
|
|
4154
|
+
const resolveMessage = (0, import_react6.useCallback)(
|
|
3818
4155
|
(key, fallback) => {
|
|
3819
4156
|
const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
|
|
3820
4157
|
const configured = messages[key];
|
|
@@ -3822,15 +4159,15 @@ function ContextFormRenderer({
|
|
|
3822
4159
|
},
|
|
3823
4160
|
[form.locale, messageResolver, messages]
|
|
3824
4161
|
);
|
|
3825
|
-
const fieldTranslate = (0,
|
|
4162
|
+
const fieldTranslate = (0, import_react6.useCallback)(
|
|
3826
4163
|
(key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
|
|
3827
4164
|
[form.translate, messageResolver, messages.requiredField, resolveMessage]
|
|
3828
4165
|
);
|
|
3829
|
-
const focusSubmitButton = (0,
|
|
4166
|
+
const focusSubmitButton = (0, import_react6.useCallback)(() => {
|
|
3830
4167
|
const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
|
|
3831
4168
|
button?.focus();
|
|
3832
4169
|
}, []);
|
|
3833
|
-
(0,
|
|
4170
|
+
(0, import_react6.useEffect)(() => {
|
|
3834
4171
|
let active = true;
|
|
3835
4172
|
if (receiptStore === void 0) {
|
|
3836
4173
|
setReceipt(null);
|
|
@@ -3851,14 +4188,14 @@ function ContextFormRenderer({
|
|
|
3851
4188
|
active = false;
|
|
3852
4189
|
};
|
|
3853
4190
|
}, [form.schema.id, form.schema.version, receiptStore]);
|
|
3854
|
-
(0,
|
|
4191
|
+
(0, import_react6.useEffect)(() => {
|
|
3855
4192
|
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
3856
4193
|
setCurrentPageIndex(0);
|
|
3857
4194
|
return;
|
|
3858
4195
|
}
|
|
3859
4196
|
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
3860
4197
|
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
3861
|
-
(0,
|
|
4198
|
+
(0, import_react6.useEffect)(() => {
|
|
3862
4199
|
if (focusFieldId === null) return;
|
|
3863
4200
|
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
3864
4201
|
(element) => element.dataset.fieldId === focusFieldId
|
|
@@ -3870,11 +4207,11 @@ function ContextFormRenderer({
|
|
|
3870
4207
|
setFocusFieldId(null);
|
|
3871
4208
|
}
|
|
3872
4209
|
}, [focusFieldId]);
|
|
3873
|
-
(0,
|
|
4210
|
+
(0, import_react6.useEffect)(() => {
|
|
3874
4211
|
if (!isReplaceMode || form.submitStatus !== "success") return;
|
|
3875
4212
|
completionRef.current?.focus();
|
|
3876
4213
|
}, [form.submitStatus, isReplaceMode]);
|
|
3877
|
-
(0,
|
|
4214
|
+
(0, import_react6.useEffect)(() => {
|
|
3878
4215
|
if (confirmation === null) return;
|
|
3879
4216
|
const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
|
|
3880
4217
|
confirmButton?.focus();
|
|
@@ -3906,7 +4243,7 @@ function ContextFormRenderer({
|
|
|
3906
4243
|
globalThis.addEventListener("keydown", onKeyDown);
|
|
3907
4244
|
return () => globalThis.removeEventListener("keydown", onKeyDown);
|
|
3908
4245
|
}, [confirmation, confirmationRenderMode, focusSubmitButton]);
|
|
3909
|
-
(0,
|
|
4246
|
+
(0, import_react6.useEffect)(() => {
|
|
3910
4247
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
3911
4248
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
3912
4249
|
if (loadedDraftKey.current === loadIdentity) return;
|
|
@@ -3918,7 +4255,7 @@ function ContextFormRenderer({
|
|
|
3918
4255
|
form.restoreValues(draft.values);
|
|
3919
4256
|
setDraftRestored(true);
|
|
3920
4257
|
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
3921
|
-
(0,
|
|
4258
|
+
(0, import_react6.useEffect)(() => {
|
|
3922
4259
|
if (form.submitStatus === "success") return;
|
|
3923
4260
|
const timeout = globalThis.setTimeout(() => {
|
|
3924
4261
|
onDraftSave?.(form.values);
|
|
@@ -3974,7 +4311,7 @@ function ContextFormRenderer({
|
|
|
3974
4311
|
if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
|
|
3975
4312
|
return { status: "cancelled" };
|
|
3976
4313
|
}
|
|
3977
|
-
const validation = (0,
|
|
4314
|
+
const validation = (0, import_core5.validateAnswers)(form.schema, form.values);
|
|
3978
4315
|
const firstInvalidFieldId = validation.issues[0]?.fieldId;
|
|
3979
4316
|
if (validation.valid && !guardsConfirmed) {
|
|
3980
4317
|
rendererSubmissionInFlight.current = true;
|
|
@@ -4276,7 +4613,7 @@ function ContextFormRenderer({
|
|
|
4276
4613
|
...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
|
|
4277
4614
|
};
|
|
4278
4615
|
if (slots.renderField !== void 0) {
|
|
4279
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4616
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react6.Fragment, { children: slots.renderField({
|
|
4280
4617
|
question: field,
|
|
4281
4618
|
value: form.values[field.id],
|
|
4282
4619
|
onChange: (value) => {
|
|
@@ -4446,5 +4783,6 @@ function FormRenderer(props) {
|
|
|
4446
4783
|
useField,
|
|
4447
4784
|
useForm,
|
|
4448
4785
|
useFormBuilder,
|
|
4449
|
-
useSubmissionReceipts
|
|
4786
|
+
useSubmissionReceipts,
|
|
4787
|
+
useTranslationWorkspace
|
|
4450
4788
|
});
|