@evenicanpm/admin-integrate 1.8.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/documents/ConfigImport/configuration-import.ts +1 -1
  2. package/documents/Endpoint/list.ts +3 -3
  3. package/documents/Process/list.ts +3 -3
  4. package/documents/ProcessExecution/list.ts +2 -2
  5. package/documents/ProcessExport/read.ts +1 -1
  6. package/documents/ProcessSchedule/read.ts +1 -1
  7. package/documents/ProcessScheduleGroup/read.ts +3 -3
  8. package/documents/ProcessTask/fragments.ts +4 -0
  9. package/documents/ProcessTemplate/fragments.ts +30 -0
  10. package/documents/ProcessTemplate/list.ts +29 -0
  11. package/documents/ProcessTemplate/read.ts +1 -1
  12. package/documents/ScheduledProcess/list.ts +1 -1
  13. package/package.json +2 -2
  14. package/src/api/process-config/mutation/process-config-import.mutation.ts +1 -1
  15. package/src/api/templates/queries/get-field-values.query.ts +48 -0
  16. package/src/api/templates/queries/get-field-values.server.ts +9 -0
  17. package/src/api/templates/queries/index.ts +4 -0
  18. package/src/api/templates/queries/types.ts +2 -2
  19. package/src/api/templates/queries/validate-field-on-check.query.ts +22 -0
  20. package/src/api/templates/queries/validate-field-on-check.server.ts +30 -0
  21. package/src/api/templates/queries/validate-field-on-submit.server.ts +18 -0
  22. package/src/components/data-table/table-row.tsx +1 -1
  23. package/src/components/icons/up-down.tsx +1 -0
  24. package/src/components/integration-view/animated-svg.tsx +0 -1
  25. package/src/components/integration-view/elk-types.ts +1 -1
  26. package/src/components/integration-view/integration-view.tsx +274 -187
  27. package/src/components/integration-view/nodes/execution-entry-node.tsx +3 -2
  28. package/src/components/integration-view/nodes/task-node.tsx +1 -1
  29. package/src/components/integration-view/types.ts +2 -0
  30. package/src/components/integration-view/utils/mapping.ts +68 -48
  31. package/src/components/list-filter/list-filter-types.ts +6 -0
  32. package/src/components/list-filter/list-filter.tsx +21 -15
  33. package/src/components/list-filter/multi-select-filter.tsx +7 -7
  34. package/src/components/list-filter/select-filter.tsx +6 -6
  35. package/src/components/scheduler/day-selector.tsx +1 -1
  36. package/src/components/templatesV2/inputs/FormikPageBody.tsx +191 -0
  37. package/src/components/templatesV2/inputs/context/FormModeContext.tsx +31 -0
  38. package/src/components/templatesV2/inputs/context/TemplateContext.tsx +37 -0
  39. package/src/components/templatesV2/inputs/field-names.ts +8 -0
  40. package/src/components/templatesV2/inputs/form-type.ts +54 -0
  41. package/src/components/templatesV2/inputs/index.tsx +33 -0
  42. package/src/components/templatesV2/inputs/input-type.ts +23 -0
  43. package/src/components/templatesV2/inputs/primitives/CheckboxInput.tsx +45 -0
  44. package/src/components/templatesV2/inputs/primitives/NumberInput.tsx +58 -0
  45. package/src/components/templatesV2/inputs/primitives/SelectorInput.tsx +111 -0
  46. package/src/components/templatesV2/inputs/primitives/TextInput.tsx +55 -0
  47. package/src/components/templatesV2/inputs/primitives/TextWithQueryInput.tsx +292 -0
  48. package/src/components/templatesV2/inputs/primitives/index.ts +10 -0
  49. package/src/components/templatesV2/inputs/renderers/index.ts +2 -0
  50. package/src/components/templatesV2/inputs/renderers/renderFormControl.tsx +384 -0
  51. package/src/components/templatesV2/inputs/renderers/renderSearchInput.tsx +128 -0
  52. package/src/components/templatesV2/inputs/search/MultiTextSearchInput.tsx +87 -0
  53. package/src/components/templatesV2/inputs/search/SearchInputWrapper.tsx +113 -0
  54. package/src/components/templatesV2/inputs/search/TextSearchInput.tsx +309 -0
  55. package/src/components/templatesV2/inputs/search/index.ts +4 -0
  56. package/src/components/templatesV2/inputs/search/search-query-key.ts +23 -0
  57. package/src/components/templatesV2/inputs/search/useFieldValuesOptions.ts +58 -0
  58. package/src/components/templatesV2/inputs/search/useSearchInput.ts +79 -0
  59. package/src/components/templatesV2/inputs/table/InputTable.tsx +268 -0
  60. package/src/components/templatesV2/inputs/table/index.ts +1 -0
  61. package/src/components/templatesV2/inputs/utils/applyTemplateConfig.ts +28 -0
  62. package/src/components/templatesV2/inputs/utils/arrangeInputs.ts +52 -0
  63. package/src/components/templatesV2/inputs/utils/index.ts +6 -0
  64. package/src/components/templatesV2/inputs/utils/parseQueryName.ts +20 -0
  65. package/src/components/templatesV2/inputs/utils/renderReadOnlyText.tsx +26 -0
  66. package/src/components/templatesV2/inputs/utils/selectors.ts +69 -0
  67. package/src/components/templatesV2/inputs/utils/toStringArray.ts +23 -0
  68. package/src/components/templatesV2/pageForm.tsx +153 -0
  69. package/src/components/{templates → templatesV2}/templates-list/templates-list-row.tsx +2 -1
  70. package/src/components/{templates → templatesV2}/templates-list/templates-list-table.tsx +2 -2
  71. package/src/components/{templates → templatesV2}/templates-list/templates-list.tsx +19 -28
  72. package/src/components/{templates → templatesV2}/types.tsx +12 -12
  73. package/src/components/templatesV2/validation/buildZodSchema.ts +212 -0
  74. package/src/components/templatesV2/validation/zodFormikValidate.ts +17 -0
  75. package/src/components/templatesV2/wizard.tsx +746 -0
  76. package/src/pages/dashboard/dashboard-link-cards.tsx +0 -1
  77. package/src/pages/dashboard/dashboard-list-sections.tsx +1 -1
  78. package/src/pages/dashboard/dashboard.tsx +0 -1
  79. package/src/pages/executions/executions-list/executions-list-table.tsx +0 -1
  80. package/src/pages/executions/executions.tsx +1 -1
  81. package/src/pages/integration-create/integration-create-steps.ts +8 -0
  82. package/src/pages/integration-create/integration-create.tsx +81 -28
  83. package/src/pages/integration-create/pre-creation-step.tsx +9 -9
  84. package/src/pages/integration-create/select-step.tsx +3 -3
  85. package/src/pages/integration-create/template-list-step.tsx +28 -3
  86. package/src/pages/integration-create/template-options-step.tsx +119 -0
  87. package/src/pages/integration-create/wizard-step.tsx +5 -4
  88. package/src/pages/integration-details/import-process/copy-tasks-list.tsx +1 -3
  89. package/src/pages/integration-details/integration-details.tsx +1 -1
  90. package/src/pages/integration-details/task-drawer/add-task.tsx +4 -4
  91. package/src/pages/integration-details/task-drawer/edit-task.tsx +5 -5
  92. package/src/pages/integration-details/task-drawer/task-drawer.tsx +2 -2
  93. package/src/components/templates/inputs/index.tsx +0 -1392
  94. package/src/components/templates/wizard.tsx +0 -737
  95. /package/src/components/{templates → templatesV2}/index.ts +0 -0
  96. /package/src/components/{templates → templatesV2}/template-filter.ts +0 -0
  97. /package/src/components/{templates → templatesV2}/templates-list/index.ts +0 -0
@@ -0,0 +1,746 @@
1
+ import type { GetProcessTemplateQuery } from "@evenicanpm/admin-core/api/e4/graphqlRequestSdk";
2
+ import { getProcessTemplate } from "@evenicanpm/admin-integrate/api/templates/queries";
3
+ import { validateFieldOnSubmit } from "@evenicanpm/admin-integrate/api/templates/queries/validate-field-on-submit.server";
4
+ import { Loading } from "@evenicanpm/admin-integrate/components/core";
5
+ import ArrowLeftIcon from "@mui/icons-material/ArrowLeft";
6
+ import ArrowRightIcon from "@mui/icons-material/ArrowRight";
7
+ import {
8
+ Box,
9
+ Button,
10
+ Card,
11
+ CardContent,
12
+ Divider,
13
+ LinearProgress,
14
+ Stack,
15
+ Step,
16
+ StepLabel,
17
+ Stepper,
18
+ Typography,
19
+ useTheme,
20
+ } from "@mui/material";
21
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
22
+ import React, { useEffect, useRef, useState } from "react";
23
+ import { applyTemplateConfig, InputType } from "./inputs";
24
+ import PageForm from "./pageForm";
25
+ import {
26
+ type FieldValue,
27
+ type FlatPage,
28
+ type FormState,
29
+ type Metadata,
30
+ type ProcessConfigMerge,
31
+ type ProcessTemplate,
32
+ type ProcessTemplateInputSchema,
33
+ type ProcessTemplatePage,
34
+ type SearchOption,
35
+ sortBy,
36
+ templateFieldKey,
37
+ } from "./types";
38
+
39
+ const isString = (x: unknown): x is string => typeof x === "string";
40
+
41
+ function isStringTypeRow(row: Record<string, unknown>): boolean {
42
+ return row?.DataType === "String" || row?.FieldType === "String";
43
+ }
44
+
45
+ function normalizeTableRowsStringLength(
46
+ rows: Array<Record<string, unknown>>,
47
+ ): Array<Record<string, unknown>> {
48
+ return rows.map((row: Record<string, unknown>) =>
49
+ isStringTypeRow(row) ? { ...row, Length: 4000 } : row,
50
+ );
51
+ }
52
+
53
+ function rowKeyToString(rawKey: unknown): string {
54
+ if (typeof rawKey === "string") return rawKey.trim();
55
+ if (
56
+ rawKey == null ||
57
+ typeof rawKey === "object" ||
58
+ typeof rawKey === "function"
59
+ )
60
+ return "";
61
+ if (
62
+ typeof rawKey === "number" ||
63
+ typeof rawKey === "boolean" ||
64
+ typeof rawKey === "bigint"
65
+ )
66
+ return String(rawKey);
67
+ return "";
68
+ }
69
+
70
+ function collapseTableRowsToObject(
71
+ rows: Array<Record<string, FieldValue>>,
72
+ keyCol: string,
73
+ valueCol: string,
74
+ ): Record<string, FieldValue> {
75
+ return rows.reduce(
76
+ (acc, row) => {
77
+ const rawKey = (row as Record<string, unknown>)[keyCol];
78
+ const k = rowKeyToString(rawKey);
79
+ if (!k) return acc;
80
+ const val = (row as Record<string, unknown>)[valueCol];
81
+ if (val !== undefined) acc[k] = val as FieldValue;
82
+ return acc;
83
+ },
84
+ {} as Record<string, FieldValue>,
85
+ );
86
+ }
87
+
88
+ function getKeyColumnForTable(
89
+ schemas: ProcessTemplateInputSchema[] | undefined,
90
+ ): string {
91
+ const nonPlaceholder = schemas?.find(
92
+ (s) => s?.fieldKey && isString(s.fieldKey) && !/^\{.*\}$/.test(s.fieldKey),
93
+ )?.fieldKey;
94
+ return isString(nonPlaceholder) ? nonPlaceholder : "{Variable}";
95
+ }
96
+
97
+ function getInputTableConfigValue(
98
+ input: {
99
+ fieldName?: string | null;
100
+ schemas?: readonly (ProcessTemplateInputSchema | null)[] | null;
101
+ },
102
+ v: Array<Record<string, FieldValue>>,
103
+ ): FieldValue | null {
104
+ if (!input?.fieldName) return null;
105
+ const schemas = (input.schemas ?? []).filter(
106
+ (s): s is ProcessTemplateInputSchema => s != null,
107
+ );
108
+ const valueCol = schemas.find((s) => s?.fieldKey === "{0}")?.fieldKey ?? null;
109
+ // When no "{0}" value column, use raw array
110
+ if (!isString(valueCol)) return v as unknown as FieldValue;
111
+
112
+ const keyCol = getKeyColumnForTable(schemas);
113
+ const collapsed = collapseTableRowsToObject(v, keyCol, valueCol);
114
+ return collapsed as unknown as FieldValue;
115
+ }
116
+
117
+ function formatFieldValueForValidation(v: FieldValue | undefined): string {
118
+ if (v === "" || v == null) return "";
119
+ if (typeof v === "boolean") return v ? "true" : "false";
120
+ // Avoid Object's default stringification ([object Object]); use JSON for objects
121
+ if (typeof v === "object" || typeof v === "function")
122
+ return JSON.stringify(v);
123
+ return String(v).trim();
124
+ }
125
+
126
+ function getInitialValuesFromTemplates(
127
+ templates: ProcessTemplate[],
128
+ ): FormState {
129
+ const newInitialValues: FormState = {};
130
+ templates.forEach((tpl) => {
131
+ tpl.pages?.forEach((page) => {
132
+ page?.inputs?.forEach((input) => {
133
+ const key = templateFieldKey(tpl.id, input?.fieldName ?? "");
134
+ if (
135
+ input?.defaultValue !== undefined &&
136
+ input?.defaultValue !== null &&
137
+ !newInitialValues[key]
138
+ ) {
139
+ newInitialValues[key] = input.defaultValue as FieldValue;
140
+ }
141
+ });
142
+ });
143
+ });
144
+ return newInitialValues;
145
+ }
146
+
147
+ function buildProcessConfigMerge(
148
+ templates: ProcessTemplate[],
149
+ values: FormState,
150
+ metadata: Metadata,
151
+ ): ProcessConfigMerge {
152
+ const config = templates.map((t) => {
153
+ const templateId = typeof t.id === "string" ? Number(t.id) : t.id;
154
+ const obj: Record<string, FieldValue> = {
155
+ processTemplateId: Number.isFinite(templateId) ? templateId : t.id,
156
+ };
157
+
158
+ t.pages?.forEach((page) => {
159
+ page?.inputs?.forEach((input) => {
160
+ const key = templateFieldKey(t.id, input?.fieldName ?? "");
161
+
162
+ if (input?.fieldName && input.fieldName in metadata) {
163
+ obj[input.fieldName] = metadata[
164
+ input.fieldName as keyof Metadata
165
+ ] as FieldValue;
166
+ return;
167
+ }
168
+ if (!Object.hasOwn(values, key)) return;
169
+
170
+ const v = values[key];
171
+
172
+ const isInputTable =
173
+ input?.inputType === InputType.INPUT_TABLE &&
174
+ Array.isArray(v) &&
175
+ (input.schemas?.length ?? 0) > 0;
176
+
177
+ if (isInputTable && input?.schemas) {
178
+ const normalized = normalizeTableRowsStringLength(
179
+ v as Array<Record<string, unknown>>,
180
+ ) as Array<Record<string, FieldValue>>;
181
+ const tableValue = getInputTableConfigValue(input, normalized);
182
+ if (tableValue !== null && input.fieldName) {
183
+ obj[input.fieldName] = tableValue;
184
+ }
185
+ return;
186
+ }
187
+
188
+ if (input?.fieldName) {
189
+ obj[input.fieldName] = v;
190
+ }
191
+ });
192
+ });
193
+
194
+ obj.processTemplateId = Number.isFinite(templateId) ? templateId : t.id;
195
+ return obj;
196
+ });
197
+
198
+ return { input: { config } };
199
+ }
200
+
201
+ export function removeTemplateConfig(input: unknown): unknown {
202
+ if (Array.isArray(input)) {
203
+ return input.map(removeTemplateConfig);
204
+ }
205
+ if (input == null || typeof input !== "object") {
206
+ return input;
207
+ }
208
+ const newObj: Record<string, unknown> = {};
209
+ for (const key in input) {
210
+ if (key !== "templateConfig") {
211
+ newObj[key] = removeTemplateConfig(
212
+ (input as Record<string, unknown>)[key],
213
+ );
214
+ }
215
+ }
216
+ return newObj;
217
+ }
218
+
219
+ function Header({
220
+ title,
221
+ subtitle,
222
+ }: {
223
+ readonly title: string;
224
+ readonly subtitle?: string;
225
+ }) {
226
+ return (
227
+ <Box sx={{ mb: 2 }}>
228
+ <Typography variant="h6" sx={{ mb: 0.5 }}>
229
+ {title}
230
+ </Typography>
231
+ {subtitle ? <Typography variant="body2">{subtitle}</Typography> : null}
232
+ </Box>
233
+ );
234
+ }
235
+
236
+ function TemplateNav({
237
+ templates,
238
+ activeIndex,
239
+ stylings,
240
+ }: {
241
+ readonly templates: ProcessTemplate[];
242
+ readonly activeIndex: number;
243
+ readonly stylings?: object;
244
+ }) {
245
+ return (
246
+ <Stepper activeStep={activeIndex} sx={stylings}>
247
+ {templates.map((t) => (
248
+ <Step key={t.id}>
249
+ <StepLabel>{t.name ?? ""}</StepLabel>
250
+ </Step>
251
+ ))}
252
+ </Stepper>
253
+ );
254
+ }
255
+
256
+ function PageNav({
257
+ pages,
258
+ activeIndex,
259
+ stylings,
260
+ isEditMode,
261
+ }: {
262
+ readonly pages: ProcessTemplatePage[];
263
+ readonly activeIndex: number;
264
+ readonly stylings?: object;
265
+ readonly isEditMode?: boolean;
266
+ }) {
267
+ return (
268
+ <Stepper nonLinear={isEditMode} activeStep={activeIndex} sx={stylings}>
269
+ {pages.map((p) => (
270
+ <Step key={p.id} sx={{ color: "green" }}>
271
+ <StepLabel>{p.title}</StepLabel>
272
+ </Step>
273
+ ))}
274
+ </Stepper>
275
+ );
276
+ }
277
+
278
+ interface Props {
279
+ readonly metadata: Metadata;
280
+ readonly processTemplateGroupId?: string;
281
+ readonly processTemplateId?: string;
282
+ readonly handleBack?: (event?: React.MouseEventHandler) => void;
283
+ readonly handleFinish?: (event?: React.MouseEventHandler) => void;
284
+ readonly setProcessConfigMerge: React.Dispatch<
285
+ React.SetStateAction<ProcessConfigMerge | null>
286
+ >;
287
+ readonly templateConfig?: string | null;
288
+ readonly disableBackToTemplates?: boolean;
289
+ readonly isEditMode?: boolean;
290
+ readonly isCreateMode?: boolean;
291
+ readonly readOnly?: boolean;
292
+ readonly sharedCount?: (count: number) => void;
293
+ }
294
+
295
+ export default function Wizard({ metadata, sharedCount, ...props }: Props) {
296
+ const theme = useTheme();
297
+
298
+ const router = useRouter();
299
+ const pathname = usePathname();
300
+ const searchParams = useSearchParams();
301
+
302
+ const [templates, setTemplates] = useState<ProcessTemplate[]>([]);
303
+ const [templatesSorted, setTemplatesSorted] = useState<ProcessTemplate[]>([]);
304
+
305
+ const { data, isLoading } = getProcessTemplate.useData({
306
+ input: {
307
+ processTemplateGroupId: props.processTemplateGroupId || null,
308
+ processTemplateId: props.processTemplateId || null,
309
+ },
310
+ });
311
+
312
+ const [initialValues, setInitialValues] = React.useState<FormState>({});
313
+ // aggregated form values across pages
314
+ const [aggregatedValues, setAggregatedValues] =
315
+ useState<FormState>(initialValues);
316
+ // keep aggregated values in sync when initial defaults change
317
+ useEffect(() => {
318
+ setAggregatedValues((prev) => ({ ...prev, ...initialValues }));
319
+ }, [initialValues]);
320
+
321
+ // submit orchestration for current page
322
+ const currentPageSubmitRef = useRef<(() => Promise<boolean>) | null>(
323
+ null,
324
+ ) as React.MutableRefObject<(() => Promise<boolean>) | null>;
325
+ const setCurrentPageSubmitRef = (fn: () => Promise<boolean>) => {
326
+ currentPageSubmitRef.current = fn;
327
+ };
328
+ // Latest merged form values (set in handlePageSubmit) so we can build payload in finish() without stale state
329
+ const lastMergedValuesRef = useRef<FormState | null>(null);
330
+ // Last submitted page values (set in handlePageSubmit) for validateFieldOnSubmit on Next
331
+ const lastSubmittedPageValuesRef = useRef<FormState>({});
332
+ // Results from validateFieldOnSubmit (keyed by templateFieldKey); block Next if any invalid
333
+ const [submitValidationResults, setSubmitValidationResults] =
334
+ React.useState<Record<
335
+ string,
336
+ { isValid: boolean; message: string }
337
+ > | null>(null);
338
+
339
+ useEffect(() => {
340
+ const typedData = data as GetProcessTemplateQuery;
341
+ if (typedData?.processTemplate) {
342
+ const templates = typedData.processTemplate as ProcessTemplate[];
343
+
344
+ setTemplates(templates);
345
+ setTemplatesSorted(
346
+ templates.map((t) => ({
347
+ ...t,
348
+ pages: sortBy(t.pages as ProcessTemplatePage[]),
349
+ })),
350
+ );
351
+ setInitialValues(getInitialValuesFromTemplates(templates));
352
+ }
353
+ }, [data]);
354
+
355
+ const [flatPages, setFlatPages] = useState<FlatPage[]>([]);
356
+ useEffect(() => {
357
+ let counter = 0;
358
+ const newFlatPages = templatesSorted.flatMap((tpl, ti) => {
359
+ if (tpl.pages) {
360
+ return (tpl.pages as ProcessTemplatePage[]).map((p, pi) => ({
361
+ ...p,
362
+ _templateIndex: ti,
363
+ _templateId: tpl.id,
364
+ _templateName: tpl.name ?? "",
365
+ _pageIndexWithinTemplate: pi,
366
+ _flatIndex: counter++,
367
+ }));
368
+ } else {
369
+ return [];
370
+ }
371
+ });
372
+ setFlatPages(newFlatPages);
373
+ }, [templatesSorted]);
374
+
375
+ const [globalStep, setGlobalStep] = React.useState(0);
376
+ const totalSteps = flatPages.length;
377
+
378
+ const [currentFlat, setCurrentFlat] = useState<FlatPage | null>(null);
379
+ const [currentTemplate, setCurrentTemplate] =
380
+ useState<ProcessTemplate | null>(null);
381
+ useEffect(() => {
382
+ if (flatPages.length > 0) {
383
+ const thisFlat = flatPages[globalStep];
384
+ setCurrentFlat(thisFlat);
385
+ setCurrentTemplate(templatesSorted[thisFlat._templateIndex]);
386
+ }
387
+ }, [flatPages, globalStep]);
388
+
389
+ useEffect(() => {
390
+ if (currentTemplate) {
391
+ const currentParams = new URLSearchParams(searchParams.toString());
392
+ currentParams.set("template", currentTemplate.id);
393
+ router.replace(`${pathname}?${currentParams.toString()}`);
394
+ }
395
+ }, [currentTemplate]);
396
+
397
+ const [disabledFields, setDisabledFields] = React.useState<Set<string>>(
398
+ new Set(),
399
+ );
400
+
401
+ // track whether the current page is valid (reported by PageForm)
402
+ const [currentPageValid, setCurrentPageValid] = React.useState<boolean>(true);
403
+ const [isFieldValidationInProgress, setIsFieldValidationInProgress] =
404
+ React.useState(false);
405
+
406
+ const isFirst = globalStep === 0;
407
+ const isLast = globalStep === totalSteps - 1;
408
+
409
+ const setFieldsDisabled = React.useCallback(
410
+ (templateId: string, fieldNames: string[], disabled: boolean) => {
411
+ setDisabledFields((prev) => {
412
+ const newSet = new Set(prev);
413
+ fieldNames.forEach((fieldName) => {
414
+ const key = templateFieldKey(templateId, fieldName);
415
+ if (disabled) {
416
+ newSet.add(key);
417
+ } else {
418
+ newSet.delete(key);
419
+ }
420
+ });
421
+ return newSet;
422
+ });
423
+ },
424
+ [],
425
+ );
426
+
427
+ useEffect(() => {
428
+ if (props.templateConfig) {
429
+ const config = JSON.parse(props.templateConfig);
430
+ applyTemplateConfig(
431
+ {
432
+ name: config.taskName as string,
433
+ templateConfig: props.templateConfig,
434
+ } as SearchOption,
435
+ (templateId: string, fieldName: string, value: FieldValue) => {
436
+ const key = templateFieldKey(templateId, fieldName);
437
+ setInitialValues((prev) => ({ ...prev, [key]: value }));
438
+ },
439
+ );
440
+ }
441
+ }, [props.templateConfig]);
442
+
443
+ const clearTemplateFields = (templateId: string) => {
444
+ setAggregatedValues((prev) => {
445
+ const copy = { ...prev };
446
+ for (const key of Object.keys(copy)) {
447
+ if (key.startsWith(`${templateId}::`)) {
448
+ delete copy[key];
449
+ }
450
+ }
451
+ return copy;
452
+ });
453
+ };
454
+
455
+ // On Next: only merge form state. Do not build or send full config (that happens only on Finish).
456
+ const handlePageSubmit = (pageValues: FormState) => {
457
+ lastSubmittedPageValuesRef.current = pageValues;
458
+ setAggregatedValues((prev) => {
459
+ const merged = { ...prev, ...pageValues };
460
+ lastMergedValuesRef.current = merged;
461
+ return merged;
462
+ });
463
+ };
464
+
465
+ const runPageSubmitValidation = async (
466
+ templateId: string,
467
+ submitValidationInputs: Array<{
468
+ fieldName?: string;
469
+ id: string;
470
+ }>,
471
+ values: FormState,
472
+ ): Promise<boolean> => {
473
+ const input = submitValidationInputs
474
+ .filter(
475
+ (inp): inp is { fieldName: string; id: string } =>
476
+ typeof inp.fieldName === "string" && typeof inp.id === "string",
477
+ )
478
+ .map((inp) => {
479
+ const k = templateFieldKey(templateId, inp.fieldName);
480
+ const v = values[k];
481
+ return {
482
+ fieldName: inp.fieldName,
483
+ validationQuery: inp.id,
484
+ value: formatFieldValueForValidation(v),
485
+ };
486
+ });
487
+ try {
488
+ const results = await validateFieldOnSubmit(input);
489
+ const byKey: Record<string, { isValid: boolean; message: string }> = {};
490
+ let anyInvalid = false;
491
+ for (const r of results) {
492
+ const key = templateFieldKey(templateId, r.fieldName ?? "");
493
+ byKey[key] = {
494
+ isValid: r.isValid ?? false,
495
+ message: r.message ?? "",
496
+ };
497
+ if (!r.isValid) anyInvalid = true;
498
+ }
499
+ setSubmitValidationResults(byKey);
500
+ return !anyInvalid;
501
+ } catch {
502
+ const fallbackKey = templateFieldKey(
503
+ templateId,
504
+ submitValidationInputs[0]?.fieldName ?? "",
505
+ );
506
+ setSubmitValidationResults({
507
+ [fallbackKey]: {
508
+ isValid: false,
509
+ message: "Validation request failed",
510
+ },
511
+ });
512
+ return false;
513
+ }
514
+ };
515
+
516
+ const next = async () => {
517
+ const submitFn = currentPageSubmitRef.current;
518
+ if (!submitFn) return;
519
+ const ok = await submitFn();
520
+ if (!ok) return;
521
+
522
+ if (props.readOnly) {
523
+ setSubmitValidationResults(null);
524
+ setGlobalStep((s) => Math.min(s + 1, totalSteps - 1));
525
+ return;
526
+ }
527
+
528
+ const page = currentFlat;
529
+ const templateId = page?._templateId ?? "";
530
+ const submitValidationInputs = (page?.inputs ?? [])
531
+ .filter(
532
+ (inp): inp is NonNullable<typeof inp> & { id: string } =>
533
+ inp != null &&
534
+ !!inp.fieldName &&
535
+ !!(inp as { validationQueryOnSubmit?: string })
536
+ .validationQueryOnSubmit &&
537
+ !!(inp as { id?: string }).id,
538
+ )
539
+ .map((inp) => ({
540
+ fieldName: inp.fieldName ?? undefined,
541
+ id: (inp as { id: string }).id,
542
+ }));
543
+
544
+ const values = {
545
+ ...aggregatedValues,
546
+ ...lastSubmittedPageValuesRef.current,
547
+ };
548
+
549
+ const shouldRunSubmitValidation =
550
+ submitValidationInputs.length > 0 &&
551
+ (!props.isEditMode ||
552
+ submitValidationInputs.some((inp) => {
553
+ const fieldName = inp.fieldName;
554
+ if (fieldName !== "uniqueIdentifier" && fieldName !== "subscriptions")
555
+ return false;
556
+ const key = templateFieldKey(templateId, fieldName ?? "");
557
+ const current = formatFieldValueForValidation(values[key]);
558
+ const initial = formatFieldValueForValidation(initialValues[key]);
559
+ return current !== initial;
560
+ }));
561
+
562
+ if (shouldRunSubmitValidation) {
563
+ const validationOk = await runPageSubmitValidation(
564
+ templateId,
565
+ submitValidationInputs,
566
+ values,
567
+ );
568
+ if (!validationOk) return;
569
+ }
570
+
571
+ setSubmitValidationResults(null);
572
+ setGlobalStep((s) => Math.min(s + 1, totalSteps - 1));
573
+ };
574
+
575
+ const back = () => {
576
+ setSubmitValidationResults(null);
577
+ const newStep = globalStep - 1;
578
+ setGlobalStep(Math.max(newStep, 0));
579
+ if (newStep < 0 && props.handleBack) {
580
+ props.handleBack();
581
+ }
582
+ };
583
+
584
+ const finish = async () => {
585
+ const submitFn = currentPageSubmitRef.current;
586
+ if (!submitFn) return;
587
+ const ok = await submitFn();
588
+ if (!ok) return;
589
+
590
+ const merged = lastMergedValuesRef.current ?? aggregatedValues;
591
+ const payload = buildProcessConfigMerge(templatesSorted, merged, metadata);
592
+ props.setProcessConfigMerge(payload);
593
+ props.handleFinish?.();
594
+ };
595
+
596
+ const progress =
597
+ totalSteps > 0 ? Math.round(((globalStep + 1) / totalSteps) * 100) : 0;
598
+
599
+ const showLoading =
600
+ isLoading &&
601
+ !currentFlat &&
602
+ !currentTemplate &&
603
+ !props.templateConfig &&
604
+ (!props.processTemplateGroupId || !props.processTemplateId);
605
+ if (showLoading) {
606
+ return (
607
+ <Box sx={{ padding: theme.spacing(10, 3) }}>
608
+ <Loading />
609
+ </Box>
610
+ );
611
+ }
612
+ if (!isLoading && !templates.length) {
613
+ return <Typography>No templates found.</Typography>;
614
+ }
615
+
616
+ return (
617
+ currentFlat &&
618
+ currentTemplate && (
619
+ <Card
620
+ sx={
621
+ props.isEditMode
622
+ ? { border: 0, boxShadow: 0, borderRadius: 0 }
623
+ : {
624
+ border: `1px solid ${theme.palette.grey[300]}`,
625
+ borderRadius: 0,
626
+ }
627
+ }
628
+ >
629
+ <CardContent>
630
+ {!props.isEditMode && (
631
+ <Typography variant="h5" gutterBottom>
632
+ {currentTemplate.name}
633
+ </Typography>
634
+ )}
635
+
636
+ {templates.length > 1 ? (
637
+ <>
638
+ <TemplateNav
639
+ templates={templates}
640
+ activeIndex={currentFlat._templateIndex}
641
+ />
642
+ <Divider sx={{ my: 2 }} />
643
+ </>
644
+ ) : null}
645
+
646
+ <PageNav
647
+ pages={currentTemplate.pages as ProcessTemplatePage[]}
648
+ activeIndex={currentFlat._pageIndexWithinTemplate}
649
+ isEditMode={props.isEditMode}
650
+ />
651
+
652
+ <Divider sx={{ my: 2 }} />
653
+
654
+ {/* Page header */}
655
+ <Header
656
+ title={currentFlat.title ?? ""}
657
+ subtitle={props.isEditMode ? "" : (currentFlat.description ?? "")}
658
+ />
659
+
660
+ {/* Per-page form */}
661
+ <Box>
662
+ <PageForm
663
+ templateId={currentFlat._templateId}
664
+ page={currentFlat}
665
+ initialValues={aggregatedValues}
666
+ onPageSubmit={handlePageSubmit}
667
+ setSubmitRef={(fn) =>
668
+ setCurrentPageSubmitRef(fn ?? (() => Promise.resolve(false)))
669
+ }
670
+ setPageValidity={(v) => setCurrentPageValid(v)}
671
+ clearTemplateFields={(templateId) =>
672
+ clearTemplateFields(templateId)
673
+ }
674
+ readOnly={props.readOnly}
675
+ isEditMode={props.isEditMode}
676
+ isCreateMode={props.isCreateMode}
677
+ disabledFields={disabledFields}
678
+ setFieldsDisabled={setFieldsDisabled}
679
+ templateConfig={props.templateConfig || ""}
680
+ setFieldValidationInProgress={setIsFieldValidationInProgress}
681
+ submitValidationResults={submitValidationResults ?? undefined}
682
+ />
683
+ </Box>
684
+
685
+ <Divider sx={{ my: 2 }} />
686
+
687
+ <Box sx={{ mt: 1, mb: 2 }}>
688
+ <LinearProgress
689
+ variant="determinate"
690
+ value={isFirst ? 0 : progress}
691
+ />
692
+ </Box>
693
+
694
+ <Stack
695
+ direction="row"
696
+ justifyContent="right"
697
+ sx={{ mt: 3 }}
698
+ spacing={3}
699
+ >
700
+ {isFirst && !props.disableBackToTemplates && (
701
+ <Button
702
+ variant="contained"
703
+ onClick={back}
704
+ color="info"
705
+ startIcon={<ArrowLeftIcon />}
706
+ >
707
+ Back to templates
708
+ </Button>
709
+ )}
710
+ {!isFirst && (
711
+ <Button
712
+ variant="contained"
713
+ onClick={() => back()}
714
+ color="info"
715
+ startIcon={<ArrowLeftIcon />}
716
+ >
717
+ Back
718
+ </Button>
719
+ )}
720
+ {!isLast && (
721
+ <Button
722
+ variant="contained"
723
+ onClick={() => next()}
724
+ color="info"
725
+ disabled={isFieldValidationInProgress || !currentPageValid}
726
+ >
727
+ Next
728
+ </Button>
729
+ )}
730
+ {isLast && !props.readOnly && (
731
+ <Button
732
+ variant="contained"
733
+ onClick={() => finish()}
734
+ color="success"
735
+ endIcon={<ArrowRightIcon />}
736
+ disabled={isFieldValidationInProgress}
737
+ >
738
+ Finish
739
+ </Button>
740
+ )}
741
+ </Stack>
742
+ </CardContent>
743
+ </Card>
744
+ )
745
+ );
746
+ }