diff --git a/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs index 04452822..954fbcd2 100644 --- a/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs @@ -1,19 +1,19 @@ {{> fragment.header.hbs }} -import { type MouseEvent, type SyntheticEvent, useCallback, useMemo, useState, useRef, useEffect } from 'react'; -import clsx from 'clsx'; -import InputAdornment from '@mui/material/InputAdornment'; +import Autocomplete from '@mui/material/Autocomplete'; import ButtonGroup from '@mui/material/ButtonGroup'; import CircularProgress from '@mui/material/CircularProgress'; import IconButton from '@mui/material/IconButton'; -import Autocomplete from '@mui/material/Autocomplete'; +import InputAdornment from '@mui/material/InputAdornment'; import TextField from '@mui/material/TextField'; -import { MdiIcon } from '~/components/MdiIcon'; import { debounce } from '@mui/material/utils'; +import clsx from 'clsx'; +import { type MouseEvent, type SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { MdiIcon } from '~/components/MdiIcon'; import { debounceInputs } from '~/config/general'; import { QueryCustomizer } from '~/services/data-api/common/QueryCustomizer'; -import { FilterByTypesString } from '~/services/data-api/rest/FilterByTypesString'; import { StringOperation } from '~/services/data-api/model/StringOperation'; +import { FilterByTypesString } from '~/services/data-api/rest/FilterByTypesString'; export interface TagsProps { id: string; @@ -26,6 +26,7 @@ export interface TagsProps { helperText?: string; editMode?: boolean; autoCompleteAttribute: keyof T; + identifierAttribute: string | keyof T; onAutoCompleteSearch: (searchText: string, preparedQueryCustomizer: QueryCustomizer) => Promise; additionalMaskAttributes?: string[]; limitOptions?: number; @@ -44,8 +45,8 @@ export interface TagsProps { /** * Experimental Tags component to serve as an alternative to aggregation->association collections. -*/ -export function Tags (props: TagsProps) { + */ +export function Tags(props: TagsProps) { const { id, label, @@ -67,10 +68,11 @@ export function Tags (props: TagsProps) { createDialogTitle, createDialogIcon = 'file-document-plus', onClearDialogsClick, - clearTitle= 'Clear', + clearTitle = 'Clear', clearIcon = 'close', additionalMaskAttributes = [], limitOptions = 10, + identifierAttribute, } = props; const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); @@ -79,10 +81,7 @@ export function Tags (props: TagsProps) { const handleSearch = async (searchText: string) => { try { setLoading(true); - const filter: FilterByTypesString[] = (ownerData[name] as T[] ?? []).map((c: any) => ({ - value: c[autoCompleteAttribute]!, - operator: StringOperation.notEqual, - })); + const filter: FilterByTypesString[] = []; if (searchText) { filter.push({ value: `%${searchText}%`, @@ -90,13 +89,13 @@ export function Tags (props: TagsProps) { }); } const queryCustomizer: QueryCustomizer = { - _mask: `{${autoCompleteAttribute as string}${additionalMaskAttributes.length ? (',' + additionalMaskAttributes.join(',')) : ''}}`, + _mask: `{${autoCompleteAttribute as string}${additionalMaskAttributes.length ? ',' + additionalMaskAttributes.join(',') : ''}}`, [autoCompleteAttribute]: filter, _orderBy: [ { attribute: autoCompleteAttribute as string, descending: false, - } + }, ], _seek: { limit: limitOptions, @@ -122,21 +121,27 @@ export function Tags (props: TagsProps) { [ownerData], ); - const onChange = useCallback((event: SyntheticEvent, value: (string | any)[]) => { - // prevent useEffect below from triggering recursion - prevValues.current = value; - onValueChange(value as any); - }, [ownerData, onValueChange]); + const onChange = useCallback( + (event: SyntheticEvent, value: (string | any)[]) => { + // prevent useEffect below from triggering recursion + prevValues.current = value; + onValueChange(value as any); + }, + [ownerData, onValueChange], + ); - const onChipClicked = useCallback((event: MouseEvent) => { - const label = (event.target as HTMLSpanElement).textContent; - if (label) { - const data = (ownerData[name] as T[] ?? []).find((c: any) => c[autoCompleteAttribute] === label); - if (data && onItemClick) { - onItemClick(data); + const onChipClicked = useCallback( + (event: MouseEvent) => { + const label = (event.target as HTMLSpanElement).textContent; + if (label) { + const data = ((ownerData[name] as T[]) ?? []).find((c: any) => c[autoCompleteAttribute] === label); + if (data && onItemClick) { + onItemClick(data); + } } - } - }, [ownerData, onItemClick]); + }, + [ownerData, onItemClick], + ); useEffect(() => { // prevent recursion @@ -156,10 +161,11 @@ export function Tags (props: TagsProps) { readOnly={readOnly} options={options} loading={loading} - value={ownerData[name] as T[] ?? []} + value={(ownerData[name] as T[]) ?? []} disableClearable={true} + getOptionKey={(option) => option[identifierAttribute]} getOptionLabel={(option) => option[autoCompleteAttribute] ?? ''} - isOptionEqualToValue={(option, value) => option[autoCompleteAttribute] === value[autoCompleteAttribute]} + isOptionEqualToValue={(option, value) => option[identifierAttribute] === value[identifierAttribute]} onOpen={ () => { setOptions([]); // always start with a clean slate handleSearch(''); @@ -187,28 +193,22 @@ export function Tags (props: TagsProps) { 'TagsButtonGroup': true, })} > - {loading ? ( - - ) : null} + {loading ? : null} {!readOnly && onClearDialogsClick ? ( ) : null} - {(!readOnly && onCreateDialogsClick) ? - - : null} - {(!readOnly && onSearchDialogsClick) ? - - : null} + {!readOnly && onCreateDialogsClick ? ( + + + + ) : null} + {!readOnly && onSearchDialogsClick ? ( + + + + ) : null} ), @@ -221,4 +221,4 @@ export function Tags (props: TagsProps) { } } /> ); -}; +}