@parallelworks/ui 0.34.0 → 0.36.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 +201 -0
- package/dist/ai/components/ChatEmptyState.js +71 -86
- package/dist/ai/components/ChatInput.d.ts +11 -3
- package/dist/ai/components/ChatInput.js +19 -11
- package/dist/ai/components/ChatSidebar.js +12 -130
- package/dist/ai/components/ChatStage.d.ts +11 -0
- package/dist/ai/components/ChatStage.js +26 -0
- package/dist/ai/components/ComposerChrome.d.ts +3 -0
- package/dist/ai/components/ComposerChrome.js +21 -2
- package/dist/ai/components/agent/AgentMessageParts.js +8 -2
- package/dist/ai/components/conversationActions.d.ts +23 -0
- package/dist/ai/components/conversationActions.js +108 -0
- package/dist/ai/index.d.ts +4 -3
- package/dist/ai/index.js +6 -5
- package/dist/ai/strings.d.ts +7 -0
- package/dist/ai/strings.js +8 -1
- package/dist/ai/types.d.ts +2 -0
- package/dist/ai/ui/ConversationSidebar.d.ts +3 -2
- package/dist/ai/ui/ConversationSidebar.js +2 -1
- package/dist/ai/ui/sidebar.d.ts +6 -0
- package/dist/ai/ui/sidebar.js +10 -1
- package/dist/components/Dropdown.js +3 -1
- package/dist/components/FilterPill.js +1 -0
- package/dist/components/Provider.d.ts +14 -0
- package/dist/components/Provider.js +14 -3
- package/dist/components/SegmentedControl.d.ts +24 -0
- package/dist/components/SegmentedControl.js +69 -0
- package/dist/editing.d.ts +32 -0
- package/dist/editor/FormCodePanel.js +1 -1
- package/dist/engine.d.ts +3 -0
- package/dist/form/FieldWrapper.js +1 -1
- package/dist/form/Fields/CheckboxGroupField.d.ts +3 -3
- package/dist/form/Fields/CheckboxGroupField.js +10 -4
- package/dist/form/Fields/RadioField.d.ts +5 -3
- package/dist/form/Fields/RadioField.js +25 -17
- package/dist/form/Fields/fieldOption.d.ts +12 -0
- package/dist/form/Fields/fieldOption.js +17 -0
- package/dist/form/Form.d.ts +4 -2
- package/dist/form/Form.js +170 -68
- package/dist/form/FormLayout.d.ts +7 -0
- package/dist/form/FormLayout.js +144 -0
- package/dist/form/SpecialTypes/FormikCustomDropdown.js +1 -1
- package/dist/form/SpecialTypes/FormikCustomInput.js +7 -0
- package/dist/form/Wizard/WizardContainer.d.ts +1 -1
- package/dist/form/Wizard/WizardContainer.js +244 -49
- package/dist/form/Wizard/WizardNavigation.js +4 -4
- package/dist/form/Wizard/WizardStepContent.d.ts +1 -1
- package/dist/form/Wizard/WizardStepContent.js +19 -10
- package/dist/form/Wizard/WizardStepIndicator.js +35 -7
- package/dist/form/Wizard/types.d.ts +19 -6
- package/dist/form/Wizard/useWizardState.d.ts +1 -0
- package/dist/form/Wizard/useWizardState.js +5 -1
- package/dist/form/Wizard/utils.d.ts +7 -0
- package/dist/form/Wizard/utils.js +18 -1
- package/dist/form/index.d.ts +2 -0
- package/dist/form/index.js +4 -2
- package/dist/form/layout.d.ts +50 -0
- package/dist/form/layout.js +125 -0
- package/dist/form/layoutCSS.d.ts +10 -0
- package/dist/form/layoutCSS.js +234 -0
- package/dist/form/lib.d.ts +2 -0
- package/dist/form/lib.js +17 -2
- package/dist/form/types/fieldTypes.d.ts +8 -23
- package/dist/graph/DependencyGraph.d.ts +20 -1
- package/dist/graph/DependencyGraph.js +437 -142
- package/dist/graph/MatrixGroup.d.ts +3 -1
- package/dist/graph/MatrixGroup.js +31 -28
- package/dist/graph/TreeView.js +6 -6
- package/dist/graph/gridSpacing.d.ts +7 -0
- package/dist/graph/gridSpacing.js +10 -0
- package/dist/graph/util.d.ts +2 -0
- package/dist/graph/util.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/list/ListViewControls.d.ts +15 -0
- package/dist/list/ListViewControls.js +9 -3
- package/dist/list/index.d.ts +1 -1
- package/dist/list/index.js +2 -2
- package/dist/styles/base.css +3 -1
- package/dist/styles/theme.css +5 -0
- package/dist/styles.css +1 -1
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +3 -2
- package/package.json +4 -2
- package/dist/ai/components/ComposerFrame.d.ts +0 -16
- package/dist/ai/components/ComposerFrame.js +0 -33
|
@@ -1,49 +1,172 @@
|
|
|
1
|
+
import { useStrings, useWorkflowEngine } from "../../components/Provider.js";
|
|
2
|
+
import { copyBounds, stepText } from "./utils.js";
|
|
3
|
+
import { initializeValues } from "../lib.js";
|
|
4
|
+
import { getValueUsingPath } from "../utils/getValueUsingPath.js";
|
|
1
5
|
import { useWizardState } from "./useWizardState.js";
|
|
2
6
|
import { WizardNavigation } from "./WizardNavigation.js";
|
|
3
7
|
import { WizardStepContent } from "./WizardStepContent.js";
|
|
4
8
|
import { WizardStepIndicator } from "./WizardStepIndicator.js";
|
|
5
|
-
import { useCallback } from "react";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
6
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
11
|
import { useFormikContext } from "formik";
|
|
8
12
|
//#region src/form/Wizard/WizardContainer.tsx
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const { validateForm, setTouched, touched } = useFormikContext();
|
|
13
|
+
function WizardContainer({ wizardConfig, values, onChange, onSubmit, className = "", labelPosition = "left", missingFields = [], spaceCompact = false, workflowForm = false, nested = false, fieldNamePrefix = "", setFieldValue, setFieldTouched }) {
|
|
14
|
+
const { validateForm, setTouched, touched, initialValues } = useFormikContext();
|
|
15
|
+
const { form: strings } = useStrings();
|
|
16
|
+
const engine = useWorkflowEngine();
|
|
14
17
|
const { config, steps, stepOrder } = wizardConfig;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const reads = useRef(/* @__PURE__ */ new Map());
|
|
19
|
+
const perCopy = useCallback((value, copy, numbered) => {
|
|
20
|
+
if (Array.isArray(value)) return value[copy] ?? (numbered ? strings.copyTitle(value[0] ?? "", copy + 1) : "");
|
|
21
|
+
const text = value ?? "";
|
|
22
|
+
if (text.includes("${{")) {
|
|
23
|
+
const key = JSON.stringify([text, copy]);
|
|
24
|
+
const inputs = JSON.stringify([...engine.inputDependencies(text).inputDeps].map((dep) => [dep, getValueUsingPath(values, dep)]));
|
|
25
|
+
const last = reads.current.get(key);
|
|
26
|
+
if (last?.inputs === inputs) return last.read;
|
|
27
|
+
const result = engine.evaluate({
|
|
28
|
+
inputs: values,
|
|
29
|
+
obj: { text },
|
|
30
|
+
orgVars: {},
|
|
31
|
+
index: copy
|
|
32
|
+
});
|
|
33
|
+
const read = String(result.text ?? "");
|
|
34
|
+
if (reads.current.size >= 500) reads.current.clear();
|
|
35
|
+
reads.current.set(key, {
|
|
36
|
+
inputs,
|
|
37
|
+
read
|
|
38
|
+
});
|
|
39
|
+
return read;
|
|
40
|
+
}
|
|
41
|
+
return numbered && text ? strings.copyTitle(text, copy + 1) : text;
|
|
42
|
+
}, [
|
|
43
|
+
engine,
|
|
44
|
+
values,
|
|
45
|
+
strings
|
|
46
|
+
]);
|
|
47
|
+
const shown = useMemo(() => {
|
|
48
|
+
const order = [];
|
|
49
|
+
const byKey = {};
|
|
50
|
+
for (const step of stepOrder) {
|
|
51
|
+
const stepConfig = steps[step];
|
|
52
|
+
if (!stepConfig) continue;
|
|
53
|
+
if (stepConfig.multi !== true) {
|
|
54
|
+
order.push(step);
|
|
55
|
+
byKey[step] = {
|
|
56
|
+
step,
|
|
57
|
+
config: stepConfig
|
|
58
|
+
};
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const rows = getValueUsingPath(values, `${fieldNamePrefix}${step}`);
|
|
62
|
+
const { lo, hi } = copyBounds(stepConfig.min, stepConfig.max);
|
|
63
|
+
const count = Math.min(Math.max(Array.isArray(rows) ? rows.length : 0, lo), hi ?? Infinity);
|
|
64
|
+
if (count === 0) {
|
|
65
|
+
if (hi === 0) continue;
|
|
66
|
+
order.push(step);
|
|
67
|
+
byKey[step] = {
|
|
68
|
+
step,
|
|
69
|
+
count,
|
|
70
|
+
canAdd: hi === void 0 || hi > 0,
|
|
71
|
+
canRemove: false,
|
|
72
|
+
config: {
|
|
73
|
+
...stepConfig,
|
|
74
|
+
title: perCopy(stepConfig.title, 0, false),
|
|
75
|
+
description: perCopy(stepConfig.description, 0, false),
|
|
76
|
+
options: {}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (let copy = 0; copy < count; copy++) {
|
|
82
|
+
const key = `${step}[${copy}]`;
|
|
83
|
+
order.push(key);
|
|
84
|
+
byKey[key] = {
|
|
85
|
+
step,
|
|
86
|
+
copy,
|
|
87
|
+
count,
|
|
88
|
+
canAdd: hi === void 0 || count < hi,
|
|
89
|
+
canRemove: count > lo,
|
|
90
|
+
config: {
|
|
91
|
+
...stepConfig,
|
|
92
|
+
title: perCopy(stepConfig.title, copy, true),
|
|
93
|
+
description: perCopy(stepConfig.description, copy, false)
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
order,
|
|
100
|
+
byKey
|
|
101
|
+
};
|
|
102
|
+
}, [
|
|
103
|
+
stepOrder,
|
|
104
|
+
steps,
|
|
105
|
+
values,
|
|
106
|
+
fieldNamePrefix,
|
|
107
|
+
perCopy
|
|
108
|
+
]);
|
|
109
|
+
const setAside = useRef({});
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
setAside.current = {};
|
|
112
|
+
}, [initialValues]);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
for (const step of stepOrder) {
|
|
115
|
+
const stepConfig = steps[step];
|
|
116
|
+
if (stepConfig?.multi !== true) continue;
|
|
117
|
+
const { lo, hi } = copyBounds(stepConfig.min, stepConfig.max);
|
|
118
|
+
const path = `${fieldNamePrefix}${step}`;
|
|
119
|
+
const rows = getValueUsingPath(values, path);
|
|
120
|
+
const now = Array.isArray(rows) ? rows : [];
|
|
121
|
+
setAside.current[path] ??= [];
|
|
122
|
+
const aside = setAside.current[path];
|
|
123
|
+
if (hi !== void 0 && now.length > hi) {
|
|
124
|
+
now.slice(hi).forEach((row, i) => {
|
|
125
|
+
aside[hi + i] = row;
|
|
126
|
+
});
|
|
127
|
+
setFieldValue(path, now.slice(0, hi));
|
|
128
|
+
} else if (now.length < lo) setFieldValue(path, Array.from({ length: lo }, (_, i) => now[i] ?? takeAside(aside, i) ?? initializeValues(stepConfig.options, {}) ?? {}));
|
|
129
|
+
}
|
|
130
|
+
}, [
|
|
131
|
+
stepOrder,
|
|
132
|
+
steps,
|
|
133
|
+
values,
|
|
134
|
+
fieldNamePrefix,
|
|
135
|
+
setFieldValue
|
|
136
|
+
]);
|
|
137
|
+
const configs = useMemo(() => Object.fromEntries(Object.entries(shown.byKey).map(([key, page]) => [key, page.config])), [shown]);
|
|
138
|
+
const validateCurrentStep = useCallback(async (pageKey) => {
|
|
139
|
+
const page = shown.byKey[pageKey];
|
|
140
|
+
if (!page?.config.options) return true;
|
|
141
|
+
const stepPrefix = page.copy !== void 0 ? `${fieldNamePrefix}${page.step}[${page.copy}].` : config.flatten !== false ? fieldNamePrefix : `${fieldNamePrefix}${page.step}.`;
|
|
142
|
+
const stepFieldPaths = Object.keys(page.config.options).map((name) => `${stepPrefix}${name}`);
|
|
20
143
|
const errors = await validateForm();
|
|
21
|
-
const hasStepErrors =
|
|
144
|
+
const hasStepErrors = stepFieldPaths.some((path) => getValueUsingPath(errors, path));
|
|
22
145
|
if (hasStepErrors) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}, false);
|
|
146
|
+
if (stepPrefix) for (const path of stepFieldPaths) setFieldTouched(path, true, false);
|
|
147
|
+
else {
|
|
148
|
+
const touchedFields = Object.fromEntries(stepFieldPaths.map((path) => [path, true]));
|
|
149
|
+
setTouched({
|
|
150
|
+
...touched,
|
|
151
|
+
...touchedFields
|
|
152
|
+
}, false);
|
|
153
|
+
}
|
|
32
154
|
}
|
|
33
155
|
return !hasStepErrors;
|
|
34
156
|
}, [
|
|
35
|
-
|
|
157
|
+
shown,
|
|
36
158
|
config,
|
|
159
|
+
fieldNamePrefix,
|
|
37
160
|
validateForm,
|
|
38
161
|
setTouched,
|
|
162
|
+
setFieldTouched,
|
|
39
163
|
touched
|
|
40
164
|
]);
|
|
41
|
-
const { currentStep, visitedSteps, invalidSteps, isLastStep, canGoBack, goToNext, goToPrevious, jumpToStep } = useWizardState({
|
|
42
|
-
stepOrder,
|
|
43
|
-
steps,
|
|
165
|
+
const { currentStep, visitedSteps, invalidSteps, isLastStep, canGoBack, goToNext, goToPrevious, jumpToStep, showStep } = useWizardState({
|
|
166
|
+
stepOrder: shown.order,
|
|
167
|
+
steps: configs,
|
|
44
168
|
validateStep: validateCurrentStep
|
|
45
169
|
});
|
|
46
|
-
const currentStepConfig = steps[currentStep];
|
|
47
170
|
const navigation = config.navigation ?? {};
|
|
48
171
|
const handleGoToNext = useCallback(async () => {
|
|
49
172
|
const success = await goToNext();
|
|
@@ -74,48 +197,120 @@ function WizardContainer({ wizardConfig, values, onChange, onSubmit, className =
|
|
|
74
197
|
touched,
|
|
75
198
|
onSubmit
|
|
76
199
|
]);
|
|
200
|
+
const shownBefore = useRef(shown.order);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
shownBefore.current = shown.order;
|
|
203
|
+
});
|
|
204
|
+
const pageKey = shown.byKey[currentStep] ? currentStep : nearestPage(currentStep, shown.order, shownBefore.current);
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (pageKey && pageKey !== currentStep) showStep(pageKey);
|
|
207
|
+
}, [
|
|
208
|
+
pageKey,
|
|
209
|
+
currentStep,
|
|
210
|
+
showStep
|
|
211
|
+
]);
|
|
212
|
+
const page = shown.byKey[pageKey];
|
|
213
|
+
const copiesOf = (step) => {
|
|
214
|
+
const rows = getValueUsingPath(values, `${fieldNamePrefix}${step}`);
|
|
215
|
+
return Array.isArray(rows) ? rows : [];
|
|
216
|
+
};
|
|
217
|
+
const addCopy = (shownPage) => {
|
|
218
|
+
const options = steps[shownPage.step]?.options;
|
|
219
|
+
const rows = copiesOf(shownPage.step);
|
|
220
|
+
const kept = Array.from({ length: shownPage.count ?? rows.length }, (_, i) => rows[i] ?? initializeValues(options, {}) ?? {});
|
|
221
|
+
const aside = setAside.current[`${fieldNamePrefix}${shownPage.step}`] ?? [];
|
|
222
|
+
setFieldValue(`${fieldNamePrefix}${shownPage.step}`, [...kept, takeAside(aside, kept.length) ?? initializeValues(options, {}) ?? {}]);
|
|
223
|
+
showStep(`${shownPage.step}[${kept.length}]`);
|
|
224
|
+
};
|
|
225
|
+
const removeCopy = (shownPage) => {
|
|
226
|
+
const copy = shownPage.copy ?? 0;
|
|
227
|
+
const rows = copiesOf(shownPage.step).filter((_, i) => i !== copy);
|
|
228
|
+
setFieldValue(`${fieldNamePrefix}${shownPage.step}`, rows);
|
|
229
|
+
showStep(rows.length > 0 ? `${shownPage.step}[${Math.max(copy - 1, 0)}]` : shownPage.step);
|
|
230
|
+
};
|
|
231
|
+
const rawTitle = page ? steps[page.step]?.title : void 0;
|
|
232
|
+
const titledPerCopy = Array.isArray(rawTitle) || typeof rawTitle === "string" && rawTitle.includes("${{");
|
|
233
|
+
const removeTitle = titledPerCopy && page ? stepText(page.config.title) : stepText(rawTitle);
|
|
234
|
+
const addTitle = titledPerCopy && page?.count !== void 0 ? perCopy(rawTitle, page.count, true) : stepText(rawTitle);
|
|
235
|
+
const copies = page?.count !== void 0 && (page.canAdd || page.canRemove) ? /* @__PURE__ */ jsxs("div", {
|
|
236
|
+
className: "mb-4 flex items-center justify-between gap-3",
|
|
237
|
+
children: [page.canRemove ? /* @__PURE__ */ jsx("button", {
|
|
238
|
+
type: "button",
|
|
239
|
+
onClick: () => removeCopy(page),
|
|
240
|
+
className: "rounded-lg border theme-border px-4 py-2 text-sm theme-muted-text hover:text-(--theme-app)",
|
|
241
|
+
children: strings.removeCopy(removeTitle)
|
|
242
|
+
}) : /* @__PURE__ */ jsx("span", {}), page.canAdd && (page.copy === void 0 || page.copy === page.count - 1) && /* @__PURE__ */ jsx("button", {
|
|
243
|
+
type: "button",
|
|
244
|
+
onClick: () => addCopy(page),
|
|
245
|
+
className: "rounded-lg bg-(--theme-element) px-4 py-2 font-medium text-(--theme-element-text) text-sm",
|
|
246
|
+
children: strings.addCopy(addTitle)
|
|
247
|
+
})]
|
|
248
|
+
}) : null;
|
|
249
|
+
const content = page ? /* @__PURE__ */ jsxs("div", {
|
|
250
|
+
className: "mb-4",
|
|
251
|
+
children: [/* @__PURE__ */ jsx(WizardStepContent, {
|
|
252
|
+
currentStep: page.step,
|
|
253
|
+
stepConfig: page.config,
|
|
254
|
+
flatten: config.flatten,
|
|
255
|
+
values,
|
|
256
|
+
labelPosition,
|
|
257
|
+
missingFields,
|
|
258
|
+
spaceCompact,
|
|
259
|
+
setFormDirty: () => onChange?.(values),
|
|
260
|
+
workflowForm,
|
|
261
|
+
fieldNamePrefix,
|
|
262
|
+
copy: page.copy,
|
|
263
|
+
setFieldValue,
|
|
264
|
+
setFieldTouched
|
|
265
|
+
}), copies]
|
|
266
|
+
}) : null;
|
|
77
267
|
return /* @__PURE__ */ jsxs("div", {
|
|
78
268
|
className,
|
|
79
269
|
children: [
|
|
80
270
|
navigation.showSteps !== false && /* @__PURE__ */ jsx(WizardStepIndicator, {
|
|
81
|
-
stepOrder,
|
|
82
|
-
currentStep,
|
|
83
|
-
steps,
|
|
271
|
+
stepOrder: shown.order,
|
|
272
|
+
currentStep: pageKey,
|
|
273
|
+
steps: configs,
|
|
84
274
|
visitedSteps,
|
|
85
275
|
invalidSteps,
|
|
86
276
|
onStepClick: jumpToStep,
|
|
87
277
|
allowJump: navigation.allowJump,
|
|
88
278
|
hideStepNumbers: navigation.hideStepNumbers
|
|
89
279
|
}),
|
|
90
|
-
|
|
91
|
-
className: "mb-4",
|
|
92
|
-
children: /* @__PURE__ */ jsx(WizardStepContent, {
|
|
93
|
-
currentStep,
|
|
94
|
-
stepConfig: currentStepConfig,
|
|
95
|
-
flatten: config.flatten,
|
|
96
|
-
values,
|
|
97
|
-
labelPosition,
|
|
98
|
-
missingFields,
|
|
99
|
-
spaceCompact,
|
|
100
|
-
setFormDirty: () => onChange?.(values),
|
|
101
|
-
workflowForm,
|
|
102
|
-
setFieldValue,
|
|
103
|
-
setFieldTouched
|
|
104
|
-
})
|
|
105
|
-
}),
|
|
280
|
+
content,
|
|
106
281
|
/* @__PURE__ */ jsx(WizardNavigation, {
|
|
107
282
|
isLastStep,
|
|
108
283
|
canGoBack,
|
|
109
|
-
isCurrentStepValid: !invalidSteps.has(
|
|
110
|
-
nextLabel:
|
|
111
|
-
prevLabel:
|
|
284
|
+
isCurrentStepValid: !invalidSteps.has(pageKey),
|
|
285
|
+
nextLabel: page?.config.nextLabel,
|
|
286
|
+
prevLabel: page?.config.prevLabel,
|
|
112
287
|
submitLabel: config.submitLabel || "Execute",
|
|
113
288
|
onNext: handleGoToNext,
|
|
114
289
|
onPrevious: goToPrevious,
|
|
115
|
-
onSubmit: handleSubmit
|
|
290
|
+
onSubmit: nested ? void 0 : handleSubmit
|
|
116
291
|
})
|
|
117
292
|
]
|
|
118
293
|
});
|
|
119
294
|
}
|
|
295
|
+
function nearestPage(key, order, before) {
|
|
296
|
+
const [, step = key, index = "0"] = /^(.*)\[(\d+)\]$/.exec(key) ?? [];
|
|
297
|
+
const copies = order.filter((other) => other.startsWith(`${step}[`));
|
|
298
|
+
if (copies.length > 0) return copies[Math.min(Number(index), copies.length - 1)] ?? copies[0] ?? "";
|
|
299
|
+
if (order.includes(step)) return step;
|
|
300
|
+
const pagesOf = (page) => order.filter((other) => stepOf(other) === stepOf(page));
|
|
301
|
+
const at = before.indexOf(key);
|
|
302
|
+
const next = before.slice(at + 1).find((page) => pagesOf(page).length > 0);
|
|
303
|
+
const last = before.slice(0, Math.max(at, 0)).reverse().find((page) => pagesOf(page).length > 0);
|
|
304
|
+
return (next && pagesOf(next)[0]) ?? (last && pagesOf(last).at(-1)) ?? order[0] ?? "";
|
|
305
|
+
}
|
|
306
|
+
function stepOf(page) {
|
|
307
|
+
return /^(.*)\[\d+\]$/.exec(page)?.[1] ?? page;
|
|
308
|
+
}
|
|
309
|
+
/** The row set aside at `index`, now taken back so it returns only once. */
|
|
310
|
+
function takeAside(aside, index) {
|
|
311
|
+
const row = aside[index];
|
|
312
|
+
delete aside[index];
|
|
313
|
+
return row;
|
|
314
|
+
}
|
|
120
315
|
//#endregion
|
|
121
316
|
export { WizardContainer };
|
|
@@ -20,7 +20,7 @@ function WizardNavigation({ isLastStep, canGoBack, isCurrentStepValid, nextLabel
|
|
|
20
20
|
setIsLoading(true);
|
|
21
21
|
setError(null);
|
|
22
22
|
try {
|
|
23
|
-
await onSubmit();
|
|
23
|
+
await onSubmit?.();
|
|
24
24
|
} catch (err) {
|
|
25
25
|
setError(err instanceof Error ? err.message : "An error occurred");
|
|
26
26
|
} finally {
|
|
@@ -50,11 +50,11 @@ function WizardNavigation({ isLastStep, canGoBack, isCurrentStepValid, nextLabel
|
|
|
50
50
|
},
|
|
51
51
|
"aria-label": "Go to previous step",
|
|
52
52
|
children: prevLabel || "← Previous"
|
|
53
|
-
}), isLastStep ? /* @__PURE__ */ jsxs("button", {
|
|
53
|
+
}), isLastStep && !onSubmit ? null : isLastStep ? /* @__PURE__ */ jsxs("button", {
|
|
54
54
|
type: "button",
|
|
55
55
|
onClick: handleSubmit,
|
|
56
56
|
disabled: !isCurrentStepValid || isLoading,
|
|
57
|
-
className: "px-8 py-3 rounded-lg font-medium transition-all duration-200 text-
|
|
57
|
+
className: "px-8 py-3 rounded-lg font-medium transition-all duration-200 text-(--theme-element-text) flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg",
|
|
58
58
|
style: { backgroundColor: "var(--theme-element)" },
|
|
59
59
|
onMouseEnter: (e) => {
|
|
60
60
|
if (isCurrentStepValid && !isLoading) e.currentTarget.style.filter = "brightness(0.9)";
|
|
@@ -71,7 +71,7 @@ function WizardNavigation({ isLastStep, canGoBack, isCurrentStepValid, nextLabel
|
|
|
71
71
|
type: "button",
|
|
72
72
|
onClick: handleNext,
|
|
73
73
|
disabled: isLoading,
|
|
74
|
-
className: "px-8 py-3 rounded-lg font-medium transition-all duration-200 text-
|
|
74
|
+
className: "px-8 py-3 rounded-lg font-medium transition-all duration-200 text-(--theme-element-text) flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg",
|
|
75
75
|
style: { backgroundColor: "var(--theme-element)" },
|
|
76
76
|
onMouseEnter: (e) => {
|
|
77
77
|
if (!isLoading) e.currentTarget.style.filter = "brightness(0.9)";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TSetFormDirty } from '../Form.js';
|
|
2
2
|
import type { WizardStepContentProps } from './types.js';
|
|
3
|
-
export declare function WizardStepContent({ currentStep, stepConfig, flatten, values, setFormDirty, setFieldValue, setFieldTouched, ...fieldsProps }: WizardStepContentProps & {
|
|
3
|
+
export declare function WizardStepContent({ currentStep, stepConfig, flatten, values, setFormDirty, setFieldValue, setFieldTouched, fieldNamePrefix, copy, ...fieldsProps }: WizardStepContentProps & {
|
|
4
4
|
setFormDirty: TSetFormDirty;
|
|
5
5
|
setFieldValue: (field: string, value: unknown, shouldValidate?: boolean) => void;
|
|
6
6
|
setFieldTouched: (field: string, touched?: boolean, shouldValidate?: boolean) => void;
|
|
@@ -1,20 +1,32 @@
|
|
|
1
|
+
import { stepText } from "./utils.js";
|
|
1
2
|
import { FieldsFromOptions } from "../Form.js";
|
|
2
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
//#region src/form/Wizard/WizardStepContent.tsx
|
|
4
|
-
function WizardStepContent({ currentStep, stepConfig, flatten = true, values, setFormDirty, setFieldValue, setFieldTouched, ...fieldsProps }) {
|
|
5
|
+
function WizardStepContent({ currentStep, stepConfig, flatten = true, values, setFormDirty, setFieldValue, setFieldTouched, fieldNamePrefix = "", copy, ...fieldsProps }) {
|
|
5
6
|
if (!stepConfig.options) return null;
|
|
7
|
+
const parentInfo = copy !== void 0 ? {
|
|
8
|
+
parentName: currentStep,
|
|
9
|
+
fieldNamePrefix: `${fieldNamePrefix}${currentStep}[${copy}].`,
|
|
10
|
+
arrayIndex: copy
|
|
11
|
+
} : flatten === false ? {
|
|
12
|
+
parentName: currentStep,
|
|
13
|
+
fieldNamePrefix: `${fieldNamePrefix}${currentStep}.`
|
|
14
|
+
} : fieldNamePrefix ? {
|
|
15
|
+
parentName: fieldNamePrefix.slice(0, -1),
|
|
16
|
+
fieldNamePrefix
|
|
17
|
+
} : void 0;
|
|
6
18
|
return /* @__PURE__ */ jsxs("div", {
|
|
7
19
|
className: "mb-4 w-full",
|
|
8
20
|
children: [
|
|
9
|
-
stepConfig.title && /* @__PURE__ */ jsx("h2", {
|
|
21
|
+
stepText(stepConfig.title) && /* @__PURE__ */ jsx("h2", {
|
|
10
22
|
className: "text-2xl font-bold mb-2",
|
|
11
23
|
style: { color: "var(--theme-app)" },
|
|
12
|
-
children: stepConfig.title
|
|
24
|
+
children: stepText(stepConfig.title)
|
|
13
25
|
}),
|
|
14
|
-
stepConfig.description && /* @__PURE__ */ jsx("p", {
|
|
26
|
+
stepText(stepConfig.description) && /* @__PURE__ */ jsx("p", {
|
|
15
27
|
className: "mb-4 leading-relaxed",
|
|
16
28
|
style: { color: "var(--theme-muted-text-color)" },
|
|
17
|
-
children: stepConfig.description
|
|
29
|
+
children: stepText(stepConfig.description)
|
|
18
30
|
}),
|
|
19
31
|
/* @__PURE__ */ jsx(FieldsFromOptions, {
|
|
20
32
|
options: stepConfig.options,
|
|
@@ -22,12 +34,9 @@ function WizardStepContent({ currentStep, stepConfig, flatten = true, values, se
|
|
|
22
34
|
setFormDirty,
|
|
23
35
|
setFieldValue,
|
|
24
36
|
setFieldTouched,
|
|
25
|
-
...
|
|
26
|
-
parentName: currentStep,
|
|
27
|
-
fieldNamePrefix: `${currentStep}.`
|
|
28
|
-
} } : {},
|
|
37
|
+
...parentInfo ? { parentInfo } : {},
|
|
29
38
|
...fieldsProps
|
|
30
|
-
}, currentStep)
|
|
39
|
+
}, copy === void 0 ? currentStep : `${currentStep}[${copy}]`)
|
|
31
40
|
]
|
|
32
41
|
});
|
|
33
42
|
}
|
|
@@ -1,15 +1,42 @@
|
|
|
1
|
+
import { useStrings } from "../../components/Provider.js";
|
|
2
|
+
import { stepText } from "./utils.js";
|
|
1
3
|
import cx from "classnames";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
6
|
//#region src/form/Wizard/WizardStepIndicator.tsx
|
|
4
7
|
function WizardStepIndicator({ stepOrder, currentStep, steps, visitedSteps, invalidSteps, onStepClick, allowJump = false, hideStepNumbers = false }) {
|
|
8
|
+
const { form: strings } = useStrings();
|
|
5
9
|
const currentIndex = stepOrder.indexOf(currentStep);
|
|
10
|
+
const row = useRef(null);
|
|
11
|
+
const [scrolls, setScrolls] = useState(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const el = row.current;
|
|
14
|
+
if (!el) return;
|
|
15
|
+
const measure = () => setScrolls(el.scrollWidth > el.clientWidth);
|
|
16
|
+
measure();
|
|
17
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
18
|
+
const observer = new ResizeObserver(measure);
|
|
19
|
+
for (const box of [el, ...el.children]) observer.observe(box);
|
|
20
|
+
return () => observer.disconnect();
|
|
21
|
+
}, [stepOrder.length]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const el = row.current;
|
|
24
|
+
const step = el?.children[currentIndex];
|
|
25
|
+
if (!el || !step || el.scrollWidth <= el.clientWidth) return;
|
|
26
|
+
const box = el.getBoundingClientRect();
|
|
27
|
+
const at = step.getBoundingClientRect();
|
|
28
|
+
if (at.left < box.left || at.right > box.right) el.scrollLeft += at.left - box.left - (box.width - at.width) / 2;
|
|
29
|
+
}, [currentIndex]);
|
|
6
30
|
const getStepStatus = (stepKey, index) => {
|
|
7
31
|
if (stepKey === currentStep) return "active";
|
|
8
32
|
if (index < currentIndex) return "completed";
|
|
9
33
|
return "upcoming";
|
|
10
34
|
};
|
|
11
|
-
return /* @__PURE__ */ jsx("
|
|
12
|
-
|
|
35
|
+
return /* @__PURE__ */ jsx("ol", {
|
|
36
|
+
ref: row,
|
|
37
|
+
"aria-label": strings.steps,
|
|
38
|
+
tabIndex: scrolls ? 0 : void 0,
|
|
39
|
+
className: "flex items-start justify-between w-full mb-2 gap-4 py-2 overflow-x-auto",
|
|
13
40
|
children: stepOrder.map((stepKey, index) => {
|
|
14
41
|
const stepConfig = steps[stepKey];
|
|
15
42
|
const status = getStepStatus(stepKey, index);
|
|
@@ -17,13 +44,14 @@ function WizardStepIndicator({ stepOrder, currentStep, steps, visitedSteps, inva
|
|
|
17
44
|
const isClickable = allowJump && visitedSteps.has(stepKey) && stepKey !== currentStep;
|
|
18
45
|
const isDotActive = status === "active";
|
|
19
46
|
const isDotCompleted = status === "completed";
|
|
20
|
-
return /* @__PURE__ */ jsxs("
|
|
47
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
21
48
|
className: "flex flex-col items-center flex-1 relative",
|
|
22
49
|
children: [
|
|
23
50
|
/* @__PURE__ */ jsxs("button", {
|
|
24
51
|
type: "button",
|
|
25
52
|
onClick: () => onStepClick(stepKey),
|
|
26
53
|
disabled: !isClickable,
|
|
54
|
+
"aria-current": isDotActive ? "step" : void 0,
|
|
27
55
|
className: cx("w-12 h-12 rounded-full border-3 flex items-center justify-center font-bold text-base transition-all duration-300 flex-shrink-0 relative", {
|
|
28
56
|
"cursor-pointer": isClickable,
|
|
29
57
|
"cursor-not-allowed": !isClickable
|
|
@@ -40,8 +68,8 @@ function WizardStepIndicator({ stepOrder, currentStep, steps, visitedSteps, inva
|
|
|
40
68
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
|
41
69
|
zIndex: 10
|
|
42
70
|
} : { zIndex: 10 },
|
|
43
|
-
"aria-label": `Step ${index + 1}: ${stepConfig?.title}`,
|
|
44
|
-
title: stepConfig?.title,
|
|
71
|
+
"aria-label": `Step ${index + 1}: ${stepText(stepConfig?.title)}`,
|
|
72
|
+
title: stepText(stepConfig?.title),
|
|
45
73
|
children: [!hideStepNumbers && /* @__PURE__ */ jsx("span", {
|
|
46
74
|
className: cx({ hidden: status === "completed" }),
|
|
47
75
|
children: index + 1
|
|
@@ -83,10 +111,10 @@ function WizardStepIndicator({ stepOrder, currentStep, steps, visitedSteps, inva
|
|
|
83
111
|
"font-bold text-white": isDotActive
|
|
84
112
|
}),
|
|
85
113
|
style: isDotActive ? { color: "var(--theme-element)" } : {},
|
|
86
|
-
children: stepConfig?.title
|
|
114
|
+
children: stepText(stepConfig?.title)
|
|
87
115
|
}), stepConfig?.description && /* @__PURE__ */ jsx("div", {
|
|
88
116
|
className: "text-xs theme-muted-text mt-1 text-center max-w-30",
|
|
89
|
-
children: stepConfig.description
|
|
117
|
+
children: stepText(stepConfig.description)
|
|
90
118
|
})]
|
|
91
119
|
})
|
|
92
120
|
]
|
|
@@ -23,10 +23,10 @@ export interface WizardConfig {
|
|
|
23
23
|
export interface StepFieldConfig {
|
|
24
24
|
/** Must be 'step' */
|
|
25
25
|
type: 'step';
|
|
26
|
-
/** Title shown in step indicator */
|
|
27
|
-
title: string;
|
|
28
|
-
/** Optional description/subtitle shown below title */
|
|
29
|
-
description?: string;
|
|
26
|
+
/** Title shown in step indicator; a repeated page's can be one per copy */
|
|
27
|
+
title: string | string[];
|
|
28
|
+
/** Optional description/subtitle shown below title; a repeated page's can be one per copy */
|
|
29
|
+
description?: string | string[];
|
|
30
30
|
/** Schema for fields within this step */
|
|
31
31
|
options: DynamicFormSchema;
|
|
32
32
|
/** Validate current step before proceeding to next (default: true) */
|
|
@@ -35,6 +35,11 @@ export interface StepFieldConfig {
|
|
|
35
35
|
nextLabel?: string;
|
|
36
36
|
/** Custom label for previous button on this step */
|
|
37
37
|
prevLabel?: string;
|
|
38
|
+
/** The person running it can add copies of this page; its values are a list, one row per copy */
|
|
39
|
+
multi?: boolean;
|
|
40
|
+
/** The fewest and most copies of a repeated page, as a list's rows: numbers or expressions */
|
|
41
|
+
min?: number | string;
|
|
42
|
+
max?: number | string;
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
40
45
|
* Extracted wizard configuration and step information
|
|
@@ -69,6 +74,10 @@ export interface WizardContainerProps {
|
|
|
69
74
|
spaceCompact?: boolean;
|
|
70
75
|
/** Workflow form mode */
|
|
71
76
|
workflowForm?: boolean;
|
|
77
|
+
/** A wizard inside a group submits with the form around it, so its last step has no submit. */
|
|
78
|
+
nested?: boolean;
|
|
79
|
+
/** Where the fields' values sit when the wizard is inside a group that keeps its own values. */
|
|
80
|
+
fieldNamePrefix?: string | undefined;
|
|
72
81
|
}
|
|
73
82
|
/**
|
|
74
83
|
* Props for WizardStepIndicator component
|
|
@@ -111,8 +120,8 @@ export interface WizardNavigationProps {
|
|
|
111
120
|
onNext: () => Promise<boolean>;
|
|
112
121
|
/** Callback to go to previous step */
|
|
113
122
|
onPrevious: () => void;
|
|
114
|
-
/** Callback to submit wizard */
|
|
115
|
-
onSubmit
|
|
123
|
+
/** Callback to submit wizard; without one the last step has no button of its own */
|
|
124
|
+
onSubmit?: (() => Promise<void>) | undefined;
|
|
116
125
|
}
|
|
117
126
|
/**
|
|
118
127
|
* Props for WizardStepContent component
|
|
@@ -132,5 +141,9 @@ export interface WizardStepContentProps {
|
|
|
132
141
|
spaceCompact?: boolean;
|
|
133
142
|
/** Workflow form mode */
|
|
134
143
|
workflowForm?: boolean;
|
|
144
|
+
/** Where the fields' values sit when the wizard is inside a group that keeps its own values. */
|
|
145
|
+
fieldNamePrefix?: string | undefined;
|
|
146
|
+
/** Which copy of a repeated page this is; its fields' values sit in that row of the page's list. */
|
|
147
|
+
copy?: number | undefined;
|
|
135
148
|
}
|
|
136
149
|
export {};
|
|
@@ -61,7 +61,11 @@ function useWizardState({ stepOrder, steps, validateStep }) {
|
|
|
61
61
|
stepOrder,
|
|
62
62
|
visitedSteps,
|
|
63
63
|
currentStep
|
|
64
|
-
])
|
|
64
|
+
]),
|
|
65
|
+
showStep: useCallback((stepKey) => {
|
|
66
|
+
setCurrentStep(stepKey);
|
|
67
|
+
setVisitedSteps((prev) => /* @__PURE__ */ new Set([...prev, stepKey]));
|
|
68
|
+
}, [])
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
71
|
//#endregion
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { ParsedWizardConfig } from './types.js';
|
|
2
2
|
export declare function parseWizardConfig(options: Record<string, unknown>): ParsedWizardConfig | null;
|
|
3
|
+
/** A step's title or description as one text: a list's first entry, as one copy shows it. */
|
|
4
|
+
export declare function stepText(value: string | string[] | undefined): string;
|
|
5
|
+
/** A repeated page's bounds on its copies, read as a list's `min`/`max`; without a `min` it starts empty. */
|
|
6
|
+
export declare function copyBounds(min: unknown, max: unknown): {
|
|
7
|
+
lo: number;
|
|
8
|
+
hi: number | undefined;
|
|
9
|
+
};
|
|
@@ -25,5 +25,22 @@ function parseWizardConfig(options) {
|
|
|
25
25
|
stepOrder
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
/** A step's title or description as one text: a list's first entry, as one copy shows it. */
|
|
29
|
+
function stepText(value) {
|
|
30
|
+
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
|
|
31
|
+
}
|
|
32
|
+
/** A repeated page's bounds on its copies, read as a list's `min`/`max`; without a `min` it starts empty. */
|
|
33
|
+
function copyBounds(min, max) {
|
|
34
|
+
const count = (value) => {
|
|
35
|
+
const n = value === void 0 || value === null || value === "" ? NaN : Number(value);
|
|
36
|
+
return Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
37
|
+
};
|
|
38
|
+
const lo = count(min) ?? 0;
|
|
39
|
+
const hi = count(max);
|
|
40
|
+
return {
|
|
41
|
+
lo,
|
|
42
|
+
hi: hi === void 0 ? void 0 : Math.max(hi, lo)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
28
45
|
//#endregion
|
|
29
|
-
export { parseWizardConfig };
|
|
46
|
+
export { copyBounds, parseWizardConfig, stepText };
|
package/dist/form/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { type FieldLabelBinding, FieldWrapper } from './FieldWrapper.js';
|
|
|
5
5
|
export { collectFieldsWithDefaults, DynamicDefaultsSync, DynamicForm, type DynamicFormProps, FieldsFromOptions, findSelfReferencingFieldNames, GroupHeader, MultiSelectionDropdown, resolveMetaOverrides, type TSetFormDirty, useDynamicDefaultsSync, } from './Form.js';
|
|
6
6
|
export { type FieldContextValue, FieldProvider, useFieldControlProps, useFieldLabelledByProps, useFieldRequired, } from './fieldContext.js';
|
|
7
7
|
export { type FieldComponent, FieldRegistryContext, Registry, } from './fieldRegistry.js';
|
|
8
|
+
export { type FormLayoutIssue, type FormLayoutNode, type LayoutBreakpoint, type LayoutGap, type LayoutTracks, type ResponsiveLayout, resolveFormLayout, } from './layout.js';
|
|
9
|
+
export { type LayoutCSSIssue, resolveLayoutCSS, } from './layoutCSS.js';
|
|
8
10
|
export { enforceOneMustBeTrue, flattenGroups, impureSetValueFromPath, initializeValues, resolvedFlag, } from './lib.js';
|
|
9
11
|
export { default as DropdownWrapper } from './SpecialTypes/DropdownWrapper.js';
|
|
10
12
|
export { default as FormikMultiSelectDropdown } from './SpecialTypes/FormikMultiSelectDropdown.js';
|