@evenicanpm/admin-integrate 1.8.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/documents/ConfigImport/configuration-import.ts +1 -1
- package/documents/Endpoint/list.ts +3 -3
- package/documents/Process/list.ts +3 -3
- package/documents/ProcessExecution/list.ts +2 -2
- package/documents/ProcessExport/read.ts +1 -1
- package/documents/ProcessSchedule/read.ts +1 -1
- package/documents/ProcessScheduleGroup/read.ts +3 -3
- package/documents/ProcessTask/fragments.ts +4 -0
- package/documents/ProcessTemplate/fragments.ts +30 -0
- package/documents/ProcessTemplate/list.ts +29 -0
- package/documents/ProcessTemplate/read.ts +1 -1
- package/documents/ScheduledProcess/list.ts +1 -1
- 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/animated-svg.tsx +0 -1
- package/src/components/integration-view/elk-types.ts +1 -1
- package/src/components/integration-view/integration-view.tsx +274 -187
- package/src/components/integration-view/nodes/execution-entry-node.tsx +3 -2
- package/src/components/integration-view/nodes/task-node.tsx +1 -1
- package/src/components/integration-view/types.ts +2 -0
- package/src/components/integration-view/utils/mapping.ts +68 -48
- package/src/components/list-filter/list-filter-types.ts +6 -0
- package/src/components/list-filter/list-filter.tsx +21 -15
- package/src/components/list-filter/multi-select-filter.tsx +7 -7
- package/src/components/list-filter/select-filter.tsx +6 -6
- package/src/components/scheduler/day-selector.tsx +1 -1
- 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 +54 -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 +384 -0
- package/src/components/templatesV2/inputs/renderers/renderSearchInput.tsx +128 -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 +79 -0
- package/src/components/templatesV2/inputs/table/InputTable.tsx +268 -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 +52 -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 +153 -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 +212 -0
- package/src/components/templatesV2/validation/zodFormikValidate.ts +17 -0
- package/src/components/templatesV2/wizard.tsx +746 -0
- package/src/pages/dashboard/dashboard-link-cards.tsx +0 -1
- package/src/pages/dashboard/dashboard-list-sections.tsx +1 -1
- package/src/pages/dashboard/dashboard.tsx +0 -1
- package/src/pages/executions/executions-list/executions-list-table.tsx +0 -1
- package/src/pages/executions/executions.tsx +1 -1
- package/src/pages/integration-create/integration-create-steps.ts +8 -0
- package/src/pages/integration-create/integration-create.tsx +81 -28
- package/src/pages/integration-create/pre-creation-step.tsx +9 -9
- 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/import-process/copy-tasks-list.tsx +1 -3
- package/src/pages/integration-details/integration-details.tsx +1 -1
- package/src/pages/integration-details/task-drawer/add-task.tsx +4 -4
- package/src/pages/integration-details/task-drawer/edit-task.tsx +5 -5
- package/src/pages/integration-details/task-drawer/task-drawer.tsx +2 -2
- 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,268 @@
|
|
|
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
|
+
type RowRecord = Record<string, string | number | boolean | null | undefined>;
|
|
32
|
+
|
|
33
|
+
function getRowId(row: RowRecord, index: number): string {
|
|
34
|
+
const id = row[ROW_ID_KEY];
|
|
35
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
36
|
+
return `row-${index}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureRowIds(
|
|
40
|
+
rows: RowRecord[],
|
|
41
|
+
setRows: (rows: RowRecord[]) => void,
|
|
42
|
+
): void {
|
|
43
|
+
const needsId = rows.some(
|
|
44
|
+
(r) => r[ROW_ID_KEY] == null || r[ROW_ID_KEY] === "",
|
|
45
|
+
);
|
|
46
|
+
if (!needsId) return;
|
|
47
|
+
setRows(
|
|
48
|
+
rows.map((r) =>
|
|
49
|
+
r[ROW_ID_KEY] != null && r[ROW_ID_KEY] !== ""
|
|
50
|
+
? r
|
|
51
|
+
: { ...r, [ROW_ID_KEY]: crypto.randomUUID() },
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function InputTable({
|
|
57
|
+
rows,
|
|
58
|
+
setRows,
|
|
59
|
+
schemas,
|
|
60
|
+
tableError,
|
|
61
|
+
rowErrors,
|
|
62
|
+
readOnly,
|
|
63
|
+
onCellBlur,
|
|
64
|
+
}: Readonly<{
|
|
65
|
+
rows: RowRecord[];
|
|
66
|
+
setRows: (rows: RowRecord[]) => void;
|
|
67
|
+
schemas: ProcessTemplateInputSchema[];
|
|
68
|
+
tableError: string | null;
|
|
69
|
+
rowErrors?: Array<Record<string, string>>;
|
|
70
|
+
readOnly?: boolean;
|
|
71
|
+
/** Called when user focuses out of a cell so errors show on blur */
|
|
72
|
+
onCellBlur?: () => void;
|
|
73
|
+
}>) {
|
|
74
|
+
React.useEffect(() => {
|
|
75
|
+
if (!rows?.length) return;
|
|
76
|
+
ensureRowIds(rows, setRows);
|
|
77
|
+
}, [rows, setRows]);
|
|
78
|
+
|
|
79
|
+
const ordered = React.useMemo(
|
|
80
|
+
() => sortBy<ProcessTemplateInputSchema>(schemas),
|
|
81
|
+
[schemas],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const hasRowWithMissingRequired = React.useMemo(() => {
|
|
85
|
+
const rowList = rows ?? [];
|
|
86
|
+
if (rowList.length === 0) return false;
|
|
87
|
+
return rowList.some((row) => {
|
|
88
|
+
return ordered.some((col, colIndex) => {
|
|
89
|
+
if (!col.isRequired) return false;
|
|
90
|
+
const key = col.fieldKey ?? col.label ?? `col_${colIndex}`;
|
|
91
|
+
const cell = row[key];
|
|
92
|
+
if (col.inputType === InputType.CHECKBOX) return !cell;
|
|
93
|
+
const strVal = cell == null ? "" : String(cell).trim();
|
|
94
|
+
return strVal.length === 0;
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}, [rows, ordered]);
|
|
98
|
+
|
|
99
|
+
const addRow = () => {
|
|
100
|
+
const empty: RowRecord = {
|
|
101
|
+
[ROW_ID_KEY]: crypto.randomUUID(),
|
|
102
|
+
};
|
|
103
|
+
ordered.forEach((s, colIndex) => {
|
|
104
|
+
const key = s.fieldKey ?? s.label ?? `col_${colIndex}`;
|
|
105
|
+
empty[key] =
|
|
106
|
+
s.inputType === InputType.CHECKBOX ? false : (s.defaultValue ?? "");
|
|
107
|
+
});
|
|
108
|
+
setRows([...(rows ?? []), empty]);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const delRow = (idx: number) => {
|
|
112
|
+
const copy = [...rows];
|
|
113
|
+
copy.splice(idx, 1);
|
|
114
|
+
setRows(copy);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const updateCell = (
|
|
118
|
+
idx: number,
|
|
119
|
+
columnKey: string,
|
|
120
|
+
value: string | number | boolean | null | undefined,
|
|
121
|
+
) => {
|
|
122
|
+
const copy = [...rows];
|
|
123
|
+
copy[idx] = { ...copy[idx], [columnKey]: value };
|
|
124
|
+
setRows(copy);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<>
|
|
129
|
+
{readOnly && rows.length === 0 && (
|
|
130
|
+
<Box>
|
|
131
|
+
<Typography>No values</Typography>
|
|
132
|
+
</Box>
|
|
133
|
+
)}
|
|
134
|
+
{(!readOnly || (readOnly && rows.length > 0)) && (
|
|
135
|
+
<Card
|
|
136
|
+
variant="outlined"
|
|
137
|
+
sx={{ mb: 1, borderColor: tableError ? "error.main" : "grey.400" }}
|
|
138
|
+
>
|
|
139
|
+
<CardContent>
|
|
140
|
+
<Table size="small">
|
|
141
|
+
<TableHead>
|
|
142
|
+
<TableRow>
|
|
143
|
+
{ordered.map((s: ProcessTemplateInputSchema) => (
|
|
144
|
+
<TableCell key={s.id}>{s.label}</TableCell>
|
|
145
|
+
))}
|
|
146
|
+
<TableCell width={1} />
|
|
147
|
+
</TableRow>
|
|
148
|
+
</TableHead>
|
|
149
|
+
<TableBody>
|
|
150
|
+
{(rows ?? []).map((r: RowRecord, i: number) => (
|
|
151
|
+
<TableRow key={getRowId(r, i)}>
|
|
152
|
+
{ordered.map(
|
|
153
|
+
(s: ProcessTemplateInputSchema, colIndex: number) => {
|
|
154
|
+
const columnKey =
|
|
155
|
+
s.fieldKey ?? s.label ?? `col_${colIndex}`;
|
|
156
|
+
const cell = r[columnKey];
|
|
157
|
+
const cellError = rowErrors?.[i]?.[columnKey] ?? null;
|
|
158
|
+
if (s.inputType === InputType.CHECKBOX) {
|
|
159
|
+
return (
|
|
160
|
+
<TableCell key={s.id}>
|
|
161
|
+
<Checkbox
|
|
162
|
+
checked={Boolean(cell)}
|
|
163
|
+
onChange={(e) =>
|
|
164
|
+
updateCell(i, columnKey, e.target.checked)
|
|
165
|
+
}
|
|
166
|
+
onBlur={onCellBlur}
|
|
167
|
+
disabled={readOnly}
|
|
168
|
+
/>
|
|
169
|
+
<FormHelperText
|
|
170
|
+
error
|
|
171
|
+
sx={{ mt: 0, minWidth: "80px" }}
|
|
172
|
+
>
|
|
173
|
+
{cellError || " "}
|
|
174
|
+
</FormHelperText>
|
|
175
|
+
</TableCell>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (s.inputType === InputType.SELECTOR) {
|
|
179
|
+
const opts = (s.options ?? [])
|
|
180
|
+
.filter(
|
|
181
|
+
(option): option is NonNullable<typeof option> =>
|
|
182
|
+
option != null,
|
|
183
|
+
)
|
|
184
|
+
.map((option) => ({
|
|
185
|
+
label: option.label ?? "",
|
|
186
|
+
value: option.optionValue ?? option.label ?? "",
|
|
187
|
+
}))
|
|
188
|
+
.filter((o) => o.value !== "" || o.label !== "");
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<TableCell key={s.id}>
|
|
192
|
+
<SelectorInput
|
|
193
|
+
insideTable={true}
|
|
194
|
+
minWidth={120}
|
|
195
|
+
fieldName={columnKey}
|
|
196
|
+
label={s.label ?? ""}
|
|
197
|
+
value={cell == null ? "" : String(cell)}
|
|
198
|
+
options={opts}
|
|
199
|
+
setValue={(v) => updateCell(i, columnKey, v)}
|
|
200
|
+
error={cellError}
|
|
201
|
+
readOnly={readOnly}
|
|
202
|
+
onBlur={onCellBlur}
|
|
203
|
+
/>
|
|
204
|
+
</TableCell>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return (
|
|
208
|
+
<TableCell key={s.id}>
|
|
209
|
+
{readOnly ? (
|
|
210
|
+
renderReadOnlyText(
|
|
211
|
+
undefined,
|
|
212
|
+
typeof cell === "boolean" ? String(cell) : cell,
|
|
213
|
+
)
|
|
214
|
+
) : (
|
|
215
|
+
<TextField
|
|
216
|
+
size="small"
|
|
217
|
+
value={cell == null ? "" : String(cell)}
|
|
218
|
+
onChange={(e) =>
|
|
219
|
+
updateCell(i, columnKey, e.target.value)
|
|
220
|
+
}
|
|
221
|
+
onBlur={onCellBlur}
|
|
222
|
+
fullWidth
|
|
223
|
+
disabled={readOnly}
|
|
224
|
+
error={Boolean(cellError)}
|
|
225
|
+
helperText={cellError ?? " "}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
</TableCell>
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
)}
|
|
232
|
+
<TableCell>
|
|
233
|
+
{!readOnly && (
|
|
234
|
+
<IconButton
|
|
235
|
+
onClick={() => delRow(i)}
|
|
236
|
+
size="small"
|
|
237
|
+
disabled={readOnly}
|
|
238
|
+
sx={{ marginTop: "-1rem" }}
|
|
239
|
+
>
|
|
240
|
+
<DeleteIcon fontSize="small" />
|
|
241
|
+
</IconButton>
|
|
242
|
+
)}
|
|
243
|
+
</TableCell>
|
|
244
|
+
</TableRow>
|
|
245
|
+
))}
|
|
246
|
+
{!readOnly && (
|
|
247
|
+
<TableRow>
|
|
248
|
+
<TableCell colSpan={ordered.length + 1}>
|
|
249
|
+
<Button
|
|
250
|
+
onClick={addRow}
|
|
251
|
+
startIcon={<AddIcon />}
|
|
252
|
+
variant="outlined"
|
|
253
|
+
size="small"
|
|
254
|
+
disabled={readOnly || hasRowWithMissingRequired}
|
|
255
|
+
>
|
|
256
|
+
Add row
|
|
257
|
+
</Button>
|
|
258
|
+
</TableCell>
|
|
259
|
+
</TableRow>
|
|
260
|
+
)}
|
|
261
|
+
</TableBody>
|
|
262
|
+
</Table>
|
|
263
|
+
</CardContent>
|
|
264
|
+
</Card>
|
|
265
|
+
)}
|
|
266
|
+
</>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -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,52 @@
|
|
|
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 = (
|
|
16
|
+
inp: ProcessTemplateInput | undefined,
|
|
17
|
+
): inp is ProcessTemplateInput => !!inp && !isTableOrHidden(inp);
|
|
18
|
+
|
|
19
|
+
const areConsecutiveSorts = (
|
|
20
|
+
a?: ProcessTemplateInput,
|
|
21
|
+
b?: ProcessTemplateInput,
|
|
22
|
+
) =>
|
|
23
|
+
!!a &&
|
|
24
|
+
!!b &&
|
|
25
|
+
typeof a.sort === "number" &&
|
|
26
|
+
typeof b.sort === "number" &&
|
|
27
|
+
b.sort === a.sort + 1;
|
|
28
|
+
|
|
29
|
+
const canPairAtAll = ordered.filter(isPairable).length >= 2;
|
|
30
|
+
|
|
31
|
+
const rows: InputRow[] = [];
|
|
32
|
+
for (let i = 0; i < ordered.length; ) {
|
|
33
|
+
const curr = ordered[i];
|
|
34
|
+
|
|
35
|
+
if (!isPairable(curr) || !canPairAtAll) {
|
|
36
|
+
rows.push({ type: "full", item: curr });
|
|
37
|
+
i += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const next = ordered[i + 1];
|
|
42
|
+
if (isPairable(next) && areConsecutiveSorts(curr, next)) {
|
|
43
|
+
rows.push({ type: "pair", items: [curr, next] });
|
|
44
|
+
i += 2;
|
|
45
|
+
} else {
|
|
46
|
+
rows.push({ type: "full", item: curr });
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return rows;
|
|
52
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Formik, type FormikProps } 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<FormikProps<FormState> | null>(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) => {
|
|
77
|
+
touched[k] = true;
|
|
78
|
+
});
|
|
79
|
+
formikHelpers.setTouched(touched);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
await formikHelpers.submitForm();
|
|
83
|
+
return true;
|
|
84
|
+
}, [pageKeys]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Formik
|
|
88
|
+
key={stepKey}
|
|
89
|
+
initialValues={initialValues}
|
|
90
|
+
initialTouched={initialTouched}
|
|
91
|
+
enableReinitialize
|
|
92
|
+
validateOnMount={false}
|
|
93
|
+
validateOnBlur={true}
|
|
94
|
+
validateOnChange={false}
|
|
95
|
+
validate={zodFormikValidate(zSchema)}
|
|
96
|
+
onSubmit={(values) => {
|
|
97
|
+
// Only pass the keys for this page back to the parent.
|
|
98
|
+
const pageKeys = (page.inputs ?? []).map(
|
|
99
|
+
(inp) => `${templateId}::${inp?.fieldName ?? ""}`,
|
|
100
|
+
);
|
|
101
|
+
const filtered: FormState = {};
|
|
102
|
+
for (const k of pageKeys) {
|
|
103
|
+
if (
|
|
104
|
+
typeof values === "object" &&
|
|
105
|
+
values !== null &&
|
|
106
|
+
Object.hasOwn(values as object, k)
|
|
107
|
+
) {
|
|
108
|
+
filtered[k] = (values as FormState)[k];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
onPageSubmit(filtered);
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{(formik) => {
|
|
115
|
+
formikRef.current = formik;
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (setSubmitRef) setSubmitRef(submit);
|
|
118
|
+
return () => {
|
|
119
|
+
if (setSubmitRef) setSubmitRef(undefined);
|
|
120
|
+
};
|
|
121
|
+
}, [submit, setSubmitRef]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (setPageValidity) setPageValidity(true);
|
|
125
|
+
}, [stepKey, setPageValidity]);
|
|
126
|
+
|
|
127
|
+
// Keep Next button enabled so user can always click it. Validation runs on Next click (submit).
|
|
128
|
+
// We do not disable Next based on errors; errors show after they click Next or touch a field.
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (setPageValidity) setPageValidity(true);
|
|
131
|
+
}, [setPageValidity]);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<form onSubmit={formik.handleSubmit}>
|
|
135
|
+
<FormikPageBody
|
|
136
|
+
templateId={templateId}
|
|
137
|
+
page={page}
|
|
138
|
+
clearTemplateFields={() => clearTemplateFields(templateId)}
|
|
139
|
+
readOnly={readOnly}
|
|
140
|
+
isEditMode={isEditMode}
|
|
141
|
+
isCreateMode={isCreateMode}
|
|
142
|
+
disabledFields={disabledFields}
|
|
143
|
+
setFieldsDisabled={setFieldsDisabled}
|
|
144
|
+
templateConfig={templateConfig}
|
|
145
|
+
setFieldValidationInProgress={setFieldValidationInProgress}
|
|
146
|
+
submitValidationResults={submitValidationResults}
|
|
147
|
+
/>
|
|
148
|
+
</form>
|
|
149
|
+
);
|
|
150
|
+
}}
|
|
151
|
+
</Formik>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -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}
|