@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.
Files changed (97) hide show
  1. package/documents/ConfigImport/configuration-import.ts +1 -1
  2. package/documents/Endpoint/list.ts +3 -3
  3. package/documents/Process/list.ts +3 -3
  4. package/documents/ProcessExecution/list.ts +2 -2
  5. package/documents/ProcessExport/read.ts +1 -1
  6. package/documents/ProcessSchedule/read.ts +1 -1
  7. package/documents/ProcessScheduleGroup/read.ts +3 -3
  8. package/documents/ProcessTask/fragments.ts +4 -0
  9. package/documents/ProcessTemplate/fragments.ts +30 -0
  10. package/documents/ProcessTemplate/list.ts +29 -0
  11. package/documents/ProcessTemplate/read.ts +1 -1
  12. package/documents/ScheduledProcess/list.ts +1 -1
  13. package/package.json +2 -2
  14. package/src/api/process-config/mutation/process-config-import.mutation.ts +1 -1
  15. package/src/api/templates/queries/get-field-values.query.ts +48 -0
  16. package/src/api/templates/queries/get-field-values.server.ts +9 -0
  17. package/src/api/templates/queries/index.ts +4 -0
  18. package/src/api/templates/queries/types.ts +2 -2
  19. package/src/api/templates/queries/validate-field-on-check.query.ts +22 -0
  20. package/src/api/templates/queries/validate-field-on-check.server.ts +30 -0
  21. package/src/api/templates/queries/validate-field-on-submit.server.ts +18 -0
  22. package/src/components/data-table/table-row.tsx +1 -1
  23. package/src/components/icons/up-down.tsx +1 -0
  24. package/src/components/integration-view/animated-svg.tsx +0 -1
  25. package/src/components/integration-view/elk-types.ts +1 -1
  26. package/src/components/integration-view/integration-view.tsx +274 -187
  27. package/src/components/integration-view/nodes/execution-entry-node.tsx +3 -2
  28. package/src/components/integration-view/nodes/task-node.tsx +1 -1
  29. package/src/components/integration-view/types.ts +2 -0
  30. package/src/components/integration-view/utils/mapping.ts +68 -48
  31. package/src/components/list-filter/list-filter-types.ts +6 -0
  32. package/src/components/list-filter/list-filter.tsx +21 -15
  33. package/src/components/list-filter/multi-select-filter.tsx +7 -7
  34. package/src/components/list-filter/select-filter.tsx +6 -6
  35. package/src/components/scheduler/day-selector.tsx +1 -1
  36. package/src/components/templatesV2/inputs/FormikPageBody.tsx +191 -0
  37. package/src/components/templatesV2/inputs/context/FormModeContext.tsx +31 -0
  38. package/src/components/templatesV2/inputs/context/TemplateContext.tsx +37 -0
  39. package/src/components/templatesV2/inputs/field-names.ts +8 -0
  40. package/src/components/templatesV2/inputs/form-type.ts +54 -0
  41. package/src/components/templatesV2/inputs/index.tsx +33 -0
  42. package/src/components/templatesV2/inputs/input-type.ts +23 -0
  43. package/src/components/templatesV2/inputs/primitives/CheckboxInput.tsx +45 -0
  44. package/src/components/templatesV2/inputs/primitives/NumberInput.tsx +58 -0
  45. package/src/components/templatesV2/inputs/primitives/SelectorInput.tsx +111 -0
  46. package/src/components/templatesV2/inputs/primitives/TextInput.tsx +55 -0
  47. package/src/components/templatesV2/inputs/primitives/TextWithQueryInput.tsx +292 -0
  48. package/src/components/templatesV2/inputs/primitives/index.ts +10 -0
  49. package/src/components/templatesV2/inputs/renderers/index.ts +2 -0
  50. package/src/components/templatesV2/inputs/renderers/renderFormControl.tsx +384 -0
  51. package/src/components/templatesV2/inputs/renderers/renderSearchInput.tsx +128 -0
  52. package/src/components/templatesV2/inputs/search/MultiTextSearchInput.tsx +87 -0
  53. package/src/components/templatesV2/inputs/search/SearchInputWrapper.tsx +113 -0
  54. package/src/components/templatesV2/inputs/search/TextSearchInput.tsx +309 -0
  55. package/src/components/templatesV2/inputs/search/index.ts +4 -0
  56. package/src/components/templatesV2/inputs/search/search-query-key.ts +23 -0
  57. package/src/components/templatesV2/inputs/search/useFieldValuesOptions.ts +58 -0
  58. package/src/components/templatesV2/inputs/search/useSearchInput.ts +79 -0
  59. package/src/components/templatesV2/inputs/table/InputTable.tsx +268 -0
  60. package/src/components/templatesV2/inputs/table/index.ts +1 -0
  61. package/src/components/templatesV2/inputs/utils/applyTemplateConfig.ts +28 -0
  62. package/src/components/templatesV2/inputs/utils/arrangeInputs.ts +52 -0
  63. package/src/components/templatesV2/inputs/utils/index.ts +6 -0
  64. package/src/components/templatesV2/inputs/utils/parseQueryName.ts +20 -0
  65. package/src/components/templatesV2/inputs/utils/renderReadOnlyText.tsx +26 -0
  66. package/src/components/templatesV2/inputs/utils/selectors.ts +69 -0
  67. package/src/components/templatesV2/inputs/utils/toStringArray.ts +23 -0
  68. package/src/components/templatesV2/pageForm.tsx +153 -0
  69. package/src/components/{templates → templatesV2}/templates-list/templates-list-row.tsx +2 -1
  70. package/src/components/{templates → templatesV2}/templates-list/templates-list-table.tsx +2 -2
  71. package/src/components/{templates → templatesV2}/templates-list/templates-list.tsx +19 -28
  72. package/src/components/{templates → templatesV2}/types.tsx +12 -12
  73. package/src/components/templatesV2/validation/buildZodSchema.ts +212 -0
  74. package/src/components/templatesV2/validation/zodFormikValidate.ts +17 -0
  75. package/src/components/templatesV2/wizard.tsx +746 -0
  76. package/src/pages/dashboard/dashboard-link-cards.tsx +0 -1
  77. package/src/pages/dashboard/dashboard-list-sections.tsx +1 -1
  78. package/src/pages/dashboard/dashboard.tsx +0 -1
  79. package/src/pages/executions/executions-list/executions-list-table.tsx +0 -1
  80. package/src/pages/executions/executions.tsx +1 -1
  81. package/src/pages/integration-create/integration-create-steps.ts +8 -0
  82. package/src/pages/integration-create/integration-create.tsx +81 -28
  83. package/src/pages/integration-create/pre-creation-step.tsx +9 -9
  84. package/src/pages/integration-create/select-step.tsx +3 -3
  85. package/src/pages/integration-create/template-list-step.tsx +28 -3
  86. package/src/pages/integration-create/template-options-step.tsx +119 -0
  87. package/src/pages/integration-create/wizard-step.tsx +5 -4
  88. package/src/pages/integration-details/import-process/copy-tasks-list.tsx +1 -3
  89. package/src/pages/integration-details/integration-details.tsx +1 -1
  90. package/src/pages/integration-details/task-drawer/add-task.tsx +4 -4
  91. package/src/pages/integration-details/task-drawer/edit-task.tsx +5 -5
  92. package/src/pages/integration-details/task-drawer/task-drawer.tsx +2 -2
  93. package/src/components/templates/inputs/index.tsx +0 -1392
  94. package/src/components/templates/wizard.tsx +0 -737
  95. /package/src/components/{templates → templatesV2}/index.ts +0 -0
  96. /package/src/components/{templates → templatesV2}/template-filter.ts +0 -0
  97. /package/src/components/{templates → templatesV2}/templates-list/index.ts +0 -0
@@ -0,0 +1,309 @@
1
+ "use client";
2
+
3
+ import type { AutocompleteChangeReason } from "@mui/material";
4
+ import { Autocomplete, Box, TextField } from "@mui/material";
5
+ import { debounce } from "lodash";
6
+ import * as React from "react";
7
+ import type { FieldValue } from "../../types";
8
+ import { templateFieldKey } from "../../types";
9
+ import { useFormMode } from "../context/FormModeContext";
10
+ import { useTemplateContext } from "../context/TemplateContext";
11
+ import { ReservedFieldName } from "../field-names";
12
+ import { applyTemplateConfig } from "../utils/applyTemplateConfig";
13
+
14
+ type SearchOption = { name: string; templateConfig: string; value?: string };
15
+
16
+ function getConfigFieldNames(
17
+ config: Record<string, unknown>,
18
+ fieldName: string | undefined,
19
+ ): string[] {
20
+ return Object.keys(config).filter(
21
+ (key) =>
22
+ key !== ReservedFieldName.ProcessTemplateId && key !== (fieldName ?? ""),
23
+ );
24
+ }
25
+
26
+ function setFieldsFromOption(
27
+ option: SearchOption,
28
+ templateId: string,
29
+ fieldName: string | undefined,
30
+ setFieldsDisabled: (
31
+ templateId: string,
32
+ fieldNames: string[],
33
+ disabled: boolean,
34
+ ) => void,
35
+ disabled: boolean,
36
+ ): void {
37
+ try {
38
+ const config = JSON.parse(option.templateConfig) as Record<string, unknown>;
39
+ const fieldNames = getConfigFieldNames(config, fieldName);
40
+ setFieldsDisabled(templateId, fieldNames, disabled);
41
+ } catch (err) {
42
+ console.error(
43
+ disabled
44
+ ? "Failed to parse template config"
45
+ : "Failed to parse template config while enabling fields",
46
+ err,
47
+ );
48
+ }
49
+ }
50
+
51
+ function findOptionByName(
52
+ options: SearchOption[],
53
+ name: string,
54
+ ): SearchOption | undefined {
55
+ return options.find((o) => o?.name === name) ?? undefined;
56
+ }
57
+
58
+ function findOptionByValue(
59
+ options: SearchOption[],
60
+ val: string,
61
+ ): SearchOption | undefined {
62
+ return (
63
+ options.find(
64
+ (o) => (o as SearchOption & { value?: string }).value === val,
65
+ ) ?? undefined
66
+ );
67
+ }
68
+
69
+ export function TextSearchInput({
70
+ fieldName,
71
+ label,
72
+ value,
73
+ setValue,
74
+ options,
75
+ optionsAsValueOptions = false,
76
+ placeholder,
77
+ helpText,
78
+ error,
79
+ setSearchText,
80
+ handleFieldChange,
81
+ readOnly,
82
+ onBlur,
83
+ loading,
84
+ }: Readonly<{
85
+ fieldName?: string;
86
+ label: string;
87
+ value: string;
88
+ setValue: (v: string) => void;
89
+ options: SearchOption[];
90
+ /** When true, options have .value; form stores option.value and displays option.name */
91
+ optionsAsValueOptions?: boolean;
92
+ placeholder?: string | null;
93
+ helpText?: string | null;
94
+ error?: string | null;
95
+ setSearchText: React.Dispatch<React.SetStateAction<string>>;
96
+ handleFieldChange?: (
97
+ templateId: string,
98
+ fieldName: string,
99
+ value: FieldValue,
100
+ ) => void;
101
+ readOnly?: boolean;
102
+ onBlur?: () => void;
103
+ /** When true (e.g. API in progress), show "Checking..." and disable input; when it becomes false, open option tray */
104
+ loading?: boolean;
105
+ }>) {
106
+ const getInitialSelected = () => {
107
+ if (optionsAsValueOptions && value) {
108
+ const byVal = findOptionByValue(options, value);
109
+ return byVal ? byVal.name : value;
110
+ }
111
+ return value ?? null;
112
+ };
113
+ const [selected, setSelected] = React.useState<string | null>(
114
+ getInitialSelected,
115
+ );
116
+
117
+ React.useEffect(() => {
118
+ if (optionsAsValueOptions && value && options.length > 0) {
119
+ const byVal = findOptionByValue(options, value);
120
+ setSelected(byVal ? byVal.name : value);
121
+ } else if (!value) {
122
+ setSelected(null);
123
+ }
124
+ }, [optionsAsValueOptions, value, options]);
125
+
126
+ const { templateId, disabledFields, setFieldsDisabled, templateConfig } =
127
+ useTemplateContext();
128
+
129
+ const { isEditMode } = useFormMode();
130
+
131
+ const appliedOptionRef = React.useRef<string | null>(null);
132
+
133
+ const selfKey =
134
+ templateId && fieldName ? templateFieldKey(templateId, fieldName) : null;
135
+ const isSelfDisabled = selfKey
136
+ ? Boolean(disabledFields?.has(selfKey))
137
+ : false;
138
+ const disabled = Boolean(readOnly || isSelfDisabled || loading);
139
+
140
+ const [open, setOpen] = React.useState(false);
141
+ const prevLoadingRef = React.useRef(false);
142
+ React.useEffect(() => {
143
+ if (prevLoadingRef.current && !loading) {
144
+ setOpen(true);
145
+ }
146
+ prevLoadingRef.current = Boolean(loading);
147
+ }, [loading]);
148
+
149
+ const debouncedSetSearchText = React.useMemo(
150
+ () => debounce((s: string) => setSearchText(s), 500),
151
+ [],
152
+ );
153
+ React.useEffect(() => {
154
+ return () => {
155
+ debouncedSetSearchText.cancel();
156
+ };
157
+ }, [debouncedSetSearchText]);
158
+
159
+ React.useEffect(() => {
160
+ if (optionsAsValueOptions) return;
161
+ if (!selected || !templateId) return;
162
+ const option = findOptionByName(options, selected);
163
+ if (!option) return;
164
+ try {
165
+ const config = JSON.parse(option.templateConfig) as Record<
166
+ string,
167
+ unknown
168
+ >;
169
+ if (config && templateConfig) {
170
+ option.templateConfig = templateConfig;
171
+ }
172
+ applyTemplateConfig(option, handleFieldChange ?? (() => {}));
173
+ appliedOptionRef.current = option.name ?? null;
174
+ if (setFieldsDisabled && !isEditMode) {
175
+ const parsed = JSON.parse(option.templateConfig) as Record<
176
+ string,
177
+ unknown
178
+ >;
179
+ const fieldNamesToDisable = getConfigFieldNames(parsed, fieldName);
180
+ setFieldsDisabled(templateId, fieldNamesToDisable, true);
181
+ }
182
+ } catch (err) {
183
+ console.error("Failed to parse template config", err);
184
+ }
185
+ }, [
186
+ optionsAsValueOptions,
187
+ selected,
188
+ templateId,
189
+ options,
190
+ templateConfig,
191
+ fieldName,
192
+ setFieldsDisabled,
193
+ handleFieldChange,
194
+ isEditMode,
195
+ ]);
196
+
197
+ const handleAutocompleteOnChange = (
198
+ event: React.SyntheticEvent,
199
+ newValue: string | null,
200
+ reason: AutocompleteChangeReason,
201
+ ) => {
202
+ if (reason !== "clear" || event.type !== "click") {
203
+ setSelected(newValue);
204
+ if (optionsAsValueOptions && newValue) {
205
+ const option = findOptionByName(options, newValue);
206
+ setValue(
207
+ option && "value" in option && option.value != null
208
+ ? option.value
209
+ : newValue,
210
+ );
211
+ } else {
212
+ setValue(newValue ?? "");
213
+ }
214
+ return;
215
+ }
216
+ setSelected(null);
217
+ setValue("");
218
+ const appliedName = appliedOptionRef.current;
219
+ if (
220
+ !optionsAsValueOptions &&
221
+ templateId &&
222
+ setFieldsDisabled &&
223
+ appliedName
224
+ ) {
225
+ const option = findOptionByName(options, appliedName);
226
+ if (option) {
227
+ setFieldsFromOption(
228
+ option,
229
+ templateId,
230
+ fieldName,
231
+ setFieldsDisabled,
232
+ false,
233
+ );
234
+ }
235
+ appliedOptionRef.current = null;
236
+ }
237
+ };
238
+
239
+ const handleInputChange = (
240
+ event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
241
+ ) => {
242
+ const newVal = event.target.value;
243
+ setValue(newVal);
244
+ debouncedSetSearchText(newVal);
245
+
246
+ const appliedName = appliedOptionRef.current;
247
+ if (
248
+ !optionsAsValueOptions &&
249
+ templateId &&
250
+ setFieldsDisabled &&
251
+ appliedName
252
+ ) {
253
+ const option = findOptionByName(options, appliedName);
254
+ if (option) {
255
+ setFieldsFromOption(
256
+ option,
257
+ templateId,
258
+ fieldName,
259
+ setFieldsDisabled,
260
+ false,
261
+ );
262
+ }
263
+ appliedOptionRef.current = null;
264
+ }
265
+ };
266
+
267
+ const shouldShowInfoMessage = !isEditMode && selected;
268
+ const helperTextFinal = (() => {
269
+ if (loading) return "Checking...";
270
+ if (shouldShowInfoMessage)
271
+ return "Existing data descriptions are not editable during creation.";
272
+ return helpText;
273
+ })();
274
+
275
+ return (
276
+ <Box sx={{ width: "50%" }}>
277
+ <Autocomplete
278
+ freeSolo={!selected}
279
+ options={options.map((o) => o.name)}
280
+ value={selected ?? ""}
281
+ onChange={handleAutocompleteOnChange}
282
+ disabled={disabled}
283
+ open={open}
284
+ onOpen={() => setOpen(true)}
285
+ onClose={() => setTimeout(() => setOpen(false), 0)}
286
+ noOptionsText="No option"
287
+ renderInput={(params) => (
288
+ <TextField
289
+ {...params}
290
+ label={label}
291
+ value={value ?? ""}
292
+ onChange={handleInputChange}
293
+ onBlur={onBlur}
294
+ placeholder={placeholder ?? undefined}
295
+ fullWidth
296
+ error={Boolean(error)}
297
+ helperText={error ?? helperTextFinal ?? " "}
298
+ disabled={disabled}
299
+ slotProps={{
300
+ formHelperText: {
301
+ sx: { marginLeft: 0 },
302
+ },
303
+ }}
304
+ />
305
+ )}
306
+ />
307
+ </Box>
308
+ );
309
+ }
@@ -0,0 +1,4 @@
1
+ export { MultiTextSearchInput } from "./MultiTextSearchInput";
2
+ export { SearchInputWrapper } from "./SearchInputWrapper";
3
+ export { TextSearchInput } from "./TextSearchInput";
4
+ export { useSearchInput } from "./useSearchInput";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Search query keys for template input search (matches API TemplateInputSearchQueryKey).
3
+ * Use this enum instead of string literals in switches and comparisons.
4
+ */
5
+ export enum TemplateSearchQueryKey {
6
+ GetSourceEndpoints = "getSourceEndpoints",
7
+ GetDataDescriptions = "getDataDescriptions",
8
+ CheckUniqueIdentifier = "checkUniqueIdentifier",
9
+ }
10
+
11
+ /**
12
+ * Map template/API query name variants (e.g. from process template input.queryName)
13
+ * to the canonical key used by the client. Add new variants here when the API
14
+ * returns prefixed or alternate names.
15
+ */
16
+ export const TEMPLATE_QUERY_NAME_TO_KEY: Record<
17
+ string,
18
+ TemplateSearchQueryKey
19
+ > = {
20
+ Templates_GetSourceEndpoints: TemplateSearchQueryKey.GetSourceEndpoints,
21
+ Templates_GetDataDescriptions: TemplateSearchQueryKey.GetDataDescriptions,
22
+ Templates_CheckUniqueIdentifier: TemplateSearchQueryKey.CheckUniqueIdentifier,
23
+ };
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import type { GetFieldValuesQuery } from "@evenicanpm/admin-core/api/e4/graphqlRequestSdk";
4
+ import { queryFn } from "@evenicanpm/admin-integrate/api/templates/queries/get-field-values.server";
5
+ import { debounce } from "lodash";
6
+ import * as React from "react";
7
+
8
+ export type FieldValueOption = { name: string; value: string };
9
+
10
+ function dataToOptions(
11
+ data: GetFieldValuesQuery | undefined,
12
+ ): FieldValueOption[] {
13
+ const values = data?.getFieldValues?.values ?? [];
14
+ return values
15
+ .filter(
16
+ (v): v is NonNullable<typeof v> => v?.name != null && v?.value != null,
17
+ )
18
+ .map((v) => ({ name: String(v.name), value: String(v.value) }));
19
+ }
20
+
21
+ export function useFieldValuesOptions(
22
+ fieldName: string | undefined,
23
+ validationQuery: string | undefined,
24
+ searchText: string,
25
+ ) {
26
+ const [options, setOptions] = React.useState<FieldValueOption[]>([]);
27
+ const [loading, setLoading] = React.useState(false);
28
+
29
+ const fetchOptions = React.useMemo(
30
+ () =>
31
+ debounce((field: string, query: string, value: string) => {
32
+ queryFn({ input: { fieldName: field, validationQuery: query, value } })
33
+ .then((data) => setOptions(dataToOptions(data)))
34
+ .catch(() => setOptions([]))
35
+ .finally(() => setLoading(false));
36
+ }, 400),
37
+ [],
38
+ );
39
+
40
+ React.useEffect(() => {
41
+ if (!fieldName || !validationQuery) {
42
+ setOptions([]);
43
+ return;
44
+ }
45
+ const trimmed = searchText.trim();
46
+ if (!trimmed) {
47
+ setOptions([]);
48
+ setLoading(false);
49
+ fetchOptions.cancel();
50
+ return;
51
+ }
52
+ setLoading(true);
53
+ fetchOptions(fieldName, validationQuery, trimmed);
54
+ return () => fetchOptions.cancel();
55
+ }, [fieldName, validationQuery, searchText, fetchOptions]);
56
+
57
+ return { options, loading };
58
+ }
@@ -0,0 +1,79 @@
1
+ import type {
2
+ CheckUniqueIdentifierQuery,
3
+ GetSourceEndpointsQuery,
4
+ } from "@evenicanpm/admin-core/api/e4/graphqlRequestSdk";
5
+ import { templateInputSearch } from "@evenicanpm/admin-integrate/api/templates/queries";
6
+ import { useSearchParams } from "next/navigation";
7
+ import React from "react";
8
+ import type { SearchOption } from "../../types";
9
+ import { ReservedFieldName } from "../field-names";
10
+ import type {
11
+ SearchInputExtendedSetValue,
12
+ SearchInputWrapperProps,
13
+ } from "../form-type";
14
+ import { InputType } from "../input-type";
15
+ import { TemplateSearchQueryKey } from "./search-query-key";
16
+
17
+ type UseSearchInputProps = Pick<
18
+ SearchInputWrapperProps,
19
+ "inputType" | "queryName" | "setValue"
20
+ >;
21
+
22
+ export function useSearchInput({
23
+ inputType,
24
+ queryName,
25
+ setValue,
26
+ }: UseSearchInputProps) {
27
+ const searchParams = useSearchParams();
28
+
29
+ const [searchText, setSearchText] = React.useState("");
30
+ const [options, setOptions] = React.useState<SearchOption[]>([]);
31
+ const [isUnique, setIsUnique] = React.useState<boolean | null>(null);
32
+
33
+ const { data } = templateInputSearch.useData(
34
+ inputType === InputType.TEXT_UNIQUE_IDENTIFIER
35
+ ? { uniqueIdentifier: searchText }
36
+ : {
37
+ input: {
38
+ processTemplateId: searchParams.get("template")?.toString() ?? null,
39
+ searchText,
40
+ },
41
+ },
42
+ queryName,
43
+ );
44
+
45
+ React.useEffect(() => {
46
+ if (!data) return;
47
+
48
+ switch (queryName) {
49
+ case TemplateSearchQueryKey.GetSourceEndpoints: {
50
+ const typed = data as GetSourceEndpointsQuery;
51
+ setOptions(
52
+ typed.getSourceEndpoints?.map((e) => ({
53
+ name: e?.name ?? "",
54
+ templateConfig: e?.templateConfig ?? "",
55
+ })) ?? [],
56
+ );
57
+ break;
58
+ }
59
+
60
+ case TemplateSearchQueryKey.CheckUniqueIdentifier: {
61
+ const typed = data as CheckUniqueIdentifierQuery;
62
+ const unique = typed.checkUniqueIdentifier?.isUnique ?? null;
63
+ setIsUnique(unique);
64
+ (setValue as SearchInputExtendedSetValue | undefined)?.(
65
+ unique,
66
+ ReservedFieldName.IsUnique,
67
+ );
68
+ break;
69
+ }
70
+ }
71
+ }, [data]);
72
+
73
+ return {
74
+ searchText,
75
+ setSearchText,
76
+ options,
77
+ isUnique,
78
+ };
79
+ }