@evenicanpm/admin-integrate 1.8.0 → 2.0.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/documents/ProcessTask/fragments.ts +4 -0
- package/documents/ProcessTemplate/fragments.ts +30 -0
- package/documents/ProcessTemplate/list.ts +29 -0
- package/package.json +2 -2
- package/src/api/process-config/mutation/process-config-import.mutation.ts +1 -1
- package/src/api/templates/queries/get-field-values.query.ts +48 -0
- package/src/api/templates/queries/get-field-values.server.ts +9 -0
- package/src/api/templates/queries/index.ts +4 -0
- package/src/api/templates/queries/types.ts +2 -2
- package/src/api/templates/queries/validate-field-on-check.query.ts +22 -0
- package/src/api/templates/queries/validate-field-on-check.server.ts +30 -0
- package/src/api/templates/queries/validate-field-on-submit.server.ts +18 -0
- package/src/components/data-table/table-row.tsx +1 -1
- package/src/components/icons/up-down.tsx +1 -0
- package/src/components/integration-view/integration-view.tsx +10 -15
- package/src/components/integration-view/nodes/task-node.tsx +1 -1
- package/src/components/integration-view/utils/mapping.ts +10 -11
- package/src/components/list-filter/list-filter.tsx +2 -0
- package/src/components/templatesV2/inputs/FormikPageBody.tsx +191 -0
- package/src/components/templatesV2/inputs/context/FormModeContext.tsx +31 -0
- package/src/components/templatesV2/inputs/context/TemplateContext.tsx +37 -0
- package/src/components/templatesV2/inputs/field-names.ts +8 -0
- package/src/components/templatesV2/inputs/form-type.ts +48 -0
- package/src/components/templatesV2/inputs/index.tsx +33 -0
- package/src/components/templatesV2/inputs/input-type.ts +23 -0
- package/src/components/templatesV2/inputs/primitives/CheckboxInput.tsx +45 -0
- package/src/components/templatesV2/inputs/primitives/NumberInput.tsx +58 -0
- package/src/components/templatesV2/inputs/primitives/SelectorInput.tsx +111 -0
- package/src/components/templatesV2/inputs/primitives/TextInput.tsx +55 -0
- package/src/components/templatesV2/inputs/primitives/TextWithQueryInput.tsx +292 -0
- package/src/components/templatesV2/inputs/primitives/index.ts +10 -0
- package/src/components/templatesV2/inputs/renderers/index.ts +2 -0
- package/src/components/templatesV2/inputs/renderers/renderFormControl.tsx +376 -0
- package/src/components/templatesV2/inputs/renderers/renderSearchInput.tsx +129 -0
- package/src/components/templatesV2/inputs/search/MultiTextSearchInput.tsx +87 -0
- package/src/components/templatesV2/inputs/search/SearchInputWrapper.tsx +113 -0
- package/src/components/templatesV2/inputs/search/TextSearchInput.tsx +309 -0
- package/src/components/templatesV2/inputs/search/index.ts +4 -0
- package/src/components/templatesV2/inputs/search/search-query-key.ts +23 -0
- package/src/components/templatesV2/inputs/search/useFieldValuesOptions.ts +58 -0
- package/src/components/templatesV2/inputs/search/useSearchInput.ts +74 -0
- package/src/components/templatesV2/inputs/table/InputTable.tsx +259 -0
- package/src/components/templatesV2/inputs/table/index.ts +1 -0
- package/src/components/templatesV2/inputs/utils/applyTemplateConfig.ts +28 -0
- package/src/components/templatesV2/inputs/utils/arrangeInputs.ts +51 -0
- package/src/components/templatesV2/inputs/utils/index.ts +6 -0
- package/src/components/templatesV2/inputs/utils/parseQueryName.ts +20 -0
- package/src/components/templatesV2/inputs/utils/renderReadOnlyText.tsx +26 -0
- package/src/components/templatesV2/inputs/utils/selectors.ts +69 -0
- package/src/components/templatesV2/inputs/utils/toStringArray.ts +23 -0
- package/src/components/templatesV2/pageForm.tsx +151 -0
- package/src/components/{templates → templatesV2}/templates-list/templates-list-row.tsx +2 -1
- package/src/components/{templates → templatesV2}/templates-list/templates-list-table.tsx +2 -2
- package/src/components/{templates → templatesV2}/templates-list/templates-list.tsx +19 -28
- package/src/components/{templates → templatesV2}/types.tsx +12 -12
- package/src/components/templatesV2/validation/buildZodSchema.ts +216 -0
- package/src/components/templatesV2/validation/zodFormikValidate.ts +17 -0
- package/src/components/templatesV2/wizard.tsx +744 -0
- package/src/pages/integration-create/integration-create-steps.ts +8 -0
- package/src/pages/integration-create/integration-create.tsx +61 -14
- package/src/pages/integration-create/select-step.tsx +3 -3
- package/src/pages/integration-create/template-list-step.tsx +28 -3
- package/src/pages/integration-create/template-options-step.tsx +119 -0
- package/src/pages/integration-create/wizard-step.tsx +5 -4
- package/src/pages/integration-details/task-drawer/add-task.tsx +4 -4
- package/src/pages/integration-details/task-drawer/edit-task.tsx +3 -3
- package/src/components/templates/inputs/index.tsx +0 -1392
- package/src/components/templates/wizard.tsx +0 -737
- /package/src/components/{templates → templatesV2}/index.ts +0 -0
- /package/src/components/{templates → templatesV2}/template-filter.ts +0 -0
- /package/src/components/{templates → templatesV2}/templates-list/index.ts +0 -0
|
@@ -0,0 +1,744 @@
|
|
|
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: any): any {
|
|
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: any = {};
|
|
209
|
+
for (const key in input) {
|
|
210
|
+
if (key !== "templateConfig") {
|
|
211
|
+
newObj[key] = removeTemplateConfig(input[key]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return newObj;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Header({
|
|
218
|
+
title,
|
|
219
|
+
subtitle,
|
|
220
|
+
}: {
|
|
221
|
+
readonly title: string;
|
|
222
|
+
readonly subtitle?: string;
|
|
223
|
+
}) {
|
|
224
|
+
return (
|
|
225
|
+
<Box sx={{ mb: 2 }}>
|
|
226
|
+
<Typography variant="h6" sx={{ mb: 0.5 }}>
|
|
227
|
+
{title}
|
|
228
|
+
</Typography>
|
|
229
|
+
{subtitle ? <Typography variant="body2">{subtitle}</Typography> : null}
|
|
230
|
+
</Box>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function TemplateNav({
|
|
235
|
+
templates,
|
|
236
|
+
activeIndex,
|
|
237
|
+
stylings,
|
|
238
|
+
}: {
|
|
239
|
+
readonly templates: ProcessTemplate[];
|
|
240
|
+
readonly activeIndex: number;
|
|
241
|
+
readonly stylings?: object;
|
|
242
|
+
}) {
|
|
243
|
+
return (
|
|
244
|
+
<Stepper activeStep={activeIndex} sx={stylings}>
|
|
245
|
+
{templates.map((t) => (
|
|
246
|
+
<Step key={t.id}>
|
|
247
|
+
<StepLabel>{t.name ?? ""}</StepLabel>
|
|
248
|
+
</Step>
|
|
249
|
+
))}
|
|
250
|
+
</Stepper>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function PageNav({
|
|
255
|
+
pages,
|
|
256
|
+
activeIndex,
|
|
257
|
+
stylings,
|
|
258
|
+
isEditMode,
|
|
259
|
+
}: {
|
|
260
|
+
readonly pages: ProcessTemplatePage[];
|
|
261
|
+
readonly activeIndex: number;
|
|
262
|
+
readonly stylings?: object;
|
|
263
|
+
readonly isEditMode?: boolean;
|
|
264
|
+
}) {
|
|
265
|
+
return (
|
|
266
|
+
<Stepper nonLinear={isEditMode} activeStep={activeIndex} sx={stylings}>
|
|
267
|
+
{pages.map((p) => (
|
|
268
|
+
<Step key={p.id} sx={{ color: "green" }}>
|
|
269
|
+
<StepLabel>{p.title}</StepLabel>
|
|
270
|
+
</Step>
|
|
271
|
+
))}
|
|
272
|
+
</Stepper>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface Props {
|
|
277
|
+
readonly metadata: Metadata;
|
|
278
|
+
readonly processTemplateGroupId?: string;
|
|
279
|
+
readonly processTemplateId?: string;
|
|
280
|
+
readonly handleBack?: (event?: React.MouseEventHandler) => void;
|
|
281
|
+
readonly handleFinish?: (event?: React.MouseEventHandler) => void;
|
|
282
|
+
readonly setProcessConfigMerge: React.Dispatch<
|
|
283
|
+
React.SetStateAction<ProcessConfigMerge | null>
|
|
284
|
+
>;
|
|
285
|
+
readonly templateConfig?: string | null;
|
|
286
|
+
readonly disableBackToTemplates?: boolean;
|
|
287
|
+
readonly isEditMode?: boolean;
|
|
288
|
+
readonly isCreateMode?: boolean;
|
|
289
|
+
readonly readOnly?: boolean;
|
|
290
|
+
readonly sharedCount?: (count: number) => void;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default function Wizard({ metadata, sharedCount, ...props }: Props) {
|
|
294
|
+
const theme = useTheme();
|
|
295
|
+
|
|
296
|
+
const router = useRouter();
|
|
297
|
+
const pathname = usePathname();
|
|
298
|
+
const searchParams = useSearchParams();
|
|
299
|
+
|
|
300
|
+
const [templates, setTemplates] = useState<ProcessTemplate[]>([]);
|
|
301
|
+
const [templatesSorted, setTemplatesSorted] = useState<ProcessTemplate[]>([]);
|
|
302
|
+
|
|
303
|
+
const { data, isLoading } = getProcessTemplate.useData({
|
|
304
|
+
input: {
|
|
305
|
+
processTemplateGroupId: props.processTemplateGroupId || null,
|
|
306
|
+
processTemplateId: props.processTemplateId || null,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const [initialValues, setInitialValues] = React.useState<FormState>({});
|
|
311
|
+
// aggregated form values across pages
|
|
312
|
+
const [aggregatedValues, setAggregatedValues] =
|
|
313
|
+
useState<FormState>(initialValues);
|
|
314
|
+
// keep aggregated values in sync when initial defaults change
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
setAggregatedValues((prev) => ({ ...prev, ...initialValues }));
|
|
317
|
+
}, [initialValues]);
|
|
318
|
+
|
|
319
|
+
// submit orchestration for current page
|
|
320
|
+
const currentPageSubmitRef = useRef<(() => Promise<boolean>) | null>(
|
|
321
|
+
null,
|
|
322
|
+
) as React.MutableRefObject<(() => Promise<boolean>) | null>;
|
|
323
|
+
const setCurrentPageSubmitRef = (fn: () => Promise<boolean>) => {
|
|
324
|
+
currentPageSubmitRef.current = fn;
|
|
325
|
+
};
|
|
326
|
+
// Latest merged form values (set in handlePageSubmit) so we can build payload in finish() without stale state
|
|
327
|
+
const lastMergedValuesRef = useRef<FormState | null>(null);
|
|
328
|
+
// Last submitted page values (set in handlePageSubmit) for validateFieldOnSubmit on Next
|
|
329
|
+
const lastSubmittedPageValuesRef = useRef<FormState>({});
|
|
330
|
+
// Results from validateFieldOnSubmit (keyed by templateFieldKey); block Next if any invalid
|
|
331
|
+
const [submitValidationResults, setSubmitValidationResults] =
|
|
332
|
+
React.useState<Record<
|
|
333
|
+
string,
|
|
334
|
+
{ isValid: boolean; message: string }
|
|
335
|
+
> | null>(null);
|
|
336
|
+
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
const typedData = data as GetProcessTemplateQuery;
|
|
339
|
+
if (typedData?.processTemplate) {
|
|
340
|
+
const templates = typedData.processTemplate as ProcessTemplate[];
|
|
341
|
+
|
|
342
|
+
setTemplates(templates);
|
|
343
|
+
setTemplatesSorted(
|
|
344
|
+
templates.map((t) => ({
|
|
345
|
+
...t,
|
|
346
|
+
pages: sortBy(t.pages as ProcessTemplatePage[]),
|
|
347
|
+
})),
|
|
348
|
+
);
|
|
349
|
+
setInitialValues(getInitialValuesFromTemplates(templates));
|
|
350
|
+
}
|
|
351
|
+
}, [data]);
|
|
352
|
+
|
|
353
|
+
const [flatPages, setFlatPages] = useState<FlatPage[]>([]);
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
let counter = 0;
|
|
356
|
+
const newFlatPages = templatesSorted.flatMap((tpl, ti) => {
|
|
357
|
+
if (tpl.pages) {
|
|
358
|
+
return (tpl.pages as ProcessTemplatePage[]).map((p, pi) => ({
|
|
359
|
+
...p,
|
|
360
|
+
_templateIndex: ti,
|
|
361
|
+
_templateId: tpl.id,
|
|
362
|
+
_templateName: tpl.name ?? "",
|
|
363
|
+
_pageIndexWithinTemplate: pi,
|
|
364
|
+
_flatIndex: counter++,
|
|
365
|
+
}));
|
|
366
|
+
} else {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
setFlatPages(newFlatPages);
|
|
371
|
+
}, [templatesSorted]);
|
|
372
|
+
|
|
373
|
+
const [globalStep, setGlobalStep] = React.useState(0);
|
|
374
|
+
const totalSteps = flatPages.length;
|
|
375
|
+
|
|
376
|
+
const [currentFlat, setCurrentFlat] = useState<FlatPage | null>(null);
|
|
377
|
+
const [currentTemplate, setCurrentTemplate] =
|
|
378
|
+
useState<ProcessTemplate | null>(null);
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
if (flatPages.length > 0) {
|
|
381
|
+
const thisFlat = flatPages[globalStep];
|
|
382
|
+
setCurrentFlat(thisFlat);
|
|
383
|
+
setCurrentTemplate(templatesSorted[thisFlat._templateIndex]);
|
|
384
|
+
}
|
|
385
|
+
}, [flatPages, globalStep]);
|
|
386
|
+
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (currentTemplate) {
|
|
389
|
+
const currentParams = new URLSearchParams(searchParams.toString());
|
|
390
|
+
currentParams.set("template", currentTemplate.id);
|
|
391
|
+
router.replace(`${pathname}?${currentParams.toString()}`);
|
|
392
|
+
}
|
|
393
|
+
}, [currentTemplate]);
|
|
394
|
+
|
|
395
|
+
const [disabledFields, setDisabledFields] = React.useState<Set<string>>(
|
|
396
|
+
new Set(),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// track whether the current page is valid (reported by PageForm)
|
|
400
|
+
const [currentPageValid, setCurrentPageValid] = React.useState<boolean>(true);
|
|
401
|
+
const [isFieldValidationInProgress, setIsFieldValidationInProgress] =
|
|
402
|
+
React.useState(false);
|
|
403
|
+
|
|
404
|
+
const isFirst = globalStep === 0;
|
|
405
|
+
const isLast = globalStep === totalSteps - 1;
|
|
406
|
+
|
|
407
|
+
const setFieldsDisabled = React.useCallback(
|
|
408
|
+
(templateId: string, fieldNames: string[], disabled: boolean) => {
|
|
409
|
+
setDisabledFields((prev) => {
|
|
410
|
+
const newSet = new Set(prev);
|
|
411
|
+
fieldNames.forEach((fieldName) => {
|
|
412
|
+
const key = templateFieldKey(templateId, fieldName);
|
|
413
|
+
if (disabled) {
|
|
414
|
+
newSet.add(key);
|
|
415
|
+
} else {
|
|
416
|
+
newSet.delete(key);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
return newSet;
|
|
420
|
+
});
|
|
421
|
+
},
|
|
422
|
+
[],
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
if (props.templateConfig) {
|
|
427
|
+
const config = JSON.parse(props.templateConfig);
|
|
428
|
+
applyTemplateConfig(
|
|
429
|
+
{
|
|
430
|
+
name: config.taskName as string,
|
|
431
|
+
templateConfig: props.templateConfig,
|
|
432
|
+
} as SearchOption,
|
|
433
|
+
(templateId: string, fieldName: string, value: FieldValue) => {
|
|
434
|
+
const key = templateFieldKey(templateId, fieldName);
|
|
435
|
+
setInitialValues((prev) => ({ ...prev, [key]: value }));
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}, [props.templateConfig]);
|
|
440
|
+
|
|
441
|
+
const clearTemplateFields = (templateId: string) => {
|
|
442
|
+
setAggregatedValues((prev) => {
|
|
443
|
+
const copy = { ...prev };
|
|
444
|
+
for (const key of Object.keys(copy)) {
|
|
445
|
+
if (key.startsWith(`${templateId}::`)) {
|
|
446
|
+
delete copy[key];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return copy;
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// On Next: only merge form state. Do not build or send full config (that happens only on Finish).
|
|
454
|
+
const handlePageSubmit = (pageValues: FormState) => {
|
|
455
|
+
lastSubmittedPageValuesRef.current = pageValues;
|
|
456
|
+
setAggregatedValues((prev) => {
|
|
457
|
+
const merged = { ...prev, ...pageValues };
|
|
458
|
+
lastMergedValuesRef.current = merged;
|
|
459
|
+
return merged;
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const runPageSubmitValidation = async (
|
|
464
|
+
templateId: string,
|
|
465
|
+
submitValidationInputs: Array<{
|
|
466
|
+
fieldName?: string;
|
|
467
|
+
id: string;
|
|
468
|
+
}>,
|
|
469
|
+
values: FormState,
|
|
470
|
+
): Promise<boolean> => {
|
|
471
|
+
const input = submitValidationInputs
|
|
472
|
+
.filter(
|
|
473
|
+
(inp): inp is { fieldName: string; id: string } =>
|
|
474
|
+
typeof inp.fieldName === "string" && typeof inp.id === "string",
|
|
475
|
+
)
|
|
476
|
+
.map((inp) => {
|
|
477
|
+
const k = templateFieldKey(templateId, inp.fieldName);
|
|
478
|
+
const v = values[k];
|
|
479
|
+
return {
|
|
480
|
+
fieldName: inp.fieldName,
|
|
481
|
+
validationQuery: inp.id,
|
|
482
|
+
value: formatFieldValueForValidation(v),
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
try {
|
|
486
|
+
const results = await validateFieldOnSubmit(input);
|
|
487
|
+
const byKey: Record<string, { isValid: boolean; message: string }> = {};
|
|
488
|
+
let anyInvalid = false;
|
|
489
|
+
for (const r of results) {
|
|
490
|
+
const key = templateFieldKey(templateId, r.fieldName ?? "");
|
|
491
|
+
byKey[key] = {
|
|
492
|
+
isValid: r.isValid ?? false,
|
|
493
|
+
message: r.message ?? "",
|
|
494
|
+
};
|
|
495
|
+
if (!r.isValid) anyInvalid = true;
|
|
496
|
+
}
|
|
497
|
+
setSubmitValidationResults(byKey);
|
|
498
|
+
return !anyInvalid;
|
|
499
|
+
} catch {
|
|
500
|
+
const fallbackKey = templateFieldKey(
|
|
501
|
+
templateId,
|
|
502
|
+
submitValidationInputs[0]?.fieldName ?? "",
|
|
503
|
+
);
|
|
504
|
+
setSubmitValidationResults({
|
|
505
|
+
[fallbackKey]: {
|
|
506
|
+
isValid: false,
|
|
507
|
+
message: "Validation request failed",
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const next = async () => {
|
|
515
|
+
const submitFn = currentPageSubmitRef.current;
|
|
516
|
+
if (!submitFn) return;
|
|
517
|
+
const ok = await submitFn();
|
|
518
|
+
if (!ok) return;
|
|
519
|
+
|
|
520
|
+
if (props.readOnly) {
|
|
521
|
+
setSubmitValidationResults(null);
|
|
522
|
+
setGlobalStep((s) => Math.min(s + 1, totalSteps - 1));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const page = currentFlat;
|
|
527
|
+
const templateId = page?._templateId ?? "";
|
|
528
|
+
const submitValidationInputs = (page?.inputs ?? [])
|
|
529
|
+
.filter(
|
|
530
|
+
(inp): inp is NonNullable<typeof inp> & { id: string } =>
|
|
531
|
+
inp != null &&
|
|
532
|
+
!!inp.fieldName &&
|
|
533
|
+
!!(inp as { validationQueryOnSubmit?: string })
|
|
534
|
+
.validationQueryOnSubmit &&
|
|
535
|
+
!!(inp as { id?: string }).id,
|
|
536
|
+
)
|
|
537
|
+
.map((inp) => ({
|
|
538
|
+
fieldName: inp.fieldName ?? undefined,
|
|
539
|
+
id: (inp as { id: string }).id,
|
|
540
|
+
}));
|
|
541
|
+
|
|
542
|
+
const values = {
|
|
543
|
+
...aggregatedValues,
|
|
544
|
+
...lastSubmittedPageValuesRef.current,
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const shouldRunSubmitValidation =
|
|
548
|
+
submitValidationInputs.length > 0 &&
|
|
549
|
+
(!props.isEditMode ||
|
|
550
|
+
submitValidationInputs.some((inp) => {
|
|
551
|
+
const fieldName = inp.fieldName;
|
|
552
|
+
if (fieldName !== "uniqueIdentifier" && fieldName !== "subscriptions")
|
|
553
|
+
return false;
|
|
554
|
+
const key = templateFieldKey(templateId, fieldName ?? "");
|
|
555
|
+
const current = formatFieldValueForValidation(values[key]);
|
|
556
|
+
const initial = formatFieldValueForValidation(initialValues[key]);
|
|
557
|
+
return current !== initial;
|
|
558
|
+
}));
|
|
559
|
+
|
|
560
|
+
if (shouldRunSubmitValidation) {
|
|
561
|
+
const validationOk = await runPageSubmitValidation(
|
|
562
|
+
templateId,
|
|
563
|
+
submitValidationInputs,
|
|
564
|
+
values,
|
|
565
|
+
);
|
|
566
|
+
if (!validationOk) return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
setSubmitValidationResults(null);
|
|
570
|
+
setGlobalStep((s) => Math.min(s + 1, totalSteps - 1));
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const back = () => {
|
|
574
|
+
setSubmitValidationResults(null);
|
|
575
|
+
const newStep = globalStep - 1;
|
|
576
|
+
setGlobalStep(Math.max(newStep, 0));
|
|
577
|
+
if (newStep < 0 && props.handleBack) {
|
|
578
|
+
props.handleBack();
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const finish = async () => {
|
|
583
|
+
const submitFn = currentPageSubmitRef.current;
|
|
584
|
+
if (!submitFn) return;
|
|
585
|
+
const ok = await submitFn();
|
|
586
|
+
if (!ok) return;
|
|
587
|
+
|
|
588
|
+
const merged = lastMergedValuesRef.current ?? aggregatedValues;
|
|
589
|
+
const payload = buildProcessConfigMerge(templatesSorted, merged, metadata);
|
|
590
|
+
props.setProcessConfigMerge(payload);
|
|
591
|
+
props.handleFinish?.();
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const progress =
|
|
595
|
+
totalSteps > 0 ? Math.round(((globalStep + 1) / totalSteps) * 100) : 0;
|
|
596
|
+
|
|
597
|
+
const showLoading =
|
|
598
|
+
isLoading &&
|
|
599
|
+
!currentFlat &&
|
|
600
|
+
!currentTemplate &&
|
|
601
|
+
!props.templateConfig &&
|
|
602
|
+
(!props.processTemplateGroupId || !props.processTemplateId);
|
|
603
|
+
if (showLoading) {
|
|
604
|
+
return (
|
|
605
|
+
<Box sx={{ padding: theme.spacing(10, 3) }}>
|
|
606
|
+
<Loading />
|
|
607
|
+
</Box>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (!isLoading && !templates.length) {
|
|
611
|
+
return <Typography>No templates found.</Typography>;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return (
|
|
615
|
+
currentFlat &&
|
|
616
|
+
currentTemplate && (
|
|
617
|
+
<Card
|
|
618
|
+
sx={
|
|
619
|
+
props.isEditMode
|
|
620
|
+
? { border: 0, boxShadow: 0, borderRadius: 0 }
|
|
621
|
+
: {
|
|
622
|
+
border: `1px solid ${theme.palette.grey[300]}`,
|
|
623
|
+
borderRadius: 0,
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
>
|
|
627
|
+
<CardContent>
|
|
628
|
+
{!props.isEditMode && (
|
|
629
|
+
<Typography variant="h5" gutterBottom>
|
|
630
|
+
{currentTemplate.name}
|
|
631
|
+
</Typography>
|
|
632
|
+
)}
|
|
633
|
+
|
|
634
|
+
{templates.length > 1 ? (
|
|
635
|
+
<>
|
|
636
|
+
<TemplateNav
|
|
637
|
+
templates={templates}
|
|
638
|
+
activeIndex={currentFlat._templateIndex}
|
|
639
|
+
/>
|
|
640
|
+
<Divider sx={{ my: 2 }} />
|
|
641
|
+
</>
|
|
642
|
+
) : null}
|
|
643
|
+
|
|
644
|
+
<PageNav
|
|
645
|
+
pages={currentTemplate.pages as ProcessTemplatePage[]}
|
|
646
|
+
activeIndex={currentFlat._pageIndexWithinTemplate}
|
|
647
|
+
isEditMode={props.isEditMode}
|
|
648
|
+
/>
|
|
649
|
+
|
|
650
|
+
<Divider sx={{ my: 2 }} />
|
|
651
|
+
|
|
652
|
+
{/* Page header */}
|
|
653
|
+
<Header
|
|
654
|
+
title={currentFlat.title ?? ""}
|
|
655
|
+
subtitle={props.isEditMode ? "" : (currentFlat.description ?? "")}
|
|
656
|
+
/>
|
|
657
|
+
|
|
658
|
+
{/* Per-page form */}
|
|
659
|
+
<Box>
|
|
660
|
+
<PageForm
|
|
661
|
+
templateId={currentFlat._templateId}
|
|
662
|
+
page={currentFlat}
|
|
663
|
+
initialValues={aggregatedValues}
|
|
664
|
+
onPageSubmit={handlePageSubmit}
|
|
665
|
+
setSubmitRef={(fn) =>
|
|
666
|
+
setCurrentPageSubmitRef(fn ?? (() => Promise.resolve(false)))
|
|
667
|
+
}
|
|
668
|
+
setPageValidity={(v) => setCurrentPageValid(v)}
|
|
669
|
+
clearTemplateFields={(templateId) =>
|
|
670
|
+
clearTemplateFields(templateId)
|
|
671
|
+
}
|
|
672
|
+
readOnly={props.readOnly}
|
|
673
|
+
isEditMode={props.isEditMode}
|
|
674
|
+
isCreateMode={props.isCreateMode}
|
|
675
|
+
disabledFields={disabledFields}
|
|
676
|
+
setFieldsDisabled={setFieldsDisabled}
|
|
677
|
+
templateConfig={props.templateConfig || ""}
|
|
678
|
+
setFieldValidationInProgress={setIsFieldValidationInProgress}
|
|
679
|
+
submitValidationResults={submitValidationResults ?? undefined}
|
|
680
|
+
/>
|
|
681
|
+
</Box>
|
|
682
|
+
|
|
683
|
+
<Divider sx={{ my: 2 }} />
|
|
684
|
+
|
|
685
|
+
<Box sx={{ mt: 1, mb: 2 }}>
|
|
686
|
+
<LinearProgress
|
|
687
|
+
variant="determinate"
|
|
688
|
+
value={isFirst ? 0 : progress}
|
|
689
|
+
/>
|
|
690
|
+
</Box>
|
|
691
|
+
|
|
692
|
+
<Stack
|
|
693
|
+
direction="row"
|
|
694
|
+
justifyContent="right"
|
|
695
|
+
sx={{ mt: 3 }}
|
|
696
|
+
spacing={3}
|
|
697
|
+
>
|
|
698
|
+
{isFirst && !props.disableBackToTemplates && (
|
|
699
|
+
<Button
|
|
700
|
+
variant="contained"
|
|
701
|
+
onClick={back}
|
|
702
|
+
color="info"
|
|
703
|
+
startIcon={<ArrowLeftIcon />}
|
|
704
|
+
>
|
|
705
|
+
Back to templates
|
|
706
|
+
</Button>
|
|
707
|
+
)}
|
|
708
|
+
{!isFirst && (
|
|
709
|
+
<Button
|
|
710
|
+
variant="contained"
|
|
711
|
+
onClick={() => back()}
|
|
712
|
+
color="info"
|
|
713
|
+
startIcon={<ArrowLeftIcon />}
|
|
714
|
+
>
|
|
715
|
+
Back
|
|
716
|
+
</Button>
|
|
717
|
+
)}
|
|
718
|
+
{!isLast && (
|
|
719
|
+
<Button
|
|
720
|
+
variant="contained"
|
|
721
|
+
onClick={() => next()}
|
|
722
|
+
color="info"
|
|
723
|
+
disabled={isFieldValidationInProgress || !currentPageValid}
|
|
724
|
+
>
|
|
725
|
+
Next
|
|
726
|
+
</Button>
|
|
727
|
+
)}
|
|
728
|
+
{isLast && !props.readOnly && (
|
|
729
|
+
<Button
|
|
730
|
+
variant="contained"
|
|
731
|
+
onClick={() => finish()}
|
|
732
|
+
color="success"
|
|
733
|
+
endIcon={<ArrowRightIcon />}
|
|
734
|
+
disabled={isFieldValidationInProgress}
|
|
735
|
+
>
|
|
736
|
+
Finish
|
|
737
|
+
</Button>
|
|
738
|
+
)}
|
|
739
|
+
</Stack>
|
|
740
|
+
</CardContent>
|
|
741
|
+
</Card>
|
|
742
|
+
)
|
|
743
|
+
);
|
|
744
|
+
}
|