@orchestrator-ui/orchestrator-ui-components 8.5.0 → 8.7.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 (65) hide show
  1. package/.turbo/turbo-build.log +10 -14
  2. package/.turbo/turbo-lint.log +2 -4
  3. package/.turbo/turbo-test.log +16 -15
  4. package/CHANGELOG.md +17 -0
  5. package/dist/index.d.ts +188 -80
  6. package/dist/index.js +3509 -2731
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -1
  9. package/src/components/WfoAutoExpandableTextArea/WfoAutoExpandableTextArea.tsx +54 -0
  10. package/src/components/WfoAutoExpandableTextArea/index.ts +1 -0
  11. package/src/components/WfoAutoExpandableTextArea/styles.ts +18 -0
  12. package/src/components/WfoPageTemplate/paths.ts +1 -0
  13. package/src/components/WfoPydanticForm/fields/WfoCron.spec.ts +88 -0
  14. package/src/components/WfoPydanticForm/fields/WfoCron.tsx +233 -0
  15. package/src/components/WfoPydanticForm/fields/index.ts +1 -0
  16. package/src/components/WfoPydanticForm/fields/styles.ts +92 -0
  17. package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResultItem.tsx +2 -1
  18. package/src/components/WfoSearchPage/utils.ts +37 -1
  19. package/src/components/WfoSubscription/WfoInUseByRelations.tsx +6 -2
  20. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionExpandableMenuItem.tsx +19 -14
  21. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +13 -1
  22. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionsMenuItem.tsx +3 -0
  23. package/src/components/WfoSubscription/WfoSubscriptionProductBlock/WfoSubscriptionProductBlock.tsx +1 -1
  24. package/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx +5 -0
  25. package/src/components/WfoSubscriptionsList/subscriptionResultMappers.ts +3 -1
  26. package/src/components/WfoTable/WfoAdvancedTable/WfoAdvancedTable.tsx +2 -12
  27. package/src/components/WfoTable/WfoStructuredSearchTable/WfoAddGroupAction.tsx +1 -1
  28. package/src/components/WfoTable/WfoStructuredSearchTable/WfoCombinatorSelector.tsx +3 -0
  29. package/src/components/WfoTable/WfoStructuredSearchTable/WfoExpandingSearchRow.tsx +21 -14
  30. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +84 -4
  31. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +119 -104
  32. package/src/components/WfoTable/WfoStructuredSearchTable/WfoInlineCombinator.tsx +17 -0
  33. package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.tsx +32 -7
  34. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRemoveRuleAction.tsx +3 -3
  35. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRuleGroup.tsx +11 -7
  36. package/src/components/WfoTable/WfoStructuredSearchTable/WfoSearchFieldWithActions.tsx +56 -0
  37. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +92 -87
  38. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +41 -11
  39. package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +74 -21
  40. package/src/components/WfoTable/WfoStructuredSearchTable/utils.ts +45 -0
  41. package/src/components/WfoTable/WfoTable/WfoTable.tsx +1 -0
  42. package/src/components/WfoTable/WfoTable/WfoTableDataRows.tsx +16 -4
  43. package/src/components/WfoTable/WfoTable/styles.ts +8 -0
  44. package/src/components/WfoTable/WfoTable/utils.spec.ts +62 -1
  45. package/src/components/WfoTable/WfoTable/utils.ts +4 -1
  46. package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.spec.ts +45 -0
  47. package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.tsx +9 -0
  48. package/src/components/WfoTable/WfoTableSettingsModal/index.ts +1 -0
  49. package/src/components/WfoTable/WfoTableSettingsModal/utils.ts +17 -0
  50. package/src/components/index.ts +1 -0
  51. package/src/configuration/version.ts +1 -1
  52. package/src/hooks/index.ts +1 -0
  53. package/src/hooks/useGetPydanticFormsConfig.tsx +12 -0
  54. package/src/hooks/useLanguageCode.ts +11 -0
  55. package/src/messages/en-GB.json +20 -0
  56. package/src/messages/nl-NL.json +20 -0
  57. package/src/pages/WfoSearchPocPage.tsx +165 -30
  58. package/src/rtk/endpoints/subscriptionList.ts +1 -0
  59. package/src/theme/baseStyles/formFieldsBaseStyle.ts +0 -1
  60. package/src/types/search.ts +4 -1
  61. package/src/types/types.ts +1 -1
  62. package/src/utils/getDefaultTableConfig.ts +6 -1
  63. package/src/utils/index.ts +1 -0
  64. package/src/utils/integer.spec.ts +20 -0
  65. package/src/utils/integer.ts +3 -0
@@ -1,4 +1,4 @@
1
- import React, { ReactNode, useEffect, useMemo, useState } from 'react';
1
+ import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import type { RuleGroupType } from 'react-querybuilder';
3
3
  import { formatQuery } from 'react-querybuilder/formatQuery';
4
4
  import { parseCEL } from 'react-querybuilder/parseCEL';
@@ -9,7 +9,7 @@ import { StringParam, useQueryParam, withDefault } from 'use-query-params';
9
9
 
10
10
  import { EuiSpacer } from '@elastic/eui';
11
11
 
12
- import { SearchParams, combineSearchFilters } from '@/components';
12
+ import { SearchParams, addStatusFilterFromTab, removeTabStatusMatchingFields } from '@/components';
13
13
  import {
14
14
  DEFAULT_PAGE_SIZE,
15
15
  StoredTableConfig,
@@ -32,16 +32,22 @@ import {
32
32
  WfoTableColumnConfig,
33
33
  subscriptionListTabs,
34
34
  } from '@/components';
35
+ import { parseCelToRuleGroup } from '@/components/WfoTable/WfoStructuredSearchTable/utils';
35
36
  import { ColumnType, WfoTableProps } from '@/components/WfoTable/WfoTable';
36
37
  import { useStoredTableConfig } from '@/hooks';
37
- import { SearchPayload, useSearchQuery } from '@/rtk';
38
- import { EntityKind, PaginatedSearchResults, RetrieverType, SortOrder } from '@/types';
39
- import { parseDateToLocaleDateTimeString } from '@/utils';
38
+ import { SearchPayload, useLazySearchQuery, useSearchQuery } from '@/rtk';
39
+ import {
40
+ EntityKind,
41
+ FieldToOperatorMap,
42
+ PaginatedSearchResults,
43
+ ResultColumToPropertyMap,
44
+ RetrieverType,
45
+ SortOrder,
46
+ } from '@/types';
47
+ import { getCsvFileNameWithDate, initiateCsvFileDownload, parseDateToLocaleDateTimeString } from '@/utils';
40
48
 
41
49
  const SEARCH_TABLE_LOCAL_STORAGE_KEY = 'SEARCH_TABLE_LOCAL_STORAGE_KEY';
42
50
 
43
- type ResultColumToPropertyMap<T> = Map<string, keyof T>;
44
-
45
51
  const getKeyByValueFromMap = <T,>(resultColumToPropertyMap: ResultColumToPropertyMap<T>, field: keyof T) => {
46
52
  return [...resultColumToPropertyMap.entries()].find(([, v]) => v === field)?.[0] || '';
47
53
  };
@@ -50,6 +56,7 @@ const getDataFromResponse = <T extends object>(
50
56
  data: PaginatedSearchResults,
51
57
  resultColumToPropertyMap: ResultColumToPropertyMap<T>,
52
58
  uniqueRowId: keyof T,
59
+ selectedTab: WfoSubscriptionListTab,
53
60
  ): {
54
61
  items: T[];
55
62
  rowExpandingConfiguration?: WfoTableProps<T>['rowExpandingConfiguration'];
@@ -66,18 +73,23 @@ const getDataFromResponse = <T extends object>(
66
73
  const rowExpandingConfiguration: WfoTableProps<T>['rowExpandingConfiguration'] = {
67
74
  uniqueRowId: uniqueRowId as keyof WfoTableColumnConfig<T>,
68
75
  uniqueRowIdToExpandedRowMap: searchResult.reduce(
69
- (rowMap, { response_columns, score, perfect_match, matching_field }) => {
76
+ (rowMap, { response_columns, score, perfect_match, matching_fields }) => {
70
77
  const idColumnInResponseColumn = getKeyByValueFromMap<T>(resultColumToPropertyMap, uniqueRowId);
71
78
  const rowId = response_columns[idColumnInResponseColumn];
72
79
  if (rowId) {
73
80
  rowMap[rowId] = (
74
- <WfoExpandingSearchRow score={score} matchingField={matching_field} perfectMatch={perfect_match} />
81
+ <WfoExpandingSearchRow
82
+ score={score}
83
+ matchingFields={removeTabStatusMatchingFields(matching_fields, selectedTab, EntityKind.SUBSCRIPTION)}
84
+ perfectMatch={perfect_match}
85
+ />
75
86
  );
76
87
  }
77
88
  return rowMap;
78
89
  },
79
90
  {} as Record<string, ReactNode>,
80
91
  ),
92
+ shouldOnlyShowOnHover: true,
81
93
  };
82
94
 
83
95
  const items: T[] = responseColumns.map((responseColumn) => {
@@ -116,6 +128,21 @@ const resultColumToPropertyMap: ResultColumToPropertyMap<SubscriptionListItem> =
116
128
  ['subscription.metadata', 'metadata'],
117
129
  ]);
118
130
 
131
+ /* These options will be added as the first options in the field dropdown in the FieldSelector */
132
+ const prefilledFieldOptions: FieldToOperatorMap = new Map([
133
+ ['subscription.subscription_id', ['eq', 'neq', 'like']],
134
+ ['subscription.description', ['eq', 'neq', 'like']],
135
+ ['subscription.status', ['eq', 'neq', 'like']],
136
+ ['subscription.insync', ['eq', 'neq']],
137
+ ['subscription.product.name', ['eq', 'neq', 'like']],
138
+ ['subscription.product.tag', ['eq', 'neq', 'like']],
139
+ ['subscription.customer_name', ['eq', 'neq', 'like']],
140
+ ['subscription.customer_abbreviation', ['eq', 'neq', 'like']],
141
+ ['subscription.start_date', ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between']],
142
+ ['subscription.end_date', ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between']],
143
+ ['subscription.note', ['eq', 'neq', 'like']],
144
+ ]);
145
+
119
146
  export const WfoSearchPocPage = () => {
120
147
  const t = useTranslations('subscriptions.index');
121
148
  const [activeTab, setActiveTab] = useQueryParam('activeTab', withDefault(StringParam, WfoSubscriptionListTab.ACTIVE));
@@ -125,30 +152,64 @@ export const WfoSearchPocPage = () => {
125
152
 
126
153
  // Part of the search endpoint payload that is passed in the q parameter
127
154
  const [queryText, setQueryText] = useState<string>('');
128
- const [committedSearchQuery, setCommittedSearchQuery] = useState<string>('');
155
+ // The committed query and filter live in the URL so a link reproduces the search and browser
156
+ // back/forward re-runs it. The filter is stored as a CEL string and parsed back to a rule group.
157
+ const [committedSearchQuery, setCommittedSearchQuery] = useQueryParam('queryString', withDefault(StringParam, ''));
158
+ const [committedFilterString, setCommittedFilterString] = useQueryParam('filterString', withDefault(StringParam, ''));
159
+ // Track the last value this page committed, so the URL->state sync effects below only rebuild the
160
+ // inputs for external changes (page load, back/forward). Rebuilding on own commits would revert
161
+ // characters typed right after committing (search bar) or visibly restructure builder rules the
162
+ // CEL round trip cannot preserve, such as 'between'. The refs hold the value as the params decode
163
+ // it (absent param -> ''); the setters receive undefined to remove an empty param from the URL.
164
+ const lastSelfCommittedFilter = useRef('');
165
+ const commitFilterString = (celString: string) => {
166
+ lastSelfCommittedFilter.current = celString;
167
+ setCommittedFilterString(celString || undefined);
168
+ };
169
+ const lastSelfCommittedQuery = useRef('');
170
+ const commitSearchQuery = (queryText: string) => {
171
+ lastSelfCommittedQuery.current = queryText;
172
+ setCommittedSearchQuery(queryText || undefined);
173
+ };
129
174
 
130
175
  // String that is displayed in the filter textarea. This is transformed and if valid passed to the search endpoint in the filter parameter
131
- const [filterString, setFilterString] = useState<string>();
176
+ const [filterString, setFilterString] = useState<string>('');
132
177
  const [queryBuilderRuleGroup, setQueryBuilderRuleGroup] = useState<RuleGroupType | undefined>();
133
- const [committedRuleGroup, setCommittedRuleGroup] = useState<RuleGroupType | undefined>();
178
+ const committedRuleGroup = useMemo<RuleGroupType | undefined>(
179
+ () => parseCelToRuleGroup(committedFilterString),
180
+ [committedFilterString],
181
+ );
134
182
  const [isValidFilterString, setIsValidFilterString] = useState<boolean>(true);
135
183
  const [tableDefaults, setTableDefaults] = useState<StoredTableConfig<SubscriptionListItem>>();
136
184
  const [pageSize, setPageSize] = useState<number>(tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE);
137
185
  const [limit, setLimit] = useState<number>(pageSize);
138
- const [cursor, setCursor] = useState<string | undefined>(undefined);
186
+ const [pageCursor, setPageCursor] = useState<{ cursor: string; searchKey: string } | undefined>(undefined);
139
187
  const [dataSorting, setDataSorting] = useState<WfoDataSorting<SubscriptionListItem>>({
140
188
  field: 'subscriptionId',
141
189
  sortOrder: SortOrder.DESC,
142
190
  });
143
191
 
144
- // The search payload is derived entirely from the committed search state. Changing any of these
145
- // values including the active tab, whether via a tab click or a browser back/forward that
146
- // updates the activeTab URL param produces a new payload and RTK Query re-runs the search
147
- // automatically. No imperative trigger or effect (and no eslint-disable) is needed.
192
+ // A next-page cursor is only valid for the committed search that produced it. Binding it to a key
193
+ // of that search covers commits that bypass the handlers below, e.g. browser back/forward changing
194
+ // the queryString/filterString/activeTab URL params: the key no longer matches, so the stale
195
+ // cursor is not sent along with the new search.
196
+ const committedSearchKey = JSON.stringify([
197
+ committedSearchQuery,
198
+ committedFilterString,
199
+ selectedTab,
200
+ retrieverType,
201
+ dataSorting,
202
+ ]);
203
+ const cursor = pageCursor?.searchKey === committedSearchKey ? pageCursor.cursor : undefined;
204
+
205
+ // The search payload is derived entirely from the committed search state, which lives in the URL
206
+ // (queryString, filterString and activeTab params). Changing any of these values — whether via the
207
+ // UI or a browser back/forward that updates the URL — produces a new payload and RTK Query re-runs
208
+ // the search automatically. No imperative trigger or effect (and no eslint-disable) is needed.
148
209
  // Cursor is stripped from the RTK cache key, so a cursor change appends to the accumulated
149
210
  // result set instead of creating a new cache entry — see search endpoint's merge() logic.
150
211
  const searchPayload: SearchPayload = useMemo(() => {
151
- const filters = combineSearchFilters(committedRuleGroup, selectedTab);
212
+ const filters = addStatusFilterFromTab(committedRuleGroup, selectedTab);
152
213
  const order_by = {
153
214
  element: getKeyByValueFromMap(resultColumToPropertyMap, dataSorting.field),
154
215
  direction: dataSorting.sortOrder.toLowerCase(),
@@ -167,6 +228,9 @@ export const WfoSearchPocPage = () => {
167
228
 
168
229
  const { data, isFetching } = useSearchQuery(searchPayload);
169
230
 
231
+ const [getSubscriptionListTrigger] = useLazySearchQuery();
232
+ const getSubscriptionListForExport = () => getSubscriptionListTrigger(searchPayload).unwrap();
233
+
170
234
  useEffect(() => {
171
235
  const storedConfig = getStoredTableConfig();
172
236
  if (storedConfig) {
@@ -188,6 +252,7 @@ export const WfoSearchPocPage = () => {
188
252
  renderDetails: (value) => value,
189
253
  renderTooltip: (value) => value,
190
254
  isSortable: true,
255
+ isFilterable: true,
191
256
  },
192
257
  description: {
193
258
  columnType: ColumnType.DATA,
@@ -195,37 +260,44 @@ export const WfoSearchPocPage = () => {
195
260
  width: '500px',
196
261
  renderData: (value, record) => <Link href={`/subscriptions/${record.subscriptionId}`}>{value}</Link>,
197
262
  renderTooltip: (value) => value,
263
+ isFilterable: true,
198
264
  },
199
265
  status: {
200
266
  columnType: ColumnType.DATA,
201
267
  label: t('status'),
202
268
  width: '120px',
203
269
  renderData: (value) => <WfoSubscriptionStatusBadge status={value} />,
270
+ isFilterable: true,
204
271
  },
205
272
  insync: {
206
273
  columnType: ColumnType.DATA,
207
274
  label: t('insync'),
208
275
  width: '75px',
209
276
  renderData: (value) => <WfoInsyncIcon inSync={value} />,
277
+ isFilterable: true,
210
278
  },
211
279
  productName: {
212
280
  columnType: ColumnType.DATA,
213
281
  width: '260px',
214
282
  label: t('product'),
283
+ isFilterable: true,
215
284
  },
216
285
  tag: {
217
286
  columnType: ColumnType.DATA,
218
287
  label: t('tag'),
219
288
  width: '100px',
289
+ isFilterable: true,
220
290
  },
221
291
  customerFullname: {
222
292
  columnType: ColumnType.DATA,
223
293
  label: t('customerFullname'),
294
+ isFilterable: true,
224
295
  },
225
296
  customerShortcode: {
226
297
  columnType: ColumnType.DATA,
227
298
  label: t('customerShortcode'),
228
299
  width: '150px',
300
+ isFilterable: true,
229
301
  },
230
302
  startDate: {
231
303
  columnType: ColumnType.DATA,
@@ -235,6 +307,7 @@ export const WfoSearchPocPage = () => {
235
307
  renderDetails: parseDateToLocaleDateTimeString,
236
308
  clipboardText: parseDateToLocaleDateTimeString,
237
309
  renderTooltip: (cellValue) => cellValue?.toString(),
310
+ isFilterable: true,
238
311
  },
239
312
  endDate: {
240
313
  columnType: ColumnType.DATA,
@@ -244,6 +317,7 @@ export const WfoSearchPocPage = () => {
244
317
  renderDetails: parseDateToLocaleDateTimeString,
245
318
  clipboardText: parseDateToLocaleDateTimeString,
246
319
  renderTooltip: (cellValue) => cellValue?.toString(),
320
+ isFilterable: true,
247
321
  },
248
322
  note: {
249
323
  columnType: ColumnType.DATA,
@@ -260,6 +334,7 @@ export const WfoSearchPocPage = () => {
260
334
  />
261
335
  );
262
336
  },
337
+ isFilterable: true,
263
338
  },
264
339
  metadata: {
265
340
  columnType: ColumnType.DATA,
@@ -268,12 +343,28 @@ export const WfoSearchPocPage = () => {
268
343
  renderData: (value) => <WfoInlineJson data={value} />,
269
344
  renderDetails: (value) => value && <WfoJsonCodeBlock data={value} isBasicStyle />,
270
345
  renderTooltip: (value) => value && <WfoJsonCodeBlock data={value} isBasicStyle={false} />,
346
+ isFilterable: true,
271
347
  },
272
348
  };
273
349
 
274
350
  const handleApplyFilter = (searchParams?: SearchParams) => {
275
- setCommittedRuleGroup(searchParams?.ruleGroup === false ? undefined : queryBuilderRuleGroup);
276
- setCursor(undefined);
351
+ const ruleGroupParam = searchParams?.ruleGroup;
352
+ // Use an explicitly passed rule group when provided (e.g. a column-header search), a cleared filter
353
+ // when `false`, and otherwise the current query builder state (the "Apply filter" button).
354
+ const effectiveRuleGroup = ruleGroupParam === false ? undefined : (ruleGroupParam ?? queryBuilderRuleGroup);
355
+ // '' (no rule group, or only placeholder rules) commits an empty filter, clearing the URL param.
356
+ const celQuery =
357
+ effectiveRuleGroup ? formatQuery(effectiveRuleGroup, { format: 'cel', fallbackExpression: '' }) : '';
358
+ // A non-empty CEL string must survive the round trip through the URL: formatQuery escapes double
359
+ // quotes in values but parseCEL has no escape support, so such a filter would silently be dropped
360
+ // after committing. Refuse the commit and flag the filter instead, keeping the URL and the search
361
+ // results consistent.
362
+ if (celQuery && !parseCelToRuleGroup(celQuery)) {
363
+ setIsValidFilterString(false);
364
+ return;
365
+ }
366
+ commitFilterString(celQuery);
367
+ setPageCursor(undefined);
277
368
  };
278
369
 
279
370
  const onChangeQueryText = (queryText: string) => {
@@ -282,22 +373,22 @@ export const WfoSearchPocPage = () => {
282
373
 
283
374
  const onSearchQueryText = (queryText: string) => {
284
375
  setQueryText(queryText);
285
- setCommittedSearchQuery(queryText);
286
- setCursor(undefined);
376
+ commitSearchQuery(queryText);
377
+ setPageCursor(undefined);
287
378
  };
288
379
 
289
380
  const onUpdateRetrieverType = (retrieverType: RetrieverType) => {
290
381
  setRetrieverType(retrieverType);
291
- setCursor(undefined);
382
+ setPageCursor(undefined);
292
383
  };
293
384
 
294
385
  const handleChangeTab = (updatedTab: WfoSubscriptionListTab) => {
295
386
  setActiveTab(updatedTab);
296
387
  setLimit(pageSize);
297
- setCursor(undefined);
388
+ setPageCursor(undefined);
298
389
  };
299
390
 
300
- const safeCelParse = (celString: string) => {
391
+ const safeCelParse = useCallback((celString: string) => {
301
392
  try {
302
393
  const ruleGroup = parseCEL(celString);
303
394
  if (celString === '') {
@@ -315,7 +406,32 @@ export const WfoSearchPocPage = () => {
315
406
  } catch {
316
407
  setIsValidFilterString(false);
317
408
  }
318
- };
409
+ }, []);
410
+
411
+ // Populate the search bar and the filter builder from the URL, both on page load and when
412
+ // back/forward navigation changes the committed search. Commits made by this page are skipped —
413
+ // see the lastSelfCommitted refs above.
414
+ useEffect(() => {
415
+ if (committedSearchQuery === lastSelfCommittedQuery.current) {
416
+ return;
417
+ }
418
+ lastSelfCommittedQuery.current = committedSearchQuery;
419
+ setQueryText(committedSearchQuery);
420
+ }, [committedSearchQuery]);
421
+
422
+ useEffect(() => {
423
+ if (committedFilterString === lastSelfCommittedFilter.current) {
424
+ return;
425
+ }
426
+ lastSelfCommittedFilter.current = committedFilterString;
427
+ setFilterString(committedFilterString);
428
+ if (committedFilterString) {
429
+ safeCelParse(committedFilterString);
430
+ } else {
431
+ setQueryBuilderRuleGroup(undefined);
432
+ setIsValidFilterString(true);
433
+ }
434
+ }, [committedFilterString, safeCelParse]);
319
435
 
320
436
  const onUpdateQueryBuilder = (ruleGroup: RuleGroupType | false) => {
321
437
  if (ruleGroup === false) {
@@ -343,27 +459,43 @@ export const WfoSearchPocPage = () => {
343
459
  };
344
460
 
345
461
  const { items: subscriptionListItems, rowExpandingConfiguration } =
346
- data ? getDataFromResponse<SubscriptionListItem>(data, resultColumToPropertyMap, 'subscriptionId') : { items: [] };
462
+ data ?
463
+ getDataFromResponse<SubscriptionListItem>(data, resultColumToPropertyMap, 'subscriptionId', selectedTab)
464
+ : { items: [] };
347
465
 
348
466
  const totalItems = getTotalItemsFromResponse(data);
349
467
  const hasNextPage = data?.page_info?.has_next_page ?? false;
350
468
  const nextPageCursor = data?.page_info?.next_page_cursor ?? undefined;
351
469
 
470
+ const exportData = async () => {
471
+ const exportResult = await getSubscriptionListForExport();
472
+ const { items: exportItems } = getDataFromResponse<SubscriptionListItem>(
473
+ exportResult,
474
+ resultColumToPropertyMap,
475
+ 'subscriptionId',
476
+ selectedTab,
477
+ );
478
+ if (!exportItems.length) {
479
+ return;
480
+ }
481
+ initiateCsvFileDownload(exportItems, Object.keys(tableColumnConfig), getCsvFileNameWithDate('Subscriptions'));
482
+ };
483
+
352
484
  const onShowMore = () => {
353
485
  if (!isFetching && nextPageCursor) {
354
- setCursor(nextPageCursor);
486
+ setPageCursor({ cursor: nextPageCursor, searchKey: committedSearchKey });
355
487
  }
356
488
  };
357
489
 
358
490
  const onUpdateDataSorting = ({ field, sortOrder }: WfoDataSorting<SubscriptionListItem>) => {
359
491
  setDataSorting({ field, sortOrder });
360
492
  setLimit(pageSize);
361
- setCursor(undefined);
493
+ setPageCursor(undefined);
362
494
  };
363
495
 
364
496
  return (
365
497
  <>
366
- <WfoContentHeader title="Subscriptions (POC)" />
498
+ <WfoContentHeader title="Subscriptions (Beta)" />
367
499
  <WfoFilterTabs
368
500
  tabs={subscriptionListTabs}
369
501
  selectedTab={selectedTab}
@@ -391,11 +523,14 @@ export const WfoSearchPocPage = () => {
391
523
  queryText={queryText}
392
524
  retrieverType={retrieverType}
393
525
  tableColumnConfig={tableColumnConfig}
526
+ getColumnSearchFieldName={(field) => getKeyByValueFromMap(resultColumToPropertyMap, field)}
394
527
  pageSize={pageSize}
395
528
  onUpdateDataSorting={onUpdateDataSorting}
396
529
  setPageSize={setPageSize}
397
530
  totalItems={totalItems}
398
531
  hasNextPage={hasNextPage}
532
+ prefilledFieldOptions={prefilledFieldOptions}
533
+ onExportData={exportData}
399
534
  />
400
535
  </>
401
536
  );
@@ -31,6 +31,7 @@ export const subscriptionListQuery = `query SubscriptionsList(
31
31
  productType
32
32
  }
33
33
  customer {
34
+ customerId
34
35
  fullname
35
36
  shortcode
36
37
  }
@@ -10,7 +10,6 @@ export const getFormFieldsBaseStyle = ({ theme }: WfoThemeHelpers) => {
10
10
  backgroundColor: theme.colors.backgroundBaseNeutral,
11
11
  boxShadow: `0 0 0 1px ${theme.colors.primary}`,
12
12
  },
13
- // boxShadow: `0 0 0 1px ${theme.colors.borderBaseSubdued} !important`,
14
13
  });
15
14
 
16
15
  return {
@@ -24,7 +24,7 @@ export interface SearchResult {
24
24
  entity_title: string;
25
25
  score: number;
26
26
  perfect_match: number;
27
- matching_field?: MatchingField | null;
27
+ matching_fields?: MatchingField[] | null;
28
28
  response_columns: Record<string, string | number | null>;
29
29
  }
30
30
 
@@ -211,3 +211,6 @@ export type ExportArtifact = {
211
211
  download_url: string;
212
212
  description: string;
213
213
  };
214
+
215
+ export type ResultColumToPropertyMap<T> = Map<string, keyof T>;
216
+ export type FieldToOperatorMap = Map<string, string[]>;
@@ -471,7 +471,7 @@ export type Subscription = {
471
471
  status: SubscriptionStatus;
472
472
  product: Pick<ProductDefinition, 'name' | 'tag' | 'productType'>;
473
473
  productBlockInstances: ProductBlockInstance[];
474
- customer: Pick<Customer, 'fullname' | 'shortcode'>;
474
+ customer: Pick<Customer, 'customerId' | 'fullname' | 'shortcode'>;
475
475
  metadata: object;
476
476
  };
477
477
 
@@ -95,7 +95,12 @@ export const getDefaultTableConfig = <T>(storageKey: string) => {
95
95
  return getTableConfig<T>(completedTasksColumns as (keyof T)[]);
96
96
  }
97
97
  case SUBSCRIPTIONS_TABLE_LOCAL_STORAGE_KEY: {
98
- const subscriptionColumns: (keyof SubscriptionListItem)[] = ['productName', 'customerFullname', 'metadata'];
98
+ const subscriptionColumns: (keyof SubscriptionListItem)[] = [
99
+ 'productName',
100
+ 'customerId',
101
+ 'customerFullname',
102
+ 'metadata',
103
+ ];
99
104
  return getTableConfig<T>(subscriptionColumns as (keyof T)[]);
100
105
  }
101
106
  default:
@@ -10,6 +10,7 @@ export * from './getProductNamesFromProcess';
10
10
  export * from './getQueryVariablesForExport';
11
11
  export * from './getStatusBadgeColor';
12
12
  export * from './getTypedFieldFromObject';
13
+ export * from './integer';
13
14
  export * from './onlyUnique';
14
15
  export * from './optionalArray';
15
16
  export * from './resultFlattener';
@@ -0,0 +1,20 @@
1
+ import { toPercentage } from './integer';
2
+
3
+ describe('toPercentage()', () => {
4
+ it('converts a fraction to a percentage string with one decimal', () => {
5
+ expect(toPercentage(0.5)).toEqual('50.0%');
6
+ });
7
+ it('converts 1 to 100.0%', () => {
8
+ expect(toPercentage(1)).toEqual('100.0%');
9
+ });
10
+ it('converts 0 to 0.0%', () => {
11
+ expect(toPercentage(0)).toEqual('0.0%');
12
+ });
13
+ it('rounds to one decimal place', () => {
14
+ expect(toPercentage(0.12345)).toEqual('12.3%');
15
+ expect(toPercentage(0.6789)).toEqual('67.9%');
16
+ });
17
+ it('handles fractions greater than 1', () => {
18
+ expect(toPercentage(1.5)).toEqual('150.0%');
19
+ });
20
+ });
@@ -0,0 +1,3 @@
1
+ export const toPercentage = (fraction: number): string => {
2
+ return `${(fraction * 100).toFixed(1)}%`;
3
+ };