@evenicanpm/admin-integrate 1.8.1 → 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.
Files changed (71) hide show
  1. package/documents/ProcessTask/fragments.ts +4 -0
  2. package/documents/ProcessTemplate/fragments.ts +30 -0
  3. package/documents/ProcessTemplate/list.ts +29 -0
  4. package/package.json +2 -2
  5. package/src/api/process-config/mutation/process-config-import.mutation.ts +1 -1
  6. package/src/api/templates/queries/get-field-values.query.ts +48 -0
  7. package/src/api/templates/queries/get-field-values.server.ts +9 -0
  8. package/src/api/templates/queries/index.ts +4 -0
  9. package/src/api/templates/queries/types.ts +2 -2
  10. package/src/api/templates/queries/validate-field-on-check.query.ts +22 -0
  11. package/src/api/templates/queries/validate-field-on-check.server.ts +30 -0
  12. package/src/api/templates/queries/validate-field-on-submit.server.ts +18 -0
  13. package/src/components/data-table/table-row.tsx +1 -1
  14. package/src/components/icons/up-down.tsx +1 -0
  15. package/src/components/integration-view/integration-view.tsx +10 -15
  16. package/src/components/integration-view/nodes/task-node.tsx +1 -1
  17. package/src/components/integration-view/utils/mapping.ts +10 -11
  18. package/src/components/list-filter/list-filter.tsx +2 -0
  19. package/src/components/templatesV2/inputs/FormikPageBody.tsx +191 -0
  20. package/src/components/templatesV2/inputs/context/FormModeContext.tsx +31 -0
  21. package/src/components/templatesV2/inputs/context/TemplateContext.tsx +37 -0
  22. package/src/components/templatesV2/inputs/field-names.ts +8 -0
  23. package/src/components/templatesV2/inputs/form-type.ts +48 -0
  24. package/src/components/templatesV2/inputs/index.tsx +33 -0
  25. package/src/components/templatesV2/inputs/input-type.ts +23 -0
  26. package/src/components/templatesV2/inputs/primitives/CheckboxInput.tsx +45 -0
  27. package/src/components/templatesV2/inputs/primitives/NumberInput.tsx +58 -0
  28. package/src/components/templatesV2/inputs/primitives/SelectorInput.tsx +111 -0
  29. package/src/components/templatesV2/inputs/primitives/TextInput.tsx +55 -0
  30. package/src/components/templatesV2/inputs/primitives/TextWithQueryInput.tsx +292 -0
  31. package/src/components/templatesV2/inputs/primitives/index.ts +10 -0
  32. package/src/components/templatesV2/inputs/renderers/index.ts +2 -0
  33. package/src/components/templatesV2/inputs/renderers/renderFormControl.tsx +376 -0
  34. package/src/components/templatesV2/inputs/renderers/renderSearchInput.tsx +129 -0
  35. package/src/components/templatesV2/inputs/search/MultiTextSearchInput.tsx +87 -0
  36. package/src/components/templatesV2/inputs/search/SearchInputWrapper.tsx +113 -0
  37. package/src/components/templatesV2/inputs/search/TextSearchInput.tsx +309 -0
  38. package/src/components/templatesV2/inputs/search/index.ts +4 -0
  39. package/src/components/templatesV2/inputs/search/search-query-key.ts +23 -0
  40. package/src/components/templatesV2/inputs/search/useFieldValuesOptions.ts +58 -0
  41. package/src/components/templatesV2/inputs/search/useSearchInput.ts +74 -0
  42. package/src/components/templatesV2/inputs/table/InputTable.tsx +259 -0
  43. package/src/components/templatesV2/inputs/table/index.ts +1 -0
  44. package/src/components/templatesV2/inputs/utils/applyTemplateConfig.ts +28 -0
  45. package/src/components/templatesV2/inputs/utils/arrangeInputs.ts +51 -0
  46. package/src/components/templatesV2/inputs/utils/index.ts +6 -0
  47. package/src/components/templatesV2/inputs/utils/parseQueryName.ts +20 -0
  48. package/src/components/templatesV2/inputs/utils/renderReadOnlyText.tsx +26 -0
  49. package/src/components/templatesV2/inputs/utils/selectors.ts +69 -0
  50. package/src/components/templatesV2/inputs/utils/toStringArray.ts +23 -0
  51. package/src/components/templatesV2/pageForm.tsx +151 -0
  52. package/src/components/{templates → templatesV2}/templates-list/templates-list-row.tsx +2 -1
  53. package/src/components/{templates → templatesV2}/templates-list/templates-list-table.tsx +2 -2
  54. package/src/components/{templates → templatesV2}/templates-list/templates-list.tsx +19 -28
  55. package/src/components/{templates → templatesV2}/types.tsx +12 -12
  56. package/src/components/templatesV2/validation/buildZodSchema.ts +216 -0
  57. package/src/components/templatesV2/validation/zodFormikValidate.ts +17 -0
  58. package/src/components/templatesV2/wizard.tsx +744 -0
  59. package/src/pages/integration-create/integration-create-steps.ts +8 -0
  60. package/src/pages/integration-create/integration-create.tsx +61 -14
  61. package/src/pages/integration-create/select-step.tsx +3 -3
  62. package/src/pages/integration-create/template-list-step.tsx +28 -3
  63. package/src/pages/integration-create/template-options-step.tsx +119 -0
  64. package/src/pages/integration-create/wizard-step.tsx +5 -4
  65. package/src/pages/integration-details/task-drawer/add-task.tsx +4 -4
  66. package/src/pages/integration-details/task-drawer/edit-task.tsx +3 -3
  67. package/src/components/templates/inputs/index.tsx +0 -1392
  68. package/src/components/templates/wizard.tsx +0 -737
  69. /package/src/components/{templates → templatesV2}/index.ts +0 -0
  70. /package/src/components/{templates → templatesV2}/template-filter.ts +0 -0
  71. /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
- {/* icon */}
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, index) => (
45
+ {props.items.map((item) => (
46
46
  <TemplatesListRow
47
- key={`list-row-${index + 1}`}
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/templates/template-filter";
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
- if (props.isTemplateGroupsList) {
98
- const typedData = props.data as GetProcessTemplateGroupsQuery;
99
- if (typedData?.processTemplateGroups?.nodes) {
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 resultsReturned = props.isTemplateGroupsList
111
- ? (props.data as GetProcessTemplateGroupsQuery)?.processTemplateGroups
112
- ?.pageInfo.resultsReturned
113
- : (props.data as GetProcessTemplatesQuery)?.processTemplates?.pageInfo
114
- .resultsReturned;
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
- export type ProcessTemplateInputSchema = NonNullable<
4
- NonNullable<NonNullable<ProcessTemplateInput>["schemas"]>[number]
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 ProcessTemplateInput = NonNullable<
8
- NonNullable<NonNullable<ProcessTemplatePage>["inputs"]>[number]
6
+ export type ProcessTemplate = ArrayElement<
7
+ NonNullable<GetProcessTemplateQuery["processTemplate"]>
9
8
  >;
10
-
11
- export type ProcessTemplatePage = NonNullable<
12
- NonNullable<NonNullable<ProcessTemplate>["pages"]>[number]
9
+ export type ProcessTemplatePage = ArrayElement<
10
+ NonNullable<ProcessTemplate["pages"]>
13
11
  >;
14
-
15
- export type ProcessTemplate = NonNullable<
16
- NonNullable<GetProcessTemplateQuery["processTemplate"]>[number]
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>>; //TODO: Actually map this out properly
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
+ }