diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationBranch.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationBranch.tsx index cf21d31cb..ccf33952c 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationBranch.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationBranch.tsx @@ -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(forceFoldout ?? false); const [showMore, setShowMore] = useState( 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); @@ -40,6 +47,7 @@ const LocationBranch = ({ locationTag={childTag} parentTag={locationTag} refetch={refetch} + forceFoldout={forceFoldout && (childTag.child_tags?.length ? true : false)} /> ); }); @@ -53,16 +61,17 @@ const LocationBranch = ({ { 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) && (
{renderSubBranches()} {canCreateNewTag && ( diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationEntry.scss b/projects/bp-gallery/src/components/views/location-curating/LocationEntry.scss index 6de60d8af..e3fe14418 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationEntry.scss +++ b/projects/bp-gallery/src/components/views/location-curating/LocationEntry.scss @@ -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; diff --git a/projects/bp-gallery/src/components/views/location-curating/LocationEntry.tsx b/projects/bp-gallery/src/components/views/location-curating/LocationEntry.tsx index 7528c546b..8109cebdd 100644 --- a/projects/bp-gallery/src/components/views/location-curating/LocationEntry.tsx +++ b/projects/bp-gallery/src/components/views/location-curating/LocationEntry.tsx @@ -57,7 +57,7 @@ const LocationEntry = ({ color='info' overlap='circular' variant='dot' - badgeContent={locationTag.unacceptedSubtags} + badgeContent={locationTag.unacceptedSubtags ?? 0} > 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( + filterValue ?? (filterType === LocationFilterType.IS_ANY_OF ? [] : '') + ); + const localFilterRef = useRef( + filterValue ?? (filterType === LocationFilterType.IS_ANY_OF ? [] : '') + ); + + const updateFilterValue = debounce(() => { + setFilterValue(localFilterRef.current); + }, 1000); + + return ( +
+ { + if (localFilterValue) { + localFilterRef.current = ''; + setLocalFilterValue(''); + updateFilterValue(); + return; + } + setOpen(false); + }} + /> + + {showTextField() && ( + { + localFilterRef.current = value.target.value; + setLocalFilterValue(value.target.value); + updateFilterValue(); + }} + /> + )} + {showAutocomplete() && ( + } + onChange={(_, values) => { + localFilterRef.current = 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..eb53578ed 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'; @@ -35,6 +36,139 @@ 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 } = useGetTagStructures(flattenedTags); + + const [isOpen, setOpen] = useState(false); + const [showFlat, setShowFlat] = useState(false); + const [filterType, setFilterType] = useState(LocationFilterType.CONTAINS); + const [filterValue, setFilterValue] = useState(); + + 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; @@ -58,8 +192,6 @@ const LocationPanel = () => { const { createNewTag, canCreateNewTag } = useCreateNewTag(refetch); - const { tagTree: sortedTagTree } = useGetTagStructures(flattenedTags); - const tagTree = useMemo(() => { if (!sortedTagTree) return; @@ -83,10 +215,40 @@ const LocationPanel = () => { } else { return ( - + { + setOpen(value); + }} + showFlat={showFlat} + setShowFlat={(value: boolean) => { + setShowFlat(value); + }} + showFilter={filterValue?.length ? true : false} + /> + {isOpen && ( + { + setFilterType(value); + }} + setFilterValue={(value: string | string[] | undefined) => { + setFilterValue(value); + }} + setOpen={(value: boolean) => { + setOpen(value); + }} + /> + )}
- {tagTree?.map(tag => ( - + {(showFlat ? filteredFlattenedTags : tagTree)?.map(tag => ( + ))} {canCreateNewTag && ( { +const LocationPanelHeader = ({ + isOpen, + setOpen, + showFlat, + setShowFlat, + showFilter, +}: { + isOpen: boolean; + setOpen: (value: boolean) => void; + showFlat: boolean; + setShowFlat: (value: boolean) => void; + showFilter: boolean; +}) => { const { t } = useTranslation(); + const [isHoveredName, setHoveredName] = useState(false); + return ( <>
-
{t('common.name')}
+
+ { + setShowFlat(!showFlat); + }} + icon={showFlat ? : } + /> +
+
{ + setHoveredName(true); + }} + onMouseLeave={() => { + setHoveredName(false); + }} + > +
{t('common.name')}
+
+ { + setOpen(!isOpen); + }} + icon={} + /> +
+
{t('curator.synonyms')}
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..741f7ba25 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)) { @@ -159,12 +161,43 @@ export const useGetTagStructures = ( return paths; }, [tagTree, topologicalOrder]); + const tagSubtagList = useMemo(() => { + if (!tagsById || !flattenedTags || !tagTree) { + return undefined; + } + const tagSubtags = Object.fromEntries(flattenedTags.map(tag => [tag.id, [] as FlatTag[]])); + + const visit = (tag: FlatTag) => { + if (!tag.child_tags) { + return []; + } + for (const childTag of tag.child_tags) { + tagSubtags[tag.id] = [ + ...tagSubtags[tag.id].concat( + visit(childTag).filter( + filterTag => + tagSubtags[tag.id].findIndex(indexTag => indexTag.id === filterTag.id) === -1 + ) + ), + childTag, + ]; + } + return tagSubtags[tag.id]; + }; + + for (const tag of tagTree) { + visit(tag); + } + return tagSubtags; + }, [flattenedTags, tagsById, tagTree]); + return { tagTree, flattenedTagTree: tagsById, tagChildTags, tagSiblingTags, tagSupertagList, + tagSubtagList, }; }; diff --git a/projects/bp-gallery/src/shared/locales/de.json b/projects/bp-gallery/src/shared/locales/de.json index b3cd0719a..6968e170f 100755 --- a/projects/bp-gallery/src/shared/locales/de.json +++ b/projects/bp-gallery/src/shared/locales/de.json @@ -688,7 +688,10 @@ "not-allowed-to-delete_one": "Für diesen Ort existiert {{ count }} Bild. Er kann erst entfernt werden, wenn es keine Bilder für diesen Ort gibt.", "not-allowed-to-delete_other": "Für diesen Ort existieren {{ count }} Bilder. Er kann erst entfernt werden, wenn es keine Bilder für diesen Ort gibt.", "not-allowed-to-delete-sublocation": "Für einen der Unterorte existieren Bilder. Dieser Ort kann erst entfernt werden, wenn es keine Bilder für seine Unterorte gibt", - "delete-coordinate": "Koordinate löschen" + "delete-coordinate": "Koordinate löschen", + "apply-filter": "Filter anwenden", + "hierarchical": "Hierarchisch", + "flat": "Flach" }, "tooltips": { "detach-location": "Ort aus der Hierarchie lösen.",