@orchestrator-ui/orchestrator-ui-components 8.9.1 → 8.9.3

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 (38) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +291 -15
  4. package/CHANGELOG.md +16 -0
  5. package/dist/index.d.ts +17 -4
  6. package/dist/index.js +1662 -1463
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/WfoKeyValueTable/styles.ts +24 -0
  10. package/src/components/WfoPydanticForm/fields/WfoLabel.tsx +5 -1
  11. package/src/components/WfoPydanticForm/fields/styles.ts +12 -3
  12. package/src/components/WfoSearchPage/utils.ts +1 -0
  13. package/src/components/WfoSettingsModal/WfoInformationModal.tsx +3 -2
  14. package/src/components/WfoTable/WfoAdvancedTable/WfoAdvancedTable.tsx +5 -1
  15. package/src/components/WfoTable/WfoStructuredSearchTable/WfoApplyFilterButton.tsx +37 -0
  16. package/src/components/WfoTable/WfoStructuredSearchTable/WfoDebounceCountdown.tsx +58 -0
  17. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.spec.tsx +196 -0
  18. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +71 -50
  19. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +24 -90
  20. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRangeEditor.tsx +0 -1
  21. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +2 -2
  22. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +9 -13
  23. package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +7 -0
  24. package/src/components/WfoTable/WfoStructuredSearchTable/useSearchWithDebouncedCallback.spec.tsx +94 -0
  25. package/src/components/WfoTable/WfoStructuredSearchTable/utils.ts +109 -1
  26. package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.tsx +3 -2
  27. package/src/components/WfoTable/WfoTableSettingsModal/styles.ts +10 -0
  28. package/src/configuration/version.ts +1 -1
  29. package/src/hooks/index.ts +1 -0
  30. package/src/hooks/useDebouncedCallback.ts +48 -0
  31. package/src/hooks/useGetPydanticFormsConfig.tsx +2 -1
  32. package/src/hooks/usePathAutoComplete.spec.tsx +94 -9
  33. package/src/hooks/usePathAutoComplete.ts +10 -15
  34. package/src/hooks/useSearchPagination.ts +1 -1
  35. package/src/messages/en-GB.json +3 -1
  36. package/src/messages/nl-NL.json +3 -1
  37. package/src/pages/WfoSearchPocPage.tsx +8 -8
  38. package/src/types/search.ts +1 -2
@@ -1,15 +1,15 @@
1
- import { renderHook, waitFor } from '@testing-library/react';
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
2
 
3
- import { EntityKind } from '@/types';
3
+ import { useSearchPathsQuery } from '@/rtk/endpoints';
4
+ import { EntityKind, PathAutocompleteResponse } from '@/types';
4
5
 
5
- import { useFieldsPathInfo } from './usePathAutoComplete';
6
+ import { useFieldsPathInfo, usePathAutocomplete } from './usePathAutoComplete';
6
7
 
7
8
  const fetchPathsMock = jest.fn();
8
9
 
9
- jest.mock('@/rtk/endpoints', () => ({
10
- useSearchPathsQuery: jest.fn(),
11
- useLazySearchPathsQuery: () => [fetchPathsMock],
12
- useSearchDefinitionsQuery: () => ({
10
+ jest.mock('@/rtk/endpoints', () => {
11
+ // Stable like RTK Query's cached data: the hooks depend on it in effects.
12
+ const definitionsResult = {
13
13
  data: {
14
14
  boolean: {
15
15
  operators: ['eq', 'neq'],
@@ -17,8 +17,13 @@ jest.mock('@/rtk/endpoints', () => ({
17
17
  },
18
18
  },
19
19
  isError: false,
20
- }),
21
- }));
20
+ };
21
+ return {
22
+ useSearchPathsQuery: jest.fn(),
23
+ useLazySearchPathsQuery: () => [fetchPathsMock],
24
+ useSearchDefinitionsQuery: () => definitionsResult,
25
+ };
26
+ });
22
27
 
23
28
  describe('useFieldsPathInfo', () => {
24
29
  beforeEach(() => {
@@ -62,3 +67,83 @@ describe('useFieldsPathInfo', () => {
62
67
  expect(fetchPathsMock).toHaveBeenCalledTimes(1);
63
68
  });
64
69
  });
70
+
71
+ describe('usePathAutocomplete', () => {
72
+ const SAPS_RESPONSE = {
73
+ leaves: [],
74
+ components: [{ name: 'saps', ui_types: ['component'], paths: ['saps.port', 'saps.vlan'] }],
75
+ };
76
+ const EMPTY_RESPONSE: PathAutocompleteResponse = { leaves: [], components: [] };
77
+
78
+ // Only the fields the hook reads; the full RTK Query result type is far larger.
79
+ const mockSearchPaths = (
80
+ getData: (args: { q: string }, options?: { skip?: boolean }) => PathAutocompleteResponse | undefined,
81
+ ) =>
82
+ jest.mocked(useSearchPathsQuery).mockImplementation(((args: { q: string }, options?: { skip?: boolean }) => ({
83
+ data: getData(args, options),
84
+ isFetching: false,
85
+ isError: false,
86
+ })) as unknown as typeof useSearchPathsQuery);
87
+
88
+ beforeEach(() => {
89
+ jest.useFakeTimers();
90
+ mockSearchPaths((args, options) => {
91
+ if (options?.skip) return undefined;
92
+ return args.q === 'sa' ? SAPS_RESPONSE : EMPTY_RESPONSE;
93
+ });
94
+ });
95
+
96
+ afterEach(() => {
97
+ jest.useRealTimers();
98
+ });
99
+
100
+ it('skips the paths request and offers no paths while the prefix is empty', () => {
101
+ const { result } = renderHook(() => usePathAutocomplete('', EntityKind.SUBSCRIPTION));
102
+
103
+ act(() => {
104
+ jest.advanceTimersByTime(300);
105
+ });
106
+
107
+ expect(useSearchPathsQuery).toHaveBeenCalled();
108
+ jest.mocked(useSearchPathsQuery).mock.calls.forEach(([, options]) => expect(options?.skip).toBe(true));
109
+ expect(result.current.paths).toEqual([]);
110
+ });
111
+
112
+ it('reports loading from the keystroke until the debounced request has settled', async () => {
113
+ const { result, rerender } = renderHook(({ prefix }) => usePathAutocomplete(prefix, EntityKind.SUBSCRIPTION), {
114
+ initialProps: { prefix: '' },
115
+ });
116
+ expect(result.current.loading).toBe(false);
117
+
118
+ rerender({ prefix: 'sa' });
119
+ expect(result.current.loading).toBe(true);
120
+ expect(result.current.paths).toEqual([]);
121
+
122
+ act(() => {
123
+ jest.advanceTimersByTime(300);
124
+ });
125
+
126
+ await waitFor(() => expect(result.current.loading).toBe(false));
127
+ expect(result.current.paths).toHaveLength(1);
128
+ });
129
+
130
+ it('maps the components returned for a typed prefix, e.g. saps for "sa"', async () => {
131
+ const { result } = renderHook(() => usePathAutocomplete('sa', EntityKind.SUBSCRIPTION));
132
+
133
+ act(() => {
134
+ jest.advanceTimersByTime(300);
135
+ });
136
+
137
+ await waitFor(() => expect(result.current.paths).toHaveLength(1));
138
+ expect(useSearchPathsQuery).toHaveBeenLastCalledWith(
139
+ { q: 'sa', entity_type: EntityKind.SUBSCRIPTION },
140
+ { skip: false },
141
+ );
142
+ expect(result.current.paths[0]).toMatchObject({
143
+ path: 'saps',
144
+ type: 'component',
145
+ group: 'component',
146
+ availablePaths: ['saps.port', 'saps.vlan'],
147
+ });
148
+ });
149
+ });
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
2
 
3
3
  import { useLazySearchPathsQuery, useSearchDefinitionsQuery, useSearchPathsQuery } from '@/rtk/endpoints';
4
4
  import { EntityKind, PathAutocompleteResponse, PathInfo, value_schema } from '@/types';
@@ -97,35 +97,30 @@ const mapPathAutocompleteResponseToPathInfos = (
97
97
  };
98
98
 
99
99
  export const usePathAutocomplete = (prefix: string, entityType: EntityKind) => {
100
- const [paths, setPaths] = useState<PathInfo[]>([]);
101
100
  const debouncedPrefix = useDebounce(prefix, 300);
102
101
  const { data: definitions = FALLBACK_DEFINITIONS, isError: defError } = useSearchDefinitionsQuery();
103
102
 
104
103
  const {
105
104
  data: pathData,
106
- isLoading,
105
+ isFetching,
107
106
  isError,
108
107
  } = useSearchPathsQuery({ q: debouncedPrefix, entity_type: entityType }, { skip: debouncedPrefix.length < 1 });
109
108
 
110
- useEffect(() => {
111
- if (debouncedPrefix.length < 1) {
112
- setPaths([]);
113
- return;
114
- }
115
-
116
- if (!pathData) {
117
- return;
118
- }
109
+ const paths = useMemo(
110
+ () =>
111
+ debouncedPrefix.length < 1 || !pathData ? [] : mapPathAutocompleteResponseToPathInfos(pathData, definitions),
112
+ [pathData, definitions, debouncedPrefix.length],
113
+ );
119
114
 
120
- setPaths(mapPathAutocompleteResponseToPathInfos(pathData, definitions));
121
- }, [pathData, definitions, debouncedPrefix?.length]);
115
+ const isDebouncing = prefix.length >= 1 && prefix !== debouncedPrefix;
116
+ const loading = isDebouncing || isFetching;
122
117
 
123
118
  const errorMessage =
124
119
  isError ? 'Failed to load paths'
125
120
  : defError ? 'Failed to load definitions'
126
121
  : null;
127
122
 
128
- return { paths, loading: isLoading, error: errorMessage };
123
+ return { paths, loading, error: errorMessage };
129
124
  };
130
125
 
131
126
  /**
@@ -51,7 +51,7 @@ export const useSearchPagination = (
51
51
  {
52
52
  page: currentPage,
53
53
  results: results.data,
54
- cursor: results.cursor.start_cursor,
54
+ cursor: results.cursor?.start_cursor ?? null,
55
55
  },
56
56
  ]);
57
57
 
@@ -372,6 +372,7 @@
372
372
  "previousPage": "Previous page",
373
373
  "removeConditionAriaLabel": "Remove condition",
374
374
  "removeGroup": "Remove group",
375
+ "loadingOptions": "Loading options",
375
376
  "removeRule": "Remove rule",
376
377
  "resultsOnPage": "{resultCount} result(s) on this page",
377
378
  "retrieval": "Retrieval",
@@ -380,7 +381,7 @@
380
381
  "retrieverHybrid": "Hybrid",
381
382
  "retrieverSemantic": "Semantic",
382
383
  "searchError": "Error",
383
- "searchFieldsPlaceholder": "Search fields...",
384
+ "searchFieldsPlaceholder": "Type to select fields",
384
385
  "searchPlaceholder": "Search for {entityType}…",
385
386
  "searchResultsPagination": "Search results pagination",
386
387
  "selectDateAndTime": "Select date",
@@ -388,6 +389,7 @@
388
389
  "selectOrEnterValue": "Select or type value",
389
390
  "selectSpecificPathPlaceholder": "Select a specific path...",
390
391
  "showFilters": "Show filters",
392
+ "startTypingToLoadOptions": "Start typing to load options",
391
393
  "structuredFilters": "Structured filters",
392
394
  "toDate": "To date",
393
395
  "toNumber": "To",
@@ -372,6 +372,7 @@
372
372
  "previousPage": "Vorige pagina",
373
373
  "removeConditionAriaLabel": "Conditie verwijderen",
374
374
  "removeGroup": "Groep verwijderen",
375
+ "loadingOptions": "Opties laden",
375
376
  "removeRule": "Regel verwijderen",
376
377
  "resultsOnPage": "{resultCount} resulta(a)t(en) op deze pagina",
377
378
  "retrieval": "Retrieval",
@@ -380,7 +381,7 @@
380
381
  "retrieverHybrid": "Hybride",
381
382
  "retrieverSemantic": "Semantic",
382
383
  "searchError": "Fout",
383
- "searchFieldsPlaceholder": "Zoek velden...",
384
+ "searchFieldsPlaceholder": "Typ om velden te selecteren",
384
385
  "searchPlaceholder": "Zoek naar {entityType}…",
385
386
  "searchResultsPagination": "Paginering zoekresultaten",
386
387
  "selectDateAndTime": "Selecteer datum",
@@ -388,6 +389,7 @@
388
389
  "selectOrEnterValue": "Selecteer of typ een waarde",
389
390
  "selectSpecificPathPlaceholder": "Selecteer een specifiek pad...",
390
391
  "showFilters": "Toon filters",
392
+ "startTypingToLoadOptions": "Begin met typen om opties te laden",
391
393
  "structuredFilters": "Gestructureerde filters",
392
394
  "toDate": "Tot datum",
393
395
  "toNumber": "Tot",
@@ -129,17 +129,17 @@ const resultColumToPropertyMap: ResultColumToPropertyMap<SubscriptionListItem> =
129
129
 
130
130
  /* These options will be added as the first options in the field dropdown in the FieldSelector */
131
131
  const prefilledFieldOptions: FieldToOperatorMap = new Map([
132
- ['subscription.subscription_id', ['eq', 'neq', 'like']],
133
- ['subscription.description', ['eq', 'neq', 'like']],
134
- ['subscription.status', ['eq', 'neq', 'like']],
132
+ ['subscription.subscription_id', ['eq', 'neq', 'like', 'not_regexp']],
133
+ ['subscription.description', ['eq', 'neq', 'like', 'not_regexp']],
134
+ ['subscription.status', ['eq', 'neq', 'like', 'not_regexp']],
135
135
  ['subscription.insync', ['eq', 'neq']],
136
- ['subscription.product.name', ['eq', 'neq', 'like']],
137
- ['subscription.product.tag', ['eq', 'neq', 'like']],
138
- ['subscription.customer_name', ['eq', 'neq', 'like']],
139
- ['subscription.customer_abbreviation', ['eq', 'neq', 'like']],
136
+ ['subscription.product.name', ['eq', 'neq', 'like', 'not_regexp']],
137
+ ['subscription.product.tag', ['eq', 'neq', 'like', 'not_regexp']],
138
+ ['subscription.customer_name', ['eq', 'neq', 'like', 'not_regexp']],
139
+ ['subscription.customer_abbreviation', ['eq', 'neq', 'like', 'not_regexp']],
140
140
  ['subscription.start_date', ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between']],
141
141
  ['subscription.end_date', ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between']],
142
- ['subscription.note', ['eq', 'neq', 'like']],
142
+ ['subscription.note', ['eq', 'neq', 'like', 'not_regexp']],
143
143
  ]);
144
144
 
145
145
  export const WfoSearchPocPage = () => {
@@ -35,7 +35,7 @@ export type PaginatedSearchResults = {
35
35
  total_items: number;
36
36
  start_cursor: number;
37
37
  end_cursor: number;
38
- };
38
+ } | null;
39
39
  page_info: {
40
40
  has_next_page: boolean;
41
41
  next_page_cursor: string | null;
@@ -219,6 +219,5 @@ export type WfoQueryBuilderContext = {
219
219
  onFieldSelected: (field: string, operators: string[], pathInfo?: PathInfo) => void;
220
220
  prefilledFieldOptions: FieldToOperatorMap;
221
221
  fieldPathInfoMap: Map<string, PathInfo>;
222
- onValueEditorEnter: () => void;
223
222
  useAdvancedNestedSearch: boolean;
224
223
  };