diff --git a/web/client/actions/__tests__/details-test.js b/web/client/actions/__tests__/details-test.js index a0474fa074..254b876ada 100644 --- a/web/client/actions/__tests__/details-test.js +++ b/web/client/actions/__tests__/details-test.js @@ -28,13 +28,21 @@ describe('details actions tests', () => { const a = closeDetailsPanel(); expect(a.type).toBe(CLOSE_DETAILS_PANEL); }); - it('detailsLoaded', () => { + it('detailsLoaded for map', () => { const mapId = 1; const detailsUri = "sada/da/"; const a = detailsLoaded(mapId, detailsUri); expect(a.type).toBe(DETAILS_LOADED); expect(a.detailsUri).toBe(detailsUri); - expect(a.mapId).toBe(mapId); + expect(a.id).toBe(mapId); + }); + it('detailsLoaded for dashboard', () => { + const dashboardId = 1; + const detailsUri = "sada/da/"; + const a = detailsLoaded(dashboardId, detailsUri); + expect(a.type).toBe(DETAILS_LOADED); + expect(a.detailsUri).toBe(detailsUri); + expect(a.id).toBe(dashboardId); }); it('updateDetails', () => { const a = updateDetails('text'); diff --git a/web/client/actions/details.js b/web/client/actions/details.js index a5f4c288da..83de607bb7 100644 --- a/web/client/actions/details.js +++ b/web/client/actions/details.js @@ -17,9 +17,9 @@ export const NO_DETAILS_AVAILABLE = "NO_DETAILS_AVAILABLE"; * @memberof actions.details * @return {action} type `UPDATE_DETAILS` */ -export const updateDetails = (detailsText) => ({ +export const updateDetails = (detailsText, resourceId) => ({ type: UPDATE_DETAILS, - detailsText + detailsText, id: resourceId }); /** @@ -27,9 +27,9 @@ export const updateDetails = (detailsText) => ({ * @memberof actions.details * @return {action} type `DETAILS_LOADED` */ -export const detailsLoaded = (mapId, detailsUri, detailsSettings) => ({ +export const detailsLoaded = (resourceId, detailsUri, detailsSettings) => ({ type: DETAILS_LOADED, - mapId, + id: resourceId, detailsUri, detailsSettings }); diff --git a/web/client/components/details/DetailsPanel.jsx b/web/client/components/details/DetailsPanel.jsx index 90309fefb7..254d24d707 100644 --- a/web/client/components/details/DetailsPanel.jsx +++ b/web/client/components/details/DetailsPanel.jsx @@ -22,7 +22,8 @@ class DetailsPanel extends React.Component { panelClassName: PropTypes.string, style: PropTypes.object, onClose: PropTypes.func, - width: PropTypes.number + width: PropTypes.number, + isDashboard: PropTypes.bool }; static contextTypes = { @@ -41,14 +42,15 @@ class DetailsPanel extends React.Component { }, active: false, panelClassName: "details-panel", - width: 550 + width: 550, + isDashboard: false }; render() { return ( * */ -export default ({id, children, header, footer, columns, height, style = {}, className, bodyClassName = "ms2-border-layout-body"}) => +export default ({id, children, header, footer, columns, height, style = {}, className, bodyClassName = "ms2-border-layout-body" }) => (
{ .withLatestFrom(props$) .switchMap(([resource, props]) => updateResource(resource) - .flatMap(rid => resource.category === 'MAP' ? updateResourceAttribute({ - id: rid, - name: 'detailsSettings', - value: JSON.stringify(resource.attributes?.detailsSettings || {}) - }) : Rx.Observable.of(rid)) + .flatMap(rid => (['MAP', 'DASHBOARD'].includes(resource.categoryName)) ? + updateResourceAttribute({ + id: rid, + name: 'detailsSettings', + value: JSON.stringify(resource.attributes?.detailsSettings || {}) + }) : Rx.Observable.of(rid)) .do(() => { if (props) { if (props.onClose) { diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 2d383ff1cf..18c1ed324a 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -676,7 +676,15 @@ "FeedbackMask" ], "dashboard": [ - "BurgerMenu", + "Details", + "AddWidgetDashboard", + "MapConnectionDashboard", + { + "name": "SidebarMenu", + "cfg" : { + "containerPosition": "columns" + } + }, { "name": "Dashboard", "cfg": { diff --git a/web/client/epics/__tests__/config-test.js b/web/client/epics/__tests__/config-test.js index 75cc41207d..85f32afe66 100644 --- a/web/client/epics/__tests__/config-test.js +++ b/web/client/epics/__tests__/config-test.js @@ -8,7 +8,8 @@ import expect from 'expect'; import {head} from 'lodash'; -import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoEpic, backgroundsListInitEpic} from '../config'; +import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoDashboardEpic, storeDetailsInfoEpic, backgroundsListInitEpic} from '../config'; + import {LOAD_USER_SESSION} from '../../actions/usersession'; import { loadMapConfig, @@ -31,6 +32,7 @@ import testConfigEPSG31468 from "raw-loader!../../test-resources/testConfigEPSG3 import ConfigUtils from "../../utils/ConfigUtils"; import { DETAILS_LOADED } from '../../actions/details'; import { EMPTY_RESOURCE_VALUE } from '../../utils/MapInfoUtils'; +import { dashboardLoaded } from '../../actions/dashboard'; const api = { getResource: () => Promise.resolve({mapId: 1234}) @@ -346,7 +348,7 @@ describe('config epics', () => { switch (action.type) { case DETAILS_LOADED: - expect(action.mapId).toBe(mapId); + expect(action.id).toBe(mapId); expect(action.detailsUri).toBe("rest/geostore/data/1/raw?decode=datauri"); break; default: @@ -457,5 +459,107 @@ describe('config epics', () => { }, {}); }); }); + + describe("storeDetailsInfoDashboardEpic", () => { + beforeEach(done => { + mockAxios = new MockAdapter(axios); + setTimeout(done); + }); + + afterEach(done => { + mockAxios.restore(); + setTimeout(done); + }); + const dashboardId = 1; + const dashboardAttributesEmptyDetails = { + "AttributeList": { + "Attribute": [ + { + "name": "details", + "type": "STRING", + "value": EMPTY_RESOURCE_VALUE + } + ] + } + }; + + const dashboardAttributesWithoutDetails = { + "AttributeList": { + "Attribute": [] + } + }; + + const dashboardAttributesWithDetails = { + AttributeList: { + Attribute: [ + { + name: 'details', + type: 'STRING', + value: 'rest\/geostore\/data\/1\/raw?decode=datauri' + }, + { + name: "thumbnail", + type: "STRING", + value: 'rest\/geostore\/data\/1\/raw?decode=datauri' + }, + { + name: 'owner', + type: 'STRING', + value: 'admin' + } + ] + } + }; + it('test storeDetailsInfoDashboardEpic', (done) => { + mockAxios.onGet().reply(200, dashboardAttributesWithDetails); + testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => { + expect(actions.length).toBe(1); + actions.map((action) => { + + switch (action.type) { + case DETAILS_LOADED: + expect(action.id).toBe(dashboardId); + expect(action.detailsUri).toBe("rest/geostore/data/1/raw?decode=datauri"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, {dashboard: { + resource: { + id: dashboardId, + attributes: {} + } + }}); + }); + it('test storeDetailsInfoDashboardEpic when api returns NODATA value', (done) => { + // const mock = new MockAdapter(axios); + mockAxios.onGet().reply(200, dashboardAttributesWithoutDetails); + testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => { + expect(actions.length).toBe(1); + actions.map((action) => expect(action.type).toBe(TEST_TIMEOUT)); + done(); + }, {dashboard: { + resource: { + id: dashboardId, + attributes: {} + } + }}); + }); + it('test storeDetailsInfoDashboardEpic when api doesnt return details', (done) => { + mockAxios.onGet().reply(200, dashboardAttributesEmptyDetails); + testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => { + expect(actions.length).toBe(1); + actions.map((action) => expect(action.type).toBe(TEST_TIMEOUT)); + done(); + }, {dashboard: { + resource: { + id: dashboardId, + attributes: {} + } + }}); + }); + }); }); diff --git a/web/client/epics/__tests__/dashboard-test.js b/web/client/epics/__tests__/dashboard-test.js index 1d67d3beb8..4c224e0efb 100644 --- a/web/client/epics/__tests__/dashboard-test.js +++ b/web/client/epics/__tests__/dashboard-test.js @@ -338,16 +338,14 @@ describe('saveDashboard', () => { const startActions = [saveDashboard(RESOURCE)]; testEpic(saveDashboardMethod, actionsCount, startActions, actions => { - expect(actions.length).toBe(actionsCount); - expect(actions[0].type).toBe(DASHBOARD_LOADING); - expect(actions[0].value).toBe(true); - expect(actions[1].type).toBe(SAVE_ERROR); + expect(actions.length).toBe(2); + expect(actions[0].type).toBe(SAVE_ERROR); expect( - actions[1].error.status === 403 - || actions[1].error.status === 404 + actions[0].error.status === 403 + || actions[0].error.status === 404 ).toBeTruthy(); - expect(actions[2].type).toBe(DASHBOARD_LOADING); - expect(actions[2].value).toBe(false); + expect(actions[1].type).toBe(DASHBOARD_LOADING); + expect(actions[1].value).toBe(false); }, BASE_STATE, done); }); @@ -364,13 +362,11 @@ describe('saveDashboard', () => { const startActions = [saveDashboard(withoutMetadata)]; testEpic(saveDashboardMethod, actionsCount, startActions, actions => { - expect(actions.length).toBe(3); - expect(actions[0].type).toBe(DASHBOARD_LOADING); - expect(actions[0].value).toBe(true); - expect(actions[1].type).toBe(SAVE_ERROR); - expect(typeof(actions[1].error) === 'string').toBeTruthy(); - expect(actions[2].type).toBe(DASHBOARD_LOADING); - expect(actions[2].value).toBe(false); + expect(actions.length).toBe(2); + expect(actions[0].type).toBe(SAVE_ERROR); + expect(typeof(actions[0].error) === 'string').toBeTruthy(); + expect(actions[1].type).toBe(DASHBOARD_LOADING); + expect(actions[1].value).toBe(false); }, BASE_STATE, done); }); }); diff --git a/web/client/epics/__tests__/details-test.js b/web/client/epics/__tests__/details-test.js index 7c6ff90a85..29cd03de96 100644 --- a/web/client/epics/__tests__/details-test.js +++ b/web/client/epics/__tests__/details-test.js @@ -35,7 +35,7 @@ let map1 = { id: mapId, name: "name" }; -const testState = { +const mapTestState = { mapInitialConfig: { mapId }, @@ -48,11 +48,116 @@ const testState = { }, details: {} }; +const dashboardTestState = { + dashboard: { + resource: { + id: "123", + attributes: { + details: encodeURIComponent(detailsUri) + + } + } + } +}; + const rootEpic = combineEpics(closeDetailsPanelEpic); const epicMiddleware = createEpicMiddleware(rootEpic); const mockStore = configureMockStore([epicMiddleware]); -describe('details epics tests', () => { +describe('details epics tests for map', () => { + const oldGetDefaults = ConfigUtils.getDefaults; + let store; + + beforeEach(() => { + store = mockStore(); + ConfigUtils.getDefaults = () => ({ + geoStoreUrl: baseUrl + }); + }); + + afterEach(() => { + epicMiddleware.replaceEpic(rootEpic); + ConfigUtils.getDefaults = oldGetDefaults; + }); + + it('test closeDetailsPanel', (done) => { + + store.dispatch(closeDetailsPanel()); + + setTimeout( () => { + try { + const actions = store.getActions(); + expect(actions.length).toBe(2); + expect(actions[0].type).toBe(CLOSE_DETAILS_PANEL); + expect(actions[1].type).toBe(SET_CONTROL_PROPERTY); + } catch (e) { + done(e); + } + done(); + }, 50); + + }); + it('test fetchDataForDetailsPanel', (done) => { + map1.details = encodeURIComponent(detailsUri); + testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => { + expect(actions.length).toBe(2); + actions.map((action) => { + switch (action.type) { + case TOGGLE_CONTROL: + expect(action.control).toBe("details"); + expect(action.property).toBe("enabled"); + break; + case UPDATE_DETAILS: + expect(action.detailsText.indexOf(detailsText)).toNotBe(-1); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, mapTestState); + }); + it('test fetchDataForDetailsPanel with Error', (done) => { + testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => { + expect(actions.length).toBe(2); + actions.map((action) => { + switch (action.type) { + case TOGGLE_CONTROL: + expect(action.control).toBe("details"); + expect(action.property).toBe("enabled"); + break; + case SHOW_NOTIFICATION: + expect(action.message).toBe("maps.feedback.errorFetchingDetailsOfMap"); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, { + locale: { + messages: { + maps: { + feedback: { + errorFetchingDetailsOfMap: "maps.feedback.errorFetchingDetailsOfMap" + } + } + } + }, + mapInitialConfig: { + mapId + }, + map: { + present: { + info: {} + } + } + }); + }); +}); + + +describe('details epics tests for dashbaord', () => { const oldGetDefaults = ConfigUtils.getDefaults; let store; @@ -103,7 +208,7 @@ describe('details epics tests', () => { } }); done(); - }, testState); + }, dashboardTestState); }); it('test fetchDataForDetailsPanel with Error', (done) => { testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => { diff --git a/web/client/epics/__tests__/tutorial-test.js b/web/client/epics/__tests__/tutorial-test.js index c3a026f54f..37bfe5c976 100644 --- a/web/client/epics/__tests__/tutorial-test.js +++ b/web/client/epics/__tests__/tutorial-test.js @@ -584,7 +584,7 @@ describe('tutorial Epics', () => { }); }); describe('openDetailsPanelEpic tests', () => { - it('should open the details panel if it has showAtStartup set to true', (done) => { + it('should open the details panel if it is a (Map) and it has showAtStartup set to true', (done) => { const NUM_ACTIONS = 1; testEpic(openDetailsPanelEpic, NUM_ACTIONS, closeTutorial(), (actions) => { @@ -595,6 +595,7 @@ describe('tutorial Epics', () => { }, { map: { present: { + mapId: "123", info: { detailsSettings: { showAtStartup: true @@ -604,7 +605,28 @@ describe('tutorial Epics', () => { } }); }); - it('should open the details panel if it has showAtStartup set to false', (done) => { + it('should open the details panel if it is a (Dashboard) and it has showAtStartup set to true', (done) => { + const NUM_ACTIONS = 1; + + testEpic(openDetailsPanelEpic, NUM_ACTIONS, closeTutorial(), (actions) => { + expect(actions.length).toBe(NUM_ACTIONS); + const [action] = actions; + expect(action.type).toBe(OPEN_DETAILS_PANEL); + done(); + }, { + dashboard: { + resource: { + id: "123", + attributes: { + detailsSettings: { + showAtStartup: true + } + } + } + } + }); + }); + it('should open the details panel if it is a(Map) and it has showAtStartup set to false', (done) => { const NUM_ACTIONS = 1; testEpic(addTimeoutEpic(openDetailsPanelEpic, 100), NUM_ACTIONS, closeTutorial(), (actions) => { @@ -615,6 +637,7 @@ describe('tutorial Epics', () => { }, { map: { present: { + mapId: "123", info: { detailsSettings: { showAtStartup: false @@ -624,5 +647,26 @@ describe('tutorial Epics', () => { } }); }); + it('should open the details panel if it is a (Dashboard) and it has showAtStartup set to false', (done) => { + const NUM_ACTIONS = 1; + + testEpic(addTimeoutEpic(openDetailsPanelEpic, 100), NUM_ACTIONS, closeTutorial(), (actions) => { + expect(actions.length).toBe(NUM_ACTIONS); + const [action] = actions; + expect(action.type).toBe(TEST_TIMEOUT); + done(); + }, { + dashboard: { + resource: { + id: "123", + attributes: { + detailsSettings: { + showAtStartup: false + } + } + } + } + }); + }); }); }); diff --git a/web/client/epics/config.js b/web/client/epics/config.js index 619c438c46..914fd7a9b7 100644 --- a/web/client/epics/config.js +++ b/web/client/epics/config.js @@ -27,6 +27,8 @@ import Persistence from '../api/persistence'; import GeoStoreApi from '../api/GeoStoreDAO'; import { isLoggedIn, userSelector } from '../selectors/security'; import { mapIdSelector, projectionDefsSelector } from '../selectors/map'; +import { getDashboardId } from '../selectors/dashboard'; +import { DASHBOARD_LOADED } from '../actions/dashboard'; import {loadUserSession, USER_SESSION_LOADED, userSessionStartSaving, saveMapConfig} from '../actions/usersession'; import { detailsLoaded, openDetailsPanel } from '../actions/details'; import {userSessionEnabledSelector, buildSessionName} from "../selectors/usersession"; @@ -248,3 +250,33 @@ export const backgroundsListInitEpic = (action$) => ...(currentBackground ? [setCurrentBackgroundLayer(currentBackground.id)] : []) ); }); + +export const storeDetailsInfoDashboardEpic = (action$, store) => + action$.ofType(DASHBOARD_LOADED) + .switchMap(() => { + const dashboardId = getDashboardId(store.getState()); + const isTutorialRunning = store.getState()?.tutorial?.run; + return !dashboardId + ? Observable.empty() + : Observable.fromPromise( + GeoStoreApi.getResourceAttributes(dashboardId) + ).switchMap((attributes) => { + let details = find(attributes, {name: 'details'}); + const detailsSettingsAttribute = find(attributes, {name: 'detailsSettings'}); + let detailsSettings = {}; + if (!details || details.value === EMPTY_RESOURCE_VALUE) { + return Observable.empty(); + } + + try { + detailsSettings = JSON.parse(detailsSettingsAttribute.value); + } catch (e) { + detailsSettings = {}; + } + + return Observable.of( + detailsLoaded(dashboardId, details.value, detailsSettings), + ...(detailsSettings.showAtStartup && !isTutorialRunning ? [openDetailsPanel()] : []) + ); + }); + }); diff --git a/web/client/epics/dashboard.js b/web/client/epics/dashboard.js index eaf304b836..5c5a85636d 100644 --- a/web/client/epics/dashboard.js +++ b/web/client/epics/dashboard.js @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ import Rx from 'rxjs'; +import { mapValues, isObject, keys, isNil } from 'lodash'; import { NEW, INSERT, EDIT, OPEN_FILTER_EDITOR, editNewWidget, onEditorChange} from '../actions/widgets'; @@ -35,7 +36,7 @@ import { isLoggedIn } from '../selectors/security'; import { getEditingWidgetLayer, getEditingWidgetFilter, getWidgetFilterKey } from '../selectors/widgets'; import { pathnameSelector } from '../selectors/router'; import { download, readJson } from '../utils/FileUtils'; -import { createResource, updateResource, getResource } from '../api/persistence'; +import { createResource, updateResource, getResource, updateResourceAttribute } from '../api/persistence'; import { wrapStartStop } from '../observables/epics'; import { LOCATION_CHANGE, push } from 'connected-react-router'; import { convertDependenciesMappingForCompatibility } from "../utils/WidgetsUtils"; @@ -161,9 +162,32 @@ export const reloadDashboardOnLoginLogout = (action$) => // saving dashboard flow (both creation and update) export const saveDashboard = action$ => action$ .ofType(SAVE_DASHBOARD) - .exhaustMap(({resource} = {}) => - (!resource.id ? createResource(resource) : updateResource(resource)) - .switchMap(rid => Rx.Observable.of( + .exhaustMap(({resource} = {}) =>{ + // convert to json if attribute is an object + const attributesFixed = mapValues(resource.attributes, attr => { + if (isObject(attr)) { + let json = null; + try { + json = JSON.stringify(attr); + } catch (e) { + json = null; + } + return json; + } + return attr; + }); + // filter out invalid attributes + // thumbnails and details are handled separately(linked resources) + const validAttributesNames = keys(attributesFixed) + .filter(attrName => attrName !== 'thumbnail' && attrName !== 'details' && !isNil(attributesFixed[attrName])); + return Rx.Observable.forkJoin( + (!resource.id ? createResource(resource) : updateResource(resource))) + .switchMap(([rid]) => (validAttributesNames.length > 0 ? + Rx.Observable.forkJoin(validAttributesNames.map(attrName => updateResourceAttribute({ + id: rid, + name: attrName, + value: attributesFixed[attrName] + }))) : Rx.Observable.of([])) .switchMap(() => Rx.Observable.of( dashboardSaved(rid), resource.id ? triggerSave(false) : triggerSaveAs(false), !resource.id @@ -175,15 +199,14 @@ export const saveDashboard = action$ => action$ title: "saveDialog.saveSuccessTitle", message: "saveDialog.saveSuccessMessage" })).delay(!resource.id ? 1000 : 0) // delay to allow loading - ) - ) - .let(wrapStartStop( - dashboardLoading(true, "saving"), - dashboardLoading(false, "saving") )) - .catch( - ({ status, statusText, data, message, ...other } = {}) => Rx.Observable.of(dashboardSaveError(status ? { status, statusText, data } : message || other), dashboardLoading(false, "saving")) - ) + .let(wrapStartStop( + dashboardLoading(true, "saving"), + dashboardLoading(false, "saving") + ) + )); + }).catch( + ({ status, statusText, data, message, ...other } = {}) => Rx.Observable.of(dashboardSaveError(status ? { status, statusText, data } : message || other), dashboardLoading(false, "saving")) ); export const exportDashboard = action$ => action$ diff --git a/web/client/epics/details.js b/web/client/epics/details.js index 2e9e246f12..48ef7e7f4b 100644 --- a/web/client/epics/details.js +++ b/web/client/epics/details.js @@ -18,26 +18,31 @@ import { import { toggleControl, setControlProperty } from '../actions/controls'; import { - mapInfoDetailsUriFromIdSelector + mapIdSelector } from '../selectors/map'; +import { getDashboardId } from '../selectors/dashboard'; import GeoStoreApi from '../api/GeoStoreDAO'; import { getIdFromUri } from '../utils/MapUtils'; import { basicError } from '../utils/NotificationUtils'; import { VISUALIZATION_MODE_CHANGED } from '../actions/maptype'; +import { detailsUriSelector } from '../selectors/details'; export const fetchDataForDetailsPanel = (action$, store) => action$.ofType(OPEN_DETAILS_PANEL) .switchMap(() => { const state = store.getState(); - const detailsUri = mapInfoDetailsUriFromIdSelector(state); + const mapId = mapIdSelector(state); + const dashboardId = getDashboardId(state); + const detailsUri = detailsUriSelector(state); const detailsId = getIdFromUri(detailsUri); + const resourceId = dashboardId || mapId; return Rx.Observable.fromPromise(GeoStoreApi.getData(detailsId) .then(data => data)) .switchMap((details) => { return Rx.Observable.of( - updateDetails(details) + updateDetails(details, resourceId) ); }).startWith(toggleControl("details", "enabled")) .catch(() => { diff --git a/web/client/epics/tutorial.js b/web/client/epics/tutorial.js index 7563764dc6..48c046e408 100644 --- a/web/client/epics/tutorial.js +++ b/web/client/epics/tutorial.js @@ -27,9 +27,9 @@ import { CONTEXT_TUTORIALS } from '../actions/contextcreator'; import { LOCATION_CHANGE } from 'connected-react-router'; import { isEmpty, isArray, isObject } from 'lodash'; import { getApi } from '../api/userPersistedStorage'; -import { mapSelector } from '../selectors/map'; import {REDUCERS_LOADED} from "../actions/storemanager"; import { VISUALIZATION_MODE_CHANGED } from '../actions/maptype'; +import { detailsSettingsSelector } from '../selectors/details'; const findTutorialId = path => path.match(/\/(viewer)\/(\w+)\/(\d+)/) && path.replace(/\/(viewer)\/(\w+)\/(\d+)/, "$2") || path.match(/\/(\w+)\/(\d+)/) && path.replace(/\/(\w+)\/(\d+)/, "$1") @@ -168,7 +168,14 @@ export const getActionsFromStepEpic = (action$) => export const openDetailsPanelEpic = (action$, store) => action$.ofType(CLOSE_TUTORIAL) - .filter(() => mapSelector(store.getState())?.info?.detailsSettings?.showAtStartup ) + .filter(() => { + const state = store.getState(); + let detailsSettings = detailsSettingsSelector(state); + if (detailsSettings && typeof detailsSettings === 'string') { + detailsSettings = JSON.parse(detailsSettings); + } + return detailsSettings?.showAtStartup; + }) .switchMap( () => { return Rx.Observable.of(openDetailsPanel()); }); diff --git a/web/client/plugins/AddWidgetDashboard.jsx b/web/client/plugins/AddWidgetDashboard.jsx new file mode 100644 index 0000000000..c70413f611 --- /dev/null +++ b/web/client/plugins/AddWidgetDashboard.jsx @@ -0,0 +1,77 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; + +import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; +import { connect } from 'react-redux'; +import ToolbarButton from '../components/misc/toolbar/ToolbarButton'; +import { buttonCanEdit, isDashboardEditing } from '../selectors/dashboard'; +import { setEditing } from '../actions/dashboard'; +import { createWidget } from '../actions/widgets'; +import { createPlugin } from '../utils/PluginsUtils'; + +class AddWidgetDashboard extends React.Component { + static propTypes = { + canEdit: PropTypes.bool, + editing: PropTypes.bool, + onAddWidget: PropTypes.func, + setEditing: PropTypes.func + } + + static defaultProps = { + editing: false, + canEdit: false + } + + render() { + if (!this.props.canEdit && !this.props.editing) return false; + return ( { + if (this.props.editing) this.props.setEditing(false); + else { + this.props.onAddWidget(); + } + }} + id={'ms-add-card-dashboard'} + tooltipPosition={'left'} + btnDefaultProps={{ tooltipPosition: 'right', className: 'square-button-md', bsStyle: this.props.editing ? 'primary' : 'tray' }}/>); + } +} + +const ConnectedAddWidget = connect( + createSelector( + buttonCanEdit, + isDashboardEditing, + ( edit, editing ) => ({ + canEdit: edit, + editing + }) + ), + { + onAddWidget: createWidget, + setEditing: setEditing + } +)(AddWidgetDashboard); + +export default createPlugin('AddWidgetDashboard', { + component: () => null, + containers: { + SidebarMenu: { + name: "AddWidgetDashboard", + position: 10, + tool: ConnectedAddWidget, + priority: 0 + } + } +}); diff --git a/web/client/plugins/Dashboard.jsx b/web/client/plugins/Dashboard.jsx index e8e6964edd..04d30e5f87 100644 --- a/web/client/plugins/Dashboard.jsx +++ b/web/client/plugins/Dashboard.jsx @@ -47,6 +47,8 @@ import { import dashboardReducers from '../reducers/dashboard'; import dashboardEpics from '../epics/dashboard'; import widgetsEpics from '../epics/widgets'; +import GlobalSpinner from '../components/misc/spinners/GlobalSpinner/GlobalSpinner'; +import { createPlugin } from '../utils/PluginsUtils'; const WidgetsView = compose( connect( @@ -183,14 +185,24 @@ class DashboardPlugin extends React.Component { } } -export default { - DashboardPlugin: withResizeDetector(DashboardPlugin), +export default createPlugin("Dashboard", { + component: withResizeDetector(DashboardPlugin), reducers: { dashboard: dashboardReducers, widgets: widgetsReducers }, + containers: { + SidebarMenu: { + name: "Dashboard-spinner", + alwaysVisible: true, + position: 2000, + tool: connect((state) => ({ + loading: isDashboardLoading(state) + }))(GlobalSpinner) + } + }, epics: { ...dashboardEpics, ...widgetsEpics } -}; +}); diff --git a/web/client/plugins/DashboardEditor.jsx b/web/client/plugins/DashboardEditor.jsx index c5649d1cf6..cfd3391b90 100644 --- a/web/client/plugins/DashboardEditor.jsx +++ b/web/client/plugins/DashboardEditor.jsx @@ -12,22 +12,17 @@ import { createSelector } from 'reselect'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { createPlugin } from '../utils/PluginsUtils'; - - -import { dashboardHasWidgets, getWidgetsDependenciesGroups } from '../selectors/widgets'; -import { isDashboardEditing, showConnectionsSelector, isDashboardLoading, buttonCanEdit, isDashboardAvailable } from '../selectors/dashboard'; +import { isDashboardEditing, isDashboardLoading, isDashboardAvailable } from '../selectors/dashboard'; import { dashboardSelector, dashboardsLocalizedSelector } from './widgetbuilder/commons'; -import { createWidget, toggleConnection } from '../actions/widgets'; +import { toggleConnection } from '../actions/widgets'; import { setEditing, setEditorAvailable, triggerShowConnections } from '../actions/dashboard'; import withDashboardExitButton from './widgetbuilder/enhancers/withDashboardExitButton'; -import LoadingSpinner from '../components/misc/LoadingSpinner'; import WidgetTypeBuilder from './widgetbuilder/WidgetTypeBuilder'; import epics from '../epics/dashboard'; import dashboard from '../reducers/dashboard'; -import Toolbar from '../components/misc/toolbar/Toolbar'; const Builder = compose( @@ -38,51 +33,6 @@ const Builder = })), withDashboardExitButton )(WidgetTypeBuilder); -const EditorToolbar = compose( - connect( - createSelector( - showConnectionsSelector, - dashboardHasWidgets, - buttonCanEdit, - getWidgetsDependenciesGroups, - (showConnections, hasWidgets, edit, groups = []) => ({ - showConnections, - hasConnections: groups.length > 0, - hasWidgets, - canEdit: edit - }) - ), - { - onShowConnections: triggerShowConnections, - onAddWidget: createWidget - } - ), - withProps(({ - onAddWidget = () => { }, - hasWidgets, - canEdit, - hasConnections, - showConnections, - onShowConnections = () => { } - }) => ({ - buttons: [{ - glyph: 'plus', - tooltipId: 'dashboard.editor.addACardToTheDashboard', - bsStyle: 'primary', - visible: canEdit, - id: 'ms-add-card-dashboard', - onClick: () => onAddWidget() - }, - { - glyph: showConnections ? 'bulb-on' : 'bulb-off', - tooltipId: showConnections ? 'dashboard.editor.hideConnections' : 'dashboard.editor.showConnections', - bsStyle: showConnections ? 'success' : 'primary', - visible: !!hasWidgets && !!hasConnections || !canEdit, - onClick: () => onShowConnections(!showConnections) - }] - })) -)(Toolbar); - /** * Side toolbar that allows to edit dashboard widgets. @@ -98,7 +48,6 @@ class DashboardEditorComponent extends React.Component { static propTypes = { id: PropTypes.string, editing: PropTypes.bool, - loading: PropTypes.bool, limitDockHeight: PropTypes.bool, fluid: PropTypes.bool, zIndex: PropTypes.number, @@ -118,7 +67,6 @@ class DashboardEditorComponent extends React.Component { id: "dashboard-editor", editing: false, dockSize: 500, - loading: true, limitDockHeight: true, zIndex: 10000, fluid: false, @@ -141,10 +89,7 @@ class DashboardEditorComponent extends React.Component { return this.props.editing ?
this.props.setEditing(false)} catalog={this.props.catalog} />
- : (
- - {this.props.loading ? : null} -
); + : false; } } @@ -153,7 +98,7 @@ const Plugin = connect( isDashboardEditing, isDashboardLoading, isDashboardAvailable, - (editing, loading, isDashboardOpened) => ({ editing, loading, isDashboardOpened }) + (editing, isDashboardOpened) => ({ editing, isDashboardOpened }) ), { setEditing, onMount: () => setEditorAvailable(true), diff --git a/web/client/plugins/DashboardSave.jsx b/web/client/plugins/DashboardSave.jsx index 25cdf61230..4afffdfe94 100644 --- a/web/client/plugins/DashboardSave.jsx +++ b/web/client/plugins/DashboardSave.jsx @@ -37,7 +37,8 @@ const SaveBaseDialog = compose( onSave: saveDashboard }), withProps({ - category: "DASHBOARD" + category: "DASHBOARD", + enableDetails: true // to enable details in dashboard }), handleSaveModal )(Save); @@ -68,6 +69,23 @@ export const DashboardSave = createPlugin('DashboardSave', { text: , icon: , action: triggerSave.bind(null, true), + tooltip: "saveDialog.saveTooltip", + // display the BurgerMenu button only if the map can be edited + selector: createSelector( + isLoggedIn, + dashboardResource, + (loggedIn, {canEdit, id} = {}) => ({ + style: loggedIn && id && canEdit ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable + }) + ) + }, + SidebarMenu: { + name: 'dashboardSave', + position: 30, + text: , + icon: , + action: triggerSave.bind(null, true), + tooltip: "saveDialog.saveTooltip", // display the BurgerMenu button only if the map can be edited selector: createSelector( isLoggedIn, @@ -120,6 +138,21 @@ export const DashboardSaveAs = createPlugin('DashboardSaveAs', { style: loggedIn ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable }) ) + }, + SidebarMenu: { + name: 'dashboardSaveAs', + position: 31, + tooltip: "saveAs", + text: , + icon: , + action: triggerSaveAs.bind(null, true), + // always display on the BurgerMenu button if logged in + selector: createSelector( + isLoggedIn, + (loggedIn) => ({ + style: loggedIn ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable + }) + ) } } }); diff --git a/web/client/plugins/Details.jsx b/web/client/plugins/Details.jsx index 113d89dd7b..90d80beb09 100644 --- a/web/client/plugins/Details.jsx +++ b/web/client/plugins/Details.jsx @@ -17,8 +17,7 @@ import { NO_DETAILS_AVAILABLE } from "../actions/details"; -import { mapIdSelector, mapInfoDetailsUriFromIdSelector, mapInfoDetailsSettingsFromIdSelector } from '../selectors/map'; -import { detailsTextSelector } from '../selectors/details'; +import { detailsTextSelector, detailsUriSelector, detailsSettingsSelector } from '../selectors/details'; import { mapLayoutValuesSelector } from '../selectors/maplayout'; import DetailsViewer from '../components/resources/modals/fragments/DetailsViewer'; @@ -32,6 +31,7 @@ import { createPlugin } from '../utils/PluginsUtils'; import details from '../reducers/details'; import * as epics from '../epics/details'; import {createStructuredSelector} from "reselect"; +import { getDashboardId } from '../selectors/dashboard'; /** * Allow to show details for the map. @@ -51,7 +51,8 @@ const DetailsPlugin = ({ dockStyle, detailsText, showAsModal = false, - onClose = () => {} + onClose = () => {}, + isDashboard }) => { const viewer = (); - return showAsModal ? + return showAsModal && active ? } onClose={onClose}> {viewer} - : + : active && { + return getDashboardId(state) ? true : false; + }, active: state => get(state, "controls.details.enabled"), - dockStyle: state => mapLayoutValuesSelector(state, { height: true, right: true }, true), + dockStyle: state => { + const isDashbaord = getDashboardId(state); + let layoutValues = mapLayoutValuesSelector(state, { height: true, right: true }, true); + if (isDashbaord) { + layoutValues = { ...layoutValues, right: 0, height: '100%'}; + } + return layoutValues; + }, detailsText: detailsTextSelector, - showAsModal: state => mapInfoDetailsSettingsFromIdSelector(state)?.showAsModal + showAsModal: state => { + let detailsSettings = detailsSettingsSelector(state); + if (detailsSettings && typeof detailsSettings === 'string') detailsSettings = JSON.parse(detailsSettings); + return detailsSettings?.showAsModal; + } }), { onClose: closeDetailsPanel })(DetailsPlugin), @@ -99,8 +115,7 @@ export default createPlugin('Details', { icon: , action: openDetailsPanel, selector: (state) => { - const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const detailsUri = detailsUriSelector(state); if (detailsUri) { return {}; } @@ -117,8 +132,7 @@ export default createPlugin('Details', { icon: , action: openDetailsPanel, selector: (state) => { - const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const detailsUri = detailsUriSelector(state); if (detailsUri) { return {}; } @@ -134,8 +148,7 @@ export default createPlugin('Details', { icon: , action: openDetailsPanel, selector: (state) => { - const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const detailsUri = detailsUriSelector(state); if (detailsUri) { return { bsStyle: state.controls.details && state.controls.details.enabled ? 'primary' : 'tray', diff --git a/web/client/plugins/MapConnectionDashboard.jsx b/web/client/plugins/MapConnectionDashboard.jsx new file mode 100644 index 0000000000..86da06303b --- /dev/null +++ b/web/client/plugins/MapConnectionDashboard.jsx @@ -0,0 +1,75 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; + +import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; +import { connect } from 'react-redux'; +import ToolbarButton from '../components/misc/toolbar/ToolbarButton'; +import { buttonCanEdit, showConnectionsSelector } from '../selectors/dashboard'; +import { dashboardHasWidgets, getWidgetsDependenciesGroups } from '../selectors/widgets'; +import { triggerShowConnections } from '../actions/dashboard'; +import { createPlugin } from '../utils/PluginsUtils'; + +class MapConnectionDashboard extends React.Component { + static propTypes = { + showConnections: PropTypes.bool, + canEdit: PropTypes.bool, + hasWidgets: PropTypes.bool, + hasConnections: PropTypes.bool, + onShowConnections: PropTypes.func + } + + static defaultProps = { + onShowConnections: () => {} + } + + render() { + const { showConnections, canEdit, hasConnections, hasWidgets, onShowConnections } = this.props; + if (!(!!hasWidgets && !!hasConnections || !canEdit)) return false; + return (onShowConnections(!showConnections)} + tooltipPosition={'left'} + id={'ms-map-connection-card-dashboard'} + btnDefaultProps={{ tooltipPosition: 'bottom', className: 'square-button-md', bsStyle: 'primary' }}/>); + } +} + +const ConnectedMapAddWidget = connect( + createSelector( + showConnectionsSelector, + dashboardHasWidgets, + buttonCanEdit, + getWidgetsDependenciesGroups, + (showConnections, hasWidgets, edit, groups = []) => ({ + showConnections, + hasConnections: groups.length > 0, + hasWidgets, + canEdit: edit + }) + ), + { + onShowConnections: triggerShowConnections + } +)(MapConnectionDashboard); + +export default createPlugin('MapConnectionDashboard', { + component: () => null, + containers: { + SidebarMenu: { + name: "MapConnectionDashboard", + tool: ConnectedMapAddWidget, + position: 10, + priority: 0 + } + } +}); diff --git a/web/client/plugins/SidebarMenu.jsx b/web/client/plugins/SidebarMenu.jsx index 538b93b823..32bb6dcdf6 100644 --- a/web/client/plugins/SidebarMenu.jsx +++ b/web/client/plugins/SidebarMenu.jsx @@ -23,7 +23,7 @@ import {createPlugin} from "../utils/PluginsUtils"; import sidebarMenuReducer from "../reducers/sidebarmenu"; import './sidebarmenu/sidebarmenu.less'; -import {lastActiveToolSelector, sidebarIsActiveSelector} from "../selectors/sidebarmenu"; +import {lastActiveToolSelector, sidebarIsActiveSelector, isSidebarWithFullHeight} from "../selectors/sidebarmenu"; import {setLastActiveItem} from "../actions/sidebarmenu"; import Message from "../components/I18N/Message"; @@ -40,7 +40,7 @@ class SidebarMenu extends React.Component { sidebarWidth: PropTypes.number, state: PropTypes.object, setLastActiveItem: PropTypes.func, - lastActiveTool: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]) + isSidebarFullHeight: PropTypes.bool }; static contextTypes = { @@ -60,7 +60,8 @@ class SidebarMenu extends React.Component { stateSelector: 'sidebarMenu', tool: SidebarElement, toolCfg: {}, - sidebarWidth: 40 + sidebarWidth: 40, + isSidebarFullHeight: false }; constructor() { @@ -90,7 +91,8 @@ class SidebarMenu extends React.Component { } return prev; }, []).length > 0 : false; - return newSize || newItems || newVisibleItems || newHeight || burgerMenuState || markedAsInactive; + const nextIsSideBarFullHeight = !this.props.isSidebarFullHeight && nextProps.isSidebarFullHeight; + return newSize || newItems || newVisibleItems || newHeight || burgerMenuState || markedAsInactive || nextIsSideBarFullHeight; } componentDidUpdate(prevProps) { @@ -229,7 +231,7 @@ class SidebarMenu extends React.Component { render() { return this.state.hidden ? false : ( -
+
{ ({ height }) => state, state => lastActiveToolSelector(state), state => mapLayoutValuesSelector(state, {dockSize: true, bottom: true, height: true}), - sidebarIsActiveSelector -], (state, lastActiveTool, style, isActive) => ({ + sidebarIsActiveSelector, + isSidebarWithFullHeight +], (state, lastActiveTool, style, isActive, isSidebarFullHeight ) => ({ style, lastActiveTool, state, - isActive + isActive, + isSidebarFullHeight })); /** diff --git a/web/client/plugins/__tests__/DashboardSave-test.jsx b/web/client/plugins/__tests__/DashboardSave-test.jsx index 3b147745b1..fa3ceaa9be 100644 --- a/web/client/plugins/__tests__/DashboardSave-test.jsx +++ b/web/client/plugins/__tests__/DashboardSave-test.jsx @@ -29,7 +29,7 @@ describe('DashboardSave Plugins (DashboardSave, DashboardSaveAs)', () => { document.body.innerHTML = ''; setTimeout(done); }); - describe('DashboardSave', () => { + describe('DashboardSave within burger menu', () => { const DUMMY_ACTION = { type: "DUMMY_ACTION" }; it('hidden by default, visibility of the button', () => { const { Plugin, containers } = getPluginForTest(DashboardSave, stateMocker(DUMMY_ACTION), { @@ -54,7 +54,32 @@ describe('DashboardSave Plugins (DashboardSave, DashboardSaveAs)', () => { expect(document.getElementsByClassName('modal-fixed').length).toBe(1); }); }); - describe('DashboardSaveAs', () => { + describe('DashboardSave within sidebar menu', () => { + const DUMMY_ACTION = { type: "DUMMY_ACTION" }; + it('hidden by default, visibility of the button', () => { + const { Plugin, containers } = getPluginForTest(DashboardSave, stateMocker(DUMMY_ACTION), { + SidebarMenuPlugin: {} + }); + // check container for burger menu + expect(Object.keys(containers)).toContain('SidebarMenu'); + ReactDOM.render(, document.getElementById("container")); + expect(document.getElementsByClassName('modal-fixed').length).toBe(0); + // check log-in logout properties selector for button in burger menu + // hide when not logged in + expect(containers.SidebarMenu.selector({ security: {} }).style.display).toBe("none"); + // hide when logged in but without resource selected + expect(containers.SidebarMenu.selector({security: {user: {}}}).style.display).toBe("none"); + // hide if you don't have permissions + expect(containers.SidebarMenu.selector({ security: { user: {} }, dashboard: { resource: { id: 1234, canEdit: false } } }).style.display ).toBe("none"); + }); + it('show when control is set to "save"', () => { + const storeState = stateMocker(DUMMY_ACTION, triggerSave(true)); + const { Plugin } = getPluginForTest(DashboardSave, storeState); + ReactDOM.render(, document.getElementById("container")); + expect(document.getElementsByClassName('modal-fixed').length).toBe(1); + }); + }); + describe('DashboardSaveAs within burger menu', () => { const DUMMY_ACTION = { type: "DUMMY_ACTION" }; it('hidden by default, visibility of the button', () => { const { Plugin, containers } = getPluginForTest(DashboardSaveAs, stateMocker(DUMMY_ACTION), { @@ -92,4 +117,43 @@ describe('DashboardSave Plugins (DashboardSave, DashboardSaveAs)', () => { expect(inputEl.value).toBe('f'); }); }); + + describe('DashboardSaveAs within sidebar menu', () => { + const DUMMY_ACTION = { type: "DUMMY_ACTION" }; + it('hidden by default, visibility of the button', () => { + const { Plugin, containers } = getPluginForTest(DashboardSaveAs, stateMocker(DUMMY_ACTION), { + SidebarMenuPlugin: {} + }); + // check container for burger menu + expect(Object.keys(containers)).toContain('SidebarMenu'); + ReactDOM.render(, document.getElementById("container")); + expect(document.getElementsByClassName('modal-fixed').length).toBe(0); + // check log-in logout properties selector for button in burger menu + // hide when not logged in + expect(containers.SidebarMenu.selector({ security: {} }).style.display).toBe("none"); + // always show when user logged in + expect(containers.SidebarMenu.selector({ security: { user: {} } }).style.display).toNotExist(); + // show if resource is available for clone + expect(containers.SidebarMenu.selector({ + security: { user: {} }, + geostory: { resource: { id: 1234, canEdit: false } } + }).style.display).toNotExist(); + }); + it('show when control is set to "saveAs"', () => { + const { Plugin } = getPluginForTest(DashboardSaveAs, stateMocker(DUMMY_ACTION, triggerSaveAs(true))); + ReactDOM.render(, document.getElementById("container")); + expect(document.getElementsByClassName('modal-fixed').length).toBe(1); + }); + it('title is editable', () => { + const { Plugin } = getPluginForTest(DashboardSaveAs, stateMocker(DUMMY_ACTION, triggerSaveAs(true))); + ReactDOM.render(, document.getElementById("container")); + const modal = document.getElementsByClassName('modal-fixed')[0]; + expect(modal).toExist(); + const inputEl = modal.getElementsByTagName('input')[1]; + expect(inputEl).toExist(); + inputEl.value = 'f'; + TestUtils.Simulate.change(inputEl); + expect(inputEl.value).toBe('f'); + }); + }); }); diff --git a/web/client/plugins/__tests__/SidebarMenu-test.jsx b/web/client/plugins/__tests__/SidebarMenu-test.jsx index 331c04d3a2..f0b4c7f295 100644 --- a/web/client/plugins/__tests__/SidebarMenu-test.jsx +++ b/web/client/plugins/__tests__/SidebarMenu-test.jsx @@ -39,4 +39,23 @@ describe('SidebarMenu Plugin', () => { const elements = document.querySelectorAll('#mapstore-sidebar-menu > button, #mapstore-sidebar-menu #extra-items + .dropdown-menu li'); expect(elements.length).toBe(2); }); + + it('sidebar menu with full height', () => { + document.getElementById('container').style.height = '600px'; + const { Plugin } = getPluginForTest(SidebarMenu, {}); + const items = [{ + name: 'test', + position: 1, + text: 'Test Item' + }, { + name: 'test2', + position: 2, + text: 'Test Item 2' + }]; + ReactDOM.render(, document.getElementById("container")); + const sidebarMenuContainer = document.getElementById('mapstore-sidebar-menu-container'); + expect(sidebarMenuContainer).toExist(); + const elements = document.querySelectorAll('#mapstore-sidebar-menu > button, #mapstore-sidebar-menu #extra-items + .dropdown-menu li'); + expect(elements.length).toBe(2); + }); }); diff --git a/web/client/plugins/sidebarmenu/sidebarmenu.less b/web/client/plugins/sidebarmenu/sidebarmenu.less index 04fdf249a6..f9aabcc145 100644 --- a/web/client/plugins/sidebarmenu/sidebarmenu.less +++ b/web/client/plugins/sidebarmenu/sidebarmenu.less @@ -35,3 +35,8 @@ } } } + +#mapstore-sidebar-menu-container.fullHeightSideBar{ + position: relative; + max-height: 100% !important; +} \ No newline at end of file diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index ceef399b86..367494a474 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -158,7 +158,9 @@ export const plugins = { WidgetsTrayPlugin: toModulePlugin('WidgetsTray', () => import(/* webpackChunkName: 'plugins/widgetsTray' */ '../plugins/WidgetsTray')), ZoomAllPlugin: toModulePlugin('ZoomAll', () => import(/* webpackChunkName: 'plugins/zoomAll' */ '../plugins/ZoomAll')), ZoomInPlugin: toModulePlugin('ZoomIn', () => import(/* webpackChunkName: 'plugins/zoomIn' */ '../plugins/ZoomIn')), - ZoomOutPlugin: toModulePlugin('ZoomOut', () => import(/* webpackChunkName: 'plugins/zoomOut' */ '../plugins/ZoomOut')) + ZoomOutPlugin: toModulePlugin('ZoomOut', () => import(/* webpackChunkName: 'plugins/zoomOut' */ '../plugins/ZoomOut')), + AddWidgetDashboardPlugin: toModulePlugin('AddWidgetDashboard', () => import(/* webpackChunkName: 'plugins/AddWidgetDashboard' */ '../plugins/AddWidgetDashboard')), + MapConnectionDashboardPlugin: toModulePlugin('MapConnectionDashboard', () => import(/* webpackChunkName: 'plugins/MapConnectionDashboard' */ '../plugins/MapConnectionDashboard')) }; const pluginsDefinition = { diff --git a/web/client/reducers/__tests__/config-test.js b/web/client/reducers/__tests__/config-test.js index 4b0ab8e061..785337cb53 100644 --- a/web/client/reducers/__tests__/config-test.js +++ b/web/client/reducers/__tests__/config-test.js @@ -75,7 +75,7 @@ describe('Test the mapConfig reducer', () => { expect(state.map.info).toExist(); expect(state.map.info.canEdit).toBe(true); }); - it('DETAILS_LOADED', () => { + it('DETAILS_LOADED Map', () => { const detailsUri = "details/uri"; var state = mapConfig({ map: { @@ -83,11 +83,24 @@ describe('Test the mapConfig reducer', () => { mapId: 1 } } - }, {type: DETAILS_LOADED, mapId: 1, detailsUri}); + }, {type: DETAILS_LOADED, id: 1, detailsUri}); expect(state.map).toExist(); expect(state.map.info).toExist(); expect(state.map.info.details).toBe(detailsUri); }); + it('DETAILS_LOADED Dahboard', () => { + const detailsUri = "details/uri"; + var state = mapConfig({ + dashboard: { + resource: { + id: "1", attributes: {} + } + } + }, {type: DETAILS_LOADED, id: "1", detailsUri}); + expect(state.dashboard).toExist(); + expect(state.dashboard.resource).toExist(); + expect(state.dashboard.resource.attributes.details).toBe(detailsUri); + }); it('map created', () => { expect(mapConfig({ diff --git a/web/client/reducers/config.js b/web/client/reducers/config.js index 8119591743..d8b648ae84 100644 --- a/web/client/reducers/config.js +++ b/web/client/reducers/config.js @@ -110,8 +110,9 @@ function mapConfig(state = null, action) { } return state; case DETAILS_LOADED: + let dashboardResource = state.dashboard?.resource; map = state && state.map && state.map.present ? state.map.present : state && state.map; - if (map && map.mapId.toString() === action.mapId.toString()) { + if (map && map.mapId.toString() === action.id.toString()) { map = assign({}, map, { info: assign({}, map.info, { @@ -120,6 +121,17 @@ function mapConfig(state = null, action) { }) }); return assign({}, state, {map: map}); + } else if (dashboardResource && dashboardResource.id === action.id.toString()) { + dashboardResource = assign({}, dashboardResource, { + attributes: + assign({}, dashboardResource.attributes, { + details: action.detailsUri, + detailsSettings: action.detailsSettings + }) + }); + return assign({}, state, {dashboard: { + ...state.dashboard, resource: dashboardResource + }}); } return state; case MAP_CREATED: { diff --git a/web/client/reducers/details.js b/web/client/reducers/details.js index 630b355bf8..a071f9b721 100644 --- a/web/client/reducers/details.js +++ b/web/client/reducers/details.js @@ -13,7 +13,7 @@ import { const details = (state = {}, action) => { switch (action.type) { case UPDATE_DETAILS: { - return {...state, detailsText: action.detailsText}; + return {...state, detailsText: action.detailsText, id: action.id}; } default: return state; diff --git a/web/client/selectors/__tests__/dashboard-test.js b/web/client/selectors/__tests__/dashboard-test.js index 1c7c2a7f11..8e9048c1d3 100644 --- a/web/client/selectors/__tests__/dashboard-test.js +++ b/web/client/selectors/__tests__/dashboard-test.js @@ -22,7 +22,10 @@ import { selectedDashboardServiceSelector, dashboardCatalogModeSelector, dashboardIsNewServiceSelector, - dashboardSaveServiceSelector + dashboardSaveServiceSelector, + dashboardResourceInfoSelector, + dashbaordInfoDetailsUriFromIdSelector, + dashboardInfoDetailsSettingsFromIdSelector } from '../dashboard'; describe('dashboard selectors', () => { @@ -122,5 +125,28 @@ describe('dashboard selectors', () => { it("getDashboardId should return undefined in case resource does not exists", () => { expect(getDashboardId({dashboard: {resource: {}}})).toBe(undefined); }); - + it("test dashboardResourceInfoSelector", () => { + const resource = {}; + expect(dashboardResourceInfoSelector({dashboard: { + resource: resource + }})).toBe(resource); + }); + it("test dashbaordInfoDetailsUriFromIdSelector", () => { + expect(dashbaordInfoDetailsUriFromIdSelector({dashboard: { + resource: { + attributes: { + details: "Details" + } + } + }})).toBe("Details"); + }); + it("test dashboardInfoDetailsSettingsFromIdSelector", () => { + expect(dashboardInfoDetailsSettingsFromIdSelector({dashboard: { + resource: { + attributes: { + detailsSettings: "detailsSettings" + } + } + }})).toBe("detailsSettings"); + }); }); diff --git a/web/client/selectors/dashboard.js b/web/client/selectors/dashboard.js index 967c64e883..5f565ce8be 100644 --- a/web/client/selectors/dashboard.js +++ b/web/client/selectors/dashboard.js @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ import { createSelector } from 'reselect'; +import {get} from 'lodash'; import { pathnameSelector } from './router'; export const getDashboardId = state => state?.dashboard?.resource?.id; @@ -27,3 +28,7 @@ export const selectedDashboardServiceSelector = state => state && state.dashboar export const dashboardCatalogModeSelector = state => state && state.dashboard && state.dashboard.mode || "view"; export const dashboardIsNewServiceSelector = state => state.dashboard?.isNew || false; export const dashboardSaveServiceSelector = state => state.dashboard?.saveServiceLoading || false; +export const dashboardResourceInfoSelector = state => get(state, "dashboard.resource"); +export const dashbaordInfoDetailsUriFromIdSelector = state => state?.dashboard?.resource?.attributes?.details; +export const dashboardInfoDetailsSettingsFromIdSelector = state => get(dashboardResource(state), "attributes.detailsSettings"); + diff --git a/web/client/selectors/details.js b/web/client/selectors/details.js index 68241b5c71..98d51ac963 100644 --- a/web/client/selectors/details.js +++ b/web/client/selectors/details.js @@ -6,4 +6,23 @@ * LICENSE file in the root directory of this source tree. */ +import { dashboardInfoDetailsSettingsFromIdSelector, getDashboardId, dashbaordInfoDetailsUriFromIdSelector } from "./dashboard"; +import { mapIdSelector, mapInfoDetailsSettingsFromIdSelector, mapInfoDetailsUriFromIdSelector } from "./map"; + export const detailsTextSelector = state => state?.details?.detailsText; + +export const detailsUriSelector = state => { + const mapId = mapIdSelector(state); + const dashboardId = getDashboardId(state); + // todo: this is now for map and dashboard only, in the future if something else needs to use this like geostory, an additional contional should be added + let detailsUri = dashboardId && dashbaordInfoDetailsUriFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + return detailsUri; +}; + +export const detailsSettingsSelector = state => { + const mapId = mapIdSelector(state); + const dashboardId = getDashboardId(state); + // todo: this is now for map and dashboard only, in the future if something else needs to use this like geostory, an additional contional should be added + let detailsSettings = dashboardId && dashboardInfoDetailsSettingsFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsSettingsFromIdSelector(state, mapId); + return detailsSettings; +}; diff --git a/web/client/selectors/sidebarmenu.js b/web/client/selectors/sidebarmenu.js index f93cf8488e..a0bdc08951 100644 --- a/web/client/selectors/sidebarmenu.js +++ b/web/client/selectors/sidebarmenu.js @@ -1,5 +1,11 @@ import {get} from "lodash"; +import { isDashboardAvailable } from "./dashboard"; export const lastActiveToolSelector = (state) => get(state, "sidebarmenu.lastActiveItem", false); export const sidebarIsActiveSelector = (state) => get(state, 'controls.sidebarMenu.enabled', false); +export const isSidebarWithFullHeight = (state) =>{ + // here It is just for dashboard, but in the future if sidebar with full height is needed for anythinf else put here + return isDashboardAvailable(state); +}; + diff --git a/web/client/themes/default/less/dashboard.less b/web/client/themes/default/less/dashboard.less index e35a1ff7f3..1ebe1ab335 100644 --- a/web/client/themes/default/less/dashboard.less +++ b/web/client/themes/default/less/dashboard.less @@ -28,7 +28,6 @@ #mapstore-navbar-container { margin-bottom: 0; - z-index: 100; } .ms2-border-layout-content { diff --git a/web/client/themes/default/less/details.less b/web/client/themes/default/less/details.less index 9223bb2738..adb0ab86d6 100644 --- a/web/client/themes/default/less/details.less +++ b/web/client/themes/default/less/details.less @@ -89,3 +89,8 @@ text-align: inherit; } } + +// issue in react-dock fixed left with a fixed value and it can't be overrided +#details-container.leftZeroPanel> div.ms-side-panel> div> div{ + left: auto !important; +} diff --git a/web/client/themes/default/less/navbar.less b/web/client/themes/default/less/navbar.less index 055be3c0ce..66f597ca3f 100644 --- a/web/client/themes/default/less/navbar.less +++ b/web/client/themes/default/less/navbar.less @@ -59,7 +59,7 @@ ol { #mapstore-navbar-container { height: @square-btn-size; - + z-index: 1032; .nav { &.pull-left { display: flex;