@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
@@ -15,12 +15,14 @@ export type WfoSubscriptionActionExpandableMenuItemProps = {
15
15
  subscriptionAction: SubscriptionAction;
16
16
  onClickLockedRelation: (relation: SubscriptionRelation) => void;
17
17
  children: React.ReactNode;
18
+ subscriptionPath?: string;
18
19
  };
19
20
 
20
21
  export const WfoSubscriptionActionExpandableMenuItem: FC<WfoSubscriptionActionExpandableMenuItemProps> = ({
21
22
  subscriptionAction,
22
23
  onClickLockedRelation,
23
24
  children,
25
+ subscriptionPath = PATH_SUBSCRIPTIONS,
24
26
  }) => {
25
27
  const t = useTranslations('subscriptions.detail.actions');
26
28
 
@@ -29,18 +31,21 @@ export const WfoSubscriptionActionExpandableMenuItem: FC<WfoSubscriptionActionEx
29
31
  const [isExpanded, setIsExpanded] = useState(false);
30
32
 
31
33
  // TODO: remove lockedRelationsUuids fallback when orchestrator-core 6.0.0 is released and only use the _detail variant
32
- const lockedRelationsDetail = subscriptionAction.locked_relations_detail;
33
- const lockedRelationsUuids = subscriptionAction.locked_relations;
34
- const hasLockedRelations = (lockedRelationsDetail?.length ?? 0) > 0 || (lockedRelationsUuids?.length ?? 0) > 0;
34
+ const relationsDetail = [
35
+ ...(subscriptionAction.locked_relations_detail ?? []),
36
+ ...(subscriptionAction.unterminated_in_use_by_subscriptions_detail ?? []),
37
+ ];
38
+ const relationsUuids = [
39
+ ...(subscriptionAction.locked_relations ?? []),
40
+ ...(subscriptionAction.unterminated_in_use_by_subscriptions ?? []),
41
+ ];
42
+ const hasRelations = relationsDetail.length > 0 || relationsUuids.length > 0;
35
43
 
36
44
  return (
37
45
  <div>
38
- <div
39
- css={[expandableMenuItemStyle, hasLockedRelations && clickableStyle]}
40
- onClick={() => setIsExpanded(!isExpanded)}
41
- >
46
+ <div css={[expandableMenuItemStyle, hasRelations && clickableStyle]} onClick={() => setIsExpanded(!isExpanded)}>
42
47
  <div>{children}</div>
43
- {hasLockedRelations && (
48
+ {hasRelations && (
44
49
  <EuiButtonIcon
45
50
  css={expandButtonStyle}
46
51
  iconType={isExpanded ? 'arrowDown' : 'arrowRight'}
@@ -49,17 +54,17 @@ export const WfoSubscriptionActionExpandableMenuItem: FC<WfoSubscriptionActionEx
49
54
  />
50
55
  )}
51
56
  </div>
52
- {hasLockedRelations && isExpanded && (
57
+ {hasRelations && isExpanded && (
53
58
  <div css={expandedContentStyle}>
54
59
  <EuiText size="xs">{t('lockedBySubscriptions')}</EuiText>
55
60
  <ul css={{ margin: 0, paddingLeft: 16, listStyleType: 'disc' }}>
56
- {lockedRelationsDetail ?
57
- lockedRelationsDetail.map((relation) => (
61
+ {relationsDetail.length > 0 ?
62
+ relationsDetail.map((relation) => (
58
63
  <li key={relation.subscription_id}>
59
64
  <EuiText size="xs">
60
65
  <Link
61
66
  css={linkStyle}
62
- href={`${PATH_SUBSCRIPTIONS}/${relation.subscription_id}`}
67
+ href={`${subscriptionPath}/${relation.subscription_id}`}
63
68
  target="_blank"
64
69
  onClick={() => onClickLockedRelation(relation)}
65
70
  >
@@ -68,10 +73,10 @@ export const WfoSubscriptionActionExpandableMenuItem: FC<WfoSubscriptionActionEx
68
73
  </EuiText>
69
74
  </li>
70
75
  ))
71
- : lockedRelationsUuids?.map((id) => (
76
+ : relationsUuids.map((id) => (
72
77
  <li key={id}>
73
78
  <EuiText size="xs">
74
- <Link css={linkStyle} href={`${PATH_SUBSCRIPTIONS}/${id}`} target="_blank">
79
+ <Link css={linkStyle} href={`${subscriptionPath}/${id}`} target="_blank">
75
80
  {id}
76
81
  </Link>
77
82
  </EuiText>
@@ -5,7 +5,13 @@ import { useRouter } from 'next/router';
5
5
 
6
6
  import { EuiButton, EuiButtonIcon, EuiLoadingSpinner, EuiTitle } from '@elastic/eui';
7
7
 
8
- import { PATH_START_NEW_TASK, PATH_START_NEW_WORKFLOW, WfoInSyncField, WfoPopover } from '@/components';
8
+ import {
9
+ PATH_START_NEW_TASK,
10
+ PATH_START_NEW_WORKFLOW,
11
+ PATH_SUBSCRIPTIONS,
12
+ WfoInSyncField,
13
+ WfoPopover,
14
+ } from '@/components';
9
15
  import { WfoSubscriptionActionsMenuItem } from '@/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionsMenuItem';
10
16
  import { useActiveProcess } from '@/components/WfoSubscription/WfoSubscriptionActions/utils';
11
17
  import { PolicyResource } from '@/configuration/policy-resources';
@@ -27,12 +33,14 @@ export type WfoSubscriptionActionsProps = {
27
33
  subscriptionId: string;
28
34
  isLoading?: boolean;
29
35
  compactMode?: boolean;
36
+ subscriptionPath?: string;
30
37
  };
31
38
 
32
39
  export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
33
40
  subscriptionId,
34
41
  isLoading,
35
42
  compactMode = false,
43
+ subscriptionPath = PATH_SUBSCRIPTIONS,
36
44
  }) => {
37
45
  const t = useTranslations('subscriptions.detail.actions');
38
46
  const { theme } = useOrchestratorTheme();
@@ -132,6 +140,7 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
132
140
  subscriptionAction={subscriptionAction}
133
141
  target={WorkflowTarget.VALIDATE}
134
142
  setPopover={setPopover}
143
+ subscriptionPath={subscriptionPath}
135
144
  onClick={() => handleActionClick(subscriptionAction.name, compactMode, true)}
136
145
  isLoading={buttonIsLoading}
137
146
  />
@@ -152,6 +161,7 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
152
161
  }
153
162
  target={WorkflowTarget.RECONCILE}
154
163
  setPopover={setPopover}
164
+ subscriptionPath={subscriptionPath}
155
165
  onClick={() => handleActionClick(subscriptionAction.name, compactMode, false)}
156
166
  isLoading={buttonIsLoading}
157
167
  />
@@ -182,6 +192,7 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
182
192
  subscriptionAction={subscriptionAction}
183
193
  target={WorkflowTarget.MODIFY}
184
194
  setPopover={setPopover}
195
+ subscriptionPath={subscriptionPath}
185
196
  onClick={() => {
186
197
  redirectToUrl(subscriptionAction.name);
187
198
  }}
@@ -199,6 +210,7 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
199
210
  subscriptionAction={subscriptionAction}
200
211
  target={WorkflowTarget.TERMINATE}
201
212
  setPopover={setPopover}
213
+ subscriptionPath={subscriptionPath}
202
214
  onClick={() => {
203
215
  redirectToUrl(subscriptionAction.name);
204
216
  }}
@@ -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);
@@ -12,7 +12,7 @@ export const WfoAddGroupAction = ({ disabled, handleOnClick }: ActionProps) => {
12
12
  const t = useTranslations('search.page');
13
13
  const { addGroupStyles, addRulePlusStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
14
14
  return disabled ? null : (
15
- <EuiFlexItem css={addGroupStyles} onClick={() => handleOnClick()}>
15
+ <EuiFlexItem grow={true} css={addGroupStyles} onClick={() => handleOnClick()}>
16
16
  <span css={addRulePlusStyles}>+</span>&nbsp;{t('addGroup')}
17
17
  </EuiFlexItem>
18
18
  );
@@ -8,6 +8,9 @@ import { useWithOrchestratorTheme } from '@/hooks';
8
8
 
9
9
  export const WfoCombinatorSelector = (props: CombinatorSelectorProps) => {
10
10
  const { buttonGroupStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
11
+
12
+ if (props.rules.length < 2) return null;
13
+
11
14
  const options = props.options.map((option) => ({
12
15
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
13
16
  // @ts-ignore This seems to be a typing error in the react-querybuilde library. Option.name does exist
@@ -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>
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import { FieldSelectorProps } from 'react-querybuilder';
3
3
 
4
4
  import { useTranslations } from 'next-intl';
@@ -7,16 +7,42 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui';
7
7
  import { EuiComboBox } from '@elastic/eui';
8
8
 
9
9
  import { usePathAutocomplete } from '@/hooks';
10
- import { EntityKind, PathInfo } from '@/types';
10
+ import { EntityKind, FieldToOperatorMap, PathInfo } from '@/types';
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
+ };
11
32
 
12
33
  export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: FieldSelectorProps) => {
13
34
  const { field } = rule;
35
+ const prefilledFieldOptions: FieldToOperatorMap = context.prefilledFieldOptions;
14
36
  const [selectedValue, setSelectedValue] = useState<string>(field);
37
+ const [searchInput, setSearchInput] = useState<HTMLInputElement | null>(null);
38
+ const optionsRef = useRef<EuiComboBoxOptionOption<string>[]>([]);
39
+ const handleFieldSelectionRef = useRef<(selected: EuiComboBoxOptionOption<string>[]) => void>(() => {});
15
40
  const t = useTranslations('search.page');
16
41
  const getOption = (path: string) => ({
17
42
  value: path,
18
43
  label: path,
19
44
  });
45
+
20
46
  const getOptionsFromPathInfo = (pathInfos: PathInfo[]): EuiComboBoxOptionOption<string>[] => {
21
47
  const pathOptions: EuiComboBoxOptionOption<string>[] = [];
22
48
 
@@ -40,14 +66,26 @@ export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: Fi
40
66
  error: errorMessage,
41
67
  } = usePathAutocomplete(selectedValue, EntityKind.SUBSCRIPTION);
42
68
 
43
- const options = getOptionsFromPathInfo(paths);
69
+ const prefilledOptions: EuiComboBoxOptionOption<string>[] = Array.from(prefilledFieldOptions.keys()).map(getOption);
70
+ const autocompleteOptions = getOptionsFromPathInfo(paths);
71
+ const placeholderOption: EuiComboBoxOptionOption<string> = {
72
+ label: '──────',
73
+ disabled: true,
74
+ };
75
+ const showPlaceholder = prefilledOptions.length > 0 && autocompleteOptions.length > 0;
76
+ const options: EuiComboBoxOptionOption<string>[] = [
77
+ ...prefilledOptions,
78
+ ...(showPlaceholder ? [placeholderOption] : []),
79
+ ...autocompleteOptions,
80
+ ];
44
81
 
45
82
  const storeFieldOperators = (selectedValue: string) => {
46
83
  const matchingPath =
47
84
  paths.find((path) => path.path === selectedValue)
48
85
  ?? paths.find((path) => path.availablePaths?.includes(selectedValue));
86
+ const operators = matchingPath?.operators ?? prefilledFieldOptions.get(selectedValue) ?? [];
49
87
 
50
- context?.onFieldSelected?.(selectedValue, matchingPath);
88
+ context?.onFieldSelected?.(selectedValue, operators);
51
89
  };
52
90
 
53
91
  const handleFieldSelection = (selectedOptions: EuiComboBoxOptionOption<string>[]) => {
@@ -57,8 +95,49 @@ export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: Fi
57
95
  storeFieldOperators(selectedValue);
58
96
 
59
97
  handleOnChange(selectedValue);
98
+
99
+ if (selectedValue) focusValueEditorAfterRender(searchInput);
60
100
  };
61
101
 
102
+ // EuiComboBox only auto-selects on Enter/Tab when exactly one option matches the typed
103
+ // text. When several options share the typed prefix, the exact-match option is not picked
104
+ // unless the user arrow-navigates. Patch that with a native keydown listener on the input.
105
+ optionsRef.current = options;
106
+ handleFieldSelectionRef.current = handleFieldSelection;
107
+
108
+ useEffect(() => {
109
+ if (!searchInput) return;
110
+
111
+ const autoSelectExactMatchOption = (event: KeyboardEvent) => {
112
+ if (event.key !== 'Enter' && event.key !== 'Tab') return;
113
+ // Let EuiComboBox handle selection when an option is keyboard-highlighted.
114
+ if (searchInput.getAttribute('aria-activedescendant')) return;
115
+ const typed = searchInput.value.trim();
116
+ if (!typed) return;
117
+ const exactMatch = optionsRef.current.find(
118
+ (option) => !option.disabled && option.label.toLowerCase() === typed.toLowerCase(),
119
+ );
120
+ if (!exactMatch) return;
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();
125
+ handleFieldSelectionRef.current([exactMatch]);
126
+
127
+ // EuiComboBox keeps its own internal `searchValue` and `isListOpen` state. Clearing
128
+ // the value via the React-recognised native setter + input event suppresses EUI's
129
+ // "typed-but-not-selected" warning icon (the `markAsInvalid` branch in combo_box.js).
130
+ // The synthetic Escape closes the dropdown without disturbing focus.
131
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
132
+ nativeInputValueSetter?.call(searchInput, '');
133
+ searchInput.dispatchEvent(new Event('input', { bubbles: true }));
134
+ searchInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
135
+ };
136
+
137
+ searchInput.addEventListener('keydown', autoSelectExactMatchOption);
138
+ return () => searchInput.removeEventListener('keydown', autoSelectExactMatchOption);
139
+ }, [searchInput]);
140
+
62
141
  return (
63
142
  <EuiComboBox
64
143
  placeholder={t('searchFieldsPlaceholder')}
@@ -73,6 +152,7 @@ export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: Fi
73
152
  setSelectedValue(inputValue);
74
153
  }
75
154
  }}
155
+ inputRef={setSearchInput}
76
156
  singleSelection={{ asPlainText: true }}
77
157
  isLoading={isLoading}
78
158
  isClearable