@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,58 @@
1
+ "use client";
2
+
3
+ import type { GetFieldValuesQuery } from "@evenicanpm/admin-core/api/e4/graphqlRequestSdk";
4
+ import { queryFn } from "@evenicanpm/admin-integrate/api/templates/queries/get-field-values.server";
5
+ import { debounce } from "lodash";
6
+ import * as React from "react";
7
+
8
+ export type FieldValueOption = { name: string; value: string };
9
+
10
+ function dataToOptions(
11
+ data: GetFieldValuesQuery | undefined,
12
+ ): FieldValueOption[] {
13
+ const values = data?.getFieldValues?.values ?? [];
14
+ return values
15
+ .filter(
16
+ (v): v is NonNullable<typeof v> => v?.name != null && v?.value != null,
17
+ )
18
+ .map((v) => ({ name: String(v.name), value: String(v.value) }));
19
+ }
20
+
21
+ export function useFieldValuesOptions(
22
+ fieldName: string | undefined,
23
+ validationQuery: string | undefined,
24
+ searchText: string,
25
+ ) {
26
+ const [options, setOptions] = React.useState<FieldValueOption[]>([]);
27
+ const [loading, setLoading] = React.useState(false);
28
+
29
+ const fetchOptions = React.useMemo(
30
+ () =>
31
+ debounce((field: string, query: string, value: string) => {
32
+ queryFn({ input: { fieldName: field, validationQuery: query, value } })
33
+ .then((data) => setOptions(dataToOptions(data)))
34
+ .catch(() => setOptions([]))
35
+ .finally(() => setLoading(false));
36
+ }, 400),
37
+ [],
38
+ );
39
+
40
+ React.useEffect(() => {
41
+ if (!fieldName || !validationQuery) {
42
+ setOptions([]);
43
+ return;
44
+ }
45
+ const trimmed = searchText.trim();
46
+ if (!trimmed) {
47
+ setOptions([]);
48
+ setLoading(false);
49
+ fetchOptions.cancel();
50
+ return;
51
+ }
52
+ setLoading(true);
53
+ fetchOptions(fieldName, validationQuery, trimmed);
54
+ return () => fetchOptions.cancel();
55
+ }, [fieldName, validationQuery, searchText, fetchOptions]);
56
+
57
+ return { options, loading };
58
+ }
@@ -0,0 +1,74 @@
1
+ import type {
2
+ CheckUniqueIdentifierQuery,
3
+ GetSourceEndpointsQuery,
4
+ } from "@evenicanpm/admin-core/api/e4/graphqlRequestSdk";
5
+ import { templateInputSearch } from "@evenicanpm/admin-integrate/api/templates/queries";
6
+ import type { TemplateInputSearchQueryKey } from "@evenicanpm/admin-integrate/api/templates/queries/types";
7
+ import { useSearchParams } from "next/navigation";
8
+ import React from "react";
9
+ import type { SearchOption } from "../../types";
10
+ import { ReservedFieldName } from "../field-names";
11
+ import { InputType } from "../input-type";
12
+ import { TemplateSearchQueryKey } from "./search-query-key";
13
+
14
+ type UseSearchInputProps = {
15
+ inputType: InputType;
16
+ queryName: TemplateInputSearchQueryKey;
17
+ setValue?: (v: any, fieldName?: string) => void;
18
+ };
19
+
20
+ export function useSearchInput({
21
+ inputType,
22
+ queryName,
23
+ setValue,
24
+ }: UseSearchInputProps) {
25
+ const searchParams = useSearchParams();
26
+
27
+ const [searchText, setSearchText] = React.useState("");
28
+ const [options, setOptions] = React.useState<SearchOption[]>([]);
29
+ const [isUnique, setIsUnique] = React.useState<boolean | null>(null);
30
+
31
+ const { data } = templateInputSearch.useData(
32
+ inputType === InputType.TEXT_UNIQUE_IDENTIFIER
33
+ ? { uniqueIdentifier: searchText }
34
+ : {
35
+ input: {
36
+ processTemplateId: searchParams.get("template")?.toString() ?? null,
37
+ searchText,
38
+ },
39
+ },
40
+ queryName,
41
+ );
42
+
43
+ React.useEffect(() => {
44
+ if (!data) return;
45
+
46
+ switch (queryName) {
47
+ case TemplateSearchQueryKey.GetSourceEndpoints: {
48
+ const typed = data as GetSourceEndpointsQuery;
49
+ setOptions(
50
+ typed.getSourceEndpoints?.map((e) => ({
51
+ name: e?.name ?? "",
52
+ templateConfig: e?.templateConfig ?? "",
53
+ })) ?? [],
54
+ );
55
+ break;
56
+ }
57
+
58
+ case TemplateSearchQueryKey.CheckUniqueIdentifier: {
59
+ const typed = data as CheckUniqueIdentifierQuery;
60
+ const unique = typed.checkUniqueIdentifier?.isUnique ?? null;
61
+ setIsUnique(unique);
62
+ setValue?.(unique, ReservedFieldName.IsUnique);
63
+ break;
64
+ }
65
+ }
66
+ }, [data]);
67
+
68
+ return {
69
+ searchText,
70
+ setSearchText,
71
+ options,
72
+ isUnique,
73
+ };
74
+ }
@@ -0,0 +1,259 @@
1
+ "use client";
2
+
3
+ import AddIcon from "@mui/icons-material/Add";
4
+ import DeleteIcon from "@mui/icons-material/Delete";
5
+ import {
6
+ Box,
7
+ Button,
8
+ Card,
9
+ CardContent,
10
+ Checkbox,
11
+ FormHelperText,
12
+ IconButton,
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableRow,
18
+ TextField,
19
+ Typography,
20
+ } from "@mui/material";
21
+ import * as React from "react";
22
+ import type { ProcessTemplateInputSchema } from "../../types";
23
+ import { sortBy } from "../../types";
24
+ import { InputType } from "../input-type";
25
+ import { SelectorInput } from "../primitives/SelectorInput";
26
+ import { renderReadOnlyText } from "../utils/renderReadOnlyText";
27
+
28
+ /** Reserved key for React list key; not a schema column. */
29
+ const ROW_ID_KEY = "__rowId" as const;
30
+
31
+ function getRowId(row: Record<string, any>, index: number): string {
32
+ const id = row[ROW_ID_KEY];
33
+ if (typeof id === "string" && id.length > 0) return id;
34
+ return `row-${index}`;
35
+ }
36
+
37
+ function ensureRowIds(
38
+ rows: Array<Record<string, any>>,
39
+ setRows: (rows: Array<Record<string, any>>) => void,
40
+ ): void {
41
+ const needsId = rows.some(
42
+ (r) => r[ROW_ID_KEY] == null || r[ROW_ID_KEY] === "",
43
+ );
44
+ if (!needsId) return;
45
+ setRows(
46
+ rows.map((r) =>
47
+ r[ROW_ID_KEY] != null && r[ROW_ID_KEY] !== ""
48
+ ? r
49
+ : { ...r, [ROW_ID_KEY]: crypto.randomUUID() },
50
+ ),
51
+ );
52
+ }
53
+
54
+ export function InputTable({
55
+ rows,
56
+ setRows,
57
+ schemas,
58
+ tableError,
59
+ rowErrors,
60
+ readOnly,
61
+ onCellBlur,
62
+ }: Readonly<{
63
+ rows: Array<Record<string, any>>;
64
+ setRows: (rows: Array<Record<string, any>>) => void;
65
+ schemas: ProcessTemplateInputSchema[];
66
+ tableError: string | null;
67
+ rowErrors?: Array<Record<string, string>>;
68
+ readOnly?: boolean;
69
+ /** Called when user focuses out of a cell so errors show on blur */
70
+ onCellBlur?: () => void;
71
+ }>) {
72
+ React.useEffect(() => {
73
+ if (!rows?.length) return;
74
+ ensureRowIds(rows, setRows);
75
+ }, [rows, setRows]);
76
+
77
+ const ordered = React.useMemo(
78
+ () => sortBy<ProcessTemplateInputSchema>(schemas),
79
+ [schemas],
80
+ );
81
+
82
+ const hasRowWithMissingRequired = React.useMemo(() => {
83
+ const rowList = rows ?? [];
84
+ if (rowList.length === 0) return false;
85
+ return rowList.some((row) => {
86
+ return ordered.some((col, colIndex) => {
87
+ if (!col.isRequired) return false;
88
+ const key = col.fieldKey ?? col.label ?? `col_${colIndex}`;
89
+ const cell = row[key];
90
+ if (col.inputType === InputType.CHECKBOX) return !cell;
91
+ const strVal = cell == null ? "" : String(cell).trim();
92
+ return strVal.length === 0;
93
+ });
94
+ });
95
+ }, [rows, ordered]);
96
+
97
+ const addRow = () => {
98
+ const empty: Record<string, any> = {
99
+ [ROW_ID_KEY]: crypto.randomUUID(),
100
+ };
101
+ ordered.forEach((s, colIndex) => {
102
+ const key = s.fieldKey ?? s.label ?? `col_${colIndex}`;
103
+ empty[key] =
104
+ s.inputType === InputType.CHECKBOX ? false : (s.defaultValue ?? "");
105
+ });
106
+ setRows([...(rows ?? []), empty]);
107
+ };
108
+
109
+ const delRow = (idx: number) => {
110
+ const copy = [...rows];
111
+ copy.splice(idx, 1);
112
+ setRows(copy);
113
+ };
114
+
115
+ const updateCell = (idx: number, columnKey: string, value: any) => {
116
+ const copy = [...rows];
117
+ copy[idx] = { ...copy[idx], [columnKey]: value };
118
+ setRows(copy);
119
+ };
120
+
121
+ return (
122
+ <>
123
+ {readOnly && rows.length === 0 && (
124
+ <Box>
125
+ <Typography>No values</Typography>
126
+ </Box>
127
+ )}
128
+ {(!readOnly || (readOnly && rows.length > 0)) && (
129
+ <Card
130
+ variant="outlined"
131
+ sx={{ mb: 1, borderColor: tableError ? "error.main" : "grey.400" }}
132
+ >
133
+ <CardContent>
134
+ <Table size="small">
135
+ <TableHead>
136
+ <TableRow>
137
+ {ordered.map((s: ProcessTemplateInputSchema) => (
138
+ <TableCell key={s.id}>{s.label}</TableCell>
139
+ ))}
140
+ <TableCell width={1} />
141
+ </TableRow>
142
+ </TableHead>
143
+ <TableBody>
144
+ {(rows ?? []).map((r: Record<string, any>, i: number) => (
145
+ <TableRow key={getRowId(r, i)}>
146
+ {ordered.map(
147
+ (s: ProcessTemplateInputSchema, colIndex: number) => {
148
+ const columnKey =
149
+ s.fieldKey ?? s.label ?? `col_${colIndex}`;
150
+ const cell = r[columnKey];
151
+ const cellError = rowErrors?.[i]?.[columnKey] ?? null;
152
+ if (s.inputType === InputType.CHECKBOX) {
153
+ return (
154
+ <TableCell key={s.id}>
155
+ <Checkbox
156
+ checked={Boolean(cell)}
157
+ onChange={(e) =>
158
+ updateCell(i, columnKey, e.target.checked)
159
+ }
160
+ onBlur={onCellBlur}
161
+ disabled={readOnly}
162
+ />
163
+ <FormHelperText
164
+ error
165
+ sx={{ mt: 0, minWidth: "80px" }}
166
+ >
167
+ {cellError || " "}
168
+ </FormHelperText>
169
+ </TableCell>
170
+ );
171
+ }
172
+ if (s.inputType === InputType.SELECTOR) {
173
+ const opts = (s.options ?? [])
174
+ .filter(
175
+ (option): option is NonNullable<typeof option> =>
176
+ option != null,
177
+ )
178
+ .map((option) => ({
179
+ label: option.label ?? "",
180
+ value: option.optionValue ?? option.label ?? "",
181
+ }))
182
+ .filter((o) => o.value !== "" || o.label !== "");
183
+
184
+ return (
185
+ <TableCell key={s.id}>
186
+ <SelectorInput
187
+ insideTable={true}
188
+ minWidth={120}
189
+ fieldName={columnKey}
190
+ label={s.label ?? ""}
191
+ value={cell}
192
+ options={opts}
193
+ setValue={(v) => updateCell(i, columnKey, v)}
194
+ error={cellError}
195
+ readOnly={readOnly}
196
+ onBlur={onCellBlur}
197
+ />
198
+ </TableCell>
199
+ );
200
+ }
201
+ return (
202
+ <TableCell key={s.id}>
203
+ {readOnly ? (
204
+ renderReadOnlyText(undefined, cell)
205
+ ) : (
206
+ <TextField
207
+ size="small"
208
+ value={cell}
209
+ onChange={(e) =>
210
+ updateCell(i, columnKey, e.target.value)
211
+ }
212
+ onBlur={onCellBlur}
213
+ fullWidth
214
+ disabled={readOnly}
215
+ error={Boolean(cellError)}
216
+ helperText={cellError ?? " "}
217
+ />
218
+ )}
219
+ </TableCell>
220
+ );
221
+ },
222
+ )}
223
+ <TableCell>
224
+ {!readOnly && (
225
+ <IconButton
226
+ onClick={() => delRow(i)}
227
+ size="small"
228
+ disabled={readOnly}
229
+ sx={{ marginTop: "-1rem" }}
230
+ >
231
+ <DeleteIcon fontSize="small" />
232
+ </IconButton>
233
+ )}
234
+ </TableCell>
235
+ </TableRow>
236
+ ))}
237
+ {!readOnly && (
238
+ <TableRow>
239
+ <TableCell colSpan={ordered.length + 1}>
240
+ <Button
241
+ onClick={addRow}
242
+ startIcon={<AddIcon />}
243
+ variant="outlined"
244
+ size="small"
245
+ disabled={readOnly || hasRowWithMissingRequired}
246
+ >
247
+ Add row
248
+ </Button>
249
+ </TableCell>
250
+ </TableRow>
251
+ )}
252
+ </TableBody>
253
+ </Table>
254
+ </CardContent>
255
+ </Card>
256
+ )}
257
+ </>
258
+ );
259
+ }
@@ -0,0 +1 @@
1
+ export { InputTable } from "./InputTable";
@@ -0,0 +1,28 @@
1
+ import type { FieldValue, ProcessConfigMerge, SearchOption } from "../../types";
2
+ import { ReservedFieldName } from "../field-names";
3
+
4
+ export function applyTemplateConfig(
5
+ selectedOption: SearchOption,
6
+ handleFieldChange: (
7
+ templateId: string,
8
+ fieldName: string,
9
+ value: FieldValue,
10
+ ) => void,
11
+ ): void {
12
+ const merge: ProcessConfigMerge = { input: { config: [] } };
13
+ try {
14
+ merge.input = { config: [JSON.parse(selectedOption.templateConfig)] };
15
+
16
+ if (merge.input.config.length > 0) {
17
+ const config = merge.input.config[0];
18
+ const processTemplateId = config[ReservedFieldName.ProcessTemplateId];
19
+ for (const key of Object.keys(config)) {
20
+ handleFieldChange(processTemplateId as string, key, config[key]);
21
+ }
22
+ }
23
+ } catch {
24
+ console.error(
25
+ `Failed to apply template config for ${selectedOption.name}. Skipping...`,
26
+ );
27
+ }
28
+ }
@@ -0,0 +1,51 @@
1
+ import type { ProcessTemplateInput } from "../../types";
2
+ import { sortBy } from "../../types";
3
+ import { InputType, isHiddenInputType } from "../input-type";
4
+
5
+ export type InputRow =
6
+ | { type: "pair"; items: [ProcessTemplateInput, ProcessTemplateInput] }
7
+ | { type: "full"; item: ProcessTemplateInput };
8
+
9
+ export function arrangeInputs(inputs: ProcessTemplateInput[]): InputRow[] {
10
+ const ordered = sortBy(inputs);
11
+
12
+ const isTableOrHidden = (inp: ProcessTemplateInput) =>
13
+ inp.inputType === InputType.INPUT_TABLE || isHiddenInputType(inp.inputType);
14
+
15
+ const isPairable = (inp?: ProcessTemplateInput) =>
16
+ !!inp && !isTableOrHidden(inp);
17
+
18
+ const areConsecutiveSorts = (
19
+ a?: ProcessTemplateInput,
20
+ b?: ProcessTemplateInput,
21
+ ) =>
22
+ !!a &&
23
+ !!b &&
24
+ typeof a.sort === "number" &&
25
+ typeof b.sort === "number" &&
26
+ b.sort === a.sort + 1;
27
+
28
+ const canPairAtAll = ordered.filter(isPairable).length >= 2;
29
+
30
+ const rows: InputRow[] = [];
31
+ for (let i = 0; i < ordered.length; ) {
32
+ const curr = ordered[i];
33
+
34
+ if (!isPairable(curr) || !canPairAtAll) {
35
+ rows.push({ type: "full", item: curr });
36
+ i += 1;
37
+ continue;
38
+ }
39
+
40
+ const next = ordered[i + 1];
41
+ if (isPairable(next) && areConsecutiveSorts(curr, next)) {
42
+ rows.push({ type: "pair", items: [curr, next!] });
43
+ i += 2;
44
+ } else {
45
+ rows.push({ type: "full", item: curr });
46
+ i += 1;
47
+ }
48
+ }
49
+
50
+ return rows;
51
+ }
@@ -0,0 +1,6 @@
1
+ export { applyTemplateConfig } from "./applyTemplateConfig";
2
+ export { arrangeInputs, type InputRow } from "./arrangeInputs";
3
+ export { parseQueryName } from "./parseQueryName";
4
+ export { renderReadOnlyText } from "./renderReadOnlyText";
5
+ export { getSelectorOptionsFromInput, getTableRowErrors } from "./selectors";
6
+ export { toStringArray } from "./toStringArray";
@@ -0,0 +1,20 @@
1
+ import {
2
+ isTemplateInputSearchQueryKey,
3
+ type TemplateInputSearchQueryKey,
4
+ } from "@evenicanpm/admin-integrate/api/templates/queries/types";
5
+ import { TEMPLATE_QUERY_NAME_TO_KEY } from "../search/search-query-key";
6
+
7
+ export function parseQueryName(queryName: string): TemplateInputSearchQueryKey {
8
+ const indexOf = queryName.indexOf("(");
9
+ const qn = queryName.substring(0, indexOf < 0 ? undefined : indexOf).trim();
10
+
11
+ if (isTemplateInputSearchQueryKey(qn)) {
12
+ return qn as TemplateInputSearchQueryKey;
13
+ }
14
+ const mapped = qn ? TEMPLATE_QUERY_NAME_TO_KEY[qn] : undefined;
15
+ if (mapped !== undefined) {
16
+ return mapped as TemplateInputSearchQueryKey;
17
+ }
18
+
19
+ throw new Error(`queryName ${queryName} invalid`);
20
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { Box, Typography } from "@mui/material";
4
+ import type * as React from "react";
5
+
6
+ export function renderReadOnlyText(
7
+ label?: string,
8
+ value?: string | string[] | number | null,
9
+ ): React.ReactElement {
10
+ return (
11
+ <>
12
+ {value != null && value !== "" && (
13
+ <Box>
14
+ {label != null && label !== "" && (
15
+ <Typography color="grey" variant="caption">
16
+ {label}
17
+ </Typography>
18
+ )}
19
+ <Typography color="inherit" variant="body1">
20
+ {Array.isArray(value) ? value.join(", ") : String(value)}
21
+ </Typography>
22
+ </Box>
23
+ )}
24
+ </>
25
+ );
26
+ }
@@ -0,0 +1,69 @@
1
+ import type {
2
+ ErrorState,
3
+ ProcessTemplateInput,
4
+ ProcessTemplateInputSchema,
5
+ } from "../../types";
6
+ import { InputType } from "../input-type";
7
+ import type { SelectorOption } from "../primitives/SelectorInput";
8
+ import { toStringArray } from "./toStringArray";
9
+
10
+ /**
11
+ * Returns selector options from the input's schema. Supports both legacy string[]
12
+ * and API-backed options { label, optionValue }[].
13
+ */
14
+ export function getSelectorOptionsFromInput(
15
+ input: ProcessTemplateInput,
16
+ ): SelectorOption[] {
17
+ const schemaWithOptions =
18
+ input?.schemas?.find(
19
+ (s: ProcessTemplateInputSchema | null) =>
20
+ s?.inputType === InputType.SELECTOR,
21
+ ) ?? input?.schemas?.[0];
22
+
23
+ const raw = schemaWithOptions?.options;
24
+ if (raw == null) return [];
25
+ if (!Array.isArray(raw)) return toStringArray(raw) as string[];
26
+
27
+ const result: SelectorOption[] = [];
28
+ for (const item of raw) {
29
+ if (item == null) continue;
30
+ if (typeof item === "string") {
31
+ result.push(item);
32
+ continue;
33
+ }
34
+ const label = (item as { label?: string | null }).label ?? "";
35
+ const value =
36
+ (item as { optionValue?: string | null }).optionValue ??
37
+ (item as { label?: string | null }).label ??
38
+ "";
39
+ if (value !== "" || label !== "") {
40
+ result.push({ label, value });
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+
46
+ /**
47
+ * Parse Formik errors for an input-table field into per-row, per-column errors.
48
+ */
49
+ export function getTableRowErrors(
50
+ errors: ErrorState,
51
+ tableKey: string,
52
+ ): Array<Record<string, string>> {
53
+ const rowErrors: Array<Record<string, string>> = [];
54
+ const prefix = `${tableKey}.`;
55
+ for (const key of Object.keys(errors)) {
56
+ const msg = errors[key];
57
+ if (msg == null || typeof msg !== "string" || !key.startsWith(prefix))
58
+ continue;
59
+ const rest = key.slice(prefix.length);
60
+ const match = /^(\d+)\.(.+)$/.exec(rest);
61
+ if (match) {
62
+ const rowIndex = Number.parseInt(match[1], 10);
63
+ const colKey = match[2];
64
+ if (!rowErrors[rowIndex]) rowErrors[rowIndex] = {};
65
+ rowErrors[rowIndex][colKey] = msg;
66
+ }
67
+ }
68
+ return rowErrors;
69
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Coerce unknown value to string[] for selector/options use.
3
+ */
4
+ export function toStringArray(value: unknown): string[] {
5
+ if (value == null) return [];
6
+
7
+ if (Array.isArray(value)) return value.map(String);
8
+
9
+ if (typeof value === "string") {
10
+ try {
11
+ const parsed = JSON.parse(value);
12
+ return Array.isArray(parsed) ? parsed.map(String) : [value];
13
+ } catch {
14
+ return [value];
15
+ }
16
+ }
17
+
18
+ if (typeof value === "object") {
19
+ return Object.values(value as Record<string, unknown>).map(String);
20
+ }
21
+
22
+ return [];
23
+ }