diff --git a/frontend/src/assets/icons/case-sensitive.svg b/frontend/src/assets/icons/case-sensitive.svg new file mode 100644 index 0000000000..1383f16794 --- /dev/null +++ b/frontend/src/assets/icons/case-sensitive.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription.ts b/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription.ts index 845d2d14bb..5e01186ad6 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription.ts +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription.ts @@ -1,6 +1,12 @@ import type { Operator } from 'constants/operators'; -export const formatOperatorDescription = (operator: Operator): string => { +export const formatOperatorDescription = ( + operator: Operator, + inverted?: boolean, +): string => { + if (inverted) { + return invertedConstraintOperatorDescriptions[operator]; + } return constraintOperatorDescriptions[operator]; }; @@ -21,3 +27,21 @@ const constraintOperatorDescriptions = { SEMVER_GT: 'is a SemVer greater than', SEMVER_LT: 'is a SemVer less than', }; + +const invertedConstraintOperatorDescriptions = { + IN: 'is not one of', + NOT_IN: 'is one of', + STR_CONTAINS: 'is a string that does not contain', + STR_STARTS_WITH: 'is a string that does not start with', + STR_ENDS_WITH: 'is a string that does not end with', + NUM_EQ: 'is a number not equal to', + NUM_GT: 'is a number not greater than', + NUM_GTE: 'is a number less than', + NUM_LT: 'is a number not less than', + NUM_LTE: 'is a number greater than', + DATE_BEFORE: 'is a date not before', + DATE_AFTER: 'is a date not after', + SEMVER_EQ: 'is a SemVer not equal to', + SEMVER_GT: 'is a SemVer not greater than', + SEMVER_LT: 'is a SemVer not less than', +}; diff --git a/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader.tsx b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader.tsx index bcaf83b751..880de40317 100644 --- a/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader.tsx +++ b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader.tsx @@ -7,24 +7,44 @@ import type { ConstraintSchema } from 'openapi'; import { formatOperatorDescription } from 'component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription'; import { StrategyEvaluationChip } from '../StrategyEvaluationChip/StrategyEvaluationChip'; import { styled, Tooltip } from '@mui/material'; +import { ReactComponent as CaseSensitiveIcon } from 'assets/icons/case-sensitive.svg'; +import { isCaseSensitive } from './isCaseSensitive'; -const Inverted: FC = () => ( - - +const Operator: FC<{ + label: ConstraintSchema['operator']; + inverted?: boolean; +}> = ({ label, inverted }) => ( + + ); -const Operator: FC<{ label: ConstraintSchema['operator'] }> = ({ label }) => ( - - - +const StrategyEvalChipLessInlinePadding = styled(StrategyEvaluationChip)( + ({ theme }) => ({ + '> span': { + paddingInline: theme.spacing(0.5), + }, + }), ); -const CaseInsensitive: FC = () => ( - - Aa} /> - -); +const CaseSensitive: FC = () => { + return ( + + + } + /> + + ); +}; const StyledOperatorGroup = styled('div')(({ theme }) => ({ display: 'flex', @@ -53,9 +73,10 @@ export const ConstraintItemHeader: FC< > {contextName} - {inverted ? : null} - - {caseInsensitive ? : null} + + {isCaseSensitive(operator, caseInsensitive) ? ( + + ) : null} ); diff --git a/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/isCaseSensitive.test.ts b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/isCaseSensitive.test.ts new file mode 100644 index 0000000000..70578bd2a2 --- /dev/null +++ b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/isCaseSensitive.test.ts @@ -0,0 +1,45 @@ +import { + allOperators, + inOperators, + stringOperators, +} from 'constants/operators'; +import { isCaseSensitive } from './isCaseSensitive'; + +test('`IN` and `NOT_IN` are always case sensitive', () => { + expect( + inOperators + .flatMap((operator) => + [true, false, undefined].map((caseInsensitive) => + isCaseSensitive(operator, caseInsensitive), + ), + ) + .every((result) => result === true), + ).toBe(true); +}); + +test('If `caseInsensitive` is true, all operators except for `IN` and `NOT_IN` are considered case insensitive', () => { + expect( + allOperators + .filter((operator) => !inOperators.includes(operator)) + .map((operator) => isCaseSensitive(operator, true)) + .every((result) => result === false), + ).toBe(true); +}); + +test.each([false, undefined])( + 'If `caseInsensitive` is %s, only string (and in) operators are considered case sensitive', + (caseInsensitive) => { + const stringResults = stringOperators.map((operator) => + isCaseSensitive(operator, caseInsensitive), + ); + const nonStringResults = allOperators + .filter( + (operator) => + ![...stringOperators, ...inOperators].includes(operator), + ) + .map((operator) => isCaseSensitive(operator, caseInsensitive)); + + expect(stringResults.every((result) => result === true)).toBe(true); + expect(nonStringResults.every((result) => result === false)).toBe(true); + }, +); diff --git a/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/isCaseSensitive.ts b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/isCaseSensitive.ts new file mode 100644 index 0000000000..d9dfbf0ae1 --- /dev/null +++ b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/isCaseSensitive.ts @@ -0,0 +1,12 @@ +import { + inOperators, + stringOperators, + type Operator, +} from 'constants/operators'; + +export const isCaseSensitive = ( + operator: Operator, + caseInsensitive?: boolean, +) => + inOperators.includes(operator) || + (stringOperators.includes(operator) && !caseInsensitive);