@orchestrator-ui/orchestrator-ui-components 8.6.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 (53) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +16 -13
  4. package/CHANGELOG.md +11 -0
  5. package/dist/index.d.ts +106 -8
  6. package/dist/index.js +2540 -2078
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -1
  9. package/src/components/WfoPydanticForm/fields/WfoCron.spec.ts +88 -0
  10. package/src/components/WfoPydanticForm/fields/WfoCron.tsx +233 -0
  11. package/src/components/WfoPydanticForm/fields/index.ts +1 -0
  12. package/src/components/WfoPydanticForm/fields/styles.ts +92 -0
  13. package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResultItem.tsx +2 -1
  14. package/src/components/WfoSearchPage/utils.ts +37 -1
  15. package/src/components/WfoSubscription/WfoInUseByRelations.tsx +6 -2
  16. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionExpandableMenuItem.tsx +19 -14
  17. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +13 -1
  18. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionsMenuItem.tsx +3 -0
  19. package/src/components/WfoSubscription/WfoSubscriptionProductBlock/WfoSubscriptionProductBlock.tsx +1 -1
  20. package/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx +5 -0
  21. package/src/components/WfoSubscriptionsList/subscriptionResultMappers.ts +3 -1
  22. package/src/components/WfoTable/WfoAdvancedTable/WfoAdvancedTable.tsx +2 -12
  23. package/src/components/WfoTable/WfoStructuredSearchTable/WfoExpandingSearchRow.tsx +21 -14
  24. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +27 -1
  25. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +97 -91
  26. package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.tsx +32 -7
  27. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRuleGroup.tsx +5 -4
  28. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +32 -30
  29. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +41 -11
  30. package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +22 -5
  31. package/src/components/WfoTable/WfoTable/WfoTable.tsx +1 -0
  32. package/src/components/WfoTable/WfoTable/WfoTableDataRows.tsx +16 -4
  33. package/src/components/WfoTable/WfoTable/styles.ts +8 -0
  34. package/src/components/WfoTable/WfoTable/utils.spec.ts +62 -1
  35. package/src/components/WfoTable/WfoTable/utils.ts +4 -1
  36. package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.spec.ts +45 -0
  37. package/src/components/WfoTable/WfoTableSettingsModal/index.ts +1 -0
  38. package/src/components/WfoTable/WfoTableSettingsModal/utils.ts +17 -0
  39. package/src/configuration/version.ts +1 -1
  40. package/src/hooks/index.ts +1 -0
  41. package/src/hooks/useGetPydanticFormsConfig.tsx +12 -0
  42. package/src/hooks/useLanguageCode.ts +11 -0
  43. package/src/messages/en-GB.json +19 -0
  44. package/src/messages/nl-NL.json +19 -0
  45. package/src/pages/WfoSearchPocPage.tsx +14 -5
  46. package/src/rtk/endpoints/subscriptionList.ts +1 -0
  47. package/src/theme/baseStyles/formFieldsBaseStyle.ts +0 -1
  48. package/src/types/search.ts +1 -1
  49. package/src/types/types.ts +1 -1
  50. package/src/utils/getDefaultTableConfig.ts +6 -1
  51. package/src/utils/index.ts +1 -0
  52. package/src/utils/integer.spec.ts +20 -0
  53. package/src/utils/integer.ts +3 -0
@@ -19,6 +19,7 @@ interface MenuItemProps {
19
19
  setPopover: (isOpen: boolean) => void;
20
20
  onClick: () => void;
21
21
  isLoading?: boolean;
22
+ subscriptionPath?: string;
22
23
  }
23
24
 
24
25
  export const WfoSubscriptionActionsMenuItem: FC<MenuItemProps> = ({
@@ -27,6 +28,7 @@ export const WfoSubscriptionActionsMenuItem: FC<MenuItemProps> = ({
27
28
  target,
28
29
  setPopover,
29
30
  isLoading = false,
31
+ subscriptionPath,
30
32
  }) => {
31
33
  const { linkMenuItemStyle, tooltipMenuItemStyle, disabledIconStyle, iconStyle, secondaryIconStyle } =
32
34
  useWithOrchestratorTheme(getSubscriptionActionStyles);
@@ -102,6 +104,7 @@ export const WfoSubscriptionActionsMenuItem: FC<MenuItemProps> = ({
102
104
  <WfoSubscriptionActionExpandableMenuItem
103
105
  subscriptionAction={subscriptionAction}
104
106
  onClickLockedRelation={() => setPopover(false)}
107
+ subscriptionPath={subscriptionPath}
105
108
  >
106
109
  {actionItem}
107
110
  </WfoSubscriptionActionExpandableMenuItem>
@@ -147,7 +147,7 @@ export const WfoSubscriptionProductBlock = ({
147
147
  </td>
148
148
  <td>
149
149
  {(inUseByRelations.length === 0 && 'None') || (
150
- <WfoInUseByRelations inUseByRelations={inUseByRelations} />
150
+ <WfoInUseByRelations inUseByRelations={inUseByRelations} subscriptionPath={subscriptionPath} />
151
151
  )}
152
152
  </td>
153
153
  </tr>
@@ -122,6 +122,11 @@ export const WfoSubscriptionsList: FC<WfoSubscriptionsListProps> = ({
122
122
  label: t('tag'),
123
123
  width: '100px',
124
124
  },
125
+ customerId: {
126
+ columnType: ColumnType.DATA,
127
+ label: t('customerId'),
128
+ width: '100px',
129
+ },
125
130
  customerFullname: {
126
131
  columnType: ColumnType.DATA,
127
132
  label: t('customerFullname'),
@@ -12,6 +12,7 @@ export type SubscriptionListItem = Pick<
12
12
  tag: string | null;
13
13
  customerFullname: string;
14
14
  customerShortcode: string;
15
+ customerId: string;
15
16
  metadata: object | null;
16
17
  };
17
18
 
@@ -27,7 +28,7 @@ export const mapGraphQlSubscriptionsResultToSubscriptionListItems = (
27
28
  subscription;
28
29
 
29
30
  const { name: productName, tag } = product;
30
- const { fullname: customerFullname, shortcode: customerShortcode } = customer;
31
+ const { fullname: customerFullname, shortcode: customerShortcode, customerId } = customer;
31
32
 
32
33
  return {
33
34
  subscriptionId,
@@ -41,6 +42,7 @@ export const mapGraphQlSubscriptionsResultToSubscriptionListItems = (
41
42
  tag,
42
43
  customerFullname,
43
44
  customerShortcode,
45
+ customerId,
44
46
  metadata: Object.keys(metadata).length > 0 ? metadata : null,
45
47
  };
46
48
  });
@@ -8,7 +8,6 @@ import {
8
8
  DEFAULT_PAGE_SIZE,
9
9
  DEFAULT_PAGE_SIZES,
10
10
  TableColumnKeys,
11
- TableSettingsColumnConfig,
12
11
  TableSettingsConfig,
13
12
  TableSettingsModal,
14
13
  WfoErrorWithMessage,
@@ -18,6 +17,7 @@ import {
18
17
  WfoSearchField,
19
18
  WfoToolTip,
20
19
  clearTableConfigFromLocalStorage,
20
+ getTableSettingsColumns,
21
21
  setTableConfigToLocalStorage,
22
22
  } from '@/components';
23
23
  import { getRowDetailData } from '@/components/WfoTable/WfoAdvancedTable/getRowDetailData';
@@ -96,17 +96,7 @@ export const WfoAdvancedTable = <T extends object>({
96
96
  ...tableColumnConfig,
97
97
  };
98
98
 
99
- const tableSettingsColumns: TableSettingsColumnConfig<T>[] = Object.entries(tableColumnConfig).map(
100
- ([key, { label }]): TableSettingsColumnConfig<T> => {
101
- const field = key as keyof T;
102
-
103
- return {
104
- field,
105
- name: label,
106
- isVisible: hiddenColumns.indexOf(field) === -1,
107
- };
108
- },
109
- );
99
+ const tableSettingsColumns = getTableSettingsColumns(tableColumnConfig, hiddenColumns);
110
100
 
111
101
  const rowDetailData: WfoKeyValueTableDataType[] | undefined =
112
102
  selectedDataForDetailModal && getRowDetailData(selectedDataForDetailModal, tableColumnConfig);
@@ -1,36 +1,43 @@
1
1
  import React from 'react';
2
2
 
3
+ import { WfoHighlightedText } from '@/components/WfoSearchPage/WfoSearchResults/WfoHighlightedText';
3
4
  import { useWithOrchestratorTheme } from '@/hooks';
4
5
  import type { MatchingField } from '@/types';
6
+ import { toPercentage } from '@/utils';
5
7
 
6
8
  import { getWfoStructuredSearchTableStyles } from './styles';
7
9
 
8
10
  interface WfoExpandingSearchRowProps {
9
11
  score?: number;
10
12
  perfectMatch?: number;
11
- matchingField?: MatchingField | null;
13
+ matchingFields?: MatchingField[] | null;
12
14
  }
13
15
 
14
- export const WfoExpandingSearchRow = ({ score, matchingField }: WfoExpandingSearchRowProps) => {
15
- const { expandingSearchRowStyles, expandingRowBodyStyles } = useWithOrchestratorTheme(
16
- getWfoStructuredSearchTableStyles,
17
- );
18
-
19
- if (!matchingField) return null;
16
+ export const WfoExpandingSearchRow = ({ score, matchingFields }: WfoExpandingSearchRowProps) => {
17
+ const { expandingSearchRowStyles, expandingRowBodyStyles, hideExpandedRowStyle, dotStyles, expandingRowFieldStyles } =
18
+ useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
20
19
 
21
- const { path, text } = matchingField;
20
+ if (!matchingFields || matchingFields.length === 0) return null;
22
21
 
22
+ // Note: The row is shown on hover by the row above is as configured in the table component expandableRow config
23
23
  return (
24
- <tr>
24
+ <tr css={hideExpandedRowStyle}>
25
25
  <td colSpan={999} css={expandingSearchRowStyles}>
26
26
  <div css={expandingRowBodyStyles}>
27
27
  <div>
28
- <span>{path}</span> {text}
29
- </div>
30
- <span>•</span>
31
- <div>
32
- <span>score</span> {score !== undefined && `${(score * 100).toFixed(1)}%`}
28
+ {matchingFields.map((field) => {
29
+ return (
30
+ <div key={`${field.path}:${field.text}`} css={expandingRowFieldStyles}>
31
+ <div>
32
+ <span css={dotStyles}>•</span>
33
+ {field.path}: <WfoHighlightedText text={field.text} highlight_indices={field.highlight_indices} />
34
+ </div>
35
+ </div>
36
+ );
37
+ })}
33
38
  </div>
39
+
40
+ {score && <div css={dotStyles}>confidence: {toPercentage(score)}</div>}
34
41
  </div>
35
42
  </td>
36
43
  </tr>
@@ -9,6 +9,27 @@ import { EuiComboBox } from '@elastic/eui';
9
9
  import { usePathAutocomplete } from '@/hooks';
10
10
  import { EntityKind, FieldToOperatorMap, PathInfo } from '@/types';
11
11
 
12
+ // react-querybuilder applies the `.rule` class to the rule container and `.rule-value` to
13
+ // the value-editor cell (see `standardClassnames` in @react-querybuilder/core). We hop from
14
+ // the EuiComboBox search input up to the enclosing rule and focus the value editor's first
15
+ // focusable child on the next frame, once react-querybuilder has re-rendered the rule with
16
+ // the editor type for the new field.
17
+ const focusValueEditorAfterRender = (searchInput: HTMLInputElement | null) => {
18
+ if (!searchInput) return;
19
+ requestAnimationFrame(() => {
20
+ const valueCell = searchInput.closest('.rule')?.querySelector('.rule-value');
21
+ const focusable = valueCell?.querySelector<HTMLElement>(
22
+ 'input:not([disabled]), textarea:not([disabled]), button:not([disabled])',
23
+ );
24
+
25
+ if (!focusable) return;
26
+ focusable.focus();
27
+ if (focusable instanceof HTMLInputElement && (focusable.type === 'text' || focusable.type === 'number')) {
28
+ focusable.select();
29
+ }
30
+ });
31
+ };
32
+
12
33
  export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: FieldSelectorProps) => {
13
34
  const { field } = rule;
14
35
  const prefilledFieldOptions: FieldToOperatorMap = context.prefilledFieldOptions;
@@ -74,6 +95,8 @@ export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: Fi
74
95
  storeFieldOperators(selectedValue);
75
96
 
76
97
  handleOnChange(selectedValue);
98
+
99
+ if (selectedValue) focusValueEditorAfterRender(searchInput);
77
100
  };
78
101
 
79
102
  // EuiComboBox only auto-selects on Enter/Tab when exactly one option matches the typed
@@ -95,7 +118,10 @@ export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: Fi
95
118
  (option) => !option.disabled && option.label.toLowerCase() === typed.toLowerCase(),
96
119
  );
97
120
  if (!exactMatch) return;
98
- if (event.key === 'Enter') event.preventDefault();
121
+ // Prevent both Enter (form submit / EUI's no-op fallback) and Tab (native focus
122
+ // shift to the operator selector) — handleFieldSelection moves focus to the value
123
+ // editor itself, and a native Tab in the meantime causes a visible focus flash.
124
+ event.preventDefault();
99
125
  handleFieldSelectionRef.current([exactMatch]);
100
126
 
101
127
  // EuiComboBox keeps its own internal `searchValue` and `isListOpen` state. Clearing
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useRef, useState } from 'react';
2
2
  import { FullOperator, QueryBuilder, type RuleGroupType, generateID } from 'react-querybuilder';
3
3
  import 'react-querybuilder/dist/query-builder.css';
4
4
 
@@ -58,7 +58,6 @@ interface WfoFilterBuilderProps {
58
58
  queryBuilderRuleGroup?: RuleGroupType;
59
59
  onUpdateQueryBuilder: (ruleGroup: RuleGroupType | false) => void;
60
60
  handleSearch: (searchParams?: SearchParams) => void;
61
- isFilterBuilderVisible: boolean;
62
61
  onToggleFilterBuilder: (isVisible: boolean) => void;
63
62
  prefilledFieldOptions: FieldToOperatorMap;
64
63
  }
@@ -82,7 +81,6 @@ export const WfoFilterBuilder = ({
82
81
  onUpdateQueryBuilder,
83
82
  handleSearch,
84
83
  prefilledFieldOptions,
85
- isFilterBuilderVisible,
86
84
  onToggleFilterBuilder,
87
85
  }: WfoFilterBuilderProps) => {
88
86
  const mapOperatorsToRQBOperatorOptions = (operators?: string[]): FullOperator[] => {
@@ -94,11 +92,20 @@ export const WfoFilterBuilder = ({
94
92
  };
95
93
 
96
94
  const t = useTranslations('common');
97
- const { queryBuilderContainerStyles, toggleButtonStyles } = useWithOrchestratorTheme(
98
- getWfoStructuredSearchTableStyles,
99
- );
95
+ const { queryBuilderContainerStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
100
96
  const [fieldToOperatorMap, setFieldToOperatorMap] = useState<FieldToOperatorMap>(prefilledFieldOptions);
101
97
 
98
+ // Enter in a value editor commits its value on blur, and that state update has not
99
+ // flushed yet when the search runs in the same keydown. onQueryChange fires
100
+ // synchronously during the blur, so this ref always holds the freshest rule group,
101
+ // which is passed to handleSearch explicitly (same pattern as the remove-filter link).
102
+ const latestRuleGroupRef = useRef<RuleGroupType | undefined>(queryBuilderRuleGroup);
103
+ latestRuleGroupRef.current = queryBuilderRuleGroup;
104
+
105
+ const handleValueEditorEnter = () => {
106
+ handleSearch({ ruleGroup: latestRuleGroupRef.current });
107
+ };
108
+
102
109
  const handleFieldSelected = (field: string, operators: string[]) => {
103
110
  setFieldToOperatorMap((previousMap) => {
104
111
  return new Map(previousMap).set(field, operators);
@@ -106,93 +113,92 @@ export const WfoFilterBuilder = ({
106
113
  };
107
114
 
108
115
  return (
109
- <EuiFlexGroup css={isFilterBuilderVisible ? queryBuilderContainerStyles : undefined}>
110
- {(isFilterBuilderVisible && (
111
- <EuiFlexGroup direction={'column'}>
112
- <EuiFlexItem>
113
- <QueryBuilder
114
- query={queryBuilderRuleGroup}
115
- enableMountQueryChange={false}
116
- onQueryChange={(ruleGroup: RuleGroupType) => {
117
- onUpdateQueryBuilder(ruleGroup);
118
- }}
119
- context={{ onFieldSelected: handleFieldSelected, prefilledFieldOptions }}
120
- getOperators={(field) => {
121
- const operators = fieldToOperatorMap.get(field);
122
- return mapOperatorsToRQBOperatorOptions(operators);
123
- }}
124
- controlElements={{
125
- fieldSelector: WfoFieldSelector,
126
- operatorSelector: WfoOperatorSelector,
127
- valueEditor: WfoValueEditor,
128
- ruleGroup: WfoRuleGroup,
129
- rule: WfoRule,
130
- combinatorSelector: WfoCombinatorSelector,
131
- inlineCombinator: WfoInlineCombinator,
132
- addRuleAction: null,
133
- addGroupAction: null,
134
- removeGroupAction: null,
135
- removeRuleAction: WfoRemoveRuleAction,
136
- }}
137
- addRuleToNewGroups
138
- onAddGroup={onAddGroupHandler}
139
- maxLevels={5}
140
- showCombinatorsBetweenRules
141
- />
142
- </EuiFlexItem>
143
- <EuiFlexItem>
144
- <WfoAutoExpandableTextArea
145
- id={'searchbox-textarea'}
146
- value={filterString ?? ''}
147
- onChange={(e) => {
148
- const filterString = e.target.value;
149
- onUpdateFilterString(filterString);
150
- }}
151
- isInvalid={!isValidFilterString}
152
- />
153
- </EuiFlexItem>
154
-
155
- <EuiFlexGroup direction={'rowReverse'} alignItems={'center'}>
156
- <EuiButton
157
- onClick={() => {
116
+ <EuiFlexGroup css={queryBuilderContainerStyles}>
117
+ <EuiFlexGroup direction={'column'}>
118
+ <EuiFlexItem>
119
+ <QueryBuilder
120
+ query={queryBuilderRuleGroup}
121
+ enableMountQueryChange={false}
122
+ onQueryChange={(ruleGroup: RuleGroupType) => {
123
+ latestRuleGroupRef.current = ruleGroup;
124
+ onUpdateQueryBuilder(ruleGroup);
125
+ }}
126
+ context={{
127
+ onFieldSelected: handleFieldSelected,
128
+ prefilledFieldOptions,
129
+ onValueEditorEnter: handleValueEditorEnter,
130
+ }}
131
+ getOperators={(field) => {
132
+ const operators = fieldToOperatorMap.get(field);
133
+ return mapOperatorsToRQBOperatorOptions(operators);
134
+ }}
135
+ controlElements={{
136
+ fieldSelector: WfoFieldSelector,
137
+ operatorSelector: WfoOperatorSelector,
138
+ valueEditor: WfoValueEditor,
139
+ ruleGroup: WfoRuleGroup,
140
+ rule: WfoRule,
141
+ combinatorSelector: WfoCombinatorSelector,
142
+ inlineCombinator: WfoInlineCombinator,
143
+ addRuleAction: null,
144
+ addGroupAction: null,
145
+ removeGroupAction: null,
146
+ removeRuleAction: WfoRemoveRuleAction,
147
+ }}
148
+ addRuleToNewGroups
149
+ onAddGroup={onAddGroupHandler}
150
+ maxLevels={5}
151
+ showCombinatorsBetweenRules
152
+ resetOnFieldChange={false}
153
+ />
154
+ </EuiFlexItem>
155
+ <EuiFlexItem>
156
+ <WfoAutoExpandableTextArea
157
+ id={'searchbox-textarea'}
158
+ value={filterString ?? ''}
159
+ onChange={(e) => {
160
+ const filterString = e.target.value;
161
+ onUpdateFilterString(filterString);
162
+ }}
163
+ onKeyDown={(event) => {
164
+ // Enter applies the filter like the Apply button (and like it, does nothing
165
+ // while the filter string is invalid); Shift+Enter inserts a newline.
166
+ if (event.key !== 'Enter' || event.shiftKey) return;
167
+ event.preventDefault();
168
+ if (isValidFilterString) {
158
169
  handleSearch();
159
- }}
160
- id={'button-apply-filter'}
161
- data-test-id={'button-apply-filter'}
162
- fill
163
- type="submit"
164
- aria-label={t('applyFilter')}
165
- disabled={!isValidFilterString}
166
- >
167
- {t('applyFilter')}
168
- </EuiButton>
169
- <WfoTextAnchor
170
- text={t('removeFilter')}
171
- onClick={() => {
172
- onUpdateQueryBuilder(false);
173
- // we call with ruleGroup: false explictly to
174
- // avoid state not having caught up yet when searching
175
- handleSearch({ ruleGroup: false });
176
- onToggleFilterBuilder(false);
177
- }}
178
- />
179
- </EuiFlexGroup>
170
+ }
171
+ }}
172
+ isInvalid={!isValidFilterString}
173
+ />
174
+ </EuiFlexItem>
175
+
176
+ <EuiFlexGroup direction={'rowReverse'} alignItems={'center'}>
177
+ <EuiButton
178
+ onClick={() => {
179
+ handleSearch();
180
+ }}
181
+ id={'button-apply-filter'}
182
+ data-test-id={'button-apply-filter'}
183
+ fill
184
+ type="submit"
185
+ aria-label={t('applyFilter')}
186
+ disabled={!isValidFilterString}
187
+ >
188
+ {t('applyFilter')}
189
+ </EuiButton>
190
+ <WfoTextAnchor
191
+ text={t('removeFilter')}
192
+ onClick={() => {
193
+ onUpdateQueryBuilder(false);
194
+ // we call with ruleGroup: false explictly to
195
+ // avoid state not having caught up yet when searching
196
+ handleSearch({ ruleGroup: false });
197
+ onToggleFilterBuilder(false);
198
+ }}
199
+ />
180
200
  </EuiFlexGroup>
181
- )) || (
182
- <EuiButton
183
- css={toggleButtonStyles}
184
- onClick={() => onToggleFilterBuilder(true)}
185
- id={'button-toggle-filter-builder'}
186
- data-test-id={'button-toggle-filter-builder'}
187
- fill
188
- type="submit"
189
- iconType="filter"
190
- iconSide="left"
191
- aria-label={t('createFilter')}
192
- >
193
- {t('createFilter')}
194
- </EuiButton>
195
- )}
201
+ </EuiFlexGroup>
196
202
  </EuiFlexGroup>
197
203
  );
198
204
  };
@@ -1,5 +1,6 @@
1
- import React, { useEffect, useMemo } from 'react';
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
2
  import type { FullOperator, OperatorSelectorProps, OptionGroup } from 'react-querybuilder';
3
+ import { defaultOperators } from 'react-querybuilder';
3
4
 
4
5
  import { EuiSelect } from '@elastic/eui';
5
6
 
@@ -7,6 +8,8 @@ const isOptionGroup = (operator: FullOperator | OptionGroup<FullOperator>): oper
7
8
  'options' in operator;
8
9
 
9
10
  export const WfoOperatorSelector = (props: OperatorSelectorProps) => {
11
+ const { value, handleOnChange } = props;
12
+
10
13
  const flatOptions = (props.options as Array<FullOperator | OptionGroup<FullOperator>>).flatMap((option) =>
11
14
  isOptionGroup(option) ? option.options : [option],
12
15
  );
@@ -16,17 +19,39 @@ export const WfoOperatorSelector = (props: OperatorSelectorProps) => {
16
19
  [flatOptions],
17
20
  );
18
21
 
22
+ const optionsKey = selectOptions.map((option) => option.value).join('|');
23
+ const previousOptionsKeyRef = useRef(optionsKey);
24
+
19
25
  useEffect(() => {
20
- if (props.options && !props.value && selectOptions.length > 0) {
21
- props.handleOnChange(selectOptions[0].value);
26
+ const optionsChanged = previousOptionsKeyRef.current !== optionsKey;
27
+ previousOptionsKeyRef.current = optionsKey;
28
+
29
+ // Reset to the first option only when the field's operator list changes — i.e. the
30
+ // user picked a (different) field, and resetOnFieldChange=false on QueryBuilder
31
+ // preserved an operator that is not valid for it. The `optionsChanged` guard keeps
32
+ // rules restored from CEL (URL or textarea) intact on mount: parseCEL can produce
33
+ // operators outside the prefilled operator lists (e.g. beginsWith), and resetting
34
+ // those here would silently rewrite the user's filter string.
35
+ if (!optionsChanged || selectOptions.length === 0) return;
36
+ const currentValueIsValid = selectOptions.some((option) => option.value === value);
37
+ if (!currentValueIsValid) {
38
+ handleOnChange(selectOptions[0].value);
22
39
  }
23
- }, [props, props.options, props.value, selectOptions]);
40
+ }, [optionsKey, selectOptions, value, handleOnChange]);
41
+
42
+ // A restored operator that falls outside the field's list must still be visible in
43
+ // the dropdown; without it EuiSelect renders an empty selection for the rule.
44
+ const currentValueIsListed = !value || selectOptions.some((option) => option.value === value);
45
+ const displayOptions =
46
+ currentValueIsListed ? selectOptions : (
47
+ [...selectOptions, { value, text: defaultOperators.find((operator) => operator.name === value)?.label ?? value }]
48
+ );
24
49
 
25
50
  return (
26
51
  <EuiSelect
27
- options={selectOptions}
28
- value={props.value}
29
- onChange={(e) => props.handleOnChange(e.target.value)}
52
+ options={displayOptions}
53
+ value={value}
54
+ onChange={(e) => handleOnChange(e.target.value)}
30
55
  disabled={props.disabled}
31
56
  />
32
57
  );
@@ -13,7 +13,8 @@ import { WfoRemoveGroupAction } from './WfoRemoveGroupAction';
13
13
 
14
14
  export const WfoRuleGroup = (props: RuleGroupProps) => {
15
15
  const ruleGroupProps = useRuleGroup(props);
16
- const ruleId = ruleGroupProps.ruleGroup.id;
16
+ const rulePath = ruleGroupProps.path;
17
+ const isRootGroup: boolean = !rulePath || rulePath.length === 0;
17
18
 
18
19
  const {
19
20
  ruleGroupContainerBlueStyles,
@@ -24,9 +25,9 @@ export const WfoRuleGroup = (props: RuleGroupProps) => {
24
25
  } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
25
26
  const getGroupContainerStyles = () => {
26
27
  if (ruleGroupProps.path.length % 2) {
27
- return ruleId !== 'root' ? innerGroupContainerWhiteStyles : ruleGroupContainerWhiteStyles;
28
+ return !isRootGroup ? innerGroupContainerWhiteStyles : ruleGroupContainerWhiteStyles;
28
29
  } else {
29
- return ruleId !== 'root' ? innerGroupContainerBlueStyles : ruleGroupContainerBlueStyles;
30
+ return !isRootGroup ? innerGroupContainerBlueStyles : ruleGroupContainerBlueStyles;
30
31
  }
31
32
  };
32
33
 
@@ -70,7 +71,7 @@ export const WfoRuleGroup = (props: RuleGroupProps) => {
70
71
  </EuiFlexItem>
71
72
  </EuiFlexGroup>
72
73
  </EuiFlexItem>
73
- {ruleId !== 'root' && <WfoRemoveGroupAction onClick={ruleGroupProps.removeGroup} />}
74
+ {!isRootGroup && <WfoRemoveGroupAction onClick={ruleGroupProps.removeGroup} />}
74
75
  </EuiFlexGroup>
75
76
  );
76
77
  };
@@ -9,7 +9,6 @@ import {
9
9
  DEFAULT_PAGE_SIZE,
10
10
  DEFAULT_PAGE_SIZES,
11
11
  TableColumnKeys,
12
- TableSettingsColumnConfig,
13
12
  TableSettingsConfig,
14
13
  TableSettingsModal,
15
14
  WfoDataSearch,
@@ -19,6 +18,7 @@ import {
19
18
  WfoKeyValueTable,
20
19
  WfoKeyValueTableDataType,
21
20
  clearTableConfigFromLocalStorage,
21
+ getTableSettingsColumns,
22
22
  setTableConfigToLocalStorage,
23
23
  } from '@/components';
24
24
  import { getRowDetailData } from '@/components/WfoTable/WfoAdvancedTable/getRowDetailData';
@@ -27,7 +27,7 @@ import {
27
27
  WfoTableControlColumnConfigItem,
28
28
  WfoTableDataColumnConfigItem,
29
29
  } from '@/components/WfoTable/WfoTable';
30
- import { useOrchestratorTheme } from '@/hooks';
30
+ import { useOrchestratorTheme, useWithOrchestratorTheme } from '@/hooks';
31
31
  import { WfoArrowsExpand } from '@/icons';
32
32
  import { WfoGraphqlError } from '@/rtk';
33
33
  import { FieldToOperatorMap, RetrieverType } from '@/types';
@@ -36,6 +36,7 @@ import { getDefaultTableConfig } from '@/utils';
36
36
  import { ColumnType, WfoTable, WfoTableProps } from '../WfoTable';
37
37
  import { WfoFilterBuilder } from './WfoFilterBuilder';
38
38
  import { WfoSearchFieldWithActions } from './WfoSearchFieldWithActions';
39
+ import { getWfoStructuredSearchTableStyles } from './styles';
39
40
  import { buildColumnFilter } from './utils';
40
41
 
41
42
  export type WfoStructuredSearchTableDataColumnConfigItem<
@@ -129,7 +130,7 @@ export const WfoStructuredSearchTable = <T extends object>({
129
130
  ...tableProps
130
131
  }: WfoStructuredSearchTableProps<T>) => {
131
132
  const { theme } = useOrchestratorTheme();
132
-
133
+ const { toggleButtonStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
133
134
  const [hiddenColumns, setHiddenColumns] = useState<TableColumnKeys<T>>(defaultHiddenColumns);
134
135
  const [isFilterBuilderVisible, setIsFilterBuilderVisible] = useState(false);
135
136
  const [showTableSettingsModal, setShowTableSettingsModal] = useState(false);
@@ -166,17 +167,7 @@ export const WfoStructuredSearchTable = <T extends object>({
166
167
  ...tableColumnConfig,
167
168
  };
168
169
 
169
- const tableSettingsColumns: TableSettingsColumnConfig<T>[] = Object.entries(tableColumnConfig).map(
170
- ([key, { label }]): TableSettingsColumnConfig<T> => {
171
- const field = key as keyof T;
172
-
173
- return {
174
- field,
175
- name: label,
176
- isVisible: hiddenColumns.indexOf(field) === -1,
177
- };
178
- },
179
- );
170
+ const tableSettingsColumns = getTableSettingsColumns(tableColumnConfig, hiddenColumns);
180
171
 
181
172
  const rowDetailData: WfoKeyValueTableDataType[] | undefined =
182
173
  rowDetailModalData && getRowDetailData(rowDetailModalData, tableColumnConfig);
@@ -211,24 +202,26 @@ export const WfoStructuredSearchTable = <T extends object>({
211
202
  handleSearch({ ruleGroup: columnFilter.ruleGroup });
212
203
  };
213
204
 
214
- const filterBuilder = (
215
- <WfoFilterBuilder
216
- filterString={filterString}
217
- onUpdateFilterString={onUpdateFilterString}
218
- isValidFilterString={isValidFilterString}
219
- queryBuilderRuleGroup={queryBuilderRuleGroup}
220
- onUpdateQueryBuilder={onUpdateQueryBuilder}
221
- handleSearch={handleSearch}
222
- isFilterBuilderVisible={isFilterBuilderVisible}
223
- onToggleFilterBuilder={setIsFilterBuilderVisible}
224
- prefilledFieldOptions={prefilledFieldOptions}
225
- />
226
- );
227
-
228
205
  return (
229
206
  <>
230
207
  <EuiFlexGroup alignItems="center" gutterSize="s">
231
- {!isFilterBuilderVisible && <EuiFlexItem grow={false}>{filterBuilder}</EuiFlexItem>}
208
+ {!isFilterBuilderVisible && (
209
+ <EuiFlexItem grow={false}>
210
+ <EuiButton
211
+ css={toggleButtonStyles}
212
+ onClick={() => setIsFilterBuilderVisible(true)}
213
+ id={'button-toggle-filter-builder'}
214
+ data-test-id={'button-toggle-filter-builder'}
215
+ fill
216
+ type="submit"
217
+ iconType="filter"
218
+ iconSide="left"
219
+ aria-label={t('createFilter')}
220
+ >
221
+ {t('createFilter')}
222
+ </EuiButton>
223
+ </EuiFlexItem>
224
+ )}
232
225
  <WfoSearchFieldWithActions
233
226
  queryText={queryText}
234
227
  onChangeQueryText={onChangeQueryText}
@@ -241,7 +234,16 @@ export const WfoStructuredSearchTable = <T extends object>({
241
234
  {isFilterBuilderVisible && (
242
235
  <>
243
236
  <EuiSpacer size="s" />
244
- {filterBuilder}
237
+ <WfoFilterBuilder
238
+ filterString={filterString}
239
+ onUpdateFilterString={onUpdateFilterString}
240
+ isValidFilterString={isValidFilterString}
241
+ queryBuilderRuleGroup={queryBuilderRuleGroup}
242
+ onUpdateQueryBuilder={onUpdateQueryBuilder}
243
+ handleSearch={handleSearch}
244
+ onToggleFilterBuilder={setIsFilterBuilderVisible}
245
+ prefilledFieldOptions={prefilledFieldOptions}
246
+ />
245
247
  </>
246
248
  )}
247
249