@orchestrator-ui/orchestrator-ui-components 8.7.3 → 8.8.2

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 (52) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +17 -15
  4. package/CHANGELOG.md +34 -0
  5. package/dist/index.d.ts +42 -20
  6. package/dist/index.js +1612 -1378
  7. package/dist/index.js.map +1 -1
  8. package/package.json +4 -3
  9. package/src/components/WfoBadges/WfoWorkflowTargetBadge/WfoWorkflowTargetBadge.tsx +2 -2
  10. package/src/components/WfoContentHeader/WfoContentHeader.tsx +1 -1
  11. package/src/components/WfoInlineNoteEdit/WfoProcessListNoteEdit.spec.tsx +35 -0
  12. package/src/components/WfoInlineNoteEdit/WfoProcessListNoteEdit.tsx +7 -9
  13. package/src/components/WfoProcessList/WfoProcessListDeltaPopover.tsx +1 -1
  14. package/src/components/WfoProcessList/WfoProcessesList.tsx +7 -5
  15. package/src/components/WfoPydanticForm/fields/WfoMultiCheckboxField.tsx +2 -1
  16. package/src/components/WfoSubscription/WfoProcessesTimeline.tsx +21 -7
  17. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +25 -9
  18. package/src/components/WfoSubscription/WfoSubscriptionGeneralSections/WfoSubscriptionDetailSection.tsx +1 -1
  19. package/src/components/WfoSubscription/styles.ts +6 -0
  20. package/src/components/WfoSubscription/utils/utils.ts +2 -4
  21. package/src/components/WfoTable/WfoStructuredSearchTable/WfoExpandingSearchRow.tsx +0 -1
  22. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +12 -8
  23. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +23 -10
  24. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +73 -3
  25. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +4 -2
  26. package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +8 -9
  27. package/src/components/WfoTable/WfoTable/WfoTable.tsx +80 -45
  28. package/src/components/WfoTable/WfoTable/WfoTableDataRows.tsx +11 -2
  29. package/src/components/WfoTable/WfoTable/WfoTableExpandedRowReveal.spec.tsx +120 -0
  30. package/src/components/WfoTable/WfoTable/WfoTableSkeletonRows.tsx +52 -0
  31. package/src/components/WfoTable/WfoTable/index.ts +1 -0
  32. package/src/components/WfoTable/WfoTable/styles.ts +6 -0
  33. package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.tsx +21 -18
  34. package/src/components/WfoTable/WfoTableSettingsModal/styles.ts +17 -0
  35. package/src/components/WfoTable/utils/tableConfigPersistence.ts +2 -0
  36. package/src/components/WfoTimeline/styles.ts +1 -1
  37. package/src/components/WfoWorkflowUserGuide/WfoPageWithUserGuide.tsx +8 -2
  38. package/src/components/WfoWorkflowUserGuide/WfoWorkflowGuideExpandablePanel.tsx +39 -30
  39. package/src/components/WfoWorkflowUserGuide/styles.ts +55 -7
  40. package/src/configuration/constants.ts +1 -1
  41. package/src/configuration/version.ts +1 -1
  42. package/src/hooks/useGetPydanticFormsConfig.tsx +5 -3
  43. package/src/hooks/useSearch.ts +2 -2
  44. package/src/hooks/useStoredTableConfig.ts +2 -0
  45. package/src/messages/en-GB.json +5 -1
  46. package/src/messages/nl-NL.json +6 -2
  47. package/src/pages/WfoSearchPocPage.tsx +29 -24
  48. package/src/pages/processes/WfoProductInformationWithLink.tsx +9 -4
  49. package/src/pages/tasks/WfoTasksListPage.tsx +1 -0
  50. package/src/types/search.ts +8 -0
  51. package/src/types/types.ts +11 -11
  52. package/src/utils/getDefaultTableConfig.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-ui/orchestrator-ui-components",
3
- "version": "8.7.3",
3
+ "version": "8.8.2",
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",
@@ -13,11 +13,11 @@ export const WfoWorkflowTargetBadge: FC<WfoWorkflowTargetBadgeProps> = ({ target
13
13
  const { theme, toSecondaryColor } = useOrchestratorTheme();
14
14
  if (!target) return null;
15
15
 
16
- const getBadgeColorFromTarget = (_target: string) => {
16
+ const getBadgeColorFromTarget = (_target: WorkflowTarget) => {
17
17
  const { primary, danger, textDanger, textPrimary, success, textSuccess, warning, textWarning, accent, textAccent } =
18
18
  theme.colors;
19
19
 
20
- switch (_target?.toLowerCase()) {
20
+ switch (_target) {
21
21
  case WorkflowTarget.CREATE:
22
22
  return {
23
23
  badgeColor: toSecondaryColor(success),
@@ -19,7 +19,7 @@ export const WfoContentHeader: FC<WfoContentHeaderProps> = ({ title, subtitle, c
19
19
  </WfoRenderElementOrString>
20
20
  {subtitle && (
21
21
  <div>
22
- <EuiSpacer size="m" />
22
+ <EuiSpacer size="s" />
23
23
  <WfoRenderElementOrString>{subtitle}</WfoRenderElementOrString>
24
24
  </div>
25
25
  )}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Regression tests for the note column going stale while paginating: the table reuses row
3
+ * components across pages, and EuiInlineEditText only follows its value prop while the value
4
+ * is truthy. A row whose note is empty must therefore still pass a truthy value (the
5
+ * invisible character), otherwise the cell keeps showing the previous page's note.
6
+ */
7
+ import React from 'react';
8
+
9
+ import '@testing-library/jest-dom';
10
+ import { render, screen } from '@testing-library/react';
11
+
12
+ import { WfoProcessListNoteEdit } from './WfoProcessListNoteEdit';
13
+
14
+ jest.mock('@/rtk', () => ({
15
+ ...jest.requireActual('@/rtk'),
16
+ usePatchProcessMutation: () => [jest.fn()],
17
+ }));
18
+
19
+ describe('WfoProcessListNoteEdit', () => {
20
+ it('stops showing the previous note when the reused row has no note', () => {
21
+ const { rerender } = render(<WfoProcessListNoteEdit processId="process-1" note="Note from page one" />);
22
+ expect(screen.getByText('Note from page one')).toBeInTheDocument();
23
+
24
+ rerender(<WfoProcessListNoteEdit processId="process-2" note={null} />);
25
+ expect(screen.queryByText('Note from page one')).not.toBeInTheDocument();
26
+ });
27
+
28
+ it('shows the new note when the reused row has a different note', () => {
29
+ const { rerender } = render(<WfoProcessListNoteEdit processId="process-1" note="Note from page one" />);
30
+
31
+ rerender(<WfoProcessListNoteEdit processId="process-2" note="Note from page two" />);
32
+ expect(screen.getByText('Note from page two')).toBeInTheDocument();
33
+ expect(screen.queryByText('Note from page one')).not.toBeInTheDocument();
34
+ });
35
+ });
@@ -1,8 +1,9 @@
1
1
  import type { FC } from 'react';
2
- import React, { useEffect, useState } from 'react';
2
+ import React from 'react';
3
3
 
4
4
  import { usePatchProcessMutation } from '@/rtk';
5
5
  import { ProcessDetail } from '@/types';
6
+ import { INVISIBLE_CHARACTER } from '@/utils';
6
7
 
7
8
  import { WfoInlineEdit } from '../WfoInlineEdit';
8
9
 
@@ -13,13 +14,6 @@ interface WfoProcessDetailNoteEditProps {
13
14
 
14
15
  export const WfoProcessListNoteEdit: FC<WfoProcessDetailNoteEditProps> = ({ processId, note }) => {
15
16
  const [patchProcess] = usePatchProcessMutation();
16
- const [noteValue, setNoteValue] = useState(note);
17
-
18
- useEffect(() => {
19
- if (note !== noteValue) {
20
- setNoteValue(note);
21
- }
22
- }, [note, noteValue]);
23
17
 
24
18
  const onSaveNote = async (note: string) => {
25
19
  const noteModifyPayload = { id: processId, note: note };
@@ -27,5 +21,9 @@ export const WfoProcessListNoteEdit: FC<WfoProcessDetailNoteEditProps> = ({ proc
27
21
  return note;
28
22
  };
29
23
 
30
- return <WfoInlineEdit value={noteValue?.trim() || ''} onSave={onSaveNote} />;
24
+ // An empty note must still be passed as a non-empty string (the invisible character):
25
+ // EuiInlineEditText only follows its value prop while the value is truthy, so an empty
26
+ // string would freeze the cell on the note of whichever row rendered here before it,
27
+ // e.g. when the table rows are reused while paginating.
28
+ return <WfoInlineEdit value={note?.trim() ? note : INVISIBLE_CHARACTER} onSave={onSaveNote} />;
31
29
  };
@@ -28,7 +28,7 @@ export const WfoProcessListDeltaPopover: FC<WfoProcessListDeltaPopoverProps> = (
28
28
 
29
29
  const { data, isFetching } = useGetRawProcessDetailQuery({ processId }, { skip: !isPopoverOpen });
30
30
 
31
- if (workflowTarget.toUpperCase() !== WorkflowTarget.MODIFY.toUpperCase()) {
31
+ if (workflowTarget !== WorkflowTarget.MODIFY) {
32
32
  return null;
33
33
  }
34
34
 
@@ -241,15 +241,17 @@ export const WfoProcessesList = ({
241
241
  const getProcessListForExport = () =>
242
242
  getProcessListTrigger(getQueryVariablesForExport(processListQueryVars)).unwrap();
243
243
 
244
+ const tableConfig = mapSortableAndFilterableValuesToTableColumnConfig(
245
+ tableColumns,
246
+ pageInfo?.sortFields,
247
+ pageInfo?.filterFields,
248
+ );
249
+
244
250
  return (
245
251
  <WfoAdvancedTable<ProcessListItem>
246
252
  queryString={queryString}
247
253
  data={mapGraphQlProcessListResultToProcessListItems(processes || [])}
248
- tableColumnConfig={mapSortableAndFilterableValuesToTableColumnConfig(
249
- tableColumns,
250
- pageInfo?.sortFields,
251
- pageInfo?.filterFields,
252
- )}
254
+ tableColumnConfig={tableConfig}
253
255
  dataSorting={[dataSorting]}
254
256
  pagination={pagination}
255
257
  isLoading={isFetching}
@@ -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,9 +1,9 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
 
3
3
  import { useTranslations } from 'next-intl';
4
4
  import Link from 'next/link';
5
5
 
6
- import { EuiComment, EuiCommentList, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
6
+ import { EuiComment, EuiCommentList, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui';
7
7
 
8
8
  import {
9
9
  PATH_TASKS,
@@ -15,7 +15,7 @@ import {
15
15
  sortProcessesByDate,
16
16
  } from '@/components';
17
17
  import { useWithOrchestratorTheme } from '@/hooks';
18
- import { SortOrder, SubscriptionDetailProcess } from '@/types';
18
+ import { SortOrder, SubscriptionDetailProcess, WorkflowTarget } from '@/types';
19
19
  import { parseDate, parseDateToLocaleDateTimeString, upperCaseFirstChar } from '@/utils';
20
20
 
21
21
  import { WfoProcessStatusBadge } from '../WfoBadges';
@@ -122,8 +122,9 @@ export const WfoProcessesTimeline = ({ subscriptionDetailProcesses }: WfoProcess
122
122
  value: SortOrder.DESC,
123
123
  },
124
124
  ];
125
-
126
- const [selectedOption, setSelectedOption] = React.useState(options[0]);
125
+ const { processToggleStyles } = useWithOrchestratorTheme(getSubscriptionDetailStyles);
126
+ const [selectedOption, setSelectedOption] = useState(options[0]);
127
+ const [hideValidateTasks, setHideValidateTasks] = useState<boolean>(true);
127
128
 
128
129
  const handleOnSelectOption = (option: WfoRadioDropdownOption<SortOrder>) => {
129
130
  setSelectedOption(option);
@@ -135,11 +136,24 @@ export const WfoProcessesTimeline = ({ subscriptionDetailProcesses }: WfoProcess
135
136
  <>
136
137
  <EuiSpacer size={'m'} />
137
138
  {!subscriptionDetailProcesses && <WfoLoading />}
138
- <WfoRadioDropdown options={options} onUpdateOption={handleOnSelectOption} selectedOption={selectedOption} />
139
+ <EuiFlexGroup alignItems={'center'} css={processToggleStyles}>
140
+ <EuiSwitch
141
+ showLabel={true}
142
+ label={t('hideValidateTasks')}
143
+ type="button"
144
+ checked={hideValidateTasks}
145
+ onChange={() => setHideValidateTasks((value) => !value)}
146
+ />
147
+ <WfoRadioDropdown options={options} onUpdateOption={handleOnSelectOption} selectedOption={selectedOption} />
148
+ </EuiFlexGroup>
149
+
139
150
  {sortedProcesses && (
140
151
  <EuiCommentList aria-label="Processes">
141
152
  {sortedProcesses
142
- .filter((process) => !process.isTask)
153
+ .filter(
154
+ (process) =>
155
+ !process.isTask || (process.workflowTarget === WorkflowTarget.VALIDATE && !hideValidateTasks),
156
+ )
143
157
  .map((subscriptionDetailProcess, index) => (
144
158
  <WfoRenderSubscriptionProcess key={index} subscriptionDetailProcess={subscriptionDetailProcess} />
145
159
  ))}
@@ -18,7 +18,7 @@ import { PolicyResource } from '@/configuration/policy-resources';
18
18
  import { useOrchestratorTheme, usePolicy } from '@/hooks';
19
19
  import { WfoDotsHorizontal } from '@/icons/WfoDotsHorizontal';
20
20
  import { useGetSubscriptionActionsQuery, useGetSubscriptionDetailQuery, useStartProcessMutation } from '@/rtk';
21
- import { WorkflowTarget } from '@/types';
21
+ import { SubscriptionAction, WorkflowTarget } from '@/types';
22
22
 
23
23
  type MenuBlockProps = {
24
24
  title: string;
@@ -129,12 +129,28 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
129
129
  }
130
130
  };
131
131
 
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);
147
+
132
148
  const compactItems = (
133
149
  <>
134
- {isAllowed(SUBSCRIPTION_VALIDATE + subscriptionId) && subscriptionActions?.validate && (
150
+ {isAllowed(SUBSCRIPTION_VALIDATE + subscriptionId) && validateActionItems.length > 0 && (
135
151
  <>
136
152
  {!compactMode && <MenuBlock title={t('tasks')} />}
137
- {subscriptionActions.validate.map((subscriptionAction, index) => (
153
+ {validateActionItems.map((subscriptionAction, index) => (
138
154
  <WfoSubscriptionActionsMenuItem
139
155
  key={`s_${index}`}
140
156
  subscriptionAction={subscriptionAction}
@@ -148,10 +164,10 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
148
164
  </>
149
165
  )}
150
166
 
151
- {isAllowed(SUBSCRIPTION_RECONCILE + subscriptionId) && (subscriptionActions?.reconcile?.length ?? 0) > 0 && (
167
+ {isAllowed(SUBSCRIPTION_RECONCILE + subscriptionId) && reconcileActionItems.length > 0 && (
152
168
  <>
153
169
  {!compactMode && <MenuBlock title={t('reconcile')} />}
154
- {subscriptionActions?.reconcile.map((subscriptionAction, index) => (
170
+ {reconcileActionItems.map((subscriptionAction, index) => (
155
171
  <WfoSubscriptionActionsMenuItem
156
172
  key={`r_${index}`}
157
173
  subscriptionAction={
@@ -183,10 +199,10 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
183
199
 
184
200
  const fullItems = (
185
201
  <>
186
- {isAllowed(SUBSCRIPTION_MODIFY + subscriptionId) && subscriptionActions?.modify && (
202
+ {isAllowed(SUBSCRIPTION_MODIFY + subscriptionId) && modifyActionItems.length > 0 && (
187
203
  <>
188
204
  <MenuBlock title={t('modify')} />
189
- {subscriptionActions.modify.map((subscriptionAction, index) => (
205
+ {modifyActionItems.map((subscriptionAction, index) => (
190
206
  <WfoSubscriptionActionsMenuItem
191
207
  key={`m_${index}`}
192
208
  subscriptionAction={subscriptionAction}
@@ -201,10 +217,10 @@ export const WfoSubscriptionActions: FC<WfoSubscriptionActionsProps> = ({
201
217
  </>
202
218
  )}
203
219
  {compactItems}
204
- {isAllowed(SUBSCRIPTION_TERMINATE + subscriptionId) && subscriptionActions?.terminate && (
220
+ {isAllowed(SUBSCRIPTION_TERMINATE + subscriptionId) && terminateActionItems.length > 0 && (
205
221
  <>
206
222
  <MenuBlock title={t('terminate')} />
207
- {subscriptionActions.terminate.map((subscriptionAction, index) => (
223
+ {terminateActionItems.map((subscriptionAction, index) => (
208
224
  <WfoSubscriptionActionsMenuItem
209
225
  key={`t_${index}`}
210
226
  subscriptionAction={subscriptionAction}
@@ -67,7 +67,7 @@ export const WfoSubscriptionDetailSection = ({ subscriptionDetail }: WfoSubscrip
67
67
  key: t('lastRunValidation'),
68
68
  value: (() => {
69
69
  const lastValidate = processes?.page
70
- ?.filter((process) => process.workflowTarget.toLowerCase() === WorkflowTarget.VALIDATE.toLowerCase())
70
+ ?.filter((process) => process.workflowTarget === WorkflowTarget.VALIDATE)
71
71
  .slice(-1)[0];
72
72
 
73
73
  if (!lastValidate) {
@@ -54,6 +54,11 @@ export const getSubscriptionDetailStyles = ({ theme, isDarkModeActive }: WfoThem
54
54
  flexDirection: 'column',
55
55
  alignItems: 'flex-start',
56
56
  });
57
+
58
+ const processToggleStyles = css({
59
+ justifySelf: 'flex-end',
60
+ });
61
+
57
62
  return {
58
63
  rowStyle,
59
64
  labelCellStyle,
@@ -66,5 +71,6 @@ export const getSubscriptionDetailStyles = ({ theme, isDarkModeActive }: WfoThem
66
71
  productBlockTreeWidth,
67
72
  customerDescriptionsCustomerNameStyle,
68
73
  customerDescriptionsFormStyle,
74
+ processToggleStyles,
69
75
  };
70
76
  };
@@ -59,8 +59,7 @@ export const flattenSubscriptionActionProps = (action?: SubscriptionAction | nul
59
59
  };
60
60
 
61
61
  export const getWorkflowTargetColor = (workflowTarget: WorkflowTarget, theme: EuiThemeComputed) => {
62
- // Data returned from graphql can't always be depended on to be lowercase
63
- switch (workflowTarget.toLocaleLowerCase()) {
62
+ switch (workflowTarget) {
64
63
  case WorkflowTarget.CREATE:
65
64
  return theme.colors.textSuccess;
66
65
  case WorkflowTarget.MODIFY:
@@ -78,8 +77,7 @@ export const getWorkflowTargetColor = (workflowTarget: WorkflowTarget, theme: Eu
78
77
  };
79
78
 
80
79
  export const getWorkflowTargetIconContent = (workflowTarget: WorkflowTarget) => {
81
- // Data returned from graphql can't always be depended on to be lowercase
82
- switch (workflowTarget.toLocaleLowerCase()) {
80
+ switch (workflowTarget) {
83
81
  case WorkflowTarget.CREATE:
84
82
  return 'C';
85
83
  case WorkflowTarget.SYSTEM:
@@ -19,7 +19,6 @@ export const WfoExpandingSearchRow = ({ score, matchingFields }: WfoExpandingSea
19
19
 
20
20
  if (!matchingFields || matchingFields.length === 0) return null;
21
21
 
22
- // Note: The row is shown on hover by the row above is as configured in the table component expandableRow config
23
22
  return (
24
23
  <tr css={hideExpandedRowStyle}>
25
24
  <td colSpan={999} css={expandingSearchRowStyles}>
@@ -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,
@@ -3,7 +3,16 @@ import type { RuleGroupType } from 'react-querybuilder';
3
3
 
4
4
  import { useTranslations } from 'next-intl';
5
5
 
6
- import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSpacer, EuiText } from '@elastic/eui';
6
+ import {
7
+ EuiButton,
8
+ EuiFlexGroup,
9
+ EuiFlexItem,
10
+ EuiFormRow,
11
+ EuiSelect,
12
+ EuiSpacer,
13
+ EuiSwitch,
14
+ EuiText,
15
+ } from '@elastic/eui';
7
16
 
8
17
  import {
9
18
  DEFAULT_PAGE_SIZE,
@@ -70,6 +79,8 @@ export type WfoStructuredSearchTableProps<T extends object> = Omit<
70
79
  tableColumnConfig: WfoStructuredSearchTableColumnConfig<T>;
71
80
  rowExpandingConfiguration: WfoTableProps<T>['rowExpandingConfiguration'];
72
81
  defaultHiddenColumns?: TableColumnKeys<T>;
82
+ defaultShowMatchDetails?: boolean;
83
+ defaultAdvancedNestedSearch?: boolean;
73
84
  queryText?: string;
74
85
  localStorageKey: string;
75
86
  exportDataIsLoading?: boolean;
@@ -91,7 +102,7 @@ export type WfoStructuredSearchTableProps<T extends object> = Omit<
91
102
  onUpdateQueryBuilder: (ruleGroup: RuleGroupType | false) => void;
92
103
  handleSearch: (searchParams?: SearchParams) => void;
93
104
  pageSize: number;
94
- setPageSize: React.Dispatch<React.SetStateAction<number>>;
105
+ setPageSize: (updatedPageSize: number) => void;
95
106
  totalItems: number | false;
96
107
  hasNextPage: boolean;
97
108
  prefilledFieldOptions: FieldToOperatorMap;
@@ -100,6 +111,8 @@ export type WfoStructuredSearchTableProps<T extends object> = Omit<
100
111
  export const WfoStructuredSearchTable = <T extends object>({
101
112
  tableColumnConfig,
102
113
  defaultHiddenColumns = [],
114
+ defaultShowMatchDetails = false,
115
+ defaultAdvancedNestedSearch = true,
103
116
  queryText,
104
117
  localStorageKey,
105
118
  exportDataIsLoading,
@@ -136,6 +149,8 @@ export const WfoStructuredSearchTable = <T extends object>({
136
149
  const [showTableSettingsModal, setShowTableSettingsModal] = useState(false);
137
150
  const [rowDetailModalData, setRowDetailModalData] = useState<T | undefined>(undefined);
138
151
  const [showInformationModal, setShowInformationModal] = useState(false);
152
+ const [showMatchDetails, setShowMatchDetails] = useState(defaultShowMatchDetails);
153
+ const [advancedNestedSearch, setAdvancedNestedSearch] = useState(defaultAdvancedNestedSearch);
139
154
  const t = useTranslations('common');
140
155
 
141
156
  useEffect(() => {
@@ -144,6 +159,14 @@ export const WfoStructuredSearchTable = <T extends object>({
144
159
  }
145
160
  }, [defaultHiddenColumns]);
146
161
 
162
+ useEffect(() => {
163
+ setShowMatchDetails(defaultShowMatchDetails);
164
+ }, [defaultShowMatchDetails]);
165
+
166
+ useEffect(() => {
167
+ setAdvancedNestedSearch(defaultAdvancedNestedSearch);
168
+ }, [defaultAdvancedNestedSearch]);
169
+
147
170
  useEffect(() => {
148
171
  if (filterString) {
149
172
  setIsFilterBuilderVisible(true);
@@ -182,6 +205,30 @@ export const WfoStructuredSearchTable = <T extends object>({
182
205
  setTableConfigToLocalStorage(localStorageKey, {
183
206
  hiddenColumns: updatedHiddenColumns,
184
207
  selectedPageSize: updatedTableConfig.selectedPageSize,
208
+ showMatchDetails,
209
+ advancedNestedSearch,
210
+ });
211
+ };
212
+
213
+ // The toggles apply live, so persist them immediately alongside the currently committed
214
+ // hidden columns and page size instead of waiting for the modal's "Update" action.
215
+ const handleToggleShowMatchDetails = (checked: boolean) => {
216
+ setShowMatchDetails(checked);
217
+ setTableConfigToLocalStorage(localStorageKey, {
218
+ hiddenColumns,
219
+ selectedPageSize: pageSize ?? DEFAULT_PAGE_SIZE,
220
+ showMatchDetails: checked,
221
+ advancedNestedSearch,
222
+ });
223
+ };
224
+
225
+ const handleToggleAdvancedNestedSearch = (checked: boolean) => {
226
+ setAdvancedNestedSearch(checked);
227
+ setTableConfigToLocalStorage(localStorageKey, {
228
+ hiddenColumns,
229
+ selectedPageSize: pageSize ?? DEFAULT_PAGE_SIZE,
230
+ showMatchDetails,
231
+ advancedNestedSearch: checked,
185
232
  });
186
233
  };
187
234
 
@@ -189,6 +236,8 @@ export const WfoStructuredSearchTable = <T extends object>({
189
236
  const defaultTableConfig = getDefaultTableConfig<T>(localStorageKey);
190
237
  setHiddenColumns(defaultTableConfig.hiddenColumns);
191
238
  setPageSize(defaultTableConfig.selectedPageSize);
239
+ setShowMatchDetails(defaultTableConfig.showMatchDetails ?? false);
240
+ setAdvancedNestedSearch(defaultTableConfig.advancedNestedSearch ?? false);
192
241
  setShowTableSettingsModal(false);
193
242
  clearTableConfigFromLocalStorage(localStorageKey);
194
243
  };
@@ -243,6 +292,7 @@ export const WfoStructuredSearchTable = <T extends object>({
243
292
  handleSearch={handleSearch}
244
293
  onToggleFilterBuilder={setIsFilterBuilderVisible}
245
294
  prefilledFieldOptions={prefilledFieldOptions}
295
+ useAdvancedNestedSearch={advancedNestedSearch}
246
296
  />
247
297
  </>
248
298
  )}
@@ -255,11 +305,13 @@ export const WfoStructuredSearchTable = <T extends object>({
255
305
  columnConfig={tableColumnsWithControlColumns}
256
306
  hiddenColumns={hiddenColumns}
257
307
  rowExpandingConfiguration={rowExpandingConfiguration}
308
+ showExpandedRows={showMatchDetails}
258
309
  onUpdateDataSorting={onUpdateDataSorting}
259
310
  onUpdateDataSearch={handleColumnFilterSearch}
260
311
  dataSorting={dataSorting}
261
312
  data={data}
262
313
  isLoading={isLoading}
314
+ loadingSkeletonRowCount={pageSize}
263
315
  {...tableProps}
264
316
  />
265
317
 
@@ -284,6 +336,24 @@ export const WfoStructuredSearchTable = <T extends object>({
284
336
  onResetToDefaults={handleResetToDefaults}
285
337
  extraSettings={
286
338
  <>
339
+ <EuiFormRow label={t('showMatchDetails')} display="columnCompressed">
340
+ <EuiSwitch
341
+ showLabel={false}
342
+ label={t('showMatchDetails')}
343
+ checked={showMatchDetails}
344
+ onChange={(event) => handleToggleShowMatchDetails(event.target.checked)}
345
+ compressed
346
+ />
347
+ </EuiFormRow>
348
+ <EuiFormRow label={t('advancedNestedSearch')} display="columnCompressed">
349
+ <EuiSwitch
350
+ showLabel={false}
351
+ label={t('advancedNestedSearch')}
352
+ checked={advancedNestedSearch}
353
+ onChange={(event) => handleToggleAdvancedNestedSearch(event.target.checked)}
354
+ compressed
355
+ />
356
+ </EuiFormRow>
287
357
  <EuiFormRow label={t('retrieval')} display="columnCompressed">
288
358
  <EuiSelect
289
359
  options={[
@@ -301,7 +371,7 @@ export const WfoStructuredSearchTable = <T extends object>({
301
371
  <>
302
372
  <EuiSpacer size="m" />
303
373
  <EuiButton isLoading={exportDataIsLoading} onClick={() => onExportData()} fullWidth>
304
- {t('export')}
374
+ {totalItems ? t('exportRows', { numberOfRows: totalItems }) : t('export')}
305
375
  </EuiButton>
306
376
  </>
307
377
  )}