@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,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved form/config field names used by the template wizard.
|
|
3
|
+
* Use this enum instead of string literals to avoid typos and keep a single source of truth.
|
|
4
|
+
*/
|
|
5
|
+
export enum ReservedFieldName {
|
|
6
|
+
ProcessTemplateId = "processTemplateId",
|
|
7
|
+
IsUnique = "isUnique",
|
|
8
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { TemplateInputSearchQueryKey } from "@evenicanpm/admin-integrate/api/templates/queries/types";
|
|
2
|
+
import type { FieldValue, ProcessTemplateInputSchema } from "../types";
|
|
3
|
+
import type { InputType } from "./input-type";
|
|
4
|
+
export type TableRow = Record<string, string | number | boolean | null>;
|
|
5
|
+
|
|
6
|
+
export type SetRowsFn = (rows: TableRow[]) => void;
|
|
7
|
+
|
|
8
|
+
/** Used for TEXT_UNIQUE_IDENTIFIER when updating a sibling field (e.g. `isUnique`). */
|
|
9
|
+
export type SearchInputExtendedSetValue = (
|
|
10
|
+
v: FieldValue,
|
|
11
|
+
fieldName?: string,
|
|
12
|
+
) => void;
|
|
13
|
+
|
|
14
|
+
export interface InputTableProps {
|
|
15
|
+
rows: TableRow[];
|
|
16
|
+
setRows: SetRowsFn;
|
|
17
|
+
schemas: ProcessTemplateInputSchema[];
|
|
18
|
+
tableError: string | null;
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SearchInputWrapperProps = {
|
|
23
|
+
fieldName?: string;
|
|
24
|
+
inputType:
|
|
25
|
+
| InputType.MULTI_TEXT_SEARCH
|
|
26
|
+
| InputType.TEXT_SEARCH
|
|
27
|
+
| InputType.TEXT_UNIQUE_IDENTIFIER;
|
|
28
|
+
|
|
29
|
+
label: string;
|
|
30
|
+
value: string | string[];
|
|
31
|
+
setValue:
|
|
32
|
+
| ((v: string) => void)
|
|
33
|
+
| ((v: string[]) => void)
|
|
34
|
+
| SearchInputExtendedSetValue;
|
|
35
|
+
|
|
36
|
+
placeholder?: string | null;
|
|
37
|
+
helpText?: string | null;
|
|
38
|
+
error?: string | null;
|
|
39
|
+
|
|
40
|
+
queryName: TemplateInputSearchQueryKey;
|
|
41
|
+
|
|
42
|
+
/** Raw query name (e.g. Templates_GetSourceEndpoints) for getFieldValues API when inputType is TEXT_SEARCH */
|
|
43
|
+
validationQuery?: string;
|
|
44
|
+
|
|
45
|
+
/** Used by TextSearchInput to apply selected template config into the form */
|
|
46
|
+
handleFieldChange?: (
|
|
47
|
+
templateId: string,
|
|
48
|
+
fieldName: string,
|
|
49
|
+
value: FieldValue,
|
|
50
|
+
) => void;
|
|
51
|
+
|
|
52
|
+
/** Call when the search input blurs so errors show on focus out */
|
|
53
|
+
onBlur?: () => void;
|
|
54
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export { FormikPageBody } from "./FormikPageBody";
|
|
4
|
+
export { ReservedFieldName } from "./field-names";
|
|
5
|
+
export type { SearchInputWrapperProps } from "./form-type";
|
|
6
|
+
export { InputType, isHiddenInputType } from "./input-type";
|
|
7
|
+
export {
|
|
8
|
+
CheckboxInput,
|
|
9
|
+
NumberInput,
|
|
10
|
+
SelectorInput,
|
|
11
|
+
TextInput,
|
|
12
|
+
} from "./primitives";
|
|
13
|
+
export { renderFormControl, renderSearchInput } from "./renderers";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
MultiTextSearchInput,
|
|
17
|
+
SearchInputWrapper,
|
|
18
|
+
TextSearchInput,
|
|
19
|
+
useSearchInput,
|
|
20
|
+
} from "./search";
|
|
21
|
+
export { TemplateSearchQueryKey } from "./search/search-query-key";
|
|
22
|
+
|
|
23
|
+
export { InputTable } from "./table";
|
|
24
|
+
export {
|
|
25
|
+
applyTemplateConfig,
|
|
26
|
+
arrangeInputs,
|
|
27
|
+
getSelectorOptionsFromInput,
|
|
28
|
+
getTableRowErrors,
|
|
29
|
+
parseQueryName,
|
|
30
|
+
renderReadOnlyText,
|
|
31
|
+
toStringArray,
|
|
32
|
+
} from "./utils";
|
|
33
|
+
export type { InputRow } from "./utils/arrangeInputs";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export enum InputType {
|
|
2
|
+
TEXT = "text",
|
|
3
|
+
NUMBER = "number",
|
|
4
|
+
CHECKBOX = "checkbox",
|
|
5
|
+
SELECTOR = "selector",
|
|
6
|
+
INPUT_TABLE = "input-table",
|
|
7
|
+
TEXT_SEARCH = "text-search",
|
|
8
|
+
MULTI_TEXT_SEARCH = "multi-text-search",
|
|
9
|
+
TEXT_UNIQUE_IDENTIFIER = "text-unique-identifier",
|
|
10
|
+
HIDDEN = "hidden",
|
|
11
|
+
HIDDEN_NUMBER = "hidden-number",
|
|
12
|
+
HIDDEN_NUMBER_OR_NULL = "hidden-number-or-null",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isHiddenInputType(
|
|
16
|
+
inputType: string | null | undefined,
|
|
17
|
+
): boolean {
|
|
18
|
+
return (
|
|
19
|
+
inputType === InputType.HIDDEN ||
|
|
20
|
+
inputType === InputType.HIDDEN_NUMBER ||
|
|
21
|
+
inputType === InputType.HIDDEN_NUMBER_OR_NULL
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { SxProps, Theme } from "@mui/material";
|
|
4
|
+
import { Box, Checkbox, FormControlLabel, FormHelperText } from "@mui/material";
|
|
5
|
+
|
|
6
|
+
export function CheckboxInput({
|
|
7
|
+
label,
|
|
8
|
+
checked,
|
|
9
|
+
setChecked,
|
|
10
|
+
helpText,
|
|
11
|
+
readOnly,
|
|
12
|
+
disabled,
|
|
13
|
+
formHelperTextSx,
|
|
14
|
+
}: Readonly<{
|
|
15
|
+
label: string;
|
|
16
|
+
checked: boolean;
|
|
17
|
+
setChecked: (v: boolean) => void;
|
|
18
|
+
helpText?: string | null;
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
/** Disable without read-only (e.g. while validating). */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Optional sx for the helper text (e.g. color: 'success.main' or 'error.main'). */
|
|
23
|
+
formHelperTextSx?: SxProps<Theme>;
|
|
24
|
+
}>) {
|
|
25
|
+
return (
|
|
26
|
+
<Box>
|
|
27
|
+
<FormControlLabel
|
|
28
|
+
control={
|
|
29
|
+
<Checkbox
|
|
30
|
+
checked={checked}
|
|
31
|
+
onChange={(e) => setChecked(e.target.checked)}
|
|
32
|
+
/>
|
|
33
|
+
}
|
|
34
|
+
label={label}
|
|
35
|
+
disabled={disabled ?? readOnly}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<FormHelperText
|
|
39
|
+
sx={{ mt: -1.5, mb: 1, marginLeft: 0, ...formHelperTextSx }}
|
|
40
|
+
>
|
|
41
|
+
{helpText || " "}
|
|
42
|
+
</FormHelperText>
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { SxProps, Theme } from "@mui/material";
|
|
4
|
+
import { TextField } from "@mui/material";
|
|
5
|
+
import { renderReadOnlyText } from "../utils/renderReadOnlyText";
|
|
6
|
+
|
|
7
|
+
export function NumberInput({
|
|
8
|
+
label,
|
|
9
|
+
placeholder,
|
|
10
|
+
value,
|
|
11
|
+
setValue,
|
|
12
|
+
error,
|
|
13
|
+
helpText,
|
|
14
|
+
readOnly,
|
|
15
|
+
disabled,
|
|
16
|
+
onBlur,
|
|
17
|
+
onFocus,
|
|
18
|
+
formHelperTextSx,
|
|
19
|
+
}: Readonly<{
|
|
20
|
+
label: string;
|
|
21
|
+
placeholder?: string | null;
|
|
22
|
+
value: number | "" | null;
|
|
23
|
+
setValue: (v: number | "") => void;
|
|
24
|
+
error?: string | null;
|
|
25
|
+
helpText?: string | null;
|
|
26
|
+
readOnly?: boolean;
|
|
27
|
+
/** Disable input without switching to read-only text (e.g. while validating). */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
onBlur?: () => void;
|
|
30
|
+
onFocus?: () => void;
|
|
31
|
+
/** Optional sx for the helper text (e.g. color: 'success.main' for validation success). */
|
|
32
|
+
formHelperTextSx?: SxProps<Theme>;
|
|
33
|
+
}>) {
|
|
34
|
+
if (readOnly) return renderReadOnlyText(label, value);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<TextField
|
|
38
|
+
type="number"
|
|
39
|
+
label={label}
|
|
40
|
+
value={value ?? ""}
|
|
41
|
+
onChange={(e) =>
|
|
42
|
+
setValue(e.target.value === "" ? "" : Number(e.target.value))
|
|
43
|
+
}
|
|
44
|
+
onBlur={onBlur}
|
|
45
|
+
onFocus={onFocus}
|
|
46
|
+
placeholder={placeholder ?? undefined}
|
|
47
|
+
fullWidth
|
|
48
|
+
error={Boolean(error)}
|
|
49
|
+
helperText={error ?? helpText ?? " "}
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
slotProps={{
|
|
52
|
+
formHelperText: {
|
|
53
|
+
sx: { marginLeft: 0, ...formHelperTextSx },
|
|
54
|
+
},
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { SxProps, Theme } from "@mui/material";
|
|
4
|
+
import { Autocomplete, Box, TextField } from "@mui/material";
|
|
5
|
+
import { renderReadOnlyText } from "../utils/renderReadOnlyText";
|
|
6
|
+
|
|
7
|
+
/** Option can be a plain string (label = value) or { label, value } for API-backed options. */
|
|
8
|
+
export type SelectorOption = string | { label: string; value: string };
|
|
9
|
+
|
|
10
|
+
function getOptionLabel(opt: SelectorOption): string {
|
|
11
|
+
return typeof opt === "string" ? opt : opt.label;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getOptionValue(opt: SelectorOption): string {
|
|
15
|
+
return typeof opt === "string" ? opt : opt.value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function optionEquals(opt: SelectorOption, value: string): boolean {
|
|
19
|
+
return getOptionValue(opt) === value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SelectorInput({
|
|
23
|
+
fieldName,
|
|
24
|
+
label,
|
|
25
|
+
value,
|
|
26
|
+
setValue,
|
|
27
|
+
helpText,
|
|
28
|
+
error,
|
|
29
|
+
options,
|
|
30
|
+
insideTable = false,
|
|
31
|
+
minWidth,
|
|
32
|
+
readOnly,
|
|
33
|
+
disabled,
|
|
34
|
+
onBlur,
|
|
35
|
+
onFocus,
|
|
36
|
+
formHelperTextSx,
|
|
37
|
+
}: Readonly<{
|
|
38
|
+
fieldName: string;
|
|
39
|
+
label: string;
|
|
40
|
+
value: string;
|
|
41
|
+
setValue: (v: string) => void;
|
|
42
|
+
helpText?: string | null;
|
|
43
|
+
error?: string | null;
|
|
44
|
+
/** String options (label = value) or { label, value } for API-backed options. */
|
|
45
|
+
options?: SelectorOption[];
|
|
46
|
+
insideTable?: boolean;
|
|
47
|
+
minWidth?: number;
|
|
48
|
+
readOnly?: boolean;
|
|
49
|
+
/** Disable input without switching to read-only text (e.g. while validating). */
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
onBlur?: () => void;
|
|
52
|
+
onFocus?: () => void;
|
|
53
|
+
/** Optional sx for the helper text (e.g. color: 'success.main' for validation success). */
|
|
54
|
+
formHelperTextSx?: SxProps<Theme>;
|
|
55
|
+
}>) {
|
|
56
|
+
const opts = options ?? [];
|
|
57
|
+
const allowFreeText = opts.length === 0;
|
|
58
|
+
const id = `selector-${fieldName}`;
|
|
59
|
+
const currentValue = value ?? "";
|
|
60
|
+
const selectedOption =
|
|
61
|
+
opts.find((o) => optionEquals(o, currentValue)) ?? (currentValue || null);
|
|
62
|
+
|
|
63
|
+
if (readOnly)
|
|
64
|
+
return renderReadOnlyText(insideTable ? undefined : label, value);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Box
|
|
68
|
+
sx={{
|
|
69
|
+
minWidth: minWidth ?? (insideTable ? 220 : undefined),
|
|
70
|
+
width: "100%",
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<Autocomplete<SelectorOption, false, false, boolean>
|
|
74
|
+
id={id}
|
|
75
|
+
disablePortal={insideTable}
|
|
76
|
+
options={opts}
|
|
77
|
+
getOptionLabel={getOptionLabel}
|
|
78
|
+
isOptionEqualToValue={(opt, v) =>
|
|
79
|
+
v == null
|
|
80
|
+
? false
|
|
81
|
+
: opt === v ||
|
|
82
|
+
optionEquals(opt, typeof v === "string" ? v : getOptionValue(v))
|
|
83
|
+
}
|
|
84
|
+
freeSolo={allowFreeText}
|
|
85
|
+
value={selectedOption ?? null}
|
|
86
|
+
onChange={(_e, val) => setValue(val != null ? getOptionValue(val) : "")}
|
|
87
|
+
onInputChange={(_e, val) => setValue(val)}
|
|
88
|
+
disabled={disabled ?? readOnly}
|
|
89
|
+
renderInput={(params) => (
|
|
90
|
+
<TextField
|
|
91
|
+
{...params}
|
|
92
|
+
size={insideTable ? "small" : "medium"}
|
|
93
|
+
label={label}
|
|
94
|
+
name={fieldName}
|
|
95
|
+
error={Boolean(error)}
|
|
96
|
+
helperText={error ?? helpText ?? " "}
|
|
97
|
+
fullWidth
|
|
98
|
+
disabled={disabled ?? readOnly}
|
|
99
|
+
onBlur={onBlur}
|
|
100
|
+
onFocus={onFocus}
|
|
101
|
+
slotProps={{
|
|
102
|
+
formHelperText: {
|
|
103
|
+
sx: { marginLeft: 0, ...formHelperTextSx },
|
|
104
|
+
},
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
/>
|
|
109
|
+
</Box>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { SxProps, Theme } from "@mui/material";
|
|
4
|
+
import { TextField } from "@mui/material";
|
|
5
|
+
import { renderReadOnlyText } from "../utils/renderReadOnlyText";
|
|
6
|
+
|
|
7
|
+
export function TextInput({
|
|
8
|
+
label,
|
|
9
|
+
placeholder,
|
|
10
|
+
value,
|
|
11
|
+
setValue,
|
|
12
|
+
error,
|
|
13
|
+
helpText,
|
|
14
|
+
readOnly,
|
|
15
|
+
disabled,
|
|
16
|
+
onBlur,
|
|
17
|
+
onFocus,
|
|
18
|
+
formHelperTextSx,
|
|
19
|
+
}: Readonly<{
|
|
20
|
+
label: string;
|
|
21
|
+
placeholder?: string | null;
|
|
22
|
+
value: string;
|
|
23
|
+
setValue: (v: string) => void;
|
|
24
|
+
error?: string | null;
|
|
25
|
+
helpText?: string | null;
|
|
26
|
+
readOnly?: boolean;
|
|
27
|
+
/** Disable input without switching to read-only text (e.g. while validating). */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
onBlur?: () => void;
|
|
30
|
+
onFocus?: () => void;
|
|
31
|
+
/** Optional sx for the helper text (e.g. color: 'success.main' for validation success). */
|
|
32
|
+
formHelperTextSx?: SxProps<Theme>;
|
|
33
|
+
}>) {
|
|
34
|
+
if (readOnly) return renderReadOnlyText(label, value);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<TextField
|
|
38
|
+
label={label}
|
|
39
|
+
value={value ?? ""}
|
|
40
|
+
onChange={(e) => setValue(e.target.value)}
|
|
41
|
+
onBlur={onBlur}
|
|
42
|
+
onFocus={onFocus}
|
|
43
|
+
placeholder={placeholder ?? undefined}
|
|
44
|
+
fullWidth
|
|
45
|
+
error={Boolean(error)}
|
|
46
|
+
helperText={error ?? helpText ?? " "}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
slotProps={{
|
|
49
|
+
formHelperText: {
|
|
50
|
+
sx: { minHeight: 16, marginLeft: 0, ...formHelperTextSx },
|
|
51
|
+
},
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { SxProps, Theme } from "@mui/material";
|
|
4
|
+
import { Box } from "@mui/material";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { validateFieldOnCheck } from "../../../../api/templates/queries/validate-field-on-check.server";
|
|
7
|
+
import { InputType } from "../input-type";
|
|
8
|
+
import { renderReadOnlyText } from "../utils/renderReadOnlyText";
|
|
9
|
+
import { CheckboxInput } from "./CheckboxInput";
|
|
10
|
+
import { NumberInput } from "./NumberInput";
|
|
11
|
+
import type { SelectorOption } from "./SelectorInput";
|
|
12
|
+
import { SelectorInput } from "./SelectorInput";
|
|
13
|
+
import { TextInput } from "./TextInput";
|
|
14
|
+
|
|
15
|
+
export type TextWithQueryInputType =
|
|
16
|
+
| InputType.TEXT
|
|
17
|
+
| InputType.NUMBER
|
|
18
|
+
| InputType.SELECTOR
|
|
19
|
+
| InputType.CHECKBOX;
|
|
20
|
+
|
|
21
|
+
export type TextWithQueryValue = string | number | boolean;
|
|
22
|
+
|
|
23
|
+
function valueToString(value: TextWithQueryValue): string {
|
|
24
|
+
if (value === "" || value == null) return "";
|
|
25
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
26
|
+
return String(value).trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getEffectiveValidation(
|
|
30
|
+
submitValidationResult:
|
|
31
|
+
| { isValid: boolean; message: string }
|
|
32
|
+
| null
|
|
33
|
+
| undefined,
|
|
34
|
+
validation: { isValid: boolean; message: string } | null,
|
|
35
|
+
): { isValid: boolean; message: string } | null {
|
|
36
|
+
if (submitValidationResult != null) {
|
|
37
|
+
return {
|
|
38
|
+
isValid: submitValidationResult.isValid,
|
|
39
|
+
message: submitValidationResult.message,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return validation;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getDisplayError(
|
|
46
|
+
validating: boolean,
|
|
47
|
+
isError: boolean,
|
|
48
|
+
helperMessage: string,
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (validating) return undefined;
|
|
51
|
+
if (isError) return helperMessage;
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatReadOnlyValue(
|
|
56
|
+
value: TextWithQueryValue,
|
|
57
|
+
): string | number | boolean {
|
|
58
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface DisplayStateInput {
|
|
63
|
+
error?: string | null;
|
|
64
|
+
validation: { isValid: boolean; message: string } | null;
|
|
65
|
+
submitValidationResult?: { isValid: boolean; message: string } | null;
|
|
66
|
+
helpText?: string | null;
|
|
67
|
+
validating: boolean;
|
|
68
|
+
readOnly?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getInputDisplayState({
|
|
72
|
+
error,
|
|
73
|
+
validation,
|
|
74
|
+
submitValidationResult,
|
|
75
|
+
helpText,
|
|
76
|
+
validating,
|
|
77
|
+
readOnly,
|
|
78
|
+
}: DisplayStateInput): {
|
|
79
|
+
displayError: string | undefined;
|
|
80
|
+
displayHelpText: string;
|
|
81
|
+
formHelperTextSx: SxProps<Theme>;
|
|
82
|
+
isDisabled: boolean;
|
|
83
|
+
} {
|
|
84
|
+
const showError = Boolean(error);
|
|
85
|
+
const effectiveValidation = getEffectiveValidation(
|
|
86
|
+
submitValidationResult,
|
|
87
|
+
validation,
|
|
88
|
+
);
|
|
89
|
+
const helperMessage =
|
|
90
|
+
showError && error
|
|
91
|
+
? error
|
|
92
|
+
: (effectiveValidation?.message ?? helpText ?? "");
|
|
93
|
+
const isSuccess = effectiveValidation?.isValid === true && !showError;
|
|
94
|
+
const isError = showError || effectiveValidation?.isValid === false;
|
|
95
|
+
const displayError = getDisplayError(validating, isError, helperMessage);
|
|
96
|
+
const displayHelpText = validating ? "Checking…" : helperMessage;
|
|
97
|
+
const isDisabled = Boolean(readOnly || validating);
|
|
98
|
+
const formHelperTextSx: SxProps<Theme> = {
|
|
99
|
+
...(validating && { color: "text.secondary" }),
|
|
100
|
+
...(isSuccess && !validating && { color: "success.main" }),
|
|
101
|
+
...(isError && !validating && { color: "error.main" }),
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
displayError,
|
|
105
|
+
displayHelpText,
|
|
106
|
+
formHelperTextSx,
|
|
107
|
+
isDisabled,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type SubmitValidationResult = { isValid: boolean; message: string };
|
|
112
|
+
|
|
113
|
+
export interface TextWithQueryInputProps {
|
|
114
|
+
inputType: TextWithQueryInputType;
|
|
115
|
+
label: string;
|
|
116
|
+
value: TextWithQueryValue;
|
|
117
|
+
setValue: (v: TextWithQueryValue) => void;
|
|
118
|
+
error?: string | null;
|
|
119
|
+
helpText?: string | null;
|
|
120
|
+
readOnly?: boolean;
|
|
121
|
+
onBlur?: () => void;
|
|
122
|
+
fieldName: string;
|
|
123
|
+
validationQuery: string;
|
|
124
|
+
fieldId: string;
|
|
125
|
+
validateFormikBeforeApi?: () => Promise<boolean>;
|
|
126
|
+
setFieldValidationInProgress?: (inProgress: boolean) => void;
|
|
127
|
+
placeholder?: string | null;
|
|
128
|
+
options?: SelectorOption[];
|
|
129
|
+
/** Result from validateFieldOnSubmit (on Next click); shown below field. */
|
|
130
|
+
submitValidationResult?: SubmitValidationResult | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function TextWithQueryInput({
|
|
134
|
+
inputType,
|
|
135
|
+
label,
|
|
136
|
+
value,
|
|
137
|
+
setValue,
|
|
138
|
+
error,
|
|
139
|
+
helpText,
|
|
140
|
+
readOnly,
|
|
141
|
+
onBlur,
|
|
142
|
+
fieldName,
|
|
143
|
+
validationQuery,
|
|
144
|
+
fieldId,
|
|
145
|
+
validateFormikBeforeApi,
|
|
146
|
+
setFieldValidationInProgress,
|
|
147
|
+
placeholder,
|
|
148
|
+
options = [],
|
|
149
|
+
submitValidationResult,
|
|
150
|
+
}: Readonly<TextWithQueryInputProps>) {
|
|
151
|
+
const [validation, setValidation] = React.useState<{
|
|
152
|
+
isValid: boolean;
|
|
153
|
+
message: string;
|
|
154
|
+
} | null>(null);
|
|
155
|
+
const [validating, setValidating] = React.useState(false);
|
|
156
|
+
|
|
157
|
+
const handleBlur = React.useCallback(async () => {
|
|
158
|
+
onBlur?.();
|
|
159
|
+
const strValue = valueToString(value);
|
|
160
|
+
if (!strValue && inputType !== InputType.CHECKBOX) {
|
|
161
|
+
setValidation(null);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const formikValid = await (validateFormikBeforeApi?.() ??
|
|
165
|
+
Promise.resolve(true));
|
|
166
|
+
if (!formikValid) return;
|
|
167
|
+
setValidating(true);
|
|
168
|
+
setValidation(null);
|
|
169
|
+
setFieldValidationInProgress?.(true);
|
|
170
|
+
try {
|
|
171
|
+
const result = await validateFieldOnCheck({
|
|
172
|
+
input: {
|
|
173
|
+
fieldName,
|
|
174
|
+
fieldId,
|
|
175
|
+
value: strValue,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
if (result) {
|
|
179
|
+
setValidation({
|
|
180
|
+
isValid: result.isValid ?? false,
|
|
181
|
+
message: result.message ?? "",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
setValidation({ isValid: false, message: "Validation failed" });
|
|
186
|
+
} finally {
|
|
187
|
+
setValidating(false);
|
|
188
|
+
setFieldValidationInProgress?.(false);
|
|
189
|
+
}
|
|
190
|
+
}, [
|
|
191
|
+
value,
|
|
192
|
+
inputType,
|
|
193
|
+
fieldName,
|
|
194
|
+
validationQuery,
|
|
195
|
+
onBlur,
|
|
196
|
+
validateFormikBeforeApi,
|
|
197
|
+
setFieldValidationInProgress,
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
if (readOnly) {
|
|
201
|
+
const formattedValue = formatReadOnlyValue(value);
|
|
202
|
+
const readOnlyValue =
|
|
203
|
+
typeof formattedValue === "string" ||
|
|
204
|
+
typeof formattedValue === "number" ||
|
|
205
|
+
(Array.isArray(formattedValue) &&
|
|
206
|
+
formattedValue.every((v) => typeof v === "string"))
|
|
207
|
+
? formattedValue
|
|
208
|
+
: "";
|
|
209
|
+
return renderReadOnlyText(label, readOnlyValue);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const display = getInputDisplayState({
|
|
213
|
+
error,
|
|
214
|
+
validation,
|
|
215
|
+
submitValidationResult,
|
|
216
|
+
helpText,
|
|
217
|
+
validating,
|
|
218
|
+
readOnly,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const clearValidationAndSet = (v: TextWithQueryValue) => {
|
|
222
|
+
setValidation(null);
|
|
223
|
+
setValue(v);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (inputType === InputType.TEXT) {
|
|
227
|
+
return (
|
|
228
|
+
<TextInput
|
|
229
|
+
label={label}
|
|
230
|
+
placeholder={placeholder}
|
|
231
|
+
value={(value as string) ?? ""}
|
|
232
|
+
setValue={clearValidationAndSet}
|
|
233
|
+
error={display.displayError}
|
|
234
|
+
helpText={display.displayHelpText}
|
|
235
|
+
readOnly={readOnly}
|
|
236
|
+
disabled={display.isDisabled}
|
|
237
|
+
onBlur={handleBlur}
|
|
238
|
+
formHelperTextSx={display.formHelperTextSx}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (inputType === InputType.NUMBER) {
|
|
243
|
+
return (
|
|
244
|
+
<NumberInput
|
|
245
|
+
label={label}
|
|
246
|
+
placeholder={placeholder}
|
|
247
|
+
value={value === "" || value == null ? "" : (value as number)}
|
|
248
|
+
setValue={clearValidationAndSet}
|
|
249
|
+
error={display.displayError}
|
|
250
|
+
helpText={display.displayHelpText}
|
|
251
|
+
readOnly={readOnly}
|
|
252
|
+
disabled={display.isDisabled}
|
|
253
|
+
onBlur={handleBlur}
|
|
254
|
+
formHelperTextSx={display.formHelperTextSx}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (inputType === InputType.SELECTOR) {
|
|
259
|
+
return (
|
|
260
|
+
<SelectorInput
|
|
261
|
+
fieldName={fieldName}
|
|
262
|
+
label={label}
|
|
263
|
+
value={(value as string) ?? ""}
|
|
264
|
+
setValue={clearValidationAndSet}
|
|
265
|
+
helpText={display.displayHelpText}
|
|
266
|
+
error={display.displayError}
|
|
267
|
+
options={options}
|
|
268
|
+
readOnly={readOnly}
|
|
269
|
+
disabled={display.isDisabled}
|
|
270
|
+
onBlur={handleBlur}
|
|
271
|
+
formHelperTextSx={display.formHelperTextSx}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (inputType === InputType.CHECKBOX) {
|
|
276
|
+
return (
|
|
277
|
+
<Box onBlur={handleBlur}>
|
|
278
|
+
<CheckboxInput
|
|
279
|
+
label={label}
|
|
280
|
+
checked={Boolean(value)}
|
|
281
|
+
setChecked={clearValidationAndSet}
|
|
282
|
+
helpText={display.displayHelpText}
|
|
283
|
+
readOnly={readOnly}
|
|
284
|
+
disabled={display.isDisabled}
|
|
285
|
+
formHelperTextSx={display.formHelperTextSx}
|
|
286
|
+
/>
|
|
287
|
+
</Box>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { CheckboxInput } from "./CheckboxInput";
|
|
2
|
+
export { NumberInput } from "./NumberInput";
|
|
3
|
+
export { SelectorInput } from "./SelectorInput";
|
|
4
|
+
export { TextInput } from "./TextInput";
|
|
5
|
+
export {
|
|
6
|
+
TextWithQueryInput,
|
|
7
|
+
type TextWithQueryInputProps,
|
|
8
|
+
type TextWithQueryInputType,
|
|
9
|
+
type TextWithQueryValue,
|
|
10
|
+
} from "./TextWithQueryInput";
|