From a1781be1947622d9ee5957c0db257107e66d246b Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Tue, 17 Dec 2024 11:12:51 -0500 Subject: [PATCH] Finalise legend component --- .../legend/legend-layer-container.tsx | 92 ++++++++------ .../components/legend/legend-layer-ctrl.tsx | 113 ++++++++++-------- .../components/legend/legend-layer-items.tsx | 43 ++++--- .../core/components/legend/legend-layer.tsx | 83 +++++++------ .../core/components/legend/legend-styles.ts | 1 - .../src/core/components/legend/legend.tsx | 1 - 6 files changed, 183 insertions(+), 150 deletions(-) diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx index fc9f7ac20fd..cb10c5b3f58 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx @@ -3,71 +3,85 @@ import { memo } from 'react'; import { Box, Collapse, List } from '@/ui'; import { TypeLegendLayer } from '@/core/components/layers/types'; import { getSxClasses } from './legend-styles'; -import { logger } from '@/core/utils/logger'; import { CV_CONST_LAYER_TYPES } from '@/api/config/types/config-constants'; import { ItemsList } from './legend-layer-items'; +import { logger } from '@/core/utils/logger'; // Define component types and interfaces type LegendLayerType = React.FC<{ layer: TypeLegendLayer }>; interface CollapsibleContentProps { layer: TypeLegendLayer; - legendExpanded: boolean; + legendExpanded: boolean; // Expanded come from store ordered layer info array initLightBox: (imgSrc: string, title: string, index: number, total: number) => void; - childLayers: TypeLegendLayer[]; - items: TypeLegendLayer['items']; LegendLayerComponent: LegendLayerType; } -// CollapsibleContent component moved after LegendLayer +interface WMSLegendImageProps { + imgSrc: string; + initLightBox: (imgSrc: string, title: string, index: number, total: number) => void; + legendExpanded: boolean; + sxClasses: Record; +} + +// Extracted WMS Legend Component +const WMSLegendImage = memo( + ({ imgSrc, initLightBox, legendExpanded, sxClasses }: WMSLegendImageProps): JSX.Element => ( + + initLightBox(imgSrc, '', 0, 2)} + onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)} + /> + + ) +); +WMSLegendImage.displayName = 'WMSLegendImage'; + export const CollapsibleContent = memo(function CollapsibleContent({ layer, legendExpanded, initLightBox, - childLayers, - items, LegendLayerComponent, -}: CollapsibleContentProps) { - logger.logDebug('legend1 collapsible', layer, childLayers, items); +}: CollapsibleContentProps): JSX.Element | null { + logger.logTraceRender('components/legend/legend-layer-container'); + + // Hooks const theme = useTheme(); const sxClasses = getSxClasses(theme); - // If it is a WMS and there is a legend image, add it with the lightbox handlers - if (layer.type === CV_CONST_LAYER_TYPES.WMS && layer.icons.length && layer.icons[0].iconImage && layer.icons[0].iconImage !== 'no data') { - const imgSrc = layer.icons[0].iconImage; + // Props extraction + const { children, items } = layer; + + // Early returns + if (children?.length === 0 && items?.length === 1) return null; + + const isWMSWithLegend = layer.type === CV_CONST_LAYER_TYPES.WMS && layer.icons?.[0]?.iconImage && layer.icons[0].iconImage !== 'no data'; + + // If it is a WMS legend, create a specific component + if (isWMSWithLegend) { return ( - - initLightBox(imgSrc, '', 0, 2)} - onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)} - /> - + ); } - // If there is only one item or no childlayer, do not create the component - if (childLayers?.length === 0 && items?.length === 1) { - return null; - } - - if (layer.children?.length > 0) layer.children.every((item) => logger.logDebug('TTT item', item.layerPath)); - - // Render list of items (layer class) or there is a child layer so render a new legend-layer component return ( - {layer.children?.length > 0 && ( - - {layer.children - .filter((d) => !['error', 'processing'].includes(d.layerStatus ?? '')) - .map((item) => ( - - ))} - - )} + + {layer.children + .filter((d) => !['error', 'processing'].includes(d.layerStatus ?? '')) + .map((item) => ( + + ))} + ); diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx index 05ef5b1a3e6..3c913aced08 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx @@ -1,5 +1,5 @@ import { useTheme } from '@mui/material'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, @@ -13,74 +13,91 @@ import { HighlightIcon, } from '@/ui'; import { useLayerHighlightedLayer, useLayerStoreActions } from '@/core/stores/store-interface-and-intial-values/layer-state'; -import { TypeLegendLayer } from '@/core/components/layers/types'; +import { TypeLegendItem, TypeLegendLayer } from '@/core/components/layers/types'; import { useMapStoreActions } from '@/core/stores/'; import { getSxClasses } from './legend-styles'; import { logger } from '@/core/utils/logger'; interface SecondaryControlsProps { layer: TypeLegendLayer; - layerStatus: string; - itemsLength: number; - childLayers: TypeLegendLayer[]; - visibility: boolean; + visibility: boolean; // Visibility come from store ordered layer info array } -// SecondaryControls component -export function SecondaryControls({ layer, layerStatus, itemsLength, childLayers, visibility }: SecondaryControlsProps): JSX.Element { +type ControlActions = { + handleToggleVisibility: (e: React.MouseEvent) => void; + handleHighlightLayer: (e: React.MouseEvent) => void; + handleZoomTo: (e: React.MouseEvent) => void; +}; + +// Custom hook for control actions +const useControlActions = (layerPath: string): ControlActions => { + const { setOrToggleLayerVisibility } = useMapStoreActions(); + const { setHighlightLayer, zoomToLayerExtent } = useLayerStoreActions(); + + return useMemo( + () => ({ + handleToggleVisibility: (e: React.MouseEvent): void => { + e.stopPropagation(); + setOrToggleLayerVisibility(layerPath); + }, + handleHighlightLayer: (e: React.MouseEvent): void => { + e.stopPropagation(); + setHighlightLayer(layerPath); + }, + handleZoomTo: (e: React.MouseEvent): void => { + e.stopPropagation(); + zoomToLayerExtent(layerPath).catch((error) => { + logger.logPromiseFailed('in zoomToLayerExtent in legend-layer.handleZoomTo', error); + }); + }, + }), + [layerPath, setHighlightLayer, setOrToggleLayerVisibility, zoomToLayerExtent] + ); +}; + +// Create subtitle +const useSubtitle = (children: TypeLegendLayer[], items: TypeLegendItem[]): string => { // Hooks const { t } = useTranslation(); + + return useMemo(() => { + if (children.length) { + return t('legend.subLayersCount').replace('{count}', children.length.toString()); + } + if (items.length > 1) { + return t('legend.itemsCount') + .replace('{count}', items.filter((item) => item.isVisible).length.toString()) + .replace('{totalCount}', items.length.toString()); + } + return ''; + }, [children.length, items, t]); +}; + +// SecondaryControls component +export function SecondaryControls({ layer, visibility }: SecondaryControlsProps): JSX.Element { + logger.logTraceRender('components/legend/legend-layer-ctrl'); + + // Hooks const theme = useTheme(); const sxClasses = useMemo(() => getSxClasses(theme), [theme]); // Stores const highlightedLayer = useLayerHighlightedLayer(); - const { setOrToggleLayerVisibility } = useMapStoreActions(); - const { setHighlightLayer, zoomToLayerExtent } = useLayerStoreActions(); // Is button disabled? const isLayerVisible = layer.controls?.visibility ?? false; - // #region Handlers Callbacks - const handleToggleVisibility = useCallback( - (e: React.MouseEvent): void => { - e.stopPropagation(); - setOrToggleLayerVisibility(layer.layerPath); - }, - [layer.layerPath, setOrToggleLayerVisibility] - ); - - const handleHighlightLayer = useCallback( - (e: React.MouseEvent): void => { - e.stopPropagation(); - setHighlightLayer(layer.layerPath); - }, - [layer.layerPath, setHighlightLayer] - ); + // Extract constant from layer prop + const { layerStatus, items, children } = layer; - const handleZoomTo = useCallback( - (e: React.MouseEvent): void => { - e.stopPropagation(); - zoomToLayerExtent(layer.layerPath).catch((error) => { - logger.logPromiseFailed('in zoomToLayerExtent in legend-layer.handleZoomTo', error); - }); - }, - [layer.layerPath, zoomToLayerExtent] - ); - // #endregion Handlers + // Component helper + const controls = useControlActions(layer.layerPath); + const subTitle = useSubtitle(children, items); - if (!['processed', 'loaded'].includes(layerStatus)) { + if (!['processed', 'loaded'].includes(layerStatus || 'error')) { return ; } - // Calculate subtitle after the condition - let subTitle = ''; - if (childLayers.length) { - subTitle = t('legend.subLayersCount').replace('{count}', childLayers.length.toString()); - } else if (itemsLength > 1) { - subTitle = t('legend.itemsCount').replace('{count}', itemsLength.toString()).replace('{totalCount}', itemsLength.toString()); - } - return ( {!!subTitle.length && {subTitle}} @@ -89,7 +106,7 @@ export function SecondaryControls({ layer, layerStatus, itemsLength, childLayers edge="end" tooltip="layers.toggleVisibility" className="buttonOutline" - onClick={handleToggleVisibility} + onClick={controls.handleToggleVisibility} disabled={!isLayerVisible} > {visibility ? : } @@ -98,11 +115,11 @@ export function SecondaryControls({ layer, layerStatus, itemsLength, childLayers tooltip="legend.highlightLayer" sx={{ marginTop: '-0.3125rem' }} className="buttonOutline" - onClick={handleHighlightLayer} + onClick={controls.handleHighlightLayer} > {highlightedLayer === layer.layerPath ? : } - + diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx index 551eb050ff7..6b03220e074 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx @@ -1,35 +1,42 @@ import { useTheme } from '@mui/material'; import { memo, useMemo } from 'react'; import { Box, ListItem, Tooltip, ListItemText, ListItemIcon, List, BrowserNotSupportedIcon } from '@/ui'; -import { TypeLegendLayer } from '@/core/components/layers/types'; +import { TypeLegendItem } from '@/core/components/layers/types'; import { getSxClasses } from './legend-styles'; import { logger } from '@/core/utils/logger'; interface ItemsListProps { - items: TypeLegendLayer['items']; + items: TypeLegendItem[]; } -// ItemsList component -export const ItemsList = memo(function ItemsList({ items }: ItemsListProps) { - logger.logDebug('legend1 item list', items); - - // Hooks - const theme = useTheme(); - const sxClasses = useMemo(() => getSxClasses(theme), [theme]); - - if (!items?.length) { - return null; - } - - // Direct mapping since we only reach this code if items has content - const listItems = items.map((item) => ( +// Extracted ListItem Component +const LegendListItem = memo( + ({ item }: { item: TypeLegendItem }): JSX.Element => ( {item.icon ? : } - )); + ) +); +LegendListItem.displayName = 'LegendListItem'; - return {listItems}; +export const ItemsList = memo(function ItemsList({ items }: ItemsListProps): JSX.Element | null { + logger.logTraceRender('components/legend/legend-layer-items'); + + // Hooks + const theme = useTheme(); + const sxClasses = useMemo(() => getSxClasses(theme), [theme]); + + if (!items?.length) return null; + + // Direct mapping since we only reach this code if items has content + return ( + + {items.map((item) => ( + + ))} + + ); }); diff --git a/packages/geoview-core/src/core/components/legend/legend-layer.tsx b/packages/geoview-core/src/core/components/legend/legend-layer.tsx index f05a3fd3ce6..315498bc911 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; import { useTheme } from '@mui/material'; import { Box, ListItem, Tooltip, ListItemText, IconButton, KeyboardArrowDownIcon, KeyboardArrowUpIcon } from '@/ui'; import { TypeLegendLayer } from '@/core/components/layers/types'; @@ -13,6 +13,37 @@ interface LegendLayerProps { layer: TypeLegendLayer; } +interface LegendLayerHeaderProps { + layer: TypeLegendLayer; + isCollapsed: boolean; + isVisible: boolean; + onExpandClick: (e: React.MouseEvent) => void; +} + +// Extracted Header Component +const LegendLayerHeader = memo( + ({ layer, isCollapsed, isVisible, onExpandClick }: LegendLayerHeaderProps): JSX.Element => ( + + + + } + /> + + {(layer.children?.length > 1 || layer.items?.length > 1) && ( + + {!isCollapsed ? : } + + )} + + ) +); +LegendLayerHeader.displayName = 'LegendLayerHeader'; + // Main LegendLayer component export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { // Hooks @@ -23,20 +54,18 @@ export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { const { initLightBox, LightBoxComponent } = useLightBox(); const { getLegendCollapsedFromOrderedLayerInfo, getVisibilityFromOrderedLayerInfo, setLegendCollapsed } = useMapStoreActions(); const { getLayerStatus } = useLayerStoreActions(); - - // const [layerState, setLayerState] = useState(layer); const isCollapsed = getLegendCollapsedFromOrderedLayerInfo(layer.layerPath); const isVisible = getVisibilityFromOrderedLayerInfo(layer.layerPath); const layerStatus = getLayerStatus(layer.layerPath); // Create a new layer object with updated status (no useMemo to ensure updates) - const currentLayer = { ...layer, layerStatus }; - - // Memoized values - const layerChildren = useMemo( - () => currentLayer.children?.filter((c) => ['processed', 'loaded'].includes(c.layerStatus ?? '')) ?? [], - [currentLayer.children] - ); + const currentLayer = { + ...layer, + layerStatus, + items: layer.items?.map((item) => ({ + ...item, + })), + }; const handleExpandGroupClick = useCallback( (e: React.MouseEvent): void => { @@ -49,43 +78,11 @@ export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { return ( <> - - - <> - - - } - /> - - {!!(layer.children?.length > 1 || layer.items?.length > 1) && ( - - {!isCollapsed ? : } - - )} - - + diff --git a/packages/geoview-core/src/core/components/legend/legend-styles.ts b/packages/geoview-core/src/core/components/legend/legend-styles.ts index 4d776e271b8..65d0a4dcfbb 100644 --- a/packages/geoview-core/src/core/components/legend/legend-styles.ts +++ b/packages/geoview-core/src/core/components/legend/legend-styles.ts @@ -24,7 +24,6 @@ export const getSxClasses = (theme: Theme): SxClasses => ({ fontWeight: 'normal', fontSize: theme.palette.geoViewFontSize.md, textAlign: 'left', - marginBottom: '15px', }, layersListContainer: { padding: '20px', diff --git a/packages/geoview-core/src/core/components/legend/legend.tsx b/packages/geoview-core/src/core/components/legend/legend.tsx index 898b3c01074..2bf0657c9dc 100644 --- a/packages/geoview-core/src/core/components/legend/legend.tsx +++ b/packages/geoview-core/src/core/components/legend/legend.tsx @@ -19,7 +19,6 @@ interface LegendType { } export function Legend({ fullWidth, containerType = 'footerBar' }: LegendType): JSX.Element { - // Log logger.logTraceRender('components/legend/legend'); const mapId = useGeoViewMapId();