From c553efda4272e5c2ff8a8471d5009cb7043b4841 Mon Sep 17 00:00:00 2001 From: Ben Furber Date: Tue, 5 Nov 2024 17:16:48 +0000 Subject: [PATCH] feat: add tags to map filters --- .../MapFilterList/MapFilterList.stories.tsx | 16 +++ .../src/MapFilterList/MapFilterList.tsx | 66 ++++++---- .../src/MapFilterList/MapFilterListItem.tsx | 12 +- .../MapFilterList/MapFilterListWrapper.tsx | 4 +- .../src/ProfileTagsList/ProfileTagsList.tsx | 6 +- packages/cypress/src/integration/map.spec.ts | 10 +- .../cypress/src/integration/settings.spec.ts | 13 +- packages/cypress/src/support/db/endpoints.ts | 10 -- .../transformAvailableFiltersToGroups.tsx | 2 +- .../Maps/Content/MapView/MapWithList.tsx | 86 ++++--------- .../Content/MapView/MapWithListHeader.tsx | 8 +- src/pages/Maps/Maps.client.tsx | 14 ++- src/pages/Maps/utils/filterPins.test.ts | 115 ++++++++++++++++++ src/pages/Maps/utils/filterPins.ts | 36 ++++++ src/stores/Maps/filter.test.ts | 18 +-- src/stores/Maps/filter.ts | 10 +- src/stores/Maps/maps.store.ts | 7 +- src/styles/leaflet.css | 2 +- src/test/factories/MapPin.ts | 7 -- 19 files changed, 295 insertions(+), 147 deletions(-) create mode 100644 src/pages/Maps/utils/filterPins.test.ts create mode 100644 src/pages/Maps/utils/filterPins.ts diff --git a/packages/components/src/MapFilterList/MapFilterList.stories.tsx b/packages/components/src/MapFilterList/MapFilterList.stories.tsx index b95f346c23..60a683ade2 100644 --- a/packages/components/src/MapFilterList/MapFilterList.stories.tsx +++ b/packages/components/src/MapFilterList/MapFilterList.stories.tsx @@ -65,6 +65,21 @@ const availableFilters: MapFilterOptionsList = [ filterType: 'workspaceType', label: 'Mix', }, + { + _id: 'tag1', + filterType: 'profileTag', + label: 'Tag 1', + }, + { + _id: 'tag2', + filterType: 'profileTag', + label: 'Tag 2', + }, + { + _id: 'tag3', + filterType: 'profileTag', + label: 'Tag 3', + }, ] export const Default: StoryFn = () => { @@ -82,6 +97,7 @@ export const Default: StoryFn = () => { availableFilters={availableFilters} onFilterChange={onFilterChange} onClose={onClose} + pinCount={7} /> ) } diff --git a/packages/components/src/MapFilterList/MapFilterList.tsx b/packages/components/src/MapFilterList/MapFilterList.tsx index a185ae59be..b5ad493ef6 100644 --- a/packages/components/src/MapFilterList/MapFilterList.tsx +++ b/packages/components/src/MapFilterList/MapFilterList.tsx @@ -1,5 +1,6 @@ -import { Flex, Heading, Image, Text } from 'theme-ui' +import { Flex, Heading, Text } from 'theme-ui' +import { Button } from '../Button/Button' import { ButtonIcon } from '../ButtonIcon/ButtonIcon' import { MemberBadge } from '../MemberBadge/MemberBadge' import { MapFilterListItem } from './MapFilterListItem' @@ -16,31 +17,38 @@ export interface IProps { availableFilters: MapFilterOptionsList onClose: () => void onFilterChange: (filter: MapFilterOption) => void + pinCount: number } export const MapFilterList = (props: IProps) => { - const { activeFilters, availableFilters, onClose, onFilterChange } = props + const { activeFilters, availableFilters, onClose, onFilterChange, pinCount } = + props const profileFilters = availableFilters.filter( ({ filterType }) => filterType === 'profileType', ) - const workspaceFilters = availableFilters.filter( - ({ filterType }) => filterType === 'workspaceType', + + const tagFilters = availableFilters.filter( + ({ filterType }) => filterType === 'profileTag', ) const isActive = (checkingFilter: string) => !!activeFilters.find((filter) => filter.label === checkingFilter) + const buttonLabel = `Show ${pinCount} result${pinCount === 1 ? '' : 's'}` + return ( - + So what are you looking for? @@ -53,11 +61,11 @@ export const MapFilterList = (props: IProps) => { /> - {workspaceFilters.length > 0 && ( - <> - Workspace: + {profileFilters.length > 0 && ( + + Profiles: - {workspaceFilters.map((typeFilter, index) => { + {profileFilters.map((typeFilter, index) => { const onClick = () => onFilterChange(typeFilter) return ( @@ -65,13 +73,12 @@ export const MapFilterList = (props: IProps) => { active={isActive(typeFilter.label)} key={index} onClick={onClick} + filterType="profile" > - {typeFilter.imageSrc && ( - - )} + {typeFilter.label} @@ -79,14 +86,14 @@ export const MapFilterList = (props: IProps) => { ) })} - + )} - {profileFilters.length > 0 && ( - <> - Profiles: + {tagFilters.length > 0 && ( + + Activities: - {profileFilters.map((typeFilter, index) => { + {tagFilters.map((typeFilter, index) => { const onClick = () => onFilterChange(typeFilter) return ( @@ -94,11 +101,9 @@ export const MapFilterList = (props: IProps) => { active={isActive(typeFilter.label)} key={index} onClick={onClick} + sx={{ maxWidth: 'auto', width: 'auto' }} + filterType="tag" > - {typeFilter.label} @@ -106,8 +111,17 @@ export const MapFilterList = (props: IProps) => { ) })} - + )} + + ) } diff --git a/packages/components/src/MapFilterList/MapFilterListItem.tsx b/packages/components/src/MapFilterList/MapFilterListItem.tsx index 0373a1151a..9dba4109ff 100644 --- a/packages/components/src/MapFilterList/MapFilterListItem.tsx +++ b/packages/components/src/MapFilterList/MapFilterListItem.tsx @@ -1,18 +1,25 @@ import { CardButton } from '../CardButton/CardButton' +import type { ThemeUIStyleObject } from 'theme-ui' + interface IProps { active: boolean onClick: () => void children: React.ReactNode + filterType: string + sx?: ThemeUIStyleObject | undefined } -export const MapFilterListItem = ({ active, onClick, children }: IProps) => { +export const MapFilterListItem = (props: IProps) => { + const { active, onClick, children, filterType, sx } = props return ( { borderColor: 'offWhite', ':hover': { borderColor: 'offWhite' }, }), + ...sx, }} > {children} diff --git a/packages/components/src/MapFilterList/MapFilterListWrapper.tsx b/packages/components/src/MapFilterList/MapFilterListWrapper.tsx index ab39db7f91..5d6c08b708 100644 --- a/packages/components/src/MapFilterList/MapFilterListWrapper.tsx +++ b/packages/components/src/MapFilterList/MapFilterListWrapper.tsx @@ -10,9 +10,9 @@ export const MapFilterListWrapper = ({ children }: IProps) => ( data-cy="MapFilterList" sx={{ listStyle: 'none', - flexWrap: 'nowrap', + flexWrap: 'wrap', gap: 2, - flexDirection: 'column', + flexDirection: 'row', padding: 0, }} > diff --git a/packages/components/src/ProfileTagsList/ProfileTagsList.tsx b/packages/components/src/ProfileTagsList/ProfileTagsList.tsx index ae56017e5a..a08e060c19 100644 --- a/packages/components/src/ProfileTagsList/ProfileTagsList.tsx +++ b/packages/components/src/ProfileTagsList/ProfileTagsList.tsx @@ -3,14 +3,16 @@ import { Flex } from 'theme-ui' import { Category } from '../Category/Category' import type { ITag } from 'oa-shared' +import type { ThemeUIStyleObject } from 'theme-ui' export interface IProps { tags: ITag[] + sx?: ThemeUIStyleObject | undefined } -export const ProfileTagsList = ({ tags }: IProps) => { +export const ProfileTagsList = ({ sx, tags }: IProps) => { return ( - + {tags.map( (tag, index) => tag?.label && ( diff --git a/packages/cypress/src/integration/map.spec.ts b/packages/cypress/src/integration/map.spec.ts index adeeedda1b..583b37b1d0 100644 --- a/packages/cypress/src/integration/map.spec.ts +++ b/packages/cypress/src/integration/map.spec.ts @@ -41,8 +41,7 @@ describe('[Map]', () => { .children() .should('have.length', profileTypesCount) cy.get('[data-cy=MapListFilter]').first().click() - // Reduction in coverage until temp API removed - // cy.get('[data-cy="list-results"]').contains('6 results in view') + cy.get('[data-cy=MapListFilter-active]').first().click() cy.get('[data-cy="list-results"]').contains(/\d+ results in view/) @@ -57,7 +56,12 @@ describe('[Map]', () => { cy.get('[data-cy=MapFilterList]').should('not.exist') cy.get('[data-cy=MapFilterList-OpenButton]').first().click() cy.get('[data-cy=MapFilterList]').should('be.visible') - cy.get('[data-cy=MapFilterList-CloseButton]').first().click() + cy.get('[data-cy=MapFilterListItem-profile]').first().click() + cy.get('[data-cy=MapFilterListItem-profile-active]').first().click() + cy.get('[data-cy=MapFilterListItem-tag]').first().click() + cy.get('[data-cy=MapFilterListItem-tag-active]').first().click() + cy.get('[data-cy=MapFilterList-ShowResultsButton]').first().click() + cy.step('As the user moves in the list updates') for (let i = 0; i < 6; i++) { cy.get('.leaflet-control-zoom-in').click() diff --git a/packages/cypress/src/integration/settings.spec.ts b/packages/cypress/src/integration/settings.spec.ts index f9c7c5c5c0..e6426eb531 100644 --- a/packages/cypress/src/integration/settings.spec.ts +++ b/packages/cypress/src/integration/settings.spec.ts @@ -51,7 +51,7 @@ describe('[Settings]', () => { const description = "I'm a very active member" const mapPinDescription = 'Fun, vibrant and full of amazing people' const profileType = 'member' - const tag = 'Sewing' + const tag = ['Sewing', 'Accounting'] const user = generateNewUserDetails() const url = 'https://social.network' @@ -80,7 +80,8 @@ describe('[Settings]', () => { country, description, }) - cy.selectTag(tag, '[data-cy=tag-select]') + cy.selectTag(tag[0], '[data-cy=tag-select]') + cy.selectTag(tag[1], '[data-cy=tag-select]') cy.get('[data-cy="country:BO"]') cy.step('Errors if trying to upload invalid image') @@ -131,6 +132,8 @@ describe('[Settings]', () => { cy.contains(displayName) cy.contains(description) cy.contains(country) + cy.contains(tag[0]) + cy.contains(tag[1]) cy.get('[data-cy="country:bo"]') cy.get(`[data-cy="MemberBadge-${profileType}"]`) cy.get('[data-cy="profile-avatar"]') @@ -153,7 +156,13 @@ describe('[Settings]', () => { cy.get('[data-cy="tab-Profile"]').click() cy.get('[data-cy=location-dropdown]').should('not.exist') + cy.step('Can view pin on new map') + cy.visit('/map#ci_myn7wmq') + cy.get('[data-cy=Banner]').contains('Test it out!').click() + cy.get('[data-cy=CardListItem]').contains(user.username) + cy.step('Can delete map pin') + cy.visit('/settings') cy.get('[data-cy="tab-Map"]').click() cy.get('[data-cy=remove-map-pin]').click() cy.get('[data-cy="Confirm.modal: Confirm"]').click() diff --git a/packages/cypress/src/support/db/endpoints.ts b/packages/cypress/src/support/db/endpoints.ts index 50e44b0d39..7ea72dac51 100644 --- a/packages/cypress/src/support/db/endpoints.ts +++ b/packages/cypress/src/support/db/endpoints.ts @@ -1,15 +1,5 @@ import { generateDBEndpoints } from 'oa-shared' -// React apps populate a process variable, however it might not always be accessible outside -// (e.g. cypress will instead use it's own env to populate a prefix) - -/** - * A prefix can be used to simplify large-scale schema changes or multisite hosting - * and allow multiple sites to use one DB (used for parallel test seed DBs) - * e.g. oa_ - * SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint - */ - /** * Mapping of generic database endpoints to specific prefixed and revisioned versions for the * current implementation diff --git a/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx b/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx index 16ceb4c2db..fac5f73963 100644 --- a/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx +++ b/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx @@ -34,7 +34,7 @@ const asOptions = (mapPins, items: Array): FilterGroupOption[] => return { label: item.displayName, value, - number: filterMapPinsByType(mapPins, filterType).length, + number: filterMapPinsByType(mapPins, filterType, false).length, imageElement: (item.type as string) === 'verified' ? ( diff --git a/src/pages/Maps/Content/MapView/MapWithList.tsx b/src/pages/Maps/Content/MapView/MapWithList.tsx index 420bbcd5a5..4b00201dcc 100644 --- a/src/pages/Maps/Content/MapView/MapWithList.tsx +++ b/src/pages/Maps/Content/MapView/MapWithList.tsx @@ -3,6 +3,7 @@ import { Tooltip } from 'react-tooltip' import { Button, Map } from 'oa-components' import { Box, Flex } from 'theme-ui' +import { filterPins } from '../../utils/filterPins' import { allMapFilterOptions } from './allMapFilterOptions' import { Clusters } from './Cluster.client' import { latLongFilter } from './latLongFilter' @@ -21,32 +22,35 @@ import type { Map as MapType } from 'react-leaflet' interface IProps { activePin: IMapPin | null center: ILatLng + initialZoom: number mapRef: React.RefObject notification?: string onBlur: () => void onPinClicked: (pin: IMapPin) => void onLocationChange: (latlng: ILatLng) => void pins: IMapPin[] + promptUserLocation: () => Promise setZoom: (arg: number) => void zoom: number - promptUserLocation: () => Promise - INITIAL_ZOOM: number } +const ZOOM_IN_TOOLTIP = 'Zoom in to your location' +const ZOOM_OUT_TOOLTIP = 'Zoom out to world view' + export const MapWithList = (props: IProps) => { const { activePin, center, + initialZoom, mapRef, notification, onBlur, onLocationChange, onPinClicked, pins, + promptUserLocation, setZoom, zoom, - promptUserLocation, - INITIAL_ZOOM, } = props const [activePinFilters, setActivePinFilters] = @@ -60,10 +64,9 @@ export const MapWithList = (props: IProps) => { const availableFilters = useMemo(() => { const pinDetails = pins.map(({ creator }) => [ creator?.profileType, - creator?.workspaceType, + ...(creator?.tags ? creator.tags.map(({ _id }) => _id) : []), ]) const filtersNeeded = [...new Set(pinDetails.flat())] - return allMapFilterOptions.filter((validFilter) => filtersNeeded.some((neededfilter) => neededfilter === validFilter._id), ) @@ -71,43 +74,16 @@ export const MapWithList = (props: IProps) => { const buttonStyle = { backgroundColor: 'white', - padding: '20px', + borderRadius: 99, + padding: 4, ':hover': { backgroundColor: 'lightgray', }, } - const ZOOM_IN_TOOLTIP = 'Zoom in to your location' - const ZOOM_OUT_TOOLTIP = 'Zoom out to world view' - useEffect(() => { - const workspaceTypeFilters = activePinFilters - .filter(({ filterType }) => filterType === 'workspaceType') - .map(({ _id }) => _id) - - if (workspaceTypeFilters.length > 0) { - const workspaceFilteredList = allPinsInView.filter( - ({ creator }) => - creator?.workspaceType && - workspaceTypeFilters.includes(creator.workspaceType), - ) - return setFilteredPins(workspaceFilteredList) - } - - const profileTypeFilters = activePinFilters - .filter(({ filterType }) => filterType === 'profileType') - .map(({ _id }) => _id) - - if (profileTypeFilters.length > 0) { - const profileTypeFilteredList = allPinsInView.filter( - ({ creator }) => - creator?.profileType && - profileTypeFilters.includes(creator?.profileType), - ) - return setFilteredPins(profileTypeFilteredList) - } - - setFilteredPins(allPinsInView) + const filtered = filterPins(activePinFilters, allPinsInView) + return setFilteredPins(filtered) }, [activePinFilters, allPinsInView]) const handleLocationFilter = () => { @@ -132,29 +108,10 @@ export const MapWithList = (props: IProps) => { ) } - const addingWorkspaceTypeFilter = - changedOption.filterType === 'workspaceType' - - if (addingWorkspaceTypeFilter) { - const existingWorkspaceTypeFilters = activePinFilters.filter( - ({ filterType }) => filterType === 'workspaceType', - ) - - return setActivePinFilters([ - { - _id: 'workspace', - filterType: 'profileType', - label: 'Workspace', - }, - ...existingWorkspaceTypeFilters, - changedOption, - ]) - } - - const existingProfileTypeFilters = activePinFilters.filter( - ({ filterType }) => filterType === 'profileType', - ) - return setActivePinFilters([...existingProfileTypeFilters, changedOption]) + return setActivePinFilters((activePinFilters) => [ + ...activePinFilters, + changedOption, + ]) } const isViewportGreaterThanTablet = window.innerWidth > 1024 @@ -257,15 +214,15 @@ export const MapWithList = (props: IProps) => { - {/* Location button to Zoom in to your location */}