@orchestrator-ui/orchestrator-ui-components 5.9.0 → 6.1.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 (61) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-lint.log +10 -1
  3. package/.turbo/turbo-test.log +10 -10
  4. package/CHANGELOG.md +12 -0
  5. package/__mocks__/@copilotkit/react-core.js +9 -0
  6. package/__mocks__/@copilotkit/react-ui.js +11 -0
  7. package/dist/index.d.ts +1432 -2
  8. package/dist/index.js +3006 -28
  9. package/dist/index.js.map +1 -1
  10. package/package.json +5 -1
  11. package/src/components/WfoAgent/FilterDisplay/FilterDisplay.tsx +182 -0
  12. package/src/components/WfoAgent/FilterDisplay/index.ts +1 -0
  13. package/src/components/WfoAgent/FilterDisplay/styles.ts +62 -0
  14. package/src/components/WfoAgent/WfoAgent/WfoAgent.tsx +100 -0
  15. package/src/components/WfoAgent/WfoAgent/index.ts +1 -0
  16. package/src/components/WfoAgent/index.ts +2 -0
  17. package/src/components/WfoSearchPage/WfoConditionRow/WfoConditionRow.tsx +388 -0
  18. package/src/components/WfoSearchPage/WfoConditionRow/WfoFieldSelector.tsx +43 -0
  19. package/src/components/WfoSearchPage/WfoConditionRow/WfoOperatorSelector.tsx +100 -0
  20. package/src/components/WfoSearchPage/WfoConditionRow/WfoPathChips.tsx +193 -0
  21. package/src/components/WfoSearchPage/WfoConditionRow/WfoPathSelector.tsx +54 -0
  22. package/src/components/WfoSearchPage/WfoConditionRow/WfoRenderFunctions.tsx +107 -0
  23. package/src/components/WfoSearchPage/WfoConditionRow/WfoSelectedPathDisplay.tsx +75 -0
  24. package/src/components/WfoSearchPage/WfoConditionRow/index.ts +11 -0
  25. package/src/components/WfoSearchPage/WfoConditionRow/types.ts +84 -0
  26. package/src/components/WfoSearchPage/WfoConditionRow/utils.ts +63 -0
  27. package/src/components/WfoSearchPage/WfoFilterGroup/WfoFilterGroup.tsx +238 -0
  28. package/src/components/WfoSearchPage/WfoFilterGroup/index.ts +1 -0
  29. package/src/components/WfoSearchPage/WfoSearch/WfoSearch.tsx +453 -0
  30. package/src/components/WfoSearchPage/WfoSearch/index.ts +1 -0
  31. package/src/components/WfoSearchPage/WfoSearchResults/WfoHighlightedText.tsx +63 -0
  32. package/src/components/WfoSearchPage/WfoSearchResults/WfoPathBreadcrumb.tsx +80 -0
  33. package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchEmptyState.tsx +24 -0
  34. package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchLoadingState.tsx +24 -0
  35. package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchMetadataHeader.tsx +24 -0
  36. package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchPaginationInfo.tsx +107 -0
  37. package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResultItem.tsx +157 -0
  38. package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResults.tsx +65 -0
  39. package/src/components/WfoSearchPage/WfoSearchResults/WfoSubscriptionDetailModal.tsx +55 -0
  40. package/src/components/WfoSearchPage/WfoSearchResults/index.ts +10 -0
  41. package/src/components/WfoSearchPage/WfoValueControl/WfoValueControl.tsx +247 -0
  42. package/src/components/WfoSearchPage/WfoValueControl/index.ts +1 -0
  43. package/src/components/WfoSearchPage/constants.ts +17 -0
  44. package/src/components/WfoSearchPage/index.ts +6 -0
  45. package/src/components/WfoSearchPage/utils.ts +271 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/configuration/version.ts +1 -1
  48. package/src/hooks/useDebounce.ts +21 -0
  49. package/src/hooks/usePathAutoComplete.ts +133 -0
  50. package/src/hooks/useSearch.ts +83 -0
  51. package/src/hooks/useSearchPagination.ts +148 -0
  52. package/src/hooks/useUrlParams.ts +120 -0
  53. package/src/icons/WfoPencil.tsx +23 -4
  54. package/src/messages/en-GB.json +79 -1
  55. package/src/messages/nl-NL.json +2 -1
  56. package/src/rtk/endpoints/index.ts +1 -0
  57. package/src/rtk/endpoints/search.ts +90 -0
  58. package/src/types/index.ts +1 -0
  59. package/src/types/search.ts +215 -0
  60. package/src/utils/optionalArray.spec.ts +27 -0
  61. package/src/utils/optionalArray.ts +5 -0
@@ -0,0 +1,247 @@
1
+ import React, { FC } from 'react';
2
+
3
+ import moment from 'moment';
4
+ import { useTranslations } from 'next-intl';
5
+
6
+ import {
7
+ EuiComboBox,
8
+ EuiDatePicker,
9
+ EuiFieldNumber,
10
+ EuiFieldText,
11
+ EuiFlexGroup,
12
+ EuiFlexItem,
13
+ EuiFormHelpText,
14
+ EuiIcon,
15
+ EuiText,
16
+ } from '@elastic/eui';
17
+
18
+ import { useOrchestratorTheme } from '@/hooks';
19
+ import { PathInfo } from '@/types';
20
+
21
+ interface ValueControlProps {
22
+ pathInfo: PathInfo | null;
23
+ operator: string;
24
+ value: unknown;
25
+ onChange: (value: unknown) => void;
26
+ }
27
+
28
+ export const ValueControl: FC<ValueControlProps> = ({
29
+ pathInfo,
30
+ operator,
31
+ value,
32
+ onChange,
33
+ }) => {
34
+ const t = useTranslations('search.page');
35
+ const { theme } = useOrchestratorTheme();
36
+
37
+ if (!pathInfo || !operator) return null;
38
+
39
+ const schema = pathInfo.value_schema[operator];
40
+ if (!schema || schema.kind === 'none') return null;
41
+
42
+ if (pathInfo.type === 'string') {
43
+ if (operator === 'like') {
44
+ const handleLikeChange = (newValue: string) => {
45
+ onChange(newValue);
46
+ };
47
+
48
+ const ensureWildcards = (inputValue: string) => {
49
+ if (!inputValue) return inputValue;
50
+ if (!inputValue.includes('%') && !inputValue.includes('_')) {
51
+ return `%${inputValue}%`;
52
+ }
53
+ return inputValue;
54
+ };
55
+
56
+ const currentValue = String(value || '');
57
+ const hasWildcards =
58
+ currentValue.includes('%') || currentValue.includes('_');
59
+
60
+ return (
61
+ <>
62
+ <EuiFieldText
63
+ placeholder="Enter pattern (% = any chars, _ = single char)"
64
+ value={currentValue}
65
+ onChange={(event) =>
66
+ handleLikeChange(event.target.value)
67
+ }
68
+ onBlur={(event) => {
69
+ const finalValue = ensureWildcards(
70
+ event.target.value,
71
+ );
72
+ if (finalValue !== event.target.value) {
73
+ handleLikeChange(finalValue);
74
+ }
75
+ }}
76
+ prepend={<EuiIcon type="search" />}
77
+ />
78
+ <EuiFormHelpText>
79
+ {hasWildcards ? (
80
+ <span>
81
+ <EuiIcon
82
+ type="checkInCircleFilled"
83
+ color="success"
84
+ size="s"
85
+ />{' '}
86
+ Pattern with wildcards:{' '}
87
+ <strong>{currentValue}</strong>
88
+ </span>
89
+ ) : (
90
+ <span>
91
+ Will search for:{' '}
92
+ <strong>%{currentValue || 'your-text'}%</strong>{' '}
93
+ (auto-wrapped with wildcards)
94
+ </span>
95
+ )}
96
+ </EuiFormHelpText>
97
+ </>
98
+ );
99
+ }
100
+
101
+ if (pathInfo.example_values && pathInfo.example_values.length > 0) {
102
+ const options = pathInfo.example_values.map((val) => ({
103
+ label: val,
104
+ value: val,
105
+ }));
106
+ return (
107
+ <EuiComboBox
108
+ placeholder={t('selectOrEnterValue')}
109
+ options={options}
110
+ selectedOptions={
111
+ value
112
+ ? [{ label: String(value), value: String(value) }]
113
+ : []
114
+ }
115
+ onChange={(selected) => onChange(selected[0]?.value || '')}
116
+ singleSelection={{ asPlainText: true }}
117
+ isClearable
118
+ />
119
+ );
120
+ }
121
+ return (
122
+ <EuiFieldText
123
+ placeholder={t('enterValue')}
124
+ value={String(value || '')}
125
+ onChange={(event) => onChange(event.target.value)}
126
+ />
127
+ );
128
+ }
129
+
130
+ if (pathInfo.type === 'number') {
131
+ if (operator === 'between') {
132
+ const betweenValue = (value as {
133
+ start: number | string;
134
+ end: number | string;
135
+ }) || { start: '', end: '' };
136
+ return (
137
+ <EuiFlexGroup gutterSize="s" alignItems="center">
138
+ <EuiFlexItem>
139
+ <EuiFieldNumber
140
+ placeholder={t('fromNumber')}
141
+ value={betweenValue.start}
142
+ onChange={(event) =>
143
+ onChange({
144
+ ...betweenValue,
145
+ start: parseFloat(event.target.value) || '',
146
+ })
147
+ }
148
+ />
149
+ </EuiFlexItem>
150
+ <EuiFlexItem grow={false}>
151
+ <EuiText size="s" color={theme.colors.textSubdued}>
152
+ to
153
+ </EuiText>
154
+ </EuiFlexItem>
155
+ <EuiFlexItem>
156
+ <EuiFieldNumber
157
+ placeholder={t('toNumber')}
158
+ value={betweenValue.end}
159
+ onChange={(event) =>
160
+ onChange({
161
+ ...betweenValue,
162
+ end: parseFloat(event.target.value) || '',
163
+ })
164
+ }
165
+ />
166
+ </EuiFlexItem>
167
+ </EuiFlexGroup>
168
+ );
169
+ }
170
+ return (
171
+ <EuiFieldNumber
172
+ placeholder={t('enterNumber')}
173
+ value={
174
+ value !== undefined && value !== null ? Number(value) : ''
175
+ }
176
+ onChange={(event) =>
177
+ onChange(parseFloat(event.target.value) || '')
178
+ }
179
+ />
180
+ );
181
+ }
182
+
183
+ if (pathInfo.type === 'datetime') {
184
+ if (operator === 'between') {
185
+ const betweenValue = (value as {
186
+ start: string | null;
187
+ end: string | null;
188
+ }) || { start: null, end: null };
189
+ return (
190
+ <EuiFlexGroup gutterSize="s" alignItems="center">
191
+ <EuiFlexItem>
192
+ <EuiDatePicker
193
+ selected={
194
+ betweenValue.start
195
+ ? moment(betweenValue.start)
196
+ : null
197
+ }
198
+ onChange={(date) =>
199
+ onChange({
200
+ ...betweenValue,
201
+ start: date?.toISOString(),
202
+ })
203
+ }
204
+ showTimeSelect
205
+ dateFormat="yyyy-MM-dd HH:mm"
206
+ placeholderText={t('fromDate')}
207
+ />
208
+ </EuiFlexItem>
209
+ <EuiFlexItem grow={false}>
210
+ <EuiText size="s" color={theme.colors.textSubdued}>
211
+ {t('valueControlTo')}
212
+ </EuiText>
213
+ </EuiFlexItem>
214
+ <EuiFlexItem>
215
+ <EuiDatePicker
216
+ selected={
217
+ betweenValue.end
218
+ ? moment(betweenValue.end)
219
+ : null
220
+ }
221
+ onChange={(date) =>
222
+ onChange({
223
+ ...betweenValue,
224
+ end: date?.toISOString(),
225
+ })
226
+ }
227
+ showTimeSelect
228
+ dateFormat="yyyy-MM-dd HH:mm"
229
+ placeholderText={t('toDate')}
230
+ />
231
+ </EuiFlexItem>
232
+ </EuiFlexGroup>
233
+ );
234
+ }
235
+ return (
236
+ <EuiDatePicker
237
+ selected={value ? moment(String(value)) : null}
238
+ onChange={(date) => onChange(date?.toISOString())}
239
+ showTimeSelect
240
+ dateFormat="yyyy-MM-dd HH:mm"
241
+ placeholderText={t('selectDateAndTime')}
242
+ />
243
+ );
244
+ }
245
+
246
+ return null;
247
+ };
@@ -0,0 +1 @@
1
+ export { ValueControl } from './WfoValueControl';
@@ -0,0 +1,17 @@
1
+ export const DEFAULT_PAGE_SIZE = 5;
2
+ export const DEFAULT_DEBOUNCE_DELAY = 300;
3
+ export const SMALL_RESULT_THRESHOLD = 10;
4
+
5
+ export const LAYOUT_RATIOS = {
6
+ RESULTS_GROW: 2,
7
+ DETAIL_GROW: 3,
8
+ } as const;
9
+
10
+ export const VALID_ENTITY_TYPES = [
11
+ 'SUBSCRIPTION',
12
+ 'PRODUCT',
13
+ 'WORKFLOW',
14
+ 'PROCESS',
15
+ ] as const;
16
+
17
+ export const DEFAULT_ENTITY_TAB = 'SUBSCRIPTION';
@@ -0,0 +1,6 @@
1
+ export * from './WfoSearch';
2
+ export * from './WfoSearchResults';
3
+ export * from './WfoFilterGroup';
4
+ export * from './WfoConditionRow';
5
+ export * from './WfoValueControl';
6
+ export * from './utils';
@@ -0,0 +1,271 @@
1
+ import {
2
+ AnySearchResult,
3
+ Condition,
4
+ EntityKind,
5
+ Group,
6
+ ProcessSearchResult,
7
+ ProductSearchResult,
8
+ SubscriptionSearchResult,
9
+ WorkflowSearchResult,
10
+ } from '@/types';
11
+
12
+ export function isSubscriptionSearchResult(
13
+ item: AnySearchResult,
14
+ ): item is SubscriptionSearchResult {
15
+ return 'subscription' in item && typeof item.subscription === 'object';
16
+ }
17
+
18
+ export function isProcessSearchResult(
19
+ item: AnySearchResult,
20
+ ): item is ProcessSearchResult {
21
+ return 'process' in item && typeof item.process === 'object';
22
+ }
23
+
24
+ export function isProductSearchResult(
25
+ item: AnySearchResult,
26
+ ): item is ProductSearchResult {
27
+ return 'product' in item && typeof item.product === 'object';
28
+ }
29
+
30
+ export function isWorkflowSearchResult(
31
+ item: AnySearchResult,
32
+ ): item is WorkflowSearchResult {
33
+ return 'workflow' in item && typeof item.workflow === 'object';
34
+ }
35
+
36
+ export const isCondition = (item: Group | Condition): item is Condition => {
37
+ return 'path' in item && 'condition' in item;
38
+ };
39
+
40
+ const ENDPOINT_PATHS: Record<EntityKind, string> = {
41
+ PROCESS: 'processes',
42
+ PRODUCT: 'products',
43
+ WORKFLOW: 'workflows',
44
+ SUBSCRIPTION: 'subscriptions',
45
+ };
46
+
47
+ export const getEndpointPath = (entityType: EntityKind): string => {
48
+ return ENDPOINT_PATHS[entityType] || ENDPOINT_PATHS.SUBSCRIPTION;
49
+ };
50
+
51
+ export const getDisplayText = (item: AnySearchResult): string => {
52
+ if (isSubscriptionSearchResult(item)) {
53
+ return item.subscription.description || 'Subscription';
54
+ }
55
+ if (isProcessSearchResult(item)) {
56
+ return item.process.workflowName;
57
+ }
58
+ if (isProductSearchResult(item)) {
59
+ return item.product.name;
60
+ }
61
+ if (isWorkflowSearchResult(item)) {
62
+ return item.workflow.name;
63
+ }
64
+ return 'Unknown result type';
65
+ };
66
+
67
+ export const getRecordId = (result: AnySearchResult): string => {
68
+ if (isSubscriptionSearchResult(result)) {
69
+ return result.subscription.subscription_id;
70
+ }
71
+ if (isProductSearchResult(result)) {
72
+ return result.product.product_id;
73
+ }
74
+ if (isProcessSearchResult(result)) {
75
+ return result.process.processId;
76
+ }
77
+ if (isWorkflowSearchResult(result)) {
78
+ return result.workflow.name;
79
+ }
80
+ return '';
81
+ };
82
+
83
+ export const findResultIndexById = (
84
+ results: AnySearchResult[],
85
+ recordId: string,
86
+ ): number => {
87
+ return results.findIndex((result) => {
88
+ if (isSubscriptionSearchResult(result)) {
89
+ return result.subscription.subscription_id === recordId;
90
+ }
91
+ if (isProductSearchResult(result)) {
92
+ return result.product.product_id === recordId;
93
+ }
94
+ if (isProcessSearchResult(result)) {
95
+ return result.process.processId === recordId;
96
+ }
97
+ if (isWorkflowSearchResult(result)) {
98
+ return result.workflow.name === recordId;
99
+ }
100
+ return false;
101
+ });
102
+ };
103
+
104
+ export const getDetailUrl = (
105
+ result: AnySearchResult,
106
+ baseUrl: string,
107
+ ): string => {
108
+ if (isSubscriptionSearchResult(result)) {
109
+ return `${baseUrl}/subscriptions/${result.subscription.subscription_id}`;
110
+ }
111
+ if (isProductSearchResult(result)) {
112
+ return `${baseUrl}/products/${result.product.product_id}`;
113
+ }
114
+ if (isProcessSearchResult(result)) {
115
+ return `${baseUrl}/processes/${result.process.processId}`;
116
+ }
117
+ if (isWorkflowSearchResult(result)) {
118
+ return `${baseUrl}/workflows/${result.workflow.name}`;
119
+ }
120
+ return '#';
121
+ };
122
+
123
+ export const getDescription = (result: AnySearchResult): string => {
124
+ if (isSubscriptionSearchResult(result)) {
125
+ return result.subscription.description;
126
+ }
127
+ if (isProductSearchResult(result)) {
128
+ return result.product.description || result.product.name;
129
+ }
130
+ if (isWorkflowSearchResult(result)) {
131
+ return result.workflow.description || result.workflow.name;
132
+ }
133
+ if (isProcessSearchResult(result)) {
134
+ return result.process.workflowName;
135
+ }
136
+ return 'Unknown';
137
+ };
138
+
139
+ export const ENTITY_TABS = [
140
+ { id: 'SUBSCRIPTION' as const, label: 'Subscriptions' },
141
+ { id: 'PRODUCT' as const, label: 'Products' },
142
+ { id: 'WORKFLOW' as const, label: 'Workflows' },
143
+ { id: 'PROCESS' as const, label: 'Processes' },
144
+ ];
145
+
146
+ interface ThemeColors {
147
+ success: string;
148
+ primary: string;
149
+ warning: string;
150
+ accent: string;
151
+ textSubdued: string;
152
+ }
153
+
154
+ interface Theme {
155
+ colors: ThemeColors;
156
+ }
157
+
158
+ const TYPE_COLOR_MAP: Record<string, keyof ThemeColors> = {
159
+ string: 'success',
160
+ number: 'primary',
161
+ boolean: 'warning',
162
+ datetime: 'accent',
163
+ component: 'primary',
164
+ };
165
+
166
+ export const getTypeColor = (type: string, theme: Theme): string => {
167
+ const colorKey = TYPE_COLOR_MAP[type.toLowerCase()];
168
+ return colorKey ? theme.colors[colorKey] : theme.colors.textSubdued;
169
+ };
170
+
171
+ interface PathInfo {
172
+ type?: string;
173
+ [key: string]: unknown;
174
+ }
175
+
176
+ interface OperatorDisplay {
177
+ symbol: string;
178
+ description: string;
179
+ }
180
+
181
+ const OPERATOR_MAP: Record<string, OperatorDisplay> = {
182
+ eq: { symbol: '=', description: 'equals' },
183
+ neq: { symbol: '≠', description: 'not equals' },
184
+ lt: { symbol: '<', description: 'less than' },
185
+ lte: { symbol: '≤', description: 'less than or equal to' },
186
+ gt: { symbol: '>', description: 'greater than' },
187
+ gte: { symbol: '≥', description: 'greater than or equal to' },
188
+ between: { symbol: '⟷', description: 'between (range)' },
189
+ has_component: { symbol: '✓', description: 'has component' },
190
+ not_has_component: { symbol: '✗', description: 'does not have component' },
191
+ };
192
+
193
+ const BOOLEAN_OPERATOR_MAP: Record<string, OperatorDisplay> = {
194
+ eq: { symbol: '✓', description: 'is true' },
195
+ neq: { symbol: '✗', description: 'is false' },
196
+ };
197
+
198
+ export const getOperatorDisplay = (
199
+ op: string,
200
+ selectedPathInfo?: PathInfo,
201
+ ): OperatorDisplay => {
202
+ if (selectedPathInfo?.type === 'boolean' && BOOLEAN_OPERATOR_MAP[op]) {
203
+ return BOOLEAN_OPERATOR_MAP[op];
204
+ }
205
+ return OPERATOR_MAP[op] || { symbol: op, description: op };
206
+ };
207
+
208
+ export const getButtonColor = (
209
+ op: string,
210
+ pathInfo: PathInfo | null,
211
+ condition: Condition,
212
+ ): 'primary' | 'text' => {
213
+ if (pathInfo?.type === 'boolean') {
214
+ const isSelected =
215
+ op === 'eq'
216
+ ? condition.condition.value === true
217
+ : condition.condition.value === false;
218
+ return isSelected ? 'primary' : 'text';
219
+ }
220
+ return condition.condition.op === op ? 'primary' : 'text';
221
+ };
222
+
223
+ export const getButtonFill = (
224
+ op: string,
225
+ pathInfo: PathInfo | null,
226
+ condition: Condition,
227
+ ): boolean => {
228
+ if (pathInfo?.type === 'boolean') {
229
+ return op === 'eq'
230
+ ? condition.condition.value === true
231
+ : condition.condition.value === false;
232
+ }
233
+ return condition.condition.op === op;
234
+ };
235
+
236
+ export const isFilterValid = (group: Group): boolean => {
237
+ return group.children.every((child) => {
238
+ if (isCondition(child)) {
239
+ return (
240
+ child.path &&
241
+ child.condition.op &&
242
+ child.condition.value !== undefined
243
+ );
244
+ }
245
+ return isFilterValid(child);
246
+ });
247
+ };
248
+
249
+ interface SearchQuery {
250
+ text?: string;
251
+ }
252
+
253
+ export const buildSearchParams = (
254
+ debouncedQuery: SearchQuery | string,
255
+ selectedEntityTab: EntityKind,
256
+ filterGroup: Group,
257
+ pageSize: number,
258
+ ) => {
259
+ const queryText =
260
+ typeof debouncedQuery === 'string'
261
+ ? debouncedQuery
262
+ : debouncedQuery?.text?.trim() || '';
263
+
264
+ return {
265
+ action: 'select' as const,
266
+ entity_type: selectedEntityTab,
267
+ query: queryText || '',
268
+ filters: filterGroup?.children.length > 0 ? filterGroup : undefined,
269
+ limit: pageSize,
270
+ };
271
+ };
@@ -36,3 +36,5 @@ export * from './WfoInlineNoteEdit';
36
36
  export * from './WfoTableCodeBlock';
37
37
  export * from './WfoInlineEdit';
38
38
  export * from './WfoPydanticForm';
39
+ export * from './WfoSearchPage';
40
+ export * from './WfoAgent';
@@ -1 +1 @@
1
- export const ORCHESTRATOR_UI_LIBRARY_VERSION = '5.9.0';
1
+ export const ORCHESTRATOR_UI_LIBRARY_VERSION = '6.1.0';
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export function useDebounce<T>(value: T, delay: number): T {
4
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
5
+
6
+ useEffect(() => {
7
+ if (delay <= 0) {
8
+ setDebouncedValue(value);
9
+ return;
10
+ }
11
+ const handler = setTimeout(() => {
12
+ setDebouncedValue(value);
13
+ }, delay);
14
+
15
+ return () => {
16
+ clearTimeout(handler);
17
+ };
18
+ }, [value, delay]);
19
+
20
+ return debouncedValue;
21
+ }