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

[Dashboard] Bloc zones réglementaires + Création du composant accordéon #1711

Merged
merged 9 commits into from
Sep 25, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ context('Side Window > Mission Form > Mission zone', () => {
// Add control which is most recent than surveillance
cy.clickButton('Ajouter')
cy.clickButton('Ajouter des contrôles')
cy.getDataCy('control-open-by').scrollIntoView().type('ABC')
cy.getDataCy('control-open-by').scrollIntoView().type('ABC', { force: true })
const controlEndDate = getFutureDate(5, 'day')
cy.fill('Date et heure du contrôle (UTC)', controlEndDate)
cy.clickButton('Ajouter un point de contrôle')
Expand Down
8 changes: 4 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"test:unit:watch": "npm run test:unit -- --watch"
},
"dependencies": {
"@mtes-mct/monitor-ui": "22.1.0",
"@mtes-mct/monitor-ui": "23.0.0",
"@reduxjs/toolkit": "2.2.7",
"@rsuite/responsive-nav": "5.0.2",
"@sentry/browser": "7.73.0",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/CustomGlobalStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,8 @@ html {
}
}

h2 {
line-height: normal;
}

`
2 changes: 2 additions & 0 deletions frontend/src/domain/shared_slices/Global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type GlobalStateType = {
displayInterestPointLayer: boolean
displayReportingToAttachLayer: boolean
displayVigilanceAreaLayer: boolean
displayDashboardLayer: boolean

// state entry for other children components whom visibility is already handled by parent components

Expand Down Expand Up @@ -115,6 +116,7 @@ const initialState: GlobalStateType = {
displayInterestPointLayer: true,
displayReportingToAttachLayer: true,
displayVigilanceAreaLayer: true,
displayDashboardLayer: true,

// state entry for other children components whom visibility is already handled by parent components
isLayersSidebarVisible: false,
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/features/Dashboard/Layers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useGetRegulatoryLayersQuery } from '@api/regulatoryLayersAPI'
import { getRegulatoryFeature } from '@features/map/layers/Regulatory/regulatoryGeometryHelpers'
import { getRegulatoryLayerStyle } from '@features/map/layers/styles/administrativeAndRegulatoryLayers.style'
import { useAppSelector } from '@hooks/useAppSelector'
import { Feature } from 'ol'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import { useEffect, useRef } from 'react'

import { Dashboard } from '../types'

import type { BaseMapChildrenProps } from '@features/map/BaseMap'
import type { VectorLayerWithName } from 'domain/types/layer'
import type { Geometry } from 'ol/geom'

export function DashboardLayer({ map }: BaseMapChildrenProps) {
const displayDashboardLayer = useAppSelector(state => state.global.displayDashboardLayer)

const activeDashboardId = useAppSelector(state => state.dashboard.activeDashboardId)

const selectedRegulatoryAreaIds = useAppSelector(state =>
activeDashboardId ? state.dashboard.dashboards?.[activeDashboardId]?.[Dashboard.Block.REGULATORY_AREAS] : []
)

const isLayerVisible = displayDashboardLayer

const { data: regulatoryLayers } = useGetRegulatoryLayersQuery()

const vectorSourceRef = useRef(new VectorSource()) as React.MutableRefObject<VectorSource<Feature<Geometry>>>
const vectorLayerRef = useRef(
new VectorLayer({
renderBuffer: 7,
renderOrder: (a, b) => b.get('area') - a.get('area'),
source: vectorSourceRef.current,
style: getRegulatoryLayerStyle,
updateWhileAnimating: true,
updateWhileInteracting: true,
zIndex: 1500 // TODO: create constants
})
) as React.MutableRefObject<VectorLayerWithName>
;(vectorLayerRef.current as VectorLayerWithName).name = 'DASHBOARD' // TODO: create constants

useEffect(() => {
if (map) {
vectorSourceRef.current.clear(true)

if (regulatoryLayers?.entities) {
const features = (selectedRegulatoryAreaIds ?? []).reduce((feats: Feature[], layerId) => {
const layer = regulatoryLayers.entities[layerId]
if (layer && layer?.geom && layer?.geom?.coordinates.length > 0) {
const feature = getRegulatoryFeature({ code: 'DASHBOARD', layer })

feats.push(feature)
}

return feats
}, [])

vectorSourceRef.current.addFeatures(features)
}
}
}, [map, regulatoryLayers, selectedRegulatoryAreaIds])

useEffect(() => {
map.getLayers().push(vectorLayerRef.current)

return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
map.removeLayer(vectorLayerRef.current)
}
}, [map])

useEffect(() => {
vectorLayerRef.current?.setVisible(isLayerVisible)
}, [isLayerVisible])

return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Accent, Icon, IconButton } from '@mtes-mct/monitor-ui'
import styled from 'styled-components'

type AccordionProps = {
children: React.ReactNode
headerButton?: React.ReactNode
isExpanded: boolean
setExpandedAccordion: () => void
title: string
}

export function Accordion({ children, headerButton, isExpanded, setExpandedAccordion, title }: AccordionProps) {
return (
<AccordionContainer $withCursor={!headerButton}>
<AccordionHeader
aria-controls={`${title}-accordion`}
aria-expanded={isExpanded}
onClick={!headerButton ? setExpandedAccordion : undefined}
>
<TitleContainer>
<Title>{title}</Title>
{headerButton}
</TitleContainer>
<StyledIconButton
$isExpanded={isExpanded}
accent={Accent.TERTIARY}
Icon={Icon.Chevron}
onClick={setExpandedAccordion}
/>
</AccordionHeader>
<HeaderSeparator />
<AccordionContent $isExpanded={isExpanded} id={`${title}-accordion`}>
{children}
</AccordionContent>
</AccordionContainer>
)
}

const AccordionContainer = styled.div<{ $withCursor: boolean }>`
box-shadow: 0px 3px 6px #70778540;
cursor: ${({ $withCursor }) => ($withCursor ? 'pointer' : 'default')};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: pourquoi mettre tout le container avec le pointer ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c'était une demande de Xavier de pouvoir cliquer sur tout le container pour ouvrir l'accordéon. J'ai fait sur tout le container quand il n'y a pas d'autre icône et quand il y a une icône (cf bloc signalement) , je l'ai mis seulement sur le chevron.

Copy link
Collaborator

@maximeperraultdev maximeperraultdev Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: comme ce n'est pas un bouton mais une div cliquable. Ce serait bien d'avoir un aria-control qui réfère ce qu'il se ferme ou pas ainsi qu'un aria-expanded

`
const StyledIconButton = styled(IconButton)<{ $isExpanded: boolean }>`
transform: ${({ $isExpanded }) => ($isExpanded ? 'rotate(180deg)' : 'rotate(0deg)')};
transition: transform 0.3s;
`
const AccordionHeader = styled.header`
display: flex;
justify-content: space-between;
padding: 24px;
`
const TitleContainer = styled.div`
align-items: center;
display: flex;
gap: 16px;
`
const Title = styled.h2`
font-size: 16px;
font-weight: 500;
`

const HeaderSeparator = styled.div`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: si tu met la border-bottom sur la div du container ca fait le meme taf non ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le header a un padding alors que le trait fait toute la longueur c'est pour ça

border-bottom: 2px solid ${p => p.theme.color.gainsboro};
padding: -24px;
`
const AccordionContent = styled.div<{ $isExpanded: boolean }>`
display: flex;
flex-direction: column;
max-height: ${({ $isExpanded }) => ($isExpanded ? '100vh' : '0px')};
overflow-x: hidden;
transition: 0.5s max-height;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { dashboardActions } from '@features/Dashboard/slice'
import { Dashboard } from '@features/Dashboard/types'
import { LayerLegend } from '@features/layersSelector/utils/LayerLegend.style'
import { LayerSelector } from '@features/layersSelector/utils/LayerSelector.style'
import { useAppSelector } from '@hooks/useAppSelector'
import { Accent, Icon, IconButton, THEME } from '@mtes-mct/monitor-ui'
import { transformExtent } from 'ol/proj'
import Projection from 'ol/proj/Projection'
import { createRef } from 'react'
import styled from 'styled-components'

import { useGetRegulatoryLayersQuery } from '../../../../../api/regulatoryLayersAPI'
import { MonitorEnvLayers } from '../../../../../domain/entities/layers/constants'
import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '../../../../../domain/entities/map/constants'
import { setFitToExtent } from '../../../../../domain/shared_slices/Map'
import { useAppDispatch } from '../../../../../hooks/useAppDispatch'

type RegulatoryLayerProps = {
dashboardId: number
isSelected: boolean
layerId: number
}

export function Layer({ dashboardId, isSelected, layerId }: RegulatoryLayerProps) {
const dispatch = useAppDispatch()
const ref = createRef<HTMLSpanElement>()

const selectedRegulatoryAreas = useAppSelector(
state => state.dashboard.dashboards?.[dashboardId]?.[Dashboard.Block.REGULATORY_AREAS]
)

const isZoneSelected = selectedRegulatoryAreas?.includes(layerId)
const { layer } = useGetRegulatoryLayersQuery(undefined, {
selectFromResult: result => ({
layer: result?.currentData?.entities[layerId]
})
})

const handleSelectZone = e => {
e.stopPropagation()

const payload = { itemIds: [layerId], type: Dashboard.Block.REGULATORY_AREAS }
if (isZoneSelected) {
dispatch(dashboardActions.removeItems(payload))
} else {
dispatch(dashboardActions.addItems(payload))
if (!layer?.bbox) {
return
}
const extent = transformExtent(
layer?.bbox,
new Projection({ code: WSG84_PROJECTION }),
new Projection({ code: OPENLAYERS_PROJECTION })
)
dispatch(setFitToExtent(extent))
}
}

const removeZone = e => {
e.stopPropagation()
dispatch(dashboardActions.removeItems({ itemIds: [layerId], type: Dashboard.Block.REGULATORY_AREAS }))
}

const toggleZoneMetadata = () => {
dispatch(dashboardActions.setDashboardPanel({ id: layerId, type: Dashboard.Block.REGULATORY_AREAS }))
}

return (
<StyledLayer ref={ref} $isSelected={isSelected} onClick={toggleZoneMetadata}>
<LayerLegend
layerType={MonitorEnvLayers.REGULATORY_ENV}
legendKey={layer?.entity_name ?? 'aucun'}
type={layer?.thematique ?? 'aucun'}
/>
<LayerSelector.Name $withLargeWidth title={layer?.entity_name}>
{layer?.entity_name ?? 'AUCUN NOM'}
</LayerSelector.Name>

<LayerSelector.IconGroup>
{isSelected ? (
<IconButton
accent={Accent.TERTIARY}
aria-label="Supprimer la zone"
color={THEME.color.slateGray}
Icon={Icon.Close}
onClick={removeZone}
title="Supprimer la/ zone"
/>
) : (
<IconButton
accent={Accent.TERTIARY}
aria-label="Sélectionner la zone"
color={isZoneSelected ? THEME.color.blueGray : THEME.color.slateGray}
data-cy="regulatory-zone-check"
Icon={isZoneSelected ? Icon.PinFilled : Icon.Pin}
onClick={handleSelectZone}
/>
)}
</LayerSelector.IconGroup>
</StyledLayer>
)
}

const StyledLayer = styled(LayerSelector.Layer)<{ $isSelected: boolean }>`
background-color: ${p => p.theme.color.white};
padding-left: 24px;
padding-right: 24px;
${p =>
p.$isSelected &&
`
padding-left: 20px;
padding-right: 20px;
margin-left: 4px;
margin-right: 4px;
`}
`
Loading
Loading