import { IconButton, styled } from '@mui/material'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import { isStringOperator, type Operator } from 'constants/operators'; import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext'; import { useCallback, useRef, type FC } from 'react'; import { operatorsForContext } from 'utils/operatorsForContext'; import { ConstraintOperatorSelect } from './ConstraintOperatorSelect.tsx'; import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; import Delete from '@mui/icons-material/Delete'; import { ValueList } from './ValueList.tsx'; import { ReactComponent as CaseSensitiveIcon } from 'assets/icons/case-sensitive.svg'; import { ReactComponent as CaseInsensitiveIcon } from 'assets/icons/case-insensitive.svg'; import { AddValuesWidget } from './AddValuesWidget.tsx'; import { ReactComponent as EqualsIcon } from 'assets/icons/constraint-equals.svg'; import { ReactComponent as NotEqualsIcon } from 'assets/icons/constraint-not-equals.svg'; import { AddSingleValueWidget } from './AddSingleValueWidget.tsx'; import { ConstraintDateInput } from './ConstraintDateInput.tsx'; import { LegalValuesSelector, SingleLegalValueSelector, } from './LegalValuesSelector.tsx'; import { useEditableConstraint } from './useEditableConstraint/useEditableConstraint.js'; import type { IConstraint } from 'interfaces/strategy'; import { type EditableConstraint as EditableConstraintType, isDateConstraint, isMultiValueConstraint, isNumberConstraint, isSemVerConstraint, } from './useEditableConstraint/editable-constraint-type.ts'; import type { ConstraintValidationResult } from './useEditableConstraint/constraint-validator.ts'; const Container = styled('article')(({ theme }) => ({ '--padding': theme.spacing(2), backgroundColor: theme.palette.background.paper, borderRadius: theme.shape.borderRadiusLarge, border: `1px solid ${theme.palette.divider}`, })); const TopRow = styled('div')(({ theme }) => ({ '--gap': theme.spacing(1), padding: 'var(--padding)', display: 'flex', flexFlow: 'row nowrap', alignItems: 'flex-start', justifyItems: 'space-between', gap: 'var(--gap)', })); const ConstraintOptions = styled('div')(({ theme }) => ({ display: 'flex', flexFlow: 'row wrap', gap: 'var(--gap)', alignSelf: 'flex-start', })); const OperatorOptions = styled(ConstraintOptions)(({ theme }) => ({ flexFlow: 'row wrap', })); const ConstraintDetails = styled('div')(({ theme }) => ({ display: 'flex', gap: 'var(--gap)', flexFlow: 'row wrap', width: '100%', height: 'min-content', })); const LegalValuesContainer = styled('div')(({ theme }) => ({ padding: 'var(--padding)', borderTop: `1px dashed ${theme.palette.divider}`, })); const StyledSelect = styled(GeneralSelect)(({ theme }) => ({ fieldset: { border: 'none', borderRadius: 0 }, maxWidth: '25ch', minWidth: '7ch', ':focus-within .MuiSelect-select': { background: 'none', }, ':focus-within fieldset': { borderBottomStyle: 'solid' }, 'label + &': { // mui adds a margin top to 'standard' selects with labels margin: 0, }, '&::before': { borderColor: theme.palette.divider, }, '&&:hover::before': { borderColor: theme.palette.primary.main, }, })); const StyledIconButton = styled(IconButton)(({ theme }) => ({ position: 'absolute', right: theme.spacing(1), })); const StyledButton = styled('button')(({ theme }) => ({ display: 'grid', placeItems: 'center', padding: 0, borderRadius: theme.shape.borderRadius, fontSize: theme.fontSizes.smallerBody, background: theme.palette.secondary.light, border: `1px solid ${theme.palette.secondary.border}`, color: theme.palette.secondary.dark, fontWeight: theme.typography.fontWeightBold, transition: 'all 0.03s ease', '&:is(:hover, :focus-visible)': { outline: `1px solid ${theme.palette.primary.main}`, }, })); const StyledEqualsIcon = styled(EqualsIcon)(({ theme }) => ({ path: { fill: 'currentcolor', }, })); const StyledNotEqualsIcon = styled(NotEqualsIcon)(({ theme }) => ({ path: { fill: theme.palette.text.disabled, }, rect: { fill: theme.palette.text.secondary, }, })); const ButtonPlaceholder = styled('div')(({ theme }) => ({ // this is a trick that lets us use absolute positioning for the button so // that it can go over the operator context fields when necessary (narrow // screens), but still retain necessary space for the button when it's all // on one line. width: theme.spacing(2), })); const StyledCaseInsensitiveIcon = styled(CaseInsensitiveIcon)(({ theme }) => ({ path: { fill: theme.palette.text.disabled, }, rect: { fill: theme.palette.text.secondary, }, })); const StyledCaseSensitiveIcon = styled(CaseSensitiveIcon)(({ theme }) => ({ fill: 'currentcolor', })); const TopRowInput: FC<{ addValues: (value: string | string[]) => void; clearValues: () => void; localConstraint: EditableConstraintType; validator: (value: string) => ConstraintValidationResult; addValuesButtonRef: React.RefObject; }> = ({ addValues, clearValues, localConstraint, validator, addValuesButtonRef, }) => { if (isDateConstraint(localConstraint)) { return ( ); } if (isSemVerConstraint(localConstraint)) { return ( ); } if (isNumberConstraint(localConstraint)) { return ( ); } return ( ); }; type Props = { constraint: IConstraint; onDelete: () => void; onUpdate: (constraint: IConstraint) => void; }; export const EditableConstraint: FC = ({ onDelete, constraint, onUpdate, }) => { const { constraint: localConstraint, updateConstraint, validator, legalValueData, } = useEditableConstraint(constraint, onUpdate); const addValues = useCallback( (value: string | string[]) => updateConstraint({ type: 'add value(s)', payload: value }), [updateConstraint], ); const removeValue = useCallback( (value: string) => updateConstraint({ type: 'remove value', payload: value }), [updateConstraint], ); const clearAll = useCallback( () => updateConstraint({ type: 'clear values' }), [updateConstraint], ); const toggleValue = useCallback( (value: string) => updateConstraint({ type: 'toggle value', payload: value }), [updateConstraint], ); const onOperatorChange = useCallback( (operator: Operator) => { updateConstraint({ type: 'set operator', payload: operator }); }, [updateConstraint], ); const { context } = useUnleashContext(); const { contextName, operator } = localConstraint; const showCaseSensitiveButton = isStringOperator(operator); const deleteButtonRef = useRef(null); const addValuesButtonRef = useRef(null); if (!context) { return null; } const constraintNameOptions = context.map((context) => { return { key: context.name, label: context.name }; }); return ( updateConstraint({ type: 'set context field', payload: contextField, }) } variant='standard' /> updateConstraint({ type: 'toggle inverted operator', }) } > {localConstraint.inverted ? ( ) : ( )} {showCaseSensitiveButton ? ( updateConstraint({ type: 'toggle case sensitivity', }) } > {localConstraint.caseInsensitive ? ( ) : ( )} ) : null} addValuesButtonRef.current ?? deleteButtonRef.current } > {legalValueData ? null : ( )} {legalValueData ? ( {isMultiValueConstraint(localConstraint) ? ( ) : ( )} ) : null} ); };