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

638 support filters for location management #639

Draft
wants to merge 12 commits into
base: staging
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,27 @@ const LocationBranch = ({
locationTag,
parentTag,
refetch,
forceFoldout,
}: {
locationTag: FlatTag;
parentTag?: FlatTag;
refetch: () => void;
forceFoldout?: boolean;
}) => {
const foldoutStatus = useFoldoutStatus();

const { t } = useTranslation();
const [localForceFoldout, setLocalForceFoldout] = useState<boolean>(forceFoldout ?? false);
const [showMore, setShowMore] = useState<boolean>(
foldoutStatus?.current && locationTag.id in foldoutStatus.current
? foldoutStatus.current[locationTag.id].isOpen
: false
);

useEffect(() => {
setLocalForceFoldout(forceFoldout ?? false);
}, [forceFoldout]);

useEffect(() => {
if (foldoutStatus?.current && locationTag.id in foldoutStatus.current) {
setShowMore(foldoutStatus.current[locationTag.id].isOpen);
Expand All @@ -40,6 +47,7 @@ const LocationBranch = ({
locationTag={childTag}
parentTag={locationTag}
refetch={refetch}
forceFoldout={forceFoldout && (childTag.child_tags?.length ? true : false)}
/>
);
});
Expand All @@ -53,16 +61,17 @@ const LocationBranch = ({
<LocationEntry
locationTag={locationTag}
parentTag={parentTag}
showMore={showMore}
showMore={showMore || localForceFoldout}
onToggleShowMore={() => {
if (foldoutStatus?.current) {
foldoutStatus.current[locationTag.id] = { isOpen: !showMore };
foldoutStatus.current[locationTag.id] = { isOpen: !(showMore || localForceFoldout) };
}
setShowMore(prev => !prev);
setShowMore(prev => !(prev || localForceFoldout));
setLocalForceFoldout(false);
}}
refetch={refetch}
/>
{showMore && (
{(showMore || localForceFoldout) && (
<div className='sub-location-container'>
{renderSubBranches()}
{canCreateNewTag && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
margin-top: auto;
margin-bottom: auto;
border-left: 2px solid darkgrey;
margin-left: 40px;
padding-left: 8px;
padding-right: 8px;
box-sizing: border-box;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const LocationEntry = ({
color='info'
overlap='circular'
variant='dot'
badgeContent={locationTag.unacceptedSubtags}
badgeContent={locationTag.unacceptedSubtags ?? 0}
>
<IconButton
className='show-more-button'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Close } from '@mui/icons-material';
import { Autocomplete, MenuItem, Select, TextField } from '@mui/material';
import { debounce } from 'lodash';
import { useRef, useState } from 'react';

export enum LocationFilterType {
CONTAINS = 'contains',
EQUALS = 'equals',
STARTS_WITH = 'starts with',
ENDS_WITH = 'ends with',
IS_EMPTY = 'is empty',
IS_NOT_EMPTY = 'is not empty',
IS_ANY_OF = 'is any of',
}

const LocationFilter = ({
filterType,
filterValue,
setFilterType,
setFilterValue,
setOpen,
}: {
filterType: LocationFilterType;
filterValue?: string | string[];
setFilterType: (value: LocationFilterType) => void;
setFilterValue: (value: string | string[] | undefined) => void;
setOpen: (value: boolean) => void;
}) => {
const showTextField = () =>
filterType !== LocationFilterType.IS_EMPTY &&
filterType !== LocationFilterType.IS_NOT_EMPTY &&
filterType !== LocationFilterType.IS_ANY_OF;

const showAutocomplete = () => filterType === LocationFilterType.IS_ANY_OF;

const [localFilterValue, setLocalFilterValue] = useState<string | string[]>(
filterValue ?? (filterType === LocationFilterType.IS_ANY_OF ? [] : '')
);
const localFilterRef = useRef<string | string[] | undefined>(
filterValue ?? (filterType === LocationFilterType.IS_ANY_OF ? [] : '')
);

const updateFilterValue = debounce(() => {
setFilterValue(localFilterRef.current);
}, 1000);

return (
<div className='p-2 fixed top-30 left-0 z-20 bg-white shadow-lg flex rounded-[5px]'>
<Close
className='my-auto mr-2 cursor-pointer'
onClick={() => {
if (localFilterValue) {
localFilterRef.current = '';
setLocalFilterValue('');
updateFilterValue();
return;
}
setOpen(false);
}}
/>
<Select
value={filterType}
onChange={value => {
setLocalFilterValue(
(value.target.value as LocationFilterType) === LocationFilterType.IS_ANY_OF ? [] : ''
);
localFilterRef.current =
(value.target.value as LocationFilterType) === LocationFilterType.IS_ANY_OF ? [] : '';
updateFilterValue();
setFilterType(value.target.value as LocationFilterType);
}}
className='mr-1'
>
<MenuItem value={LocationFilterType.CONTAINS}>{LocationFilterType.CONTAINS}</MenuItem>
<MenuItem value={LocationFilterType.EQUALS}>{LocationFilterType.EQUALS}</MenuItem>
<MenuItem value={LocationFilterType.STARTS_WITH}>{LocationFilterType.STARTS_WITH}</MenuItem>
<MenuItem value={LocationFilterType.ENDS_WITH}>{LocationFilterType.ENDS_WITH}</MenuItem>
<MenuItem value={LocationFilterType.IS_EMPTY}>{LocationFilterType.IS_EMPTY}</MenuItem>
<MenuItem value={LocationFilterType.IS_NOT_EMPTY}>
{LocationFilterType.IS_NOT_EMPTY}
</MenuItem>
<MenuItem value={LocationFilterType.IS_ANY_OF}>{LocationFilterType.IS_ANY_OF}</MenuItem>
</Select>
{showTextField() && (
<TextField
value={localFilterValue}
onChange={value => {
localFilterRef.current = value.target.value;
setLocalFilterValue(value.target.value);
updateFilterValue();
}}
/>
)}
{showAutocomplete() && (
<Autocomplete
value={filterValue && Array.isArray(filterValue) ? filterValue : []}
options={[]}
multiple
freeSolo
renderInput={props => <TextField {...props} />}
onChange={(_, values) => {
localFilterRef.current = values;
setLocalFilterValue(values);
updateFilterValue();
}}
/>
)}
</div>
);
};

export default LocationFilter;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSimplifiedQueryResponseData } from '../../../graphql/queryUtils';
import { useVisit } from '../../../helpers/history';
Expand All @@ -10,6 +10,7 @@ import QueryErrorDisplay from '../../common/QueryErrorDisplay';
import AddLocationEntry from './AddLocationEntry';
import { useFoldoutStatus } from './FoldoutStatusContext';
import LocationBranch from './LocationBranch';
import LocationFilter, { LocationFilterType } from './LocationFilter';
import LocationPanelHeader from './LocationPanelHeader';
import { LocationPanelPermissionsProvider } from './LocationPanelPermissionsProvider';
import { useCreateNewTag } from './location-management-helpers';
Expand All @@ -35,6 +36,139 @@ const LocationPanel = () => {
const flattened = useSimplifiedQueryResponseData(data);
const flattenedTags: FlatTag[] | undefined = flattened ? Object.values(flattened)[0] : undefined;

const [filteredFlattenedTags, setFilteredFlattenedTags] = useState<FlatTag[] | undefined>(
flattenedTags
);

const { tagTree: sortedTagTree } = useGetTagStructures(filteredFlattenedTags);
const { tagSubtagList } = useGetTagStructures(flattenedTags);

const [isOpen, setOpen] = useState<boolean>(false);
const [showFlat, setShowFlat] = useState<boolean>(false);
const [filterType, setFilterType] = useState<LocationFilterType>(LocationFilterType.CONTAINS);
const [filterValue, setFilterValue] = useState<string | string[] | undefined>();

useEffect(() => {
if (!flattenedTags || !tagSubtagList) {
return;
}
// check for correct filterValue type
if (typeof filterValue === 'string' && filterType === LocationFilterType.IS_ANY_OF) {
return;
}
if (Array.isArray(filterValue) && filterType !== LocationFilterType.IS_ANY_OF) {
return;
}
switch (filterType) {
case LocationFilterType.CONTAINS:
setFilteredFlattenedTags(
flattenedTags
.filter(
flattenedTag =>
!filterValue?.length ||
flattenedTag.name.toLowerCase().includes((filterValue as string).toLowerCase()) ||
(!showFlat &&
tagSubtagList[flattenedTag.id].findIndex(subtag =>
subtag.name.toLowerCase().includes((filterValue as string).toLowerCase())
) !== -1)
)
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
break;
case LocationFilterType.EQUALS:
setFilteredFlattenedTags(
flattenedTags
.filter(
flattenedTag =>
!filterValue?.length ||
flattenedTag.name.toLowerCase() === (filterValue as string).toLowerCase() ||
(!showFlat &&
tagSubtagList[flattenedTag.id].findIndex(
subtag => subtag.name.toLowerCase() === (filterValue as string).toLowerCase()
) !== -1)
)
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
break;
case LocationFilterType.STARTS_WITH:
setFilteredFlattenedTags(
flattenedTags
.filter(
flattenedTag =>
!filterValue?.length ||
flattenedTag.name.toLowerCase().startsWith((filterValue as string).toLowerCase()) ||
(!showFlat &&
tagSubtagList[flattenedTag.id].findIndex(subtag =>
subtag.name.toLowerCase().startsWith((filterValue as string).toLowerCase())
) !== -1)
)
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
break;
case LocationFilterType.ENDS_WITH:
setFilteredFlattenedTags(
flattenedTags
.filter(
flattenedTag =>
!filterValue?.length ||
flattenedTag.name.toLowerCase().endsWith((filterValue as string).toLowerCase()) ||
(!showFlat &&
tagSubtagList[flattenedTag.id].findIndex(subtag =>
subtag.name.toLowerCase().endsWith((filterValue as string).toLowerCase())
) !== -1)
)
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
break;
case LocationFilterType.IS_EMPTY:
setFilteredFlattenedTags(
flattenedTags
.filter(
flattenedTag =>
flattenedTag.name === '' ||
(!showFlat &&
tagSubtagList[flattenedTag.id].findIndex(subtag => subtag.name === '') !== -1)
)
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
break;
case LocationFilterType.IS_NOT_EMPTY:
setFilteredFlattenedTags(
flattenedTags
.filter(
flattenedTag =>
flattenedTag.name !== '' ||
(!showFlat &&
tagSubtagList[flattenedTag.id].findIndex(subtag => subtag.name !== '') !== -1)
)
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
break;
case LocationFilterType.IS_ANY_OF:
setFilteredFlattenedTags(
flattenedTags
.filter(
flattenedTag =>
!filterValue?.length ||
(filterValue as string[]).findIndex(
value => value.toLowerCase() === flattenedTag.name.toLowerCase()
) !== -1 ||
(!showFlat &&
tagSubtagList[flattenedTag.id].findIndex(
subtag =>
(filterValue as string[]).findIndex(
value => value.toLowerCase() === subtag.name.toLowerCase()
) !== -1
) !== -1)
)
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
break;
default:
setFilteredFlattenedTags(flattenedTags);
}
}, [filterValue, filterType, flattenedTags, tagSubtagList, showFlat]);

useEffect(() => {
if (!foldoutStatus) {
return;
Expand All @@ -58,8 +192,6 @@ const LocationPanel = () => {

const { createNewTag, canCreateNewTag } = useCreateNewTag(refetch);

const { tagTree: sortedTagTree } = useGetTagStructures(flattenedTags);

const tagTree = useMemo(() => {
if (!sortedTagTree) return;

Expand All @@ -83,10 +215,40 @@ const LocationPanel = () => {
} else {
return (
<LocationPanelPermissionsProvider>
<LocationPanelHeader />
<LocationPanelHeader
isOpen={isOpen}
setOpen={(value: boolean) => {
setOpen(value);
}}
showFlat={showFlat}
setShowFlat={(value: boolean) => {
setShowFlat(value);
}}
showFilter={filterValue?.length ? true : false}
/>
{isOpen && (
<LocationFilter
filterType={filterType}
filterValue={filterValue}
setFilterType={(value: LocationFilterType) => {
setFilterType(value);
}}
setFilterValue={(value: string | string[] | undefined) => {
setFilterValue(value);
}}
setOpen={(value: boolean) => {
setOpen(value);
}}
/>
)}
<div className='location-panel-content'>
{tagTree?.map(tag => (
<LocationBranch key={tag.id} locationTag={tag} refetch={refetch} />
{(showFlat ? filteredFlattenedTags : tagTree)?.map(tag => (
<LocationBranch
key={tag.id}
locationTag={tag}
refetch={refetch}
forceFoldout={filterValue?.length && tag.child_tags?.length ? true : false}
/>
))}
{canCreateNewTag && (
<AddLocationEntry
Expand Down
Loading
Loading