@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,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Formik } from "formik";
|
|
4
|
+
import React, { useEffect } from "react";
|
|
5
|
+
import { FormikPageBody } from "./inputs";
|
|
6
|
+
import type { FlatPage, FormState } from "./types";
|
|
7
|
+
import { buildZodSchemaForPage } from "./validation/buildZodSchema";
|
|
8
|
+
import { zodFormikValidate } from "./validation/zodFormikValidate";
|
|
9
|
+
|
|
10
|
+
export default function PageForm({
|
|
11
|
+
templateId,
|
|
12
|
+
page,
|
|
13
|
+
initialValues,
|
|
14
|
+
onPageSubmit,
|
|
15
|
+
setSubmitRef,
|
|
16
|
+
setPageValidity,
|
|
17
|
+
clearTemplateFields,
|
|
18
|
+
readOnly,
|
|
19
|
+
isEditMode,
|
|
20
|
+
isCreateMode,
|
|
21
|
+
disabledFields,
|
|
22
|
+
setFieldsDisabled,
|
|
23
|
+
templateConfig,
|
|
24
|
+
setFieldValidationInProgress,
|
|
25
|
+
submitValidationResults,
|
|
26
|
+
}: Readonly<{
|
|
27
|
+
templateId: string;
|
|
28
|
+
page: FlatPage;
|
|
29
|
+
initialValues: FormState;
|
|
30
|
+
onPageSubmit: (values: FormState) => void;
|
|
31
|
+
setSubmitRef?: (fn: (() => Promise<boolean>) | undefined) => void;
|
|
32
|
+
setPageValidity?: (valid: boolean) => void;
|
|
33
|
+
clearTemplateFields: (templateId: string) => void;
|
|
34
|
+
readOnly?: boolean;
|
|
35
|
+
isEditMode?: boolean;
|
|
36
|
+
isCreateMode?: boolean;
|
|
37
|
+
disabledFields?: Set<string>;
|
|
38
|
+
setFieldsDisabled?: (
|
|
39
|
+
templateId: string,
|
|
40
|
+
fieldNames: string[],
|
|
41
|
+
disabled: boolean,
|
|
42
|
+
) => void;
|
|
43
|
+
templateConfig?: string;
|
|
44
|
+
setFieldValidationInProgress?: (inProgress: boolean) => void;
|
|
45
|
+
submitValidationResults?: Record<
|
|
46
|
+
string,
|
|
47
|
+
{ isValid: boolean; message: string }
|
|
48
|
+
>;
|
|
49
|
+
}>) {
|
|
50
|
+
const zSchema = buildZodSchemaForPage(page, templateId);
|
|
51
|
+
const pageKeys = React.useMemo(
|
|
52
|
+
() =>
|
|
53
|
+
(page.inputs ?? []).map(
|
|
54
|
+
(inp) => templateId + "::" + (inp?.fieldName ?? ""),
|
|
55
|
+
),
|
|
56
|
+
[page.inputs, templateId],
|
|
57
|
+
);
|
|
58
|
+
const initialTouched = React.useMemo(() => ({}), []);
|
|
59
|
+
|
|
60
|
+
const stepKey = `${templateId}:${page._flatIndex ?? ""}`;
|
|
61
|
+
|
|
62
|
+
const formikRef = React.useRef<any>(null);
|
|
63
|
+
|
|
64
|
+
const submit = React.useCallback(async () => {
|
|
65
|
+
const formikHelpers = formikRef.current;
|
|
66
|
+
if (!formikHelpers) return false;
|
|
67
|
+
const errors = await formikHelpers.validateForm();
|
|
68
|
+
const errorKeys = Object.keys(errors || {});
|
|
69
|
+
const hasErrors = pageKeys.some(
|
|
70
|
+
(k) =>
|
|
71
|
+
Boolean(errors[k]) ||
|
|
72
|
+
errorKeys.some((ek) => ek === k || ek.startsWith(k + ".")),
|
|
73
|
+
);
|
|
74
|
+
if (hasErrors) {
|
|
75
|
+
const touched: Record<string, boolean> = {};
|
|
76
|
+
pageKeys.forEach((k) => (touched[k] = true));
|
|
77
|
+
formikHelpers.setTouched(touched);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
await formikHelpers.submitForm();
|
|
81
|
+
return true;
|
|
82
|
+
}, [pageKeys]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Formik
|
|
86
|
+
key={stepKey}
|
|
87
|
+
initialValues={initialValues}
|
|
88
|
+
initialTouched={initialTouched}
|
|
89
|
+
enableReinitialize
|
|
90
|
+
validateOnMount={false}
|
|
91
|
+
validateOnBlur={true}
|
|
92
|
+
validateOnChange={false}
|
|
93
|
+
validate={zodFormikValidate(zSchema)}
|
|
94
|
+
onSubmit={(values) => {
|
|
95
|
+
// Only pass the keys for this page back to the parent.
|
|
96
|
+
const pageKeys = (page.inputs ?? []).map(
|
|
97
|
+
(inp) => templateId + "::" + (inp?.fieldName ?? ""),
|
|
98
|
+
);
|
|
99
|
+
const filtered: FormState = {};
|
|
100
|
+
for (const k of pageKeys) {
|
|
101
|
+
if (
|
|
102
|
+
typeof values === "object" &&
|
|
103
|
+
values !== null &&
|
|
104
|
+
Object.hasOwn(values as object, k)
|
|
105
|
+
) {
|
|
106
|
+
filtered[k] = (values as Record<string, any>)[k];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
onPageSubmit(filtered);
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{(formik) => {
|
|
113
|
+
formikRef.current = formik;
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (setSubmitRef) setSubmitRef(submit);
|
|
116
|
+
return () => {
|
|
117
|
+
if (setSubmitRef) setSubmitRef(undefined);
|
|
118
|
+
};
|
|
119
|
+
}, [submit, setSubmitRef]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (setPageValidity) setPageValidity(true);
|
|
123
|
+
}, [stepKey, setPageValidity]);
|
|
124
|
+
|
|
125
|
+
// Keep Next button enabled so user can always click it. Validation runs on Next click (submit).
|
|
126
|
+
// We do not disable Next based on errors; errors show after they click Next or touch a field.
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (setPageValidity) setPageValidity(true);
|
|
129
|
+
}, [setPageValidity]);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<form onSubmit={formik.handleSubmit}>
|
|
133
|
+
<FormikPageBody
|
|
134
|
+
templateId={templateId}
|
|
135
|
+
page={page}
|
|
136
|
+
clearTemplateFields={() => clearTemplateFields(templateId)}
|
|
137
|
+
readOnly={readOnly}
|
|
138
|
+
isEditMode={isEditMode}
|
|
139
|
+
isCreateMode={isCreateMode}
|
|
140
|
+
disabledFields={disabledFields}
|
|
141
|
+
setFieldsDisabled={setFieldsDisabled}
|
|
142
|
+
templateConfig={templateConfig}
|
|
143
|
+
setFieldValidationInProgress={setFieldValidationInProgress}
|
|
144
|
+
submitValidationResults={submitValidationResults}
|
|
145
|
+
/>
|
|
146
|
+
</form>
|
|
147
|
+
);
|
|
148
|
+
}}
|
|
149
|
+
</Formik>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
StyledTableCell,
|
|
6
6
|
StyledTableRow,
|
|
7
7
|
} from "@evenicanpm/admin-integrate/components/data-table/table-row";
|
|
8
|
+
import { IntegrationStatusIcon } from "@evenicanpm/admin-integrate/components/integration-list";
|
|
8
9
|
import { ChevronRightOutlined } from "@mui/icons-material";
|
|
9
10
|
// MUI
|
|
10
11
|
import {
|
|
@@ -33,7 +34,7 @@ const TemplatesListRow: React.FC<Props> = (props: Props) => {
|
|
|
33
34
|
>
|
|
34
35
|
<Stack spacing={1} direction="row">
|
|
35
36
|
<Box display="flex" justifyContent="flex-end" alignItems="center">
|
|
36
|
-
{
|
|
37
|
+
<IntegrationStatusIcon processIcon={"DB"} />
|
|
37
38
|
</Box>
|
|
38
39
|
<Box display="flex" justifyContent="flex-end" alignItems="center">
|
|
39
40
|
<Typography>{props.item.name}</Typography>
|
|
@@ -42,9 +42,9 @@ export default function TemplatesListTable(props: Readonly<Props>) {
|
|
|
42
42
|
rowSx={props.rowSx}
|
|
43
43
|
/>
|
|
44
44
|
<TableBody>
|
|
45
|
-
{props.items.map((item
|
|
45
|
+
{props.items.map((item) => (
|
|
46
46
|
<TemplatesListRow
|
|
47
|
-
key={
|
|
47
|
+
key={item.id}
|
|
48
48
|
item={item}
|
|
49
49
|
heading={props.heading}
|
|
50
50
|
sx={props.rowSx}
|
|
@@ -14,7 +14,7 @@ import type { ListFilterOutput } from "@evenicanpm/admin-integrate/components/li
|
|
|
14
14
|
import {
|
|
15
15
|
type TemplateListFilters,
|
|
16
16
|
templateListFilters,
|
|
17
|
-
} from "@evenicanpm/admin-integrate/components/
|
|
17
|
+
} from "@evenicanpm/admin-integrate/components/templatesV2/template-filter";
|
|
18
18
|
import {
|
|
19
19
|
Box,
|
|
20
20
|
Card,
|
|
@@ -25,9 +25,16 @@ import {
|
|
|
25
25
|
useTheme,
|
|
26
26
|
} from "@mui/material";
|
|
27
27
|
import { useTranslations } from "next-intl";
|
|
28
|
-
import { useEffect, useState } from "react";
|
|
28
|
+
import { useEffect, useMemo, useState } from "react";
|
|
29
29
|
import TemplatesListTable from "./templates-list-table";
|
|
30
30
|
|
|
31
|
+
/** Shared shape for processTemplateGroups / processTemplates used to avoid duplicated branches. */
|
|
32
|
+
function getConnection(props: Readonly<Props>) {
|
|
33
|
+
return props.isTemplateGroupsList
|
|
34
|
+
? (props.data as GetProcessTemplateGroupsQuery)?.processTemplateGroups
|
|
35
|
+
: (props.data as GetProcessTemplatesQuery)?.processTemplates;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
export interface Item {
|
|
32
39
|
id: string;
|
|
33
40
|
name: string;
|
|
@@ -94,29 +101,16 @@ export default function TemplatesList(props: Readonly<Props>) {
|
|
|
94
101
|
};
|
|
95
102
|
|
|
96
103
|
useEffect(() => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
setItems(typedData.processTemplateGroups.nodes as Item[]);
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
const typedData = props.data as GetProcessTemplatesQuery;
|
|
104
|
-
if (typedData?.processTemplates?.nodes) {
|
|
105
|
-
setItems(typedData.processTemplates.nodes as Item[]);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}, [props.data]);
|
|
104
|
+
const conn = getConnection(props);
|
|
105
|
+
if (conn?.nodes) setItems(conn.nodes as Item[]);
|
|
106
|
+
}, [props.data, props.isTemplateGroupsList]);
|
|
109
107
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const recordCount = props.isTemplateGroupsList
|
|
117
|
-
? (props.data as GetProcessTemplateGroupsQuery)?.processTemplateGroups
|
|
118
|
-
?.recordCount
|
|
119
|
-
: (props.data as GetProcessTemplatesQuery)?.processTemplates?.recordCount;
|
|
108
|
+
const connection = useMemo(
|
|
109
|
+
() => getConnection(props),
|
|
110
|
+
[props.data, props.isTemplateGroupsList],
|
|
111
|
+
);
|
|
112
|
+
const resultsReturned = connection?.pageInfo?.resultsReturned;
|
|
113
|
+
const recordCount = connection?.recordCount;
|
|
120
114
|
|
|
121
115
|
return (
|
|
122
116
|
<>
|
|
@@ -144,10 +138,7 @@ export default function TemplatesList(props: Readonly<Props>) {
|
|
|
144
138
|
</Card>
|
|
145
139
|
)}
|
|
146
140
|
|
|
147
|
-
{!props.loading &&
|
|
148
|
-
((props.data as GetProcessTemplateGroupsQuery)?.processTemplateGroups ||
|
|
149
|
-
(props.data as GetProcessTemplatesQuery)?.processTemplates) &&
|
|
150
|
-
items.length > 0 ? (
|
|
141
|
+
{!props.loading && connection && items.length > 0 ? (
|
|
151
142
|
<Box
|
|
152
143
|
sx={{
|
|
153
144
|
minHeight:
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import type { GetProcessTemplateQuery } from "@evenicanpm/admin-core/api/e4/graphqlRequestSdk";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
>;
|
|
3
|
+
/** Element type of an array (or nullable array); avoids repeating NonNullable<...>[number]. */
|
|
4
|
+
type ArrayElement<T> = T extends readonly (infer U)[] ? NonNullable<U> : never;
|
|
6
5
|
|
|
7
|
-
export type
|
|
8
|
-
NonNullable<
|
|
6
|
+
export type ProcessTemplate = ArrayElement<
|
|
7
|
+
NonNullable<GetProcessTemplateQuery["processTemplate"]>
|
|
9
8
|
>;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
NonNullable<NonNullable<ProcessTemplate>["pages"]>[number]
|
|
9
|
+
export type ProcessTemplatePage = ArrayElement<
|
|
10
|
+
NonNullable<ProcessTemplate["pages"]>
|
|
13
11
|
>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
export type ProcessTemplateInput = ArrayElement<
|
|
13
|
+
NonNullable<ProcessTemplatePage["inputs"]>
|
|
14
|
+
>;
|
|
15
|
+
export type ProcessTemplateInputSchema = ArrayElement<
|
|
16
|
+
NonNullable<ProcessTemplateInput["schemas"]>
|
|
17
17
|
>;
|
|
18
18
|
|
|
19
19
|
export type Data = { processTemplate: ProcessTemplate[] };
|
|
@@ -38,7 +38,7 @@ export type ErrorState = Record<string, string | null>;
|
|
|
38
38
|
//This needs to be shared, maybe a way to also have this componentized so theres a proper schema for it?
|
|
39
39
|
export type ProcessConfigMerge = {
|
|
40
40
|
input: {
|
|
41
|
-
config: Array<Record<string, FieldValue>>;
|
|
41
|
+
config: Array<Record<string, FieldValue>>;
|
|
42
42
|
};
|
|
43
43
|
};
|
|
44
44
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { InputType } from "../inputs/input-type";
|
|
3
|
+
import type { ProcessTemplateInputSchema, ProcessTemplatePage } from "../types";
|
|
4
|
+
import { sortBy, templateFieldKey, toRegex } from "../types";
|
|
5
|
+
|
|
6
|
+
type CellError = { colKey: string; message: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate a single input-table row against column schemas.
|
|
10
|
+
* Returns per-cell errors so we can show them below each input.
|
|
11
|
+
*/
|
|
12
|
+
function validateTableRow(
|
|
13
|
+
row: Record<string, unknown>,
|
|
14
|
+
columnSchemas: ProcessTemplateInputSchema[],
|
|
15
|
+
): CellError[] {
|
|
16
|
+
const errors: CellError[] = [];
|
|
17
|
+
const ordered = sortBy(columnSchemas);
|
|
18
|
+
|
|
19
|
+
ordered.forEach((col, colIndex) => {
|
|
20
|
+
const colKey = col.fieldKey ?? col.label ?? `col_${colIndex}`;
|
|
21
|
+
const cell = row[colKey];
|
|
22
|
+
let strVal: string;
|
|
23
|
+
if (cell == null || typeof cell === "object") {
|
|
24
|
+
strVal = "";
|
|
25
|
+
} else if (typeof cell === "string") {
|
|
26
|
+
strVal = cell.trim();
|
|
27
|
+
} else if (typeof cell === "number" || typeof cell === "boolean") {
|
|
28
|
+
strVal = String(cell).trim();
|
|
29
|
+
} else {
|
|
30
|
+
strVal = "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (col.inputType === InputType.CHECKBOX) {
|
|
34
|
+
if (col.isRequired && !cell) {
|
|
35
|
+
errors.push({ colKey, message: "required" });
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (col.isRequired && strVal.length === 0) {
|
|
41
|
+
errors.push({ colKey, message: "required" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (strVal.length === 0) return;
|
|
46
|
+
|
|
47
|
+
const regex = toRegex(col.validationRegex ?? null);
|
|
48
|
+
if (regex && !regex.test(strVal)) {
|
|
49
|
+
errors.push({ colKey, message: "invalid format" });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return errors;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build zod schema for an input-table field, including nested row validation.
|
|
58
|
+
* Emits one issue per cell error with path [rowIndex, colKey] so Formik can show errors below each input.
|
|
59
|
+
*/
|
|
60
|
+
function buildInputTableSchema(
|
|
61
|
+
input: NonNullable<ProcessTemplatePage["inputs"]>[number],
|
|
62
|
+
): z.ZodTypeAny {
|
|
63
|
+
const arraySchema = z.array(z.record(z.string(), z.any()));
|
|
64
|
+
const arrayWithMin = input?.isRequired
|
|
65
|
+
? arraySchema.min(1, "At least one row is required")
|
|
66
|
+
: arraySchema;
|
|
67
|
+
|
|
68
|
+
const base = z.preprocess((val) => val ?? [], arrayWithMin);
|
|
69
|
+
|
|
70
|
+
const schemas = (input?.schemas ?? []).filter(
|
|
71
|
+
(s): s is NonNullable<typeof s> => s != null,
|
|
72
|
+
);
|
|
73
|
+
if (schemas.length === 0) return base;
|
|
74
|
+
|
|
75
|
+
return base.superRefine((rows, ctx) => {
|
|
76
|
+
for (let i = 0; i < rows.length; i++) {
|
|
77
|
+
const row = rows[i] as Record<string, unknown>;
|
|
78
|
+
const cellErrors = validateTableRow(row, schemas);
|
|
79
|
+
for (const { colKey, message } of cellErrors) {
|
|
80
|
+
ctx.addIssue({
|
|
81
|
+
code: z.ZodIssueCode.custom,
|
|
82
|
+
message,
|
|
83
|
+
path: [i, colKey],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build schema for a single non-table field based on input type.
|
|
92
|
+
* Formik values can be string, number, boolean, or string[] depending on input type.
|
|
93
|
+
*/
|
|
94
|
+
function buildFieldSchema(
|
|
95
|
+
input: NonNullable<ProcessTemplatePage["inputs"]>[number],
|
|
96
|
+
): z.ZodTypeAny {
|
|
97
|
+
if (input == null) return z.any();
|
|
98
|
+
const inputType = input.inputType;
|
|
99
|
+
|
|
100
|
+
let schema: z.ZodTypeAny;
|
|
101
|
+
switch (inputType) {
|
|
102
|
+
case InputType.NUMBER:
|
|
103
|
+
schema = z.union([z.number(), z.string(), z.undefined(), z.null()]);
|
|
104
|
+
break;
|
|
105
|
+
case InputType.CHECKBOX:
|
|
106
|
+
schema = z.union([z.boolean(), z.undefined(), z.null()]);
|
|
107
|
+
break;
|
|
108
|
+
case InputType.MULTI_TEXT_SEARCH:
|
|
109
|
+
schema = z.union([z.array(z.string()), z.undefined(), z.null()]);
|
|
110
|
+
break;
|
|
111
|
+
case InputType.TEXT:
|
|
112
|
+
case InputType.SELECTOR:
|
|
113
|
+
case InputType.TEXT_UNIQUE_IDENTIFIER:
|
|
114
|
+
case InputType.TEXT_SEARCH:
|
|
115
|
+
default:
|
|
116
|
+
schema = z.union([z.string(), z.undefined(), z.null()]);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (input?.isRequired) {
|
|
121
|
+
switch (inputType) {
|
|
122
|
+
case InputType.CHECKBOX:
|
|
123
|
+
schema = schema.refine((val: unknown) => val === true, "required");
|
|
124
|
+
break;
|
|
125
|
+
case InputType.MULTI_TEXT_SEARCH:
|
|
126
|
+
schema = schema.refine(
|
|
127
|
+
(val: unknown) => Array.isArray(val) && val.length > 0,
|
|
128
|
+
"required",
|
|
129
|
+
);
|
|
130
|
+
break;
|
|
131
|
+
case InputType.NUMBER:
|
|
132
|
+
schema = schema.refine((val: unknown) => {
|
|
133
|
+
if (val == null || val === "") return false;
|
|
134
|
+
if (typeof val === "number") return true;
|
|
135
|
+
if (typeof val === "object") return false;
|
|
136
|
+
if (typeof val === "string") return val.trim().length > 0;
|
|
137
|
+
if (typeof val === "boolean")
|
|
138
|
+
return (val ? "true" : "false").trim().length > 0;
|
|
139
|
+
return false;
|
|
140
|
+
}, "required");
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
schema = schema.refine((val: unknown) => {
|
|
144
|
+
if (val == null || typeof val === "object") return false;
|
|
145
|
+
if (typeof val === "string") return val.trim().length > 0;
|
|
146
|
+
if (typeof val === "number") return String(val).trim().length > 0;
|
|
147
|
+
if (typeof val === "boolean")
|
|
148
|
+
return (val ? "true" : "false").trim().length > 0;
|
|
149
|
+
return false;
|
|
150
|
+
}, "required");
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (input?.validationRegex) {
|
|
156
|
+
const regex = toRegex(input.validationRegex);
|
|
157
|
+
if (regex) {
|
|
158
|
+
schema = schema.refine((val: unknown) => {
|
|
159
|
+
if (!val) return true;
|
|
160
|
+
if (typeof val === "object") return false;
|
|
161
|
+
if (typeof val === "string") return regex.test(val);
|
|
162
|
+
if (typeof val === "number") return regex.test(String(val));
|
|
163
|
+
if (typeof val === "boolean") return regex.test(val ? "true" : "false");
|
|
164
|
+
return false;
|
|
165
|
+
}, "Invalid format");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return schema;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build schema for a single page (only keys present on that page).
|
|
174
|
+
*/
|
|
175
|
+
export function buildZodSchemaForPage(
|
|
176
|
+
page: ProcessTemplatePage,
|
|
177
|
+
templateId: string,
|
|
178
|
+
) {
|
|
179
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
180
|
+
const requiredKeys: string[] = [];
|
|
181
|
+
|
|
182
|
+
page?.inputs?.forEach((input) => {
|
|
183
|
+
if (!input?.fieldName) return;
|
|
184
|
+
if (
|
|
185
|
+
input.inputType === InputType.HIDDEN ||
|
|
186
|
+
input.inputType === InputType.HIDDEN_NUMBER ||
|
|
187
|
+
input.inputType === InputType.HIDDEN_NUMBER_OR_NULL
|
|
188
|
+
) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const key = templateFieldKey(templateId, input.fieldName);
|
|
193
|
+
if (input.isRequired) requiredKeys.push(key);
|
|
194
|
+
|
|
195
|
+
if (input.inputType === InputType.INPUT_TABLE) {
|
|
196
|
+
shape[key] = buildInputTableSchema(input);
|
|
197
|
+
} else {
|
|
198
|
+
shape[key] = buildFieldSchema(input);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return z
|
|
203
|
+
.object(shape)
|
|
204
|
+
.passthrough()
|
|
205
|
+
.superRefine((obj, ctx) => {
|
|
206
|
+
for (const key of requiredKeys) {
|
|
207
|
+
if (!Object.hasOwn(obj, key)) {
|
|
208
|
+
ctx.addIssue({
|
|
209
|
+
code: z.ZodIssueCode.custom,
|
|
210
|
+
path: [key],
|
|
211
|
+
message: "required",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export function zodFormikValidate(schema: z.ZodTypeAny) {
|
|
4
|
+
return (values: unknown) => {
|
|
5
|
+
const result = schema.safeParse(values);
|
|
6
|
+
|
|
7
|
+
if (result.success) return {};
|
|
8
|
+
|
|
9
|
+
const errors: Record<string, string> = {};
|
|
10
|
+
|
|
11
|
+
result.error.issues.forEach((issue) => {
|
|
12
|
+
errors[issue.path.join(".")] = issue.message;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return errors;
|
|
16
|
+
};
|
|
17
|
+
}
|