@evenicanpm/admin-integrate 1.2.2 → 1.4.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 (215) hide show
  1. package/documents/ConfigImport/configuration-import.ts +54 -0
  2. package/documents/CopyProcess/copy.ts +25 -0
  3. package/documents/Endpoint/fragments.ts +9 -0
  4. package/documents/Endpoint/list.ts +30 -0
  5. package/documents/Process/fragments.ts +22 -0
  6. package/documents/Process/get-process-details.ts +72 -0
  7. package/documents/Process/list.ts +24 -0
  8. package/documents/ProcessConfig/fragments.ts +116 -0
  9. package/documents/ProcessConfig/read.ts +35 -0
  10. package/documents/ProcessExecution/fragments.ts +12 -0
  11. package/documents/ProcessExecution/list.ts +30 -1
  12. package/documents/ProcessExecutionDetails/fragments.ts +96 -0
  13. package/documents/ProcessExecutionDetails/read.ts +12 -0
  14. package/documents/ProcessExport/read.ts +108 -0
  15. package/documents/ProcessScheduleGroup/read.ts +43 -0
  16. package/documents/ProcessTask/fragments.ts +288 -0
  17. package/documents/ProcessTask/read.ts +12 -0
  18. package/documents/ProcessTemplate/fragments.ts +74 -0
  19. package/documents/ProcessTemplate/list.ts +67 -0
  20. package/documents/ProcessTemplate/read.ts +12 -0
  21. package/documents/RunProcess/run.ts +11 -0
  22. package/package.json +4 -3
  23. package/src/api/Integrations/queries/get-process.query.ts +12 -12
  24. package/src/api/Integrations/queries/get-process.server.ts +7 -2
  25. package/src/api/configuration/queries/configuration-export.query.ts +48 -0
  26. package/src/api/configuration/queries/configuration-export.server.ts +9 -0
  27. package/src/api/configuration/queries/index.ts +1 -0
  28. package/src/api/dashboard/queries/get-process-executions.query.ts +6 -3
  29. package/src/api/dashboard/queries/get-process-executions.server.ts +1 -1
  30. package/src/api/execution-details/queries/get-process-execution-details.query.ts +46 -0
  31. package/src/api/execution-details/queries/get-process-execution-details.server.ts +11 -0
  32. package/src/api/execution-details/queries/get-process-executions-by-process.query.ts +48 -0
  33. package/src/api/execution-details/queries/get-process-executions-by-process.server.ts +11 -0
  34. package/src/api/execution-details/queries/index.ts +2 -0
  35. package/src/api/process/mutations/copy-process.mutation.ts +45 -0
  36. package/src/api/process/mutations/copy-process.server.ts +14 -0
  37. package/src/api/process/mutations/index.ts +2 -0
  38. package/src/api/process/mutations/run-process.mutation.ts +41 -0
  39. package/src/api/process/mutations/run-process.server.ts +14 -0
  40. package/src/api/process/queries/get-process-by-id.query.ts +85 -0
  41. package/src/api/process/queries/get-process-by-id.server.ts +20 -0
  42. package/src/api/process/queries/index.ts +4 -0
  43. package/src/api/process-config/mutation/index.ts +2 -0
  44. package/src/api/process-config/mutation/process-config-import.mutation.ts +42 -0
  45. package/src/api/process-config/mutation/process-config-import.server.ts +14 -0
  46. package/src/api/process-config/mutation/process-config-merge.mutation.ts +33 -0
  47. package/src/api/process-config/mutation/process-config-merge.server.ts +14 -0
  48. package/src/api/process-task/queries/get-process-task-by-id.query.ts +46 -0
  49. package/src/api/process-task/queries/get-process-task-by-id.server.ts +9 -0
  50. package/src/api/process-task/queries/index.ts +1 -0
  51. package/src/api/scheduler/mutation/create-process-schedule-group-details.mutation.ts +47 -0
  52. package/src/api/scheduler/mutation/create-process-schedule-group-details.server.ts +14 -0
  53. package/src/api/scheduler/mutation/index.ts +2 -0
  54. package/src/api/scheduler/mutation/update-process-schedule-group-details.mutation.ts +47 -0
  55. package/src/api/scheduler/mutation/update-process-schedule-group-details.server.ts +14 -0
  56. package/src/api/scheduler/queries/get-non-scheduled-processes.query.ts +48 -0
  57. package/src/api/scheduler/queries/get-non-scheduled-processes.server.ts +11 -0
  58. package/src/api/scheduler/queries/get-process-schedule-group-details.query.ts +6 -3
  59. package/src/api/scheduler/queries/get-process-schedule-group-details.server.ts +3 -3
  60. package/src/api/scheduler/queries/get-process-schedule.query.ts +6 -3
  61. package/src/api/scheduler/queries/get-process-schedule.server.ts +3 -3
  62. package/src/api/scheduler/queries/get-scheduled-processes.query.ts +6 -3
  63. package/src/api/scheduler/queries/get-scheduled-processes.server.ts +1 -1
  64. package/src/api/scheduler/queries/index.ts +3 -2
  65. package/src/api/templates/queries/get-process-template-groups.query.ts +48 -0
  66. package/src/api/templates/queries/get-process-template-groups.server.ts +11 -0
  67. package/src/api/templates/queries/get-process-template-records.query.ts +48 -0
  68. package/src/api/templates/queries/get-process-template-records.server.ts +11 -0
  69. package/src/api/templates/queries/get-process-template.query.ts +48 -0
  70. package/src/api/templates/queries/get-process-template.server.ts +9 -0
  71. package/src/api/templates/queries/get-process-templates.query.ts +45 -0
  72. package/src/api/templates/queries/get-process-templates.server.ts +9 -0
  73. package/src/api/templates/queries/index.ts +5 -0
  74. package/src/api/templates/queries/template-input-search.query.ts +39 -0
  75. package/src/api/templates/queries/template-input-search.server.ts +42 -0
  76. package/src/api/templates/queries/types.ts +34 -0
  77. package/src/components/breadcrumbs/breadcrumbs.tsx +33 -6
  78. package/src/components/button/outlined-icon-button.tsx +2 -2
  79. package/src/components/core/date-time-popover.tsx +12 -5
  80. package/src/components/core/drawer-buttons.tsx +52 -0
  81. package/src/components/core/edited-typography.tsx +2 -2
  82. package/src/components/core/index.ts +4 -2
  83. package/src/components/core/loading.tsx +1 -1
  84. package/src/components/core/status-tag.tsx +97 -0
  85. package/src/components/core/title-edit.tsx +31 -22
  86. package/src/components/dashboard-list/dashboard-list-row.tsx +22 -29
  87. package/src/components/dashboard-list/dashboard-list.tsx +20 -16
  88. package/src/components/data-table/table-header.tsx +6 -8
  89. package/src/components/data-table/table-pagination.tsx +1 -1
  90. package/src/components/data-table/table-row.tsx +1 -1
  91. package/src/components/data-table/table-skeleton.tsx +10 -5
  92. package/src/components/execution/execution-filter.ts +1 -1
  93. package/src/components/execution/execution-status-icon.tsx +7 -7
  94. package/src/components/execution/execution-tag.tsx +12 -77
  95. package/src/components/execution/index.ts +1 -1
  96. package/src/components/execution-history-drawer/execution-history-drawer.tsx +202 -0
  97. package/src/components/execution-history-drawer/executions-card.tsx +85 -0
  98. package/src/components/execution-history-drawer/index.ts +1 -0
  99. package/src/components/footer/footer.tsx +10 -6
  100. package/src/components/header/dashboard-list-header.tsx +12 -7
  101. package/src/components/header/execution-details-header.tsx +35 -26
  102. package/src/components/header/header.tsx +10 -3
  103. package/src/components/header/index.ts +1 -1
  104. package/src/components/header/integration-details-header.tsx +108 -45
  105. package/src/components/integration-list/index.ts +2 -1
  106. package/src/components/integration-list/integration-filter.ts +1 -1
  107. package/src/components/integration-list/integration-status-icon.tsx +7 -8
  108. package/src/components/integration-list/integration-tag.tsx +6 -50
  109. package/src/components/integration-list/list/integration-list-row.tsx +161 -0
  110. package/src/components/integration-list/list/integration-list-table.tsx +62 -0
  111. package/src/{pages/integrations/integration-list → components/integration-list/list}/integration-list.tsx +47 -51
  112. package/src/components/integration-view/animated-svg.tsx +31 -0
  113. package/src/components/integration-view/edges/default-edge.tsx +2 -6
  114. package/src/components/integration-view/elk-layout-options.ts +2 -2
  115. package/src/components/integration-view/elk-types.ts +35 -6
  116. package/src/components/integration-view/flow-types.tsx +12 -0
  117. package/src/components/integration-view/integration-view.tsx +562 -56
  118. package/src/components/integration-view/nodes/add-node.tsx +20 -0
  119. package/src/components/integration-view/nodes/empty-node.tsx +65 -0
  120. package/src/components/integration-view/nodes/entry-node.tsx +98 -4
  121. package/src/components/integration-view/nodes/execution-entry-node.tsx +107 -0
  122. package/src/components/integration-view/nodes/execution-task-node.tsx +88 -0
  123. package/src/components/integration-view/nodes/group-node.tsx +33 -13
  124. package/src/components/integration-view/nodes/row-node.tsx +19 -0
  125. package/src/components/integration-view/nodes/task-node.tsx +50 -16
  126. package/src/components/integration-view/task-icon.tsx +193 -0
  127. package/src/components/integration-view/temp-data/initialElements.tsx +34 -32
  128. package/src/components/integration-view/types.ts +2 -2
  129. package/src/components/integration-view/utils/mapping.ts +404 -0
  130. package/src/components/layouts/main-layout.tsx +3 -4
  131. package/src/components/layouts/root-container.tsx +5 -11
  132. package/src/components/link-cards/link-card.tsx +15 -4
  133. package/src/components/link-cards/styles.ts +1 -1
  134. package/src/components/list-filter/date-filter.tsx +7 -4
  135. package/src/components/list-filter/date-range-filter.tsx +15 -7
  136. package/src/components/list-filter/index.ts +1 -1
  137. package/src/components/list-filter/list-filter.tsx +31 -26
  138. package/src/components/list-filter/multi-select-filter.tsx +11 -10
  139. package/src/components/list-filter/select-filter.tsx +23 -12
  140. package/src/components/scheduler/day-selector.tsx +29 -15
  141. package/src/components/scheduler/day-tag.tsx +14 -51
  142. package/src/components/scheduler/index.ts +2 -1
  143. package/src/components/scheduler/schedule-drawer/details-reducer.ts +217 -0
  144. package/src/components/scheduler/schedule-drawer/schedule-details-integrations.tsx +501 -0
  145. package/src/{pages → components}/scheduler/schedule-drawer/schedule-details-main.tsx +137 -100
  146. package/src/components/scheduler/schedule-drawer/schedule-details-name.tsx +94 -0
  147. package/src/{pages → components}/scheduler/schedule-drawer/schedule-details-skeleton.tsx +1 -2
  148. package/src/{pages → components}/scheduler/schedule-drawer/schedule-details.tsx +10 -14
  149. package/src/components/scheduler/schedule-drawer/schedule-drawer-group.tsx +214 -0
  150. package/src/{pages → components}/scheduler/schedule-drawer/schedule-drawer-legacy.tsx +7 -26
  151. package/src/components/scheduler/schedule-drawer/schedule-drawer.tsx +116 -0
  152. package/src/components/scheduler/schedule-drawer/sortable-integration-row.tsx +130 -0
  153. package/src/components/scheduler/schedule-filter.ts +1 -1
  154. package/src/components/scheduler/schedule-icon.tsx +5 -6
  155. package/src/components/scheduler/schedule-integration-icon.tsx +31 -0
  156. package/src/components/templates/index.ts +1 -0
  157. package/src/components/templates/inputs/index.tsx +1392 -0
  158. package/src/components/templates/template-filter.ts +5 -0
  159. package/src/components/templates/templates-list/index.ts +1 -0
  160. package/src/components/templates/templates-list/templates-list-row.tsx +55 -0
  161. package/src/components/templates/templates-list/templates-list-table.tsx +58 -0
  162. package/src/components/templates/templates-list/templates-list.tsx +253 -0
  163. package/src/components/templates/types.tsx +76 -0
  164. package/src/components/templates/wizard.tsx +737 -0
  165. package/src/hooks/use-breadcrumbs.ts +1 -1
  166. package/src/hooks/use-recurrence.tsx +1 -2
  167. package/src/pages/dashboard/dashboard-link-cards.tsx +8 -11
  168. package/src/pages/dashboard/dashboard-list-sections.tsx +8 -10
  169. package/src/pages/dashboard/dashboard.tsx +1 -2
  170. package/src/pages/execution-details/execution-details.tsx +212 -14
  171. package/src/pages/execution-details/task-execution-details.tsx +160 -0
  172. package/src/pages/execution-details/task-execution-drawer.tsx +54 -0
  173. package/src/pages/executions/executions-list/executions-list-row.tsx +21 -27
  174. package/src/pages/executions/executions-list/executions-list-table.tsx +29 -36
  175. package/src/pages/executions/executions-list/executions-list.tsx +24 -18
  176. package/src/pages/executions/executions.tsx +11 -15
  177. package/src/pages/integration-create/index.ts +1 -0
  178. package/src/pages/integration-create/integration-create.tsx +216 -0
  179. package/src/pages/integration-create/pre-creation-step.tsx +84 -0
  180. package/src/pages/integration-create/select-step.tsx +105 -0
  181. package/src/pages/integration-create/template-list-step.tsx +77 -0
  182. package/src/pages/integration-create/wizard-step.tsx +51 -0
  183. package/src/pages/integration-details/import-process/copy-tasks-list.tsx +265 -0
  184. package/src/pages/integration-details/import-process/copy-tasks-row.tsx +77 -0
  185. package/src/pages/integration-details/import-process/import-confirm.tsx +61 -0
  186. package/src/pages/integration-details/import-process/import-drawer.tsx +48 -0
  187. package/src/pages/integration-details/import-process/import-process-reducer.ts +90 -0
  188. package/src/pages/integration-details/import-process/import-process.tsx +149 -0
  189. package/src/pages/integration-details/index.ts +1 -0
  190. package/src/pages/integration-details/integration-details.tsx +468 -0
  191. package/src/pages/integration-details/task-drawer/add-task.tsx +172 -0
  192. package/src/pages/integration-details/task-drawer/edit-task.tsx +161 -0
  193. package/src/pages/integration-details/task-drawer/task-drawer.tsx +157 -0
  194. package/src/pages/integration-details/task-drawer/task-reducer.ts +103 -0
  195. package/src/pages/integrations/integrations.tsx +11 -14
  196. package/src/pages/scheduler/scheduler.tsx +12 -24
  197. package/src/pages/scheduler/schedules-list/schedules-list-row.tsx +27 -37
  198. package/src/pages/scheduler/schedules-list/schedules-list-table.tsx +28 -43
  199. package/src/pages/scheduler/schedules-list/schedules-list.tsx +48 -21
  200. package/src/utils/date-functions.ts +69 -0
  201. package/src/utils/slugify.ts +3 -0
  202. package/tsconfig.json +12 -4
  203. package/src/hooks/use-timestamp.tsx +0 -25
  204. package/src/pages/integrations/integration-list/integration-list-row.tsx +0 -147
  205. package/src/pages/integrations/integration-list/integration-list-table.tsx +0 -78
  206. package/src/pages/scheduler/schedule-drawer/schedule-details-integrations.tsx +0 -227
  207. package/src/pages/scheduler/schedule-drawer/schedule-details-name.tsx +0 -77
  208. package/src/pages/scheduler/schedule-drawer/schedule-drawer-buttons.tsx +0 -72
  209. package/src/pages/scheduler/schedule-drawer/schedule-drawer-group.tsx +0 -103
  210. package/src/pages/scheduler/schedule-drawer/schedule-drawer.tsx +0 -101
  211. package/src/pages/scheduler/schedule-drawer/sortable-integration-row.tsx +0 -93
  212. /package/src/{pages/integrations/integration-list → components/integration-list/list}/index.ts +0 -0
  213. /package/src/{pages → components}/scheduler/schedule-drawer/edited/edited-types.ts +0 -0
  214. /package/src/{pages → components}/scheduler/schedule-drawer/edited/index.ts +0 -0
  215. /package/src/{pages → components}/scheduler/schedule-drawer/index.ts +0 -0
@@ -0,0 +1,1392 @@
1
+ "use client";
2
+
3
+ import type {
4
+ CheckUniqueIdentifierQuery,
5
+ GetSourceEndpointsQuery,
6
+ } from "@evenicanpm/admin-core/api/e4/graphqlRequestSdk";
7
+ import { templateInputSearch } from "@evenicanpm/admin-integrate/api/templates/queries";
8
+ import {
9
+ isTemplateInputSearchQueryKey,
10
+ type TemplateInputSearchQueryKey,
11
+ } from "@evenicanpm/admin-integrate/api/templates/queries/types";
12
+ import AddIcon from "@mui/icons-material/Add";
13
+ import DeleteIcon from "@mui/icons-material/Delete";
14
+ import {
15
+ Autocomplete,
16
+ type AutocompleteChangeReason,
17
+ Box,
18
+ Button,
19
+ Card,
20
+ CardContent,
21
+ Checkbox,
22
+ FormControlLabel,
23
+ FormHelperText,
24
+ IconButton,
25
+ Table,
26
+ TableBody,
27
+ TableCell,
28
+ TableHead,
29
+ TableRow,
30
+ TextField,
31
+ Typography,
32
+ } from "@mui/material";
33
+ import { debounce } from "lodash";
34
+ import { useSearchParams } from "next/navigation";
35
+ import * as React from "react";
36
+ import {
37
+ type ErrorState,
38
+ type FieldValue,
39
+ type FormState,
40
+ type ProcessConfigMerge,
41
+ type ProcessTemplateInput,
42
+ type ProcessTemplateInputSchema,
43
+ type ProcessTemplatePage,
44
+ type SearchOption,
45
+ sortBy,
46
+ templateFieldKey,
47
+ } from "../types";
48
+
49
+ type InputRow =
50
+ | { type: "pair"; items: [ProcessTemplateInput, ProcessTemplateInput] }
51
+ | { type: "full"; item: ProcessTemplateInput };
52
+
53
+ export function TextInput({
54
+ label,
55
+ placeholder,
56
+ value,
57
+ setValue,
58
+ error,
59
+ helpText,
60
+ readOnly,
61
+ }: Readonly<{
62
+ label: string;
63
+ placeholder?: string | null;
64
+ value: string;
65
+ setValue: (v: string) => void;
66
+ error?: string | null;
67
+ helpText?: string | null;
68
+ readOnly?: boolean;
69
+ }>) {
70
+ if (readOnly) return renderReadOnlyText(label, value);
71
+
72
+ return (
73
+ <TextField
74
+ label={label}
75
+ value={value ?? ""}
76
+ onChange={(e) => setValue(e.target.value)}
77
+ placeholder={placeholder ?? undefined}
78
+ fullWidth
79
+ error={Boolean(error)}
80
+ helperText={error ?? helpText ?? ""}
81
+ disabled={readOnly}
82
+ />
83
+ );
84
+ }
85
+
86
+ export function NumberInput({
87
+ label,
88
+ placeholder,
89
+ value,
90
+ setValue,
91
+ error,
92
+ helpText,
93
+ readOnly,
94
+ }: Readonly<{
95
+ label: string;
96
+ placeholder?: string | null;
97
+ value: number | "" | null;
98
+ setValue: (v: number | "") => void;
99
+ error?: string | null;
100
+ helpText?: string | null;
101
+ readOnly?: boolean;
102
+ }>) {
103
+ if (readOnly) return renderReadOnlyText(label, value);
104
+
105
+ return (
106
+ <TextField
107
+ type="number"
108
+ label={label}
109
+ value={value ?? ""}
110
+ onChange={(e) =>
111
+ setValue(e.target.value === "" ? "" : Number(e.target.value))
112
+ }
113
+ placeholder={placeholder ?? undefined}
114
+ fullWidth
115
+ error={Boolean(error)}
116
+ helperText={error ?? helpText ?? ""}
117
+ disabled={readOnly}
118
+ />
119
+ );
120
+ }
121
+
122
+ export function CheckboxInput({
123
+ label,
124
+ checked,
125
+ setChecked,
126
+ helpText,
127
+ readOnly,
128
+ }: Readonly<{
129
+ label: string;
130
+ checked: boolean;
131
+ setChecked: (v: boolean) => void;
132
+ helpText?: string | null;
133
+ readOnly?: boolean;
134
+ }>) {
135
+ return (
136
+ <Box>
137
+ <FormControlLabel
138
+ control={
139
+ <Checkbox
140
+ checked={checked}
141
+ onChange={(e) => setChecked(e.target.checked)}
142
+ />
143
+ }
144
+ label={label}
145
+ disabled={readOnly}
146
+ />
147
+ {helpText ? (
148
+ <FormHelperText sx={{ mt: -1.5, mb: 1 }}>{helpText}</FormHelperText>
149
+ ) : null}
150
+ </Box>
151
+ );
152
+ }
153
+
154
+ function toStringArray(value: unknown): string[] {
155
+ if (value == null) return [];
156
+
157
+ // Already an array: coerce each item to string
158
+ if (Array.isArray(value)) return value.map(String);
159
+
160
+ // String: try JSON-parse; if it isn't a JSON array, treat as a single option
161
+ if (typeof value === "string") {
162
+ try {
163
+ const parsed = JSON.parse(value);
164
+ return Array.isArray(parsed) ? parsed.map(String) : [value];
165
+ } catch {
166
+ return [value];
167
+ }
168
+ }
169
+
170
+ // Plain object: use its values as the option labels/values
171
+ if (typeof value === "object") {
172
+ return Object.values(value as Record<string, unknown>).map(String);
173
+ }
174
+
175
+ // Everything else → no options
176
+ return [];
177
+ }
178
+
179
+ function getSelectorOptionsFromInput(input: ProcessTemplateInput): string[] {
180
+ const schemaWithOptions =
181
+ input?.schemas?.find(
182
+ (s: ProcessTemplateInputSchema | null) => s?.inputType === "selector",
183
+ ) ?? input?.schemas?.[0];
184
+
185
+ return toStringArray(schemaWithOptions?.options);
186
+ }
187
+
188
+ export function SelectorInput({
189
+ fieldName,
190
+ label,
191
+ value,
192
+ setValue,
193
+ helpText,
194
+ error,
195
+ options,
196
+ insideTable = false,
197
+ minWidth,
198
+ readOnly,
199
+ }: Readonly<{
200
+ fieldName: string;
201
+ label: string;
202
+ value: string;
203
+ setValue: (v: string | null) => void;
204
+ helpText?: string | null;
205
+ error?: string | null;
206
+ options?: string[];
207
+ insideTable?: boolean;
208
+ minWidth?: number;
209
+ readOnly?: boolean;
210
+ }>) {
211
+ const opts = options ?? [];
212
+ const allowFreeText = opts.length === 0;
213
+ const id = `selector-${fieldName}`;
214
+
215
+ if (readOnly)
216
+ return renderReadOnlyText(insideTable ? undefined : label, value);
217
+
218
+ return (
219
+ <Box
220
+ sx={{
221
+ minWidth: minWidth ?? (insideTable ? 220 : undefined),
222
+ width: "100%",
223
+ }}
224
+ >
225
+ <Autocomplete
226
+ id={id}
227
+ disablePortal={insideTable}
228
+ disableClearable
229
+ options={opts}
230
+ freeSolo={allowFreeText}
231
+ value={value ?? ""}
232
+ onChange={(_e, val) => setValue((val as string | null) ?? "")}
233
+ onInputChange={(_e, val) => setValue(val)}
234
+ disabled={readOnly}
235
+ renderInput={(params) => (
236
+ <TextField
237
+ {...params}
238
+ size={insideTable ? "small" : "medium"}
239
+ label={label}
240
+ name={fieldName}
241
+ error={Boolean(error)}
242
+ helperText={error ?? helpText ?? ""}
243
+ fullWidth
244
+ disabled={readOnly}
245
+ />
246
+ )}
247
+ />
248
+ </Box>
249
+ );
250
+ }
251
+
252
+ function SearchInputWrapper({
253
+ fieldName,
254
+ inputType,
255
+ label,
256
+ value,
257
+ setValue,
258
+ placeholder,
259
+ helpText,
260
+ error,
261
+ queryName,
262
+ handleFieldChange,
263
+ clearTemplateFields,
264
+ readOnly,
265
+ isEditMode,
266
+ isCreateMode,
267
+ setFieldsDisabled,
268
+ templateId,
269
+ disabledFields,
270
+ templateConfig,
271
+ }: Readonly<{
272
+ fieldName?: string;
273
+ inputType: "multi-text-search" | "text-search" | "text-unique-identifier";
274
+ label: string;
275
+ value: string | string[] | null;
276
+ setValue:
277
+ | ((v: string | string[] | boolean | null, fieldName?: string) => void)
278
+ | ((v: string | string[] | boolean | null) => void);
279
+ placeholder?: string | null;
280
+ helpText?: string | null;
281
+ error?: string | null;
282
+ queryName: TemplateInputSearchQueryKey;
283
+ handleFieldChange: (
284
+ templateId: string,
285
+ fieldName: string,
286
+ value: FieldValue,
287
+ ) => void;
288
+ clearTemplateFields?: (templateId: string) => void;
289
+ readOnly?: boolean;
290
+ sharedCountCallBack?: (count: number) => void;
291
+ isEditMode?: boolean;
292
+ isCreateMode?: boolean;
293
+ setFieldsDisabled?: (
294
+ templateId: string,
295
+ fieldNames: string[],
296
+ disabled: boolean,
297
+ ) => void;
298
+ templateId?: string;
299
+ disabledFields?: Set<string>;
300
+ templateConfig?: string;
301
+ }>) {
302
+ const searchParams = useSearchParams();
303
+
304
+ const [searchText, setSearchText] = React.useState<string>("");
305
+ const [options, setOptions] = React.useState<SearchOption[]>([]);
306
+
307
+ const { data } = templateInputSearch.useData(
308
+ inputType === "text-unique-identifier"
309
+ ? {
310
+ uniqueIdentifier: searchText,
311
+ }
312
+ : {
313
+ input: {
314
+ processTemplateId: searchParams.get("template")?.toString() ?? null,
315
+ searchText: searchText,
316
+ },
317
+ },
318
+ queryName,
319
+ );
320
+
321
+ React.useEffect(() => {
322
+ if (data) {
323
+ switch (queryName) {
324
+ case "getSourceEndpoints": {
325
+ const typedData = data as GetSourceEndpointsQuery;
326
+ setOptions(
327
+ typedData.getSourceEndpoints?.map((e) => ({
328
+ name: e?.name ?? "",
329
+ templateConfig: e?.templateConfig ?? "",
330
+ })) ?? [],
331
+ );
332
+ break;
333
+ }
334
+ case "checkUniqueIdentifier": {
335
+ const typedData = data as CheckUniqueIdentifierQuery;
336
+ setValue(
337
+ (typedData.checkUniqueIdentifier?.isUnique ?? null) as
338
+ | string
339
+ | string[]
340
+ | boolean
341
+ | null,
342
+ "isUnique",
343
+ );
344
+ break;
345
+ }
346
+ }
347
+ }
348
+ }, [data]);
349
+
350
+ if (readOnly) {
351
+ return renderReadOnlyText(label, value);
352
+ }
353
+
354
+ return (
355
+ <>
356
+ {inputType === "multi-text-search" && (
357
+ <Box sx={{ display: "flex", gap: "10px" }}>
358
+ <MultiTextSearchInput
359
+ label={label}
360
+ value={value as string[]}
361
+ setValue={setValue as (v: string[]) => void}
362
+ options={options}
363
+ placeholder={placeholder}
364
+ helpText={helpText}
365
+ error={error}
366
+ setSearchText={setSearchText}
367
+ readOnly={readOnly}
368
+ />
369
+ </Box>
370
+ )}
371
+
372
+ {inputType === "text-search" && (
373
+ <Box sx={{ display: "flex", gap: "10px" }}>
374
+ <TextSearchInput
375
+ fieldName={fieldName}
376
+ label={label}
377
+ value={value as string}
378
+ setValue={
379
+ setValue as (v: string | string[] | boolean | null) => void
380
+ }
381
+ options={options}
382
+ placeholder={placeholder}
383
+ helpText={helpText}
384
+ error={error}
385
+ setSearchText={setSearchText}
386
+ handleFieldChange={handleFieldChange}
387
+ clearTemplateFields={clearTemplateFields}
388
+ readOnly={readOnly}
389
+ setFieldsDisabled={setFieldsDisabled}
390
+ templateId={templateId}
391
+ disabledFields={disabledFields}
392
+ isEditMode={isEditMode}
393
+ templateConfig={templateConfig}
394
+ />
395
+ </Box>
396
+ )}
397
+
398
+ {inputType === "text-unique-identifier" && (
399
+ <Box sx={{ display: "flex", gap: "10px" }}>
400
+ <TextSearchInput
401
+ fieldName={fieldName}
402
+ label={label}
403
+ value={value as string}
404
+ setValue={
405
+ setValue as (v: string | string[] | boolean | null) => void
406
+ }
407
+ options={[]}
408
+ placeholder={placeholder}
409
+ helpText={helpText}
410
+ error={error}
411
+ setSearchText={setSearchText}
412
+ handleFieldChange={handleFieldChange}
413
+ clearTemplateFields={clearTemplateFields}
414
+ readOnly={isCreateMode === false || readOnly}
415
+ setFieldsDisabled={setFieldsDisabled}
416
+ templateId={templateId}
417
+ disabledFields={disabledFields}
418
+ isEditMode={isEditMode}
419
+ templateConfig={templateConfig}
420
+ />
421
+ </Box>
422
+ )}
423
+ </>
424
+ );
425
+ }
426
+
427
+ export function applyTemplateConfig(
428
+ selectedOption: SearchOption,
429
+ handleFieldChange: (
430
+ templateId: string,
431
+ fieldName: string,
432
+ value: FieldValue,
433
+ ) => void,
434
+ ) {
435
+ const merge: ProcessConfigMerge = { input: { config: [] } };
436
+ try {
437
+ merge.input = { config: [JSON.parse(selectedOption.templateConfig)] };
438
+
439
+ if (merge.input.config.length > 0) {
440
+ const config = merge.input.config[0];
441
+ const processTemplateId = config.processTemplateId;
442
+ for (const key of Object.keys(config)) {
443
+ handleFieldChange(processTemplateId as string, key, config[key]);
444
+ }
445
+ }
446
+ } catch {
447
+ console.error(
448
+ `Failed to apply template config for ${selectedOption.name}. Skipping...`,
449
+ );
450
+ }
451
+ }
452
+
453
+ export function TextSearchInput({
454
+ fieldName,
455
+ label,
456
+ value,
457
+ setValue,
458
+ options,
459
+ placeholder,
460
+ helpText,
461
+ error,
462
+ setSearchText,
463
+ handleFieldChange,
464
+ clearTemplateFields,
465
+ readOnly,
466
+ setFieldsDisabled,
467
+ templateId,
468
+ disabledFields,
469
+ isEditMode,
470
+ templateConfig,
471
+ }: Readonly<{
472
+ fieldName?: string;
473
+ label: string;
474
+ value: string;
475
+ setValue: (v: string | string[] | boolean | null) => void;
476
+ options: SearchOption[];
477
+ placeholder?: string | null;
478
+ helpText?: string | null;
479
+ error?: string | null;
480
+ setSearchText: React.Dispatch<React.SetStateAction<string>>;
481
+ handleFieldChange: (
482
+ templateId: string,
483
+ fieldName: string,
484
+ value: FieldValue,
485
+ ) => void;
486
+ clearTemplateFields?: (templateId: string) => void;
487
+ readOnly?: boolean;
488
+ setFieldsDisabled?: (
489
+ templateId: string,
490
+ fieldNames: string[],
491
+ disabled: boolean,
492
+ ) => void;
493
+ templateId?: string;
494
+ disabledFields?: Set<string>;
495
+ isEditMode?: boolean;
496
+ templateConfig?: string;
497
+ }>) {
498
+ const searchParams = useSearchParams();
499
+ const [selected, setSelected] = React.useState<string | null>(value ?? null);
500
+
501
+ // remember which option was actually applied so we can reliably re-enable later
502
+ const appliedOptionRef = React.useRef<string | null>(null);
503
+
504
+ // disabledFields + readOnly
505
+ const selfKey =
506
+ templateId && fieldName ? templateFieldKey(templateId, fieldName) : null;
507
+ const isSelfDisabled = selfKey
508
+ ? Boolean(disabledFields?.has(selfKey))
509
+ : false;
510
+ const disabled = Boolean(readOnly || isSelfDisabled);
511
+
512
+ const debouncedSetSearchText = React.useMemo(
513
+ () => debounce((s: string) => setSearchText(s), 500),
514
+ [],
515
+ );
516
+ React.useEffect(() => {
517
+ // Cleanup function to cancel any pending debounced calls
518
+ return () => {
519
+ debouncedSetSearchText.cancel();
520
+ };
521
+ }, [debouncedSetSearchText]);
522
+
523
+ React.useEffect(() => {
524
+ if (selected && templateId) {
525
+ const option = options.find((o) => o?.name === selected);
526
+ if (option) {
527
+ try {
528
+ const config = JSON.parse(option.templateConfig);
529
+
530
+ if (
531
+ config &&
532
+ templateConfig //&&
533
+ //config.dataDescription === inputConfig.dataDescription
534
+ ) {
535
+ option.templateConfig = templateConfig;
536
+ }
537
+
538
+ // Always apply config (both edit and non-edit modes)
539
+ applyTemplateConfig(option, handleFieldChange);
540
+ appliedOptionRef.current = option.name ?? null;
541
+
542
+ // Only disable fields when NOT in edit mode
543
+ if (setFieldsDisabled && !isEditMode) {
544
+ const config = JSON.parse(option.templateConfig);
545
+ const fieldNamesToDisable = Object.keys(config).filter(
546
+ (key) => key !== "processTemplateId" && key !== (fieldName ?? ""),
547
+ );
548
+ setFieldsDisabled(templateId, fieldNamesToDisable, true);
549
+ }
550
+ } catch (err) {
551
+ console.error("Failed to parse template config", err);
552
+ }
553
+ }
554
+ }
555
+ }, [
556
+ selected,
557
+ templateId,
558
+ options,
559
+ setFieldsDisabled,
560
+ handleFieldChange,
561
+ isEditMode,
562
+ ]);
563
+
564
+ const handleAutocompleteOnChange = (
565
+ event: React.SyntheticEvent,
566
+ value: string | null,
567
+ reason: AutocompleteChangeReason,
568
+ ) => {
569
+ if (reason === "clear" && event.type === "click") {
570
+ setSelected(null);
571
+ // Re-enable all fields when cleared
572
+ if (templateId && setFieldsDisabled) {
573
+ const appliedName = appliedOptionRef.current;
574
+ if (appliedName) {
575
+ const option = options.find((o) => o?.name === appliedName);
576
+ if (option) {
577
+ try {
578
+ const config = JSON.parse(option.templateConfig);
579
+ // exclude the search field itself when re-enabling (it was never disabled)
580
+ const fieldNamesToEnable = Object.keys(config).filter(
581
+ (key) =>
582
+ key !== "processTemplateId" && key !== (fieldName ?? ""),
583
+ );
584
+ setFieldsDisabled(templateId, fieldNamesToEnable, false);
585
+ } catch (err) {
586
+ console.error("Failed to parse template config", err);
587
+ } finally {
588
+ appliedOptionRef.current = null;
589
+ }
590
+ }
591
+ }
592
+ }
593
+ const templateIdFromParams = searchParams.get("template");
594
+ if (clearTemplateFields && templateIdFromParams) {
595
+ clearTemplateFields(templateIdFromParams);
596
+ }
597
+ } else {
598
+ setSelected(value);
599
+ }
600
+ };
601
+
602
+ const handleInputChange = (
603
+ event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
604
+ ) => {
605
+ const newVal = event.target.value;
606
+ setValue(newVal);
607
+ debouncedSetSearchText(newVal);
608
+
609
+ if (newVal.trim() === "" && templateId && setFieldsDisabled) {
610
+ const appliedName = appliedOptionRef.current;
611
+ if (appliedName) {
612
+ const option = options.find((o) => o?.name === appliedName);
613
+ if (option) {
614
+ try {
615
+ const config = JSON.parse(option.templateConfig);
616
+ const fieldNamesToEnable = Object.keys(config).filter(
617
+ (key) => key !== "processTemplateId" && key !== (fieldName ?? ""),
618
+ );
619
+ setFieldsDisabled(templateId, fieldNamesToEnable, false);
620
+ } catch (err) {
621
+ console.error(
622
+ "Failed to parse template config while enabling fields",
623
+ err,
624
+ );
625
+ } finally {
626
+ appliedOptionRef.current = null;
627
+ }
628
+ }
629
+ }
630
+
631
+ if (clearTemplateFields) {
632
+ clearTemplateFields(templateId);
633
+ }
634
+ }
635
+ };
636
+
637
+ const shouldShowInfoMessage = !isEditMode && selected;
638
+ const helperTextFinal = shouldShowInfoMessage
639
+ ? "Existing data descriptions are not editable during creation."
640
+ : helpText;
641
+
642
+ return (
643
+ <Box>
644
+ <Autocomplete
645
+ freeSolo={!selected}
646
+ options={options.map((o) => o.name)}
647
+ value={selected ?? ""}
648
+ onChange={handleAutocompleteOnChange}
649
+ disabled={disabled}
650
+ renderInput={(params) => (
651
+ <TextField
652
+ {...params}
653
+ label={label}
654
+ value={value ?? ""}
655
+ onChange={handleInputChange}
656
+ placeholder={placeholder ?? undefined}
657
+ fullWidth
658
+ error={Boolean(error)}
659
+ helperText={error ?? helperTextFinal ?? ""}
660
+ disabled={disabled}
661
+ />
662
+ )}
663
+ />
664
+ </Box>
665
+ );
666
+ }
667
+
668
+ export function MultiTextSearchInput({
669
+ label,
670
+ value,
671
+ setValue,
672
+ options,
673
+ placeholder,
674
+ helpText,
675
+ error,
676
+ setSearchText,
677
+ readOnly,
678
+ }: Readonly<{
679
+ label: string;
680
+ value: string[];
681
+ setValue: (v: string[]) => void;
682
+ options: SearchOption[];
683
+ placeholder?: string | null;
684
+ helpText?: string | null;
685
+ error?: string | null;
686
+ setSearchText: React.Dispatch<React.SetStateAction<string>>;
687
+ readOnly?: boolean;
688
+ }>) {
689
+ const [input, setInput] = React.useState<string>("");
690
+
691
+ const debouncedSetSearchText = React.useMemo(
692
+ () => debounce((s: string) => setSearchText(s), 500),
693
+ [],
694
+ );
695
+
696
+ React.useEffect(() => {
697
+ // Cleanup function to cancel any pending debounced calls
698
+ return () => {
699
+ debouncedSetSearchText.cancel();
700
+ };
701
+ }, [debouncedSetSearchText]);
702
+
703
+ const handleAutocompleteOnChange = (
704
+ _e: React.SyntheticEvent,
705
+ val: string[],
706
+ ) => {
707
+ setValue(val);
708
+ debouncedSetSearchText("");
709
+ };
710
+
711
+ const handleInputChange = (
712
+ event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
713
+ ) => {
714
+ setInput(event.target.value);
715
+ debouncedSetSearchText(event.target.value);
716
+ };
717
+
718
+ return (
719
+ <Autocomplete
720
+ multiple
721
+ freeSolo
722
+ options={options.map((o) => o.name)}
723
+ value={value ?? []}
724
+ onChange={handleAutocompleteOnChange}
725
+ disabled={readOnly}
726
+ renderInput={(params) => (
727
+ <TextField
728
+ {...params}
729
+ value={input}
730
+ onChange={handleInputChange}
731
+ label={label}
732
+ placeholder={placeholder ?? undefined}
733
+ error={Boolean(error)}
734
+ helperText={error ?? helpText ?? ""}
735
+ fullWidth
736
+ disabled={readOnly}
737
+ />
738
+ )}
739
+ />
740
+ );
741
+ }
742
+
743
+ export function InputTable({
744
+ rows,
745
+ setRows,
746
+ schemas,
747
+ tableError,
748
+ readOnly,
749
+ }: Readonly<{
750
+ rows: Array<Record<string, any>>;
751
+ setRows: (rows: Array<Record<string, any>>) => void;
752
+ schemas: ProcessTemplateInputSchema[];
753
+ tableError: string | null;
754
+ readOnly?: boolean;
755
+ }>) {
756
+ const ordered = React.useMemo(
757
+ () => sortBy<ProcessTemplateInputSchema>(schemas),
758
+ [schemas],
759
+ );
760
+ const addRow = () => {
761
+ const empty: Record<string, any> = {};
762
+ for (const s of ordered) {
763
+ const key = s.fieldKey ?? s.label ?? "";
764
+ empty[key] =
765
+ s.inputType === "checkbox" ? false : ((s.defaultValue as any) ?? "");
766
+ }
767
+ setRows([...(rows ?? []), empty]);
768
+ };
769
+
770
+ const delRow = (idx: number) => {
771
+ const copy = [...rows];
772
+ copy.splice(idx, 1);
773
+ setRows(copy);
774
+ };
775
+
776
+ const updateCell = (idx: number, columnKey: string, value: any) => {
777
+ const copy = [...rows];
778
+ copy[idx] = { ...copy[idx], [columnKey]: value };
779
+ setRows(copy);
780
+ };
781
+
782
+ return (
783
+ <>
784
+ {readOnly && rows.length === 0 && (
785
+ <Box>
786
+ <Typography>No values</Typography>
787
+ </Box>
788
+ )}
789
+ {(!readOnly || (readOnly && rows.length > 0)) && (
790
+ <Card
791
+ variant="outlined"
792
+ sx={{ mb: 1, borderColor: tableError ? "error.main" : "grey.400" }}
793
+ >
794
+ <CardContent>
795
+ <Table size="small">
796
+ <TableHead>
797
+ <TableRow>
798
+ {ordered.map((s: ProcessTemplateInputSchema) => (
799
+ <TableCell key={s.id}>{s.label}</TableCell>
800
+ ))}
801
+ <TableCell width={1} />
802
+ </TableRow>
803
+ </TableHead>
804
+ <TableBody>
805
+ {(rows ?? []).map((r: Record<string, any>, i: number) => (
806
+ <TableRow key={`table-row-${i + 1}`}>
807
+ {ordered.map((s: ProcessTemplateInputSchema) => {
808
+ const columnKey = s.fieldKey ?? s.label;
809
+ const cell = columnKey ? r[columnKey] : "";
810
+ if (s.inputType === "checkbox") {
811
+ return (
812
+ <TableCell key={s.id}>
813
+ <Checkbox
814
+ checked={Boolean(cell)}
815
+ onChange={(e) =>
816
+ updateCell(i, columnKey ?? "", e.target.checked)
817
+ }
818
+ disabled={readOnly}
819
+ />
820
+ </TableCell>
821
+ );
822
+ }
823
+ if (s.inputType === "selector") {
824
+ const columnKey = s.fieldKey ?? s.label;
825
+ const cell = columnKey ? r[columnKey] : "";
826
+ const opts = toStringArray(s.options);
827
+
828
+ return (
829
+ <TableCell key={s.id}>
830
+ <SelectorInput
831
+ insideTable={true}
832
+ minWidth={120}
833
+ fieldName={columnKey ?? ""}
834
+ label={s.label ?? ""}
835
+ value={cell}
836
+ options={opts}
837
+ setValue={(v) =>
838
+ updateCell(i, columnKey ?? "", v)
839
+ }
840
+ readOnly={readOnly}
841
+ />
842
+ </TableCell>
843
+ );
844
+ }
845
+ return (
846
+ <TableCell key={s.id}>
847
+ {readOnly ? (
848
+ renderReadOnlyText(undefined, cell)
849
+ ) : (
850
+ <TextField
851
+ size="small"
852
+ value={cell}
853
+ onChange={(e) =>
854
+ updateCell(i, columnKey ?? "", e.target.value)
855
+ }
856
+ fullWidth
857
+ disabled={readOnly}
858
+ />
859
+ )}
860
+ </TableCell>
861
+ );
862
+ })}
863
+ <TableCell>
864
+ {!readOnly && (
865
+ <IconButton
866
+ onClick={() => delRow(i)}
867
+ size="small"
868
+ disabled={readOnly}
869
+ >
870
+ <DeleteIcon fontSize="small" />
871
+ </IconButton>
872
+ )}
873
+ </TableCell>
874
+ </TableRow>
875
+ ))}
876
+ {!readOnly && (
877
+ <TableRow>
878
+ <TableCell colSpan={ordered.length + 1}>
879
+ <Button
880
+ onClick={addRow}
881
+ startIcon={<AddIcon />}
882
+ variant="outlined"
883
+ size="small"
884
+ disabled={readOnly}
885
+ >
886
+ Add row
887
+ </Button>
888
+ </TableCell>
889
+ </TableRow>
890
+ )}
891
+ </TableBody>
892
+ </Table>
893
+ </CardContent>
894
+ </Card>
895
+ )}
896
+ </>
897
+ );
898
+ }
899
+
900
+ function renderFormControl({
901
+ input,
902
+ templateId,
903
+ values,
904
+ errors,
905
+ handleFieldChange,
906
+ clearTemplateFields,
907
+ readOnly,
908
+ sharedCount,
909
+ isEditMode,
910
+ isCreateMode,
911
+ disabledFields,
912
+ setFieldsDisabled,
913
+ templateConfig,
914
+ }: Readonly<{
915
+ input: ProcessTemplateInput;
916
+ templateId: string;
917
+ values: FormState;
918
+ errors: ErrorState;
919
+ handleFieldChange: (
920
+ templateId: string,
921
+ fieldName: string,
922
+ value: FieldValue,
923
+ ) => void;
924
+ clearTemplateFields: (templateId: string) => void;
925
+ readOnly?: boolean;
926
+ sharedCount?: (count: number) => void;
927
+ isEditMode?: boolean;
928
+ isCreateMode?: boolean;
929
+ disabledFields?: Set<string>;
930
+ setFieldsDisabled?: (
931
+ templateId: string,
932
+ fieldNames: string[],
933
+ disabled: boolean,
934
+ ) => void;
935
+ templateConfig?: string;
936
+ }>) {
937
+ const k = templateFieldKey(templateId, input.fieldName ?? "");
938
+ const err = errors[k] ?? null;
939
+ const isFieldDisabled = disabledFields?.has(k) ?? false;
940
+ const finalReadOnly = readOnly || isFieldDisabled;
941
+
942
+ if (
943
+ input.inputType === "hidden" ||
944
+ input.inputType === "hidden-number" ||
945
+ input.inputType === "hidden-number-or-null"
946
+ ) {
947
+ return null;
948
+ }
949
+
950
+ if (input.inputType === "checkbox") {
951
+ return (
952
+ <CheckboxInput
953
+ label={input.label ?? ""}
954
+ checked={Boolean(values[k] ?? false)}
955
+ setChecked={(v) =>
956
+ handleFieldChange(templateId, input.fieldName ?? "", v)
957
+ }
958
+ helpText={input.helpText ?? ""}
959
+ readOnly={finalReadOnly}
960
+ />
961
+ );
962
+ }
963
+
964
+ if (input.inputType === "number") {
965
+ return (
966
+ <NumberInput
967
+ label={input.label ?? ""}
968
+ value={(values[k] as number) ?? ""}
969
+ setValue={(v) =>
970
+ handleFieldChange(templateId, input.fieldName ?? "", v as any)
971
+ }
972
+ placeholder={input.placeholder ?? ""}
973
+ helpText={input.helpText ?? ""}
974
+ error={err}
975
+ readOnly={finalReadOnly}
976
+ />
977
+ );
978
+ }
979
+
980
+ if (input.inputType === "selector") {
981
+ return (
982
+ <SelectorInput
983
+ fieldName={input.fieldName ?? ""}
984
+ label={input.label ?? ""}
985
+ value={(values[k] as string) ?? ""}
986
+ setValue={(v) =>
987
+ handleFieldChange(templateId, input.fieldName ?? "", v)
988
+ }
989
+ helpText={input.helpText ?? ""}
990
+ error={err}
991
+ options={getSelectorOptionsFromInput(input)}
992
+ readOnly={finalReadOnly}
993
+ />
994
+ );
995
+ }
996
+
997
+ if (input.inputType === "multi-text-search") {
998
+ const queryName = parseQueryName(input.queryName ?? "");
999
+ return (
1000
+ <SearchInputWrapper
1001
+ inputType="multi-text-search"
1002
+ queryName={queryName}
1003
+ label={input.label ?? ""}
1004
+ value={(values[k] as string[]) ?? []}
1005
+ setValue={(v: string | string[] | boolean | null) =>
1006
+ handleFieldChange(templateId, input.fieldName ?? "", v)
1007
+ }
1008
+ placeholder={input.placeholder ?? ""}
1009
+ helpText={input.helpText ?? ""}
1010
+ error={err}
1011
+ handleFieldChange={handleFieldChange}
1012
+ readOnly={readOnly}
1013
+ sharedCountCallBack={(count) => sharedCount?.(count)}
1014
+ isEditMode={isEditMode}
1015
+ setFieldsDisabled={setFieldsDisabled}
1016
+ templateId={templateId}
1017
+ disabledFields={disabledFields}
1018
+ fieldName={input.fieldName ?? ""}
1019
+ templateConfig={templateConfig}
1020
+ />
1021
+ );
1022
+ }
1023
+
1024
+ if (input.inputType === "text-search") {
1025
+ const queryName = parseQueryName(input.queryName ?? "");
1026
+ return (
1027
+ <SearchInputWrapper
1028
+ inputType="text-search"
1029
+ queryName={queryName}
1030
+ label={input.label ?? ""}
1031
+ value={(values[k] as string) ?? ""}
1032
+ setValue={(v: string | string[] | boolean | null) =>
1033
+ handleFieldChange(templateId, input.fieldName ?? "", v)
1034
+ }
1035
+ placeholder={input.placeholder ?? ""}
1036
+ helpText={input.helpText ?? ""}
1037
+ error={err}
1038
+ handleFieldChange={handleFieldChange}
1039
+ clearTemplateFields={clearTemplateFields}
1040
+ readOnly={readOnly}
1041
+ sharedCountCallBack={(count) => sharedCount?.(count)}
1042
+ isEditMode={isEditMode}
1043
+ setFieldsDisabled={setFieldsDisabled}
1044
+ templateId={templateId}
1045
+ disabledFields={disabledFields}
1046
+ fieldName={input.fieldName ?? ""}
1047
+ templateConfig={templateConfig}
1048
+ />
1049
+ );
1050
+ }
1051
+
1052
+ if (input.inputType === "text-unique-identifier") {
1053
+ const queryName = parseQueryName(input.queryName ?? "");
1054
+ return (
1055
+ <SearchInputWrapper
1056
+ inputType="text-unique-identifier"
1057
+ queryName={queryName}
1058
+ label={input.label ?? ""}
1059
+ value={(values[k] as string) ?? ""}
1060
+ setValue={(
1061
+ v: string | string[] | boolean | null,
1062
+ fieldName?: string,
1063
+ ) => {
1064
+ if (fieldName) {
1065
+ handleFieldChange(templateId, fieldName, v);
1066
+ } else {
1067
+ handleFieldChange(templateId, input.fieldName ?? "", v);
1068
+ }
1069
+ }}
1070
+ placeholder={input.placeholder ?? ""}
1071
+ helpText={input.helpText ?? ""}
1072
+ error={err}
1073
+ handleFieldChange={handleFieldChange}
1074
+ clearTemplateFields={clearTemplateFields}
1075
+ readOnly={readOnly}
1076
+ sharedCountCallBack={(count) => sharedCount?.(count)}
1077
+ isEditMode={isEditMode}
1078
+ isCreateMode={isCreateMode}
1079
+ setFieldsDisabled={setFieldsDisabled}
1080
+ templateId={templateId}
1081
+ disabledFields={disabledFields}
1082
+ fieldName={input.fieldName ?? ""}
1083
+ templateConfig={templateConfig}
1084
+ />
1085
+ );
1086
+ }
1087
+
1088
+ if (input.inputType === "input-table") {
1089
+ const value = values[k];
1090
+ const rows = (Array.isArray(value) ? value : []) as Array<
1091
+ Record<string, unknown>
1092
+ >;
1093
+
1094
+ // Specifically for "properties" key in values. Need to convert to array. Might need to consider a less specific solution
1095
+ if (value && rows.length === 0 && input.schemas?.length === 2) {
1096
+ for (const schemaKey of Object.keys(value)) {
1097
+ if (input.schemas[0]?.fieldKey && input.schemas[1]?.fieldKey) {
1098
+ const row: Record<string, unknown> = {};
1099
+ row[input.schemas[0].fieldKey] = schemaKey;
1100
+ row[input.schemas[1].fieldKey] = (value as Record<string, string>)[
1101
+ schemaKey
1102
+ ];
1103
+ rows.push(row);
1104
+ }
1105
+ }
1106
+ }
1107
+
1108
+ return (
1109
+ <Box>
1110
+ <Typography
1111
+ variant="subtitle2"
1112
+ sx={{ mb: 1, color: err ? "error.main" : "main.900" }}
1113
+ >
1114
+ {input.label}
1115
+ </Typography>
1116
+ <InputTable
1117
+ rows={rows}
1118
+ setRows={(rows) =>
1119
+ handleFieldChange(templateId, input.fieldName ?? "", rows)
1120
+ }
1121
+ schemas={(input.schemas as ProcessTemplateInputSchema[]) ?? []}
1122
+ tableError={err}
1123
+ readOnly={finalReadOnly}
1124
+ />
1125
+ {!readOnly && input.helpText && !err ? (
1126
+ <FormHelperText sx={{ my: 1, ml: 2 }}>
1127
+ {input.helpText ?? ""}
1128
+ </FormHelperText>
1129
+ ) : null}
1130
+ {err ? (
1131
+ <FormHelperText
1132
+ error
1133
+ sx={{ my: 1, ml: 2, whiteSpace: "pre-wrap", fontFamily: "inherit" }}
1134
+ >
1135
+ {String(err)}
1136
+ </FormHelperText>
1137
+ ) : null}
1138
+ </Box>
1139
+ );
1140
+ }
1141
+
1142
+ return (
1143
+ <TextInput
1144
+ label={input.label ?? ""}
1145
+ value={(values[k] as string) ?? ""}
1146
+ setValue={(v) => handleFieldChange(templateId, input.fieldName ?? "", v)}
1147
+ placeholder={input.placeholder ?? ""}
1148
+ helpText={input.helpText ?? ""}
1149
+ error={err}
1150
+ readOnly={finalReadOnly}
1151
+ />
1152
+ );
1153
+ }
1154
+
1155
+ function arrangeInputs(inputs: ProcessTemplateInput[]): InputRow[] {
1156
+ const ordered = sortBy(inputs);
1157
+
1158
+ const isTableOrHidden = (inp: ProcessTemplateInput) =>
1159
+ inp.inputType === "input-table" || inp.inputType?.startsWith("hidden");
1160
+
1161
+ const isPairable = (inp?: ProcessTemplateInput) =>
1162
+ !!inp && !isTableOrHidden(inp);
1163
+
1164
+ const areConsecutiveSorts = (
1165
+ a?: ProcessTemplateInput,
1166
+ b?: ProcessTemplateInput,
1167
+ ) =>
1168
+ !!a &&
1169
+ !!b &&
1170
+ typeof a.sort === "number" &&
1171
+ typeof b.sort === "number" &&
1172
+ b.sort === a.sort + 1;
1173
+
1174
+ const canPairAtAll = ordered.filter(isPairable).length >= 2;
1175
+
1176
+ const rows: InputRow[] = [];
1177
+ for (let i = 0; i < ordered.length; ) {
1178
+ const curr = ordered[i];
1179
+
1180
+ if (!isPairable(curr) || !canPairAtAll) {
1181
+ rows.push({ type: "full", item: curr });
1182
+ i += 1;
1183
+ continue;
1184
+ }
1185
+
1186
+ const next = ordered[i + 1];
1187
+ if (isPairable(next) && areConsecutiveSorts(curr, next)) {
1188
+ rows.push({ type: "pair", items: [curr, next!] });
1189
+ i += 2;
1190
+ } else {
1191
+ rows.push({ type: "full", item: curr });
1192
+ i += 1;
1193
+ }
1194
+ }
1195
+
1196
+ return rows;
1197
+ }
1198
+
1199
+ export default function PageBody({
1200
+ templateId,
1201
+ page,
1202
+ values,
1203
+ errors,
1204
+ handleFieldChange,
1205
+ clearTemplateFields,
1206
+ readOnly,
1207
+ sharedCount,
1208
+ isEditMode,
1209
+ isCreateMode,
1210
+ disabledFields,
1211
+ setFieldsDisabled,
1212
+ templateConfig,
1213
+ }: Readonly<{
1214
+ templateId: string;
1215
+ page: ProcessTemplatePage;
1216
+ values: FormState;
1217
+ errors: ErrorState;
1218
+ handleFieldChange: (
1219
+ templateId: string,
1220
+ fieldName: string,
1221
+ value: FieldValue,
1222
+ ) => void;
1223
+ clearTemplateFields: (templateId: string) => void;
1224
+ readOnly?: boolean;
1225
+ isEditMode?: boolean;
1226
+ isCreateMode?: boolean;
1227
+ sharedCount?: (count: number) => void;
1228
+ disabledFields?: Set<string>;
1229
+ setFieldsDisabled?: (
1230
+ templateId: string,
1231
+ fieldNames: string[],
1232
+ disabled: boolean,
1233
+ ) => void;
1234
+ templateConfig?: string;
1235
+ }>) {
1236
+ React.useEffect(
1237
+ function ensureHiddenDefaults() {
1238
+ const hiddenInputs = page.inputs?.filter(
1239
+ (i: ProcessTemplateInput | null) =>
1240
+ i?.inputType === "hidden" ||
1241
+ i?.inputType === "hidden-number" ||
1242
+ i?.inputType === "hidden-number-or-null",
1243
+ );
1244
+
1245
+ if (hiddenInputs) {
1246
+ for (const input of hiddenInputs) {
1247
+ const key = templateFieldKey(templateId, input?.fieldName ?? "");
1248
+ if (values[key] != null) continue;
1249
+
1250
+ switch (input?.fieldName) {
1251
+ case "processTemplateId":
1252
+ handleFieldChange(
1253
+ templateId,
1254
+ input.fieldName ?? "",
1255
+ Number(templateId),
1256
+ );
1257
+ break;
1258
+ default:
1259
+ handleFieldChange(
1260
+ templateId,
1261
+ input?.fieldName ?? "",
1262
+ input?.inputType === "hidden-number"
1263
+ ? (0 as FieldValue)
1264
+ : input?.inputType === "hidden-number-or-null"
1265
+ ? null
1266
+ : ("" as FieldValue),
1267
+ );
1268
+ break;
1269
+ }
1270
+ }
1271
+ }
1272
+ },
1273
+ [templateId, page.id, page.inputs, values, handleFieldChange],
1274
+ );
1275
+
1276
+ const arrangedInputs = React.useMemo(
1277
+ () => arrangeInputs(page.inputs as ProcessTemplateInput[]),
1278
+ [page.inputs],
1279
+ );
1280
+
1281
+ return (
1282
+ <Box sx={{ display: "grid", rowGap: 3 }}>
1283
+ {arrangedInputs.map((ai: InputRow, idx: number) => {
1284
+ if (ai.type === "full") {
1285
+ return (
1286
+ <Box key={`full-${idx + 1}`} sx={{ width: "100%" }}>
1287
+ {renderFormControl({
1288
+ input: ai.item,
1289
+ templateId,
1290
+ values,
1291
+ errors,
1292
+ handleFieldChange,
1293
+ clearTemplateFields,
1294
+ readOnly,
1295
+ sharedCount,
1296
+ isEditMode,
1297
+ isCreateMode,
1298
+ disabledFields,
1299
+ setFieldsDisabled,
1300
+ templateConfig,
1301
+ })}
1302
+ </Box>
1303
+ );
1304
+ }
1305
+
1306
+ const [i, j] = ai.items;
1307
+ return (
1308
+ <Box
1309
+ key={`pair-${idx + 1}`}
1310
+ sx={{
1311
+ display: "grid",
1312
+ gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" },
1313
+ columnGap: 3,
1314
+ rowGap: 3,
1315
+ width: "100%",
1316
+ }}
1317
+ >
1318
+ <Box>
1319
+ {renderFormControl({
1320
+ input: i,
1321
+ templateId,
1322
+ values,
1323
+ errors,
1324
+ handleFieldChange,
1325
+ clearTemplateFields,
1326
+ readOnly,
1327
+ sharedCount,
1328
+ isEditMode,
1329
+ isCreateMode,
1330
+ disabledFields,
1331
+ setFieldsDisabled,
1332
+ templateConfig,
1333
+ })}
1334
+ </Box>
1335
+ <Box>
1336
+ {renderFormControl({
1337
+ input: j,
1338
+ templateId,
1339
+ values,
1340
+ errors,
1341
+ handleFieldChange,
1342
+ clearTemplateFields,
1343
+ readOnly,
1344
+ sharedCount,
1345
+ isEditMode,
1346
+ isCreateMode,
1347
+ disabledFields,
1348
+ setFieldsDisabled,
1349
+ templateConfig,
1350
+ })}
1351
+ </Box>
1352
+ </Box>
1353
+ );
1354
+ })}
1355
+ </Box>
1356
+ );
1357
+ }
1358
+
1359
+ const parseQueryName = (queryName: string) => {
1360
+ let result: TemplateInputSearchQueryKey | null = null;
1361
+ const indexOf = queryName.indexOf("(");
1362
+ const qn = queryName.substring(0, indexOf < 0 ? undefined : indexOf);
1363
+
1364
+ if (isTemplateInputSearchQueryKey(qn)) {
1365
+ result = qn as TemplateInputSearchQueryKey;
1366
+ return result;
1367
+ }
1368
+
1369
+ throw new Error(`queryName ${queryName} invalid`);
1370
+ };
1371
+
1372
+ const renderReadOnlyText = (
1373
+ label?: string,
1374
+ value?: string | string[] | number | null,
1375
+ ) => {
1376
+ return (
1377
+ <>
1378
+ {value && (
1379
+ <Box>
1380
+ {label && (
1381
+ <Typography color="grey" variant="caption">
1382
+ {label}
1383
+ </Typography>
1384
+ )}
1385
+ <Typography color="inherit" variant="body1">
1386
+ {Array.isArray(value) ? (value as string[]).join(", ") : value}
1387
+ </Typography>
1388
+ </Box>
1389
+ )}
1390
+ </>
1391
+ );
1392
+ };