diff --git a/packages/geoview-core/src/api/plugin/plugin-types.ts b/packages/geoview-core/src/api/plugin/plugin-types.ts index ff3bfd0e9e4..09930402155 100644 --- a/packages/geoview-core/src/api/plugin/plugin-types.ts +++ b/packages/geoview-core/src/api/plugin/plugin-types.ts @@ -20,6 +20,7 @@ export type TypePluginStructure = { defaultConfig?: () => TypeJsonObject; added?: () => void; removed?: () => void; + onSelected?: () => void; }; /** ****************************************************************************************************************************** diff --git a/packages/geoview-core/src/core/components/common/index.ts b/packages/geoview-core/src/core/components/common/index.ts index 92b567ee059..a2285c1f8f9 100644 --- a/packages/geoview-core/src/core/components/common/index.ts +++ b/packages/geoview-core/src/core/components/common/index.ts @@ -4,3 +4,4 @@ export * from './layer-list'; export * from './layer-title'; export * from './layout'; export * from './use-footer-panel-height'; +export * from './use-lightbox'; diff --git a/packages/geoview-core/src/core/components/data-table/hooks/useLightbox.tsx b/packages/geoview-core/src/core/components/common/use-lightbox.tsx similarity index 68% rename from packages/geoview-core/src/core/components/data-table/hooks/useLightbox.tsx rename to packages/geoview-core/src/core/components/common/use-lightbox.tsx index d31dd3f3420..7e318f09cae 100644 --- a/packages/geoview-core/src/core/components/data-table/hooks/useLightbox.tsx +++ b/packages/geoview-core/src/core/components/common/use-lightbox.tsx @@ -1,13 +1,17 @@ import { useState } from 'react'; import { Box } from '@/ui'; import { LightBoxSlides, LightboxImg } from '@/core/components/lightbox/lightbox'; + +interface UseLightBoxReturnType { + initLightBox: (images: string, alias: string, index: number | undefined) => void; + LightBoxComponent: () => JSX.Element; +} + /** * Custom Lightbox hook which handle rendering of the lightbox. - * @returns {Object} + * @returns {UseLightBoxReturnType} */ -// TODO: Refactor - Maybe worth creating an explicit type here instead of 'any'? -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function useLightBox(): any { +export function useLightBox(): UseLightBoxReturnType { const [isLightBoxOpen, setIsLightBoxOpen] = useState(false); const [slides, setSlides] = useState([]); const [slidesIndex, setSlidesIndex] = useState(0); @@ -15,12 +19,14 @@ export function useLightBox(): any { /** * Initialize lightbox with state. * @param {string} images images url formatted as string and joined with ';' identifier. - * @param {string} cellId id of the cell. + * @param {string} alias alt tag for the image. + * @param {number | undefined} index index of the image which is displayed. */ - const initLightBox = (images: string, cellId: string): void => { + const initLightBox = (images: string, alias: string, index: number | undefined): void => { setIsLightBoxOpen(true); - const slidesList = images.split(';').map((item) => ({ src: item, alt: cellId, downloadUrl: item })); + const slidesList = images.split(';').map((item) => ({ src: item, alt: alias, downloadUrl: item })); setSlides(slidesList); + setSlidesIndex(index ?? 0); }; /** diff --git a/packages/geoview-core/src/core/components/data-table/data-panel.tsx b/packages/geoview-core/src/core/components/data-table/data-panel.tsx index 639c93b72c7..bda5dae6d7f 100644 --- a/packages/geoview-core/src/core/components/data-table/data-panel.tsx +++ b/packages/geoview-core/src/core/components/data-table/data-panel.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useTheme } from '@mui/material/styles'; +import { delay } from 'lodash'; import { Box, FilterAltIcon, Skeleton } from '@/ui'; import DataTable from './data-table'; import { @@ -15,12 +16,8 @@ import { useUIActiveFooterBarTabId } from '@/core/stores/store-interface-and-int import { LayerListEntry, Layout } from '@/core/components/common'; import { logger } from '@/core/utils/logger'; import { useFeatureFieldInfos } from './hooks'; -import { TypeFieldEntry, TypeLayerData } from '@/geo/layer/layer-sets/abstract-layer-set'; import { LAYER_STATUS, TABS } from '@/core/utils/constant'; - -export interface MappedLayerDataType extends TypeLayerData { - fieldInfos: Record; -} +import { MappedLayerDataType } from './data-table-types'; interface DataPanelType { fullWidth?: boolean; @@ -64,6 +61,9 @@ export function Datapanel({ fullWidth = false }: DataPanelType): JSX.Element { */ const handleLayerChange = useCallback( (_layer: LayerListEntry) => { + // Log + logger.logTraceUseCallback('DATA-PANEL - handleLayerChange'); + setSelectedLayerPath(_layer.layerPath); setIsLoading(true); @@ -83,25 +83,38 @@ export function Datapanel({ fullWidth = false }: DataPanelType): JSX.Element { * @param {string} layerPath The path of the layer * @returns boolean */ - const isMapFilteredSelectedForLayer = (layerPath: string): boolean => - !!datatableSettings[layerPath].mapFilteredRecord && !!datatableSettings[layerPath].rowsFilteredRecord; + const isMapFilteredSelectedForLayer = useCallback( + (layerPath: string): boolean => { + // Log + logger.logTraceUseCallback('DATA-PANEL - isMapFilteredSelectedForLayer'); + + return !datatableSettings[layerPath].mapFilteredRecord && !!datatableSettings[layerPath].rowsFilteredRecord; + }, + [datatableSettings] + ); /** * Get number of features of a layer with filtered or selected layer or unknown when data table is loaded. * @param {string} layerPath the path of the layer * @returns */ - const getFeaturesOfLayer = (layerPath: string): string => { - if (datatableSettings[layerPath] && datatableSettings[layerPath].rowsFilteredRecord) { - return `${datatableSettings[layerPath].rowsFilteredRecord} ${t('dataTable.featureFiltered')}`; - } - let featureStr = t('dataTable.noFeatures'); - const features = orderedLayerData?.find((layer) => layer.layerPath === layerPath)?.features?.length ?? 0; - if (features > 0) { - featureStr = `${features} ${t('dataTable.features')}`; - } - return featureStr; - }; + const getFeaturesOfLayer = useCallback( + (layerPath: string): string => { + // Log + logger.logTraceUseCallback('DATA-PANEL - getFeaturesOfLayer'); + + if (datatableSettings[layerPath] && datatableSettings[layerPath].rowsFilteredRecord) { + return `${datatableSettings[layerPath].rowsFilteredRecord} ${t('dataTable.featureFiltered')}`; + } + let featureStr = t('dataTable.noFeatures'); + const features = orderedLayerData?.find((layer) => layer.layerPath === layerPath)?.features?.length ?? 0; + if (features > 0) { + featureStr = `${features} ${t('dataTable.features')}`; + } + return featureStr; + }, + [datatableSettings, orderedLayerData, t] + ); /** * Create layer tooltip @@ -109,14 +122,20 @@ export function Datapanel({ fullWidth = false }: DataPanelType): JSX.Element { * @param {string} layerPath the path of the layer. * @returns */ - const getLayerTooltip = (layerName: string, layerPath: string): JSX.Element => { - return ( - - {`${layerName}, ${getFeaturesOfLayer(layerPath)}`} - {isMapFilteredSelectedForLayer(layerPath) && } - - ); - }; + const getLayerTooltip = useCallback( + (layerName: string, layerPath: string): JSX.Element => { + // Log + logger.logTraceUseCallback('DATA-PANEL - getLayerTooltip'); + + return ( + + {`${layerName}, ${getFeaturesOfLayer(layerPath)}`} + {isMapFilteredSelectedForLayer(layerPath) && } + + ); + }, + [getFeaturesOfLayer, isMapFilteredSelectedForLayer] + ); /** * Checks if layer is disabled when layer is selected and features have null value. @@ -143,8 +162,7 @@ export function Datapanel({ fullWidth = false }: DataPanelType): JSX.Element { // Log logger.logTraceUseEffect('DATA-PANEL - isLoading', isLoading, selectedLayerPath); - // TODO: Get rid of this setTimeout of 1 second? - const clearLoading = setTimeout(() => { + const clearLoading = delay(() => { setIsLoading(false); }, 100); return () => clearTimeout(clearLoading); @@ -172,6 +190,11 @@ export function Datapanel({ fullWidth = false }: DataPanelType): JSX.Element { return () => !!orderedLayerData.find((layer) => layer.queryStatus === LAYER_STATUS.PROCESSING); }, [orderedLayerData]); + /** + * Render the right panel content based on table data and layer loading status. + * NOTE: Here we return null, so that in responsive grid layout, it can be used as flag to render the guide for data table. + * @returns {JSX.Element | null} JSX.Element | null + */ const renderContent = (): JSX.Element | null => { if (isLoading || memoIsLayerQueryStatusProcessing()) { return ; @@ -191,6 +214,9 @@ export function Datapanel({ fullWidth = false }: DataPanelType): JSX.Element { return null; }; + /** + * Callback function to update the store state for clearing the selecting layer from left panel. + */ const handleGuideIsOpen = useCallback( (guideIsOpen: boolean): void => { if (guideIsOpen) { diff --git a/packages/geoview-core/src/core/components/data-table/data-table-modal.tsx b/packages/geoview-core/src/core/components/data-table/data-table-modal.tsx index 9bba8733b6f..a9acb9388d5 100644 --- a/packages/geoview-core/src/core/components/data-table/data-table-modal.tsx +++ b/packages/geoview-core/src/core/components/data-table/data-table-modal.tsx @@ -14,15 +14,14 @@ import { } from '@/ui'; import { useUIActiveFocusItem, useUIStoreActions } from '@/core/stores/store-interface-and-intial-values/ui-state'; import { useLayerSelectedLayerPath } from '@/core/stores/store-interface-and-intial-values/layer-state'; - -import { FieldInfos } from './data-table'; import { getSxClasses } from './data-table-style'; import { logger } from '@/core/utils/logger'; import { useDataTableAllFeaturesDataArray } from '@/core/stores/store-interface-and-intial-values/data-table-state'; import { useFeatureFieldInfos } from './hooks'; +import { TypeFieldEntry } from '@/geo/layer/layer-sets/abstract-layer-set'; interface ColumnsType { - [key: string]: FieldInfos; + [key: string]: TypeFieldEntry; } /** * Open lighweight version (no function) of data table in a modal window @@ -63,9 +62,19 @@ export default function DataTableModal(): JSX.Element { * @param {string} cellValue cell value to be displayed in cell * @returns {JSX.Element} */ - const getCellValue = (cellValue: string): JSX.Element => { - return {cellValue}; - }; + const getCellValue = useCallback( + (cellValue: string): JSX.Element => { + // Log + logger.logTraceUseCallback('DATA-TABLE-MODAL - getCellValue'); + + return ( + + {cellValue} + + ); + }, + [sxClasses.tableCell] + ); /** * Create table header cell diff --git a/packages/geoview-core/src/core/components/data-table/data-table-style.ts b/packages/geoview-core/src/core/components/data-table/data-table-style.ts index 210a8324fa7..f0257831941 100644 --- a/packages/geoview-core/src/core/components/data-table/data-table-style.ts +++ b/packages/geoview-core/src/core/components/data-table/data-table-style.ts @@ -1,78 +1,77 @@ import { Theme } from '@mui/material'; -// ? I doubt we want to define an explicit type for style properties? -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const getSxClasses = (theme: Theme): any => ({ - dataPanel: { background: theme.palette.geoViewColor.bgColor.main, paddingBottom: '1rem' }, - gridContainer: { paddingLeft: '1rem', paddingRight: '1rem' }, - selectedRows: { - transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - fontWeight: 400, - fontSize: theme.palette.geoViewFontSize.sm, - linHeight: 1.43, - letterSpacing: '0.01071em', - display: 'flex', - padding: '6px', - }, - selectedRowsDirection: { - display: 'flex', - flexDirection: 'column', - }, - tableCell: { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }, - dataTableWrapper: { - '& .MuiTableContainer-root': { - borderRadius: '6px', +export const getSxClasses = (theme: Theme) => + ({ + dataPanel: { background: theme.palette.geoViewColor.bgColor.main, paddingBottom: '1rem' }, + gridContainer: { paddingLeft: '1rem', paddingRight: '1rem' }, + selectedRows: { + transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', + fontWeight: 400, + fontSize: theme.palette.geoViewFontSize.sm, + linHeight: 1.43, + letterSpacing: '0.01071em', + display: 'flex', + padding: '6px', }, - '& .MuiToolbar-root ': { - borderRadius: '6px', + selectedRowsDirection: { + display: 'flex', + flexDirection: 'column', }, - }, - filterMap: { - '& .Mui-checked': { - '& .MuiTouchRipple-root': { - color: theme.palette.action.active, + tableCell: { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }, + dataTableWrapper: { + '& .MuiTableContainer-root': { + borderRadius: '6px', }, - }, - '& .MuiTouchRipple-root': { - color: theme.palette.geoViewColor.grey.dark[900], - }, - }, - tableHeadCell: { - '& .MuiCollapse-wrapperInner': { - '& .MuiBox-root': { - gridTemplateColumns: '1fr', + '& .MuiToolbar-root ': { + borderRadius: '6px', }, }, - '& .MuiInput-root': { fontSize: theme.palette.geoViewFontSize.sm, '& .MuiSvgIcon-root': { width: '0.75em', height: '0.75em' } }, - '& .MuiBadge-root': { - marginLeft: '0.5rem', - '>span': { - width: '100%', + filterMap: { + '& .Mui-checked': { + '& .MuiTouchRipple-root': { + color: theme.palette.action.active, + }, }, - svg: { - marginTop: '0.25rem', - marginBottom: '0.25rem', + '& .MuiTouchRipple-root': { + color: theme.palette.geoViewColor.grey.dark[900], }, - '& .keyboard-focused': { - backgroundColor: 'rgba(81, 91, 165, 0.08)', - borderRadius: '50%', - border: `1px solid black !important`, - '> svg': { - opacity: 1, + }, + tableHeadCell: { + '& .MuiCollapse-wrapperInner': { + '& .MuiBox-root': { + gridTemplateColumns: '1fr', }, }, + '& .MuiInput-root': { fontSize: theme.palette.geoViewFontSize.sm, '& .MuiSvgIcon-root': { width: '0.75em', height: '0.75em' } }, + '& .MuiBadge-root': { + marginLeft: '0.5rem', + '>span': { + width: '100%', + }, + svg: { + marginTop: '0.25rem', + marginBottom: '0.25rem', + }, + '& .keyboard-focused': { + backgroundColor: 'rgba(81, 91, 165, 0.08)', + borderRadius: '50%', + border: `1px solid black !important`, + '> svg': { + opacity: 1, + }, + }, + }, + }, + dataTableInstructionsTitle: { + fontSize: theme.palette.geoViewFontSize.lg, + fontWeight: '600', + lineHeight: '1.5em', + }, + dataTableInstructionsBody: { + fontSize: theme.palette.geoViewFontSize.sm, + }, + rightPanelContainer: { + overflowY: 'auto', + color: theme.palette.geoViewColor.textColor.main, }, - }, - dataTableInstructionsTitle: { - fontSize: theme.palette.geoViewFontSize.lg, - fontWeight: '600', - lineHeight: '1.5em', - }, - dataTableInstructionsBody: { - fontSize: theme.palette.geoViewFontSize.sm, - }, - rightPanelContainer: { - overflowY: 'auto', - color: theme.palette.geoViewColor.textColor.main, - }, -}); + } as const); diff --git a/packages/geoview-core/src/core/components/data-table/data-table-types.ts b/packages/geoview-core/src/core/components/data-table/data-table-types.ts new file mode 100644 index 00000000000..458c794e75f --- /dev/null +++ b/packages/geoview-core/src/core/components/data-table/data-table-types.ts @@ -0,0 +1,17 @@ +import { TypeFieldEntry, TypeLayerData } from '@/geo/layer/layer-sets/abstract-layer-set'; + +export interface MappedLayerDataType extends TypeLayerData { + fieldInfos: Partial>; +} + +export interface ColumnsType { + ICON: TypeFieldEntry; + ZOOM: TypeFieldEntry; + [key: string]: TypeFieldEntry; +} + +export interface DataTableProps { + data: MappedLayerDataType; + layerPath: string; + tableHeight: number; +} diff --git a/packages/geoview-core/src/core/components/data-table/data-table.tsx b/packages/geoview-core/src/core/components/data-table/data-table.tsx index 5d9668fbd97..cd792cb84cc 100644 --- a/packages/geoview-core/src/core/components/data-table/data-table.tsx +++ b/packages/geoview-core/src/core/components/data-table/data-table.tsx @@ -46,69 +46,14 @@ import { DateMgt } from '@/core/utils/date-mgt'; import { isImage, delay } from '@/core/utils/utilities'; import { logger } from '@/core/utils/logger'; import { TypeFeatureInfoEntry } from '@/geo/layer/layer-sets/abstract-layer-set'; - -import { MappedLayerDataType } from './data-panel'; -import { useLightBox, useFilterRows, useToolbarActionMessage, useGlobalFilter } from './hooks'; +import { useFilterRows, useToolbarActionMessage, useGlobalFilter } from './hooks'; import { getSxClasses } from './data-table-style'; import ExportButton from './export-button'; import JSONExportButton from './json-export-button'; import FilterMap from './filter-map'; - -export interface FieldInfos { - alias: string; - dataType: string; - domain?: string; - fieldKey: number; - value: string | null; -} - -export interface ColumnsType { - ICON: FieldInfos; - ZOOM: FieldInfos; - [key: string]: FieldInfos; -} - -interface DataTableProps { - data: MappedLayerDataType; - layerPath: string; - tableHeight: number; -} - -const DATE_FILTER: Record = { - greaterThan: `> date 'value'`, - greaterThanOrEqualTo: `>= date 'value'`, - lessThan: `< date 'value'`, - lessThanOrEqualTo: `<= date 'value'`, - equals: `= date 'value'`, - empty: 'is null', - notEmpty: 'is not null', - notEquals: `<> date 'value'`, - between: `> date 'value'`, - betweenInclusive: `>= date 'value'`, -}; - -const STRING_FILTER: Record = { - contains: `(filterId) like ('%value%')`, - startsWith: `(filterId) like ('value%')`, - endsWith: `(filterId) like ('%value')`, - empty: '(filterId) is null', - notEmpty: '(filterId) is not null', - equals: `filterId = 'value'`, - notEquals: `filterId <> 'value'`, -}; - -const NUMBER_FILTER: Record = { - lessThanOrEqualTo: '<=', - lessThan: '<', - greaterThan: '>', - greaterThanOrEqualTo: '>=', - empty: 'is null', - notEmpty: 'is not null', - between: '>', - betweenInclusive: '>=', - equals: '=', - notEquals: '<>', -}; +import { useLightBox } from '../common'; +import { NUMBER_FILTER, DATE_FILTER, STRING_FILTER } from '@/core/utils/constant'; +import { DataTableProps, ColumnsType } from './data-table-types'; /** * Build Data table from map. @@ -176,22 +121,28 @@ function DataTable({ data, layerPath, tableHeight = 600 }: DataTableProps): JSX. * @param {string} cellId id of the column. * @returns {string | number | JSX.Element} */ - const createLightBoxButton = (cellValue: string | number, cellId: string): string | number | JSX.Element => { - if (typeof cellValue === 'string' && isImage(cellValue)) { - return ( - - ); - } - // convert string to react component. - return typeof cellValue === 'string' ? : cellValue; - }; + const createLightBoxButton = useCallback( + (cellValue: string | number, cellId: string): string | number | JSX.Element => { + // Log + logger.logTraceUseCallback('DATA-TABLE - createLightBoxButton'); + + if (typeof cellValue === 'string' && isImage(cellValue)) { + return ( + + ); + } + // convert string to react component. + return typeof cellValue === 'string' ? : cellValue; + }, + [initLightBox, t] + ); /** * Create data table body cell with tooltip @@ -199,19 +150,25 @@ function DataTable({ data, layerPath, tableHeight = 600 }: DataTableProps): JSX. * @param {string | number | JSX.Element} cellValue - Cell value to be displayed in cell * @returns {JSX.Element} */ - const getCellValueWithTooltip = (cellValue: string | number | JSX.Element, cellId: string): JSX.Element => { - return typeof cellValue === 'string' || typeof cellValue === 'number' ? ( - + const getCellValueWithTooltip = useCallback( + (cellValue: string | number | JSX.Element, cellId: string): JSX.Element => { + // Log + logger.logTraceUseCallback('DATA-TABLE - getCellValueWithTooltip'); + + return typeof cellValue === 'string' || typeof cellValue === 'number' ? ( + + + {createLightBoxButton(cellValue, cellId)} + + + ) : ( - {createLightBoxButton(cellValue, cellId)} + {cellValue} - - ) : ( - - {cellValue} - - ); - }; + ); + }, + [createLightBoxButton, density, sxClasses.tableCell] + ); /** * Create Date filter with Datepicker. @@ -219,45 +176,54 @@ function DataTable({ data, layerPath, tableHeight = 600 }: DataTableProps): JSX. * @param {MRTColumn} column - Filter column. * @returns {JSX.Element} */ - const getDateFilter = (column: MRTColumn): JSX.Element => { - // eslint-disable-next-line no-underscore-dangle - const filterFn = startCase(column.columnDef._filterFn).replaceAll(' ', ''); - const key = `filter${filterFn}` as keyof MRTLocalization; - const filterFnKey = dataTableLocalization[key]; - const helperText = dataTableLocalization.filterMode.replace('{filterType}', filterFnKey); - return ( - - { - column.setFilterValue(newValue); - }} - slotProps={{ - textField: { - placeholder: language === 'fr' ? 'AAAA/MM/JJ' : 'YYYY/MM/DD', - helperText, - sx: { minWidth: '120px', width: '100%' }, - variant: 'standard', - }, - }} - /> - - ); - }; + const getDateFilter = useCallback( + (column: MRTColumn): JSX.Element => { + // Log + logger.logTraceUseCallback('DATA-TABLE - getDateFilter'); + + // eslint-disable-next-line no-underscore-dangle + const filterFn = startCase(column.columnDef._filterFn).replaceAll(' ', ''); + const key = `filter${filterFn}` as keyof MRTLocalization; + const helperText = dataTableLocalization.filterMode.replace('{filterType}', dataTableLocalization[key]); + return ( + + { + column.setFilterValue(newValue); + }} + slotProps={{ + textField: { + placeholder: language === 'fr' ? 'AAAA/MM/JJ' : 'YYYY/MM/DD', + helperText, + sx: { minWidth: '120px', width: '100%' }, + variant: 'standard', + }, + }} + /> + + ); + }, + [dataTableLocalization, language] + ); /** * Custom date type Column tooltip * @param {Date} date value to be shown in column. * @returns JSX.Element */ - const getDateColumnTooltip = (date: Date): JSX.Element => { + const getDateColumnTooltip = useCallback((date: Date): JSX.Element => { + // Log + logger.logTraceUseCallback('DATA-TABLE - getDateColumnTooltip'); + + const formattedDate = DateMgt.formatDate(date, 'YYYY-MM-DDThh:mm:ss'); return ( - - {DateMgt.formatDate(date, 'YYYY-MM-DDThh:mm:ss')} + + {formattedDate} ); - }; + }, []); /** * Build material react data table column header. @@ -523,6 +489,9 @@ function DataTable({ data, layerPath, tableHeight = 600 }: DataTableProps): JSX. * @param {MRTColumnFiltersState} columnFilter list of filter from table. */ const buildFilterList = useCallback((columnFilter: MRTColumnFiltersState) => { + // Log + logger.logTraceUseEffect('DATA-TABLE - buildFilterList'); + const tableState = useTable!.getState(); if (!columnFilter.length) return ['']; diff --git a/packages/geoview-core/src/core/components/data-table/export-button.tsx b/packages/geoview-core/src/core/components/data-table/export-button.tsx index de83fa8c119..135e1bd73e6 100644 --- a/packages/geoview-core/src/core/components/data-table/export-button.tsx +++ b/packages/geoview-core/src/core/components/data-table/export-button.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useState } from 'react'; +import { ReactElement, useState, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +8,7 @@ import { type MRT_ColumnDef as MRTColumnDef } from 'material-react-table'; import { IconButton, DownloadIcon, Tooltip, Menu, MenuItem } from '@/ui'; import { logger } from '@/core/utils/logger'; -import { ColumnsType } from './data-table'; +import { ColumnsType } from './data-table-types'; interface ExportButtonProps { rows: ColumnsType[]; @@ -36,23 +36,32 @@ function ExportButton({ rows, columns, children }: ExportButtonProps): JSX.Eleme * Show export menu. */ - const handleClick = (event: React.MouseEvent): void => { + const handleClick = useCallback((event: React.MouseEvent) => { + // Log + logger.logTraceUseCallback('DATA-TABLE - EXPORT BUTTON - handleClick'); + setAnchorEl(event.currentTarget); - }; + }, []); /** * Close export menu. */ - const handleClose = (): void => { + const handleClose = useCallback(() => { + // Log + logger.logTraceUseCallback('DATA-TABLE - EXPORT BUTTON - handleClose'); + setAnchorEl(null); - }; + }, []); /** * Build CSV Options for download. */ - const getCsvOptions = (): Options => { - return { + const getCsvOptions = useMemo(() => { + // Log + logger.logTraceUseMemo('DATA-TABLE - EXPORT BUTTON - getCsvOptions', columns); + + return (): Options => ({ fieldSeparator: ',', quoteStrings: '"', decimalSeparator: '.', @@ -60,24 +69,28 @@ function ExportButton({ rows, columns, children }: ExportButtonProps): JSX.Eleme useBom: true, useKeysAsHeaders: false, headers: columns.map((c) => c.id as string), - }; - }; + }); + }, [columns]); /** * Export data table in csv format. */ - const handleExportData = (): void => { + + const handleExportData = useCallback((): void => { + // Log + logger.logTraceUseCallback('DATA-TABLE - EXPORT BUTTON - handleExportData'); + // format the rows for csv. const csvRows = rows.map((row) => { const mappedRow = Object.keys(row).reduce((acc, curr) => { acc[curr] = row[curr]?.value ?? ''; return acc; - }, {} as Record); + }, {} as Record); return mappedRow; }); const csvExporter = new ExportToCsv(getCsvOptions()); csvExporter.generateCsv(csvRows); - }; + }, [getCsvOptions, rows]); return ( <> diff --git a/packages/geoview-core/src/core/components/data-table/hooks/index.ts b/packages/geoview-core/src/core/components/data-table/hooks/index.ts index 445fc81de84..59bcd877754 100644 --- a/packages/geoview-core/src/core/components/data-table/hooks/index.ts +++ b/packages/geoview-core/src/core/components/data-table/hooks/index.ts @@ -1,4 +1,3 @@ -export * from './useLightbox'; export * from './useFilterRows'; export * from './useToolbarActionMessage'; export * from './useFeatureFieldInfos'; diff --git a/packages/geoview-core/src/core/components/data-table/hooks/useFeatureFieldInfos.tsx b/packages/geoview-core/src/core/components/data-table/hooks/useFeatureFieldInfos.tsx index dc7038de99a..f4bad666bc2 100644 --- a/packages/geoview-core/src/core/components/data-table/hooks/useFeatureFieldInfos.tsx +++ b/packages/geoview-core/src/core/components/data-table/hooks/useFeatureFieldInfos.tsx @@ -1,5 +1,7 @@ -import { TypeLayerData, TypeFieldEntry } from '@/geo/layer/layer-sets/abstract-layer-set'; -import { MappedLayerDataType } from '@/core/components/data-table/data-panel'; +import { useMemo } from 'react'; +import { TypeLayerData } from '@/geo/layer/layer-sets/abstract-layer-set'; +import { MappedLayerDataType } from '@/core/components/data-table/data-table-types'; +import { logger } from '@/core/utils/logger'; /** * Custom hook for caching the mapping of fieldInfos aka columns for data table. @@ -7,13 +9,14 @@ import { MappedLayerDataType } from '@/core/components/data-table/data-panel'; * @returns {MappedLayerDataType[]} layerData with columns. */ export function useFeatureFieldInfos(layerData: TypeLayerData[]): MappedLayerDataType[] { - const mappedLayerData = layerData?.map((layer) => { - let fieldInfos = {} as Record; - if (layer.features?.length) { - fieldInfos = layer.features[0].fieldInfo; - } - return { ...layer, fieldInfos }; - }); + const mappedLayerData = useMemo(() => { + // Log + logger.logTraceUseEffect('DATA TABLE - useFeatureFieldInfos', layerData); + + return layerData?.map((layer) => { + return { ...layer, fieldInfos: layer.features?.length ? layer.features[0].fieldInfo : {} }; + }); + }, [layerData]); return mappedLayerData; } diff --git a/packages/geoview-core/src/core/components/data-table/hooks/useToolbarActionMessage.tsx b/packages/geoview-core/src/core/components/data-table/hooks/useToolbarActionMessage.tsx index a40a3418d14..f5af940a9f2 100644 --- a/packages/geoview-core/src/core/components/data-table/hooks/useToolbarActionMessage.tsx +++ b/packages/geoview-core/src/core/components/data-table/hooks/useToolbarActionMessage.tsx @@ -3,8 +3,7 @@ import { type MRT_TableInstance as MRTTableInstance, type MRT_ColumnFiltersState import { useTranslation } from 'react-i18next'; import { useDataTableStoreActions, useDataTableLayerSettings } from '@/core/stores/store-interface-and-intial-values/data-table-state'; import { logger } from '@/core/utils/logger'; -import { MappedLayerDataType } from '@/core/components/data-table/data-panel'; -import { ColumnsType } from '@/core/components/data-table/data-table'; +import { MappedLayerDataType, ColumnsType } from '@/core/components/data-table/data-table-types'; interface UseSelectedRowMessageProps { data: MappedLayerDataType; diff --git a/packages/geoview-core/src/core/components/data-table/json-export-button.tsx b/packages/geoview-core/src/core/components/data-table/json-export-button.tsx index 2a532f6116e..3d3f9040470 100644 --- a/packages/geoview-core/src/core/components/data-table/json-export-button.tsx +++ b/packages/geoview-core/src/core/components/data-table/json-export-button.tsx @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Geometry, Point, Polygon, LineString, MultiPoint } from 'ol/geom'; @@ -27,38 +28,38 @@ function JSONExportButton({ features, layerPath }: JSONExportButtonProps): JSX.E /** * Creates a geometry json - * * @param {Geometry} geometry - The geometry * @returns {TypeJsonObject} The geometry json - * */ - const buildGeometry = (geometry: Geometry): TypeJsonObject => { - let builtGeometry = {}; - - if (geometry instanceof Polygon) { - builtGeometry = { - type: 'Polygon', - coordinates: geometry.getCoordinates().map((coords) => { - return coords.map((coord) => transformPoints([coord], 4326)[0]); - }), - }; - } else if (geometry instanceof LineString) { - builtGeometry = { type: 'LineString', coordinates: geometry.getCoordinates().map((coord) => transformPoints([coord], 4326)[0]) }; - } else if (geometry instanceof Point) { - builtGeometry = { type: 'Point', coordinates: transformPoints([geometry.getCoordinates()], 4326)[0] }; - } else if (geometry instanceof MultiPoint) { - builtGeometry = { type: 'MultiPoint', coordinates: geometry.getCoordinates().map((coord) => transformPoints([coord], 4326)[0]) }; - } - - return builtGeometry; - }; + const buildGeometry = useCallback( + (geometry: Geometry): TypeJsonObject => { + let builtGeometry = {}; + + if (geometry instanceof Polygon) { + builtGeometry = { + type: 'Polygon', + coordinates: geometry.getCoordinates().map((coords) => { + return coords.map((coord) => transformPoints([coord], 4326)[0]); + }), + }; + } else if (geometry instanceof LineString) { + builtGeometry = { type: 'LineString', coordinates: geometry.getCoordinates().map((coord) => transformPoints([coord], 4326)[0]) }; + } else if (geometry instanceof Point) { + builtGeometry = { type: 'Point', coordinates: transformPoints([geometry.getCoordinates()], 4326)[0] }; + } else if (geometry instanceof MultiPoint) { + builtGeometry = { type: 'MultiPoint', coordinates: geometry.getCoordinates().map((coord) => transformPoints([coord], 4326)[0]) }; + } + + return builtGeometry; + }, + [transformPoints] + ); /** * Builds the JSON file * @returns {string} Json file content as string - * */ - const getJson = (): string => { + const getJson = useCallback((): string => { const geoData = features.map((feature) => { const { geometry, fieldInfo } = feature; return { @@ -70,14 +71,14 @@ function JSONExportButton({ features, layerPath }: JSONExportButtonProps): JSX.E // Stringify with some indentation return JSON.stringify({ type: 'FeatureCollection', features: geoData }, null, 2); - }; + }, [buildGeometry, features]); /** * Exports the blob to a file * @param {Blob} blob - The blob to save to file * @param {string} filename - File name */ - const exportBlob = (blob: Blob, filename: string): void => { + const exportBlob = useCallback((blob: Blob, filename: string): void => { // Save the blob in a json file const url = URL.createObjectURL(blob); @@ -87,19 +88,19 @@ function JSONExportButton({ features, layerPath }: JSONExportButtonProps): JSX.E a.click(); URL.revokeObjectURL(url); - }; + }, []); /** * Exports data table in csv format. */ - const handleExportData = (): void => { + const handleExportData = useCallback((): void => { const jsonString = getJson(); const blob = new Blob([jsonString], { type: 'text/json', }); exportBlob(blob, `table-${layerPath}.json`); - }; + }, [exportBlob, getJson, layerPath]); return {t('dataTable.jsonExportBtn')}; } diff --git a/packages/geoview-core/src/core/components/details/details-panel.tsx b/packages/geoview-core/src/core/components/details/details-panel.tsx index ad87a44a921..6a47527a159 100644 --- a/packages/geoview-core/src/core/components/details/details-panel.tsx +++ b/packages/geoview-core/src/core/components/details/details-panel.tsx @@ -316,23 +316,34 @@ export function DetailsPanel({ fullWidth = false }: DetailsPanelType): JSX.Eleme * * @param {-1 | 1} change The change to index number (-1 for back, 1 for forward) */ - const handleFeatureNavigateChange = (change: -1 | 1): void => { - // Keep previous index for navigation - prevFeatureIndex.current = currentFeatureIndex; + const handleFeatureNavigateChange = useCallback( + (change: -1 | 1): void => { + // Log + logger.logTraceUseCallback('DETAILS PANEL - handleFeatureNavigateChange', currentFeatureIndex); - // Update current index - updateFeatureSelected(currentFeatureIndex + change, memoSelectedLayerData!); - }; + // Keep previous index for navigation + prevFeatureIndex.current = currentFeatureIndex; + + // Update current index + updateFeatureSelected(currentFeatureIndex + change, memoSelectedLayerData!); + }, + [currentFeatureIndex, memoSelectedLayerData, updateFeatureSelected] + ); /** * Handles click to change the selected layer in left panel. * * @param {LayerListEntry} layerEntry The data of the newly selected layer */ - const handleLayerChange = (layerEntry: LayerListEntry): void => { - // Set the selected layer path in the store which will in turn trigger the store listeners on this component - setSelectedLayerPath(layerEntry.layerPath); - }; + const handleLayerChange = useCallback( + (layerEntry: LayerListEntry): void => { + // Log + logger.logTraceUseCallback('DETAILS-PANEL - handleLayerChange', layerEntry.layerPath); + // Set the selected layer path in the store which will in turn trigger the store listeners on this component + setSelectedLayerPath(layerEntry.layerPath); + }, + [setSelectedLayerPath] + ); // #endregion @@ -370,8 +381,13 @@ export function DetailsPanel({ fullWidth = false }: DetailsPanelType): JSX.Eleme resetCurrentIndex(); } + /** + * Callback function to update the store state for clearing the details selected layer from left panel. + */ const handleGuideIsOpen = useCallback( (guideIsOpenVal: boolean): void => { + // Log + logger.logTraceUseCallback('DETAILS PANEL - handleGuideIsOpen'); if (guideIsOpenVal) { setSelectedLayerPath(''); } @@ -383,6 +399,9 @@ export function DetailsPanel({ fullWidth = false }: DetailsPanelType): JSX.Eleme * Select the layer after layer is selected from map. */ useEffect(() => { + // Log + logger.logTraceUseEffect('DETAILS-PANEL- mapClickCoordinates', mapClickCoordinates); + if (mapClickCoordinates && memoLayersList?.length && !selectedLayerPath.length) { const selectedLayer = memoLayersList.find((layer) => !!layer.numOffeatures); setSelectedLayerPath(selectedLayer?.layerPath ?? ''); @@ -395,7 +414,7 @@ export function DetailsPanel({ fullWidth = false }: DetailsPanelType): JSX.Eleme */ const memoIsLayerQueryStatusProcessing = useMemo(() => { // Log - logger.logTraceUseMemo('DATA-PANEL - order layer status processing.'); + logger.logTraceUseMemo('DETAILS-PANEL - order layer status processing.'); return () => !!arrayOfLayerDataBatch?.find((layer) => layer.queryStatus === LAYER_STATUS.PROCESSING); }, [arrayOfLayerDataBatch]); @@ -405,11 +424,11 @@ export function DetailsPanel({ fullWidth = false }: DetailsPanelType): JSX.Eleme // #region RENDER SECTION ******************************************************************************************* /** - * Renders the complete Details Panel component - * @returns {JSX.Element} + * Render the right panel content based on detail's layer and loading status. + * NOTE: Here we return null, so that in responsive grid layout, it can be used as flag to render the guide for details. + * @returns {JSX.Element | null} JSX.Element | null */ const renderContent = (): JSX.Element | null => { - // render skeleton when layer is fetching data. if (memoIsLayerQueryStatusProcessing()) { return ; } @@ -418,12 +437,12 @@ export function DetailsPanel({ fullWidth = false }: DetailsPanelType): JSX.Eleme - + {t('details.featureDetailsTitle') .replace('{count}', `${currentFeatureIndex + 1}`) .replace('{total}', `${memoSelectedLayerDataFeatures?.length}`)} - + ); } - // return null to render the guide when detail tab is opened. return null; }; diff --git a/packages/geoview-core/src/core/components/details/feature-detail-modal.tsx b/packages/geoview-core/src/core/components/details/feature-detail-modal.tsx index c79e4cfd7f0..d9efe282233 100644 --- a/packages/geoview-core/src/core/components/details/feature-detail-modal.tsx +++ b/packages/geoview-core/src/core/components/details/feature-detail-modal.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useTheme } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; import { useDataTableSelectedFeature } from '@/core/stores/store-interface-and-intial-values/data-table-state'; @@ -27,15 +28,23 @@ export default function FeatureDetailModal(): JSX.Element { const activeModalId = useUIActiveFocusItem().activeElementId; const feature = useDataTableSelectedFeature()!; - const featureInfoList: TypeFieldEntry[] = Object.keys(feature?.fieldInfo ?? {}).map((fieldName) => { - return { - fieldKey: feature.fieldInfo[fieldName]!.fieldKey, - value: feature.fieldInfo[fieldName]!.value, - dataType: feature.fieldInfo[fieldName]!.dataType, - alias: feature.fieldInfo[fieldName]!.alias ? feature.fieldInfo[fieldName]!.alias : fieldName, - domain: null, - }; - }); + /** + * Build features list to displayed in table + */ + const featureInfoList: TypeFieldEntry[] = useMemo(() => { + // Log + logger.logTraceUseMemo('DETAILS PANEL - Feature Detail Modal - featureInfoList'); + + return Object.keys(feature?.fieldInfo ?? {}).map((fieldName) => { + return { + fieldKey: feature.fieldInfo[fieldName]!.fieldKey, + value: feature.fieldInfo[fieldName]!.value, + dataType: feature.fieldInfo[fieldName]!.dataType, + alias: feature.fieldInfo[fieldName]!.alias ? feature.fieldInfo[fieldName]!.alias : fieldName, + domain: null, + }; + }); + }, [feature]); return ( { - return { - fieldKey: feature.fieldInfo[fieldName]!.fieldKey, - value: feature.fieldInfo[fieldName]!.value, - dataType: feature.fieldInfo[fieldName]!.dataType, - alias: feature.fieldInfo[fieldName]!.alias ? feature.fieldInfo[fieldName]!.alias : fieldName, - domain: null, - }; - }); - - const handleSelect = (e: React.ChangeEvent): void => { - e.stopPropagation(); - - if (!checked) { - addCheckedFeature(feature); - } else { - removeCheckedFeature(feature); - } - }; + /** + * Build feature list to be displayed inside table. + */ + const featureInfoList: TypeFieldEntry[] = useMemo(() => { + // Log + logger.logTraceUseMemo('DETAILS PANEL - Feature Info new - featureInfoList'); + + return Object.keys(feature?.fieldInfo ?? {}).map((fieldName) => { + return { + fieldKey: feature.fieldInfo[fieldName]!.fieldKey, + value: feature.fieldInfo[fieldName]!.value, + dataType: feature.fieldInfo[fieldName]!.dataType, + alias: feature.fieldInfo[fieldName]!.alias ? feature.fieldInfo[fieldName]!.alias : fieldName, + domain: null, + }; + }); + }, [feature]); + + /** + * Toggle feature selected. + */ + const handleFeatureSelectedChange = useCallback( + (e: React.ChangeEvent): void => { + e.stopPropagation(); + + if (!checked) { + addCheckedFeature(feature); + } else { + removeCheckedFeature(feature); + } + }, + [addCheckedFeature, checked, feature, removeCheckedFeature] + ); const handleZoomIn = (e: React.MouseEvent): void => { e.stopPropagation(); @@ -96,15 +110,14 @@ export function FeatureInfo({ features, currentFeatureIndex }: TypeFeatureInfoPr useEffect(() => { // Log - logger.logTraceUseEffect('FEATURE-INFO-NEW - checkedFeatures', checkedFeatures, feature); + logger.logTraceUseEffect('FEATURE-INFO-NEW - checkedFeatures', checkedFeatures); setChecked( checkedFeatures.some((checkedFeature) => { return (checkedFeature.geometry as TypeGeometry)?.ol_uid === featureUid; }) ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [checkedFeatures, feature]); // GV Check if feature is necessary in this dependency array? If so explain it in comment? Should be featurUid? + }, [checkedFeatures, featureUid]); return ( @@ -128,7 +141,7 @@ export function FeatureInfo({ features, currentFeatureIndex }: TypeFeatureInfoPr handleSelect(e)} + onChange={(e) => handleFeatureSelectedChange(e)} checked={checked} sx={sxClasses.selectFeatureCheckbox} /> diff --git a/packages/geoview-core/src/core/components/details/feature-info-table.tsx b/packages/geoview-core/src/core/components/details/feature-info-table.tsx index 2ab47b8da93..2b86a3c201b 100644 --- a/packages/geoview-core/src/core/components/details/feature-info-table.tsx +++ b/packages/geoview-core/src/core/components/details/feature-info-table.tsx @@ -1,16 +1,16 @@ -import { useState } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useTheme } from '@mui/material/styles'; import linkifyHtml from 'linkify-html'; import { CardMedia, Box, Grid } from '@/ui'; -import { LightboxImg, LightBoxSlides } from '@/core/components/lightbox/lightbox'; import { isImage, stringify, generateId, sanitizeHtmlContent } from '@/core/utils/utilities'; import { HtmlToReact } from '@/core/containers/html-to-react'; import { logger } from '@/core/utils/logger'; import { TypeFieldEntry } from '@/geo/layer/layer-sets/abstract-layer-set'; import { getSxClasses } from './details-style'; +import { useLightBox } from '../common'; interface FeatureInfoTableProps { featureInfoList: TypeFieldEntry[]; @@ -31,23 +31,25 @@ export function FeatureInfoTable({ featureInfoList }: FeatureInfoTableProps): JS const theme = useTheme(); const sxClasses = getSxClasses(theme); - // internal state - const [isLightBoxOpen, setIsLightBoxOpen] = useState(false); - const [slides, setSlides] = useState([]); - const [slidesIndex, setSlidesIndex] = useState(0); + const { initLightBox, LightBoxComponent } = useLightBox(); // linkify options - const linkifyOptions = { - attributes: { - title: t('details.externalLink'), - }, - defaultProtocol: 'https', - format: { - url: (value: string) => (value.length > 50 ? `${value.slice(0, 40)}…${value.slice(value.length - 10, value.length)}` : value), - }, - ignoreTags: ['script', 'style', 'img'], - target: '_blank', - }; + const linkifyOptions = useMemo(() => { + // Log + logger.logTraceUseMemo('DETAILS PANEL - Feature Info table - linkifyOptions'); + + return { + attributes: { + title: t('details.externalLink'), + }, + defaultProtocol: 'https', + format: { + url: (value: string) => (value.length > 50 ? `${value.slice(0, 40)}…${value.slice(value.length - 10, value.length)}` : value), + }, + ignoreTags: ['script', 'style', 'img'], + target: '_blank', + }; + }, [t]); /** * Parse the content of the field to see if we need to create an image, a string element or a link @@ -55,28 +57,20 @@ export function FeatureInfoTable({ featureInfoList }: FeatureInfoTableProps): JS * @returns {JSX.Element | JSX.Element[]} the React element(s) */ function setFeatureItem(featureInfoItem: TypeFieldEntry): JSX.Element | JSX.Element[] { - const slidesSetup: LightBoxSlides[] = []; - function process(item: string, alias: string, index: number): JSX.Element { let element: JSX.Element; if (typeof item === 'string' && isImage(item)) { - slidesSetup.push({ src: item, alt: alias, downloadUrl: item }); element = ( { - setIsLightBoxOpen(true); - setSlides(slidesSetup); - setSlidesIndex(index); - }} + click={() => initLightBox(featureInfoItem.value as string, featureInfoItem.alias, index)} keyDown={(e: KeyboardEvent) => { if (e.key === 'Enter') { - setIsLightBoxOpen(true); - setSlides(slidesSetup); + initLightBox(featureInfoItem.value as string, featureInfoItem.alias, index); } }} /> @@ -104,20 +98,6 @@ export function FeatureInfoTable({ featureInfoList }: FeatureInfoTableProps): JS return ( - {isLightBoxOpen && ( - { - // TODO: because lighbox element is render outside the map container, the focus trap is not able to access it. - // TODO: if we use the keyboard to access the image, we can only close with esc key. - // TODO: #1113 - setIsLightBoxOpen(false); - setSlides([]); - }} - /> - )} {featureInfoList.map((featureInfoItem, index) => ( 0 ? theme.palette.geoViewColor.bgColor.darken(0.1) : '', color: index % 2 > 0 ? theme.palette.geoViewColor.bgColor.darken(0.9) : '', - marginBottom: '20px', + marginBottom: '1.25rem', }} - // eslint-disable-next-line react/no-array-index-key - key={index} + key={`${featureInfoItem.alias} ${index.toString()}`} > {featureInfoItem.alias} - + {setFeatureItem(featureInfoItem)} ))} + ); } diff --git a/packages/geoview-core/src/core/components/export/export-modal.tsx b/packages/geoview-core/src/core/components/export/export-modal.tsx index 3e416369476..1a23a104369 100644 --- a/packages/geoview-core/src/core/components/export/export-modal.tsx +++ b/packages/geoview-core/src/core/components/export/export-modal.tsx @@ -14,12 +14,6 @@ import { useMapAttribution, useMapNorthArrow, useMapScale } from '@/core/stores/ import useManageArrow from '@/core/components/north-arrow/hooks/useManageArrow'; import { logger } from '@/core/utils/logger'; -/** - * !NOTE: Error loading remote stylesheet DOMException: Failed to read the 'cssRules' property from 'CSSStyleSheet' for google api - * please add `crossOrigin="anonymous"` in stylesheet link - * like - */ - /** * Export modal window component to export the viewer information in a PNG file * @@ -128,11 +122,15 @@ export default function ExportModal(): JSX.Element { .toPng(legendContainer) .then((dataUrl) => { setIsLegendLoading(false); - const img = new Image(); - img.src = dataUrl; - img.style.maxWidth = `${getCanvasWidth(dialogBox)}px`; - legendContainerRef.current?.appendChild(img); - if (hasHiddenAttr) legendTab.hidden = true; + // TODO: we need to render the legend even if its hidden. + // Note: When legend is hidden in appbar + if (dataUrl.indexOf('base64') !== -1) { + const img = new Image(); + img.src = dataUrl; + img.style.maxWidth = `${getCanvasWidth(dialogBox)}px`; + legendContainerRef.current?.appendChild(img); + if (hasHiddenAttr) legendTab.hidden = true; + } }) .catch((error: Error) => { logger.logError('Error occured while converting legend to image', error); diff --git a/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx b/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx index a0a34571b73..626e93740b2 100644 --- a/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx +++ b/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx @@ -31,6 +31,7 @@ import { Datapanel } from '@/core/components/data-table/data-panel'; import { logger } from '@/core/utils/logger'; import { GuidePanel } from '@/core/components/guide/guide-panel'; import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor'; +import { TypeRecordOfPlugin } from '@/api/plugin/plugin-types'; interface Tab { icon: ReactNode; @@ -231,11 +232,10 @@ export function FooterBar(props: FooterBarProps): JSX.Element | null { // If clicked on a tab with a plugin MapEventProcessor.getMapViewerPlugins(mapId) .then((plugins) => { - if (plugins[selectedTab]) { + const selectedTabKey = 'selectedTab' as keyof TypeRecordOfPlugin; + if (plugins[selectedTabKey]) { // Get the plugin - // ? unknown type cannot be use, need to escape - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const theSelectedPlugin = plugins[selectedTab] as any; + const theSelectedPlugin = plugins[selectedTabKey]; // A bit hacky, but not much other choice for now... if (typeof theSelectedPlugin.onSelected === 'function') { @@ -340,12 +340,15 @@ export function FooterBar(props: FooterBarProps): JSX.Element | null { }, [footerBarTabsConfig, mapId]); // Handle focus using dynamic focus button - const handleDynamicFocus = (): void => { + const handleDynamicFocus = useCallback((): void => { + // Log + logger.logTraceUseCallback('FOOTER BAR - handleDynamicFocus', isFocusToMap, mapId); + const shell = document.getElementById(`shell-${mapId}`); const block = isFocusToMap ? 'start' : 'end'; shell?.scrollIntoView({ behavior: 'smooth', block }); setIsFocusToMap(!isFocusToMap); - }; + }, [isFocusToMap, mapId]); return memoFooterBarTabs.length > 0 ? ( ; -/** - * Get the title for tooltip - * @param {name} - name of the geo item - * @param {province} - province of the geo item - * @param {category} - category of the geo item - * @returns {string} - tooltip title - */ -const getTooltipTitle = ({ name, province, category }: tooltipProp): string => { - let title = name; - if (category && category !== 'null') { - title += `, ${category}`; - } - - if (province && province !== 'null') { - title += `, ${province}`; - } - - return title; -}; - -/** - * Transform the search value in search result with bold css. - * @param {string} title list title in search result - * @param {string} searchValue value with user did the search - * @param {string} province province of the list title in search result - * @returns {JSX.Element} - */ -const transformListTitle = (_title: string, _searchValue: string, province: string): JSX.Element => { - const title = _title.toUpperCase(); - const searchValue = _searchValue.toUpperCase(); - const idx = title.indexOf(searchValue); - if (!searchValue || idx === -1) { - return ( - - {_title} - - ); - } - - const len = searchValue.length; - return ( - ${_title.slice(idx, idx + len)}${_title.slice(idx + len)}${province}`} - /> - ); -}; - /** * Create list of items to display under search. * @param {GeoListItem[]} geoListItems - items to display @@ -69,6 +22,55 @@ const transformListTitle = (_title: string, _searchValue: string, province: stri export default function GeoList({ geoListItems, searchValue }: GeoListProps): JSX.Element { const { zoomToGeoLocatorLocation } = useMapStoreActions(); + /** + * Get the title for tooltip + * @param {name} - name of the geo item + * @param {province} - province of the geo item + * @param {category} - category of the geo item + * @returns {string} - tooltip title + */ + const getTooltipTitle = useCallback(({ name, province, category }: tooltipProp): string => { + // Log + logger.logTraceUseCallback('GEOLOCATOR - geolist - getTooltipTitle', name, province, category); + + let title = name; + if (category && category !== 'null') { + title += `, ${category}`; + } + + if (province && province !== 'null') { + title += `, ${province}`; + } + + return title; + }, []); + + /** + * Transform the search value in search result with bold css. + * @param {string} title list title in search result + * @param {string} searchValue value with user did the search + * @param {string} province province of the list title in search result + * @returns {JSX.Element} + */ + const transformListTitle = useCallback((_title: string, _searchValue: string, province: string): JSX.Element | string => { + // Log + logger.logTraceUseCallback('GEOLOCATOR - geolist - transformListTitle', _title, _searchValue, province); + + const title = _title.toUpperCase(); + const searchItem = _searchValue.toUpperCase(); + const idx = title.indexOf(searchItem); + const len = searchItem.length; + if (!searchItem || idx === -1) return _title; + + return ( + ${_title.slice(idx, idx + len)}${_title.slice(idx + len)}${province}`} + /> + ); + }, []); + return ( {geoListItems.map((geoListItem, index) => ( @@ -82,7 +84,7 @@ export default function GeoList({ geoListItems, searchValue }: GeoListProps): JS zoomToGeoLocatorLocation([geoListItem.lng, geoListItem.lat], geoListItem.bbox)}> - + {transformListTitle( geoListItem.name, searchValue, diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx index b97de0a7c8c..a90e38284fb 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx @@ -105,19 +105,12 @@ export function GeolocatorResult({ geoLocationData, searchValue, error }: Geoloc // eslint-disable-next-line react-hooks/exhaustive-deps }, [geoLocationData]); - useEffect(() => { + // Cache the filter data + const memoFilterData = useMemo(() => { // Log - logger.logTraceUseEffect('GEOLOCATOR-RESULT - geoLocationData', geoLocationData); - - setData(geoLocationData); - }, [geoLocationData]); + logger.logTraceUseMemo('GEOLOCATOR-RESULT - memoFilterData', geoLocationData, province, category); - useEffect(() => { - // Log - logger.logTraceUseEffect('GEOLOCATOR-RESULT - geoLocationData province category', geoLocationData, province, category); - - // update result list after setting the province and type. - const filterData = geoLocationData.filter((item) => { + return geoLocationData.filter((item) => { let result = true; if (province.length && !category.length) { result = item.province.toLowerCase() === province.toLowerCase(); @@ -128,8 +121,22 @@ export function GeolocatorResult({ geoLocationData, searchValue, error }: Geoloc } return result; }); - setData(filterData); - }, [geoLocationData, province, category, categories, provinces]); + }, [category, geoLocationData, province]); + + useEffect(() => { + // Log + logger.logTraceUseEffect('GEOLOCATOR-RESULT - geoLocationData', geoLocationData); + + setData(geoLocationData); + }, [geoLocationData]); + + useEffect(() => { + // Log + logger.logTraceUseEffect('GEOLOCATOR-RESULT - geoLocationData province category', memoFilterData); + + // update result list after setting the province and type. + setData(memoFilterData); + }, [memoFilterData]); useEffect(() => { // Log diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator-style.ts b/packages/geoview-core/src/core/components/geolocator/geolocator-style.ts index 29c192bd5ba..be89082bdc4 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator-style.ts +++ b/packages/geoview-core/src/core/components/geolocator/geolocator-style.ts @@ -70,13 +70,9 @@ export const sxClasses = { export const sxClassesList = { listStyle: { fontSize: (theme: Theme) => theme.palette.geoViewFontSize.sm, - '& .list-title': { - '>div': { - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', }, main: { whiteSpace: 'nowrap', diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx index 7cb58366ef7..e9c75cd07a8 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx @@ -45,6 +45,7 @@ export function Geolocator(): JSX.Element { const urlRef = useRef(`${geolocatorServiceURL}&lang=${displayLanguage}`); const abortControllerRef = useRef(null); const MIN_SEARCH_LENGTH = 3; + /** * Checks if search term is decimal degree coordinate and return geo list item. * @param {string} searchTerm search term user searched. diff --git a/packages/geoview-core/src/core/components/guide/guide-panel.tsx b/packages/geoview-core/src/core/components/guide/guide-panel.tsx index ce8251667e3..d5a0ac49b48 100644 --- a/packages/geoview-core/src/core/components/guide/guide-panel.tsx +++ b/packages/geoview-core/src/core/components/guide/guide-panel.tsx @@ -77,11 +77,17 @@ export function GuidePanel({ fullWidth }: GuidePanelType): JSX.Element { * Handle Guide layer list. * @param {LayerListEntry} layer geoview layer. */ - const handleGuideItemClick = (layer: LayerListEntry): void => { - const index: number = layersList.findIndex((item) => item.layerName === layer.layerName); - setGuideItemIndex(index); - setSelectedLayerPath(layer.layerPath); - }; + const handleGuideItemClick = useCallback( + (layer: LayerListEntry): void => { + // Log + logger.logTraceUseCallback('GUIDE PANEL - handleGuideItemClick', layer); + + const index: number = layersList.findIndex((item) => item.layerName === layer.layerName); + setGuideItemIndex(index); + setSelectedLayerPath(layer.layerPath); + }, + [layersList] + ); return ( @@ -93,9 +99,7 @@ export function GuidePanel({ fullWidth }: GuidePanelType): JSX.Element { aria-label={t('guide.title')} > - - {layersList[guideItemIndex]?.content} - + {layersList[guideItemIndex]?.content} diff --git a/packages/geoview-core/src/core/components/guide/guide-style.ts b/packages/geoview-core/src/core/components/guide/guide-style.ts index e9dfbc031d8..863cb447ad0 100644 --- a/packages/geoview-core/src/core/components/guide/guide-style.ts +++ b/packages/geoview-core/src/core/components/guide/guide-style.ts @@ -1,39 +1,38 @@ import { Theme } from '@mui/material/styles'; -// ? I doubt we want to define an explicit type for style properties? -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const getSxClasses = (theme: Theme): any => ({ - guideContainer: { - '& .responsive-layout-right-main-content': { - backgroundColor: theme.palette.geoViewColor.white, +export const getSxClasses = (theme: Theme) => + ({ + guideContainer: { + '& .responsive-layout-right-main-content': { + backgroundColor: theme.palette.geoViewColor.white, + }, }, - }, - rightPanelContainer: { - color: theme.palette.geoViewColor.textColor.main, - }, - footerGuideListItemText: { - '&:hover': { - cursor: 'pointer', + rightPanelContainer: { + color: theme.palette.geoViewColor.textColor.main, }, - '& .MuiListItemText-primary': { - padding: '15px', - fontSize: `${theme.palette.geoViewFontSize.lg} !important`, - lineHeight: 1.5, - fontWeight: '700', - textTransform: 'capitalize', + footerGuideListItemText: { + '&:hover': { + cursor: 'pointer', + }, + '& .MuiListItemText-primary': { + padding: '15px', + fontSize: `${theme.palette.geoViewFontSize.lg} !important`, + lineHeight: 1.5, + fontWeight: '700', + textTransform: 'capitalize', + }, }, - }, - footerGuideListItemCollapse: { - '& .MuiListItemText-primary': { - padding: '15px 15px 15px 30px', - fontSize: `${theme.palette.geoViewFontSize.md} !important`, - lineHeight: 1.5, - whiteSpace: 'unset', + footerGuideListItemCollapse: { + '& .MuiListItemText-primary': { + padding: '15px 15px 15px 30px', + fontSize: `${theme.palette.geoViewFontSize.md} !important`, + lineHeight: 1.5, + whiteSpace: 'unset', + }, }, - }, - errorMessage: { - marginLeft: '60px', - marginTop: '30px', - marginBottom: '12px', - }, -}); + errorMessage: { + marginLeft: '60px', + marginTop: '30px', + marginBottom: '12px', + }, + } as const); diff --git a/packages/geoview-core/src/core/containers/html-to-react.tsx b/packages/geoview-core/src/core/containers/html-to-react.tsx index ace685019e5..1e59c5bdfb0 100644 --- a/packages/geoview-core/src/core/containers/html-to-react.tsx +++ b/packages/geoview-core/src/core/containers/html-to-react.tsx @@ -7,12 +7,10 @@ import { Box } from '@/ui/layout'; */ interface HtmlToReactProps { htmlContent: string; - // eslint-disable-next-line react/require-default-props className?: string; - // eslint-disable-next-line react/require-default-props style?: CSSProperties; - // eslint-disable-next-line react/require-default-props extraOptions?: Record; + itemOptions?: Record; } /** @@ -21,9 +19,7 @@ interface HtmlToReactProps { * @param {HtmlToReactProps} props the properties to pass to the converted component * @returns {JSX.Element} returns the converted JSX component */ -export function HtmlToReact(props: HtmlToReactProps): JSX.Element { - const { htmlContent, className, style, extraOptions } = props; - +export function HtmlToReact({ htmlContent, className, style, extraOptions, itemOptions = {} }: HtmlToReactProps): JSX.Element { // the html-react-parser can return 2 type in an array or not, make sure we have an array const parsed = parse(htmlContent) as string | Array; const items = typeof parsed === 'string' || typeof parsed === 'object' ? [parsed] : parsed; @@ -36,12 +32,13 @@ export function HtmlToReact(props: HtmlToReactProps): JSX.Element { else reactItems.push(items[i]); } - // eslint-disable-next-line react/jsx-props-no-spreading return ( {reactItems.map((item: TrustedHTML, index) => ( // eslint-disable-next-line react/no-array-index-key - {item as ReactNode} + + {item as ReactNode} + ))} ); diff --git a/packages/geoview-core/src/core/utils/constant.ts b/packages/geoview-core/src/core/utils/constant.ts index 1e566be19e0..78b16ef1176 100644 --- a/packages/geoview-core/src/core/utils/constant.ts +++ b/packages/geoview-core/src/core/utils/constant.ts @@ -43,3 +43,39 @@ export const TABS = { TIME_SLIDER: 'time-slider', GEO_CHART: 'geochart', } as const; + +export const NUMBER_FILTER: Record = { + lessThanOrEqualTo: '<=', + lessThan: '<', + greaterThan: '>', + greaterThanOrEqualTo: '>=', + empty: 'is null', + notEmpty: 'is not null', + between: '>', + betweenInclusive: '>=', + equals: '=', + notEquals: '<>', +}; + +export const DATE_FILTER: Record = { + greaterThan: `> date 'value'`, + greaterThanOrEqualTo: `>= date 'value'`, + lessThan: `< date 'value'`, + lessThanOrEqualTo: `<= date 'value'`, + equals: `= date 'value'`, + empty: 'is null', + notEmpty: 'is not null', + notEquals: `<> date 'value'`, + between: `> date 'value'`, + betweenInclusive: `>= date 'value'`, +}; + +export const STRING_FILTER: Record = { + contains: `(filterId) like ('%value%')`, + startsWith: `(filterId) like ('value%')`, + endsWith: `(filterId) like ('%value')`, + empty: '(filterId) is null', + notEmpty: '(filterId) is not null', + equals: `filterId = 'value'`, + notEquals: `filterId <> 'value'`, +}; diff --git a/packages/geoview-core/src/ui/linear-progress/linear-progress.tsx b/packages/geoview-core/src/ui/linear-progress/linear-progress.tsx index cfc23da36f2..28df429a0c6 100644 --- a/packages/geoview-core/src/ui/linear-progress/linear-progress.tsx +++ b/packages/geoview-core/src/ui/linear-progress/linear-progress.tsx @@ -22,22 +22,10 @@ interface ProgressbarProps { * @param {ProgressbarProps} props the properties passed to the element * @returns {JSX.Element} the created element */ -export function ProgressBar(props: ProgressbarProps): JSX.Element { - const { className, variant, value } = props; - +export function ProgressBar({ className = '', variant = 'indeterminate', value = 0 }: ProgressbarProps): JSX.Element { return ; } -/** - * Default property values - */ -// TODO: Refactor - Remove defaultProps as it's no longer a good practice -ProgressBar.defaultProps = { - className: '', - variant: 'indeterminate', - value: 0, -}; - /** * Example of usage by application code *