Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(conditionBuilder): add support for custom operators in ConditionBuilder #6841

Merged
merged 10 commits into from
Feb 7, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ import { useDataConfigs } from '../utils/useDataConfigs';
import {
Condition,
ConditionGroup,
ConfigType,
LogicalOperator,
Property,
PropertyConfig,
PropertyConfigCustom,
} from '../ConditionBuilder.types';

Expand Down Expand Up @@ -188,8 +188,8 @@ const ConditionBlock = (props: ConditionBlockProps) => {
return isLastCondition(conditionIndex, conditions);
};
const getOperators = () => {
if ((config as PropertyConfigCustom['config'])?.operators) {
return (config as PropertyConfigCustom['config']).operators;
if ((config as ConfigType)?.operators) {
return (config as ConfigType).operators;
}
return operatorConfig.filter(
(operator) => operator.type.indexOf(type) != -1 || operator.type == 'all'
Expand Down Expand Up @@ -304,6 +304,7 @@ const ConditionBlock = (props: ConditionBlockProps) => {
data-name="operatorField"
condition={condition}
type={type}
config={config as ConfigType}
onChange={onOperatorChangeHandler}
>
<ItemOption
Expand All @@ -326,7 +327,7 @@ const ConditionBlock = (props: ConditionBlockProps) => {
showToolTip={true}
data-name="valueField"
condition={condition}
config={config as PropertyConfig}
config={config as ConfigType}
onChange={onValueChangeHandler}
renderChildren={renderChildren}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ import { ConditionBuilder } from '.';
import mdx from './ConditionBuilder.mdx';

import styles from './_storybook-styles.scss?inline';
import { inputData, inputDataDynamicOptions } from './assets/sampleInput';
import {
inputData,
inputDataDynamicOptions,
inputDataForCustomOperator,
} from './assets/sampleInput';
import {
sampleDataStructure_nonHierarchical,
sampleDataStructure_Hierarchical,
initialStateWithCustomOperators,
} from './assets/SampleData';
import uuidv4 from '../../global/js/utils/uuidv4';
import { HIERARCHICAL_VARIANT, NON_HIERARCHICAL_VARIANT } from './utils/util';
Expand All @@ -25,10 +30,6 @@ export default {
component: ConditionBuilder,
tags: ['autodocs'],

// TODO: Define argTypes for props not represented by standard JS types.
// argTypes: {
// egProp: { control: 'color' },
// },
parameters: {
layout: 'fullscreen',
styles,
Expand Down Expand Up @@ -232,6 +233,7 @@ const statementConfigCustom = [
label: 'excl. if',
},
];

export const conditionBuilder = ConditionBuilderTemplate.bind({});
conditionBuilder.storyName = 'Condition Builder';
conditionBuilder.args = {
Expand Down Expand Up @@ -272,6 +274,20 @@ conditionBuilderWithCustomStatements.args = {
statementConfigCustom: statementConfigCustom,
};

export const conditionBuilderWithCustomOperators =
ConditionBuilderTemplate.bind({});
conditionBuilderWithCustomOperators.storyName =
'With Custom operator configuration';
conditionBuilderWithCustomOperators.args = {
inputConfig: inputDataForCustomOperator,
initialState: {
state: initialStateWithCustomOperators,
enabledDefault: true,
},
variant: NON_HIERARCHICAL_VARIANT,
translateWithId: translateWithId,
};

export const conditionBuilderWithActions = ConditionBuilderTemplate.bind({});
conditionBuilderWithActions.storyName = 'With Actions';
conditionBuilderWithActions.args = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from './assets/SampleData';
import { HIERARCHICAL_VARIANT, NON_HIERARCHICAL_VARIANT } from './utils/util';
import CustomInput from './assets/CustomInput';
import { inputDataForCustomOperator } from './assets/sampleInput';

const blockClass = `${pkg.prefix}--condition-builder`;
const componentName = ConditionBuilder.displayName;
Expand Down Expand Up @@ -1688,6 +1689,40 @@ describe(componentName, () => {
expect(screen.getByRole('button', { name: 'excl. if' }));
});

it('check with custom operator configuration ', async () => {
render(
<ConditionBuilder
{...defaultProps}
inputConfig={inputDataForCustomOperator}
/>
);

// add one condition
await act(() => userEvent.click(screen.getByText('Add condition')));

expect(screen.getByRole('option', { name: 'Continent' }));

await act(() =>
userEvent.click(screen.getByRole('option', { name: 'Continent' }))
);

expect(screen.getByRole('option', { name: 'has value' }));

await act(() =>
userEvent.click(screen.getByRole('option', { name: 'has value' }))
);

expect(screen.getByRole('option', { name: 'Africa' }));

await act(() =>
userEvent.click(screen.getByRole('option', { name: 'Africa' }))
);

const selectedItem = screen.getByRole('button', { name: 'Africa' });

expect(selectedItem);
});

it('check description tooltip for property', async () => {
const inputConfig_ = JSON.parse(JSON.stringify(inputData));
inputConfig_.properties[0].description = 'This is a tooltip';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,31 +56,36 @@ export type Operators = {
date: DateOperator;
};

export type option = {
id: string;
label: string;
type Item = { id: string; label: string };

export type option = Item & {
icon?: CarbonIconType;
};

export type PropertyConfigOption = {
type: 'option';
config?: {
options?: option[];
operators?: (Item & { isMultiSelect?: boolean })[];
};
};

export interface PropertyConfigText {
type: 'text';
config: TextInputProps;
operators?: Item[];
}

export interface PropertyConfigTextArea {
type: 'textarea';
config: TextAreaProps;
operators?: Item[];
}

export interface PropertyConfigNumber {
type: 'number';
config: {
operators?: Item[];
min?: number;
max?: number;
step?: number;
Expand All @@ -92,12 +97,14 @@ export type PropertyConfigDate = {
type: 'date';
config: {
valueFormatter?: (value: string) => string;
operators?: Item[];
};
};

export type PropertyConfigTime = {
type: 'time';
config: {
operators?: Item[];
timeZones: string[];
};
};
Expand All @@ -106,46 +113,29 @@ export type PropertyConfigCustom = {
type: 'custom';
config: {
component: React.ComponentType<any>;
operators: {
label: string;
id: string;
}[];
operators?: Item[];
valueFormatter?: (value: string) => string;
};
};

export type PropertyConfig = {
option: PropertyConfigOption;
text: PropertyConfigText;
textarea: PropertyConfigTextArea;
number: PropertyConfigNumber;
date: PropertyConfigDate;
time: PropertyConfigTime;
custom: PropertyConfigCustom;
};

export type Property = {
id: string;
label: string;
export type ConfigType =
| PropertyConfigOption['config']
| PropertyConfigTextArea['config']
| PropertyConfigText['config']
| PropertyConfigNumber['config']
| PropertyConfigDate['config']
| PropertyConfigTime['config']
| PropertyConfigCustom['config'];
export type Property = Item & {
icon?: CarbonIconType;
description?: string;
} & (
| PropertyConfig['option']
| PropertyConfig['text']
| PropertyConfig['number']
| PropertyConfig['date']
| PropertyConfig['textarea']
| PropertyConfig['time']
| PropertyConfig['custom']
);
} & ConfigType;

export type inputConfig = {
properties: Property[];
};

export type Option = {
id: string;
label: string;
export type Option = Item & {
icon?: CarbonIconType;
};
export type Condition = {
Expand Down Expand Up @@ -175,10 +165,8 @@ export type Action = {

export type variantsType = 'Non-Hierarchical' | 'Hierarchical';

export type statementConfig = {
id: string;
export type statementConfig = Item & {
connector: 'and' | 'or';
label: string;
secondaryLabel?: string;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ import { ConditionBuilderContext } from '../ConditionBuilderContext/ConditionBui
import { handleKeyDownForPopover } from '../utils/handleKeyboardEvents';
import {
Condition,
PropertyConfig,
Action,
Option,
ConfigType,
} from '../ConditionBuilder.types';
import { blockClass, getValue } from '../utils/util';
import {
blockClass,
checkForMultiSelectOperator,
getValue,
} from '../utils/util';
import { translationsObject } from '../ConditionBuilderContext/translationObject';

interface ConditionBuilderItemProps extends PropsWithChildren {
Expand All @@ -43,7 +47,7 @@ interface ConditionBuilderItemProps extends PropsWithChildren {
type?: string;
description?: string;
condition?: Action & Condition;
config?: PropertyConfig;
config?: ConfigType;
renderChildren?: (ref: Ref<HTMLDivElement>) => ReactNode;
onChange?: (val: string) => void;
tabIndex?: number;
Expand Down Expand Up @@ -142,8 +146,8 @@ export const ConditionBuilderItem = ({
closePopover();
} else if (
currentField == 'valueField' &&
type == 'option' &&
condition?.operator !== 'oneOf'
type === 'option' &&
!checkForMultiSelectOperator(condition, config)
) {
//close the current popover if the field is valueField and is a single select dropdown. For all other inputs ,popover need to be open on value changes.
closePopover();
Expand Down Expand Up @@ -202,8 +206,19 @@ export const ConditionBuilderItem = ({
}
};

const getCustomOperatorLabel = (propertyLabel) => {
return (
propertyLabel &&
config?.operators?.find((operator) => {
return operator.id === propertyLabel;
})
);
};

const getLabel = () => {
if (propertyLabel) {
if (config?.operators && rest['data-name'] === 'operatorField') {
return getCustomOperatorLabel(propertyLabel)?.label ?? propertyLabel;
} else if (propertyLabel) {
return propertyLabel;
} else if (rest['data-name'] === 'propertyField') {
return addPropertyText;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
Option,
PropertyConfigOption,
} from '../../ConditionBuilder.types';
import { blockClass } from '../../utils/util';
import { blockClass, checkForMultiSelectOperator } from '../../utils/util';

interface ItemOptionForValueFieldProps {
conditionState: Condition & { label?: string };
Expand All @@ -38,7 +38,7 @@ export const ItemOptionForValueField = ({
config = {},
onChange,
}: ItemOptionForValueFieldProps) => {
const multiSelectable = conditionState.operator === 'oneOf';
const multiSelectable = checkForMultiSelectOperator(conditionState, config);

const { popOverSearchThreshold, getOptions, rootState } = useContext(
ConditionBuilderContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,37 @@ export const sampleDataStructure_nonHierarchical = {
],
operator: 'or',
};

export const initialStateWithCustomOperators = {
operator: 'or',
groups: [
{
groupOperator: 'and',
statement: 'ifAll',
id: 'e1c37cb2-3e11-4eb6-937a-b9add468345b',
conditions: [
{
property: 'continent',
operator: 'hasValues',
value: [
{
label: 'Africa',
id: 'Africa',
},
{
label: 'Antarctica',
id: 'Antarctica',
},
],
id: 'b7720ec9-e52a-4a7b-90c1-b4aa3c55daeb',
},
{
property: 'id',
operator: 'hasValue',
value: 'test',
id: 'eba8a891-7203-4b22-bf44-c4a9f0c80c4b',
},
],
},
],
};
Loading
Loading