From 8f6e6a9891b2ffc1e7b753d53b80fe83e6a81b4e Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Thu, 21 Dec 2023 13:42:45 +0300 Subject: [PATCH 01/11] support of arcgis content --- package.json | 1 + public/icons/arrow-down.svg | 3 + src/components/common.tsx | 4 +- .../comparison-side/comparison-side.tsx | 23 +- .../arcgis-import-panel.tsx | 220 ++++++++++++++++++ .../layers-control-panel.spec.tsx | 6 +- .../layers-panel/layers-control-panel.tsx | 9 +- .../layers-panel/layers-panel.spec.tsx | 2 + src/components/layers-panel/layers-panel.tsx | 47 +++- src/components/modal-dialog/modal-dialog.tsx | 32 ++- src/pages/auth/auth.tsx | 2 +- src/pages/debug-app/debug-app.tsx | 17 ++ src/pages/viewer-app/viewer-app.tsx | 19 +- src/redux/slices/arcgis-auth-slice.spec.ts | 4 +- src/redux/slices/arcgis-auth-slice.ts | 2 +- src/redux/slices/arcgis-content-slice.ts | 104 +++++++++ src/redux/store.ts | 2 + src/types.ts | 8 + src/utils/arcgis-auth.spec.ts | 2 +- src/utils/{arcgis-auth.ts => arcgis.ts} | 28 +++ 20 files changed, 511 insertions(+), 24 deletions(-) create mode 100644 public/icons/arrow-down.svg create mode 100644 src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx create mode 100644 src/redux/slices/arcgis-content-slice.ts rename src/utils/{arcgis-auth.ts => arcgis.ts} (78%) diff --git a/package.json b/package.json index bd93774c..92225483 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@esri/arcgis-rest-auth": "^3.7.0", "@esri/arcgis-rest-request": "^4.2.0", + "@esri/arcgis-rest-portal": "^4.4.0", "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.17", diff --git a/public/icons/arrow-down.svg b/public/icons/arrow-down.svg new file mode 100644 index 00000000..8d4acfdb --- /dev/null +++ b/public/icons/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common.tsx b/src/components/common.tsx index 5fce3c01..1c566be2 100644 --- a/src/components/common.tsx +++ b/src/components/common.tsx @@ -64,8 +64,10 @@ export const PanelContent = styled.div` export const PanelHorizontalLine = styled.div<{ top?: number; bottom?: number; + left?: number; + right?: number; }>` - margin: ${({ top = 24, bottom = 16 }) => `${top}px 16px ${bottom}px 16px`}; + margin: ${({ top = 24, bottom = 16, left = 16, right = 16 }) => `${top}px ${right}px ${bottom}px ${left}px`}; border: 1px solid ${({ theme }) => theme.colors.mainHiglightColorInverted}; border-radius: 1px; background: ${({ theme }) => theme.colors.mainHiglightColorInverted}; diff --git a/src/components/comparison/comparison-side/comparison-side.tsx b/src/components/comparison/comparison-side/comparison-side.tsx index b10277c0..20af34e1 100644 --- a/src/components/comparison/comparison-side/comparison-side.tsx +++ b/src/components/comparison/comparison-side/comparison-side.tsx @@ -376,7 +376,27 @@ export const ComparisonSide = ({ ); }; - const onLayerInsertHandler = ( + // TODO: Do we need it? Why don't we use onLayerInsertHandler? + const onArcGisInsertHandler = ( + newLayer: LayerExample, + bookmarks?: Bookmark[] +) => { + const newExamples = [...examples, newLayer]; + setExamples(newExamples); + const newActiveLayers = handleSelectAllLeafsInGroup(newLayer); + const newActiveLayersIds = newActiveLayers.map((layer) => layer.id); + setActiveLayers(newActiveLayers); + onChangeLayers && onChangeLayers(newExamples, newActiveLayersIds); + + /** + * There is no sense to use webscene bookmarks in across layers mode. + */ + if (bookmarks?.length) { + onInsertBookmarks && onInsertBookmarks(bookmarks); + } +}; + +const onLayerInsertHandler = ( newLayer: LayerExample, bookmarks?: Bookmark[] ) => { @@ -514,6 +534,7 @@ export const ComparisonSide = ({ ? ComparisonSideMode.left : side } + onArcGisImport={onArcGisInsertHandler} onLayerInsert={onLayerInsertHandler} onLayerSelect={onLayerSelectHandler} onLayerDelete={(id) => onLayerDeleteHandler(id)} diff --git a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx new file mode 100644 index 00000000..d828978e --- /dev/null +++ b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx @@ -0,0 +1,220 @@ +import styled, { css, useTheme } from "styled-components"; +import { RadioButton } from "../../radio-button/radio-button"; +import { Fragment, useEffect, useState } from "react"; +import { PanelHorizontalLine } from "../../common"; +import SortDownIcon from "../../../../public/icons/arrow-down.svg"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { ModalDialog } from "../../modal-dialog/modal-dialog"; +import { + selectArcGisContent, + selectArcGisContentSelected, + setArcGisContentSelected, + resetArcGisContentSelected, + setSortOrder, +} from "../../../redux/slices/arcgis-content-slice"; + +type InsertLayerProps = { + onImport: (object: { name: string; url: string; token?: string }) => void; + onCancel: () => void; +}; + +const Table = styled.div` + width: 584px; + display: flex; + flex-direction: column; + justify-content: start; + row-gap: 16px; +`; + +const TableHeader = styled.div` + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 19px; + overflow: hidden; + display: flex; + flex-direction: row; + justify-content: start; + column-gap: 24px; +`; + +const TableHeaderItem2 = styled.div` + margin-left: 68px; + width: 343px; +`; +const TableHeaderItem3 = styled.div` + width: 149px; +`; + +const TableRowItem1 = styled.div` + width: 44px; +`; +const TableRowItem2 = styled.div` + font-weight: 700; + width: 343px; +`; +const TableRowItem3 = styled.div` + width: 149px; +`; + +const TableContent = styled.div` + display: flex; + flex-direction: column; + justify-content: start; + row-gap: 8px; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 19px; + overflow: auto; + max-height: 300px; +`; + +type ContainerProps = { + checked: boolean; +}; + +const TableRow = styled.div` + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; + column-gap: 24px; + + background: transparent; + cursor: pointer; + ${({ checked }) => + checked && + css` + background: ${({ theme }) => theme.colors.mainHiglightColor}; + box-shadow: 0px 17px 80px rgba(0, 0, 0, 0.1); + border-radius: 8px; + `} + &:hover { + background: ${({ theme }) => theme.colors.mainDimColor}; + box-shadow: 0px 17px 80px rgba(0, 0, 0, 0.1); + border-radius: 8px; + } +`; + +const Radio = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 44px; + height: 44px; +`; + +const DateContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; + gap: 4px; +`; + +const IconContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding-top: 2px; + width: 16px; + height: 16px; + cursor: pointer; + fill: ${({ theme }) => theme.colors.buttonDimIconColor}; +`; + +export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { + const dispatch = useAppDispatch(); + const arcGisContentArray = useAppSelector(selectArcGisContent); + const arcGisContentSelected = useAppSelector(selectArcGisContentSelected); + const [sortDateOrder, setSortDateOrder] = useState(false); + + useEffect(() => { + dispatch(resetArcGisContentSelected()); + }, []); + + const handleImport = () => { + const arcGisItem = arcGisContentArray.find( + (item) => item.id === arcGisContentSelected + ); + if (arcGisItem) { + onImport(arcGisItem); + } + }; + + const onSort = () => { + setSortDateOrder((prevValue) => !prevValue); + dispatch(setSortOrder(sortDateOrder)); + }; + + const formatDate = (date: number) => { + const formatter = new Intl.DateTimeFormat("en-US", { + month: "long", + day: "2-digit", + year: "numeric", + }); + return formatter.format(date); + }; + + const theme = useTheme(); + return ( + + + + Title + + + Date + + + + + + + + {arcGisContentArray.map((contentItem) => { + const isMapSelected = arcGisContentSelected === contentItem.id; + + return ( + + { + dispatch(setArcGisContentSelected(contentItem.id)); + }} + > + + + { + dispatch(setArcGisContentSelected(contentItem.id)); + }} + /> + + + {contentItem.name} + + {formatDate(contentItem.created)} + + + + + ); + })} + +
+
+ ); +}; diff --git a/src/components/layers-panel/layers-control-panel.spec.tsx b/src/components/layers-panel/layers-control-panel.spec.tsx index eb0dcb1b..b78d0db5 100644 --- a/src/components/layers-panel/layers-control-panel.spec.tsx +++ b/src/components/layers-panel/layers-control-panel.spec.tsx @@ -19,13 +19,13 @@ import { arcGisRequestLogin, arcGisCompleteLogin, arcGisRequestLogout, -} from "../../utils/arcgis-auth"; +} from "../../utils/arcgis"; jest.mock("./list-item/list-item"); jest.mock("../action-icon-button/action-icon-button"); jest.mock("./delete-confirmation"); jest.mock("./layer-options-menu/layer-options-menu"); -jest.mock("../../utils/arcgis-auth"); +jest.mock("../../utils/arcgis"); const ListItemMock = ListItem as unknown as jest.Mocked; const PlusButtonMock = ActionIconButton as unknown as jest.Mocked; @@ -51,11 +51,13 @@ const onDeleteLayerMock = jest.fn(); const onSelectLayerMock = jest.fn(); const onLayerSettingsClickMock = jest.fn(); const onPointToLayerMock = jest.fn(); +const onArcGisImportMock = jest.fn(); const callRender = (renderFunc, props = {}, store = setupStore()) => { return renderFunc( void; onLayerInsertClick: () => void; onSceneInsertClick: () => void; + onArcGisImportClick: () => void; onLayerSettingsClick: ReactEventHandler; onPointToLayer: (viewState?: LayerViewState) => void; deleteLayer: (id: string) => void; @@ -100,6 +101,7 @@ export const LayersControlPanel = ({ hasSettings = false, onLayerInsertClick, onSceneInsertClick, + onArcGisImportClick, onLayerSettingsClick, onPointToLayer, deleteLayer, @@ -115,7 +117,11 @@ export const LayersControlPanel = ({ const [layerToDeleteId, setLayerToDeleteId] = useState(""); const onArcGisActionClick = () => { - !isLoggedIn && dispatch(arcGisLogin()); + if (isLoggedIn) { + onArcGisImportClick(); + } else { + dispatch(arcGisLogin()); + } }; const onArcGisLogoutClick = () => { setShowLogoutWarning(true); @@ -272,6 +278,7 @@ export const LayersControlPanel = ({ { dispatch(arcGisLogout()); setShowLogoutWarning(false); diff --git a/src/components/layers-panel/layers-panel.spec.tsx b/src/components/layers-panel/layers-panel.spec.tsx index 67d496ce..cdc9e86a 100644 --- a/src/components/layers-panel/layers-panel.spec.tsx +++ b/src/components/layers-panel/layers-panel.spec.tsx @@ -73,6 +73,7 @@ beforeAll(() => { )); }); +const arcGisImportMock = jest.fn(); const layerInsertMock = jest.fn(); const layerSelectMock = jest.fn(); const layerDeleteMock = jest.fn(); @@ -90,6 +91,7 @@ const callRender = (renderFunc, props = {}, store = setupStore()) => { sublayers={[]} selectedLayerIds={[]} type={0} + onArcGisImport={arcGisImportMock} onLayerInsert={layerInsertMock} onLayerSelect={layerSelectMock} onLayerDelete={layerDeleteMock} diff --git a/src/components/layers-panel/layers-panel.tsx b/src/components/layers-panel/layers-panel.tsx index bec8fa9d..0c310b15 100644 --- a/src/components/layers-panel/layers-panel.tsx +++ b/src/components/layers-panel/layers-panel.tsx @@ -38,6 +38,8 @@ import { getTilesetType } from "../../utils/url-utils"; import { convertArcGisSlidesToBookmars } from "../../utils/bookmarks-utils"; import { useAppDispatch } from "../../redux/hooks"; import { addBaseMap } from "../../redux/slices/base-maps-slice"; +import { getArcGisContent } from "../../redux/slices/arcgis-content-slice"; +import { ArcGisImportPanel } from "./arcgis-import-panel/arcgis-import-panel"; const EXISTING_AREA_ERROR = "You are trying to add an existing area to the map"; @@ -149,6 +151,7 @@ type LayersPanelProps = { viewWidth?: number; viewHeight?: number; side?: ComparisonSideMode; + onArcGisImport: (layer: LayerExample, bookmarks?: Bookmark[]) => void; onLayerInsert: (layer: LayerExample, bookmarks?: Bookmark[]) => void; onLayerSelect: (layer: LayerExample, rootLayer?: LayerExample) => void; onLayerDelete: (id: string) => void; @@ -165,6 +168,7 @@ export const LayersPanel = ({ layers, sublayers, selectedLayerIds, + onArcGisImport, onLayerInsert, onLayerSelect, onLayerDelete, @@ -183,6 +187,7 @@ export const LayersPanel = ({ const [tab, setTab] = useState(Tabs.Layers); const [showLayerInsertPanel, setShowLayerInsertPanel] = useState(false); const [showSceneInsertPanel, setShowSceneInsertPanel] = useState(false); + const [showArcGisImportPanel, setShowArcGisImportPanel] = useState(false); const [showLayerSettings, setShowLayerSettings] = useState(false); const [showInsertMapPanel, setShowInsertMapPanel] = useState(false); const [showExistedError, setShowExistedError] = useState(false); @@ -194,9 +199,36 @@ export const LayersPanel = ({ useState(false); const [warningNode, setWarningNode] = useState(null); const [showAddingSlidesWarning, setShowAddingSlidesWarning] = useState(false); - + useClickOutside([warningNode], () => setShowExistedError(false)); + const handleArcGisImport = (layer: { + name: string; + url: string; + token?: string; + }) => { + const existedLayer = layers.some( + (exisLayer) => exisLayer.url.trim() === layer.url.trim() + ); + + if (existedLayer) { + setShowArcGisImportPanel(false); + setShowExistedError(true); + return; + } + + const id = layer.url.replace(/" "/g, "-"); + const newLayer: LayerExample = { + ...layer, + id, + custom: true, + type: getTilesetType(layer.url), + }; + + onArcGisImport(newLayer); + setShowArcGisImportPanel(false); + }; + const handleInsertLayer = (layer: { name: string; url: string; @@ -370,6 +402,11 @@ export const LayersPanel = ({ onLayerSelect={onLayerSelect} onLayerInsertClick={() => setShowLayerInsertPanel(true)} onSceneInsertClick={() => setShowSceneInsertPanel(true)} + onArcGisImportClick={() => { + dispatch(getArcGisContent()); + setShowArcGisImportPanel(true) + } + } onLayerSettingsClick={() => setShowLayerSettings(true)} onPointToLayer={onPointToLayer} deleteLayer={onLayerDelete} @@ -438,6 +475,14 @@ export const LayersPanel = ({ /> )} + {showArcGisImportPanel && ( + + handleArcGisImport(item)} + onCancel={() => setShowArcGisImportPanel(false)} + /> + + )} )} {showLayerSettings && ( diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx index e2248bba..e497e221 100644 --- a/src/components/modal-dialog/modal-dialog.tsx +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -70,10 +70,14 @@ const Title = styled.div` line-height: 45px; `; -const ButtonsContainer = styled.div` +type ButtonsContainerProps = { + justify: string; +}; + +const ButtonsContainer = styled.div` display: flex; flex-direction: row; - justify-content: center; + justify-content: ${(props) => props.justify}; margin: 32px; column-gap: 18px; { @@ -102,8 +106,8 @@ const CloseCrossButton = styled(CloseIcon)` export const ModalDialog = ({ title, children, - cancelButtonText = "Cancel", - okButtonText = "Ok", + cancelButtonText, + okButtonText, onCancel, onConfirm, }: LogoutPanelProps) => { @@ -124,14 +128,18 @@ export const ModalDialog = ({ {title} {children} - - - {cancelButtonText} - - {okButtonText} + + {cancelButtonText ? ( + + {cancelButtonText} + + ) : ( + <> + )} + {okButtonText}{" "} diff --git a/src/pages/auth/auth.tsx b/src/pages/auth/auth.tsx index 2bb257d7..1c6617a8 100644 --- a/src/pages/auth/auth.tsx +++ b/src/pages/auth/auth.tsx @@ -6,7 +6,7 @@ import { useAppLayout, } from "../../utils/hooks/layout"; -import { arcGisCompleteLogin } from "../../utils/arcgis-auth"; +import { arcGisCompleteLogin } from "../../utils/arcgis"; export type LayoutProps = { layout: string; diff --git a/src/pages/debug-app/debug-app.tsx b/src/pages/debug-app/debug-app.tsx index a743648e..154c6224 100644 --- a/src/pages/debug-app/debug-app.tsx +++ b/src/pages/debug-app/debug-app.tsx @@ -447,6 +447,22 @@ export const DebugApp = () => { } }; + // Need? onArcGisInsertHandler + const onArcGisImportHandler = ( + newLayer: LayerExample, + bookmarks?: Bookmark[] + ) => { + const newExamples = [...examples, newLayer]; + setExamples(newExamples); + const newActiveLayers = handleSelectAllLeafsInGroup(newLayer); + setActiveLayers(newActiveLayers); + setPreventTransitions(false); + + if (bookmarks?.length) { + updateBookmarks(bookmarks); + } + }; + const onLayerSelectHandler = ( layer: LayerExample, rootLayer?: LayerExample @@ -759,6 +775,7 @@ export const DebugApp = () => { pageId={PageId.debug} layers={examples} selectedLayerIds={selectedLayerIds} + onArcGisImport={onArcGisImportHandler} onLayerInsert={onLayerInsertHandler} onLayerSelect={onLayerSelectHandler} onLayerDelete={(id) => onLayerDeleteHandler(id)} diff --git a/src/pages/viewer-app/viewer-app.tsx b/src/pages/viewer-app/viewer-app.tsx index 80dfb736..f4c88c71 100644 --- a/src/pages/viewer-app/viewer-app.tsx +++ b/src/pages/viewer-app/viewer-app.tsx @@ -326,7 +326,23 @@ export const ViewerApp = () => { } }; - const onLayerSelectHandler = ( +// Need? +const onArcGisImportHandler = ( + newLayer: LayerExample, + bookmarks?: Bookmark[] +) => { + const newExamples = [...examples, newLayer]; + setExamples(newExamples); + const newActiveLayers = handleSelectAllLeafsInGroup(newLayer); + setActiveLayers(newActiveLayers); + setPreventTransitions(false); + + if (bookmarks?.length) { + updateBookmarks(bookmarks); + } +}; + +const onLayerSelectHandler = ( layer: LayerExample, rootLayer?: LayerExample ) => { @@ -607,6 +623,7 @@ export const ViewerApp = () => { pageId={PageId.viewer} layers={examples} selectedLayerIds={selectedLayerIds} + onArcGisImport={onArcGisImportHandler} onLayerInsert={onLayerInsertHandler} onLayerSelect={onLayerSelectHandler} onLayerDelete={(id) => onLayerDeleteHandler(id)} diff --git a/src/redux/slices/arcgis-auth-slice.spec.ts b/src/redux/slices/arcgis-auth-slice.spec.ts index 8023ff9a..b12b421d 100644 --- a/src/redux/slices/arcgis-auth-slice.spec.ts +++ b/src/redux/slices/arcgis-auth-slice.spec.ts @@ -10,9 +10,9 @@ import { arcGisRequestLogin, arcGisCompleteLogin, arcGisRequestLogout, -} from "../../utils/arcgis-auth"; +} from "../../utils/arcgis"; -jest.mock("../../utils/arcgis-auth"); +jest.mock("../../utils/arcgis"); const getAuthenticatedUserMock = getAuthenticatedUser as unknown as jest.Mocked; diff --git a/src/redux/slices/arcgis-auth-slice.ts b/src/redux/slices/arcgis-auth-slice.ts index 7e7f6ea6..14e09917 100644 --- a/src/redux/slices/arcgis-auth-slice.ts +++ b/src/redux/slices/arcgis-auth-slice.ts @@ -4,7 +4,7 @@ import { getAuthenticatedUser, arcGisRequestLogin, arcGisRequestLogout, -} from "../../utils/arcgis-auth"; +} from "../../utils/arcgis"; // Define a type for the slice state export interface ArcGisAuthState { diff --git a/src/redux/slices/arcgis-content-slice.ts b/src/redux/slices/arcgis-content-slice.ts new file mode 100644 index 00000000..3589f952 --- /dev/null +++ b/src/redux/slices/arcgis-content-slice.ts @@ -0,0 +1,104 @@ +import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit"; +import { ArcGisContent } from "../../types"; +import { RootState } from "../store"; +import { getArcGisUserContent } from "../../utils/arcgis"; + +// Define a type for the slice state +interface ArcGisContentState { + arcGisContent: ArcGisContent[]; + arcGisContentSelected: string; + sortOrder: boolean; +} +const initialState: ArcGisContentState = { + arcGisContent: [], + arcGisContentSelected: "", + sortOrder: false, +}; + +const sortList = (state) => { + state.arcGisContent.sort((a: ArcGisContent, b: ArcGisContent) => + state.sortOrder ? a.created - b.created : b.created - a.created + ); +}; + +const arcGisContentSlice = createSlice({ + name: "arcGisContent", + initialState, + reducers: { + setInitialArcGisContent: () => { + return initialState; + }, + + setSortOrder: ( + state: ArcGisContentState, + action: PayloadAction + ) => { + state.sortOrder = action.payload; + sortList(state); + }, + + // Content + addArcGisContent: ( + state: ArcGisContentState, + action: PayloadAction + ) => { + state.arcGisContent.push(action.payload); + sortList(state); + }, + deleteArcGisContent: ( + state: ArcGisContentState, + action: PayloadAction + ) => { + state.arcGisContent = state.arcGisContent.filter( + (map) => map.id !== action.payload + ); + if (state.arcGisContentSelected === action.payload) { + state.arcGisContentSelected = ""; + } + sortList(state); + }, + + // Content Selected + setArcGisContentSelected: ( + state: ArcGisContentState, + action: PayloadAction + ) => { + const item = state.arcGisContent.find( + (item) => item.id === action.payload + ); + if (item) { + state.arcGisContentSelected = action.payload; + } + }, + resetArcGisContentSelected: (state: ArcGisContentState) => { + state.arcGisContentSelected = ""; + }, + }, + extraReducers: (builder) => { + builder.addCase(getArcGisContent.fulfilled, (state, action) => { + state.arcGisContent = [...action.payload]; + }); + }, +}); + +export const getArcGisContent = createAsyncThunk( + "getArcGisContent", + async (): Promise => { + const response: ArcGisContent[] = await getArcGisUserContent(); + return response; + } +); + +export const selectArcGisContent = (state: RootState): ArcGisContent[] => + state.arcGisContent.arcGisContent; +export const selectArcGisContentSelected = (state: RootState): string => + state.arcGisContent.arcGisContentSelected; + +export const { setInitialArcGisContent } = arcGisContentSlice.actions; +export const { addArcGisContent } = arcGisContentSlice.actions; +export const { setArcGisContentSelected } = arcGisContentSlice.actions; +export const { resetArcGisContentSelected } = arcGisContentSlice.actions; +export const { deleteArcGisContent } = arcGisContentSlice.actions; +export const { setSortOrder } = arcGisContentSlice.actions; + +export default arcGisContentSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 4b5b407d..fc89986a 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -11,6 +11,7 @@ import i3sStatsSliceReducer from "./slices/i3s-stats-slice"; import baseMapsSliceReducer from "./slices/base-maps-slice"; import symbolizationSliceReducer from "./slices/symbolization-slice"; import arcGisAuthSliceReducer from "./slices/arcgis-auth-slice"; +import arcGisContentSliceReducer from "./slices/arcgis-content-slice"; // Create the root reducer separately so we can extract the RootState type const rootReducer = combineReducers({ @@ -22,6 +23,7 @@ const rootReducer = combineReducers({ symbolization: symbolizationSliceReducer, i3sStats: i3sStatsSliceReducer, arcGisAuth: arcGisAuthSliceReducer, + arcGisContent: arcGisContentSliceReducer, }); export const setupStore = (preloadedState?: PreloadedState) => { diff --git a/src/types.ts b/src/types.ts index 1a44e9f8..825444e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -367,3 +367,11 @@ export type TilesetMetadata = { hasChildren: boolean; type?: TilesetType; }; + +export type ArcGisContent = { + id: string; + url: string; + name: string; + token?: string; + created: number; +}; diff --git a/src/utils/arcgis-auth.spec.ts b/src/utils/arcgis-auth.spec.ts index 34ebf69d..fe223bcb 100644 --- a/src/utils/arcgis-auth.spec.ts +++ b/src/utils/arcgis-auth.spec.ts @@ -2,7 +2,7 @@ import { getAuthenticatedUser, arcGisRequestLogin, arcGisRequestLogout, -} from "./arcgis-auth"; +} from "./arcgis"; jest.mock("@esri/arcgis-rest-request"); diff --git a/src/utils/arcgis-auth.ts b/src/utils/arcgis.ts similarity index 78% rename from src/utils/arcgis-auth.ts rename to src/utils/arcgis.ts index 36d397ca..b8bbcb6a 100644 --- a/src/utils/arcgis-auth.ts +++ b/src/utils/arcgis.ts @@ -1,5 +1,8 @@ import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"; +import { getUserContent } from "@esri/arcgis-rest-portal"; +import { ArcGisContent } from "../types"; + const ARCGIS_REST_USER_SESSION = "__ARCGIS_REST_USER_SESSION__"; const ARCGIS_REST_USER_INFO = "__ARCGIS_REST_USER_INFO__"; @@ -97,3 +100,28 @@ export const arcGisRequestLogout = async () => { } return await updateSessionInfo(); }; + +export const getArcGisUserContent = async (): Promise => { + const contentItems: ArcGisContent[] = []; + const authentication = getArcGisSession(); + if (authentication) { + const content = await getUserContent({ authentication }); + for (const item of content.items) { + if ( + item.url && + item.type === "Scene Service" && + item.typeKeywords && + item.typeKeywords.includes("Hosted Service") + ) { + const contentItem: ArcGisContent = { + id: item.id, + name: item.title, + url: item.url, + created: item.created, + }; + contentItems.push(contentItem); + } + } + } + return contentItems; +}; From 0cd42704828740058954ffc272a420df1eb471fb Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Thu, 21 Dec 2023 13:53:34 +0300 Subject: [PATCH 02/11] fix lint issue --- src/components/modal-dialog/modal-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx index e497e221..08d3a1aa 100644 --- a/src/components/modal-dialog/modal-dialog.tsx +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -128,7 +128,7 @@ export const ModalDialog = ({ {title} {children} - + {cancelButtonText ? ( Date: Fri, 22 Dec 2023 13:34:39 +0300 Subject: [PATCH 03/11] getting arcgis token for a content --- src/redux/slices/arcgis-content-slice.ts | 14 ++++++++------ src/utils/arcgis.ts | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/redux/slices/arcgis-content-slice.ts b/src/redux/slices/arcgis-content-slice.ts index 3589f952..a0db830a 100644 --- a/src/redux/slices/arcgis-content-slice.ts +++ b/src/redux/slices/arcgis-content-slice.ts @@ -94,11 +94,13 @@ export const selectArcGisContent = (state: RootState): ArcGisContent[] => export const selectArcGisContentSelected = (state: RootState): string => state.arcGisContent.arcGisContentSelected; -export const { setInitialArcGisContent } = arcGisContentSlice.actions; -export const { addArcGisContent } = arcGisContentSlice.actions; -export const { setArcGisContentSelected } = arcGisContentSlice.actions; -export const { resetArcGisContentSelected } = arcGisContentSlice.actions; -export const { deleteArcGisContent } = arcGisContentSlice.actions; -export const { setSortOrder } = arcGisContentSlice.actions; +export const { + setInitialArcGisContent, + addArcGisContent, + setArcGisContentSelected, + resetArcGisContentSelected, + deleteArcGisContent, + setSortOrder, +} = arcGisContentSlice.actions; export default arcGisContentSlice.reducer; diff --git a/src/utils/arcgis.ts b/src/utils/arcgis.ts index b8bbcb6a..95398320 100644 --- a/src/utils/arcgis.ts +++ b/src/utils/arcgis.ts @@ -101,6 +101,10 @@ export const arcGisRequestLogout = async () => { return await updateSessionInfo(); }; +/** + * Gets the ArcGIS user's content list. + * @returns The content list containig enough info to load the content items. + */ export const getArcGisUserContent = async (): Promise => { const contentItems: ArcGisContent[] = []; const authentication = getArcGisSession(); @@ -113,11 +117,13 @@ export const getArcGisUserContent = async (): Promise => { item.typeKeywords && item.typeKeywords.includes("Hosted Service") ) { + const token = await authentication.getToken(item.url); const contentItem: ArcGisContent = { id: item.id, name: item.title, url: item.url, created: item.created, + token: token, }; contentItems.push(contentItem); } From 64b8274ff2a703fbf369e185aa6eef5a937ef2d2 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Mon, 25 Dec 2023 12:57:23 +0300 Subject: [PATCH 04/11] sorting on columns added, fixed dev review issues --- __mocks__/@esri/arcgis-rest-portal.ts | 28 +++++ __mocks__/@esri/arcgis-rest-request.ts | 27 +++-- .../arcgis-import-panel.tsx | 63 +++++++--- src/components/modal-dialog/modal-dialog.tsx | 6 +- src/pages/debug-app/debug-app.tsx | 18 +-- src/redux/slices/arcgis-content-slice.spec.ts | 111 ++++++++++++++++++ src/redux/slices/arcgis-content-slice.ts | 67 ++++++++--- src/types.ts | 1 + .../{arcgis-auth.spec.ts => arcgis.spec.ts} | 26 ++++ src/utils/arcgis.ts | 5 +- 10 files changed, 290 insertions(+), 62 deletions(-) create mode 100644 __mocks__/@esri/arcgis-rest-portal.ts create mode 100644 src/redux/slices/arcgis-content-slice.spec.ts rename src/utils/{arcgis-auth.spec.ts => arcgis.spec.ts} (63%) diff --git a/__mocks__/@esri/arcgis-rest-portal.ts b/__mocks__/@esri/arcgis-rest-portal.ts new file mode 100644 index 00000000..a278e2e4 --- /dev/null +++ b/__mocks__/@esri/arcgis-rest-portal.ts @@ -0,0 +1,28 @@ +const mockContent = { + items: [ + { + id: "new-york", + name: "NewYork.slpk", + url: "https://123.com", + created: 123456, + type: "Scene Service", + typeKeywords: "This is a Hosted Service", + title: "New York", + token: "token-https://123.com", + }, + { + id: "turanga-library", + name: "TurangaLibrary.slpk", + url: "https://456.com", + created: 123457, + type: "Scene Service", + typeKeywords: "This is a Hosted Service", + title: "Turanga Library", + token: "token-https://456.com", + }, + ], +}; + +export const getUserContent = async (authentication) => { + return mockContent; +}; diff --git a/__mocks__/@esri/arcgis-rest-request.ts b/__mocks__/@esri/arcgis-rest-request.ts index 391ef811..45ca3611 100644 --- a/__mocks__/@esri/arcgis-rest-request.ts +++ b/__mocks__/@esri/arcgis-rest-request.ts @@ -1,17 +1,22 @@ const mockEmailExpected = "usermail@gmail.com"; const mockSessionExpected = '{"usermail": "usermail"}'; +const mockTokenExpectedPrefix = "token-"; + +const session = { + usermail: mockEmailExpected, + serialize: () => { + return mockSessionExpected; + }, + getUser: async () => { + return { email: mockEmailExpected }; + }, + getToken: async (url: string) => { + return mockTokenExpectedPrefix + url; + }, +}; export class ArcGISIdentityManager { static beginOAuth2 = async () => { - const session = { - usermail: mockEmailExpected, - serialize: () => { - return mockSessionExpected; - }, - getUser: async () => { - return { email: mockEmailExpected }; - }, - }; return session; }; static completeOAuth2 = async () => { @@ -20,7 +25,7 @@ export class ArcGISIdentityManager { static destroy = async () => { return; }; - static deserialize = (session) => { - return { usermail: "usermail" }; + static deserialize = () => { + return session; }; } diff --git a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx index d828978e..30de6fd9 100644 --- a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx +++ b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx @@ -8,16 +8,28 @@ import { ModalDialog } from "../../modal-dialog/modal-dialog"; import { selectArcGisContent, selectArcGisContentSelected, + selectSortAscending, + selectSortColumn, + selectStatus, + setSortAscending, + setSortColumn, setArcGisContentSelected, resetArcGisContentSelected, - setSortOrder, } from "../../../redux/slices/arcgis-content-slice"; +import { LoadingSpinner } from "../../loading-spinner/loading-spinner"; type InsertLayerProps = { onImport: (object: { name: string; url: string; token?: string }) => void; onCancel: () => void; }; +const SpinnerContainer = styled.div<{ visible: boolean }>` + position: absolute; + left: calc(50% - 22px); + top: calc(50% - 22px); + opacity: ${({ visible }) => (visible ? 1 : 0)}; +`; + const Table = styled.div` width: 584px; display: flex; @@ -112,24 +124,29 @@ const DateContainer = styled.div` justify-content: start; align-items: center; gap: 4px; + cursor: pointer; `; -const IconContainer = styled.div` +const IconContainer = styled.div<{ enabled: boolean }>` display: flex; justify-content: center; align-items: center; padding-top: 2px; width: 16px; height: 16px; - cursor: pointer; fill: ${({ theme }) => theme.colors.buttonDimIconColor}; + visibility: ${({ enabled }) => (enabled ? "visible" : "hidden")}; `; export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { const dispatch = useAppDispatch(); const arcGisContentArray = useAppSelector(selectArcGisContent); const arcGisContentSelected = useAppSelector(selectArcGisContentSelected); - const [sortDateOrder, setSortDateOrder] = useState(false); + const sortAscending = useAppSelector(selectSortAscending); + const sortColumn = useAppSelector(selectSortColumn); + const loadingStatus = useAppSelector(selectStatus); + + const isLoading = loadingStatus === "loading"; useEffect(() => { dispatch(resetArcGisContentSelected()); @@ -144,9 +161,12 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { } }; - const onSort = () => { - setSortDateOrder((prevValue) => !prevValue); - dispatch(setSortOrder(sortDateOrder)); + const onSort = (dataColumnName: string) => { + if (sortColumn === dataColumnName) { + dispatch(setSortAscending(!sortAscending)); + } else { + dispatch(setSortColumn(dataColumnName)); + } }; const formatDate = (date: number) => { @@ -168,25 +188,40 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { > - Title + + onSort("title")}> + Title + + + + + + - + onSort("created")}> Date - + - + + + + + {arcGisContentArray.map((contentItem) => { const isMapSelected = arcGisContentSelected === contentItem.id; return ( - + { @@ -204,7 +239,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { /> - {contentItem.name} + {contentItem.title} {formatDate(contentItem.created)} diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx index 08d3a1aa..be908100 100644 --- a/src/components/modal-dialog/modal-dialog.tsx +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -129,17 +129,15 @@ export const ModalDialog = ({ {children} - {cancelButtonText ? ( + {cancelButtonText && ( {cancelButtonText} - ) : ( - <> )} - {okButtonText}{" "} + {okButtonText} diff --git a/src/pages/debug-app/debug-app.tsx b/src/pages/debug-app/debug-app.tsx index 154c6224..a04edb3c 100644 --- a/src/pages/debug-app/debug-app.tsx +++ b/src/pages/debug-app/debug-app.tsx @@ -447,22 +447,6 @@ export const DebugApp = () => { } }; - // Need? onArcGisInsertHandler - const onArcGisImportHandler = ( - newLayer: LayerExample, - bookmarks?: Bookmark[] - ) => { - const newExamples = [...examples, newLayer]; - setExamples(newExamples); - const newActiveLayers = handleSelectAllLeafsInGroup(newLayer); - setActiveLayers(newActiveLayers); - setPreventTransitions(false); - - if (bookmarks?.length) { - updateBookmarks(bookmarks); - } - }; - const onLayerSelectHandler = ( layer: LayerExample, rootLayer?: LayerExample @@ -775,7 +759,7 @@ export const DebugApp = () => { pageId={PageId.debug} layers={examples} selectedLayerIds={selectedLayerIds} - onArcGisImport={onArcGisImportHandler} + onArcGisImport={onLayerInsertHandler} onLayerInsert={onLayerInsertHandler} onLayerSelect={onLayerSelectHandler} onLayerDelete={(id) => onLayerDeleteHandler(id)} diff --git a/src/redux/slices/arcgis-content-slice.spec.ts b/src/redux/slices/arcgis-content-slice.spec.ts new file mode 100644 index 00000000..04e14742 --- /dev/null +++ b/src/redux/slices/arcgis-content-slice.spec.ts @@ -0,0 +1,111 @@ +import { setupStore } from "../store"; +import reducer, { + selectArcGisContentSelected, + selectSortAscending, + selectSortColumn, + selectStatus, + setSortAscending, + setSortColumn, + setArcGisContentSelected, + resetArcGisContentSelected, + addArcGisContent, + deleteArcGisContent, +} from "./arcgis-content-slice"; + +import { getArcGisUserContent } from "../../utils/arcgis"; + +jest.mock("../../utils/arcgis"); + +const getArcGisUserContentMock = + getArcGisUserContent as unknown as jest.Mocked; + +const mockEmailExpected = "usermail@gmail.com"; +let mockStorageUserinfo = mockEmailExpected; +const mockInitValueExpected = { + arcGisContent: [], + arcGisContentSelected: "", + sortColumn: "", + sortAscending: false, + status: "idle", +}; + +const contentItem = { + id: "123", + name: "NewYork.slpk", + url: "https://123.com", + created: 123456, + title: "New York", + token: "token-https://123.com", +}; + +describe("slice: arcgis-content", () => { + beforeAll(() => { + getArcGisUserContentMock.mockImplementation(async () => { + mockStorageUserinfo = mockEmailExpected; + return mockStorageUserinfo; + }); + }); + + beforeEach(() => { + mockStorageUserinfo = mockEmailExpected; + }); + + it("Reducer should return the initial state", () => { + expect(reducer(undefined, { type: undefined })).toEqual( + mockInitValueExpected + ); + }); + + it("Selector should return the initial state", () => { + const store = setupStore(); + const state = store.getState(); + expect(selectStatus(state)).toEqual("idle"); + }); + + it("Selector should return the updated sort column", () => { + const store = setupStore(); + store.dispatch(setSortColumn("title")); + const state = store.getState(); + expect(selectSortColumn(state)).toEqual("title"); + }); + + it("Selector should return the updated sort order", () => { + const store = setupStore(); + store.dispatch(setSortAscending(true)); + const state = store.getState(); + expect(selectSortAscending(state)).toEqual(true); + }); + + it("Selector should return empty string if no content added", () => { + const store = setupStore(); + store.dispatch(setArcGisContentSelected("123")); + const state = store.getState(); + expect(selectArcGisContentSelected(state)).toEqual(""); + }); + + it("Selector should return id of selected item", () => { + const store = setupStore(); + store.dispatch(addArcGisContent(contentItem)); + store.dispatch(setArcGisContentSelected("123")); + const state = store.getState(); + expect(selectArcGisContentSelected(state)).toEqual("123"); + }); + + it("Selector should return empty string if no content added", () => { + const store = setupStore(); + store.dispatch(addArcGisContent(contentItem)); + store.dispatch(setArcGisContentSelected("123")); + store.dispatch(deleteArcGisContent("123")); + const state = store.getState(); + expect(selectArcGisContentSelected(state)).toEqual(""); + }); + + it("Selector should return empty string", () => { + const store = setupStore(); + store.dispatch(addArcGisContent(contentItem)); + store.dispatch(setArcGisContentSelected("123")); + store.dispatch(resetArcGisContentSelected()); + const state = store.getState(); + expect(selectArcGisContentSelected(state)).toEqual(""); + }); +}); diff --git a/src/redux/slices/arcgis-content-slice.ts b/src/redux/slices/arcgis-content-slice.ts index a0db830a..bfad934d 100644 --- a/src/redux/slices/arcgis-content-slice.ts +++ b/src/redux/slices/arcgis-content-slice.ts @@ -7,18 +7,36 @@ import { getArcGisUserContent } from "../../utils/arcgis"; interface ArcGisContentState { arcGisContent: ArcGisContent[]; arcGisContentSelected: string; - sortOrder: boolean; + sortColumn: string; + sortAscending: boolean; + status: "idle" | "loading"; } + const initialState: ArcGisContentState = { arcGisContent: [], arcGisContentSelected: "", - sortOrder: false, + sortColumn: "", + sortAscending: false, + status: "idle", }; -const sortList = (state) => { - state.arcGisContent.sort((a: ArcGisContent, b: ArcGisContent) => - state.sortOrder ? a.created - b.created : b.created - a.created - ); +const sortList = (state: ArcGisContentState) => { + const column = state.sortColumn; + if (column) { + state.arcGisContent.sort((a: ArcGisContent, b: ArcGisContent) => { + let ac = a[column]; + let bc = b[column]; + if (typeof ac === "string") { + ac = ac.toLowerCase(); + bc = bc.toLowerCase(); + } + if (ac === bc) { + return 0; + } + const comp = state.sortAscending ? ac > bc : ac < bc; + return comp ? -1 : 1; + }); + } }; const arcGisContentSlice = createSlice({ @@ -29,15 +47,22 @@ const arcGisContentSlice = createSlice({ return initialState; }, - setSortOrder: ( + setSortAscending: ( state: ArcGisContentState, action: PayloadAction ) => { - state.sortOrder = action.payload; + state.sortAscending = action.payload; + sortList(state); + }, + + setSortColumn: ( + state: ArcGisContentState, + action: PayloadAction + ) => { + state.sortColumn = action.payload; sortList(state); }, - // Content addArcGisContent: ( state: ArcGisContentState, action: PayloadAction @@ -45,6 +70,7 @@ const arcGisContentSlice = createSlice({ state.arcGisContent.push(action.payload); sortList(state); }, + deleteArcGisContent: ( state: ArcGisContentState, action: PayloadAction @@ -58,7 +84,6 @@ const arcGisContentSlice = createSlice({ sortList(state); }, - // Content Selected setArcGisContentSelected: ( state: ArcGisContentState, action: PayloadAction @@ -70,14 +95,21 @@ const arcGisContentSlice = createSlice({ state.arcGisContentSelected = action.payload; } }, + resetArcGisContentSelected: (state: ArcGisContentState) => { state.arcGisContentSelected = ""; }, }, + extraReducers: (builder) => { - builder.addCase(getArcGisContent.fulfilled, (state, action) => { - state.arcGisContent = [...action.payload]; - }); + builder + .addCase(getArcGisContent.fulfilled, (state, action) => { + state.arcGisContent = [...action.payload]; + state.status = "idle"; + }) + .addCase(getArcGisContent.pending, (state) => { + state.status = "loading"; + }); }, }); @@ -93,6 +125,12 @@ export const selectArcGisContent = (state: RootState): ArcGisContent[] => state.arcGisContent.arcGisContent; export const selectArcGisContentSelected = (state: RootState): string => state.arcGisContent.arcGisContentSelected; +export const selectSortAscending = (state: RootState): boolean => + state.arcGisContent.sortAscending; +export const selectSortColumn = (state: RootState): string => + state.arcGisContent.sortColumn; +export const selectStatus = (state: RootState): string => + state.arcGisContent.status; export const { setInitialArcGisContent, @@ -100,7 +138,8 @@ export const { setArcGisContentSelected, resetArcGisContentSelected, deleteArcGisContent, - setSortOrder, + setSortAscending, + setSortColumn, } = arcGisContentSlice.actions; export default arcGisContentSlice.reducer; diff --git a/src/types.ts b/src/types.ts index 825444e4..7bf7e7b0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -372,6 +372,7 @@ export type ArcGisContent = { id: string; url: string; name: string; + title: string; token?: string; created: number; }; diff --git a/src/utils/arcgis-auth.spec.ts b/src/utils/arcgis.spec.ts similarity index 63% rename from src/utils/arcgis-auth.spec.ts rename to src/utils/arcgis.spec.ts index fe223bcb..e0784d7b 100644 --- a/src/utils/arcgis-auth.spec.ts +++ b/src/utils/arcgis.spec.ts @@ -2,12 +2,32 @@ import { getAuthenticatedUser, arcGisRequestLogin, arcGisRequestLogout, + getArcGisUserContent, } from "./arcgis"; jest.mock("@esri/arcgis-rest-request"); +jest.mock("@esri/arcgis-rest-portal"); const ARCGIS_REST_USER_INFO = "__ARCGIS_REST_USER_INFO__"; const mockEmailExpected = "usermail@gmail.com"; +const mockContentExpected = [ + { + id: "new-york", + name: "NewYork.slpk", + url: "https://123.com", + created: 123456, + title: "New York", + token: "token-https://123.com", + }, + { + id: "turanga-library", + name: "TurangaLibrary.slpk", + url: "https://456.com", + created: 123457, + title: "Turanga Library", + token: "token-https://456.com", + }, +]; let OLD_ENV = {}; @@ -41,4 +61,10 @@ describe("ArcGIS auth functions", () => { const email = await arcGisRequestLogout(); expect(email).toBe(""); }); + + it("Should return content with token", async () => { + const email = await arcGisRequestLogin(); + const content = await getArcGisUserContent(); + expect(content).toEqual(mockContentExpected); + }); }); diff --git a/src/utils/arcgis.ts b/src/utils/arcgis.ts index 95398320..e59a21de 100644 --- a/src/utils/arcgis.ts +++ b/src/utils/arcgis.ts @@ -103,7 +103,7 @@ export const arcGisRequestLogout = async () => { /** * Gets the ArcGIS user's content list. - * @returns The content list containig enough info to load the content items. + * @returns The content list containig the necessay info to load the content items. */ export const getArcGisUserContent = async (): Promise => { const contentItems: ArcGisContent[] = []; @@ -120,7 +120,8 @@ export const getArcGisUserContent = async (): Promise => { const token = await authentication.getToken(item.url); const contentItem: ArcGisContent = { id: item.id, - name: item.title, + name: item.name || item.title, + title: item.title, url: item.url, created: item.created, token: token, From 8968ba5bbed7f199969678155f2b1e29f968c6fd Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Mon, 25 Dec 2023 15:26:54 +0300 Subject: [PATCH 05/11] fix dev review issues --- .../arcgis-import-panel/arcgis-import-panel.tsx | 16 ++++++---------- src/components/modal-dialog/modal-dialog.tsx | 9 +++------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx index 30de6fd9..0f704d50 100644 --- a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx +++ b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx @@ -82,11 +82,7 @@ const TableContent = styled.div` max-height: 300px; `; -type ContainerProps = { - checked: boolean; -}; - -const TableRow = styled.div` +const TableRow = styled.div<{ checked: boolean }>` display: flex; flex-direction: row; justify-content: start; @@ -118,7 +114,7 @@ const Radio = styled.div` height: 44px; `; -const DateContainer = styled.div` +const TitleCellContainer = styled.div` display: flex; flex-direction: row; justify-content: start; @@ -189,7 +185,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => {
- onSort("title")}> + onSort("title")}> Title { transform={sortAscending ? "" : "rotate(180)"} /> - + - onSort("created")}> + onSort("created")}> Date { transform={sortAscending ? "" : "rotate(180)"} /> - + diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx index be908100..c25df8d8 100644 --- a/src/components/modal-dialog/modal-dialog.tsx +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -50,6 +50,7 @@ const IconContainer = styled.div` right: 14px; width: 44px; height: 44px; + cursor: pointer; `; const ContentContainer = styled.div` @@ -97,7 +98,6 @@ type LogoutPanelProps = { }; const CloseCrossButton = styled(CloseIcon)` - cursor: pointer; &:hover { fill: ${({ theme }) => theme.colors.mainDimColorInverted}; } @@ -118,11 +118,8 @@ export const ModalDialog = ({ - - + + {title} From 2d9725916e8cb627bc7c5ebbc33e6a10ae3395e7 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Fri, 29 Dec 2023 13:21:03 +0300 Subject: [PATCH 06/11] fix table and tests --- public/icons/arrow-down.svg | 3 - .../comparison-side/comparison-side.tsx | 23 +-- src/components/expand-icon/expand-icon.tsx | 21 ++- .../arcgis-control-panel.spec.tsx | 150 ++++++++++++++++ .../layers-panel/arcgis-control-panel.tsx | 133 ++++++++++++++ .../arcgis-import-panel.tsx | 169 ++++++++++++------ .../layers-control-panel.spec.tsx | 141 +-------------- .../layers-panel/layers-control-panel.tsx | 95 +--------- .../layers-panel/layers-panel.spec.tsx | 2 - src/components/layers-panel/layers-panel.tsx | 32 ++-- src/pages/comparison/e2e.comparison.spec.ts | 2 +- src/pages/debug-app/debug-app.tsx | 1 - src/pages/debug-app/e2e.debug-app.spec.ts | 2 +- src/pages/viewer-app/e2e.viewer-app.spec.ts | 2 +- src/pages/viewer-app/viewer-app.tsx | 19 +- src/redux/slices/arcgis-content-slice.spec.ts | 141 ++++++++++----- src/redux/slices/arcgis-content-slice.ts | 56 +++--- src/types.ts | 2 + src/utils/testing-utils/e2e-layers-panel.tsx | 12 +- 19 files changed, 554 insertions(+), 452 deletions(-) delete mode 100644 public/icons/arrow-down.svg create mode 100644 src/components/layers-panel/arcgis-control-panel.spec.tsx create mode 100644 src/components/layers-panel/arcgis-control-panel.tsx diff --git a/public/icons/arrow-down.svg b/public/icons/arrow-down.svg deleted file mode 100644 index 8d4acfdb..00000000 --- a/public/icons/arrow-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/components/comparison/comparison-side/comparison-side.tsx b/src/components/comparison/comparison-side/comparison-side.tsx index 20af34e1..b10277c0 100644 --- a/src/components/comparison/comparison-side/comparison-side.tsx +++ b/src/components/comparison/comparison-side/comparison-side.tsx @@ -376,27 +376,7 @@ export const ComparisonSide = ({ ); }; - // TODO: Do we need it? Why don't we use onLayerInsertHandler? - const onArcGisInsertHandler = ( - newLayer: LayerExample, - bookmarks?: Bookmark[] -) => { - const newExamples = [...examples, newLayer]; - setExamples(newExamples); - const newActiveLayers = handleSelectAllLeafsInGroup(newLayer); - const newActiveLayersIds = newActiveLayers.map((layer) => layer.id); - setActiveLayers(newActiveLayers); - onChangeLayers && onChangeLayers(newExamples, newActiveLayersIds); - - /** - * There is no sense to use webscene bookmarks in across layers mode. - */ - if (bookmarks?.length) { - onInsertBookmarks && onInsertBookmarks(bookmarks); - } -}; - -const onLayerInsertHandler = ( + const onLayerInsertHandler = ( newLayer: LayerExample, bookmarks?: Bookmark[] ) => { @@ -534,7 +514,6 @@ const onLayerInsertHandler = ( ? ComparisonSideMode.left : side } - onArcGisImport={onArcGisInsertHandler} onLayerInsert={onLayerInsertHandler} onLayerSelect={onLayerSelectHandler} onLayerDelete={(id) => onLayerDeleteHandler(id)} diff --git a/src/components/expand-icon/expand-icon.tsx b/src/components/expand-icon/expand-icon.tsx index 45ce62dd..e826462a 100644 --- a/src/components/expand-icon/expand-icon.tsx +++ b/src/components/expand-icon/expand-icon.tsx @@ -47,6 +47,17 @@ const IconButton = styled.div<{ } `; +const IconButtonContainer = styled.div<{ + width: number; + height: number; +}>` + display: flex; + justify-content: center; + align-items: center; + width: ${({ width }) => `${width}px`}} + height: ${({ height }) => `${height}px`}} +`; + type ExpandIconProps = { /** expanded/collapsed */ expandState: ExpandState; @@ -56,6 +67,10 @@ type ExpandIconProps = { fillExpanded?: string; /** icon color for collapsed state */ fillCollapsed?: string; + /** Width of the icon */ + width?: number; + /** Height of the icon */ + height?: number; /** click event handler */ onClick: (e: SyntheticEvent) => void; }; @@ -65,6 +80,8 @@ export const ExpandIcon = ({ fillExpanded, fillCollapsed, collapseDirection = CollapseDirection.top, + width = 8, + height = 12, }: ExpandIconProps) => { return ( - + + + ); }; diff --git a/src/components/layers-panel/arcgis-control-panel.spec.tsx b/src/components/layers-panel/arcgis-control-panel.spec.tsx new file mode 100644 index 00000000..5e09438e --- /dev/null +++ b/src/components/layers-panel/arcgis-control-panel.spec.tsx @@ -0,0 +1,150 @@ +import { renderWithThemeProviders } from "../../utils/testing-utils/render-with-theme"; +import { screen, within } from "@testing-library/react"; +import { setupStore } from "../../redux/store"; +import { ArcGisControlPanel } from "./arcgis-control-panel"; +import userEvent from "@testing-library/user-event"; +import { + arcGisLogin, + arcGisLogout, +} from "../../redux/slices/arcgis-auth-slice"; +import { + getAuthenticatedUser, + arcGisRequestLogin, + arcGisCompleteLogin, + arcGisRequestLogout, +} from "../../utils/arcgis"; + +jest.mock("../../utils/arcgis"); + +const getAuthenticatedUserMock = + getAuthenticatedUser as unknown as jest.Mocked; +const arcGisRequestLoginMock = + arcGisRequestLogin as unknown as jest.Mocked; +const arcGisCompleteLoginMock = + arcGisCompleteLogin as unknown as jest.Mocked; +const arcGisRequestLogoutMock = + arcGisRequestLogout as unknown as jest.Mocked; + +const EMAIL_EXPECTED = "usermail@gmail.com"; +let storageUserinfo = ""; + +const onArcGisImportMock = jest.fn(); + +const callRender = (renderFunc, props = {}, store = setupStore()) => { + return renderFunc( + , + store + ); +}; + +describe("Layers Control Panel - ArcGIS auth", () => { + beforeAll(() => { + arcGisRequestLoginMock.mockImplementation(async () => { + storageUserinfo = EMAIL_EXPECTED; + return storageUserinfo; + }); + arcGisCompleteLoginMock.mockImplementation(async () => { + return storageUserinfo; + }); + arcGisRequestLogoutMock.mockImplementation(async () => { + storageUserinfo = ""; + return storageUserinfo; + }); + getAuthenticatedUserMock.mockImplementation(() => { + return storageUserinfo; + }); + }); + + it("Should render ArcGIS Login button", async () => { + const store = setupStore(); + // Let's Log out... + await store.dispatch(arcGisLogout()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + expect(arcGisRequestLogoutMock).toHaveBeenCalledTimes(1); + + // We are in the "Logged out" state, so the "Log in" button should be there. + const loginButton = await screen.findByText("Login to ArcGIS"); + expect(loginButton).toBeInTheDocument(); + loginButton && userEvent.click(loginButton); + expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); + + const importButton = screen.queryByText("Import from ArcGIS"); + expect(importButton).not.toBeInTheDocument(); + }); + + it("Should render ArcGIS Import and Logout buttons", async () => { + const store = setupStore(); + // Let's Log in... + await store.dispatch(arcGisLogin()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); + + // We are in the "Logged in" state, so the "Log in" button should NOT be there. + const importButton = await screen.findByText("Import from ArcGIS"); + expect(importButton).toBeInTheDocument(); + + const logoutUserInfo = await screen.findByText(EMAIL_EXPECTED); + expect(logoutUserInfo).toBeInTheDocument(); + + const loginButton = screen.queryByText("Login to ArcGIS"); + expect(loginButton).not.toBeInTheDocument(); + }); + + it("Should respond to action on the ArcGIS Login button", async () => { + const store = setupStore(); + // Let's Log out... + await store.dispatch(arcGisLogout()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + + const loginButton = screen.getByText("Login to ArcGIS"); + loginButton && userEvent.click(loginButton); + expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); + + const importButton = await screen.findByText("Import from ArcGIS"); + expect(importButton).toBeInTheDocument(); + + const loginButtonHidden = screen.queryByText("Login to ArcGIS"); + expect(loginButtonHidden).not.toBeInTheDocument(); + }); + + it("Should respond to action on ArcGIS Logout button", async () => { + const store = setupStore(); + // Let's Log in... + await store.dispatch(arcGisLogin()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + + const logoutButton = await screen.findByTestId("userinfo-button"); + logoutButton && userEvent.click(logoutButton); + + const modalDialog = await screen.findByTestId("modal-dialog-content"); + expect(modalDialog).toContainHTML("Are you sure you want to log out?"); + + const cancelButton = within(modalDialog).getByText("Log out"); + cancelButton && userEvent.click(cancelButton); + + const modalDialogHidden = screen.queryByTestId("modal-dialog-content"); + expect(modalDialogHidden).not.toBeInTheDocument(); + + const loginButton = await screen.findByText("Login to ArcGIS"); + expect(loginButton).toBeInTheDocument(); + }); +}); diff --git a/src/components/layers-panel/arcgis-control-panel.tsx b/src/components/layers-panel/arcgis-control-panel.tsx new file mode 100644 index 00000000..ec4b07a1 --- /dev/null +++ b/src/components/layers-panel/arcgis-control-panel.tsx @@ -0,0 +1,133 @@ +import { useState } from "react"; +import styled from "styled-components"; +import ImportIcon from "../../../public/icons/import.svg"; +import { AcrGisUser } from "../arcgis-user/arcgis-user"; +import { + arcGisLogin, + arcGisLogout, + selectUser, +} from "../../redux/slices/arcgis-auth-slice"; +import { getArcGisContent } from "../../redux/slices/arcgis-content-slice"; +import { ModalDialog } from "../modal-dialog/modal-dialog"; +import EsriImage from "../../../public/images/esri.svg"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { ActionIconButton } from "../action-icon-button/action-icon-button"; +import { ButtonSize } from "../../types"; +import { ArcGisImportPanel } from "./arcgis-import-panel/arcgis-import-panel"; + +type ArcGisControlPanelProps = { + onArcGisImportClick: (layer: { + name: string; + url: string; + token?: string; + }) => void; +}; + +const ActionButtonsContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; + row-gap: 8px; + margin-top: 8px; +`; + +const ActionIconButtonContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; +`; + +const EsriStyledImage = styled(EsriImage)` + margin-left: 16px; + fill: ${({ theme }) => theme.colors.esriImageColor}; +`; + +const TextInfo = styled.div` + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 19px; +`; + +const TextUser = styled.div` + font-style: normal; + font-weight: 700; + font-size: 16px; + line-height: 19px; +`; + +export const ArcGisControlPanel = ({ + onArcGisImportClick, +}: ArcGisControlPanelProps) => { + const dispatch = useAppDispatch(); + + const username = useAppSelector(selectUser); + const isLoggedIn = !!username; + + const [showLogoutWarning, setShowLogoutWarning] = useState(false); + const [showArcGisImportPanel, setShowArcGisImportPanel] = useState(false); + + const onArcGisActionClick = () => { + if (isLoggedIn) { + dispatch(getArcGisContent()); + setShowArcGisImportPanel(true); + } else { + dispatch(arcGisLogin()); + } + }; + const onArcGisLogoutClick = () => { + setShowLogoutWarning(true); + }; + + return ( + <> + + + + {isLoggedIn ? "Import from ArcGIS" : "Login to ArcGIS"} + + + + + {isLoggedIn && ( + {username} + )} + + + {showArcGisImportPanel && ( + { + onArcGisImportClick(item); + setShowArcGisImportPanel(false); + }} + onCancel={() => setShowArcGisImportPanel(false)} + /> + )} + + {showLogoutWarning && ( + { + dispatch(arcGisLogout()); + setShowLogoutWarning(false); + }} + onCancel={() => { + setShowLogoutWarning(false); + }} + > + Are you sure you want to log out? + You are logged in as + {username} + + )} + + ); +}; diff --git a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx index 0f704d50..1dcdd7f8 100644 --- a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx +++ b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx @@ -1,8 +1,6 @@ import styled, { css, useTheme } from "styled-components"; import { RadioButton } from "../../radio-button/radio-button"; -import { Fragment, useEffect, useState } from "react"; -import { PanelHorizontalLine } from "../../common"; -import SortDownIcon from "../../../../public/icons/arrow-down.svg"; +import { Fragment, useEffect } from "react"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; import { ModalDialog } from "../../modal-dialog/modal-dialog"; import { @@ -16,12 +14,13 @@ import { setArcGisContentSelected, resetArcGisContentSelected, } from "../../../redux/slices/arcgis-content-slice"; +import { + ArcGisContentColumnName, + ExpandState, + CollapseDirection, +} from "../../../types"; import { LoadingSpinner } from "../../loading-spinner/loading-spinner"; - -type InsertLayerProps = { - onImport: (object: { name: string; url: string; token?: string }) => void; - onCancel: () => void; -}; +import { ExpandIcon } from "../../expand-icon/expand-icon"; const SpinnerContainer = styled.div<{ visible: boolean }>` position: absolute; @@ -30,50 +29,57 @@ const SpinnerContainer = styled.div<{ visible: boolean }>` opacity: ${({ visible }) => (visible ? 1 : 0)}; `; -const Table = styled.div` +const Table = styled.table` width: 584px; - display: flex; - flex-direction: column; - justify-content: start; - row-gap: 16px; + border-collapse: collapse; `; -const TableHeader = styled.div` +const TableHeader = styled.thead` font-style: normal; font-weight: 500; font-size: 16px; line-height: 19px; + color: ${({ theme }) => theme.colors.secondaryFontColor}; overflow: hidden; - display: flex; - flex-direction: row; - justify-content: start; - column-gap: 24px; `; -const TableHeaderItem2 = styled.div` - margin-left: 68px; +const TableHeaderItem1 = styled.th` + width: 44px; + padding: 0; +`; +const TableHeaderItem2 = styled.th` width: 343px; + padding: 0; `; -const TableHeaderItem3 = styled.div` +const TableHeaderItem3 = styled.th` width: 149px; + padding: 0; `; -const TableRowItem1 = styled.div` +const TableRowItem1 = styled.td` width: 44px; + padding: 0; `; -const TableRowItem2 = styled.div` - font-weight: 700; +const TableRowItem2 = styled.td` width: 343px; + padding: 0; + font-weight: 700; `; -const TableRowItem3 = styled.div` +const TableRowItem3 = styled.td` width: 149px; + padding: 0; `; -const TableContent = styled.div` +const CellDiv = styled.div` display: flex; flex-direction: column; - justify-content: start; - row-gap: 8px; + justify-content: center; + align-items: start; + margin: 8px 0 8px 0; + height: 44px; +`; + +const TableContent = styled.tbody` font-style: normal; font-weight: 500; font-size: 16px; @@ -82,26 +88,46 @@ const TableContent = styled.div` max-height: 300px; `; -const TableRow = styled.div<{ checked: boolean }>` - display: flex; - flex-direction: row; - justify-content: start; - align-items: center; - column-gap: 24px; - +const TableRow = styled.tr<{ checked: boolean }>` background: transparent; cursor: pointer; + border: 0; + border-style: solid; + border-bottom: 1px solid + ${({ theme }) => `${theme.colors.mainHiglightColorInverted}1f`}; + border-radius: 1px; + ${({ checked }) => checked && css` - background: ${({ theme }) => theme.colors.mainHiglightColor}; + > * > :first-child { + background: ${({ theme }) => theme.colors.mainHiglightColor}; + } + > :first-child > :first-child { + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + } + > :last-child > :first-child { + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + } + box-shadow: 0px 17px 80px rgba(0, 0, 0, 0.1); - border-radius: 8px; `} &:hover { - background: ${({ theme }) => theme.colors.mainDimColor}; + > * > :first-child { + background: ${({ theme }) => theme.colors.mainDimColor}; + } + > :first-child > :first-child { + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + } + > :last-child > :first-child { + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + } + box-shadow: 0px 17px 80px rgba(0, 0, 0, 0.1); - border-radius: 8px; } `; @@ -127,13 +153,18 @@ const IconContainer = styled.div<{ enabled: boolean }>` display: flex; justify-content: center; align-items: center; - padding-top: 2px; + margin-top: 2px; width: 16px; height: 16px; fill: ${({ theme }) => theme.colors.buttonDimIconColor}; visibility: ${({ enabled }) => (enabled ? "visible" : "hidden")}; `; +type InsertLayerProps = { + onImport: (object: { name: string; url: string; token?: string }) => void; + onCancel: () => void; +}; + export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { const dispatch = useAppDispatch(); const arcGisContentArray = useAppSelector(selectArcGisContent); @@ -157,7 +188,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { } }; - const onSort = (dataColumnName: string) => { + const onSort = (dataColumnName: ArcGisContentColumnName) => { if (sortColumn === dataColumnName) { dispatch(setSortAscending(!sortAscending)); } else { @@ -184,13 +215,21 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { >
+ onSort("title")}> Title - onSort("title")} + fillExpanded={theme.colors.buttonDimIconColor} + width={6} /> @@ -200,14 +239,22 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { onSort("created")}> Date - onSort("created")} + fillExpanded={theme.colors.buttonDimIconColor} + width={6} /> + @@ -225,22 +272,26 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { }} > - - { - dispatch(setArcGisContentSelected(contentItem.id)); - }} - /> - + + + { + dispatch(setArcGisContentSelected(contentItem.id)); + }} + /> + + - {contentItem.title} + + + {contentItem.title} + - {formatDate(contentItem.created)} + {formatDate(contentItem.created)} - ); })} diff --git a/src/components/layers-panel/layers-control-panel.spec.tsx b/src/components/layers-panel/layers-control-panel.spec.tsx index b78d0db5..a1b1eeb4 100644 --- a/src/components/layers-panel/layers-control-panel.spec.tsx +++ b/src/components/layers-panel/layers-control-panel.spec.tsx @@ -1,11 +1,6 @@ -import { act, screen, within } from "@testing-library/react"; +import { act, screen } from "@testing-library/react"; import { renderWithThemeProviders } from "../../utils/testing-utils/render-with-theme"; import { LayersControlPanel } from "./layers-control-panel"; -import userEvent from "@testing-library/user-event"; -import { - arcGisLogin, - arcGisLogout, -} from "../../redux/slices/arcgis-auth-slice"; // Mocked components import { ActionIconButton } from "../action-icon-button/action-icon-button"; @@ -14,18 +9,10 @@ import { LayerOptionsMenu } from "./layer-options-menu/layer-options-menu"; import { ListItem } from "./list-item/list-item"; import { setupStore } from "../../redux/store"; -import { - getAuthenticatedUser, - arcGisRequestLogin, - arcGisCompleteLogin, - arcGisRequestLogout, -} from "../../utils/arcgis"; - jest.mock("./list-item/list-item"); jest.mock("../action-icon-button/action-icon-button"); jest.mock("./delete-confirmation"); jest.mock("./layer-options-menu/layer-options-menu"); -jest.mock("../../utils/arcgis"); const ListItemMock = ListItem as unknown as jest.Mocked; const PlusButtonMock = ActionIconButton as unknown as jest.Mocked; @@ -33,31 +20,17 @@ const DeleteConfirmationMock = DeleteConfirmation as unknown as jest.Mocked; const LayerOptionsMenuMock = LayerOptionsMenu as unknown as jest.Mocked; -const getAuthenticatedUserMock = - getAuthenticatedUser as unknown as jest.Mocked; -const arcGisRequestLoginMock = - arcGisRequestLogin as unknown as jest.Mocked; -const arcGisCompleteLoginMock = - arcGisCompleteLogin as unknown as jest.Mocked; -const arcGisRequestLogoutMock = - arcGisRequestLogout as unknown as jest.Mocked; - -const mockEmailExpected = "usermail@gmail.com"; -let mockStorageUserinfo = ""; - const onInsertLayerMock = jest.fn(); const onInsertSceneMock = jest.fn(); const onDeleteLayerMock = jest.fn(); const onSelectLayerMock = jest.fn(); const onLayerSettingsClickMock = jest.fn(); const onPointToLayerMock = jest.fn(); -const onArcGisImportMock = jest.fn(); const callRender = (renderFunc, props = {}, store = setupStore()) => { return renderFunc( { LayerOptionsMenuMock.mockImplementation(() =>
Layers Options
); }); -describe("Layers Control Panel - ArcGIS auth", () => { - beforeAll(() => { - arcGisRequestLoginMock.mockImplementation(async () => { - mockStorageUserinfo = mockEmailExpected; - return mockStorageUserinfo; - }); - arcGisCompleteLoginMock.mockImplementation(async () => { - return mockStorageUserinfo; - }); - arcGisRequestLogoutMock.mockImplementation(async () => { - mockStorageUserinfo = ""; - return mockStorageUserinfo; - }); - getAuthenticatedUserMock.mockImplementation(() => { - return mockStorageUserinfo; - }); - }); - - it("Should render ArcGIS Login button", async () => { - const store = setupStore(); - // Let's Log out... - await store.dispatch(arcGisLogout()); - const { container } = callRender( - renderWithThemeProviders, - undefined, - store - ); - expect(container).toBeInTheDocument(); - expect(arcGisRequestLogoutMock).toHaveBeenCalledTimes(1); - - // We are in the "Logged out" state, so the "Log in" button should be there. - const loginButton = await screen.findByText("Login to ArcGIS"); - expect(loginButton).toBeInTheDocument(); - loginButton && userEvent.click(loginButton); - expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); - - const importButton = screen.queryByText("Import from ArcGIS"); - expect(importButton).not.toBeInTheDocument(); - }); - - it("Should render ArcGIS Import and Logout buttons", async () => { - const store = setupStore(); - // Let's Log in... - await store.dispatch(arcGisLogin()); - const { container } = callRender( - renderWithThemeProviders, - undefined, - store - ); - expect(container).toBeInTheDocument(); - expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); - - // We are in the "Logged in" state, so the "Log in" button should NOT be there. - const importButton = await screen.findByText("Import from ArcGIS"); - expect(importButton).toBeInTheDocument(); - - const logoutUserInfo = await screen.findByText(mockEmailExpected); - expect(logoutUserInfo).toBeInTheDocument(); - - const loginButton = screen.queryByText("Login to ArcGIS"); - expect(loginButton).not.toBeInTheDocument(); - }); - - it("Should respond to action on the ArcGIS Login button", async () => { - const store = setupStore(); - // Let's Log out... - await store.dispatch(arcGisLogout()); - const { container } = callRender( - renderWithThemeProviders, - undefined, - store - ); - expect(container).toBeInTheDocument(); - - const loginButton = screen.getByText("Login to ArcGIS"); - loginButton && userEvent.click(loginButton); - expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); - - const importButton = await screen.findByText("Import from ArcGIS"); - expect(importButton).toBeInTheDocument(); - - const loginButtonHidden = screen.queryByText("Login to ArcGIS"); - expect(loginButtonHidden).not.toBeInTheDocument(); - }); - - it("Should respond to action on ArcGIS Logout button", async () => { - const store = setupStore(); - // Let's Log in... - await store.dispatch(arcGisLogin()); - const { container } = callRender( - renderWithThemeProviders, - undefined, - store - ); - - const logoutButton = await screen.findByTestId("userinfo-button"); - logoutButton && userEvent.click(logoutButton); - - const modalDialog = await screen.findByTestId("modal-dialog-content"); - expect(modalDialog).toContainHTML("Are you sure you want to log out?"); - - const cancelButton = within(modalDialog).getByText("Log out"); - cancelButton && userEvent.click(cancelButton); - - const modalDialogHidden = screen.queryByTestId("modal-dialog-content"); - expect(modalDialogHidden).not.toBeInTheDocument(); - - const loginButton = await screen.findByText("Login to ArcGIS"); - expect(loginButton).toBeInTheDocument(); - }); -}); - describe("Layers Control Panel", () => { it("Should render LayersControlPanel without layers", () => { const store = setupStore(); diff --git a/src/components/layers-panel/layers-control-panel.tsx b/src/components/layers-panel/layers-control-panel.tsx index 8a421dff..4caf5dfb 100644 --- a/src/components/layers-panel/layers-control-panel.tsx +++ b/src/components/layers-panel/layers-control-panel.tsx @@ -8,22 +8,11 @@ import { } from "../../types"; import { ListItem } from "./list-item/list-item"; import PlusIcon from "../../../public/icons/plus.svg"; -import ImportIcon from "../../../public/icons/import.svg"; -import EsriImage from "../../../public/images/esri.svg"; import { ActionIconButton } from "../action-icon-button/action-icon-button"; -import { AcrGisUser } from "../arcgis-user/arcgis-user"; import { DeleteConfirmation } from "./delete-confirmation"; import { LayerOptionsMenu } from "./layer-options-menu/layer-options-menu"; import { handleSelectAllLeafsInGroup } from "../../utils/layer-utils"; import { ButtonSize } from "../../types"; -import { PanelHorizontalLine } from "../common"; -import { - arcGisLogin, - arcGisLogout, - selectUser, -} from "../../redux/slices/arcgis-auth-slice"; -import { useAppDispatch, useAppSelector } from "../../redux/hooks"; -import { ModalDialog } from "../modal-dialog/modal-dialog"; type LayersControlPanelProps = { layers: LayerExample[]; @@ -33,7 +22,6 @@ type LayersControlPanelProps = { onLayerSelect: (layer: LayerExample, rootLayer?: LayerExample) => void; onLayerInsertClick: () => void; onSceneInsertClick: () => void; - onArcGisImportClick: () => void; onLayerSettingsClick: ReactEventHandler; onPointToLayer: (viewState?: LayerViewState) => void; deleteLayer: (id: string) => void; @@ -57,6 +45,7 @@ const InsertButtons = styled.div` flex-direction: column; row-gap: 8px; margin-top: 8px; + margin-bottom: 8px; `; const ChildrenContainer = styled.div` @@ -67,32 +56,6 @@ const ChildrenContainer = styled.div` padding-left: 12px; `; -const ActionIconButtonContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: start; - align-items: center; -`; - -const EsriStyledImage = styled(EsriImage)` - margin-left: 16px; - fill: ${({ theme }) => theme.colors.esriImageColor}; -`; - -const TextInfo = styled.div` - font-style: normal; - font-weight: 500; - font-size: 16px; - line-height: 19px; -`; - -const TextUser = styled.div` - font-style: normal; - font-weight: 700; - font-size: 16px; - line-height: 19px; -`; - export const LayersControlPanel = ({ layers, type, @@ -101,32 +64,14 @@ export const LayersControlPanel = ({ hasSettings = false, onLayerInsertClick, onSceneInsertClick, - onArcGisImportClick, onLayerSettingsClick, onPointToLayer, deleteLayer, }: LayersControlPanelProps) => { - const dispatch = useAppDispatch(); - - const username = useAppSelector(selectUser); - const isLoggedIn = !!username; - - const [showLogoutWarning, setShowLogoutWarning] = useState(false); const [settingsLayerId, setSettingsLayerId] = useState(""); const [showLayerSettings, setShowLayerSettings] = useState(false); const [layerToDeleteId, setLayerToDeleteId] = useState(""); - const onArcGisActionClick = () => { - if (isLoggedIn) { - onArcGisImportClick(); - } else { - dispatch(arcGisLogin()); - } - }; - const onArcGisLogoutClick = () => { - setShowLogoutWarning(true); - }; - const isListItemSelected = ( layer: LayerExample, parentLayer?: LayerExample @@ -254,44 +199,6 @@ export const LayersControlPanel = ({ > Insert scene - - - - - - {!isLoggedIn && "Login to ArcGIS"} - {isLoggedIn && "Import from ArcGIS"} - - - - - {isLoggedIn && ( - {username} - )} - - {showLogoutWarning && ( - { - dispatch(arcGisLogout()); - setShowLogoutWarning(false); - }} - onCancel={() => { - setShowLogoutWarning(false); - }} - > - Are you sure you want to log out? - You are logged in as - {username} - - )} ); diff --git a/src/components/layers-panel/layers-panel.spec.tsx b/src/components/layers-panel/layers-panel.spec.tsx index cdc9e86a..67d496ce 100644 --- a/src/components/layers-panel/layers-panel.spec.tsx +++ b/src/components/layers-panel/layers-panel.spec.tsx @@ -73,7 +73,6 @@ beforeAll(() => { )); }); -const arcGisImportMock = jest.fn(); const layerInsertMock = jest.fn(); const layerSelectMock = jest.fn(); const layerDeleteMock = jest.fn(); @@ -91,7 +90,6 @@ const callRender = (renderFunc, props = {}, store = setupStore()) => { sublayers={[]} selectedLayerIds={[]} type={0} - onArcGisImport={arcGisImportMock} onLayerInsert={layerInsertMock} onLayerSelect={layerSelectMock} onLayerDelete={layerDeleteMock} diff --git a/src/components/layers-panel/layers-panel.tsx b/src/components/layers-panel/layers-panel.tsx index 0c310b15..3cfb1478 100644 --- a/src/components/layers-panel/layers-panel.tsx +++ b/src/components/layers-panel/layers-panel.tsx @@ -19,6 +19,7 @@ import { import { CloseButton } from "../close-button/close-button"; import { InsertPanel } from "./insert-panel/insert-panel"; import { LayersControlPanel } from "./layers-control-panel"; +import { ArcGisControlPanel } from "./arcgis-control-panel"; import { MapOptionPanel } from "./map-options-panel"; import { PanelContainer, @@ -38,8 +39,6 @@ import { getTilesetType } from "../../utils/url-utils"; import { convertArcGisSlidesToBookmars } from "../../utils/bookmarks-utils"; import { useAppDispatch } from "../../redux/hooks"; import { addBaseMap } from "../../redux/slices/base-maps-slice"; -import { getArcGisContent } from "../../redux/slices/arcgis-content-slice"; -import { ArcGisImportPanel } from "./arcgis-import-panel/arcgis-import-panel"; const EXISTING_AREA_ERROR = "You are trying to add an existing area to the map"; @@ -151,7 +150,6 @@ type LayersPanelProps = { viewWidth?: number; viewHeight?: number; side?: ComparisonSideMode; - onArcGisImport: (layer: LayerExample, bookmarks?: Bookmark[]) => void; onLayerInsert: (layer: LayerExample, bookmarks?: Bookmark[]) => void; onLayerSelect: (layer: LayerExample, rootLayer?: LayerExample) => void; onLayerDelete: (id: string) => void; @@ -168,7 +166,6 @@ export const LayersPanel = ({ layers, sublayers, selectedLayerIds, - onArcGisImport, onLayerInsert, onLayerSelect, onLayerDelete, @@ -187,7 +184,6 @@ export const LayersPanel = ({ const [tab, setTab] = useState(Tabs.Layers); const [showLayerInsertPanel, setShowLayerInsertPanel] = useState(false); const [showSceneInsertPanel, setShowSceneInsertPanel] = useState(false); - const [showArcGisImportPanel, setShowArcGisImportPanel] = useState(false); const [showLayerSettings, setShowLayerSettings] = useState(false); const [showInsertMapPanel, setShowInsertMapPanel] = useState(false); const [showExistedError, setShowExistedError] = useState(false); @@ -199,7 +195,7 @@ export const LayersPanel = ({ useState(false); const [warningNode, setWarningNode] = useState(null); const [showAddingSlidesWarning, setShowAddingSlidesWarning] = useState(false); - + useClickOutside([warningNode], () => setShowExistedError(false)); const handleArcGisImport = (layer: { @@ -212,7 +208,6 @@ export const LayersPanel = ({ ); if (existedLayer) { - setShowArcGisImportPanel(false); setShowExistedError(true); return; } @@ -225,8 +220,7 @@ export const LayersPanel = ({ type: getTilesetType(layer.url), }; - onArcGisImport(newLayer); - setShowArcGisImportPanel(false); + onLayerInsert(newLayer); }; const handleInsertLayer = (layer: { @@ -402,11 +396,6 @@ export const LayersPanel = ({ onLayerSelect={onLayerSelect} onLayerInsertClick={() => setShowLayerInsertPanel(true)} onSceneInsertClick={() => setShowSceneInsertPanel(true)} - onArcGisImportClick={() => { - dispatch(getArcGisContent()); - setShowArcGisImportPanel(true) - } - } onLayerSettingsClick={() => setShowLayerSettings(true)} onPointToLayer={onPointToLayer} deleteLayer={onLayerDelete} @@ -418,6 +407,13 @@ export const LayersPanel = ({ /> )} + + + + + + + {showExistedError && ( setWarningNode(element)}> )} - {showArcGisImportPanel && ( - - handleArcGisImport(item)} - onCancel={() => setShowArcGisImportPanel(false)} - /> - - )} )} {showLayerSettings && ( diff --git a/src/pages/comparison/e2e.comparison.spec.ts b/src/pages/comparison/e2e.comparison.spec.ts index dc16bde1..89adc288 100644 --- a/src/pages/comparison/e2e.comparison.spec.ts +++ b/src/pages/comparison/e2e.comparison.spec.ts @@ -282,7 +282,7 @@ describe("Compare - Map Control Panel", () => { // Dropdown button const dropdownButton = await page.$eval( - `${panelId} > :first-child > svg`, + `${panelId} > :first-child > :first-child > svg`, (node) => node.innerHTML ); expect(dropdownButton).toBe(chevronSvgHtml); diff --git a/src/pages/debug-app/debug-app.tsx b/src/pages/debug-app/debug-app.tsx index a04edb3c..a743648e 100644 --- a/src/pages/debug-app/debug-app.tsx +++ b/src/pages/debug-app/debug-app.tsx @@ -759,7 +759,6 @@ export const DebugApp = () => { pageId={PageId.debug} layers={examples} selectedLayerIds={selectedLayerIds} - onArcGisImport={onLayerInsertHandler} onLayerInsert={onLayerInsertHandler} onLayerSelect={onLayerSelectHandler} onLayerDelete={(id) => onLayerDeleteHandler(id)} diff --git a/src/pages/debug-app/e2e.debug-app.spec.ts b/src/pages/debug-app/e2e.debug-app.spec.ts index 22f916bd..a1dedc16 100644 --- a/src/pages/debug-app/e2e.debug-app.spec.ts +++ b/src/pages/debug-app/e2e.debug-app.spec.ts @@ -333,7 +333,7 @@ describe("Debug - Map Control Panel", () => { // Dropdown button const dropdownButton = await page.$eval( - `${panelId} > :first-child > svg`, + `${panelId} > :first-child > :first-child > svg`, (node) => node.innerHTML ); expect(dropdownButton).toBe(chevronSvgHtml); diff --git a/src/pages/viewer-app/e2e.viewer-app.spec.ts b/src/pages/viewer-app/e2e.viewer-app.spec.ts index f34bf9a0..78537766 100644 --- a/src/pages/viewer-app/e2e.viewer-app.spec.ts +++ b/src/pages/viewer-app/e2e.viewer-app.spec.ts @@ -163,7 +163,7 @@ describe("Viewer - Map Control Panel", () => { // Dropdown button const dropdownButton = await page.$eval( - `${panelId} > :first-child > svg`, + `${panelId} > :first-child > :first-child > svg`, (node) => node.innerHTML ); expect(dropdownButton).toBe(chevronSvgHtml); diff --git a/src/pages/viewer-app/viewer-app.tsx b/src/pages/viewer-app/viewer-app.tsx index f4c88c71..80dfb736 100644 --- a/src/pages/viewer-app/viewer-app.tsx +++ b/src/pages/viewer-app/viewer-app.tsx @@ -326,23 +326,7 @@ export const ViewerApp = () => { } }; -// Need? -const onArcGisImportHandler = ( - newLayer: LayerExample, - bookmarks?: Bookmark[] -) => { - const newExamples = [...examples, newLayer]; - setExamples(newExamples); - const newActiveLayers = handleSelectAllLeafsInGroup(newLayer); - setActiveLayers(newActiveLayers); - setPreventTransitions(false); - - if (bookmarks?.length) { - updateBookmarks(bookmarks); - } -}; - -const onLayerSelectHandler = ( + const onLayerSelectHandler = ( layer: LayerExample, rootLayer?: LayerExample ) => { @@ -623,7 +607,6 @@ const onLayerSelectHandler = ( pageId={PageId.viewer} layers={examples} selectedLayerIds={selectedLayerIds} - onArcGisImport={onArcGisImportHandler} onLayerInsert={onLayerInsertHandler} onLayerSelect={onLayerSelectHandler} onLayerDelete={(id) => onLayerDeleteHandler(id)} diff --git a/src/redux/slices/arcgis-content-slice.spec.ts b/src/redux/slices/arcgis-content-slice.spec.ts index 04e14742..3b66de69 100644 --- a/src/redux/slices/arcgis-content-slice.spec.ts +++ b/src/redux/slices/arcgis-content-slice.spec.ts @@ -1,5 +1,6 @@ import { setupStore } from "../store"; import reducer, { + selectArcGisContent, selectArcGisContentSelected, selectSortAscending, selectSortColumn, @@ -8,51 +9,92 @@ import reducer, { setSortColumn, setArcGisContentSelected, resetArcGisContentSelected, - addArcGisContent, - deleteArcGisContent, + getArcGisContent, } from "./arcgis-content-slice"; import { getArcGisUserContent } from "../../utils/arcgis"; jest.mock("../../utils/arcgis"); - const getArcGisUserContentMock = getArcGisUserContent as unknown as jest.Mocked; -const mockEmailExpected = "usermail@gmail.com"; -let mockStorageUserinfo = mockEmailExpected; -const mockInitValueExpected = { +const CONTENT_EXPECTED = [ + { + id: "123", + name: "123.slpk", + url: "https://123.com", + created: 123453, + title: "City-123", + token: "token-https://123.com", + }, + { + id: "789", + name: "789.slpk", + url: "https://789.com", + created: 123457, + title: "City-789", + token: "token-https://789.com", + }, + { + id: "456", + name: "456.slpk", + url: "https://456.com", + created: 123454, + title: "City-456", + token: "token-https://456.com", + }, +]; + +const CONTENT_SORTED_EXPECTED = [ + { + id: "123", + name: "123.slpk", + url: "https://123.com", + created: 123453, + title: "City-123", + token: "token-https://123.com", + }, + { + id: "456", + name: "456.slpk", + url: "https://456.com", + created: 123454, + title: "City-456", + token: "token-https://456.com", + }, + { + id: "789", + name: "789.slpk", + url: "https://789.com", + created: 123457, + title: "City-789", + token: "token-https://789.com", + }, +]; + +const INIT_VALUE_EXPECTED = { arcGisContent: [], arcGisContentSelected: "", - sortColumn: "", - sortAscending: false, + sortColumn: null, + sortAscending: true, status: "idle", }; -const contentItem = { - id: "123", - name: "NewYork.slpk", - url: "https://123.com", - created: 123456, - title: "New York", - token: "token-https://123.com", -}; +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} describe("slice: arcgis-content", () => { beforeAll(() => { getArcGisUserContentMock.mockImplementation(async () => { - mockStorageUserinfo = mockEmailExpected; - return mockStorageUserinfo; + sleep(10); + return CONTENT_EXPECTED; }); }); - beforeEach(() => { - mockStorageUserinfo = mockEmailExpected; - }); - it("Reducer should return the initial state", () => { expect(reducer(undefined, { type: undefined })).toEqual( - mockInitValueExpected + INIT_VALUE_EXPECTED ); }); @@ -62,18 +104,11 @@ describe("slice: arcgis-content", () => { expect(selectStatus(state)).toEqual("idle"); }); - it("Selector should return the updated sort column", () => { - const store = setupStore(); - store.dispatch(setSortColumn("title")); - const state = store.getState(); - expect(selectSortColumn(state)).toEqual("title"); - }); - - it("Selector should return the updated sort order", () => { + it("Selector should return 'loading' status", () => { const store = setupStore(); - store.dispatch(setSortAscending(true)); + /* No await! */ store.dispatch(getArcGisContent()); const state = store.getState(); - expect(selectSortAscending(state)).toEqual(true); + expect(selectStatus(state)).toEqual("loading"); }); it("Selector should return empty string if no content added", () => { @@ -83,29 +118,45 @@ describe("slice: arcgis-content", () => { expect(selectArcGisContentSelected(state)).toEqual(""); }); - it("Selector should return id of selected item", () => { + it("Selector should return id of selected item", async () => { const store = setupStore(); - store.dispatch(addArcGisContent(contentItem)); - store.dispatch(setArcGisContentSelected("123")); + await store.dispatch(getArcGisContent()); + store.dispatch(setArcGisContentSelected("456")); const state = store.getState(); - expect(selectArcGisContentSelected(state)).toEqual("123"); + expect(selectArcGisContentSelected(state)).toEqual("456"); }); - it("Selector should return empty string if no content added", () => { + it("Selector should return empty string if nothing is selected", async () => { const store = setupStore(); - store.dispatch(addArcGisContent(contentItem)); + await store.dispatch(getArcGisContent()); store.dispatch(setArcGisContentSelected("123")); - store.dispatch(deleteArcGisContent("123")); + store.dispatch(resetArcGisContentSelected()); const state = store.getState(); expect(selectArcGisContentSelected(state)).toEqual(""); }); - it("Selector should return empty string", () => { + it("Selector should return content received (unsorted)", async () => { const store = setupStore(); - store.dispatch(addArcGisContent(contentItem)); - store.dispatch(setArcGisContentSelected("123")); - store.dispatch(resetArcGisContentSelected()); + await store.dispatch(getArcGisContent()); const state = store.getState(); - expect(selectArcGisContentSelected(state)).toEqual(""); + expect(selectArcGisContent(state)).toEqual(CONTENT_EXPECTED); + expect(selectStatus(state)).toEqual("idle"); + }); + + it("Selector should return content received (sorted)", async () => { + const store = setupStore(); + store.dispatch(setSortColumn("title")); + store.dispatch(setSortAscending(true)); + + await store.dispatch(getArcGisContent()); + let state = store.getState(); + expect(selectSortColumn(state)).toEqual("title"); + expect(selectSortAscending(state)).toEqual(true); + expect(selectArcGisContent(state)).toEqual(CONTENT_SORTED_EXPECTED); + + store.dispatch(setSortColumn("created")); + store.dispatch(setSortAscending(true)); + state = store.getState(); + expect(selectArcGisContent(state)).toEqual(CONTENT_SORTED_EXPECTED); }); }); diff --git a/src/redux/slices/arcgis-content-slice.ts b/src/redux/slices/arcgis-content-slice.ts index bfad934d..f671afeb 100644 --- a/src/redux/slices/arcgis-content-slice.ts +++ b/src/redux/slices/arcgis-content-slice.ts @@ -1,22 +1,27 @@ import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit"; -import { ArcGisContent } from "../../types"; +import { ArcGisContent, ArcGisContentColumnName } from "../../types"; import { RootState } from "../store"; import { getArcGisUserContent } from "../../utils/arcgis"; // Define a type for the slice state interface ArcGisContentState { + /** Array of user's content items taken from ArcGIS */ arcGisContent: ArcGisContent[]; + /** Content item selected in UI */ arcGisContentSelected: string; - sortColumn: string; + /** Column name to sort the list by */ + sortColumn: ArcGisContentColumnName | null; + /** Sort order: 'ascending' - true, 'descending' - false */ sortAscending: boolean; + /** Content loading status: when in progress - 'loading', otherwise - 'idle' */ status: "idle" | "loading"; } const initialState: ArcGisContentState = { arcGisContent: [], arcGisContentSelected: "", - sortColumn: "", - sortAscending: false, + sortColumn: null, + sortAscending: true, status: "idle", }; @@ -26,7 +31,10 @@ const sortList = (state: ArcGisContentState) => { state.arcGisContent.sort((a: ArcGisContent, b: ArcGisContent) => { let ac = a[column]; let bc = b[column]; - if (typeof ac === "string") { + if (ac === undefined || bc === undefined || ac === null || bc === null) { + return 0; + } + if (typeof ac === "string" && typeof bc === "string") { ac = ac.toLowerCase(); bc = bc.toLowerCase(); } @@ -34,7 +42,7 @@ const sortList = (state: ArcGisContentState) => { return 0; } const comp = state.sortAscending ? ac > bc : ac < bc; - return comp ? -1 : 1; + return comp ? 1 : -1; }); } }; @@ -43,10 +51,6 @@ const arcGisContentSlice = createSlice({ name: "arcGisContent", initialState, reducers: { - setInitialArcGisContent: () => { - return initialState; - }, - setSortAscending: ( state: ArcGisContentState, action: PayloadAction @@ -55,35 +59,16 @@ const arcGisContentSlice = createSlice({ sortList(state); }, + // Note, sortColumn will never be set to its initial value (null). + // It's done on purpose. We don't support a scenario to get back to the unsorted content list. setSortColumn: ( state: ArcGisContentState, - action: PayloadAction + action: PayloadAction ) => { state.sortColumn = action.payload; sortList(state); }, - addArcGisContent: ( - state: ArcGisContentState, - action: PayloadAction - ) => { - state.arcGisContent.push(action.payload); - sortList(state); - }, - - deleteArcGisContent: ( - state: ArcGisContentState, - action: PayloadAction - ) => { - state.arcGisContent = state.arcGisContent.filter( - (map) => map.id !== action.payload - ); - if (state.arcGisContentSelected === action.payload) { - state.arcGisContentSelected = ""; - } - sortList(state); - }, - setArcGisContentSelected: ( state: ArcGisContentState, action: PayloadAction @@ -105,6 +90,7 @@ const arcGisContentSlice = createSlice({ builder .addCase(getArcGisContent.fulfilled, (state, action) => { state.arcGisContent = [...action.payload]; + sortList(state); state.status = "idle"; }) .addCase(getArcGisContent.pending, (state) => { @@ -123,21 +109,19 @@ export const getArcGisContent = createAsyncThunk( export const selectArcGisContent = (state: RootState): ArcGisContent[] => state.arcGisContent.arcGisContent; + export const selectArcGisContentSelected = (state: RootState): string => state.arcGisContent.arcGisContentSelected; export const selectSortAscending = (state: RootState): boolean => state.arcGisContent.sortAscending; -export const selectSortColumn = (state: RootState): string => +export const selectSortColumn = (state: RootState): ArcGisContentColumnName | null => state.arcGisContent.sortColumn; export const selectStatus = (state: RootState): string => state.arcGisContent.status; export const { - setInitialArcGisContent, - addArcGisContent, setArcGisContentSelected, resetArcGisContentSelected, - deleteArcGisContent, setSortAscending, setSortColumn, } = arcGisContentSlice.actions; diff --git a/src/types.ts b/src/types.ts index 7bf7e7b0..2d3afb23 100644 --- a/src/types.ts +++ b/src/types.ts @@ -376,3 +376,5 @@ export type ArcGisContent = { token?: string; created: number; }; + +export type ArcGisContentColumnName = keyof ArcGisContent; diff --git a/src/utils/testing-utils/e2e-layers-panel.tsx b/src/utils/testing-utils/e2e-layers-panel.tsx index 20c1d604..0ea50430 100644 --- a/src/utils/testing-utils/e2e-layers-panel.tsx +++ b/src/utils/testing-utils/e2e-layers-panel.tsx @@ -110,7 +110,7 @@ export const inserAndDeleteLayer = async ( `${panelId} > :nth-child(4) > :first-child > :nth-child(2) > :first-child` ); await insertButton.click(); - let insertPanel = await page.$(`${panelId} > :nth-child(5)`); + let insertPanel = await page.$(`${panelId} > :nth-child(7)`); // Header const insertPanelHeaderText = await insertPanel.$eval( @@ -156,19 +156,19 @@ export const inserAndDeleteLayer = async ( `${panelId} form.insert-form button[type='submit']` ); await submitInsert.click(); - await page.waitForSelector(`${panelId} > :nth-child(5)`); - const warningPanel = await page.$(`${panelId} > :nth-child(5)`); + await page.waitForSelector(`${panelId} > :nth-child(7)`); + const warningPanel = await page.$(`${panelId} > :nth-child(7)`); const warningText = await warningPanel.$eval( `:first-child > :first-child`, (node) => node.innerText ); expect(warningText).toBe("You are trying to add an existing area to the map"); await expect(page).toClick("button", { text: "Ok" }); - let anyExtraPanel = await page.$(`${panelId} > :nth-child(5)`); + let anyExtraPanel = await page.$(`${panelId} > :nth-child(7)`); expect(anyExtraPanel).toBeNull(); await insertButton.click(); - insertPanel = await page.$(`${panelId} > :nth-child(5)`); + insertPanel = await page.$(`${panelId} > :nth-child(7)`); // Add layer await fillForm(page, `${panelId} form.insert-form`, { @@ -180,7 +180,7 @@ export const inserAndDeleteLayer = async ( `${panelId} form.insert-form button[type='submit']` ); await submitInsert.click(); - anyExtraPanel = await page.$(`${panelId} > :nth-child(5)`); + anyExtraPanel = await page.$(`${panelId} > :nth-child(7)`); expect(anyExtraPanel).toBeNull(); let layers = await page.$$( From 03eb72937ca0ba35d131be1877a2ced6642fad9d Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Fri, 29 Dec 2023 13:43:45 +0300 Subject: [PATCH 07/11] fix tests --- src/redux/slices/arcgis-auth-slice.spec.ts | 24 +++++++++++----------- src/utils/arcgis.spec.ts | 14 ++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/redux/slices/arcgis-auth-slice.spec.ts b/src/redux/slices/arcgis-auth-slice.spec.ts index b12b421d..62b42483 100644 --- a/src/redux/slices/arcgis-auth-slice.spec.ts +++ b/src/redux/slices/arcgis-auth-slice.spec.ts @@ -23,48 +23,48 @@ const arcGisCompleteLoginMock = const arcGisRequestLogoutMock = arcGisRequestLogout as unknown as jest.Mocked; -const mockEmailExpected = "usermail@gmail.com"; -let mockStorageUserinfo = mockEmailExpected; +const EMAIL_EXPECTED = "usermail@gmail.com"; +let storageUserinfo = EMAIL_EXPECTED; describe("slice: arcgis-auth", () => { beforeAll(() => { arcGisRequestLoginMock.mockImplementation(async () => { - mockStorageUserinfo = mockEmailExpected; - return mockStorageUserinfo; + storageUserinfo = EMAIL_EXPECTED; + return storageUserinfo; }); arcGisCompleteLoginMock.mockImplementation(async () => { - return mockStorageUserinfo; + return storageUserinfo; }); arcGisRequestLogoutMock.mockImplementation(async () => { - mockStorageUserinfo = ""; - return mockStorageUserinfo; + storageUserinfo = ""; + return storageUserinfo; }); getAuthenticatedUserMock.mockImplementation(() => { - return mockStorageUserinfo; + return storageUserinfo; }); }); beforeEach(() => { - mockStorageUserinfo = mockEmailExpected; + storageUserinfo = EMAIL_EXPECTED; }); it("Reducer should return the initial state", () => { expect(reducer(undefined, { type: undefined })).toEqual({ - user: mockEmailExpected, + user: EMAIL_EXPECTED, }); }); it("Selector should return the initial state", () => { const store = setupStore(); const state = store.getState(); - expect(selectUser(state)).toEqual(mockEmailExpected); + expect(selectUser(state)).toEqual(EMAIL_EXPECTED); }); it("Selector should return the updated value after Login", async () => { const store = setupStore(); await store.dispatch(arcGisLogin()); const state = store.getState(); - expect(selectUser(state)).toEqual(mockEmailExpected); + expect(selectUser(state)).toEqual(EMAIL_EXPECTED); }); it("Selector should return the updated value after Logout", async () => { diff --git a/src/utils/arcgis.spec.ts b/src/utils/arcgis.spec.ts index e0784d7b..2a95b573 100644 --- a/src/utils/arcgis.spec.ts +++ b/src/utils/arcgis.spec.ts @@ -9,8 +9,8 @@ jest.mock("@esri/arcgis-rest-request"); jest.mock("@esri/arcgis-rest-portal"); const ARCGIS_REST_USER_INFO = "__ARCGIS_REST_USER_INFO__"; -const mockEmailExpected = "usermail@gmail.com"; -const mockContentExpected = [ +const EMAIL_EXPECTED = "usermail@gmail.com"; +const CONTENT_EXPECTED = [ { id: "new-york", name: "NewYork.slpk", @@ -47,14 +47,14 @@ describe("ArcGIS auth functions", () => { }); it("Should return email of user logged in", () => { - localStorage.setItem(ARCGIS_REST_USER_INFO, mockEmailExpected); + localStorage.setItem(ARCGIS_REST_USER_INFO, EMAIL_EXPECTED); const email = getAuthenticatedUser(); - expect(email).toEqual(mockEmailExpected); + expect(email).toEqual(EMAIL_EXPECTED); }); it("Should request login and return email of user", async () => { const email = await arcGisRequestLogin(); - expect(email).toBe(mockEmailExpected); + expect(email).toBe(EMAIL_EXPECTED); }); it("Should request logout and return empty string", async () => { @@ -63,8 +63,8 @@ describe("ArcGIS auth functions", () => { }); it("Should return content with token", async () => { - const email = await arcGisRequestLogin(); + await arcGisRequestLogin(); const content = await getArcGisUserContent(); - expect(content).toEqual(mockContentExpected); + expect(content).toEqual(CONTENT_EXPECTED); }); }); From 31c0e51fb8a67f606da88e7215b9d0d0c06a849e Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Wed, 10 Jan 2024 23:27:04 +0300 Subject: [PATCH 08/11] fix dev review issues --- .../arcgis-control-panel.spec.tsx | 2 +- .../arcgis-import-panel.spec.tsx | 146 ++++++++++++ .../arcgis-import-panel.tsx | 224 ++++++++++-------- src/types.ts | 1 + src/utils/arcgis.ts | 33 ++- src/utils/format/format-utils.spec.ts | 14 +- src/utils/format/format-utils.ts | 14 ++ 7 files changed, 320 insertions(+), 114 deletions(-) create mode 100644 src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.spec.tsx diff --git a/src/components/layers-panel/arcgis-control-panel.spec.tsx b/src/components/layers-panel/arcgis-control-panel.spec.tsx index 5e09438e..1a7b1028 100644 --- a/src/components/layers-panel/arcgis-control-panel.spec.tsx +++ b/src/components/layers-panel/arcgis-control-panel.spec.tsx @@ -126,7 +126,7 @@ describe("Layers Control Panel - ArcGIS auth", () => { const store = setupStore(); // Let's Log in... await store.dispatch(arcGisLogin()); - const { container } = callRender( + callRender( renderWithThemeProviders, undefined, store diff --git a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.spec.tsx b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.spec.tsx new file mode 100644 index 00000000..a4b25961 --- /dev/null +++ b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.spec.tsx @@ -0,0 +1,146 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { renderWithThemeProviders } from "../../../utils/testing-utils/render-with-theme"; +import { ArcGisImportPanel } from "./arcgis-import-panel"; +import { setupStore } from "../../../redux/store"; +import { + selectArcGisContent, + getArcGisContent, +} from "../../../redux/slices/arcgis-content-slice"; +import { getArcGisUserContent } from "../../../utils/arcgis"; + +jest.mock("../../../utils/arcgis"); + +const getArcGisUserContentMock = + getArcGisUserContent as unknown as jest.Mocked; + +const onImportMock = jest.fn(); +const onCancelMock = jest.fn(); + +const CONTENT_EXPECTED = [ + { + id: "123", + name: "123.slpk", + url: "https://123.com", + created: 123453, + title: "City-123", + token: "token-https://123.com", + }, + { + id: "789", + name: "789.slpk", + url: "https://789.com", + created: 123457, + title: "City-789", + token: "token-https://789.com", + }, + { + id: "456", + name: "456.slpk", + url: "https://456.com", + created: 123454, + title: "City-456", + token: "token-https://456.com", + }, +]; + +const callRender = (renderFunc, props = {}, store = setupStore()) => { + return renderFunc( + , + store + ); +}; + +describe("Import panel", () => { + beforeAll(() => { + getArcGisUserContentMock.mockImplementation(async () => { + return CONTENT_EXPECTED; + }); + }); + + it("Should render import panel", async () => { + const store = setupStore(); + await store.dispatch(getArcGisContent()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + + expect(container).toBeInTheDocument(); + expect(screen.getByText("Select map to import")).toBeInTheDocument(); + expect(screen.getByText("Import Selected")).toBeInTheDocument(); + expect(screen.getByText("Title")).toBeInTheDocument(); + expect(screen.getByText("Date")).toBeInTheDocument(); + }); + + it("Should close the dialog", async () => { + const store = setupStore(); + callRender(renderWithThemeProviders, undefined, store); + + const cross = document.querySelector("svg"); + cross && userEvent.click(cross); + expect(onCancelMock).toHaveBeenCalledTimes(1); + }); + + it("Should import an item", async () => { + const store = setupStore(); + callRender(renderWithThemeProviders, undefined, store); + await store.dispatch(getArcGisContent()); + + const importSelected = screen.getByText("Import Selected"); + + // No item is selected yet. + importSelected && userEvent.click(importSelected); + expect(onImportMock).toHaveBeenCalledTimes(0); + + // Select an item to import + const row = screen.getByText("City-123"); + row && userEvent.click(row); + + importSelected && userEvent.click(importSelected); + expect(onImportMock).toHaveBeenCalledTimes(1); + }); + + it("Should change the sorting order", async () => { + const store = setupStore(); + callRender(renderWithThemeProviders, undefined, store); + await store.dispatch(getArcGisContent()); + + let state = store.getState(); + let cont = selectArcGisContent(state); + + const title = screen.getByText("Title"); + + title && userEvent.click(title); + state = store.getState(); + cont = selectArcGisContent(state); + expect(cont[0]["id"]).toBe("123"); + + title && userEvent.click(title); + state = store.getState(); + cont = selectArcGisContent(state); + expect(cont[0]["id"]).toBe("789"); + + const date = screen.getByText("Date"); + + date && userEvent.click(date); + state = store.getState(); + cont = selectArcGisContent(state); + expect(cont[0]["id"]).toBe("789"); + + date && userEvent.click(date); + state = store.getState(); + cont = selectArcGisContent(state); + expect(cont[0]["id"]).toBe("123"); + + date && userEvent.click(date); + state = store.getState(); + cont = selectArcGisContent(state); + expect(cont[0]["id"]).toBe("789"); + }); +}); diff --git a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx index 1dcdd7f8..7eeca31b 100644 --- a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx +++ b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx @@ -1,6 +1,6 @@ import styled, { css, useTheme } from "styled-components"; import { RadioButton } from "../../radio-button/radio-button"; -import { Fragment, useEffect } from "react"; +import { useEffect } from "react"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; import { ModalDialog } from "../../modal-dialog/modal-dialog"; import { @@ -15,6 +15,7 @@ import { resetArcGisContentSelected, } from "../../../redux/slices/arcgis-content-slice"; import { + ArcGisContent, ArcGisContentColumnName, ExpandState, CollapseDirection, @@ -43,31 +44,24 @@ const TableHeader = styled.thead` overflow: hidden; `; -const TableHeaderItem1 = styled.th` - width: 44px; - padding: 0; -`; -const TableHeaderItem2 = styled.th` - width: 343px; - padding: 0; -`; -const TableHeaderItem3 = styled.th` - width: 149px; - padding: 0; -`; +const TableHeaderRow = styled.tr``; -const TableRowItem1 = styled.td` - width: 44px; - padding: 0; -`; -const TableRowItem2 = styled.td` - width: 343px; +const TableHeaderItem = styled.th<{ width: number }>` + width: ${({ width }) => `${width}px`}; padding: 0; - font-weight: 700; `; -const TableRowItem3 = styled.td` - width: 149px; + +const TableRowItem = styled.td<{ + width: number; + fontWeight: number | undefined; +}>` + width: ${({ width }) => `${width}px`}; padding: 0; + ${({ fontWeight }) => + fontWeight !== undefined && + css` + font-weight: ${fontWeight}; + `} `; const CellDiv = styled.div` @@ -196,13 +190,91 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { } }; - const formatDate = (date: number) => { - const formatter = new Intl.DateTimeFormat("en-US", { - month: "long", - day: "2-digit", - year: "numeric", - }); - return formatter.format(date); + type Column = { + width: number; + fontWeight?: number; + dataColumnName?: ArcGisContentColumnName; + sortDataColumnName?: ArcGisContentColumnName; + columnName?: string; + }; + + const columns: Column[] = [ + { + width: 44, + }, + { + width: 343, + fontWeight: 700, + dataColumnName: "title", + columnName: "Title", + }, + { + width: 149, + dataColumnName: "createdFormatted", + sortDataColumnName: "created", + columnName: "Date", + }, + ]; + + const renderHeaderCell = (column: Column): JSX.Element => { + const sortDataColumnName = + column.sortDataColumnName || column.dataColumnName; + return ( + + {typeof sortDataColumnName !== "undefined" && ( + onSort(sortDataColumnName)}> + {column.columnName || ""} + + onSort(sortDataColumnName)} + fillExpanded={theme.colors.buttonDimIconColor} + width={6} + /> + + + )} + + ); + }; + + const renderRowCell = ( + column: Column, + contentItem: ArcGisContent, + isRowSelected: boolean + ): JSX.Element => { + const dataColumnName = column.dataColumnName; + return ( + + + {dataColumnName ? ( + contentItem[dataColumnName] + ) : ( + + { + dispatch(setArcGisContentSelected(contentItem.id)); + }} + /> + + )} + + + ); }; const theme = useTheme(); @@ -213,86 +285,32 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { onConfirm={handleImport} onCancel={onCancel} > + + + +
- - - onSort("title")}> - Title - - onSort("title")} - fillExpanded={theme.colors.buttonDimIconColor} - width={6} - /> - - - - - - onSort("created")}> - Date - - onSort("created")} - fillExpanded={theme.colors.buttonDimIconColor} - width={6} - /> - - - + + {columns.map((column: Column) => renderHeaderCell(column))} + - - - - - - + {arcGisContentArray.map((contentItem) => { - const isMapSelected = arcGisContentSelected === contentItem.id; + const isRowSelected = arcGisContentSelected === contentItem.id; return ( - - { - dispatch(setArcGisContentSelected(contentItem.id)); - }} - > - - - - { - dispatch(setArcGisContentSelected(contentItem.id)); - }} - /> - - - - - - {contentItem.title} - - - {formatDate(contentItem.created)} - - - + { + dispatch(setArcGisContentSelected(contentItem.id)); + }} + > + {columns.map((column: Column) => + renderRowCell(column, contentItem, isRowSelected) + )} + ); })} diff --git a/src/types.ts b/src/types.ts index 2d3afb23..8a2d2cef 100644 --- a/src/types.ts +++ b/src/types.ts @@ -375,6 +375,7 @@ export type ArcGisContent = { title: string; token?: string; created: number; + createdFormatted: string; }; export type ArcGisContentColumnName = keyof ArcGisContent; diff --git a/src/utils/arcgis.ts b/src/utils/arcgis.ts index e59a21de..d6d6fcca 100644 --- a/src/utils/arcgis.ts +++ b/src/utils/arcgis.ts @@ -1,7 +1,8 @@ import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"; -import { getUserContent } from "@esri/arcgis-rest-portal"; +import { getUserContent, IItem } from "@esri/arcgis-rest-portal"; import { ArcGisContent } from "../types"; +import { formatTimestamp } from "../utils/format/format-utils"; const ARCGIS_REST_USER_SESSION = "__ARCGIS_REST_USER_SESSION__"; const ARCGIS_REST_USER_INFO = "__ARCGIS_REST_USER_INFO__"; @@ -101,6 +102,27 @@ export const arcGisRequestLogout = async () => { return await updateSessionInfo(); }; +class ArcGisContentClass implements ArcGisContent { + id = ""; + url = ""; + name = ""; + title = ""; + token? = ""; + created = 0; + get createdFormatted(): string { + return formatTimestamp(this.created); + } + + constructor(item: IItem, token: string) { + this.id = item.id; + this.url = item.url || ""; + this.name = item.name || item.title; + this.title = item.title; + this.token = token; + this.created = item.created; + } +} + /** * Gets the ArcGIS user's content list. * @returns The content list containig the necessay info to load the content items. @@ -118,14 +140,7 @@ export const getArcGisUserContent = async (): Promise => { item.typeKeywords.includes("Hosted Service") ) { const token = await authentication.getToken(item.url); - const contentItem: ArcGisContent = { - id: item.id, - name: item.name || item.title, - title: item.title, - url: item.url, - created: item.created, - token: token, - }; + const contentItem: ArcGisContent = new ArcGisContentClass(item, token); contentItems.push(contentItem); } } diff --git a/src/utils/format/format-utils.spec.ts b/src/utils/format/format-utils.spec.ts index 935f9100..94d83dbb 100644 --- a/src/utils/format/format-utils.spec.ts +++ b/src/utils/format/format-utils.spec.ts @@ -1,4 +1,10 @@ -import { formatBoolean, formatFloatNumber, formatIntValue, formatStringValue } from "./format-utils"; +import { + formatBoolean, + formatFloatNumber, + formatIntValue, + formatStringValue, + formatTimestamp, +} from "./format-utils"; describe("Format Utils", () => { describe("formatStringValue", () => { @@ -54,4 +60,10 @@ describe("Format Utils", () => { expect(result1).toBe("No"); }); }); + + describe("formatTimestamp", () => { + it("Should return date formatted", () => { + expect(formatTimestamp(1704877308897)).toBe("January 10, 2024"); + }); + }); }); diff --git a/src/utils/format/format-utils.ts b/src/utils/format/format-utils.ts index cfeded16..d0a71f64 100644 --- a/src/utils/format/format-utils.ts +++ b/src/utils/format/format-utils.ts @@ -49,3 +49,17 @@ export const formatFloatNumber = (value: number): string => { export const formatBoolean = (value: boolean): string => { return value ? "Yes" : "No"; }; + +/** + * Formats a date according to "en-US" locale. + * @param timestamp - timestamp to convert to string. + * @returns date formatted. + */ +export const formatTimestamp = (timestamp: number): string => { + const formatter = new Intl.DateTimeFormat("en-US", { + month: "long", + day: "2-digit", + year: "numeric", + }); + return formatter.format(timestamp); +} From 75241f97448b2f400059b9c9de0f7c53452e9b55 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Thu, 11 Jan 2024 11:50:49 +0300 Subject: [PATCH 09/11] use handleInsertLayer for import as well --- src/components/layers-panel/layers-panel.tsx | 34 +++----------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/src/components/layers-panel/layers-panel.tsx b/src/components/layers-panel/layers-panel.tsx index 3cfb1478..17487a49 100644 --- a/src/components/layers-panel/layers-panel.tsx +++ b/src/components/layers-panel/layers-panel.tsx @@ -198,31 +198,6 @@ export const LayersPanel = ({ useClickOutside([warningNode], () => setShowExistedError(false)); - const handleArcGisImport = (layer: { - name: string; - url: string; - token?: string; - }) => { - const existedLayer = layers.some( - (exisLayer) => exisLayer.url.trim() === layer.url.trim() - ); - - if (existedLayer) { - setShowExistedError(true); - return; - } - - const id = layer.url.replace(/" "/g, "-"); - const newLayer: LayerExample = { - ...layer, - id, - custom: true, - type: getTilesetType(layer.url), - }; - - onLayerInsert(newLayer); - }; - const handleInsertLayer = (layer: { name: string; url: string; @@ -233,7 +208,6 @@ export const LayersPanel = ({ ); if (existedLayer) { - setShowLayerInsertPanel(false); setShowExistedError(true); return; } @@ -247,7 +221,6 @@ export const LayersPanel = ({ }; onLayerInsert(newLayer); - setShowLayerInsertPanel(false); }; const prepareLayerExamples = (layers: OperationalLayer[]): LayerExample[] => { @@ -411,7 +384,7 @@ export const LayersPanel = ({ - + {showExistedError && ( @@ -457,7 +430,10 @@ export const LayersPanel = ({ handleInsertLayer(layer)} + onInsert={(layer) => { + handleInsertLayer(layer); + setShowLayerInsertPanel(false); + }} onCancel={() => setShowLayerInsertPanel(false)} /> From aa0be786b086d35739dee268ca5c25e3be541b9b Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Thu, 11 Jan 2024 12:13:30 +0300 Subject: [PATCH 10/11] cosmetic change --- .../arcgis-import-panel/arcgis-import-panel.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx index 7eeca31b..a2e377ee 100644 --- a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx +++ b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx @@ -46,14 +46,14 @@ const TableHeader = styled.thead` const TableHeaderRow = styled.tr``; -const TableHeaderItem = styled.th<{ width: number }>` +const TableHeaderCell = styled.th<{ width: number }>` width: ${({ width }) => `${width}px`}; padding: 0; `; -const TableRowItem = styled.td<{ +const TableRowCell = styled.td<{ width: number; - fontWeight: number | undefined; + fontWeight?: number; }>` width: ${({ width }) => `${width}px`}; padding: 0; @@ -220,7 +220,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { const sortDataColumnName = column.sortDataColumnName || column.dataColumnName; return ( - @@ -242,7 +242,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { )} - + ); }; @@ -253,7 +253,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { ): JSX.Element => { const dataColumnName = column.dataColumnName; return ( - { )} - + ); }; From cfb06030cc2c20679bf0492ee3c1f7abed94c0b1 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Tue, 16 Jan 2024 20:13:59 +0300 Subject: [PATCH 11/11] fix dev review issues --- .../arcgis-import-panel.tsx | 70 +++++++++---------- src/redux/slices/arcgis-content-slice.ts | 14 ++-- src/types.ts | 6 +- src/utils/arcgis.ts | 11 ++- 4 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx index a2e377ee..4af11e31 100644 --- a/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx +++ b/src/components/layers-panel/arcgis-import-panel/arcgis-import-panel.tsx @@ -15,7 +15,7 @@ import { resetArcGisContentSelected, } from "../../../redux/slices/arcgis-content-slice"; import { - ArcGisContent, + IArcGisContent, ArcGisContentColumnName, ExpandState, CollapseDirection, @@ -44,8 +44,6 @@ const TableHeader = styled.thead` overflow: hidden; `; -const TableHeaderRow = styled.tr``; - const TableHeaderCell = styled.th<{ width: number }>` width: ${({ width }) => `${width}px`}; padding: 0; @@ -154,6 +152,36 @@ const IconContainer = styled.div<{ enabled: boolean }>` visibility: ${({ enabled }) => (enabled ? "visible" : "hidden")}; `; +type Column = { + id: string; + width: number; + fontWeight?: number; + dataColumnName?: ArcGisContentColumnName; + sortDataColumnName?: ArcGisContentColumnName; + columnName?: string; +}; + +const columns: Column[] = [ + { + id: "radio", + width: 44, + }, + { + id: "title", + width: 343, + fontWeight: 700, + dataColumnName: "title", + columnName: "Title", + }, + { + id: "created", + width: 149, + dataColumnName: "createdFormatted", + sortDataColumnName: "created", + columnName: "Date", + }, +]; + type InsertLayerProps = { onImport: (object: { name: string; url: string; token?: string }) => void; onCancel: () => void; @@ -190,39 +218,13 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { } }; - type Column = { - width: number; - fontWeight?: number; - dataColumnName?: ArcGisContentColumnName; - sortDataColumnName?: ArcGisContentColumnName; - columnName?: string; - }; - - const columns: Column[] = [ - { - width: 44, - }, - { - width: 343, - fontWeight: 700, - dataColumnName: "title", - columnName: "Title", - }, - { - width: 149, - dataColumnName: "createdFormatted", - sortDataColumnName: "created", - columnName: "Date", - }, - ]; - const renderHeaderCell = (column: Column): JSX.Element => { const sortDataColumnName = column.sortDataColumnName || column.dataColumnName; return ( {typeof sortDataColumnName !== "undefined" && ( onSort(sortDataColumnName)}> @@ -248,7 +250,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { const renderRowCell = ( column: Column, - contentItem: ArcGisContent, + contentItem: IArcGisContent, isRowSelected: boolean ): JSX.Element => { const dataColumnName = column.dataColumnName; @@ -256,7 +258,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => { {dataColumnName ? ( @@ -291,9 +293,7 @@ export const ArcGisImportPanel = ({ onImport, onCancel }: InsertLayerProps) => {
- - {columns.map((column: Column) => renderHeaderCell(column))} - + {columns.map((column: Column) => renderHeaderCell(column))} {arcGisContentArray.map((contentItem) => { diff --git a/src/redux/slices/arcgis-content-slice.ts b/src/redux/slices/arcgis-content-slice.ts index f671afeb..e9f7cf38 100644 --- a/src/redux/slices/arcgis-content-slice.ts +++ b/src/redux/slices/arcgis-content-slice.ts @@ -1,12 +1,12 @@ import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit"; -import { ArcGisContent, ArcGisContentColumnName } from "../../types"; +import { IArcGisContent, ArcGisContentColumnName } from "../../types"; import { RootState } from "../store"; import { getArcGisUserContent } from "../../utils/arcgis"; // Define a type for the slice state interface ArcGisContentState { /** Array of user's content items taken from ArcGIS */ - arcGisContent: ArcGisContent[]; + arcGisContent: IArcGisContent[]; /** Content item selected in UI */ arcGisContentSelected: string; /** Column name to sort the list by */ @@ -28,7 +28,7 @@ const initialState: ArcGisContentState = { const sortList = (state: ArcGisContentState) => { const column = state.sortColumn; if (column) { - state.arcGisContent.sort((a: ArcGisContent, b: ArcGisContent) => { + state.arcGisContent.sort((a: IArcGisContent, b: IArcGisContent) => { let ac = a[column]; let bc = b[column]; if (ac === undefined || bc === undefined || ac === null || bc === null) { @@ -99,15 +99,15 @@ const arcGisContentSlice = createSlice({ }, }); -export const getArcGisContent = createAsyncThunk( +export const getArcGisContent = createAsyncThunk( "getArcGisContent", - async (): Promise => { - const response: ArcGisContent[] = await getArcGisUserContent(); + async (): Promise => { + const response: IArcGisContent[] = await getArcGisUserContent(); return response; } ); -export const selectArcGisContent = (state: RootState): ArcGisContent[] => +export const selectArcGisContent = (state: RootState): IArcGisContent[] => state.arcGisContent.arcGisContent; export const selectArcGisContentSelected = (state: RootState): string => diff --git a/src/types.ts b/src/types.ts index 8a2d2cef..c55a1f6f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -368,7 +368,7 @@ export type TilesetMetadata = { type?: TilesetType; }; -export type ArcGisContent = { +export interface IArcGisContent { id: string; url: string; name: string; @@ -376,6 +376,6 @@ export type ArcGisContent = { token?: string; created: number; createdFormatted: string; -}; +} -export type ArcGisContentColumnName = keyof ArcGisContent; +export type ArcGisContentColumnName = keyof IArcGisContent; diff --git a/src/utils/arcgis.ts b/src/utils/arcgis.ts index d6d6fcca..1f57666c 100644 --- a/src/utils/arcgis.ts +++ b/src/utils/arcgis.ts @@ -1,7 +1,6 @@ import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"; - import { getUserContent, IItem } from "@esri/arcgis-rest-portal"; -import { ArcGisContent } from "../types"; +import { IArcGisContent } from "../types"; import { formatTimestamp } from "../utils/format/format-utils"; const ARCGIS_REST_USER_SESSION = "__ARCGIS_REST_USER_SESSION__"; @@ -102,7 +101,7 @@ export const arcGisRequestLogout = async () => { return await updateSessionInfo(); }; -class ArcGisContentClass implements ArcGisContent { +class ArcGisContent implements IArcGisContent { id = ""; url = ""; name = ""; @@ -127,8 +126,8 @@ class ArcGisContentClass implements ArcGisContent { * Gets the ArcGIS user's content list. * @returns The content list containig the necessay info to load the content items. */ -export const getArcGisUserContent = async (): Promise => { - const contentItems: ArcGisContent[] = []; +export const getArcGisUserContent = async (): Promise => { + const contentItems: IArcGisContent[] = []; const authentication = getArcGisSession(); if (authentication) { const content = await getUserContent({ authentication }); @@ -140,7 +139,7 @@ export const getArcGisUserContent = async (): Promise => { item.typeKeywords.includes("Hosted Service") ) { const token = await authentication.getToken(item.url); - const contentItem: ArcGisContent = new ArcGisContentClass(item, token); + const contentItem: ArcGisContent = new ArcGisContent(item, token); contentItems.push(contentItem); } }