@orchestrator-ui/orchestrator-ui-components 8.7.2 → 8.8.1
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.
- package/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +17 -10
- package/CHANGELOG.md +28 -0
- package/dist/index.d.ts +506 -19
- package/dist/index.js +614 -399
- package/dist/index.js.map +1 -1
- package/jest.config.cjs +9 -8
- package/package.json +1 -1
- package/src/components/WfoBadges/WfoWorkflowTargetBadge/WfoWorkflowTargetBadge.tsx +2 -2
- package/src/components/WfoContentHeader/WfoContentHeader.tsx +1 -1
- package/src/components/WfoInlineNoteEdit/WfoProcessListNoteEdit.spec.tsx +35 -0
- package/src/components/WfoInlineNoteEdit/WfoProcessListNoteEdit.tsx +7 -9
- package/src/components/WfoProcessList/WfoProcessListDeltaPopover.tsx +1 -1
- package/src/components/WfoProcessList/WfoProcessesList.tsx +7 -5
- package/src/components/WfoSubscription/WfoProcessesTimeline.tsx +21 -7
- package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +25 -9
- package/src/components/WfoSubscription/WfoSubscriptionGeneralSections/WfoSubscriptionDetailSection.tsx +1 -1
- package/src/components/WfoSubscription/styles.ts +6 -0
- package/src/components/WfoSubscription/utils/utils.ts +2 -4
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoExpandingSearchRow.tsx +0 -1
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +30 -3
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.spec.tsx +143 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.tsx +33 -11
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoRestoreLoop.spec.tsx +130 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +40 -1
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.spec.tsx +60 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +22 -4
- package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +8 -9
- package/src/components/WfoTable/WfoStructuredSearchTable/utils.spec.ts +56 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/utils.ts +40 -1
- package/src/components/WfoTable/WfoTable/WfoTable.tsx +5 -1
- package/src/components/WfoTable/WfoTable/WfoTableDataRows.tsx +11 -2
- package/src/components/WfoTable/WfoTable/WfoTableExpandedRowReveal.spec.tsx +120 -0
- package/src/components/WfoTable/WfoTable/styles.ts +6 -0
- package/src/components/WfoTable/utils/tableConfigPersistence.ts +1 -0
- package/src/components/WfoTimeline/styles.ts +1 -1
- package/src/components/WfoWorkflowUserGuide/WfoPageWithUserGuide.tsx +8 -2
- package/src/components/WfoWorkflowUserGuide/WfoWorkflowGuideExpandablePanel.tsx +39 -30
- package/src/components/WfoWorkflowUserGuide/styles.ts +55 -7
- package/src/configuration/constants.ts +1 -1
- package/src/configuration/version.ts +1 -1
- package/src/hooks/usePathAutoComplete.spec.tsx +64 -0
- package/src/hooks/usePathAutoComplete.ts +98 -41
- package/src/hooks/useStoredTableConfig.ts +1 -0
- package/src/messages/en-GB.json +3 -1
- package/src/messages/nl-NL.json +4 -2
- package/src/pages/WfoSearchPocPage.tsx +19 -18
- package/src/pages/tasks/WfoTasksListPage.tsx +1 -0
- package/src/rtk/endpoints/search.ts +1 -0
- package/src/types/types.ts +11 -11
- package/src/utils/getDefaultTableConfig.ts +2 -0
package/jest.config.cjs
CHANGED
|
@@ -2,19 +2,20 @@ const base = require('@orchestrator-ui/jest-config/jest-base.config.js');
|
|
|
2
2
|
const nextJest = require('next/jest');
|
|
3
3
|
|
|
4
4
|
const createJestConfig = nextJest({
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
|
6
|
+
dir: './',
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// The entry for "uuid" in the moduleNameMapper can be removed when EUI updates the dependency version to 9.0.0 or higher.
|
|
10
10
|
// https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md#900-2022-09-05
|
|
11
11
|
const customJestConfig = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
...base,
|
|
13
|
+
displayName: 'Wfo-UI Tests',
|
|
14
|
+
moduleNameMapper: {
|
|
15
|
+
'^uuid$': 'uuid',
|
|
16
|
+
// Mirrors the "@/*" path alias from tsconfig.json
|
|
17
|
+
'^@/(.*)$': '<rootDir>/src/$1',
|
|
18
|
+
},
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
module.exports = createJestConfig(customJestConfig);
|
package/package.json
CHANGED
|
@@ -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:
|
|
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
|
|
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="
|
|
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
|
|
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
|
-
|
|
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
|
|
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={
|
|
249
|
-
tableColumns,
|
|
250
|
-
pageInfo?.sortFields,
|
|
251
|
-
pageInfo?.filterFields,
|
|
252
|
-
)}
|
|
254
|
+
tableColumnConfig={tableConfig}
|
|
253
255
|
dataSorting={[dataSorting]}
|
|
254
256
|
pagination={pagination}
|
|
255
257
|
isLoading={isFetching}
|
|
@@ -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] =
|
|
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
|
-
<
|
|
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(
|
|
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) &&
|
|
150
|
+
{isAllowed(SUBSCRIPTION_VALIDATE + subscriptionId) && validateActionItems.length > 0 && (
|
|
135
151
|
<>
|
|
136
152
|
{!compactMode && <MenuBlock title={t('tasks')} />}
|
|
137
|
-
{
|
|
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) &&
|
|
167
|
+
{isAllowed(SUBSCRIPTION_RECONCILE + subscriptionId) && reconcileActionItems.length > 0 && (
|
|
152
168
|
<>
|
|
153
169
|
{!compactMode && <MenuBlock title={t('reconcile')} />}
|
|
154
|
-
{
|
|
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) &&
|
|
202
|
+
{isAllowed(SUBSCRIPTION_MODIFY + subscriptionId) && modifyActionItems.length > 0 && (
|
|
187
203
|
<>
|
|
188
204
|
<MenuBlock title={t('modify')} />
|
|
189
|
-
{
|
|
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) &&
|
|
220
|
+
{isAllowed(SUBSCRIPTION_TERMINATE + subscriptionId) && terminateActionItems.length > 0 && (
|
|
205
221
|
<>
|
|
206
222
|
<MenuBlock title={t('terminate')} />
|
|
207
|
-
{
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, { useRef, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useMemo, 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
|
|
|
@@ -8,8 +8,8 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|
|
8
8
|
|
|
9
9
|
import { SearchParams, WfoAutoExpandableTextArea, WfoTextAnchor } from '@/components';
|
|
10
10
|
import { WfoCombinatorSelector } from '@/components/WfoTable/WfoStructuredSearchTable/WfoCombinatorSelector';
|
|
11
|
-
import { useWithOrchestratorTheme } from '@/hooks';
|
|
12
|
-
import { OperatorDisplay } from '@/types';
|
|
11
|
+
import { useFieldsPathInfo, useWithOrchestratorTheme } from '@/hooks';
|
|
12
|
+
import { EntityKind, OperatorDisplay } from '@/types';
|
|
13
13
|
import type { FieldToOperatorMap, PathInfo } from '@/types';
|
|
14
14
|
|
|
15
15
|
import { WfoFieldSelector } from './WfoFieldSelector';
|
|
@@ -20,6 +20,7 @@ import { WfoRule } from './WfoRule';
|
|
|
20
20
|
import { WfoRuleGroup } from './WfoRuleGroup';
|
|
21
21
|
import { WfoValueEditor } from './WfoValueEditor';
|
|
22
22
|
import { getWfoStructuredSearchTableStyles } from './styles';
|
|
23
|
+
import { collectRuleFields } from './utils';
|
|
23
24
|
|
|
24
25
|
// Maps PathInfo operator names to react-querybuilder's native operator names,
|
|
25
26
|
// which is what parseCEL produces and formatQuery(cel) expects.
|
|
@@ -129,6 +130,27 @@ export const WfoFilterBuilder = ({
|
|
|
129
130
|
}
|
|
130
131
|
};
|
|
131
132
|
|
|
133
|
+
// Fields can enter the query without passing through the field selector — a filter
|
|
134
|
+
// restored from the URL or added by a column-header search. Resolve their path info
|
|
135
|
+
// so the value editor renders the right input type (date picker, boolean toggle, ...)
|
|
136
|
+
// and the operator selector gets the field's operator list.
|
|
137
|
+
const unresolvedFields = useMemo(
|
|
138
|
+
() =>
|
|
139
|
+
collectRuleFields(queryBuilderRuleGroup).filter(
|
|
140
|
+
(field) => field && field !== '~' && !fieldPathInfoMap.has(field),
|
|
141
|
+
),
|
|
142
|
+
[queryBuilderRuleGroup, fieldPathInfoMap],
|
|
143
|
+
);
|
|
144
|
+
const resolvedFieldsPathInfo = useFieldsPathInfo(unresolvedFields, EntityKind.SUBSCRIPTION);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
resolvedFieldsPathInfo.forEach((pathInfo, field) => {
|
|
148
|
+
if (pathInfo && !fieldPathInfoMap.has(field)) {
|
|
149
|
+
handleFieldSelected(field, pathInfo.operators, pathInfo);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}, [fieldPathInfoMap, resolvedFieldsPathInfo]);
|
|
153
|
+
|
|
132
154
|
return (
|
|
133
155
|
<EuiFlexGroup css={queryBuilderContainerStyles}>
|
|
134
156
|
<EuiFlexGroup direction={'column'}>
|
|
@@ -162,6 +184,11 @@ export const WfoFilterBuilder = ({
|
|
|
162
184
|
addGroupAction: null,
|
|
163
185
|
removeGroupAction: null,
|
|
164
186
|
removeRuleAction: WfoRemoveRuleAction,
|
|
187
|
+
// Field-to-field comparisons are not supported, but a rule can briefly hold
|
|
188
|
+
// valueSource 'field' while a CEL literal is being typed in the textarea
|
|
189
|
+
// ('lldp == fals' parses 'fals' as an identifier) — without this override
|
|
190
|
+
// react-querybuilder's default value source selector flashes into the rule.
|
|
191
|
+
valueSourceSelector: null,
|
|
165
192
|
}}
|
|
166
193
|
addRuleToNewGroups
|
|
167
194
|
onAddGroup={onAddGroupHandler}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reproduces the WfoFilterBuilder wiring around operator selection: a controlled
|
|
3
|
+
* QueryBuilder with resetOnFieldChange=false, getOperators backed by a map that is
|
|
4
|
+
* filled when a field is selected (like fieldToOperatorMap), and a value editor
|
|
5
|
+
* that hides itself for the unary null/notNull operators.
|
|
6
|
+
*/
|
|
7
|
+
import React, { useState } from 'react';
|
|
8
|
+
import type { FieldSelectorProps, FullOperator, RuleGroupType, ValueEditorProps } from 'react-querybuilder';
|
|
9
|
+
import { QueryBuilder } from 'react-querybuilder';
|
|
10
|
+
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
13
|
+
|
|
14
|
+
import { WfoOperatorSelector } from './WfoOperatorSelector';
|
|
15
|
+
|
|
16
|
+
const FIELD_OPERATORS: Record<string, FullOperator[]> = {
|
|
17
|
+
componentField: [
|
|
18
|
+
{ name: 'notNull', label: '✓ has component', value: 'notNull', arity: 'unary' },
|
|
19
|
+
{ name: 'null', label: '✗ does not have component', value: 'null', arity: 'unary' },
|
|
20
|
+
],
|
|
21
|
+
textField: [
|
|
22
|
+
{ name: '=', label: '= equals', value: '=' },
|
|
23
|
+
{ name: '!=', label: '≠ not equals', value: '!=' },
|
|
24
|
+
],
|
|
25
|
+
// A field the autocomplete could not resolve: storeFieldOperators falls back to []
|
|
26
|
+
unknownField: [],
|
|
27
|
+
// A field whose operator list starts with the unary component operators
|
|
28
|
+
mixedField: [
|
|
29
|
+
{ name: 'notNull', label: '✓ has component', value: 'notNull', arity: 'unary' },
|
|
30
|
+
{ name: 'null', label: '✗ does not have component', value: 'null', arity: 'unary' },
|
|
31
|
+
{ name: 'contains', label: '∋ contains', value: 'contains' },
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const FieldSelectorStub = ({ handleOnChange, value, context }: FieldSelectorProps) => (
|
|
36
|
+
<select
|
|
37
|
+
data-testid="field-selector"
|
|
38
|
+
value={value}
|
|
39
|
+
onChange={(e) => {
|
|
40
|
+
context.onFieldSelected(e.target.value);
|
|
41
|
+
handleOnChange(e.target.value);
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<option value="~">~</option>
|
|
45
|
+
<option value="componentField">componentField</option>
|
|
46
|
+
<option value="textField">textField</option>
|
|
47
|
+
<option value="unknownField">unknownField</option>
|
|
48
|
+
<option value="mixedField">mixedField</option>
|
|
49
|
+
</select>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const ValueEditorStub = ({ operator }: ValueEditorProps) => {
|
|
53
|
+
if (operator === 'null' || operator === 'notNull') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return <input data-testid="value-editor" />;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const initialRuleGroup: RuleGroupType = {
|
|
60
|
+
id: 'root',
|
|
61
|
+
rules: [{ id: 'rule-0', field: '~', operator: '=', value: '' }],
|
|
62
|
+
combinator: 'and',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const Harness = ({ onQueryChange }: { onQueryChange?: (q: RuleGroupType) => void }) => {
|
|
66
|
+
const [query, setQuery] = useState<RuleGroupType>(initialRuleGroup);
|
|
67
|
+
const [fieldToOperatorMap, setFieldToOperatorMap] = useState<Map<string, FullOperator[]>>(new Map());
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<QueryBuilder
|
|
71
|
+
query={query}
|
|
72
|
+
enableMountQueryChange={false}
|
|
73
|
+
onQueryChange={(q: RuleGroupType) => {
|
|
74
|
+
setQuery(q);
|
|
75
|
+
onQueryChange?.(q);
|
|
76
|
+
}}
|
|
77
|
+
context={{
|
|
78
|
+
onFieldSelected: (field: string) => {
|
|
79
|
+
setFieldToOperatorMap((previousMap) => new Map(previousMap).set(field, FIELD_OPERATORS[field] ?? []));
|
|
80
|
+
},
|
|
81
|
+
}}
|
|
82
|
+
getOperators={(field) => fieldToOperatorMap.get(field) ?? []}
|
|
83
|
+
controlElements={{
|
|
84
|
+
fieldSelector: FieldSelectorStub,
|
|
85
|
+
operatorSelector: WfoOperatorSelector,
|
|
86
|
+
valueEditor: ValueEditorStub,
|
|
87
|
+
}}
|
|
88
|
+
resetOnFieldChange={false}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const selectField = (field: string) => {
|
|
94
|
+
fireEvent.change(screen.getByTestId('field-selector'), { target: { value: field } });
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// The operator selector is the only select inside the rule row besides the field-selector stub.
|
|
98
|
+
const getOperatorSelect = () =>
|
|
99
|
+
screen
|
|
100
|
+
.getAllByRole<HTMLSelectElement>('combobox')
|
|
101
|
+
.find((select) => select.closest('.rule') && select.dataset.testid !== 'field-selector') as HTMLSelectElement;
|
|
102
|
+
|
|
103
|
+
describe('WfoOperatorSelector operator reset on field change', () => {
|
|
104
|
+
it('resets to the first operator when the selected field does not support the current one', () => {
|
|
105
|
+
render(<Harness />);
|
|
106
|
+
|
|
107
|
+
selectField('componentField');
|
|
108
|
+
expect(getOperatorSelect().value).toBe('notNull');
|
|
109
|
+
expect(screen.queryByTestId('value-editor')).not.toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('recovers from a unary operator when switching to a field without unary operators', () => {
|
|
113
|
+
render(<Harness />);
|
|
114
|
+
|
|
115
|
+
selectField('componentField');
|
|
116
|
+
expect(getOperatorSelect().value).toBe('notNull');
|
|
117
|
+
|
|
118
|
+
selectField('textField');
|
|
119
|
+
expect(getOperatorSelect().value).toBe('=');
|
|
120
|
+
expect(screen.getByTestId('value-editor')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('recovers from a unary operator when switching to a field whose operators are unknown', () => {
|
|
124
|
+
render(<Harness />);
|
|
125
|
+
|
|
126
|
+
selectField('componentField');
|
|
127
|
+
expect(getOperatorSelect().value).toBe('notNull');
|
|
128
|
+
|
|
129
|
+
selectField('unknownField');
|
|
130
|
+
expect(getOperatorSelect().value).not.toBe('notNull');
|
|
131
|
+
expect(screen.getByTestId('value-editor')).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('prefers a non-unary operator as the default when the current operator is invalid', () => {
|
|
135
|
+
render(<Harness />);
|
|
136
|
+
|
|
137
|
+
// The initial rule operator '=' is not in mixedField's list; the reset should skip
|
|
138
|
+
// the leading unary operators so the value editor stays visible.
|
|
139
|
+
selectField('mixedField');
|
|
140
|
+
expect(getOperatorSelect().value).toBe('contains');
|
|
141
|
+
expect(screen.getByTestId('value-editor')).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -15,11 +15,20 @@ const FALLBACK_OPERATOR_LABELS: Record<string, string> = {
|
|
|
15
15
|
null: '✗ does not have component',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// null/notNull hide the value editor, so they only make a sane default when a field
|
|
19
|
+
// offers nothing else; prefer the first operator that keeps the value editor visible.
|
|
20
|
+
const getDefaultOperator = (options: FullOperator[]) =>
|
|
21
|
+
(options.find((option) => option.arity !== 'unary') ?? options[0]).name;
|
|
22
|
+
|
|
18
23
|
export const WfoOperatorSelector = (props: OperatorSelectorProps) => {
|
|
19
24
|
const { value, handleOnChange } = props;
|
|
20
25
|
|
|
21
|
-
const flatOptions = (
|
|
22
|
-
|
|
26
|
+
const flatOptions = useMemo(
|
|
27
|
+
() =>
|
|
28
|
+
(props.options as Array<FullOperator | OptionGroup<FullOperator>>).flatMap((option) =>
|
|
29
|
+
isOptionGroup(option) ? option.options : [option],
|
|
30
|
+
),
|
|
31
|
+
[props.options],
|
|
23
32
|
);
|
|
24
33
|
|
|
25
34
|
const selectOptions = useMemo(
|
|
@@ -34,18 +43,31 @@ export const WfoOperatorSelector = (props: OperatorSelectorProps) => {
|
|
|
34
43
|
const optionsChanged = previousOptionsKeyRef.current !== optionsKey;
|
|
35
44
|
previousOptionsKeyRef.current = optionsKey;
|
|
36
45
|
|
|
37
|
-
// Reset to
|
|
38
|
-
// user picked a (different) field
|
|
39
|
-
// preserved an operator that is not valid for it
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
46
|
+
// Reset to a default option only when the field's operator list changes — i.e. the
|
|
47
|
+
// user picked a (different) field and resetOnFieldChange=false on QueryBuilder
|
|
48
|
+
// preserved an operator that is not valid for it, or WfoFilterBuilder resolved the
|
|
49
|
+
// real operator list of a field restored from CEL (URL or textarea). The
|
|
50
|
+
// `optionsChanged` guard keeps restored rules intact while their field's operators
|
|
51
|
+
// are still unknown: parseCEL can produce operators outside the prefilled operator
|
|
52
|
+
// lists (e.g. beginsWith), and resetting those would silently rewrite the user's
|
|
53
|
+
// filter string.
|
|
54
|
+
if (!optionsChanged) return;
|
|
55
|
+
|
|
56
|
+
if (flatOptions.length === 0) {
|
|
57
|
+
// The new field's operators are unknown (nothing stored for it). A left-over unary
|
|
58
|
+
// operator would keep the value editor hidden with nothing in the dropdown to bring
|
|
59
|
+
// it back, so fall back to the query builder's default operator.
|
|
60
|
+
if (value === 'null' || value === 'notNull') {
|
|
61
|
+
handleOnChange('=');
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
const currentValueIsValid = selectOptions.some((option) => option.value === value);
|
|
45
67
|
if (!currentValueIsValid) {
|
|
46
|
-
handleOnChange(
|
|
68
|
+
handleOnChange(getDefaultOperator(flatOptions));
|
|
47
69
|
}
|
|
48
|
-
}, [optionsKey, selectOptions, value, handleOnChange]);
|
|
70
|
+
}, [optionsKey, flatOptions, selectOptions, value, handleOnChange]);
|
|
49
71
|
|
|
50
72
|
// A restored operator that falls outside the field's list must still be visible in
|
|
51
73
|
// the dropdown; without it EuiSelect renders an empty selection for the rule.
|