From f195031fece12c2cf7f612f11c5473f5704b243a Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Tue, 27 Aug 2024 13:22:40 +0200 Subject: [PATCH] refactor: Property filter controller operates internal objects (#2618) --- src/property-filter/controller.ts | 73 ++++++++++++++----------- src/property-filter/internal.tsx | 54 ++++++++++-------- src/property-filter/property-editor.tsx | 6 +- src/property-filter/token.tsx | 39 +++++++------ 4 files changed, 92 insertions(+), 80 deletions(-) diff --git a/src/property-filter/controller.ts b/src/property-filter/controller.ts index 73c2ed22a8..a435e1f392 100644 --- a/src/property-filter/controller.ts +++ b/src/property-filter/controller.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { AutosuggestProps } from '../autosuggest/interfaces'; -import { AutosuggestInputRef } from '../internal/components/autosuggest-input'; import { fireNonCancelableEvent, NonCancelableEventHandler } from '../internal/events'; import { I18nStringsOperators, operatorToDescription } from './i18n-utils'; import { @@ -12,55 +11,63 @@ import { InternalFilteringOption, InternalFilteringProperty, InternalFreeTextFiltering, + InternalQuery, + InternalToken, JoinOperation, ParsedText, Query, Token, } from './interfaces'; -import { matchFilteringProperty, matchOperator, matchOperatorPrefix, removeOperator, trimStart } from './utils'; +import { + matchFilteringProperty, + matchOperator, + matchOperatorPrefix, + matchTokenValue, + removeOperator, + trimStart, +} from './utils'; type I18nStringsController = I18nStringsOperators & Pick; -export const getQueryActions = ( - query: Query, - onChange: NonCancelableEventHandler, - inputRef: React.RefObject -) => { - const { tokens, operation } = query; +export const getQueryActions = ({ + query, + onChange, + filteringOptions, +}: { + query: InternalQuery; + onChange: NonCancelableEventHandler; + filteringOptions: readonly InternalFilteringOption[]; +}) => { const fireOnChange = (tokens: readonly Token[], operation: JoinOperation) => fireNonCancelableEvent(onChange, { tokens, operation }); - const setToken = (index: number, newToken: Token) => { - const newTokens = [...tokens]; - if (newTokens && index < newTokens.length) { - newTokens[index] = newToken; - } - fireOnChange(newTokens, operation); + + const setQuery = ({ operation, tokens }: InternalQuery) => { + const matchedTokens = tokens.map(token => matchTokenValue(token, filteringOptions)); + fireOnChange(matchedTokens, operation); }; - const removeToken = (index: number) => { - const newTokens = tokens.filter((_, i) => i !== index); - fireOnChange(newTokens, operation); - inputRef.current?.focus({ preventDropdown: true }); + + const addToken = (token: InternalToken) => { + setQuery({ ...query, tokens: [...query.tokens, token] }); }; - const removeAllTokens = () => { - fireOnChange([], operation); - inputRef.current?.focus({ preventDropdown: true }); + + const updateToken = (updateIndex: number, newToken: InternalToken) => { + setQuery({ ...query, tokens: query.tokens.map((token, index) => (index === updateIndex ? newToken : token)) }); }; - const addToken = (newToken: Token) => { - const newTokens = [...tokens]; - newTokens.push(newToken); - fireOnChange(newTokens, operation); + + const updateOperation = (operation: JoinOperation) => { + setQuery({ ...query, operation }); }; - const setOperation = (newOperation: JoinOperation) => { - fireOnChange(tokens, newOperation); + + const removeToken = (removeIndex: number) => { + setQuery({ ...query, tokens: query.tokens.filter((_, index) => index !== removeIndex) }); }; - return { - setToken, - removeToken, - removeAllTokens, - addToken, - setOperation, + + const removeAllTokens = () => { + setQuery({ ...query, tokens: [] }); }; + + return { addToken, updateToken, updateOperation, removeToken, removeAllTokens }; }; export const getAllowedOperators = (property: InternalFilteringProperty): ComparisonOperator[] => { diff --git a/src/property-filter/internal.tsx b/src/property-filter/internal.tsx index 87cd764746..0c047cf8ef 100644 --- a/src/property-filter/internal.tsx +++ b/src/property-filter/internal.tsx @@ -26,16 +26,15 @@ import { InternalFilteringProperty, InternalFreeTextFiltering, InternalQuery, + InternalToken, ParsedText, PropertyFilterProps, Ref, - Token, } from './interfaces'; import { PropertyEditor } from './property-editor'; import PropertyFilterAutosuggest, { PropertyFilterAutosuggestProps } from './property-filter-autosuggest'; import { TokenButton } from './token'; import { useLoadItems } from './use-load-items'; -import { matchTokenValue } from './utils'; import styles from './styles.css.js'; @@ -95,11 +94,6 @@ const PropertyFilterInternal = React.forwardRef( useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }), []); const showResults = !!query.tokens?.length && !disabled && !!countText; - const { addToken, removeToken, setToken, setOperation, removeAllTokens } = getQueryActions( - query, - onChange, - inputRef - ); const [filteringText, setFilteringText] = useState(''); const { internalProperties, internalOptions, internalQuery, internalFreeText } = (() => { @@ -149,6 +143,12 @@ const PropertyFilterInternal = React.forwardRef( return { internalProperties: [...propertyByKey.values()], internalOptions, internalQuery, internalFreeText }; })(); + const { addToken, updateToken, updateOperation, removeToken, removeAllTokens } = getQueryActions({ + query: internalQuery, + filteringOptions: internalOptions, + onChange, + }); + const parsedText = parseText(filteringText, internalProperties, internalFreeText); const autosuggestOptions = getAutosuggestOptions( parsedText, @@ -160,21 +160,19 @@ const PropertyFilterInternal = React.forwardRef( const createToken = (currentText: string) => { const parsedText = parseText(currentText, internalProperties, internalFreeText); - let newToken: Token; + let newToken: InternalToken; switch (parsedText.step) { case 'property': { - newToken = matchTokenValue( - { - property: parsedText.property, - operator: parsedText.operator, - value: parsedText.value, - }, - internalOptions - ); + newToken = { + property: parsedText.property, + operator: parsedText.operator, + value: parsedText.value, + }; break; } case 'free-text': { newToken = { + property: null, operator: parsedText.operator || internalFreeText.defaultOperator, value: parsedText.value, }; @@ -182,6 +180,7 @@ const PropertyFilterInternal = React.forwardRef( } case 'operator': { newToken = { + property: null, operator: internalFreeText.defaultOperator, value: currentText, }; @@ -342,17 +341,21 @@ const PropertyFilterInternal = React.forwardRef( items={internalQuery.tokens} limitShowFewerAriaLabel={tokenLimitShowFewerAriaLabel} limitShowMoreAriaLabel={tokenLimitShowMoreAriaLabel} - renderItem={(token, tokenIndex) => ( + renderItem={(_, tokenIndex) => ( { + query={internalQuery} + tokenIndex={tokenIndex} + onUpdateToken={token => { + updateToken(tokenIndex, token); + }} + onUpdateOperation={operation => { + updateOperation(operation); + }} + onRemoveToken={() => { removeToken(tokenIndex); + inputRef.current?.focus({ preventDropdown: true }); setRemovedTokenIndex(tokenIndex); }} - setToken={(newToken: Token) => setToken(tokenIndex, newToken)} - setOperation={setOperation} filteringProperties={internalProperties} filteringOptions={internalOptions} asyncProps={asyncProps} @@ -377,7 +380,10 @@ const PropertyFilterInternal = React.forwardRef( ) : ( { + removeAllTokens(); + inputRef.current?.focus({ preventDropdown: true }); + }} className={styles['remove-all']} disabled={disabled} > diff --git a/src/property-filter/property-editor.tsx b/src/property-filter/property-editor.tsx index caa3cc6517..684bae17f0 100644 --- a/src/property-filter/property-editor.tsx +++ b/src/property-filter/property-editor.tsx @@ -6,7 +6,7 @@ import React, { useState } from 'react'; import InternalButton from '../button/internal'; import InternalFormField from '../form-field/internal'; import { I18nStringsInternal } from './i18n-utils'; -import { ComparisonOperator, ExtendedOperatorForm, InternalFilteringProperty, Token } from './interfaces'; +import { ComparisonOperator, ExtendedOperatorForm, InternalFilteringProperty, InternalToken } from './interfaces'; import styles from './styles.css.js'; @@ -16,7 +16,7 @@ interface PropertyEditorProps { filter: string; operatorForm: ExtendedOperatorForm; onCancel: () => void; - onSubmit: (value: Token) => void; + onSubmit: (value: InternalToken) => void; i18nStrings: I18nStringsInternal; } @@ -30,7 +30,7 @@ export function PropertyEditor({ i18nStrings, }: PropertyEditorProps) { const [value, onChange] = useState(null); - const submitToken = () => onSubmit({ propertyKey: property.propertyKey, operator, value }); + const submitToken = () => onSubmit({ property, operator, value }); return (
diff --git a/src/property-filter/token.tsx b/src/property-filter/token.tsx index 76ca28e36a..de33a47641 100644 --- a/src/property-filter/token.tsx +++ b/src/property-filter/token.tsx @@ -13,17 +13,21 @@ import { InternalFilteringOption, InternalFilteringProperty, InternalFreeTextFiltering, + InternalQuery, InternalToken, JoinOperation, LoadItemsDetail, - Token, } from './interfaces'; import { TokenEditor } from './token-editor'; -import { matchTokenValue } from './utils'; import styles from './styles.css.js'; interface TokenProps { + query: InternalQuery; + tokenIndex: number; + onUpdateToken: (newToken: InternalToken) => void; + onUpdateOperation: (newOperation: JoinOperation) => void; + onRemoveToken: () => void; asyncProperties?: boolean; asyncProps: DropdownStatusProps; customGroupsText: readonly GroupText[]; @@ -32,27 +36,20 @@ interface TokenProps { expandToViewport?: boolean; filteringProperties: readonly InternalFilteringProperty[]; filteringOptions: readonly InternalFilteringOption[]; - first?: boolean; hideOperations?: boolean; i18nStrings: I18nStringsInternal; onLoadItems?: NonCancelableEventHandler; - operation: JoinOperation; - removeToken: () => void; - setOperation: (newOperation: JoinOperation) => void; - setToken: (newToken: Token) => void; - token: InternalToken; enableTokenGroups: boolean; } const emptyHandler = () => {}; export const TokenButton = ({ - token, - operation = 'and', - first, - removeToken, - setToken, - setOperation, + query, + onUpdateToken, + onUpdateOperation, + onRemoveToken, + tokenIndex, filteringProperties, filteringOptions, asyncProps, @@ -67,6 +64,8 @@ export const TokenButton = ({ enableTokenGroups, }: TokenProps) => { const tokenRef = useRef(null); + const token = query.tokens[tokenIndex]; + const first = tokenIndex === 0; const formattedToken = i18nStrings.formatToken(token); const [temporaryToken, setTemporaryToken] = useState(token); return ( @@ -84,12 +83,12 @@ export const TokenButton = ({ }, ]} showOperation={!first && !hideOperations} - operation={operation} + operation={query.operation} andText={i18nStrings.operationAndText ?? ''} orText={i18nStrings.operationOrText ?? ''} operationAriaLabel={i18nStrings.tokenOperatorAriaLabel ?? ''} - onChangeOperation={setOperation} - onDismissToken={removeToken} + onChangeOperation={onUpdateOperation} + onDismissToken={onRemoveToken} disabled={disabled} editorContent={ tokenRef.current?.closeEditor()} onSubmit={() => { - setToken(matchTokenValue(temporaryToken, filteringOptions)); + onUpdateToken(temporaryToken); tokenRef.current?.closeEditor(); }} /> @@ -121,9 +120,9 @@ export const TokenButton = ({ onEditorOpen={() => setTemporaryToken(token)} // The properties below are only relevant for grouped tokens that are not supported // by the property filter component yet. - groupOperation={operation} + groupOperation={query.operation} groupAriaLabel={i18nStrings.formatToken(token).formattedText} - groupEditAriaLabel={i18nStrings.groupEditAriaLabel({ operation, tokens: [token] })} + groupEditAriaLabel={i18nStrings.groupEditAriaLabel({ operation: query.operation, tokens: [token] })} onChangeGroupOperation={() => {}} hasGroups={false} popoverSize={enableTokenGroups ? 'content' : 'large'}