@orchestrator-ui/orchestrator-ui-components 8.8.1 → 8.9.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 (42) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +15 -14
  4. package/CHANGELOG.md +23 -0
  5. package/dist/index.d.ts +63 -24
  6. package/dist/index.js +2695 -2480
  7. package/dist/index.js.map +1 -1
  8. package/package.json +4 -3
  9. package/src/components/WfoBadges/WfoProductStatusBadge/WfoProductStatusBadge.tsx +2 -5
  10. package/src/components/WfoPydanticForm/fields/WfoMultiCheckboxField.tsx +2 -1
  11. package/src/components/WfoStartButton/WfoStartButtonComboBox.tsx +6 -11
  12. package/src/components/WfoStartButton/WfoStartWorkflowComboBox.tsx +1 -1
  13. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +6 -16
  14. package/src/components/WfoSubscription/utils/utils.spec.ts +57 -0
  15. package/src/components/WfoSubscription/utils/utils.ts +15 -0
  16. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +12 -8
  17. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +23 -10
  18. package/src/components/WfoTable/WfoStructuredSearchTable/WfoSearchHelpModal.tsx +59 -0
  19. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +37 -20
  20. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +4 -2
  21. package/src/components/WfoTable/WfoTable/WfoTable.tsx +77 -46
  22. package/src/components/WfoTable/WfoTable/WfoTableSkeletonRows.tsx +52 -0
  23. package/src/components/WfoTable/WfoTable/index.ts +1 -0
  24. package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.tsx +21 -18
  25. package/src/components/WfoTable/WfoTableSettingsModal/styles.ts +17 -0
  26. package/src/components/WfoTable/utils/tableConfigPersistence.ts +1 -0
  27. package/src/configuration/version.ts +1 -1
  28. package/src/hooks/useGetPydanticFormsConfig.tsx +5 -3
  29. package/src/hooks/useSearch.ts +2 -2
  30. package/src/hooks/useStoredTableConfig.ts +1 -0
  31. package/src/messages/en-GB.json +21 -0
  32. package/src/messages/nl-NL.json +22 -1
  33. package/src/pages/WfoSearchPocPage.tsx +28 -23
  34. package/src/pages/processes/WfoProductInformationWithLink.tsx +9 -4
  35. package/src/rtk/endpoints/metadata/productBlocks.ts +9 -1
  36. package/src/rtk/endpoints/metadata/products.ts +5 -2
  37. package/src/rtk/endpoints/startOptions.ts +4 -3
  38. package/src/types/search.ts +8 -0
  39. package/src/types/types.ts +3 -3
  40. package/src/utils/getProductLifecycleStatus.spec.ts +26 -0
  41. package/src/utils/getProductLifecycleStatus.ts +13 -0
  42. package/src/utils/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-ui/orchestrator-ui-components",
3
- "version": "8.8.1",
3
+ "version": "8.9.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Library of UI Components used to display the workflow orchestrator frontend",
6
6
  "author": {
@@ -51,7 +51,7 @@
51
51
  "next-query-params": "^5.0.0",
52
52
  "object-hash": "^3.0.0",
53
53
  "prism-themes": "^1.9.0",
54
- "pydantic-forms": "^3.0.1",
54
+ "pydantic-forms": "^3.1.0",
55
55
  "react-diff-view": "^3.2.0",
56
56
  "react-draggable": "^4.4.6",
57
57
  "react-querybuilder": "^8.18.0",
@@ -59,7 +59,8 @@
59
59
  "react-select": "^5.8.0",
60
60
  "scroll-into-view": "^1.16.2",
61
61
  "unidiff": "^1.0.4",
62
- "use-query-params": "^2.2.2"
62
+ "use-query-params": "^2.2.2",
63
+ "zod": "^4.4.3"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@elastic/eui": "^113.0.0",
@@ -8,16 +8,13 @@ export type WfoProductStatusBadgeProps = {
8
8
  status: ProductLifecycleStatus;
9
9
  };
10
10
 
11
- const normalizedStatus = (status: ProductLifecycleStatus): string => status.toLowerCase().replace(/_/g, ' ');
12
-
13
11
  export const WfoProductStatusBadge: FC<WfoProductStatusBadgeProps> = ({ status }) => {
14
12
  const { theme, toSecondaryColor } = useOrchestratorTheme();
15
13
 
16
- const productLifeCycleStatus = normalizedStatus(status);
17
14
  const getBadgeColorFromStatus = () => {
18
15
  const { primary, borderBaseSubdued, textPrimary, textParagraph, success, textSuccess } = theme.colors;
19
16
 
20
- switch (productLifeCycleStatus) {
17
+ switch (status) {
21
18
  case ProductLifecycleStatus.ACTIVE:
22
19
  return {
23
20
  badgeColor: toSecondaryColor(success),
@@ -41,7 +38,7 @@ export const WfoProductStatusBadge: FC<WfoProductStatusBadgeProps> = ({ status }
41
38
 
42
39
  return (
43
40
  <WfoBadge textColor={textColor} color={badgeColor}>
44
- {productLifeCycleStatus}
41
+ {status}
45
42
  </WfoBadge>
46
43
  );
47
44
  };
@@ -24,7 +24,8 @@ export const WfoMultiCheckboxField: PydanticFormControlledElement = ({ pydanticF
24
24
 
25
25
  const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState<Record<string, boolean>>({});
26
26
 
27
- const { options, id } = pydanticFormField;
27
+ const { arrayItem, id } = pydanticFormField;
28
+ const options = arrayItem?.options || [];
28
29
 
29
30
  const checkboxes = options?.map((option, index) => ({
30
31
  label: option.label,
@@ -1,12 +1,11 @@
1
1
  import React, { useState } from 'react';
2
2
 
3
- import { capitalize } from 'lodash';
4
-
5
3
  import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiPopover, EuiSelectable, EuiSpacer } from '@elastic/eui';
6
4
 
7
5
  import { useGetOrchestratorConfig, useOrchestratorTheme, useWithOrchestratorTheme } from '@/hooks';
8
6
  import { WfoChevronDown, WfoPlusCircleFill } from '@/icons';
9
7
  import { ProductLifecycleStatus, StartComboBoxOption } from '@/types';
8
+ import { getProductLifecycleStatus } from '@/utils';
10
9
 
11
10
  import { getStyles } from './styles';
12
11
 
@@ -16,12 +15,8 @@ export type WfoStartButtonComboBoxProps = {
16
15
  onOptionChange: (selectedOption: StartComboBoxOption) => void;
17
16
  isProcess: boolean;
18
17
  className?: string;
19
- selectedProductStatus?: ProductLifecycleStatus | string;
20
- setSelectedProductStatus?: (status: ProductLifecycleStatus | string) => void;
21
- };
22
-
23
- const formatProductStatusLabel = (productStatus: string) => {
24
- return capitalize(productStatus.replace(/_/g, ' '));
18
+ selectedProductStatus?: ProductLifecycleStatus;
19
+ setSelectedProductStatus?: (status: ProductLifecycleStatus) => void;
25
20
  };
26
21
 
27
22
  export const WfoStartButtonComboBox = ({
@@ -67,7 +62,7 @@ export const WfoStartButtonComboBox = ({
67
62
  iconType={() => <WfoChevronDown height={18} width={18} color="currentColor" />}
68
63
  onClick={() => setFilterPopoverOpen((v) => !v)}
69
64
  >
70
- <b>{formatProductStatusLabel(selectedProductStatus ?? ProductLifecycleStatus.ACTIVE)}</b>
65
+ <b>{selectedProductStatus ?? ProductLifecycleStatus.ACTIVE}</b>
71
66
  </EuiButtonEmpty>
72
67
  }
73
68
  isOpen={isFilterPopoverOpen}
@@ -80,11 +75,11 @@ export const WfoStartButtonComboBox = ({
80
75
  key={productStatus}
81
76
  size="xs"
82
77
  onClick={() => {
83
- setSelectedProductStatus(productStatus.toUpperCase());
78
+ setSelectedProductStatus(getProductLifecycleStatus(productStatus));
84
79
  setFilterPopoverOpen(false);
85
80
  }}
86
81
  >
87
- {formatProductStatusLabel(productStatus ?? ProductLifecycleStatus.ACTIVE)}
82
+ {productStatus ?? ProductLifecycleStatus.ACTIVE}
88
83
  </EuiButtonEmpty>
89
84
  </div>
90
85
  ))}
@@ -14,7 +14,7 @@ export const WfoStartWorkflowButtonComboBox = () => {
14
14
  const router = useRouter();
15
15
  const t = useTranslations('common');
16
16
  const { isEngineRunningNow } = useCheckEngineStatus();
17
- const [selectedProductStatus, setSelectedProductStatus] = React.useState<ProductLifecycleStatus | string>(
17
+ const [selectedProductStatus, setSelectedProductStatus] = React.useState<ProductLifecycleStatus>(
18
18
  ProductLifecycleStatus.ACTIVE,
19
19
  );
20
20
 
@@ -12,13 +12,14 @@ import {
12
12
  WfoInSyncField,
13
13
  WfoPopover,
14
14
  } from '@/components';
15
+ import { getActionItemsByTarget } from '@/components/WfoSubscription';
15
16
  import { WfoSubscriptionActionsMenuItem } from '@/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionsMenuItem';
16
17
  import { useActiveProcess } from '@/components/WfoSubscription/WfoSubscriptionActions/utils';
17
18
  import { PolicyResource } from '@/configuration/policy-resources';
18
19
  import { useOrchestratorTheme, usePolicy } from '@/hooks';
19
20
  import { WfoDotsHorizontal } from '@/icons/WfoDotsHorizontal';
20
21
  import { useGetSubscriptionActionsQuery, useGetSubscriptionDetailQuery, useStartProcessMutation } from '@/rtk';
21
- import { SubscriptionAction, WorkflowTarget } from '@/types';
22
+ import { WorkflowTarget } from '@/types';
22
23
 
23
24
  type MenuBlockProps = {
24
25
  title: string;
@@ -129,21 +130,10 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
129
130
  }
130
131
  };
131
132
 
132
- const getActionItems = (workflowTarget: WorkflowTarget): SubscriptionAction[] => {
133
- // Core versions 5.2 and lower return subscriptionActions result with lowercase keynames. Higher version return them uppercased to align
134
- // with all the other places they are used. We support both for now. The lowercase keys are deliberately not part of the
135
- // SubscriptionActions type, so the fallback is a runtime-only check behind a cast.
136
- const actionsByTarget = subscriptionActions as unknown as
137
- | Record<string, SubscriptionAction[] | undefined>
138
- | undefined;
139
-
140
- return actionsByTarget?.[workflowTarget] ?? actionsByTarget?.[workflowTarget.toLowerCase()] ?? [];
141
- };
142
-
143
- const validateActionItems = getActionItems(WorkflowTarget.VALIDATE);
144
- const reconcileActionItems = getActionItems(WorkflowTarget.RECONCILE);
145
- const modifyActionItems = getActionItems(WorkflowTarget.MODIFY);
146
- const terminateActionItems = getActionItems(WorkflowTarget.TERMINATE);
133
+ const validateActionItems = getActionItemsByTarget(WorkflowTarget.VALIDATE, subscriptionActions);
134
+ const reconcileActionItems = getActionItemsByTarget(WorkflowTarget.RECONCILE, subscriptionActions);
135
+ const modifyActionItems = getActionItemsByTarget(WorkflowTarget.MODIFY, subscriptionActions);
136
+ const terminateActionItems = getActionItemsByTarget(WorkflowTarget.TERMINATE, subscriptionActions);
147
137
 
148
138
  const compactItems = (
149
139
  <>
@@ -5,11 +5,13 @@ import {
5
5
  ProcessStatus,
6
6
  ProductBlockInstance,
7
7
  SubscriptionAction,
8
+ SubscriptionActions,
8
9
  SubscriptionDetailProcess,
9
10
  WorkflowTarget,
10
11
  } from '../../../types';
11
12
  import {
12
13
  flattenSubscriptionActionProps,
14
+ getActionItemsByTarget,
13
15
  getFieldFromProductBlockInstanceValues,
14
16
  getLastUncompletedProcess,
15
17
  getLatestTaskDate,
@@ -470,6 +472,61 @@ describe('mapProductBlockInstancesToEuiSelectableOptions', () => {
470
472
  });
471
473
  });
472
474
 
475
+ describe('getActionItemsByTarget', () => {
476
+ const modifyAction: SubscriptionAction = {
477
+ name: 'modify_note',
478
+ description: 'Modify note',
479
+ };
480
+ const terminateAction: SubscriptionAction = {
481
+ name: 'terminate',
482
+ description: 'Terminate subscription',
483
+ };
484
+
485
+ const subscriptionActions: SubscriptionActions = {
486
+ [WorkflowTarget.MODIFY]: [modifyAction],
487
+ [WorkflowTarget.TERMINATE]: [terminateAction],
488
+ [WorkflowTarget.SYSTEM]: [],
489
+ [WorkflowTarget.VALIDATE]: [],
490
+ [WorkflowTarget.RECONCILE]: [],
491
+ };
492
+
493
+ it('returns actions for the given target with uppercase keys', () => {
494
+ expect(getActionItemsByTarget(WorkflowTarget.MODIFY, subscriptionActions)).toEqual([modifyAction]);
495
+ expect(getActionItemsByTarget(WorkflowTarget.TERMINATE, subscriptionActions)).toEqual([terminateAction]);
496
+ });
497
+
498
+ it('falls back to lowercase keys returned by core versions 5.2 and lower', () => {
499
+ const lowercaseActions = {
500
+ modify: [modifyAction],
501
+ terminate: [terminateAction],
502
+ } as unknown as SubscriptionActions;
503
+
504
+ expect(getActionItemsByTarget(WorkflowTarget.MODIFY, lowercaseActions)).toEqual([modifyAction]);
505
+ expect(getActionItemsByTarget(WorkflowTarget.TERMINATE, lowercaseActions)).toEqual([terminateAction]);
506
+ });
507
+
508
+ it('prefers the uppercase key when both casings are present', () => {
509
+ const mixedActions = {
510
+ [WorkflowTarget.MODIFY]: [modifyAction],
511
+ modify: [terminateAction],
512
+ } as unknown as SubscriptionActions;
513
+
514
+ expect(getActionItemsByTarget(WorkflowTarget.MODIFY, mixedActions)).toEqual([modifyAction]);
515
+ });
516
+
517
+ it('returns an empty array when the target is missing in either casing', () => {
518
+ const actionsWithoutValidate = {
519
+ [WorkflowTarget.MODIFY]: [modifyAction],
520
+ } as unknown as SubscriptionActions;
521
+
522
+ expect(getActionItemsByTarget(WorkflowTarget.VALIDATE, actionsWithoutValidate)).toEqual([]);
523
+ });
524
+
525
+ it('returns an empty array when subscriptionActions is undefined', () => {
526
+ expect(getActionItemsByTarget(WorkflowTarget.MODIFY, undefined)).toEqual([]);
527
+ });
528
+ });
529
+
473
530
  describe('parseErrorDetail', () => {
474
531
  it('parses a single UUID inside brackets', () => {
475
532
  const input = "Subscription 123 has still failed processes with id's: ['11111111-1111-1111-1111-111111111111']";
@@ -9,6 +9,7 @@ import {
9
9
  ProductBlockInstance,
10
10
  SortOrder,
11
11
  SubscriptionAction,
12
+ SubscriptionActions,
12
13
  SubscriptionDetailProcess,
13
14
  SubscriptionRelation,
14
15
  WorkflowTarget,
@@ -189,3 +190,17 @@ export const mapProductBlockInstancesToEuiSelectableOptions = (
189
190
  },
190
191
  }));
191
192
  };
193
+
194
+ export const getActionItemsByTarget = (
195
+ workflowTarget: WorkflowTarget,
196
+ subscriptionActions?: SubscriptionActions,
197
+ ): SubscriptionAction[] => {
198
+ // https://github.com/workfloworchestrator/orchestrator-core/issues/1820 will align the WorkflowTarget enum and make it
199
+ // uppercase. We support both for now. The lowercase keys are deliberately not part of the
200
+ // SubscriptionActions type, so the fallback is a runtime-only check behind a cast.
201
+ const actionsByTarget = subscriptionActions as unknown as
202
+ | Record<string, SubscriptionAction[] | undefined>
203
+ | undefined;
204
+
205
+ return actionsByTarget?.[workflowTarget] ?? actionsByTarget?.[workflowTarget.toLowerCase()] ?? [];
206
+ };
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef, useState } from 'react';
1
+ import React, { FC, useEffect, useRef, useState } from 'react';
2
2
  import { FieldSelectorProps } from 'react-querybuilder';
3
3
 
4
4
  import { useTranslations } from 'next-intl';
@@ -7,7 +7,7 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui';
7
7
  import { EuiComboBox } from '@elastic/eui';
8
8
 
9
9
  import { usePathAutocomplete } from '@/hooks';
10
- import { EntityKind, FieldToOperatorMap, PathInfo } from '@/types';
10
+ import { EntityKind, PathInfo, WfoQueryBuilderContext } from '@/types';
11
11
 
12
12
  // react-querybuilder applies the `.rule` class to the rule container and `.rule-value` to
13
13
  // the value-editor cell (see `standardClassnames` in @react-querybuilder/core). We hop from
@@ -30,9 +30,13 @@ const focusValueEditorAfterRender = (searchInput: HTMLInputElement | null) => {
30
30
  });
31
31
  };
32
32
 
33
- export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: FieldSelectorProps) => {
33
+ interface WfoFieldSelectorProps extends Omit<FieldSelectorProps, 'context'> {
34
+ context: WfoQueryBuilderContext;
35
+ }
36
+
37
+ export const WfoFieldSelector: FC<WfoFieldSelectorProps> = ({ handleOnChange, disabled, rule, context }) => {
34
38
  const { field } = rule;
35
- const prefilledFieldOptions: FieldToOperatorMap = context.prefilledFieldOptions;
39
+ const { useAdvancedNestedSearch, prefilledFieldOptions, onFieldSelected } = context;
36
40
  const [selectedValue, setSelectedValue] = useState<string>(field);
37
41
  const [searchInput, setSearchInput] = useState<HTMLInputElement | null>(null);
38
42
  const optionsRef = useRef<EuiComboBoxOptionOption<string>[]>([]);
@@ -43,13 +47,13 @@ export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: Fi
43
47
  label: path,
44
48
  });
45
49
 
50
+ const isSelectablePath = (path: string) => useAdvancedNestedSearch || !path.includes('.');
51
+
46
52
  const getOptionsFromPathInfo = (pathInfos: PathInfo[]): EuiComboBoxOptionOption<string>[] => {
47
53
  const pathOptions: EuiComboBoxOptionOption<string>[] = [];
48
54
 
49
55
  pathInfos.forEach((pathInfo) => {
50
- pathOptions.push(getOption(pathInfo.path));
51
- // Adds more specific paths
52
- pathInfo.availablePaths?.forEach((path) => {
56
+ [pathInfo.path, ...(pathInfo.availablePaths ?? [])].filter(isSelectablePath).forEach((path) => {
53
57
  pathOptions.push(getOption(path));
54
58
  });
55
59
  });
@@ -85,7 +89,7 @@ export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: Fi
85
89
  ?? paths.find((path) => path.availablePaths?.includes(selectedValue));
86
90
  const operators = matchingPath?.operators ?? prefilledFieldOptions.get(selectedValue) ?? [];
87
91
 
88
- context?.onFieldSelected?.(selectedValue, operators, matchingPath);
92
+ onFieldSelected(selectedValue, operators, matchingPath);
89
93
  };
90
94
 
91
95
  const handleFieldSelection = (selectedOptions: EuiComboBoxOptionOption<string>[]) => {
@@ -1,5 +1,11 @@
1
- import React, { useEffect, useMemo, useRef, useState } from 'react';
2
- import { FullOperator, QueryBuilder, type RuleGroupType, generateID } from 'react-querybuilder';
1
+ import React, { type ComponentType, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ type FieldSelectorProps,
4
+ FullOperator,
5
+ QueryBuilder,
6
+ type RuleGroupType,
7
+ generateID,
8
+ } from 'react-querybuilder';
3
9
  import 'react-querybuilder/dist/query-builder.css';
4
10
 
5
11
  import { useTranslations } from 'next-intl';
@@ -10,7 +16,7 @@ import { SearchParams, WfoAutoExpandableTextArea, WfoTextAnchor } from '@/compon
10
16
  import { WfoCombinatorSelector } from '@/components/WfoTable/WfoStructuredSearchTable/WfoCombinatorSelector';
11
17
  import { useFieldsPathInfo, useWithOrchestratorTheme } from '@/hooks';
12
18
  import { EntityKind, OperatorDisplay } from '@/types';
13
- import type { FieldToOperatorMap, PathInfo } from '@/types';
19
+ import type { FieldToOperatorMap, PathInfo, WfoQueryBuilderContext } from '@/types';
14
20
 
15
21
  import { WfoFieldSelector } from './WfoFieldSelector';
16
22
  import { WfoInlineCombinator } from './WfoInlineCombinator';
@@ -65,6 +71,7 @@ interface WfoFilterBuilderProps {
65
71
  handleSearch: (searchParams?: SearchParams) => void;
66
72
  onToggleFilterBuilder: (isVisible: boolean) => void;
67
73
  prefilledFieldOptions: FieldToOperatorMap;
74
+ useAdvancedNestedSearch?: boolean;
68
75
  }
69
76
 
70
77
  const initialRuleGroup: RuleGroupType = {
@@ -87,6 +94,7 @@ export const WfoFilterBuilder = ({
87
94
  handleSearch,
88
95
  prefilledFieldOptions,
89
96
  onToggleFilterBuilder,
97
+ useAdvancedNestedSearch = true,
90
98
  }: WfoFilterBuilderProps) => {
91
99
  const mapOperatorsToRQBOperatorOptions = (operators?: string[]): FullOperator[] => {
92
100
  return (operators ?? []).map((operator) => {
@@ -143,6 +151,14 @@ export const WfoFilterBuilder = ({
143
151
  );
144
152
  const resolvedFieldsPathInfo = useFieldsPathInfo(unresolvedFields, EntityKind.SUBSCRIPTION);
145
153
 
154
+ const queryBuilderContext: WfoQueryBuilderContext = {
155
+ onFieldSelected: handleFieldSelected,
156
+ prefilledFieldOptions,
157
+ fieldPathInfoMap,
158
+ onValueEditorEnter: handleValueEditorEnter,
159
+ useAdvancedNestedSearch,
160
+ };
161
+
146
162
  useEffect(() => {
147
163
  resolvedFieldsPathInfo.forEach((pathInfo, field) => {
148
164
  if (pathInfo && !fieldPathInfoMap.has(field)) {
@@ -162,18 +178,15 @@ export const WfoFilterBuilder = ({
162
178
  latestRuleGroupRef.current = ruleGroup;
163
179
  onUpdateQueryBuilder(ruleGroup);
164
180
  }}
165
- context={{
166
- onFieldSelected: handleFieldSelected,
167
- prefilledFieldOptions,
168
- fieldPathInfoMap,
169
- onValueEditorEnter: handleValueEditorEnter,
170
- }}
181
+ context={queryBuilderContext}
171
182
  getOperators={(field) => {
172
183
  const operators = fieldToOperatorMap.get(field);
173
184
  return mapOperatorsToRQBOperatorOptions(operators);
174
185
  }}
175
186
  controlElements={{
176
- fieldSelector: WfoFieldSelector,
187
+ // WfoFieldSelector requires the context this QueryBuilder always provides,
188
+ // while react-querybuilder declares it optional — hence the cast.
189
+ fieldSelector: WfoFieldSelector as ComponentType<FieldSelectorProps>,
177
190
  operatorSelector: WfoOperatorSelector,
178
191
  valueEditor: WfoValueEditor,
179
192
  ruleGroup: WfoRuleGroup,
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+
3
+ import { useTranslations } from 'next-intl';
4
+
5
+ import { EuiText } from '@elastic/eui';
6
+
7
+ import { WfoInformationModal } from '@/components';
8
+
9
+ export type WfoSearchHelpModalProps = {
10
+ onClose: () => void;
11
+ };
12
+
13
+ export const WfoSearchHelpModal = ({ onClose }: WfoSearchHelpModalProps) => {
14
+ const t = useTranslations('search.page');
15
+ const tCommon = useTranslations('common');
16
+
17
+ return (
18
+ <WfoInformationModal title={t('help.title')} onClose={onClose}>
19
+ <EuiText size="s">
20
+ <p>
21
+ <strong>{t('help.searchFieldTitle')}</strong> - {t('help.searchFieldDescription')}
22
+ </p>
23
+
24
+ <h4>{t('help.filterTitle')}</h4>
25
+ <p>{t('help.filterIntro')}</p>
26
+ <ul>
27
+ <li>
28
+ <strong>{t('addRule')}</strong> - {t('help.addRuleDescription')}
29
+ </li>
30
+ <li>
31
+ <strong>{t('addGroup')}</strong> - {t('help.addGroupDescription')}
32
+ </li>
33
+ </ul>
34
+
35
+ <h4>{t('help.quickFilterTitle')}</h4>
36
+ <p>{t('help.quickFilterDescription')}</p>
37
+
38
+ <h4>{t('help.matchingRowTitle')}</h4>
39
+ <p>{t('help.matchingRowDescription')}</p>
40
+
41
+ <h4>{t('help.tableSettingsTitle')}</h4>
42
+ <ul>
43
+ <li>
44
+ <strong>{tCommon('showMatchDetails')}</strong> - {t('help.showMatchDetailsDescription')}
45
+ </li>
46
+ <li>
47
+ <strong>{tCommon('retrieval')}</strong> - {t('help.retrievalDescription')}
48
+ </li>
49
+ <li>
50
+ <strong>{tCommon('export')}</strong> - {t('help.exportDescription')}
51
+ </li>
52
+ <li>
53
+ <strong>{t('help.advancedNestedSearchLabel')}</strong> - {t('help.advancedNestedSearchDescription')}
54
+ </li>
55
+ </ul>
56
+ </EuiText>
57
+ </WfoInformationModal>
58
+ );
59
+ };
@@ -3,16 +3,7 @@ import type { RuleGroupType } from 'react-querybuilder';
3
3
 
4
4
  import { useTranslations } from 'next-intl';
5
5
 
6
- import {
7
- EuiButton,
8
- EuiFlexGroup,
9
- EuiFlexItem,
10
- EuiFormRow,
11
- EuiSelect,
12
- EuiSpacer,
13
- EuiSwitch,
14
- EuiText,
15
- } from '@elastic/eui';
6
+ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSpacer, EuiSwitch } from '@elastic/eui';
16
7
 
17
8
  import {
18
9
  DEFAULT_PAGE_SIZE,
@@ -45,6 +36,7 @@ import { getDefaultTableConfig } from '@/utils';
45
36
  import { ColumnType, WfoTable, WfoTableProps } from '../WfoTable';
46
37
  import { WfoFilterBuilder } from './WfoFilterBuilder';
47
38
  import { WfoSearchFieldWithActions } from './WfoSearchFieldWithActions';
39
+ import { WfoSearchHelpModal } from './WfoSearchHelpModal';
48
40
  import { getWfoStructuredSearchTableStyles } from './styles';
49
41
  import { buildColumnFilter } from './utils';
50
42
 
@@ -80,6 +72,7 @@ export type WfoStructuredSearchTableProps<T extends object> = Omit<
80
72
  rowExpandingConfiguration: WfoTableProps<T>['rowExpandingConfiguration'];
81
73
  defaultHiddenColumns?: TableColumnKeys<T>;
82
74
  defaultShowMatchDetails?: boolean;
75
+ defaultAdvancedNestedSearch?: boolean;
83
76
  queryText?: string;
84
77
  localStorageKey: string;
85
78
  exportDataIsLoading?: boolean;
@@ -101,7 +94,7 @@ export type WfoStructuredSearchTableProps<T extends object> = Omit<
101
94
  onUpdateQueryBuilder: (ruleGroup: RuleGroupType | false) => void;
102
95
  handleSearch: (searchParams?: SearchParams) => void;
103
96
  pageSize: number;
104
- setPageSize: React.Dispatch<React.SetStateAction<number>>;
97
+ setPageSize: (updatedPageSize: number) => void;
105
98
  totalItems: number | false;
106
99
  hasNextPage: boolean;
107
100
  prefilledFieldOptions: FieldToOperatorMap;
@@ -111,6 +104,7 @@ export const WfoStructuredSearchTable = <T extends object>({
111
104
  tableColumnConfig,
112
105
  defaultHiddenColumns = [],
113
106
  defaultShowMatchDetails = false,
107
+ defaultAdvancedNestedSearch = true,
114
108
  queryText,
115
109
  localStorageKey,
116
110
  exportDataIsLoading,
@@ -148,6 +142,7 @@ export const WfoStructuredSearchTable = <T extends object>({
148
142
  const [rowDetailModalData, setRowDetailModalData] = useState<T | undefined>(undefined);
149
143
  const [showInformationModal, setShowInformationModal] = useState(false);
150
144
  const [showMatchDetails, setShowMatchDetails] = useState(defaultShowMatchDetails);
145
+ const [advancedNestedSearch, setAdvancedNestedSearch] = useState(defaultAdvancedNestedSearch);
151
146
  const t = useTranslations('common');
152
147
 
153
148
  useEffect(() => {
@@ -160,6 +155,10 @@ export const WfoStructuredSearchTable = <T extends object>({
160
155
  setShowMatchDetails(defaultShowMatchDetails);
161
156
  }, [defaultShowMatchDetails]);
162
157
 
158
+ useEffect(() => {
159
+ setAdvancedNestedSearch(defaultAdvancedNestedSearch);
160
+ }, [defaultAdvancedNestedSearch]);
161
+
163
162
  useEffect(() => {
164
163
  if (filterString) {
165
164
  setIsFilterBuilderVisible(true);
@@ -199,10 +198,11 @@ export const WfoStructuredSearchTable = <T extends object>({
199
198
  hiddenColumns: updatedHiddenColumns,
200
199
  selectedPageSize: updatedTableConfig.selectedPageSize,
201
200
  showMatchDetails,
201
+ advancedNestedSearch,
202
202
  });
203
203
  };
204
204
 
205
- // The toggle applies live, so persist it immediately alongside the currently committed
205
+ // The toggles apply live, so persist them immediately alongside the currently committed
206
206
  // hidden columns and page size instead of waiting for the modal's "Update" action.
207
207
  const handleToggleShowMatchDetails = (checked: boolean) => {
208
208
  setShowMatchDetails(checked);
@@ -210,6 +210,17 @@ export const WfoStructuredSearchTable = <T extends object>({
210
210
  hiddenColumns,
211
211
  selectedPageSize: pageSize ?? DEFAULT_PAGE_SIZE,
212
212
  showMatchDetails: checked,
213
+ advancedNestedSearch,
214
+ });
215
+ };
216
+
217
+ const handleToggleAdvancedNestedSearch = (checked: boolean) => {
218
+ setAdvancedNestedSearch(checked);
219
+ setTableConfigToLocalStorage(localStorageKey, {
220
+ hiddenColumns,
221
+ selectedPageSize: pageSize ?? DEFAULT_PAGE_SIZE,
222
+ showMatchDetails,
223
+ advancedNestedSearch: checked,
213
224
  });
214
225
  };
215
226
 
@@ -218,6 +229,7 @@ export const WfoStructuredSearchTable = <T extends object>({
218
229
  setHiddenColumns(defaultTableConfig.hiddenColumns);
219
230
  setPageSize(defaultTableConfig.selectedPageSize);
220
231
  setShowMatchDetails(defaultTableConfig.showMatchDetails ?? false);
232
+ setAdvancedNestedSearch(defaultTableConfig.advancedNestedSearch ?? false);
221
233
  setShowTableSettingsModal(false);
222
234
  clearTableConfigFromLocalStorage(localStorageKey);
223
235
  };
@@ -272,6 +284,7 @@ export const WfoStructuredSearchTable = <T extends object>({
272
284
  handleSearch={handleSearch}
273
285
  onToggleFilterBuilder={setIsFilterBuilderVisible}
274
286
  prefilledFieldOptions={prefilledFieldOptions}
287
+ useAdvancedNestedSearch={advancedNestedSearch}
275
288
  />
276
289
  </>
277
290
  )}
@@ -290,6 +303,7 @@ export const WfoStructuredSearchTable = <T extends object>({
290
303
  dataSorting={dataSorting}
291
304
  data={data}
292
305
  isLoading={isLoading}
306
+ loadingSkeletonRowCount={pageSize}
293
307
  {...tableProps}
294
308
  />
295
309
 
@@ -323,6 +337,15 @@ export const WfoStructuredSearchTable = <T extends object>({
323
337
  compressed
324
338
  />
325
339
  </EuiFormRow>
340
+ <EuiFormRow label={t('advancedNestedSearch')} display="columnCompressed">
341
+ <EuiSwitch
342
+ showLabel={false}
343
+ label={t('advancedNestedSearch')}
344
+ checked={advancedNestedSearch}
345
+ onChange={(event) => handleToggleAdvancedNestedSearch(event.target.checked)}
346
+ compressed
347
+ />
348
+ </EuiFormRow>
326
349
  <EuiFormRow label={t('retrieval')} display="columnCompressed">
327
350
  <EuiSelect
328
351
  options={[
@@ -340,7 +363,7 @@ export const WfoStructuredSearchTable = <T extends object>({
340
363
  <>
341
364
  <EuiSpacer size="m" />
342
365
  <EuiButton isLoading={exportDataIsLoading} onClick={() => onExportData()} fullWidth>
343
- {t('export')}
366
+ {totalItems ? t('exportRows', { numberOfRows: totalItems }) : t('export')}
344
367
  </EuiButton>
345
368
  </>
346
369
  )}
@@ -349,13 +372,7 @@ export const WfoStructuredSearchTable = <T extends object>({
349
372
  />
350
373
  )}
351
374
 
352
- {showInformationModal && (
353
- <WfoInformationModal title={t('searchModalTitle')} onClose={() => setShowInformationModal(false)}>
354
- <EuiText>
355
- <p>TODO: Info about searching</p>
356
- </EuiText>
357
- </WfoInformationModal>
358
- )}
375
+ {showInformationModal && <WfoSearchHelpModal onClose={() => setShowInformationModal(false)} />}
359
376
 
360
377
  {rowDetailData && (
361
378
  <WfoInformationModal title={'TODO: Information modal title'} onClose={() => setRowDetailModalData(undefined)}>
@@ -10,6 +10,7 @@ import { EuiButtonGroup, EuiDatePicker, EuiFieldNumber, EuiFieldText } from '@el
10
10
  import { WfoRangeEditor } from '@/components/WfoTable/WfoStructuredSearchTable/WfoRangeEditor';
11
11
  import { getWfoStructuredSearchTableStyles } from '@/components/WfoTable/WfoStructuredSearchTable/styles';
12
12
  import { useWithOrchestratorTheme } from '@/hooks';
13
+ import type { WfoQueryBuilderContext } from '@/types';
13
14
 
14
15
  export interface EditorInputFieldProps<T = string> {
15
16
  handleOnChange: ValueEditorProps['handleOnChange'];
@@ -176,7 +177,8 @@ export const WfoValueEditor = ({
176
177
  return TextEditor;
177
178
  };
178
179
 
179
- const fieldPathInfoMap = context?.fieldPathInfoMap;
180
+ const queryBuilderContext: WfoQueryBuilderContext | undefined = context;
181
+ const fieldPathInfoMap = queryBuilderContext?.fieldPathInfoMap;
180
182
 
181
183
  const fieldInfo = fieldPathInfoMap && fieldPathInfoMap.has(fieldName) ? fieldPathInfoMap.get(fieldName) : undefined;
182
184
  const uiFieldType = fieldInfo?.ui_types?.[0] || UiFieldType.text;
@@ -198,7 +200,7 @@ export const WfoValueEditor = ({
198
200
  // the boolean buttons means "select" — its click fires only after this keydown, so
199
201
  // searching there would use the pre-toggle value.
200
202
  if (!(event.target instanceof HTMLInputElement)) return;
201
- context?.onValueEditorEnter?.();
203
+ queryBuilderContext?.onValueEditorEnter();
202
204
  };
203
205
 
204
206
  // react-querybuilder delivers the standard `rule-value` class via this prop; the wrapper