diff --git a/public/icons/basemaps/arcgis-dark-gray.png b/public/icons/basemaps/arcgis-dark-gray.png new file mode 100644 index 00000000..8b35f5a3 Binary files /dev/null and b/public/icons/basemaps/arcgis-dark-gray.png differ diff --git a/public/icons/basemaps/arcgis-light-gray.png b/public/icons/basemaps/arcgis-light-gray.png new file mode 100644 index 00000000..9e11fed9 Binary files /dev/null and b/public/icons/basemaps/arcgis-light-gray.png differ diff --git a/public/icons/basemaps/arcgis-streets-dark.png b/public/icons/basemaps/arcgis-streets-dark.png new file mode 100644 index 00000000..1d347a6d Binary files /dev/null and b/public/icons/basemaps/arcgis-streets-dark.png differ diff --git a/public/icons/basemaps/arcgis-streets.png b/public/icons/basemaps/arcgis-streets.png new file mode 100644 index 00000000..100f773c Binary files /dev/null and b/public/icons/basemaps/arcgis-streets.png differ diff --git a/public/icons/basemaps/custom-map.png b/public/icons/basemaps/custom-map.png new file mode 100644 index 00000000..339413a2 Binary files /dev/null and b/public/icons/basemaps/custom-map.png differ diff --git a/public/icons/basemaps/maplibre-dark.png b/public/icons/basemaps/maplibre-dark.png new file mode 100644 index 00000000..37627a6b Binary files /dev/null and b/public/icons/basemaps/maplibre-dark.png differ diff --git a/public/icons/basemaps/maplibre-light.png b/public/icons/basemaps/maplibre-light.png new file mode 100644 index 00000000..8725db39 Binary files /dev/null and b/public/icons/basemaps/maplibre-light.png differ diff --git a/public/icons/basemaps/terrain.png b/public/icons/basemaps/terrain.png new file mode 100644 index 00000000..94fbc5e4 Binary files /dev/null and b/public/icons/basemaps/terrain.png differ diff --git a/public/icons/custom-map.svg b/public/icons/custom-map.svg index 0511df45..e4295877 100644 --- a/public/icons/custom-map.svg +++ b/public/icons/custom-map.svg @@ -1,3 +1,3 @@ - - + + diff --git a/public/icons/terrain-map.png b/public/icons/terrain-map.png deleted file mode 100644 index 46c8d8f9..00000000 Binary files a/public/icons/terrain-map.png and /dev/null differ diff --git a/src/app.tsx b/src/app.tsx index fbefe9f1..ad8f6150 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -89,6 +89,8 @@ const THEMES: AppThemes = { switchCheckedBackground: color_brand_tertiary, switchCheckedBackgroundHovered: dim_brand_tertinary, bullet: color_canvas_secondary, + dropdownArrow: hilite_canvas_secondary, + customIconBackground: color_brand_quaternary, }, name: Theme.Dark, }, @@ -132,6 +134,8 @@ const THEMES: AppThemes = { switchCheckedBackground: color_brand_tertiary, switchCheckedBackgroundHovered: dim_brand_tertinary, bullet: hilite_canvas_primary, + dropdownArrow: color_brand_primary, + customIconBackground: dim_canvas_secondary, }, name: Theme.Light, }, diff --git a/src/components/comparison/comparison-side/comparison-side.tsx b/src/components/comparison/comparison-side/comparison-side.tsx index 98d2a337..6323a648 100644 --- a/src/components/comparison/comparison-side/comparison-side.tsx +++ b/src/components/comparison/comparison-side/comparison-side.tsx @@ -19,6 +19,7 @@ import { PageId, type TilesetMetadata, type LayoutProps, + BaseMapGroup, } from "../../../types"; import { DeckGlWrapper } from "../../deck-gl-wrapper/deck-gl-wrapper"; import { MainToolsPanel } from "../../main-tools-panel/main-tools-panel"; @@ -61,7 +62,7 @@ import { useSelector } from "react-redux"; import { type RootState } from "../../../redux/store"; import { selectFiltersByAttribute } from "../../../redux/slices/symbolization-slice"; import { selectViewState } from "../../../redux/slices/view-state-slice"; -import { selectSelectedBaseMapId } from "../../../redux/slices/base-maps-slice"; +import { selectSelectedBaseMap } from "../../../redux/slices/base-maps-slice"; import { ArcgisWrapper } from "../../../components/arcgis-wrapper/arcgis-wrapper"; const Container = styled.div` @@ -138,9 +139,11 @@ export const ComparisonSide = ({ ? selectLeftSublayers : selectRightSublayers ); - const selectedBaseMapId = useAppSelector(selectSelectedBaseMapId); + const selectedBaseMap = useAppSelector(selectSelectedBaseMap); const MapWrapper = - selectedBaseMapId === "ArcGis" ? ArcgisWrapper : DeckGlWrapper; + selectedBaseMap?.group === BaseMapGroup.ArcGIS + ? ArcgisWrapper + : DeckGlWrapper; const [isCompressedGeometry, setIsCompressedGeometry] = useState(true); const [isCompressedTextures, setIsCompressedTextures] = diff --git a/src/components/debug-panel/debug-panel.tsx b/src/components/debug-panel/debug-panel.tsx index 2141dde5..49c610b5 100644 --- a/src/components/debug-panel/debug-panel.tsx +++ b/src/components/debug-panel/debug-panel.tsx @@ -4,6 +4,7 @@ import { addIconItem } from "../../redux/slices/icon-list-slice"; import { type IIconItem, IconListSetName, + BaseMapGroup, ButtonSize, FileType, type FileUploaded, @@ -18,7 +19,6 @@ import { IconListPanel } from "../icon-list-panel/icon-list-panel"; import { ActionIconButton } from "../action-icon-button/action-icon-button"; import PlusIcon from "../../../public/icons/plus.svg"; import { UploadPanel } from "../upload-panel/upload-panel"; - import { useAppLayout } from "../../utils/hooks/layout"; import { CloseButton } from "../close-button/close-button"; import { @@ -35,7 +35,7 @@ import { setDebugOptions, selectDebugOptions, } from "../../redux/slices/debug-options-slice"; -import { selectSelectedBaseMapId } from "../../redux/slices/base-maps-slice"; +import { selectSelectedBaseMap } from "../../redux/slices/base-maps-slice"; export const TEXTURE_ICON_SIZE = 54; @@ -95,8 +95,8 @@ export const DebugPanel = ({ onClose }: DebugPanelProps) => { const dispatch = useAppDispatch(); const [showFileUploadPanel, setShowFileUploadPanel] = useState(false); const debugOptions = useAppSelector(selectDebugOptions); - const selectedBaseMapId = useAppSelector(selectSelectedBaseMapId); - const minimapDisabled = selectedBaseMapId === "ArcGis"; + const selectedBaseMap = useAppSelector(selectSelectedBaseMap); + const minimapDisabled = selectedBaseMap?.group === BaseMapGroup.ArcGIS; if (minimapDisabled && debugOptions.minimap) { dispatch(setDebugOptions({ minimap: false })); } diff --git a/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx b/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx index 983b5c3d..a5f10ad4 100644 --- a/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx +++ b/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx @@ -1,7 +1,12 @@ // Get tileset stub before Mocks. The order is important import { getTileset3d, getTile3d } from "../../test/tile-stub"; import { getTilesetJson } from "../../test/tileset-header-stub"; -import { DragMode, TilesetType, TileColoredBy } from "../../types"; +import { + DragMode, + TilesetType, + TileColoredBy, + BaseMapGroup, +} from "../../types"; import { act } from "@testing-library/react"; import { DeckGlWrapper } from "./deck-gl-wrapper"; @@ -357,7 +362,15 @@ describe("Deck.gl I3S map component", () => { describe("Render TerrainLayer", () => { const store = setupStore(); - store.dispatch(addBaseMap({ id: "Terrain", mapUrl: "", name: "Terrain" })); + store.dispatch( + addBaseMap({ + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Dark", + }) + ); it("Should render terrain", () => { callRender(renderWithProvider, undefined, store); expect(TerrainLayer).toHaveBeenCalled(); @@ -366,7 +379,13 @@ describe("Deck.gl I3S map component", () => { it("Should call onTerrainTileLoad", async () => { const store = setupStore(); store.dispatch( - addBaseMap({ id: "Terrain", mapUrl: "", name: "Terrain" }) + addBaseMap({ + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", + }) ); const { rerender } = callRender(renderWithProvider, undefined, store); const { onTileLoad } = TerrainLayer.mock.lastCall[0]; diff --git a/src/components/input-dropdown/input-dropdown.spec.tsx b/src/components/input-dropdown/input-dropdown.spec.tsx new file mode 100644 index 00000000..33a4e7c3 --- /dev/null +++ b/src/components/input-dropdown/input-dropdown.spec.tsx @@ -0,0 +1,89 @@ +import { fireEvent } from "@testing-library/react"; +import { renderWithTheme } from "../../utils/testing-utils/render-with-theme"; +import { InputDropdown } from "./input-dropdown"; + +describe("Input Text", () => { + it("Should render InputText without label", () => { + const onChange = jest.fn(); + + const dom = renderWithTheme( + + ); + expect(dom).toBeDefined(); + if (!dom) { + return; + } + const input: HTMLSelectElement | null = + dom.container.querySelector("select"); + const inputLabel: HTMLLabelElement | null = + dom.container.querySelector("label"); + + expect(input).toBeInTheDocument(); + expect(inputLabel).not.toBeInTheDocument(); + }); + + it("Should render InputText with label", () => { + const onChange = jest.fn(); + + const dom = renderWithTheme( + + ); + expect(dom).toBeDefined(); + if (!dom) { + return; + } + const input: HTMLSelectElement | null = + dom.container.querySelector("select"); + const inputLabel: HTMLLabelElement | null = + dom.container.querySelector("label"); + + expect(input).toBeInTheDocument(); + expect(input?.value).toBe("test1"); + expect(inputLabel).toBeInTheDocument(); + expect(inputLabel?.textContent).toEqual("Label Text"); + }); + + it("Should change InputText", () => { + let changedValue = ""; + const onChange = jest + .fn() + .mockImplementation((event) => (changedValue = event.target.value)); + + const dom = renderWithTheme( + + ); + expect(dom).toBeDefined(); + if (!dom) { + return; + } + const input: HTMLSelectElement | null = + dom.container.querySelector("select"); + if (input) { + fireEvent.change(input, { target: { value: "test2" } }); + } + + expect(changedValue).toBe("test2"); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("Should handle value as prop", () => { + const onChange = jest.fn(); + + const dom = renderWithTheme( + + ); + expect(dom).toBeDefined(); + if (!dom) { + return; + } + const input: HTMLSelectElement | null = + dom.container.querySelector("select"); + + expect(input?.value).toBe("test1"); + expect(onChange).toHaveBeenCalledTimes(0); + }); +}); diff --git a/src/components/input-dropdown/input-dropdown.tsx b/src/components/input-dropdown/input-dropdown.tsx new file mode 100644 index 00000000..afce5555 --- /dev/null +++ b/src/components/input-dropdown/input-dropdown.tsx @@ -0,0 +1,119 @@ +import { type ChangeEvent, type FC, useId } from "react"; +import styled, { useTheme } from "styled-components"; +import { ExpandIcon } from "../expand-icon/expand-icon"; +import { CollapseDirection, ExpandState } from "../../types"; + +const InputWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; +`; + +const SelectDiv = styled.div` + position: relative; + width: 100%; + height: 46px; + border-radius: 8px; + background: ${({ theme }) => theme.colors.mainHiglightColor}; + &:hover { + background: ${({ theme }) => theme.colors.mainDimColor}; + } + magrin: 0; +`; + +const Input = styled.select` + width: 100%; + padding: 13px 30px 13px 16px; + border-radius: 8px; + color: ${({ theme }) => theme.colors.secondaryFontColor}; + border: 1px solid ${({ theme }) => theme.colors.mainHiglightColor}; + + &:hover { + border: 1px solid ${({ theme }) => theme.colors.mainDimColor}; + cursor: pointer; + } + &:focus { + color: ${({ theme }) => theme.colors.fontColor}; + outline: none; + } + appearance: none; + background: transparent; + magrin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const SelectOption = styled.option` + height: 20px; + background: ${({ theme }) => theme.colors.mainHiglightColor}; + &:hover { + background-color: ${({ theme }) => theme.colors.mainDimColor}; + box-shadow: 0 0 10px 100px green inset; + } + &:checked { + background-color: ${({ theme }) => theme.colors.mainDimColor}; + box-shadow: 0 0 10px 100px green inset; + } + box-shadow: 0 0 10px 100px green inset; +`; + +const ExpandIconWrapper = styled.div` + position: absolute; + top: 50%; + transform: translate(0, -50%); + right: 12px; +`; + +const Label = styled.label` + display: block; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 19px; + margin-bottom: 8px; + color: ${({ theme }) => theme.colors.fontColor}; +`; + +interface InputDropdownProps { + label?: string; + options: string[]; + onChange: (event: ChangeEvent) => void; +} + +export const InputDropdown: FC = ({ + label, + options, + onChange, + ...rest +}) => { + const inputId = useId(); + const theme = useTheme(); + return ( + + {label && } + + + {}} + /> + + + + + ); +}; diff --git a/src/components/layers-panel/base-map-icon/base-map-icon.spec.tsx b/src/components/layers-panel/base-map-icon/base-map-icon.spec.tsx deleted file mode 100644 index 6fa3865f..00000000 --- a/src/components/layers-panel/base-map-icon/base-map-icon.spec.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { renderWithTheme } from "../../../utils/testing-utils/render-with-theme"; -import { BaseMapIcon } from "./base-map-icon"; - -const validatePresetBaseMap = (id: string) => { - const { container } = renderWithTheme() ?? {}; - expect(container).toBeDefined(); - if (!container) { - return; - } - const component = container.firstChild; - expect(component?.nodeName).toBe("DIV"); - if (component) { - const background = getComputedStyle(component as Element).getPropertyValue( - "background" - ); - expect(background).toBe("rgb(35, 36, 48) url() no-repeat center"); - } - expect(component?.firstChild).toBeNull(); -}; - -describe("BaseMapIcon", () => { - it("Should render BaseMapIcons with background", () => { - const presetBaseMapIds = ["Dark", "Light", "Terrain"]; - for (const baseMapId of presetBaseMapIds) { - validatePresetBaseMap(baseMapId); - } - }); - - it("Should render SVG component", () => { - const { container } = - renderWithTheme() ?? {}; - expect(container).toBeDefined(); - if (!container) { - return; - } - const component = container.firstChild; - expect(component?.nodeName).toBe("DIV"); - if (component) { - const background = getComputedStyle( - component as Element - ).getPropertyValue("background"); - expect(background).toBe("rgb(35, 36, 48)"); - } - - const svgElement = component?.firstChild; - expect(svgElement).not.toBeNull(); - expect(svgElement?.nodeName).toBe("svg"); - }); -}); diff --git a/src/components/layers-panel/base-map-icon/base-map-icon.tsx b/src/components/layers-panel/base-map-icon/base-map-icon.tsx deleted file mode 100644 index 0fdb9ef9..00000000 --- a/src/components/layers-panel/base-map-icon/base-map-icon.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import styled from "styled-components"; -import DarkMap from "../../../../public/icons/dark-map.png"; -import LightMap from "../../../../public/icons/light-map.png"; -import TerrainMap from "../../../../public/icons/terrain-map.png"; -import CustomMap from "../../../../public/icons/custom-map.svg"; - -interface BaseMapIconProps { - baseMapId: string; -} - -const MapIcon = styled.div` - background: #232430; - width: 40px; - height: 40px; - border-radius: 8px; - display: flex; - justify-content: center; - align-items: center; -`; - -const MapIconWithBackground = styled(MapIcon)<{ url: string }>` - background: url(${(props) => props.url}) no-repeat center #232430; - width: 40px; - height: 40px; - border-radius: 8px; -`; - -export const BaseMapIcon = ({ baseMapId }: BaseMapIconProps) => { - switch (baseMapId) { - case "Dark": - return ; - case "Light": - return ; - case "Terrain": - return ; - default: - return ( - - - - ); - } -}; diff --git a/src/components/layers-panel/base-map-list-item/base-map-list-item.spec.tsx b/src/components/layers-panel/base-map-list-item/base-map-list-item.spec.tsx deleted file mode 100644 index c1d86ac1..00000000 --- a/src/components/layers-panel/base-map-list-item/base-map-list-item.spec.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { BaseMapListItem } from "./base-map-list-item"; -import { renderWithTheme } from "../../../utils/testing-utils/render-with-theme"; -import { SelectionState } from "../../../types"; - -describe("Base Map List Item", () => { - it("Should render base map list item", async () => { - const onChange = jest.fn(); - const onOptionsClick = jest.fn(); - - renderWithTheme( - - ); - const component = screen.getByText("san-francisco"); - expect(component).toBeInTheDocument(); - await userEvent.click(component); - expect(onChange).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/components/layers-panel/base-map-list-item/base-map-list-item.tsx b/src/components/layers-panel/base-map-list-item/base-map-list-item.tsx deleted file mode 100644 index d0bf6bf0..00000000 --- a/src/components/layers-panel/base-map-list-item/base-map-list-item.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import styled from "styled-components"; -import { type SelectionState } from "../../../types"; -import { BaseMapIcon } from "../base-map-icon/base-map-icon"; -import { ListItemWrapper } from "../list-item-wrapper/list-item-wrapper"; - -interface BaseMapsItemProps { - id: string; - title: string; - optionsContent?: JSX.Element; - selected: SelectionState; - isOptionsPanelOpen: boolean; - onMapsSelect: (id) => void; - onOptionsClick: (id: string) => void; - onClickOutside?: () => void; -} - -const Title = styled.div` - margin-left: 16px; - font-style: normal; - font-weight: 500; - font-size: 16px; - line-height: 19px; - color: ${({ theme }) => theme.colors.fontColor}; -`; - -export const BaseMapListItem = ({ - id, - title, - optionsContent, - isOptionsPanelOpen, - selected, - onOptionsClick, - onClickOutside, - onMapsSelect, -}: BaseMapsItemProps) => { - const handleClick = () => { - onMapsSelect(id); - }; - return ( - - - {title} - - ); -}; diff --git a/src/components/layers-panel/basemap-list-panel/basemap-list-panel.spec.tsx b/src/components/layers-panel/basemap-list-panel/basemap-list-panel.spec.tsx new file mode 100644 index 00000000..f64a9889 --- /dev/null +++ b/src/components/layers-panel/basemap-list-panel/basemap-list-panel.spec.tsx @@ -0,0 +1,206 @@ +import { screen, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { setupStore } from "../../../redux/store"; +import { renderWithThemeProviders } from "../../../utils/testing-utils/render-with-theme"; +import { BasemapListPanel } from "./basemap-list-panel"; + +import { DeleteConfirmation } from "../delete-confirmation"; +import { + addBaseMap, + selectSelectedBaseMap, + selectBaseMapsByGroup, +} from "../../../redux/slices/base-maps-slice"; +import { BaseMapGroup } from "../../../types"; + +jest.mock("../delete-confirmation"); + +const DeleteConfirmationMock = + DeleteConfirmation as unknown as jest.Mocked; + +beforeAll(() => { + DeleteConfirmationMock.mockImplementation(() => ( +
Delete Confirmation
+ )); +}); + +const callRender = (renderFunc, props = {}, store = setupStore()) => { + return renderFunc( + , + store + ); +}; + +describe("Basemap List Panel", () => { + it("Should render basemaps", async () => { + const store = setupStore(); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + + expect(screen.getByText("Dark")).toBeInTheDocument(); + expect(screen.getByText("Light")).toBeInTheDocument(); + }); + + it("Should select a map", async () => { + const store = setupStore(); + store.dispatch( + addBaseMap({ + id: "first", + name: "first name", + mapUrl: "https://first-url.com", + group: BaseMapGroup.Maplibre, + iconId: "Dark", + }) + ); + // Element "first" is added and made selected + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + const el = screen.getByText("first name"); + expect(el).toBeInTheDocument(); + + const iconWrapperElement = el.parentElement; + // Select "first" element + await act(async () => { + iconWrapperElement && (await userEvent.click(iconWrapperElement)); + }); + + const state = store.getState(); + const baseMapId = selectSelectedBaseMap(state)?.id; + expect(baseMapId).toEqual("first"); + }); + + it("Should render options menu, keep or delete a map", async () => { + const store = setupStore(); + store.dispatch( + // Candidate to delete + addBaseMap({ + id: "custom", + name: "custom name", + mapUrl: "https://first-url.com", + group: BaseMapGroup.Maplibre, + iconId: "Light", + custom: true, + }) + ); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + const el = screen.getByText("custom name"); + expect(el).toBeInTheDocument(); + + let state = store.getState(); + let baseMaps = selectBaseMapsByGroup(state, ""); + + expect(baseMaps.find((item) => item.id === "custom")).not.toBeUndefined(); + + const iconWrapperElement = el.parentElement; + const optionsElement = iconWrapperElement?.lastElementChild; + + // Keep a map + // Click on options menu + await act(async () => { + optionsElement && (await userEvent.click(optionsElement)); + }); + + let optionsMenu = screen.getByText("Delete map"); + expect(optionsMenu).toBeInTheDocument(); + + // Click on Delete Map + await act(async () => { + optionsMenu && (await userEvent.click(optionsMenu)); + }); + + let confirmation = screen.getByText("Delete Confirmation"); + expect(confirmation).toBeInTheDocument(); + + const { onKeepHandler } = DeleteConfirmationMock.mock.lastCall[0]; + + act(() => { + onKeepHandler(); + }); + + state = store.getState(); + baseMaps = selectBaseMapsByGroup(state, ""); + expect(baseMaps.find((item) => item.id === "custom")).not.toBeUndefined(); + + // Delete a map + // Click on options menu + await act(async () => { + optionsElement && (await userEvent.click(optionsElement)); + }); + + optionsMenu = screen.getByText("Delete map"); + expect(optionsMenu).toBeInTheDocument(); + + // Click on Delete Map + await act(async () => { + optionsMenu && (await userEvent.click(optionsMenu)); + }); + + confirmation = screen.getByText("Delete Confirmation"); + expect(confirmation).toBeInTheDocument(); + + const { onDeleteHandler } = DeleteConfirmationMock.mock.lastCall[0]; + + act(() => { + onDeleteHandler(); + }); + + state = store.getState(); + baseMaps = selectBaseMapsByGroup(state, ""); + expect(baseMaps.find((item) => item.id === "custom")).toBeUndefined(); + }); + + it("Should close options menu if clicked outside", async () => { + const store = setupStore(); + store.dispatch( + // Candidate to delete + addBaseMap({ + id: "custom", + name: "custom name", + mapUrl: "https://first-url.com", + group: BaseMapGroup.Maplibre, + iconId: "Light", + custom: true, + }) + ); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + const el = screen.getByText("custom name"); + expect(el).toBeInTheDocument(); + + const iconWrapperElement = el.parentElement; + const optionsElement = iconWrapperElement?.lastElementChild; + + // Click on options menu + await act(async () => { + optionsElement && (await userEvent.click(optionsElement)); + }); + + const optionsMenu = screen.getByText("Delete map"); + expect(optionsMenu).toBeInTheDocument(); + + // Click on out of options menu + const elOutside = screen.getByText("Dark"); + await act(async () => { + elOutside && (await userEvent.click(elOutside)); + }); + + const optionsMenuClosed = screen.queryByText("Delete map"); + expect(optionsMenuClosed).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/layers-panel/basemap-list-panel/basemap-list-panel.tsx b/src/components/layers-panel/basemap-list-panel/basemap-list-panel.tsx new file mode 100644 index 00000000..8dbc32d2 --- /dev/null +++ b/src/components/layers-panel/basemap-list-panel/basemap-list-panel.tsx @@ -0,0 +1,235 @@ +import styled, { css, useTheme } from "styled-components"; +import { useState, type FC } from "react"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; + +import { OptionsIcon, Panels } from "../../common"; +import { + selectSelectedBaseMap, + setSelectedBaseMap, + selectBaseMapsByGroup, + deleteBaseMap, +} from "../../../redux/slices/base-maps-slice"; +import { basemapIcons } from "../../../constants/map-styles"; +import { Popover } from "react-tiny-popover"; +import { DeleteConfirmation } from "../delete-confirmation"; +import { BaseMapOptionsMenu } from "../basemap-options-menu/basemap-options-menu"; + +const BASEMAP_ICON_WIDTH = "100px"; +const BASEMAP_ICON_HEIGHT = "70px"; + +const BasemapContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: start; + align-items: start; + border-width: 0; + margin: 0; +`; + +const BasemapTitle = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 17px; + color: ${({ theme }) => theme.colors.fontColor}; + + margin-bottom: 13px; +`; + +const BasemapPanel = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: start; + align-items: center; + border-width: 0; + border-radius: 8px; +`; + +const BasemapImageWrapper = styled.div<{ + active?: boolean; +}>` + position: relative; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + cursor: pointer; + + border-width: 0; + border-radius: 8px; + + width: ${BASEMAP_ICON_WIDTH}; + padding: 4px; + + ${({ active = false }) => + active && + css` + background-color: ${({ theme }) => theme.colors.mainHiglightColor}; + `} +`; + +const BasemapCustomIcon = styled.div` + display: flex; + position: relative; + justify-content: center; + align-items: center; + height: ${BASEMAP_ICON_HEIGHT}; + width: ${BASEMAP_ICON_WIDTH}; + margin: 0; + border-width: 0; + border-radius: 8px; + background-color: ${({ theme }) => theme.colors.customIconBackground}; +`; + +const BasemapIcon = styled.div<{ + icon: string; +}>` + display: flex; + position: relative; + height: ${BASEMAP_ICON_HEIGHT}; + width: ${BASEMAP_ICON_WIDTH}; + margin: 0; + background-image: ${({ icon }) => `url(${icon})`}; + background-size: cover; + background-repeat: no-repeat; + border-width: 0; + border-radius: 8px; +`; + +const BasemapImageName = styled.div` + flex-direction: column; + justify-content: center; + align-items: center; + + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 17px; + color: ${({ theme }) => theme.colors.fontColor}; + + margin: 4px 0 0 0; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +`; + +const OptionsButton = styled.div` + position: absolute; + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + top: 6px; + right: 6px; + width: 24px; + height: 24px; + cursor: pointer; + + &:hover { + background: ${({ theme }) => theme.colors.mainDimColor}; + } +`; + +interface BasemapListPanelProps { + group: string; +} + +export const BasemapListPanel: FC = ({ group }) => { + const theme = useTheme(); + const dispatch = useAppDispatch(); + + const baseMapArray = useAppSelector((state) => + selectBaseMapsByGroup(state, group) + ); + const baseMapPicked = useAppSelector(selectSelectedBaseMap); + + const [optionsMapId, setOptionsMapId] = useState(""); + const [mapToDeleteId, setMapToDeleteId] = useState(""); + + return ( + <> + + {group} + + {baseMapArray.map((item) => { + const basemapIcon = basemapIcons[item.iconId]; + const iconUrl = basemapIcon?.iconUrl; + const IconComponent = basemapIcon?.IconComponent; + return ( + { + dispatch(setSelectedBaseMap(item.id)); + }} + > + {iconUrl && ( + + )} + + {IconComponent && ( + + + + )} + + {item.name || ""} + {item.custom && ( + { + setMapToDeleteId(optionsMapId); + setOptionsMapId(""); + }} + /> + } + containerStyle={{ zIndex: "2" }} + onClickOutside={() => { + setOptionsMapId(""); + }} + > + { + event.stopPropagation(); + setOptionsMapId(item.id); + }} + > + + + + )} + + ); + })} + + + {mapToDeleteId && ( + { + setMapToDeleteId(""); + }} + onDeleteHandler={() => { + dispatch(deleteBaseMap(mapToDeleteId)); + setMapToDeleteId(""); + }} + > + Delete map? + + )} + + ); +}; diff --git a/src/components/layers-panel/basemap-options-menu/basemap-options-menu.tsx b/src/components/layers-panel/basemap-options-menu/basemap-options-menu.tsx index cff64b36..ba77540e 100644 --- a/src/components/layers-panel/basemap-options-menu/basemap-options-menu.tsx +++ b/src/components/layers-panel/basemap-options-menu/basemap-options-menu.tsx @@ -3,10 +3,6 @@ import styled from "styled-components"; import { color_accent_primary } from "../../../constants/colors"; import DeleteIcon from "../../../../public/icons/delete.svg"; -interface BaseMapOptionsMenuProps { - onDeleteBasemap: () => void; -} - const MapSettingsItem = styled.div<{ customColor?: string; opacity?: number; @@ -16,8 +12,7 @@ const MapSettingsItem = styled.div<{ font-size: 16px; line-height: 19px; padding: 10px 0px; - color: ${({ theme, customColor }) => - customColor ?? theme.colors.fontColor}; + color: ${({ theme, customColor }) => customColor ?? theme.colors.fontColor}; opacity: ${({ opacity = 1 }) => opacity}; display: flex; gap: 10px; @@ -41,19 +36,28 @@ const SettingsMenuContainer = styled.div` color: ${({ theme }) => theme.colors.fontColor}; `; +interface BaseMapOptionsMenuProps { + onDeleteBasemap: () => void; +} + export const BaseMapOptionsMenu = ({ onDeleteBasemap, -}: BaseMapOptionsMenuProps) => ( - - - - - - Delete map - - -); +}: BaseMapOptionsMenuProps) => { + return ( + + { + event.stopPropagation(); + onDeleteBasemap(); + }} + > + + + + Delete map + + + ); +}; diff --git a/src/components/layers-panel/insert-panel/insert-panel.spec.tsx b/src/components/layers-panel/insert-panel/insert-panel.spec.tsx index 84f56139..fd20dc26 100644 --- a/src/components/layers-panel/insert-panel/insert-panel.spec.tsx +++ b/src/components/layers-panel/insert-panel/insert-panel.spec.tsx @@ -3,12 +3,18 @@ import userEvent from "@testing-library/user-event"; import { renderWithThemeProviders } from "../../../utils/testing-utils/render-with-theme"; import { InsertPanel } from "./insert-panel"; import { setupStore } from "../../../redux/store"; +import { BaseMapGroup } from "../../../types"; + import "@testing-library/jest-dom"; const onInsertMock = jest.fn(); const onCancelMock = jest.fn(); -const callRender = (renderFunc, props = {}, store = setupStore()): RenderResult => { +const callRender = ( + renderFunc, + props = {}, + store = setupStore() +): RenderResult => { return renderFunc( { expect(container).toBeInTheDocument(); expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.queryByText("Basemap Provider")).not.toBeInTheDocument(); + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("URL")).toBeInTheDocument(); + expect(screen.getByText("Token")).toBeInTheDocument(); + expect(screen.getByText("Cancel")).toBeInTheDocument(); + expect(screen.getByText("Insert")).toBeInTheDocument(); + }); + + it("Should render insert panel for BaseMaps", () => { + const { container } = callRender(renderWithThemeProviders, { + groups: [BaseMapGroup.Maplibre], + }); + + expect(container).toBeInTheDocument(); + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.getByText("Basemap Provider")).toBeInTheDocument(); expect(screen.getByText("Name")).toBeInTheDocument(); expect(screen.getByText("URL")).toBeInTheDocument(); expect(screen.getByText("Token")).toBeInTheDocument(); diff --git a/src/components/layers-panel/insert-panel/insert-panel.tsx b/src/components/layers-panel/insert-panel/insert-panel.tsx index 51fc18d8..cfd4ffe2 100644 --- a/src/components/layers-panel/insert-panel/insert-panel.tsx +++ b/src/components/layers-panel/insert-panel/insert-panel.tsx @@ -6,6 +6,7 @@ import { FetchingStatus, type LayoutProps, TilesetType, + BaseMapGroup, } from "../../../types"; import { getCurrentLayoutProperty, @@ -13,6 +14,8 @@ import { } from "../../../utils/hooks/layout"; import { ActionButton } from "../../action-button/action-button"; import { InputText } from "./input-text/input-text"; +import { InputDropdown } from "../../input-dropdown/input-dropdown"; + import { getTilesetType } from "../../../utils/url-utils"; import { LoadingSpinner } from "../../loading-spinner/loading-spinner"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; @@ -24,13 +27,17 @@ import { const NO_NAME_ERROR = "Please enter name"; const INVALID_URL_ERROR = "Invalid URL"; +export interface CustomLayerData { + name: string; + url: string; + token?: string; + group?: BaseMapGroup; +} + interface InsertLayerProps { title: string; - onInsert: (object: { - name: string; - url: string; - token?: string; - }) => Promise | void; + groups?: string[]; + onInsert: (object: CustomLayerData) => Promise | void; onCancel: () => void; children?: React.ReactNode; } @@ -93,10 +100,12 @@ const SpinnerContainer = styled.div` export const InsertPanel = ({ title, + groups, onInsert, onCancel, children = null, }: InsertLayerProps) => { + const [group, setGroup] = useState(BaseMapGroup.Maplibre); const [name, setName] = useState(""); const [url, setUrl] = useState(""); const [token, setToken] = useState(""); @@ -128,7 +137,12 @@ export const InsertPanel = ({ } if (isFormValid) { - void onInsert({ name: name || layerNames[url]?.name, url, token }); + void onInsert({ + name: name || layerNames[url]?.name, + url, + token, + group: groups ? group : undefined, + }); } }; @@ -162,6 +176,10 @@ export const InsertPanel = ({ const { name, value } = event.target; switch (name) { + case "BasemapProvider": + setGroup(value); + setNameError(""); + break; case "Name": setName(value); setNameError(""); @@ -190,6 +208,13 @@ export const InsertPanel = ({
+ {groups && ( + + )} {
{children}
)); DeleteConfirmationMock.mockImplementation(() => ( -
Delete Conformation
+
Delete Confirmation
)); LayerOptionsMenuMock.mockImplementation(() =>
Layers Options
); }); @@ -150,7 +150,7 @@ describe("Layers Control Panel", () => { }); }); - it("Should render conformation panel", () => { + it("Should render confirmation panel", () => { callRender(renderWithThemeProviders, { layers: [ { id: "first", name: "first name", mapUrl: "https://first-url.com" }, @@ -159,7 +159,7 @@ describe("Layers Control Panel", () => { ], }); - expect(screen.getByText("Delete Conformation")).toBeInTheDocument(); + expect(screen.getByText("Delete Confirmation")).toBeInTheDocument(); const { onDeleteHandler, onKeepHandler } = DeleteConfirmationMock.mock.lastCall[0]; diff --git a/src/components/layers-panel/layers-panel.spec.tsx b/src/components/layers-panel/layers-panel.spec.tsx index 1f96b417..57d0c367 100644 --- a/src/components/layers-panel/layers-panel.spec.tsx +++ b/src/components/layers-panel/layers-panel.spec.tsx @@ -11,13 +11,13 @@ import { LayersPanel } from "./layers-panel"; // Mocked compnents import { LayersControlPanel } from "./layers-control-panel"; import { MapOptionPanel } from "./map-options-panel"; -import { InsertPanel } from "./insert-panel/insert-panel"; +import { InsertPanel, type CustomLayerData } from "./insert-panel/insert-panel"; import { WarningPanel } from "./warning/warning-panel"; import { LayerSettingsPanel } from "./layer-settings-panel"; import { load } from "@loaders.gl/core"; -import { PageId } from "../../types"; +import { BaseMapGroup, PageId } from "../../types"; import { setupStore } from "../../redux/store"; -import { selectSelectedBaseMapId } from "../../redux/slices/base-maps-slice"; +import { selectSelectedBaseMap } from "../../redux/slices/base-maps-slice"; import "@testing-library/jest-dom"; jest.mock("@loaders.gl/core", () => ({ @@ -275,16 +275,18 @@ describe("Layers Panel", () => { const { onInsert } = InsertPanelMock.mock.lastCall[0]; // Click insert baseMap - act(() => { - onInsert({ + await act(async () => { + const customMap: CustomLayerData = { name: "test-basemap", url: "https://test-base-map.url", token: "", - }); + group: BaseMapGroup.Maplibre, + }; + await onInsert(customMap); }); const state = store.getState(); - const baseMapId = selectSelectedBaseMapId(state); + const baseMapId = selectSelectedBaseMap(state)?.id; expect(baseMapId).toEqual("https://test-base-map.url"); }); diff --git a/src/components/layers-panel/layers-panel.tsx b/src/components/layers-panel/layers-panel.tsx index c6b2498c..cd9c1223 100644 --- a/src/components/layers-panel/layers-panel.tsx +++ b/src/components/layers-panel/layers-panel.tsx @@ -15,9 +15,10 @@ import { type Bookmark, type PageId, type ComparisonSideMode, + BaseMapGroup, } from "../../types"; import { CloseButton } from "../close-button/close-button"; -import { InsertPanel } from "./insert-panel/insert-panel"; +import { InsertPanel, type CustomLayerData } from "./insert-panel/insert-panel"; import { LayersControlPanel } from "./layers-control-panel"; import { ArcGisControlPanel } from "./arcgis-control-panel"; import { MapOptionPanel } from "./map-options-panel"; @@ -59,12 +60,6 @@ interface TabProps { $active: boolean; } -interface CustomItem { - name: string; - url: string; - token?: string; -} - const Tab = styled.div` position: relative; font-style: normal; @@ -211,11 +206,7 @@ export const LayersPanel = ({ setShowExistedError(false); }); - const handleInsertLayer = (layer: { - name: string; - url: string; - token?: string; - }) => { + const handleInsertLayer = (layer: CustomLayerData) => { const existedLayer = layers.some( (exisLayer) => exisLayer.url.trim() === layer.url.trim() ); @@ -272,11 +263,7 @@ export const LayersPanel = ({ }; // TODO Add loader to show webscene loading - const handleInsertScene = async (scene: { - name: string; - url: string; - token?: string; - }): Promise => { + const handleInsertScene = async (scene: CustomLayerData): Promise => { scene.url = convertUrlToRestFormat(scene.url); const existedScene = layers.some( @@ -353,17 +340,20 @@ export const LayersPanel = ({ } }; - const handleInsertMap = (map: CustomItem): void => { + const handleInsertMap = (map: CustomLayerData): void => { const id = map.url.replace(/" "/g, "-"); - const newMap: BaseMap = { - id, - mapUrl: map.url, - name: map.name, - token: map.token, - custom: true, - }; - - dispatch(addBaseMap(newMap)); + if (map.group !== undefined) { + const newMap: BaseMap = { + id, + mapUrl: map.url, + name: map.name, + token: map.token, + iconId: "Custom", + custom: true, + group: map.group, + }; + dispatch(addBaseMap(newMap)); + } setShowInsertMapPanel(false); }; @@ -418,6 +408,7 @@ export const LayersPanel = ({ )} {tab === Tabs.MapOptions && ( { setShowInsertMapPanel(true); }} @@ -550,6 +541,7 @@ export const LayersPanel = ({ { handleInsertMap(map); }} diff --git a/src/components/layers-panel/map-options-panel.spec.tsx b/src/components/layers-panel/map-options-panel.spec.tsx index dbf56d78..e6b1b71e 100644 --- a/src/components/layers-panel/map-options-panel.spec.tsx +++ b/src/components/layers-panel/map-options-panel.spec.tsx @@ -1,60 +1,37 @@ -import { act, screen } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { renderWithThemeProviders } from "../../utils/testing-utils/render-with-theme"; import { MapOptionPanel } from "./map-options-panel"; +import { PageId } from "../../types"; -import { BaseMapListItem } from "./base-map-list-item/base-map-list-item"; import { ActionIconButton } from "../action-icon-button/action-icon-button"; -import { DeleteConfirmation } from "./delete-confirmation"; -import { BaseMapOptionsMenu } from "./basemap-options-menu/basemap-options-menu"; import { setupStore } from "../../redux/store"; -import { - addBaseMap, - selectBaseMaps, - selectSelectedBaseMapId, -} from "../../redux/slices/base-maps-slice"; -jest.mock("@loaders.gl/i3s", () => { - return jest.fn().mockImplementation(() => { - return null; - }); -}); -jest.mock("./base-map-list-item/base-map-list-item"); jest.mock("../action-icon-button/action-icon-button"); -jest.mock("./delete-confirmation"); -jest.mock("./basemap-options-menu/basemap-options-menu"); -const BaseMapListItemMock = BaseMapListItem as unknown as jest.Mocked; const PlusButtonMock = ActionIconButton as unknown as jest.Mocked; -const DeleteConfirmationMock = - DeleteConfirmation as unknown as jest.Mocked; -const BaseMapOptionsMenuMock = - BaseMapOptionsMenu as unknown as jest.Mocked; beforeAll(() => { - BaseMapListItemMock.mockImplementation((props) => ( -
{`BaseMap ListItem-${props.id}`}
- )); PlusButtonMock.mockImplementation(({ children, onClick }) => (
{children}
)); - DeleteConfirmationMock.mockImplementation(() => ( -
Delete Conformation
- )); - BaseMapOptionsMenuMock.mockImplementation(() =>
BaseMap Options
); }); const onInsertBaseMapMock = jest.fn(); const callRender = (renderFunc, props = {}, store = setupStore()) => { return renderFunc( - , + , store ); }; describe("Map Options Panel", () => { - it("Should render without basemaps", async () => { + it("Should render panel", async () => { const store = setupStore(); const { container } = callRender( renderWithThemeProviders, @@ -67,47 +44,24 @@ describe("Map Options Panel", () => { // Insert Button should be present const insertBaseMapButton = screen.getByText("Insert Base Map"); expect(insertBaseMapButton).toBeInTheDocument(); - // Should be able to click on insert button - await userEvent.click(insertBaseMapButton); - expect(onInsertBaseMapMock).toHaveBeenCalled(); }); - it("Should render base maps", () => { + it("Should click Insert button", async () => { const store = setupStore(); - store.dispatch( - addBaseMap({ - id: "first", - name: "first name", - mapUrl: "https://first-url.com", - }) - ); - store.dispatch( - addBaseMap({ - id: "second", - name: "second name", - mapUrl: "https://second-url.com", - }) - ); const { container } = callRender( renderWithThemeProviders, undefined, store ); expect(container).toBeInTheDocument(); - - expect(screen.getByText("BaseMap ListItem-first")).toBeInTheDocument(); - expect(screen.getByText("BaseMap ListItem-second")).toBeInTheDocument(); + const insertBaseMapButton = screen.getByText("Insert Base Map"); + // Should be able to click on insert button + await userEvent.click(insertBaseMapButton); + expect(onInsertBaseMapMock).toHaveBeenCalled(); }); - it("Should be able to call functions", () => { + it("Should render base maps", () => { const store = setupStore(); - store.dispatch( - addBaseMap({ - id: "first", - name: "first name", - mapUrl: "https://first-url.com", - }) - ); const { container } = callRender( renderWithThemeProviders, undefined, @@ -115,77 +69,11 @@ describe("Map Options Panel", () => { ); expect(container).toBeInTheDocument(); - expect(screen.getByText("BaseMap ListItem-first")).toBeInTheDocument(); - - const { onOptionsClick, onMapsSelect, onClickOutside } = - BaseMapListItemMock.mock.lastCall[0]; - - act(() => { - onOptionsClick(); - }); - - act(() => { - onMapsSelect(); - }); - - const state = store.getState(); - const baseMapId = selectSelectedBaseMapId(state); - expect(baseMapId).toEqual("first"); - - act(() => { - onClickOutside(); - }); - }); - - it("Should render conformation panel", () => { - const store = setupStore(); - store.dispatch( - addBaseMap({ - id: "first", - name: "first name", - mapUrl: "https://first-url.com", - }) - ); - store.dispatch( - // Candidate to delete - addBaseMap({ - id: "", - name: "second name", - mapUrl: "https://second-url.com", - }) - ); - callRender(renderWithThemeProviders, undefined, store); - expect(screen.getByText("Delete Conformation")).toBeInTheDocument(); - - const { onDeleteHandler, onKeepHandler } = - DeleteConfirmationMock.mock.lastCall[0]; - - act(() => { - onDeleteHandler(); - }); - - const state = store.getState(); - const baseMap = selectBaseMaps(state); - expect(baseMap).toEqual([ - { - id: "Dark", - mapUrl: - "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json", - name: "Dark", - }, - { - id: "Light", - mapUrl: - "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", - name: "Light", - }, - { id: "Terrain", mapUrl: "", name: "Terrain" }, - { id: "ArcGis", name: "ArcGis", mapUrl: "" }, - { id: "first", mapUrl: "https://first-url.com", name: "first name" }, - ]); - - act(() => { - onKeepHandler(); - }); + expect(screen.getByText("Dark")).toBeInTheDocument(); + expect(screen.getByText("Light")).toBeInTheDocument(); + expect(screen.getByText("Light gray")).toBeInTheDocument(); + expect(screen.getByText("Dark gray")).toBeInTheDocument(); + expect(screen.getByText("Streets")).toBeInTheDocument(); + expect(screen.getByText("Streets(night)")).toBeInTheDocument(); }); }); diff --git a/src/components/layers-panel/map-options-panel.tsx b/src/components/layers-panel/map-options-panel.tsx index b3e06b1b..09e4854d 100644 --- a/src/components/layers-panel/map-options-panel.tsx +++ b/src/components/layers-panel/map-options-panel.tsx @@ -1,32 +1,16 @@ -import { useState, Fragment } from "react"; import styled from "styled-components"; -import { BaseMapListItem } from "./base-map-list-item/base-map-list-item"; import PlusIcon from "../../../public/icons/plus.svg"; import { ActionIconButton } from "../action-icon-button/action-icon-button"; -import { DeleteConfirmation } from "./delete-confirmation"; -import { SelectionState, ButtonSize } from "../../types"; -import { BaseMapOptionsMenu } from "./basemap-options-menu/basemap-options-menu"; -import { useAppDispatch, useAppSelector } from "../../redux/hooks"; -import { - selectBaseMaps, - deleteBaseMaps, - selectSelectedBaseMapId, - setSelectedBaseMaps, -} from "../../redux/slices/base-maps-slice"; - -interface MapOptionPanelProps { - insertBaseMap: () => void; -} +import { ButtonSize, PageId, BaseMapGroup } from "../../types"; +import { BasemapListPanel } from "../layers-panel/basemap-list-panel/basemap-list-panel"; const MapOptionTitle = styled.div` width: 100; - height: 19px; font-style: normal; font-weight: 700; font-size: 16px; line-height: 19px; color: ${({ theme }) => theme.colors.fontColor}; - margin-bottom: 24px; `; const MapOptionsContainer = styled.div` @@ -35,12 +19,8 @@ const MapOptionsContainer = styled.div` width: 100%; overflow: auto; position: relative; -`; - -const MapList = styled.div` - display: flex; - flex-direction: column; - width: 100%; + gap: 16px; + margin-bottom: 8px; `; const InsertButtons = styled.div` @@ -49,71 +29,33 @@ const InsertButtons = styled.div` row-gap: 8px; `; -export const MapOptionPanel = ({ insertBaseMap }: MapOptionPanelProps) => { - const dispatch = useAppDispatch(); - const baseMaps = useAppSelector(selectBaseMaps); - const selectedBaseMapId = useAppSelector(selectSelectedBaseMapId); - const [settingsMapId, setSettingsMapId] = useState(""); - const [showMapSettings, setShowMapSettings] = useState(false); - const [mapToDeleteId, setMapToDeleteId] = useState(""); +interface MapOptionPanelProps { + pageId: PageId; + insertBaseMap: () => void; +} +export const MapOptionPanel = ({ + pageId, + insertBaseMap, +}: MapOptionPanelProps) => { return ( Base Map - - {baseMaps.map((baseMap) => { - const isMapSelected = selectedBaseMapId === baseMap.id; - return ( - - { - setShowMapSettings(true); - setSettingsMapId(baseMap.id); - }} - onMapsSelect={() => { - dispatch(setSelectedBaseMaps(baseMap.id)); - }} - isOptionsPanelOpen={ - showMapSettings && settingsMapId === baseMap.id - } - optionsContent={ - { - setMapToDeleteId(settingsMapId); - setShowMapSettings(false); - }} - /> - } - onClickOutside={() => { - setShowMapSettings(false); - setSettingsMapId(""); - }} - /> - {mapToDeleteId === baseMap.id && ( - { setMapToDeleteId(""); }} - onDeleteHandler={() => { - dispatch(deleteBaseMaps(settingsMapId)); - setMapToDeleteId(""); - }} - > - Delete map? - - )} - - ); - })} - + + {pageId !== PageId.comparison && ( + + )} + {pageId !== PageId.comparison && ( + + )} + - + Insert Base Map diff --git a/src/constants/map-styles.ts b/src/constants/map-styles.ts index 91b8e714..dd10be97 100644 --- a/src/constants/map-styles.ts +++ b/src/constants/map-styles.ts @@ -1,22 +1,86 @@ -import { type BaseMap } from "../types"; +import { type FC } from "react"; +import CustomMap from "../../public/icons/custom-map.svg"; + +import MaplibreDarkMap from "../../public/icons/basemaps/maplibre-dark.png"; +import MaplibreLightMap from "../../public/icons/basemaps/maplibre-light.png"; + +import TerrainMap from "../../public/icons/basemaps/terrain.png"; + +import ArcGisDarkGrayMap from "../../public/icons/basemaps/arcgis-dark-gray.png"; +import ArcGisLightGrayMap from "../../public/icons/basemaps/arcgis-light-gray.png"; +import ArcGisStreetsDarkMap from "../../public/icons/basemaps/arcgis-streets-dark.png"; +import ArcGisStreetsMap from "../../public/icons/basemaps/arcgis-streets.png"; +import { type BaseMap, BaseMapGroup } from "../types"; + +interface BasemapIcon { + IconComponent?: FC<{ fill: string }>; + iconUrl?: string; +} + +export const basemapIcons: Record = { + Dark: { iconUrl: MaplibreDarkMap }, + Light: { iconUrl: MaplibreLightMap }, + Terrain: { iconUrl: TerrainMap }, + ArcGisDarkGray: { iconUrl: ArcGisDarkGrayMap }, + ArcGisLightGray: { iconUrl: ArcGisLightGrayMap }, + ArcGisStreetsDark: { iconUrl: ArcGisStreetsDarkMap }, + ArcGisStreets: { iconUrl: ArcGisStreetsMap }, + + Custom: { IconComponent: CustomMap }, +}; export const BASE_MAPS: BaseMap[] = [ { id: "Dark", name: "Dark", + iconId: "Dark", + group: BaseMapGroup.Maplibre, mapUrl: "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json", }, { id: "Light", name: "Light", + iconId: "Light", + group: BaseMapGroup.Maplibre, mapUrl: "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", }, - { id: "Terrain", name: "Terrain", mapUrl: "" }, + + { + id: "Terrain", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", + mapUrl: "", + }, + + { + id: "gray-vector", + name: "Light gray", + group: BaseMapGroup.ArcGIS, + iconId: "ArcGisLightGray", + mapUrl: "", + }, + { + id: "dark-gray-vector", + name: "Dark gray", + group: BaseMapGroup.ArcGIS, + iconId: "ArcGisDarkGray", + mapUrl: "", + }, + { + id: "streets-vector", + name: "Streets", + group: BaseMapGroup.ArcGIS, + iconId: "ArcGisStreets", + mapUrl: "", + }, { - id: "ArcGis", - name: "ArcGis", + id: "streets-night-vector", + name: "Streets(night)", + group: BaseMapGroup.ArcGIS, + iconId: "ArcGisStreetsDark", mapUrl: "", }, ]; diff --git a/src/hooks/use-arcgis-hook/use-arcgis-hook.spec.ts b/src/hooks/use-arcgis-hook/use-arcgis-hook.spec.ts deleted file mode 100644 index 86413428..00000000 --- a/src/hooks/use-arcgis-hook/use-arcgis-hook.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { renderHook } from "@testing-library/react-hooks"; -import { loadArcGISModules } from "@deck.gl/arcgis"; -import { useArcgis } from "./use-arcgis-hook"; - -jest.mock("@deck.gl/arcgis", () => { - return { - loadArcGISModules: jest.fn().mockReturnValue(Promise.resolve({})), - }; -}); - -describe("ArcGis Hook", () => { - it("Should be able to call ArcGis hook", async () => { - const hook = renderHook(() => - useArcgis( - { current: null }, - { main: { longitude: 1, latitude: 2, pitch: 3, bearing: 4, zoom: 5 } }, - null - ) - ); - const renderer = hook.result.current; - expect(renderer).toBeNull(); - expect(loadArcGISModules).not.toHaveBeenCalled(); - }); -}); diff --git a/src/hooks/use-arcgis-hook/use-arcgis-hook.spec.tsx b/src/hooks/use-arcgis-hook/use-arcgis-hook.spec.tsx new file mode 100644 index 00000000..d4071ddb --- /dev/null +++ b/src/hooks/use-arcgis-hook/use-arcgis-hook.spec.tsx @@ -0,0 +1,45 @@ +import { loadArcGISModules } from "@deck.gl/arcgis"; + +import type { PropsWithChildren } from "react"; +import { Provider } from "react-redux"; +import { renderHook } from "@testing-library/react-hooks"; +import { useArcgis } from "./use-arcgis-hook"; +import { type AppStore, setupStore } from "../../redux/store"; + +const getWrapper = (store: AppStore) => { + // eslint-disable-next-line @typescript-eslint/ban-types + function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element { + return {children}; + } + return Wrapper; +}; + +jest.mock("@deck.gl/arcgis", () => { + return { + loadArcGISModules: jest.fn().mockReturnValue(Promise.resolve({})), + }; +}); + +describe("ArcGis Hook", () => { + it("Should be able to call ArcGis hook", async () => { + const store = setupStore(); + const wrapper = getWrapper(store); + + const hook = renderHook( + () => + useArcgis( + { current: null }, + { + main: { longitude: 1, latitude: 2, pitch: 3, bearing: 4, zoom: 5 }, + }, + null + ), + { + wrapper, + } + ); + const renderer = hook.result.current; + expect(renderer).toBeNull(); + expect(loadArcGISModules).not.toHaveBeenCalled(); + }); +}); diff --git a/src/hooks/use-arcgis-hook/use-arcgis-hook.ts b/src/hooks/use-arcgis-hook/use-arcgis-hook.ts index 51ae0daf..16a5422d 100644 --- a/src/hooks/use-arcgis-hook/use-arcgis-hook.ts +++ b/src/hooks/use-arcgis-hook/use-arcgis-hook.ts @@ -1,4 +1,7 @@ import { loadArcGISModules } from "@deck.gl/arcgis"; +import { selectSelectedBaseMap } from "../../redux/slices/base-maps-slice"; +import { useAppSelector } from "../../redux/hooks"; +import { BaseMapGroup } from "../../types"; import { useState, useEffect, type MutableRefObject, useRef } from "react"; export function useArcgis( @@ -10,6 +13,15 @@ export function useArcgis( const [sceneView, setSceneView] = useState(null); const { longitude, latitude, pitch, bearing, zoom } = viewState.main; const isLoadingRef = useRef(false); + const selectedBaseMap = useAppSelector(selectSelectedBaseMap); + const selectedBaseMapId = selectedBaseMap?.id; + + useEffect(() => { + if (sceneView && selectedBaseMap?.group === BaseMapGroup.ArcGIS) { + // update Basemap Style + (sceneView as any).map.basemap = selectedBaseMapId; + } + }, [selectedBaseMapId]); // sceneView, useEffect(() => { if (!sceneView) { @@ -25,7 +37,16 @@ export function useArcgis( }, { animate: true } ); - }, [longitude, latitude, zoom, bearing, pitch, sceneView]); + }, [ + longitude, + latitude, + zoom, + bearing, + pitch, + sceneView, + (sceneView as any)?.map.basemap, + selectedBaseMapId, + ]); useEffect(() => { if (mapContainer.current == null) { @@ -47,7 +68,7 @@ export function useArcgis( const sceneView = new SceneView({ container: mapContainer.current, map: new ArcGISMap({ - basemap: "dark-gray-vector", + basemap: selectedBaseMapId, }), environment: { atmosphereEnabled: false, diff --git a/src/hooks/use-deckgl-hook/use-deckgl-hook.ts b/src/hooks/use-deckgl-hook/use-deckgl-hook.ts index 50adc619..420e6966 100644 --- a/src/hooks/use-deckgl-hook/use-deckgl-hook.ts +++ b/src/hooks/use-deckgl-hook/use-deckgl-hook.ts @@ -25,10 +25,7 @@ import { selectBoundingVolumeColorMode, selectBoundingVolumeType, } from "../../redux/slices/debug-options-slice"; -import { - selectBaseMaps, - selectSelectedBaseMapId, -} from "../../redux/slices/base-maps-slice"; +import { selectSelectedBaseMap } from "../../redux/slices/base-maps-slice"; import { selectViewState } from "../../redux/slices/view-state-slice"; import type { Tileset3D } from "@loaders.gl/tiles"; @@ -51,9 +48,7 @@ export function useDeckGl( const tileColorMode = useAppSelector(selectTileColorMode); const boundingVolumeColorMode = useAppSelector(selectBoundingVolumeColorMode); const wireframe = useAppSelector(selectWireframe); - const baseMaps = useAppSelector(selectBaseMaps); - const selectedBaseMapId = useAppSelector(selectSelectedBaseMapId); - const selectedBaseMap = baseMaps.find((map) => map.id === selectedBaseMapId); + const selectedBaseMap = useAppSelector(selectSelectedBaseMap); const showTerrain = selectedBaseMap?.id === "Terrain"; const mapStyle = selectedBaseMap?.mapUrl; const boundingVolume = useAppSelector(selectBoundingVolume); diff --git a/src/pages/comparison/comparison.tsx b/src/pages/comparison/comparison.tsx index 278f8f96..f703a325 100644 --- a/src/pages/comparison/comparison.tsx +++ b/src/pages/comparison/comparison.tsx @@ -14,6 +14,7 @@ import { type LayerViewState, type StatsData, PageId, + BaseMapGroup, type LayoutProps, } from "../../types"; @@ -35,9 +36,7 @@ import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { setDragMode } from "../../redux/slices/drag-mode-slice"; import { setColorsByAttrubute } from "../../redux/slices/symbolization-slice"; import { - deleteBaseMaps, - setInitialBaseMaps, - selectSelectedBaseMapId, + selectSelectedBaseMap, } from "../../redux/slices/base-maps-slice"; import { selectViewState, @@ -46,10 +45,6 @@ import { import { WarningPanel } from "../../components/layers-panel/warning/warning-panel"; import { CenteredContainer } from "../../components/common"; -interface ComparisonPageProps { - mode: ComparisonMode; -} - const INITIAL_VIEW_STATE = { main: { longitude: 0, @@ -92,12 +87,16 @@ const Devider = styled.div` background-color: ${color_brand_primary}; `; +interface ComparisonPageProps { + mode: ComparisonMode; +} + export const Comparison = ({ mode }: ComparisonPageProps) => { const loadManagerRef = useRef( new ComparisonLoadManager() ); - const selectedBaseMapId = useAppSelector(selectSelectedBaseMapId); + const selectedBaseMap = useAppSelector(selectSelectedBaseMap); const globalViewState = useAppSelector(selectViewState); const [layersLeftSide, setLayersLeftSide] = useState([]); const [layersRightSide, setLayersRightSide] = useState([]); @@ -118,10 +117,7 @@ export const Comparison = ({ mode }: ComparisonPageProps) => { ); compareButtonModeRef.current = compareButtonMode; - const [disableButton, setDisableButton] = useState([ - true, - true, - ]); + const [disableButton, setDisableButton] = useState([true, true]); const [leftSideLoaded, setLeftSideLoaded] = useState(true); const [hasBeenCompared, setHasBeenCompared] = useState(false); const [showBookmarksPanel, setShowBookmarksPanel] = useState(false); @@ -151,10 +147,6 @@ export const Comparison = ({ mode }: ComparisonPageProps) => { dispatch(setColorsByAttrubute(null)); dispatch(setDragMode(DragMode.pan)); dispatch(setViewState(INITIAL_VIEW_STATE)); - dispatch(deleteBaseMaps("Terrain")); - return () => { - dispatch(setInitialBaseMaps()); - }; }, [mode]); useEffect(() => { @@ -463,18 +455,22 @@ export const Comparison = ({ mode }: ComparisonPageProps) => { loadNumber={loadNumber} buildingExplorerOpened={buildingExplorerOpenedLeft} pointToTileset={pointToTileset} - onChangeLayers={(layers, activeIds) => { onChangeLayersHandler(layers, activeIds, ComparisonSideMode.left); } - } + onChangeLayers={(layers, activeIds) => { + onChangeLayersHandler(layers, activeIds, ComparisonSideMode.left); + }} onLoadingStateChange={disableButtonHandlerLeft} onTilesetLoaded={(stats: StatsMap) => { loadManagerRef.current.resolveLeftSide(stats); setLeftSideLoaded(true); }} - onBuildingExplorerOpened={(opened) => { setBuildingExplorerOpenedLeft(opened); } - } + onBuildingExplorerOpened={(opened) => { + setBuildingExplorerOpenedLeft(opened); + }} onShowBookmarksChange={onBookmarkClick} onInsertBookmarks={updateBookmarks} - onUpdateSublayers={(sublayers) => { setSublayersLeftSide(sublayers); }} + onUpdateSublayers={(sublayers) => { + setSublayersLeftSide(sublayers); + }} /> { onSelectBookmark={onSelectBookmarkHandler} onCollapsed={onCloseBookmarkPanel} onDownloadBookmarks={onDownloadBookmarksHandler} - onClearBookmarks={() => { setBookmarks([]); }} + onClearBookmarks={() => { + setBookmarks([]); + }} onBookmarksUploaded={onBookmarksUploadedHandler} onDeleteBookmark={onDeleteBookmarkHandler} onEditBookmark={onEditBookmarkHandler} @@ -536,14 +534,16 @@ export const Comparison = ({ mode }: ComparisonPageProps) => { (mode === ComparisonMode.withinLayer && buildingExplorerOpenedLeft) } pointToTileset={pointToTileset} - onChangeLayers={(layers, activeIds) => { onChangeLayersHandler(layers, activeIds, ComparisonSideMode.right); } - } + onChangeLayers={(layers, activeIds) => { + onChangeLayersHandler(layers, activeIds, ComparisonSideMode.right); + }} onLoadingStateChange={disableButtonHandlerRight} onTilesetLoaded={(stats: StatsMap) => { loadManagerRef.current.resolveRightSide(stats); }} - onBuildingExplorerOpened={(opened) => { setBuildingExplorerOpenedRight(opened); } - } + onBuildingExplorerOpened={(opened) => { + setBuildingExplorerOpenedRight(opened); + }} onShowBookmarksChange={onBookmarkClick} /> @@ -554,14 +554,16 @@ export const Comparison = ({ mode }: ComparisonPageProps) => { onZoomOut={onZoomOut} onCompassClick={onCompassClick} bottom={layout === Layout.Mobile ? 8 : 16} - isDragModeVisible={selectedBaseMapId !== "ArcGis"} + isDragModeVisible={selectedBaseMap?.group !== BaseMapGroup.ArcGIS} /> )} {wrongBookmarkPageId && ( { setWrongBookmarkPageId(null); }} + onConfirm={() => { + setWrongBookmarkPageId(null); + }} /> )} diff --git a/src/pages/comparison/e2e.comparison.spec.ts b/src/pages/comparison/e2e.comparison.spec.ts index 87145228..1c5a71e5 100644 --- a/src/pages/comparison/e2e.comparison.spec.ts +++ b/src/pages/comparison/e2e.comparison.spec.ts @@ -2,7 +2,7 @@ import puppeteer, { type Page } from "puppeteer"; import { checkLayersPanel, - inserAndDeleteLayer, + insertAndDeleteLayer, } from "../../utils/testing-utils/e2e-layers-panel"; import { PageId } from "../../types"; import { configurePage } from "../../utils/testing-utils/configure-tests"; @@ -231,13 +231,13 @@ describe("Compare - Layers Panel Across Layers mode", () => { }); it("Should insert and delete layer", async () => { - await inserAndDeleteLayer( + await insertAndDeleteLayer( page, "#left-layers-panel", "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/Rancho_Mesh_mesh_v17_1/SceneServer/layers/0" ); - await inserAndDeleteLayer( + await insertAndDeleteLayer( page, "#right-layers-panel", "https://fake.layer.url" @@ -391,7 +391,10 @@ describe("Compare - Comparison Params Panel", () => { // Horizontal Line expect( - await page.$eval(`${panelId} > :nth-child(2)`, (node) => (node as HTMLElement).innerText) + await page.$eval( + `${panelId} > :nth-child(2)`, + (node) => (node as HTMLElement).innerText + ) ).toBe(""); // Draco @@ -484,7 +487,10 @@ describe("Compare - Statistics", () => { // Horizontal Line expect( - await page.$eval(`${panelId} > :nth-child(2)`, (node) => (node as HTMLElement).innerText) + await page.$eval( + `${panelId} > :nth-child(2)`, + (node) => (node as HTMLElement).innerText + ) ).toBe(""); }; diff --git a/src/pages/debug-app/debug-app.tsx b/src/pages/debug-app/debug-app.tsx index 657c234d..e393bc84 100644 --- a/src/pages/debug-app/debug-app.tsx +++ b/src/pages/debug-app/debug-app.tsx @@ -15,6 +15,7 @@ import { type MinimapPosition, type TileSelectedColor, PageId, + BaseMapGroup, type TilesetMetadata, } from "../../types"; @@ -22,7 +23,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; // eslint-disable-next-line react/no-deprecated import { render } from "react-dom"; import { lumaStats } from "@luma.gl/core"; -import { type PickingInfo, type InteractionStateChange, type ViewState } from "@deck.gl/core"; +import { + type PickingInfo, + type InteractionStateChange, + type ViewState, +} from "@deck.gl/core"; import { v4 as uuidv4 } from "uuid"; import { type Stats } from "@probe.gl/stats"; @@ -82,14 +87,20 @@ import { } from "../../redux/slices/flattened-sublayers-slice"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { setDragMode } from "../../redux/slices/drag-mode-slice"; -import { setColorsByAttrubute, setFiltersByAttrubute } from "../../redux/slices/symbolization-slice"; +import { + setColorsByAttrubute, + setFiltersByAttrubute, +} from "../../redux/slices/symbolization-slice"; import { resetDebugOptions, setDebugOptions, selectDebugOptions, selectPickable, } from "../../redux/slices/debug-options-slice"; -import { setInitialBaseMaps, selectSelectedBaseMapId } from "../../redux/slices/base-maps-slice"; +import { + setInitialBaseMaps, + selectSelectedBaseMap, +} from "../../redux/slices/base-maps-slice"; import { clearBSLStatisitcsSummary } from "../../redux/slices/i3s-stats-slice"; import { selectViewState, @@ -107,7 +118,7 @@ export const DebugApp = () => { const tilesetRef = useRef(null); const layout = useAppLayout(); const debugOptions = useAppSelector(selectDebugOptions); - const selectedBaseMapId = useAppSelector(selectSelectedBaseMapId); + const selectedBaseMap = useAppSelector(selectSelectedBaseMap); const [normalsDebugData, setNormalsDebugData] = useState(null); const [trianglesPercentage, setTrianglesPercentage] = useState( @@ -139,7 +150,9 @@ export const DebugApp = () => { const [buildingExplorerOpened, setBuildingExplorerOpened] = useState(false); const MapWrapper = - selectedBaseMapId === "ArcGis" ? ArcgisWrapper : DeckGlWrapper; + selectedBaseMap?.group === BaseMapGroup.ArcGIS + ? ArcgisWrapper + : DeckGlWrapper; const [stateUrlViewStateParams, setStateUrlViewStateParams] = useState({}); @@ -244,7 +257,11 @@ export const DebugApp = () => { setColoredTilesMap({}); setSelectedTile(null); dispatch(resetDebugOptions()); - dispatch(setDebugOptions({ minimap: !(selectedBaseMapId === "ArcGis") })); + dispatch( + setDebugOptions({ + minimap: !(selectedBaseMap?.group === BaseMapGroup.ArcGIS), + }) + ); dispatch(clearBSLStatisitcsSummary()); dispatch(setFiltersByAttrubute({ filter: null })); }, [activeLayers, buildingExplorerOpened]); @@ -366,7 +383,9 @@ export const DebugApp = () => { setColoredTilesMap(updatedColoredMap); }; - const handleClearWarnings = () => { setWarnings([]); }; + const handleClearWarnings = () => { + setWarnings([]); + }; const onShowNormals = (tile) => { if (normalsDebugData === null) { @@ -425,8 +444,12 @@ export const DebugApp = () => { onChangeTrianglesPercentage={onChangeTrianglesPercentage} onChangeNormalsLength={onChangeNormalsLength} handleClosePanel={handleCloseTilePanel} - deactiveDebugPanel={() => { setActiveButton(ActiveButton.none); }} - activeDebugPanel={() => { setActiveButton(ActiveButton.debug); }} + deactiveDebugPanel={() => { + setActiveButton(ActiveButton.none); + }} + activeDebugPanel={() => { + setActiveButton(ActiveButton.debug); + }} > {isShowColorPicker && ( { selectedLayerIds={selectedLayerIds} onLayerInsert={onLayerInsertHandler} onLayerSelect={onLayerSelectHandler} - onLayerDelete={(id) => { onLayerDeleteHandler(id); }} - onPointToLayer={(viewState) => { pointToTileset(viewState); }} + onLayerDelete={(id) => { + onLayerDeleteHandler(id); + }} + onPointToLayer={(viewState) => { + pointToTileset(viewState); + }} type={ListItemType.Radio} sublayers={sublayers} onUpdateSublayerVisibility={onUpdateSublayerVisibilityHandler} - onClose={() => { onChangeMainToolsPanelHandler(ActiveButton.options); }} - onBuildingExplorerOpened={(opened) => { setBuildingExplorerOpened(opened); } - } + onClose={() => { + onChangeMainToolsPanelHandler(ActiveButton.options); + }} + onBuildingExplorerOpened={(opened) => { + setBuildingExplorerOpened(opened); + }} /> )} {activeButton === ActiveButton.debug && ( { onChangeMainToolsPanelHandler(ActiveButton.debug); }} + onClose={() => { + onChangeMainToolsPanelHandler(ActiveButton.debug); + }} /> )} @@ -821,8 +853,9 @@ export const DebugApp = () => { { onChangeMainToolsPanelHandler(ActiveButton.validator); } - } + onClose={() => { + onChangeMainToolsPanelHandler(ActiveButton.validator); + }} /> )} @@ -835,7 +868,9 @@ export const DebugApp = () => { tilesetStats={tilesetsStats} contentFormats={tilesetRef.current?.contentFormats} updateNumber={updateStatsNumber} - onClose={() => { onChangeMainToolsPanelHandler(ActiveButton.memory); }} + onClose={() => { + onChangeMainToolsPanelHandler(ActiveButton.memory); + }} /> )} @@ -850,7 +885,9 @@ export const DebugApp = () => { onSelectBookmark={onSelectBookmarkHandler} onCollapsed={onCloseBookmarkPanel} onDownloadBookmarks={onDownloadBookmarksHandler} - onClearBookmarks={() => { setBookmarks([]); }} + onClearBookmarks={() => { + setBookmarks([]); + }} onBookmarksUploaded={onBookmarksUploadedHandler} onDeleteBookmark={onDeleteBookmarkHandler} onEditBookmark={onEditBookmarkHandler} @@ -861,13 +898,15 @@ export const DebugApp = () => { onZoomIn={onZoomIn} onZoomOut={onZoomOut} onCompassClick={onCompassClick} - isDragModeVisible={selectedBaseMapId !== "ArcGis"} + isDragModeVisible={selectedBaseMap?.group !== BaseMapGroup.ArcGIS} /> {wrongBookmarkPageId && ( { setWrongBookmarkPageId(null); }} + onConfirm={() => { + setWrongBookmarkPageId(null); + }} /> )} diff --git a/src/pages/debug-app/e2e.debug-app.spec.ts b/src/pages/debug-app/e2e.debug-app.spec.ts index 9b710c48..ad0eb06b 100644 --- a/src/pages/debug-app/e2e.debug-app.spec.ts +++ b/src/pages/debug-app/e2e.debug-app.spec.ts @@ -2,7 +2,7 @@ import puppeteer, { type Browser, type Page } from "puppeteer"; import { checkInserLayerErrors, checkLayersPanel, - inserAndDeleteLayer, + insertAndDeleteLayer, } from "../../utils/testing-utils/e2e-layers-panel"; import { configurePage } from "../../utils/testing-utils/configure-tests"; @@ -159,7 +159,7 @@ describe("Debug - Layers panel", () => { }); it("Should insert and delete layers", async () => { - await inserAndDeleteLayer( + await insertAndDeleteLayer( page, "#debug--layers-panel", "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/Rancho_Mesh_mesh_v17_1/SceneServer/layers/0" diff --git a/src/pages/viewer-app/e2e.viewer-app.spec.ts b/src/pages/viewer-app/e2e.viewer-app.spec.ts index 78592ed9..6dc6bb99 100644 --- a/src/pages/viewer-app/e2e.viewer-app.spec.ts +++ b/src/pages/viewer-app/e2e.viewer-app.spec.ts @@ -1,7 +1,7 @@ import puppeteer, { type Browser, type Page } from "puppeteer"; import { checkLayersPanel, - inserAndDeleteLayer, + insertAndDeleteLayer, } from "../../utils/testing-utils/e2e-layers-panel"; import { configurePage } from "../../utils/testing-utils/configure-tests"; @@ -134,7 +134,7 @@ describe("Viewer - Layers panel", () => { }); it("Should insert and delete layers", async () => { - await inserAndDeleteLayer( + await insertAndDeleteLayer( page, "#viewer--layers-panel", "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/Rancho_Mesh_mesh_v17_1/SceneServer/layers/0" diff --git a/src/pages/viewer-app/viewer-app.tsx b/src/pages/viewer-app/viewer-app.tsx index 92f897f9..d44e006c 100644 --- a/src/pages/viewer-app/viewer-app.tsx +++ b/src/pages/viewer-app/viewer-app.tsx @@ -32,6 +32,7 @@ import { type Bookmark, DragMode, PageId, + BaseMapGroup, type TilesetMetadata, } from "../../types"; import { useAppLayout } from "../../utils/hooks/layout"; @@ -72,7 +73,7 @@ import { import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { setDragMode } from "../../redux/slices/drag-mode-slice"; import { - selectSelectedBaseMapId, + selectSelectedBaseMap, setInitialBaseMaps, } from "../../redux/slices/base-maps-slice"; import { ArcgisWrapper } from "../../components/arcgis-wrapper/arcgis-wrapper"; @@ -119,7 +120,7 @@ export const ViewerApp = () => { const [isAttributesLoading, setAttributesLoading] = useState(false); const flattenedSublayers = useAppSelector(selectLayers); const bslSublayers = useAppSelector(selectSublayers); - const selectedBaseMapId = useAppSelector(selectSelectedBaseMapId); + const selectedBaseMap = useAppSelector(selectSelectedBaseMap); const [tilesetsStats, setTilesetsStats] = useState(initStats()); const [memoryStats, setMemoryStats] = useState(null); const [updateStatsNumber, setUpdateStatsNumber] = useState(0); @@ -143,7 +144,9 @@ export const ViewerApp = () => { const [, setSearchParams] = useSearchParams(); const dispatch = useAppDispatch(); const MapWrapper = - selectedBaseMapId === "ArcGis" ? ArcgisWrapper : DeckGlWrapper; + selectedBaseMap?.group === BaseMapGroup.ArcGIS + ? ArcgisWrapper + : DeckGlWrapper; const filtersByAttribute = useSelector((state: RootState) => selectFiltersByAttribute(state) ); @@ -725,7 +728,7 @@ export const ViewerApp = () => { onZoomIn={onZoomIn} onZoomOut={onZoomOut} onCompassClick={onCompassClick} - isDragModeVisible={selectedBaseMapId !== "ArcGis"} + isDragModeVisible={selectedBaseMap?.group !== BaseMapGroup.ArcGIS} /> {wrongBookmarkPageId && ( diff --git a/src/redux/slices/base-maps-slice.spec.ts b/src/redux/slices/base-maps-slice.spec.ts index b0b7e44e..d9a6d413 100644 --- a/src/redux/slices/base-maps-slice.spec.ts +++ b/src/redux/slices/base-maps-slice.spec.ts @@ -1,13 +1,14 @@ import { setupStore } from "../store"; import reducer, { type BaseMapsState, - selectBaseMaps, - selectSelectedBaseMapId, + selectBaseMapsByGroup, + selectSelectedBaseMap, setInitialBaseMaps, addBaseMap, - setSelectedBaseMaps, - deleteBaseMaps, + setSelectedBaseMap, + deleteBaseMap, } from "./base-maps-slice"; +import { BaseMapGroup } from "../../types"; import { BASE_MAPS } from "../../constants/map-styles"; jest.mock("@loaders.gl/i3s", () => { @@ -19,29 +20,39 @@ jest.mock("@loaders.gl/i3s", () => { describe("slice: base-maps", () => { it("Reducer should return the initial state", () => { expect(reducer(undefined, { type: "none" })).toEqual({ - baseMap: BASE_MAPS, - selectedBaseMap: "Dark", + basemaps: BASE_MAPS, + selectedBaseMapId: "Dark", }); }); it("Reducer setBaseMaps should add base map", () => { const previousState: BaseMapsState = { - baseMap: [ + basemaps: [ { id: "Dark", mapUrl: "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json", name: "Dark", + group: BaseMapGroup.Maplibre, + iconId: "Dark", }, { id: "Light", mapUrl: "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", name: "Light", + group: BaseMapGroup.Maplibre, + iconId: "Light", + }, + { + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", }, - { id: "Terrain", mapUrl: "", name: "Terrain" }, ], - selectedBaseMap: "Dark", + selectedBaseMapId: "Dark", }; expect( @@ -51,158 +62,236 @@ describe("slice: base-maps", () => { id: "first", mapUrl: "https://first-url.com", name: "first name", + group: BaseMapGroup.Maplibre, + iconId: "Dark", }) ) ).toEqual({ - baseMap: [ + basemaps: [ { id: "Dark", mapUrl: "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json", name: "Dark", + group: BaseMapGroup.Maplibre, + iconId: "Dark", }, { id: "Light", mapUrl: "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", name: "Light", + group: BaseMapGroup.Maplibre, + iconId: "Light", + }, + { + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", + }, + { + id: "first", + mapUrl: "https://first-url.com", + name: "first name", + group: BaseMapGroup.Maplibre, + iconId: "Dark", }, - { id: "Terrain", mapUrl: "", name: "Terrain" }, - { id: "first", mapUrl: "https://first-url.com", name: "first name" }, ], - selectedBaseMap: "first", + selectedBaseMapId: "first", }); }); - it("Reducer deleteBaseMaps should remove base map", () => { + it("Reducer deleteBaseMap should remove base map", () => { const previousState: BaseMapsState = { - baseMap: [ + basemaps: [ { id: "Dark", mapUrl: "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json", name: "Dark", + group: BaseMapGroup.Maplibre, + iconId: "Dark", }, { id: "Light", mapUrl: "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", name: "Light", + group: BaseMapGroup.Maplibre, + iconId: "Light", + }, + { + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", }, - { id: "Terrain", mapUrl: "", name: "Terrain" }, ], - selectedBaseMap: "Dark", + selectedBaseMapId: "Dark", }; - expect(reducer(previousState, deleteBaseMaps("Dark"))).toEqual({ - baseMap: [ + expect(reducer(previousState, deleteBaseMap("Dark"))).toEqual({ + basemaps: [ { id: "Light", mapUrl: "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", name: "Light", + group: BaseMapGroup.Maplibre, + iconId: "Light", + }, + { + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", }, - { id: "Terrain", mapUrl: "", name: "Terrain" }, ], - selectedBaseMap: "Light", + selectedBaseMapId: "Light", }); }); - it("Reducer setSelectedBaseMaps should update selected base map", () => { + it("Reducer setSelectedBaseMap should update selected base map", () => { const previousState: BaseMapsState = { - baseMap: [ + basemaps: [ { id: "Dark", mapUrl: "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json", name: "Dark", + group: BaseMapGroup.Maplibre, + iconId: "Dark", }, { id: "Light", mapUrl: "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", name: "Light", + group: BaseMapGroup.Maplibre, + iconId: "Light", + }, + { + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", }, - { id: "Terrain", mapUrl: "", name: "Terrain" }, ], - selectedBaseMap: "Dark", + selectedBaseMapId: "Dark", }; - expect(reducer(previousState, setSelectedBaseMaps("Light"))).toEqual({ - baseMap: [ + expect(reducer(previousState, setSelectedBaseMap("Light"))).toEqual({ + basemaps: [ { id: "Dark", mapUrl: "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json", name: "Dark", + group: BaseMapGroup.Maplibre, + iconId: "Dark", }, { id: "Light", mapUrl: "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", name: "Light", + group: BaseMapGroup.Maplibre, + iconId: "Light", + }, + { + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", }, - { id: "Terrain", mapUrl: "", name: "Terrain" }, ], - selectedBaseMap: "Light", + selectedBaseMapId: "Light", }); }); it("Reducer setInitialBaseMaps should return initial base maps", () => { const previousState: BaseMapsState = { - baseMap: [{ id: "Terrain", mapUrl: "", name: "Terrain" }], - selectedBaseMap: "Terrain", + basemaps: [ + { + id: "Terrain", + mapUrl: "", + name: "Terrain", + group: BaseMapGroup.Terrain, + iconId: "Terrain", + }, + ], + selectedBaseMapId: "Terrain", }; expect(reducer(previousState, setInitialBaseMaps())).toEqual({ - baseMap: BASE_MAPS, - selectedBaseMap: "Dark", + basemaps: BASE_MAPS, + selectedBaseMapId: "Dark", }); }); - it("Selectors should return initial value", () => { + it("Selectors should return initially selected value", () => { const store = setupStore(); const state = store.getState(); - expect(selectSelectedBaseMapId(state)).toEqual("Dark"); - expect(selectBaseMaps(state)).toEqual(BASE_MAPS); + const mapId = selectSelectedBaseMap(state)?.id; + expect(mapId).toEqual("Dark"); + expect(selectBaseMapsByGroup(state, "")).toEqual(BASE_MAPS); }); it("Selectors should return updated value", () => { const store = setupStore(); - store.dispatch(deleteBaseMaps("Dark")); - store.dispatch( - addBaseMap({ - id: "first", - mapUrl: "https://first-url.com", - name: "first name", - }) - ); - store.dispatch(setSelectedBaseMaps("Terrain")); + store.dispatch(deleteBaseMap("Dark")); + const newItem = { + id: "first", + mapUrl: "https://first-url.com", + name: "first name", + group: BaseMapGroup.Maplibre, + iconId: "Dark", + }; + store.dispatch(addBaseMap(newItem)); + store.dispatch(setSelectedBaseMap("Terrain")); + const state = store.getState(); + const mapId = selectSelectedBaseMap(state)?.id; + expect(mapId).toEqual("Terrain"); + + const expectedArray = BASE_MAPS.filter((item) => item.id !== "Dark"); + expectedArray.push(newItem); + + expect(selectBaseMapsByGroup(state, "")).toEqual(expectedArray); + // set wrong id of basemap + store.dispatch(setSelectedBaseMap("Dark")); + const newState = store.getState(); + // it doesn't use wrong id and keeps previous one + expect(selectSelectedBaseMap(newState)?.id).toEqual("Terrain"); + }); + + it("Selector should return value selected by group", () => { + const store = setupStore(); const state = store.getState(); - expect(selectSelectedBaseMapId(state)).toEqual("Terrain"); - expect(selectBaseMaps(state)).toEqual([ + const mapId = selectSelectedBaseMap(state)?.id; + expect(mapId).toEqual("Dark"); + expect(selectBaseMapsByGroup(state, BaseMapGroup.Maplibre)).toEqual([ + { + id: "Dark", + mapUrl: + "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json", + name: "Dark", + group: BaseMapGroup.Maplibre, + iconId: "Dark", + }, { id: "Light", mapUrl: "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json", name: "Light", - }, - { id: "Terrain", mapUrl: "", name: "Terrain" }, - { - id: "ArcGis", - name: "ArcGis", - mapUrl: "", - }, - { - id: "first", - mapUrl: "https://first-url.com", - name: "first name", + group: BaseMapGroup.Maplibre, + iconId: "Light", }, ]); - // set wrong id of basemap - store.dispatch(setSelectedBaseMaps("Dark")); - const newState = store.getState(); - // it doesn't use wrong id and keeps previous one - expect(selectSelectedBaseMapId(newState)).toEqual("Terrain"); }); }); diff --git a/src/redux/slices/base-maps-slice.ts b/src/redux/slices/base-maps-slice.ts index eedf6bef..1fa20ddc 100644 --- a/src/redux/slices/base-maps-slice.ts +++ b/src/redux/slices/base-maps-slice.ts @@ -2,15 +2,16 @@ import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; import { type BaseMap } from "../../types"; import { BASE_MAPS } from "../../constants/map-styles"; import { type RootState } from "../store"; +import { createSelector } from "reselect"; // Define a type for the slice state export interface BaseMapsState { - baseMap: BaseMap[]; - selectedBaseMap: string; + basemaps: BaseMap[]; + selectedBaseMapId: string; } const initialState: BaseMapsState = { - baseMap: BASE_MAPS, - selectedBaseMap: BASE_MAPS[0].id, + basemaps: BASE_MAPS, + selectedBaseMapId: BASE_MAPS[0]?.id ?? "", }; const baseMapsSlice = createSlice({ name: "baseMaps", @@ -20,34 +21,53 @@ const baseMapsSlice = createSlice({ return initialState; }, addBaseMap: (state: BaseMapsState, action: PayloadAction) => { - state.baseMap.push(action.payload); - state.selectedBaseMap = action.payload.id; + state.basemaps.push(action.payload); + state.selectedBaseMapId = action.payload.id; }, - setSelectedBaseMaps: ( + setSelectedBaseMap: ( state: BaseMapsState, action: PayloadAction ) => { - const newMap = state.baseMap.find((map) => map.id === action.payload); + const newMap = state.basemaps.find((map) => map.id === action.payload); if (newMap) { - state.selectedBaseMap = action.payload; + state.selectedBaseMapId = action.payload; } }, - deleteBaseMaps: (state: BaseMapsState, action: PayloadAction) => { - state.baseMap = state.baseMap.filter( - (keepMap) => keepMap.id !== action.payload + deleteBaseMap: (state: BaseMapsState, action: PayloadAction) => { + const idToDelete = action.payload; + state.basemaps = state.basemaps.filter( + (keepMap) => keepMap.id !== idToDelete ); - state.selectedBaseMap = state.baseMap[0].id; + if (state.selectedBaseMapId === idToDelete) { + state.selectedBaseMapId = state.basemaps[0]?.id ?? ""; + } }, }, }); -export const selectBaseMaps = (state: RootState): BaseMap[] => - state.baseMaps.baseMap; -export const selectSelectedBaseMapId = (state: RootState): string => - state.baseMaps.selectedBaseMap; +export const selectSelectedBaseMap = createSelector( + [ + (state: RootState) => state.baseMaps.basemaps, + (state: RootState) => state.baseMaps.selectedBaseMapId, + ], + (maps, selectedId): BaseMap | null => { + const el = maps.find((item) => item.id === selectedId); + return el ?? null; + } +); + +export const selectBaseMapsByGroup = createSelector( + [ + (state: RootState) => state.baseMaps.basemaps, + (_: RootState, group: string) => group, + ], + (maps, group): BaseMap[] => { + return group ? maps.filter((item) => item.group === group) : maps; + } +); export const { setInitialBaseMaps } = baseMapsSlice.actions; export const { addBaseMap } = baseMapsSlice.actions; -export const { setSelectedBaseMaps } = baseMapsSlice.actions; -export const { deleteBaseMaps } = baseMapsSlice.actions; +export const { setSelectedBaseMap } = baseMapsSlice.actions; +export const { deleteBaseMap } = baseMapsSlice.actions; export default baseMapsSlice.reducer; diff --git a/src/types.ts b/src/types.ts index 76437cb1..c51c1012 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,7 @@ -import type { BuildingSceneSublayer, StatsInfo } from "@loaders.gl/i3s/src/types"; +import type { + BuildingSceneSublayer, + StatsInfo, +} from "@loaders.gl/i3s/src/types"; import type { OrientedBoundingBox, BoundingSphere } from "@math.gl/culling"; import type { DefaultTheme } from "styled-components"; import type { Vector3, Matrix4 } from "@math.gl/core"; @@ -189,12 +192,20 @@ export interface Sublayer extends BuildingSceneSublayer { sublayers: Sublayer[]; } +export enum BaseMapGroup { + Maplibre = "Maplibre", + ArcGIS = "ArcGIS", + Terrain = "Terrain", +} + export interface BaseMap { id: string; name: string; mapUrl: string; token?: string; custom?: boolean; + group: BaseMapGroup; + iconId: string; } export interface PositionsData { diff --git a/src/utils/testing-utils/e2e-layers-panel.tsx b/src/utils/testing-utils/e2e-layers-panel.tsx index 5d06363b..5bd60613 100644 --- a/src/utils/testing-utils/e2e-layers-panel.tsx +++ b/src/utils/testing-utils/e2e-layers-panel.tsx @@ -3,27 +3,27 @@ import { PageId } from "../../types"; import type { Page } from "puppeteer"; export const checkLayersPanel = async ( - page, + page: Page, panelId: string, hasSelectedLayer = false, appMode = "" ): Promise => { // Tabs const tabsContainer = await page.$(`${panelId} > :first-child`); - expect((await tabsContainer.$$(":scope > *")).length).toBe(2); - expect(await tabsContainer.$(":first-child::after")).toBeDefined(); - expect(await tabsContainer.$(":last-child::after")).toBeNull(); + expect((await tabsContainer?.$$(":scope > *"))?.length).toBe(2); + expect(await tabsContainer?.$(":first-child::after")).toBeDefined(); + expect(await tabsContainer?.$(":last-child::after")).toBeNull(); // Close button const closeButtonIcon = await page.$( `${panelId} > :nth-child(2) > :first-child > :first-child` ); - expect(await closeButtonIcon.$(":scope::after")).toBeDefined(); - expect(await closeButtonIcon.$(":scope::before")).toBeDefined(); + expect(await closeButtonIcon?.$(":scope::after")).toBeDefined(); + expect(await closeButtonIcon?.$(":scope::before")).toBeDefined(); // Horizontal Line expect( - await page.$eval(`${panelId} > :nth-child(3)`, (node) => node.innerText) + await page.$eval(`${panelId} > :nth-child(3)`, (node) => node.innerHTML) ).toBe(""); // Layers @@ -47,38 +47,55 @@ export const checkLayersPanel = async ( await expect(panel).toMatchTextContent("Insert scene"); // Open map options - const mapOptionsTab = await tabsContainer.$(":last-child"); - await mapOptionsTab.click(); - expect(await tabsContainer.$(":first-child::after")).toBeNull(); - expect(await tabsContainer.$(":last-child::after")).toBeDefined(); + const mapOptionsTab = await tabsContainer?.$(":last-child"); + await mapOptionsTab?.click(); + expect(await tabsContainer?.$(":first-child::after")).toBeNull(); + expect(await tabsContainer?.$(":last-child::after")).toBeDefined(); // Header await expect(panel).toMatchTextContent("Base Map"); - // Base maps list - const baseMapsNames = await page.$$eval( - `${panelId} > :nth-child(4) > :first-child > :nth-child(2) > div`, - (nodes) => nodes.map((node) => node.innerText) - ); - - if (appMode === PageId.comparison) { - expect(baseMapsNames.length).toBe(3); - expect(baseMapsNames).toEqual(["Dark", "Light", "ArcGis"]); - } else { - expect(baseMapsNames.length).toBe(4); - expect(baseMapsNames).toEqual(["Dark", "Light", "Terrain", "ArcGis"]); + // Basemap list + let names = ["Dark", "Light"]; + const namesForViewDebug = [ + "Light gray", + "Dark gray", + "Streets", + "Streets(night)", + "Terrain", + ]; + if (appMode !== PageId.comparison) { + names = [...names, ...namesForViewDebug]; } + let successCount = 0; + for (const text of names) { + const element = await page?.$(`text/${text}`); + if (element) { + successCount++; + } + } + expect(successCount).toBe(names.length); // Dark is selected - const darkMapBackground = await page.$eval( - `${panelId} > :nth-child(4) > :first-child > :nth-child(2) > :first-child`, - (node) => getComputedStyle(node).getPropertyValue("background-color") - ); - expect(darkMapBackground).toBe("rgb(57, 58, 69)"); + const elementDark = await page?.$("div ::-p-text(Dark)"); + + let darkMapBackground; + if (elementDark) { + darkMapBackground = await page.evaluate((el) => { + const parent = el.parentElement; + if (parent) { + const computedStyle = window.getComputedStyle(parent); + return computedStyle.backgroundColor; + } else { + return null; + } + }, elementDark); + } + expect(darkMapBackground).toBe("rgb(57, 58, 69)"); // "#393A45" // Insert Base Map button await page.waitForSelector("#map-options-container"); - const optionsContainer = await panel.$("#map-options-container"); + const optionsContainer = await panel?.$("#map-options-container"); await expect(optionsContainer).toMatchTextContent("Insert Base Map"); }; @@ -170,7 +187,7 @@ export const checkInserLayerErrors = async ( expect(anyExtraPanel).toBeNull(); }; -export const inserAndDeleteLayer = async ( +export const insertAndDeleteLayer = async ( page: Page, panelId: string, url: string