diff --git a/src/components/filter/expert/expertFilterConstants.ts b/src/components/filter/expert/expertFilterConstants.ts index 22f449e0..28170043 100644 --- a/src/components/filter/expert/expertFilterConstants.ts +++ b/src/components/filter/expert/expertFilterConstants.ts @@ -194,12 +194,12 @@ export const OPERATOR_OPTIONS = { IS_PART_OF: { name: OperatorType.IS_PART_OF, customName: OperatorType.IS_PART_OF, - label: 'isPartOf', + label: 'inFilter', }, IS_NOT_PART_OF: { name: OperatorType.IS_NOT_PART_OF, customName: OperatorType.IS_NOT_PART_OF, - label: 'isNotPartOf', + label: 'notInFilter', }, }; diff --git a/src/components/filter/expert/stylesExpertFilter.css b/src/components/filter/expert/stylesExpertFilter.css index d0630750..13b1cd9a 100644 --- a/src/components/filter/expert/stylesExpertFilter.css +++ b/src/components/filter/expert/stylesExpertFilter.css @@ -95,6 +95,14 @@ display: none; } +.queryBuilder-branches .rule:hover .rule-remove { + visibility: visible; +} + +.queryBuilder-branches .rule .rule-remove { + visibility: hidden; +} + .queryBuilder-branches .ruleGroup .ruleGroup::before, .queryBuilder-branches .ruleGroup .ruleGroup::after { left: calc(calc(-0.5rem - 1px) - 1px); @@ -110,7 +118,6 @@ } /* Justify layout */ -.queryBuilder .ruleGroup-remove, .queryBuilder .rule-remove { margin-left: auto; } diff --git a/src/components/inputs/reactQueryBuilder/CombinatorSelector.tsx b/src/components/inputs/reactQueryBuilder/CombinatorSelector.tsx index ca949556..81f4ff75 100644 --- a/src/components/inputs/reactQueryBuilder/CombinatorSelector.tsx +++ b/src/components/inputs/reactQueryBuilder/CombinatorSelector.tsx @@ -9,9 +9,10 @@ import { CombinatorSelectorProps } from 'react-querybuilder'; import { useCallback, useState } from 'react'; import { MaterialValueSelector } from '@react-querybuilder/material'; import { PopupConfirmationDialog } from '../../dialogs/popupConfirmationDialog/PopupConfirmationDialog'; +import { useSelectAppearance } from '../../../hooks/useSelectAppearance'; export function CombinatorSelector(props: CombinatorSelectorProps) { - const { value, handleOnChange } = props; + const { options, value, handleOnChange } = props; const [tempCombinator, setTempCombinator] = useState(value); const [openPopup, setOpenPopup] = useState(false); @@ -35,6 +36,7 @@ export function CombinatorSelector(props: CombinatorSelectorProps) { setTempCombinator(newCombinator); setOpenPopup(true); }} + {...useSelectAppearance(options.length)} /> ); diff --git a/src/components/inputs/reactQueryBuilder/FieldSelector.tsx b/src/components/inputs/reactQueryBuilder/FieldSelector.tsx index 6c166ab3..f23a00e6 100644 --- a/src/components/inputs/reactQueryBuilder/FieldSelector.tsx +++ b/src/components/inputs/reactQueryBuilder/FieldSelector.tsx @@ -7,6 +7,7 @@ import { ValueSelectorProps } from 'react-querybuilder'; import { MaterialValueSelector } from '@react-querybuilder/material'; +import { useSelectAppearance } from '../../../hooks/useSelectAppearance'; const ITEM_HEIGHT = 32; // default value from React query builder defaultNativeSelectStyles that can't be accessed const ITEM_PADDING = 4; @@ -21,5 +22,6 @@ const MenuProps = { }; export function FieldSelector(props: Readonly) { - return ; + const { options } = props; + return ; } diff --git a/src/components/inputs/reactQueryBuilder/PropertyValueEditor.tsx b/src/components/inputs/reactQueryBuilder/PropertyValueEditor.tsx index 70eb3f50..ac432eee 100644 --- a/src/components/inputs/reactQueryBuilder/PropertyValueEditor.tsx +++ b/src/components/inputs/reactQueryBuilder/PropertyValueEditor.tsx @@ -16,6 +16,8 @@ import { OPERATOR_OPTIONS } from '../../filter/expert/expertFilterConstants'; import { FieldConstants } from '../../../utils/constants/fieldConstants'; import { usePredefinedProperties } from '../../../hooks/usePredefinedProperties'; import { EquipmentType } from '../../../utils'; +import { useSelectAppearance } from '../../../hooks/useSelectAppearance'; +import { useCustomFilterOptions } from '../../../hooks/useCustomFilterOptions'; const PROPERTY_VALUE_OPERATORS = [OPERATOR_OPTIONS.IN]; @@ -71,7 +73,7 @@ export function PropertyValueEditor(props: ExpertFilterPropertyProps) { ); return ( - + - + - + } + renderInput={(params) => ( + 0 ? '' : intl.formatMessage({ id: 'valuesList' })} + /> + )} freeSolo autoSelect onChange={(event, value) => { onChange(FieldConstants.PROPERTY_VALUES, value); }} size="small" + filterOptions={useCustomFilterOptions()} /> diff --git a/src/components/inputs/reactQueryBuilder/TextValueEditor.tsx b/src/components/inputs/reactQueryBuilder/TextValueEditor.tsx index 34190a97..896d8997 100644 --- a/src/components/inputs/reactQueryBuilder/TextValueEditor.tsx +++ b/src/components/inputs/reactQueryBuilder/TextValueEditor.tsx @@ -8,8 +8,10 @@ import { ValueEditorProps } from 'react-querybuilder'; import { MaterialValueEditor } from '@react-querybuilder/material'; import { Autocomplete, TextField } from '@mui/material'; +import { useIntl } from 'react-intl'; import { useConvertValue } from './hooks/useConvertValue'; import { useValid } from './hooks/useValid'; +import { useCustomFilterOptions } from '../../../hooks/useCustomFilterOptions'; export function TextValueEditor(props: ValueEditorProps) { useConvertValue(props); @@ -18,6 +20,9 @@ export function TextValueEditor(props: ValueEditorProps) { const { value, handleOnChange, title } = props; // The displayed component totally depends on the value type and not the operator. This way, we have smoother transition. + const customFilterOptions = useCustomFilterOptions(); + const intl = useIntl(); + if (!Array.isArray(value)) { return ; } @@ -29,9 +34,16 @@ export function TextValueEditor(props: ValueEditorProps) { onChange={(event, newValue: any) => handleOnChange(newValue)} multiple fullWidth - renderInput={(params) => } + renderInput={(params) => ( + 0 ? '' : intl.formatMessage({ id: 'valuesList' })} + /> + )} size="small" title={title} + filterOptions={customFilterOptions} /> ); } diff --git a/src/components/inputs/reactQueryBuilder/ValueSelector.tsx b/src/components/inputs/reactQueryBuilder/ValueSelector.tsx index 1fca2b90..9b8f0032 100644 --- a/src/components/inputs/reactQueryBuilder/ValueSelector.tsx +++ b/src/components/inputs/reactQueryBuilder/ValueSelector.tsx @@ -7,7 +7,9 @@ import { ValueSelectorProps } from 'react-querybuilder'; import { MaterialValueSelector } from '@react-querybuilder/material'; +import { useSelectAppearance } from '../../../hooks/useSelectAppearance'; export function ValueSelector(props: ValueSelectorProps) { - return ; + const { options } = props; + return ; } diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 154ad96c..644e6515 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -15,3 +15,4 @@ export * from './usePredefinedProperties'; export * from './usePrevious'; export * from './useSnackMessage'; export * from './useFormatLabelWithUnit'; +export * from './useSelectAppearance'; diff --git a/src/hooks/useCustomFilterOptions.ts b/src/hooks/useCustomFilterOptions.ts new file mode 100644 index 00000000..4b24cb21 --- /dev/null +++ b/src/hooks/useCustomFilterOptions.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { useCallback } from 'react'; +import { createFilterOptions, FilterOptionsState } from '@mui/material'; + +/** + * Hook used to add custom filterOptions, use only when freeSolo = true + */ +export function useCustomFilterOptions() { + return useCallback((options: string[], params: FilterOptionsState) => { + const filter = createFilterOptions(); + const filteredOptions = filter(options, params); + const { inputValue } = params; + + const isExisting = options.some((option) => inputValue === option); + if (isExisting && options.length === 1 && options[0] === inputValue) { + // exact match : nothing to show + return []; + } + + if (inputValue !== '' && !isExisting) { + filteredOptions.push(inputValue); + } + return filteredOptions; + }, []); +} diff --git a/src/hooks/useSelectAppearance.ts b/src/hooks/useSelectAppearance.ts new file mode 100644 index 00000000..c25943b8 --- /dev/null +++ b/src/hooks/useSelectAppearance.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { useMemo } from 'react'; + +/** Hook used to modify the appearance of Select into a readonly TextField, + by hiding display button and setting readOnly prop to true + if options list is only one element long. + P.S : Do not use on AutoComplete. +*/ +export function useSelectAppearance(listLength: number) { + return useMemo(() => { + if (listLength === 1) { + return { + IconComponent: () => null, + sx: { + boxShadow: 'none', + '.MuiOutlinedInput-notchedOutline': { border: 'none' }, + pointerEvents: 'none', + border: 'none', + }, + readOnly: true, + disableUnderline: true, + }; + } + return {}; + }, [listLength]); +} diff --git a/src/translations/en/filterEn.ts b/src/translations/en/filterEn.ts index fe81c1ab..7306840f 100644 --- a/src/translations/en/filterEn.ts +++ b/src/translations/en/filterEn.ts @@ -18,8 +18,8 @@ export const filterEn = { not_exists: 'not exists', between: 'between', in: 'in', - isPartOf: 'is part of', - isNotPartOf: 'is not part of', + inFilter: 'in filter', + notInFilter: 'not in filter', emptyRule: 'Filter contains an empty field', incorrectRule: 'Filter contains an incorrect field', obsoleteFilter: 'This filter is no longer supported. Please remove it or change its equipment type.', diff --git a/src/translations/en/filterExpertEn.ts b/src/translations/en/filterExpertEn.ts index e72652a1..f7465e61 100644 --- a/src/translations/en/filterExpertEn.ts +++ b/src/translations/en/filterExpertEn.ts @@ -189,4 +189,5 @@ export const filterExpertEn = { 'The operator will be changed and will be applied to all the criteria already created in the group.', lowShortCircuitCurrentLimit: 'Low short-circuit current limit', highShortCircuitCurrentLimit: 'High short-circuit current limit', + valuesList: 'Values list', }; diff --git a/src/translations/fr/filterExpertFr.ts b/src/translations/fr/filterExpertFr.ts index 61a462c7..9086b821 100644 --- a/src/translations/fr/filterExpertFr.ts +++ b/src/translations/fr/filterExpertFr.ts @@ -188,4 +188,5 @@ export const filterExpertFr = { changeOperatorMessage: "L'opérateur sera modifié et s'appliquera sur tous les critères déjà créés dans le groupe.", lowShortCircuitCurrentLimit: 'Limite ICC min', highShortCircuitCurrentLimit: 'Limite ICC max', + valuesList: 'Liste de valeurs', }; diff --git a/src/translations/fr/filterFr.ts b/src/translations/fr/filterFr.ts index b4f0f414..e17a6e27 100644 --- a/src/translations/fr/filterFr.ts +++ b/src/translations/fr/filterFr.ts @@ -18,8 +18,8 @@ export const filterFr = { not_exists: "n'existe pas", between: 'entre', in: 'dans', - isPartOf: 'fait partie de', - isNotPartOf: 'ne fait pas partie de', + inFilter: 'dans le filtre', + notInFilter: 'pas dans le filtre', emptyRule: 'Le filtre contient un champ vide', incorrectRule: 'Le filtre contient un champ incorrect', obsoleteFilter: "Ce filtre n'est plus supporté. Veuillez le supprimer ou changer son type d'équipement.",