From 82e5f48a2b436a2c4cf0cb1e383b48e061dbdeee Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Mon, 16 Dec 2024 16:35:24 -0500 Subject: [PATCH] fix children vis-expand --- .../public/configs/navigator/28-geocore.json | 12 + .../legend/legend-layer-container.tsx | 144 ++++++------ .../components/legend/legend-layer-ctrl.tsx | 222 +++++++++--------- .../components/legend/legend-layer-items.tsx | 71 +++--- .../core/components/legend/legend-layer.tsx | 124 +++++----- .../src/core/components/legend/legend.tsx | 1 - .../layer-state.ts | 12 + .../map-state.ts | 29 ++- 8 files changed, 330 insertions(+), 285 deletions(-) diff --git a/packages/geoview-core/public/configs/navigator/28-geocore.json b/packages/geoview-core/public/configs/navigator/28-geocore.json index d0dfec36113..139ba497d62 100644 --- a/packages/geoview-core/public/configs/navigator/28-geocore.json +++ b/packages/geoview-core/public/configs/navigator/28-geocore.json @@ -17,6 +17,18 @@ { "geoviewLayerType": "geoCore", "geoviewLayerId": "ccc75c12-5acc-4a6a-959f-ef6f621147b9" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "0fca08b5-e9d0-414b-a3c4-092ff9c5e326" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "03ccfb5c-a06e-43e3-80fd-09d4f8f69703" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "6433173f-bca8-44e6-be8e-3e8a19d3c299" } ] }, 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 980d204d369..fc9f7ac20fd 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 @@ -1,70 +1,74 @@ -import { useTheme } from '@mui/material'; -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'; - -// Define component types and interfaces -type LegendLayerType = React.FC<{ layer: TypeLegendLayer }>; - -interface CollapsibleContentProps { - layer: TypeLegendLayer; - legendExpanded: boolean; - initLightBox: (imgSrc: string, title: string, index: number, total: number) => void; - childLayers: TypeLegendLayer[]; - items: TypeLegendLayer['items']; - LegendLayerComponent: LegendLayerType; -} - -// CollapsibleContent component moved after LegendLayer -export const CollapsibleContent = memo(function CollapsibleContent({ - layer, - legendExpanded, - initLightBox, - childLayers, - items, - LegendLayerComponent, -}: CollapsibleContentProps) { - logger.logDebug('legend1 collapsible', layer, childLayers, items); - const theme = useTheme(); - const sxClasses = getSxClasses(theme); - - 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; - return ( - - initLightBox(imgSrc, '', 0, 2)} - onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)} - /> - - ); - } - - // if (!(childLayers?.length > 1 || items?.length > 1)) { - // return null; - // } - - // TODO: childslayers always empty... seems to be use for items - return ( - - {layer.children?.length > 0 && ( - - {layer.children - .filter((d) => !['error', 'processing'].includes(d.layerStatus ?? '')) - .map((item) => ( - - ))} - - )} - - - ); -}); +import { useTheme } from '@mui/material'; +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'; + +// Define component types and interfaces +type LegendLayerType = React.FC<{ layer: TypeLegendLayer }>; + +interface CollapsibleContentProps { + layer: TypeLegendLayer; + legendExpanded: boolean; + initLightBox: (imgSrc: string, title: string, index: number, total: number) => void; + childLayers: TypeLegendLayer[]; + items: TypeLegendLayer['items']; + LegendLayerComponent: LegendLayerType; +} + +// CollapsibleContent component moved after LegendLayer +export const CollapsibleContent = memo(function CollapsibleContent({ + layer, + legendExpanded, + initLightBox, + childLayers, + items, + LegendLayerComponent, +}: CollapsibleContentProps) { + logger.logDebug('legend1 collapsible', layer, childLayers, items); + 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; + 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) => ( + + ))} + + )} + + + ); +}); 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 bd993ee435c..05ef5b1a3e6 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,111 +1,111 @@ -import { useTheme } from '@mui/material'; -import { memo, useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Box, - IconButton, - Stack, - VisibilityOutlinedIcon, - HighlightOutlinedIcon, - ZoomInSearchIcon, - Typography, - VisibilityOffOutlinedIcon, - HighlightIcon, -} from '@/ui'; -import { useLayerHighlightedLayer, useLayerStoreActions } from '@/core/stores/store-interface-and-intial-values/layer-state'; -import { 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[]; -} - -// SecondaryControls component -export const SecondaryControls = memo(function SecondaryControls({ layer, layerStatus, itemsLength, childLayers }: SecondaryControlsProps) { - logger.logDebug('legend1 - ctrl', layer, layerStatus, itemsLength, childLayers); - // Hooks - const { t } = useTranslation(); - const theme = useTheme(); - const sxClasses = getSxClasses(theme); - - // Stores - const highlightedLayer = useLayerHighlightedLayer(); - const { getVisibilityFromOrderedLayerInfo, setOrToggleLayerVisibility } = useMapStoreActions(); - const { setHighlightLayer, zoomToLayerExtent } = useLayerStoreActions(); - - const [visibility, setVisibility] = useState(getVisibilityFromOrderedLayerInfo(layer.layerPath)); - const isLayerVisible = layer.controls?.visibility ?? false; - - // #region Handlers - const handleToggleVisibility = useCallback( - (e: React.MouseEvent): void => { - e.stopPropagation(); - setOrToggleLayerVisibility(layer.layerPath); - setVisibility(getVisibilityFromOrderedLayerInfo(layer.layerPath)); - }, - [layer.layerPath, setOrToggleLayerVisibility] - ); - - const handleHighlightLayer = useCallback( - (e: React.MouseEvent): void => { - e.stopPropagation(); - setHighlightLayer(layer.layerPath); - }, - [layer.layerPath, setHighlightLayer] - ); - - 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 - - if (!['processed', 'loaded'].includes(layerStatus)) { - return ; - } - - 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}} - - - {visibility ? : } - - - {highlightedLayer === layer.layerPath ? : } - - - - - - - ); -}); +import { useTheme } from '@mui/material'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Box, + IconButton, + Stack, + VisibilityOutlinedIcon, + HighlightOutlinedIcon, + ZoomInSearchIcon, + Typography, + VisibilityOffOutlinedIcon, + HighlightIcon, +} from '@/ui'; +import { useLayerHighlightedLayer, useLayerStoreActions } from '@/core/stores/store-interface-and-intial-values/layer-state'; +import { 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; +} + +// SecondaryControls component +export function SecondaryControls({ layer, layerStatus, itemsLength, childLayers, visibility }: SecondaryControlsProps): JSX.Element { + // Hooks + const { t } = useTranslation(); + 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] + ); + + 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 + + if (!['processed', 'loaded'].includes(layerStatus)) { + 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}} + + + {visibility ? : } + + + {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 8b3ee083497..551eb050ff7 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,36 +1,35 @@ -import { useTheme } from '@mui/material'; -import { memo } from 'react'; -import { Box, ListItem, Tooltip, ListItemText, ListItemIcon, List, BrowserNotSupportedIcon } from '@/ui'; -import { TypeLegendLayer } from '@/core/components/layers/types'; -import { getSxClasses } from './legend-styles'; -import { logger } from '@/core/utils/logger'; - -interface ItemsListProps { - items: TypeLegendLayer['items']; -} - -// ItemsList component -export const ItemsList = memo(function ItemsList({ items }: ItemsListProps) { - logger.logDebug('legend1 item list', items); - - // Hooks - const theme = useTheme(); - const sxClasses = getSxClasses(theme); - - if (!items?.length) { - return null; - } - - return ( - - {items.map((item, index) => ( - - {item.icon ? : } - - - - - ))} - - ); -}); +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 { getSxClasses } from './legend-styles'; +import { logger } from '@/core/utils/logger'; + +interface ItemsListProps { + items: TypeLegendLayer['items']; +} + +// 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) => ( + + {item.icon ? : } + + + + + )); + + return {listItems}; +}); 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 d01e3adbb21..f05a3fd3ce6 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer.tsx @@ -1,93 +1,97 @@ -import { memo, useCallback, useMemo, useState } from 'react'; -import { ListItem, Tooltip, ListItemText, IconButton, KeyboardArrowDownIcon, KeyboardArrowUpIcon } from '@/ui'; +import { useCallback, useMemo } from 'react'; +import { useTheme } from '@mui/material'; +import { Box, ListItem, Tooltip, ListItemText, IconButton, KeyboardArrowDownIcon, KeyboardArrowUpIcon } from '@/ui'; import { TypeLegendLayer } from '@/core/components/layers/types'; -import { useMapStoreActions } from '@/core/stores/'; -import { logger } from '@/core/utils/logger'; +import { useLayerStoreActions, useMapStoreActions } from '@/core/stores/'; import { useLightBox } from '@/core/components/common'; import { LayerIcon } from '../common/layer-icon'; import { SecondaryControls } from './legend-layer-ctrl'; import { CollapsibleContent } from './legend-layer-container'; - -// Define component types and interfaces -type LegendLayerType = React.FC<{ layer: TypeLegendLayer }>; +import { getSxClasses } from './legend-styles'; interface LegendLayerProps { layer: TypeLegendLayer; } // Main LegendLayer component -export const LegendLayer: LegendLayerType = memo(function LegendLayer({ layer }: LegendLayerProps) { +export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { // Hooks - logger.logDebug('legend1 LegendLayerType', layer); - - // const { t } = useTranslation(); - // const theme = useTheme(); - // const sxClasses = getSxClasses(theme); + const theme = useTheme(); + const sxClasses = getSxClasses(theme); // Stores const { initLightBox, LightBoxComponent } = useLightBox(); - const { getLegendCollapsedFromOrderedLayerInfo, setLegendCollapsed } = useMapStoreActions(); + 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( - () => layer.children?.filter((c) => ['processed', 'loaded'].includes(c.layerStatus ?? '')) ?? [], - [layer.children] + () => currentLayer.children?.filter((c) => ['processed', 'loaded'].includes(c.layerStatus ?? '')) ?? [], + [currentLayer.children] ); - const [legendExpanded, setLegendExpanded] = useState(getLegendCollapsedFromOrderedLayerInfo(layer.layerPath)); - const handleExpandGroupClick = useCallback( (e: React.MouseEvent): void => { e.stopPropagation(); - setLegendCollapsed(layer.layerPath); - setLegendExpanded(getLegendCollapsedFromOrderedLayerInfo(layer.layerPath)); + setLegendCollapsed(layer.layerPath); // store value }, [layer.layerPath, setLegendCollapsed] ); return ( <> - - - <> - - - } - /> - - {!!(layer.children?.length > 1 || layer.items?.length > 1) && ( - - {legendExpanded ? : } - - )} - - - - {LightBoxComponent} + + + + <> + + + } + /> + + {!!(layer.children?.length > 1 || layer.items?.length > 1) && ( + + {!isCollapsed ? : } + + )} + + + + + ); -}); +} export default LegendLayer; diff --git a/packages/geoview-core/src/core/components/legend/legend.tsx b/packages/geoview-core/src/core/components/legend/legend.tsx index fa7ac2b2f11..898b3c01074 100644 --- a/packages/geoview-core/src/core/components/legend/legend.tsx +++ b/packages/geoview-core/src/core/components/legend/legend.tsx @@ -80,7 +80,6 @@ export function Legend({ fullWidth, containerType = 'footerBar' }: LegendType): useEffect(() => { // Log logger.logTraceUseEffect('LEGEND - visibleLayers', orderedLayerInfo.length, orderedLayerInfo); - setLegendLayers(layersList); updateLegendLayerListByWindowSize(layersList); diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts index d6447c9571d..3a690232e96 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts @@ -11,6 +11,7 @@ import { TypeGetStore, TypeSetStore } from '@/core/stores/geoview-store'; import { layerEntryIsEsriDynamic, TypeFeatureInfoEntryPartial, + TypeLayerStatus, TypeLayerStyleConfig, TypeResultSet, TypeResultSetEntry, @@ -42,6 +43,7 @@ export interface ILayerState { getLayer: (layerPath: string) => TypeLegendLayer | undefined; getLayerBounds: (layerPath: string) => number[] | undefined; getLayerDeleteInProgress: () => boolean; + getLayerStatus: (layerPath: string) => TypeLayerStatus; refreshLayer: (layerPath: string) => void; setAllItemsVisibility: (layerPath: string, visibility: boolean) => void; setDisplayState: (newDisplayState: TypeLayersViewDisplayState) => void; @@ -154,6 +156,16 @@ export function initializeLayerState(set: TypeSetStore, get: TypeGetStore): ILay */ getLayerDeleteInProgress: () => get().layerState.layerDeleteInProgress, + /** + * Gets the layer status in the store which correspond to the layer path + * @param {string} layerPath - The layer path of the bounds to get + * @returns {TypeLayerStatus | undefined} The status or undefined + */ + getLayerStatus: (layerPath: string): TypeLayerStatus | undefined => { + const curLayers = get().layerState.legendLayers; + return LegendEventProcessor.findLayerByPath(curLayers, layerPath)!.layerStatus; + }, + /** * Refresh layer and set states to original values. * @param {string} layerPath - The layer path of the layer to change. diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts index ff9095f68bf..f2ce171ed59 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts @@ -811,14 +811,29 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore): IMapSt * @param {boolean} collapsed - Flag indicating if the layer should be hoverable. */ setLegendCollapsed: (layerPath: string, collapsed?: boolean): void => { - const curLayerInfo = get().mapState.orderedLayerInfo; - const layerInfo = curLayerInfo.find((info) => info.layerPath === layerPath); - if (layerInfo) { - const newCollapsed = collapsed || !layerInfo.legendCollapsed; - layerInfo.legendCollapsed = newCollapsed; + // const curLayerInfo = get().mapState.orderedLayerInfo; + // const layerInfo = curLayerInfo.find((info) => info.layerPath === layerPath); + // if (layerInfo) { + // const newCollapsed = collapsed || !layerInfo.legendCollapsed; + // layerInfo.legendCollapsed = newCollapsed; - // Redirect - get().mapState.setterActions.setOrderedLayerInfo(curLayerInfo); + // // Redirect + // get().mapState.setterActions.setOrderedLayerInfo(curLayerInfo); + // } + + const curLayerInfo = get().mapState.orderedLayerInfo; + const layerIndex = curLayerInfo.findIndex((info) => info.layerPath === layerPath); + + if (layerIndex !== -1) { + // Create shallow copy of array + const newLayerInfo = curLayerInfo.slice(); + // Only create new object for the changed layer + newLayerInfo[layerIndex] = { + ...curLayerInfo[layerIndex], + legendCollapsed: collapsed ?? !curLayerInfo[layerIndex].legendCollapsed, + }; + + get().mapState.setterActions.setOrderedLayerInfo(newLayerInfo); } },