Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JNG-5905 Fix tags component #454

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<P, T> {
id: string;
Expand All @@ -26,6 +26,7 @@ export interface TagsProps<P, T> {
helperText?: string;
editMode?: boolean;
autoCompleteAttribute: keyof T;
identifierAttribute: string | keyof T;
onAutoCompleteSearch: (searchText: string, preparedQueryCustomizer: QueryCustomizer<T>) => Promise<T[]>;
additionalMaskAttributes?: string[];
limitOptions?: number;
Expand All @@ -44,8 +45,8 @@ export interface TagsProps<P, T> {

/**
* Experimental Tags component to serve as an alternative to aggregation->association collections.
*/
export function Tags<P, T> (props: TagsProps<P, T>) {
*/
export function Tags<P, T>(props: TagsProps<P, T>) {
const {
id,
label,
Expand All @@ -67,10 +68,11 @@ export function Tags<P, T> (props: TagsProps<P, T>) {
createDialogTitle,
createDialogIcon = 'file-document-plus',
onClearDialogsClick,
clearTitle= 'Clear',
clearTitle = 'Clear',
clearIcon = 'close',
additionalMaskAttributes = [],
limitOptions = 10,
identifierAttribute,
} = props;
const [options, setOptions] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(false);
Expand All @@ -79,24 +81,21 @@ export function Tags<P, T> (props: TagsProps<P, T>) {
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}%`,
operator: StringOperation.like,
});
}
const queryCustomizer: QueryCustomizer<any> = {
_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,
Expand All @@ -122,21 +121,27 @@ export function Tags<P, T> (props: TagsProps<P, T>) {
[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
Expand All @@ -156,10 +161,11 @@ export function Tags<P, T> (props: TagsProps<P, T>) {
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('');
Expand Down Expand Up @@ -187,28 +193,22 @@ export function Tags<P, T> (props: TagsProps<P, T>) {
'TagsButtonGroup': true,
})}
>
{loading ? (
<CircularProgress color="inherit" size="1rem" className="TagsLoading" />
) : null}
{loading ? <CircularProgress color="inherit" size="1rem" className="TagsLoading" /> : null}
{!readOnly && onClearDialogsClick ? (
<IconButton disabled={disabled} onClick={onClearDialogsClick} title={clearTitle}>
<MdiIcon path={clearIcon} />
</IconButton>
) : null}
{(!readOnly && onCreateDialogsClick) ? <IconButton
disabled={disabled}
onClick={onCreateDialogsClick}
title={createDialogTitle}
>
<MdiIcon path={createDialogIcon} />
</IconButton> : null}
{(!readOnly && onSearchDialogsClick) ? <IconButton
disabled={disabled}
onClick={onSearchDialogsClick}
title={searchDialogTitle}
>
<MdiIcon path={searchDialogIcon} />
</IconButton> : null}
{!readOnly && onCreateDialogsClick ? (
<IconButton disabled={disabled} onClick={onCreateDialogsClick} title={createDialogTitle}>
<MdiIcon path={createDialogIcon} />
</IconButton>
) : null}
{!readOnly && onSearchDialogsClick ? (
<IconButton disabled={disabled} onClick={onSearchDialogsClick} title={searchDialogTitle}>
<MdiIcon path={searchDialogIcon} />
</IconButton>
) : null}
</ButtonGroup>
</InputAdornment>
),
Expand All @@ -221,4 +221,4 @@ export function Tags<P, T> (props: TagsProps<P, T>) {
} }
/>
);
};
}
Loading