From 363052324e6e81e3e1fd0ca96794408579eb81b6 Mon Sep 17 00:00:00 2001 From: LinoH5 Date: Fri, 8 Sep 2023 16:48:35 +0200 Subject: [PATCH 01/12] add basic filter component for location management --- .../location-curating/LocationFilter.tsx | 100 ++++++++++++++++++ .../views/location-curating/LocationPanel.tsx | 27 ++++- .../location-curating/LocationPanelHeader.tsx | 25 ++++- 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx new file mode 100644 index 000000000..8924a1212 --- /dev/null +++ b/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx @@ -0,0 +1,100 @@ +import { Close } from '@mui/icons-material'; +import { Autocomplete, MenuItem, Select, TextField } from '@mui/material'; +import { debounce } from 'lodash'; +import { useEffect, 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, + setFilterType, + setFilterValue, + setOpen, +}: { + filterType: LocationFilterType; + 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( + filterType === LocationFilterType.IS_ANY_OF ? [] : '' + ); + + useEffect(() => { + setLocalFilterValue(filterType === LocationFilterType.IS_ANY_OF ? [] : ''); + }, [filterType]); + + const updateFilterValue = debounce(() => { + setFilterValue(localFilterValue); + }, 1000); + + return ( +
+ { + if (localFilterValue) { + setLocalFilterValue(''); + return; + } + setOpen(false); + }} + /> + + {showTextField() && ( + { + setLocalFilterValue(value.target.value); + updateFilterValue(); + }} + /> + )} + {showAutocomplete() && ( + } + onChange={(_, values) => { + setLocalFilterValue(values); + updateFilterValue(); + }} + /> + )} +
+ ); +}; + +export default LocationFilter; diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx index 34af03667..eca8aa339 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx @@ -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'; @@ -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'; @@ -73,6 +74,10 @@ const LocationPanel = () => { const { canRun: canUseLocationPanel, loading: canUseLocationPanelLoading } = canUseTagTableViewQuery(); + const [isOpen, setOpen] = useState(false); + const [filterType, setFilterType] = useState(LocationFilterType.CONTAINS); + const [filterValue, setFilterValue] = useState(); + return ( {() => { @@ -83,7 +88,25 @@ const LocationPanel = () => { } else { return ( - + { + setOpen(value); + }} + /> + {isOpen && ( + { + setFilterType(value); + }} + setFilterValue={(value: string | string[] | undefined) => { + setFilterValue(value); + }} + setOpen={(value: boolean) => { + setOpen(value); + }} + /> + )}
{tagTree?.map(tag => ( diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationPanelHeader.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationPanelHeader.tsx index 883c7ddf4..3f62aec08 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationPanelHeader.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationPanelHeader.tsx @@ -1,13 +1,34 @@ +import { FilterAlt } from '@mui/icons-material'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import './LocationEntry.scss'; -const LocationPanelHeader = () => { +const LocationPanelHeader = ({ setOpen }: { setOpen: (value: boolean) => void }) => { const { t } = useTranslation(); + const [isHoveredName, setHoveredName] = useState(false); + return ( <>
-
{t('common.name')}
+
{ + setHoveredName(true); + }} + onMouseLeave={() => { + setHoveredName(false); + }} + > +
{t('common.name')}
+ { + setOpen(true); + }} + /> +
{t('curator.synonyms')}
From 220a0c8af585b0e9caaf73a4f45452fa8b4f7178 Mon Sep 17 00:00:00 2001 From: LinoH5 Date: Sat, 9 Sep 2023 19:36:48 +0200 Subject: [PATCH 02/12] add support for basic contains filter for location tags --- .../location-curating/LocationFilter.tsx | 12 +++++- .../views/location-curating/LocationPanel.tsx | 41 +++++++++++++++---- .../tag-structure-helpers.tsx | 21 ++++++++++ 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx index 8924a1212..4eb33f06b 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx @@ -1,7 +1,7 @@ import { Close } from '@mui/icons-material'; import { Autocomplete, MenuItem, Select, TextField } from '@mui/material'; import { debounce } from 'lodash'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; export enum LocationFilterType { CONTAINS = 'contains', @@ -34,13 +34,17 @@ const LocationFilter = ({ const [localFilterValue, setLocalFilterValue] = useState( filterType === LocationFilterType.IS_ANY_OF ? [] : '' ); + const localFilterRef = useRef( + filterType === LocationFilterType.IS_ANY_OF ? [] : '' + ); useEffect(() => { setLocalFilterValue(filterType === LocationFilterType.IS_ANY_OF ? [] : ''); + localFilterRef.current = filterType === LocationFilterType.IS_ANY_OF ? [] : ''; }, [filterType]); const updateFilterValue = debounce(() => { - setFilterValue(localFilterValue); + setFilterValue(localFilterRef.current); }, 1000); return ( @@ -49,7 +53,9 @@ const LocationFilter = ({ className='my-auto mr-2 cursor-pointer' onClick={() => { if (localFilterValue) { + localFilterRef.current = ''; setLocalFilterValue(''); + updateFilterValue(); return; } setOpen(false); @@ -76,6 +82,7 @@ const LocationFilter = ({ { + localFilterRef.current = value.target.value; setLocalFilterValue(value.target.value); updateFilterValue(); }} @@ -88,6 +95,7 @@ const LocationFilter = ({ freeSolo renderInput={props => } onChange={(_, values) => { + localFilterRef.current = values; setLocalFilterValue(values); updateFilterValue(); }} diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx index eca8aa339..1d9bfc8cb 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx @@ -14,7 +14,7 @@ import LocationFilter, { LocationFilterType } from './LocationFilter'; import LocationPanelHeader from './LocationPanelHeader'; import { LocationPanelPermissionsProvider } from './LocationPanelPermissionsProvider'; import { useCreateNewTag } from './location-management-helpers'; -import { useGetTagStructures } from './tag-structure-helpers'; +import { useGetSubtags, useGetTagStructures } from './tag-structure-helpers'; const setUnacceptedSubtagsCount = (tag: FlatTag) => { let subtagCount = 0; @@ -36,6 +36,39 @@ const LocationPanel = () => { const flattened = useSimplifiedQueryResponseData(data); const flattenedTags: FlatTag[] | undefined = flattened ? Object.values(flattened)[0] : undefined; + const [filteredFlattenedTags, setFilteredFlattenedTags] = useState( + flattenedTags + ); + + const { tagTree: sortedTagTree } = useGetTagStructures(filteredFlattenedTags); + const tagSubtagList = useGetSubtags(flattenedTags); + + const [isOpen, setOpen] = useState(false); + const [filterType, setFilterType] = useState(LocationFilterType.CONTAINS); + const [filterValue, setFilterValue] = useState(); + + useEffect(() => { + if (!flattenedTags || !tagSubtagList) { + return; + } + // convert everything to lowercase before matching + switch (filterType) { + case LocationFilterType.CONTAINS: + setFilteredFlattenedTags( + flattenedTags.filter( + flattenedTag => + !filterValue?.length || + flattenedTag.name.toLowerCase().includes((filterValue as string).toLowerCase()) || + tagSubtagList[flattenedTag.id].findIndex(subtag => + subtag.name.toLowerCase().includes((filterValue as string).toLowerCase()) + ) !== -1 + ) + ); + break; + default: + } + }, [filterValue, filterType, flattenedTags, tagSubtagList]); + useEffect(() => { if (!foldoutStatus) { return; @@ -59,8 +92,6 @@ const LocationPanel = () => { const { createNewTag, canCreateNewTag } = useCreateNewTag(refetch); - const { tagTree: sortedTagTree } = useGetTagStructures(flattenedTags); - const tagTree = useMemo(() => { if (!sortedTagTree) return; @@ -74,10 +105,6 @@ const LocationPanel = () => { const { canRun: canUseLocationPanel, loading: canUseLocationPanelLoading } = canUseTagTableViewQuery(); - const [isOpen, setOpen] = useState(false); - const [filterType, setFilterType] = useState(LocationFilterType.CONTAINS); - const [filterValue, setFilterValue] = useState(); - return ( {() => { diff --git a/projects/bp-gallery/src/components/views/location-curating/tag-structure-helpers.tsx b/projects/bp-gallery/src/components/views/location-curating/tag-structure-helpers.tsx index 15bd03066..f7d55e12a 100644 --- a/projects/bp-gallery/src/components/views/location-curating/tag-structure-helpers.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/tag-structure-helpers.tsx @@ -168,6 +168,27 @@ export const useGetTagStructures = ( }; }; +export const useGetSubtags = (flattenedTags?: FlatTag[]) => { + const tagsById = useGetTagsById(flattenedTags); + + const tagSubtagList = useMemo(() => { + if (!tagsById || !flattenedTags) { + return undefined; + } + const tagSubtags = Object.fromEntries(flattenedTags.map(tag => [tag.id, [] as FlatTag[]])); + for (const tag of flattenedTags) { + tagsById[tag.id].parent_tags?.forEach(parent => { + if (tagSubtags[parent.id].findIndex(subtag => subtag.id === parent.id) === -1) { + tagSubtags[parent.id].push(tagsById[tag.id]); + } + }); + } + return tagSubtags; + }, [flattenedTags, tagsById]); + + return tagSubtagList; +}; + export const useGetBreadthFirstOrder = ( tagTree: FlatTag[] | undefined, prioritizedOptions: FlatTag[] | undefined From bf238c836121d7e78e8584ddbb074c32001b63c2 Mon Sep 17 00:00:00 2001 From: LinoH5 Date: Mon, 11 Sep 2023 15:34:48 +0200 Subject: [PATCH 03/12] support closing location tag filter, without resetting it --- .../views/location-curating/LocationFilter.tsx | 18 +++++++++--------- .../views/location-curating/LocationPanel.tsx | 3 +++ .../location-curating/LocationPanelHeader.tsx | 16 +++++++++++++--- .../tag-structure-helpers.tsx | 4 +++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx index 4eb33f06b..c1c09ec49 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx @@ -1,7 +1,7 @@ import { Close } from '@mui/icons-material'; import { Autocomplete, MenuItem, Select, TextField } from '@mui/material'; import { debounce } from 'lodash'; -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; export enum LocationFilterType { CONTAINS = 'contains', @@ -15,11 +15,13 @@ export enum LocationFilterType { 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; @@ -32,23 +34,18 @@ const LocationFilter = ({ const showAutocomplete = () => filterType === LocationFilterType.IS_ANY_OF; const [localFilterValue, setLocalFilterValue] = useState( - filterType === LocationFilterType.IS_ANY_OF ? [] : '' + filterValue ?? (filterType === LocationFilterType.IS_ANY_OF ? [] : '') ); const localFilterRef = useRef( - filterType === LocationFilterType.IS_ANY_OF ? [] : '' + filterValue ?? (filterType === LocationFilterType.IS_ANY_OF ? [] : '') ); - useEffect(() => { - setLocalFilterValue(filterType === LocationFilterType.IS_ANY_OF ? [] : ''); - localFilterRef.current = filterType === LocationFilterType.IS_ANY_OF ? [] : ''; - }, [filterType]); - const updateFilterValue = debounce(() => { setFilterValue(localFilterRef.current); }, 1000); return ( -
+
{ @@ -65,6 +62,9 @@ const LocationFilter = ({ value={filterType} onChange={value => { setFilterType(value.target.value as LocationFilterType); + setLocalFilterValue(filterType === LocationFilterType.IS_ANY_OF ? [] : ''); + localFilterRef.current = filterType === LocationFilterType.IS_ANY_OF ? [] : ''; + updateFilterValue(); }} className='mr-1' > diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx index 1d9bfc8cb..ab123853e 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx @@ -116,13 +116,16 @@ const LocationPanel = () => { return ( { setOpen(value); }} + showFilter={filterValue?.length ? true : false} /> {isOpen && ( { setFilterType(value); }} diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationPanelHeader.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationPanelHeader.tsx index 3f62aec08..bd198fc38 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationPanelHeader.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationPanelHeader.tsx @@ -3,7 +3,15 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import './LocationEntry.scss'; -const LocationPanelHeader = ({ setOpen }: { setOpen: (value: boolean) => void }) => { +const LocationPanelHeader = ({ + isOpen, + setOpen, + showFilter, +}: { + isOpen: boolean; + setOpen: (value: boolean) => void; + showFilter: boolean; +}) => { const { t } = useTranslation(); const [isHoveredName, setHoveredName] = useState(false); @@ -23,9 +31,11 @@ const LocationPanelHeader = ({ setOpen }: { setOpen: (value: boolean) => void })
{t('common.name')}
{ - setOpen(true); + setOpen(!isOpen); }} />
diff --git a/projects/bp-gallery/src/components/views/location-curating/tag-structure-helpers.tsx b/projects/bp-gallery/src/components/views/location-curating/tag-structure-helpers.tsx index f7d55e12a..a11d2889f 100644 --- a/projects/bp-gallery/src/components/views/location-curating/tag-structure-helpers.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/tag-structure-helpers.tsx @@ -27,7 +27,9 @@ const useGetTagTree = ( // set child tags for each tag in tree for (const tag of Object.values(tagsById)) { tag.parent_tags?.forEach(parentTag => { - tagsById[parentTag.id].child_tags?.push(tag); + if (parentTag.id in tagsById) { + tagsById[parentTag.id].child_tags?.push(tag); + } }); } for (const tag of Object.values(tagsById)) { From 97f0f14c58ad06c47e588ea5ed6a189e2307b14e Mon Sep 17 00:00:00 2001 From: LinoH5 Date: Mon, 11 Sep 2023 15:59:31 +0200 Subject: [PATCH 04/12] add further filter functionality --- .../views/location-curating/LocationPanel.tsx | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx index ab123853e..ab9cb977d 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationPanel.tsx @@ -51,7 +51,6 @@ const LocationPanel = () => { if (!flattenedTags || !tagSubtagList) { return; } - // convert everything to lowercase before matching switch (filterType) { case LocationFilterType.CONTAINS: setFilteredFlattenedTags( @@ -65,7 +64,79 @@ const LocationPanel = () => { ) ); break; + case LocationFilterType.EQUALS: + setFilteredFlattenedTags( + flattenedTags.filter( + flattenedTag => + !filterValue?.length || + flattenedTag.name.toLowerCase() === (filterValue as string).toLowerCase() || + tagSubtagList[flattenedTag.id].findIndex( + subtag => subtag.name.toLowerCase() === (filterValue as string).toLowerCase() + ) !== -1 + ) + ); + break; + case LocationFilterType.STARTS_WITH: + setFilteredFlattenedTags( + flattenedTags.filter( + flattenedTag => + !filterValue?.length || + flattenedTag.name.toLowerCase().startsWith((filterValue as string).toLowerCase()) || + tagSubtagList[flattenedTag.id].findIndex(subtag => + subtag.name.toLowerCase().startsWith((filterValue as string).toLowerCase()) + ) !== -1 + ) + ); + break; + case LocationFilterType.ENDS_WITH: + setFilteredFlattenedTags( + flattenedTags.filter( + flattenedTag => + !filterValue?.length || + flattenedTag.name.toLowerCase().endsWith((filterValue as string).toLowerCase()) || + tagSubtagList[flattenedTag.id].findIndex(subtag => + subtag.name.toLowerCase().endsWith((filterValue as string).toLowerCase()) + ) !== -1 + ) + ); + break; + case LocationFilterType.IS_EMPTY: + setFilteredFlattenedTags( + flattenedTags.filter( + flattenedTag => + flattenedTag.name === '' || + tagSubtagList[flattenedTag.id].findIndex(subtag => subtag.name === '') !== -1 + ) + ); + break; + case LocationFilterType.IS_NOT_EMPTY: + setFilteredFlattenedTags( + flattenedTags.filter( + flattenedTag => + flattenedTag.name !== '' || + tagSubtagList[flattenedTag.id].findIndex(subtag => subtag.name !== '') !== -1 + ) + ); + break; + case LocationFilterType.IS_ANY_OF: + setFilteredFlattenedTags( + flattenedTags.filter( + flattenedTag => + !filterValue?.length || + (filterValue as string[]).findIndex( + value => value.toLowerCase() === flattenedTag.name.toLowerCase() + ) !== -1 || + tagSubtagList[flattenedTag.id].findIndex( + subtag => + (filterValue as string[]).findIndex( + value => value.toLowerCase() === subtag.name.toLowerCase() + ) !== -1 + ) !== -1 + ) + ); + break; default: + setFilteredFlattenedTags(flattenedTags); } }, [filterValue, filterType, flattenedTags, tagSubtagList]); From 7bb9ce71c0dc8335a5a5615ec8a2566fc38476c5 Mon Sep 17 00:00:00 2001 From: LinoH5 Date: Mon, 11 Sep 2023 16:38:54 +0200 Subject: [PATCH 05/12] reset location filter on filterType change, type checks --- .../views/location-curating/LocationFilter.tsx | 10 +++++++--- .../views/location-curating/LocationPanel.tsx | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx index c1c09ec49..1cf43550f 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationFilter.tsx @@ -61,10 +61,13 @@ const LocationFilter = ({