diff --git a/web/client/actions/__tests__/details-test.js b/web/client/actions/__tests__/details-test.js index a0474fa074..4565666d32 100644 --- a/web/client/actions/__tests__/details-test.js +++ b/web/client/actions/__tests__/details-test.js @@ -34,7 +34,7 @@ describe('details actions tests', () => { 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('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..3414a072a8 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,7 +42,8 @@ class DetailsPanel extends React.Component { }, active: false, panelClassName: "details-panel", - width: 550 + width: 550, + isDashboard: false }; render() { @@ -58,9 +60,10 @@ class DetailsPanel extends React.Component { onClose={() => this.props.onClose()} glyph="sheet" style={this.props.dockStyle} + isDashboard={this.props.isDashboard} > - + {this.props.children} diff --git a/web/client/components/layout/BorderLayout.jsx b/web/client/components/layout/BorderLayout.jsx index c03a1854fa..440257ce2f 100644 --- a/web/client/components/layout/BorderLayout.jsx +++ b/web/client/components/layout/BorderLayout.jsx @@ -19,13 +19,14 @@ import React from 'react'; * /> * */ -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", isDashboard }) => (
{header} diff --git a/web/client/components/misc/panels/DockPanel.jsx b/web/client/components/misc/panels/DockPanel.jsx index 65a92b6e4c..26f2dd4bb8 100644 --- a/web/client/components/misc/panels/DockPanel.jsx +++ b/web/client/components/misc/panels/DockPanel.jsx @@ -56,9 +56,10 @@ export default withState('fullscreen', 'onFullscreen', false)( onFullscreen = () => {}, fixed = false, resizable = false, - hideHeader + hideHeader, + isDashboard }) => -
+
{ .withLatestFrom(props$) .switchMap(([resource, props]) => updateResource(resource) - .flatMap(rid => resource.category === 'MAP' ? updateResourceAttribute({ + .flatMap(rid => 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 b01959f586..efd32164e4 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -742,7 +742,15 @@ "FeedbackMask" ], "dashboard": [ - "BurgerMenu", + "Details", + "AddWidgetDashboard", + "MapConnectionDashboard", + { + "name": "SidebarMenu", + "cfg" : { + "containerPosition": "columns" + } + }, "Dashboard", "Notifications", "Login", diff --git a/web/client/epics/__tests__/config-test.js b/web/client/epics/__tests__/config-test.js index 92c1fe121a..3aa9a63d34 100644 --- a/web/client/epics/__tests__/config-test.js +++ b/web/client/epics/__tests__/config-test.js @@ -8,7 +8,7 @@ import expect from 'expect'; import {head} from 'lodash'; -import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoEpic} from '../config'; +import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoDashboardEpic, storeDetailsInfoEpic} from '../config'; import {LOAD_USER_SESSION} from '../../actions/usersession'; import { loadMapConfig, @@ -30,6 +30,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}) @@ -345,7 +346,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: @@ -379,5 +380,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..376adaf31a 100644 --- a/web/client/epics/__tests__/dashboard-test.js +++ b/web/client/epics/__tests__/dashboard-test.js @@ -338,16 +338,16 @@ 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(DASHBOARD_LOADING); + // expect(actions[0].value).toBe(true); + 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 +364,13 @@ 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(DASHBOARD_LOADING); + // expect(actions[0].value).toBe(true); + 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 a9fb45ad8a..37dc56b1b6 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"; @@ -212,3 +214,32 @@ export const storeDetailsInfoEpic = (action$, store) => ); }); }); +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..611005447e 100644 --- a/web/client/epics/details.js +++ b/web/client/epics/details.js @@ -18,8 +18,10 @@ import { import { toggleControl, setControlProperty } from '../actions/controls'; import { + mapIdSelector, mapInfoDetailsUriFromIdSelector } from '../selectors/map'; +import { getDashboardId, dashbaordInfoDetailsUriFromIdSelector } from '../selectors/dashboard'; import GeoStoreApi from '../api/GeoStoreDAO'; @@ -31,13 +33,16 @@ 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 = dashboardId && dashbaordInfoDetailsUriFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsUriFromIdSelector(state, mapId); 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..735a660a62 100644 --- a/web/client/epics/tutorial.js +++ b/web/client/epics/tutorial.js @@ -27,9 +27,10 @@ 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 { mapIdSelector, mapInfoDetailsSettingsFromIdSelector } from '../selectors/map'; import {REDUCERS_LOADED} from "../actions/storemanager"; import { VISUALIZATION_MODE_CHANGED } from '../actions/maptype'; +import { dashboardInfoDetailsSettingsFromIdSelector, getDashboardId } from '../selectors/dashboard'; 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 +169,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(); + const mapId = mapIdSelector(state); + const dashboardId = getDashboardId(state); + let detailsSettings = dashboardId && dashboardInfoDetailsSettingsFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsSettingsFromIdSelector(state, mapId); + if (detailsSettings && typeof detailsSettings === 'string') detailsSettings = JSON.parse(detailsSettings); + return detailsSettings?.showAtStartup; + }) .switchMap( () => { return Rx.Observable.of(openDetailsPanel()); }); diff --git a/web/client/plugins/DashboardEditor.jsx b/web/client/plugins/DashboardEditor.jsx index e1238de7a9..7971ebc9bf 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 } from '../selectors/dashboard'; +import { isDashboardEditing, isDashboardLoading } 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. @@ -141,10 +91,7 @@ class DashboardEditorComponent extends React.Component { return this.props.editing ?
this.props.setEditing(false)} catalog={this.props.catalog} />
- : (
- - {this.props.loading ? : null} -
); + : false; } } diff --git a/web/client/plugins/DashboardSave.jsx b/web/client/plugins/DashboardSave.jsx index 25cdf61230..b758ee2ed4 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); @@ -76,6 +77,21 @@ export const DashboardSave = createPlugin('DashboardSave', { 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), + // 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 + }) + ) } } }); @@ -120,6 +136,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..7a5535cb51 100644 --- a/web/client/plugins/Details.jsx +++ b/web/client/plugins/Details.jsx @@ -32,6 +32,7 @@ import { createPlugin } from '../utils/PluginsUtils'; import details from '../reducers/details'; import * as epics from '../epics/details'; import {createStructuredSelector} from "reselect"; +import { dashbaordInfoDetailsUriFromIdSelector, dashboardInfoDetailsSettingsFromIdSelector, getDashboardId } from '../selectors/dashboard'; /** * Allow to show details for the map. @@ -51,7 +52,8 @@ const DetailsPlugin = ({ dockStyle, detailsText, showAsModal = false, - onClose = () => {} + onClose = () => {}, + isDashboard }) => { const viewer = ( : { + 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.right = 0; + return layoutValues; + }, detailsText: detailsTextSelector, - showAsModal: state => mapInfoDetailsSettingsFromIdSelector(state)?.showAsModal + showAsModal: state => { + const mapId = mapIdSelector(state); + const dashboardId = getDashboardId(state); + let detailsSettings = dashboardId && dashboardInfoDetailsSettingsFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsSettingsFromIdSelector(state, mapId); + if (detailsSettings && typeof detailsSettings === 'string') detailsSettings = JSON.parse(detailsSettings); + return detailsSettings?.showAsModal; + } }), { onClose: closeDetailsPanel })(DetailsPlugin), @@ -100,7 +117,8 @@ export default createPlugin('Details', { action: openDetailsPanel, selector: (state) => { const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const dashboardId = getDashboardId(state); + const detailsUri = dashboardId && dashbaordInfoDetailsUriFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsUriFromIdSelector(state, mapId); if (detailsUri) { return {}; } @@ -118,7 +136,8 @@ export default createPlugin('Details', { action: openDetailsPanel, selector: (state) => { const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const dashboardId = getDashboardId(state); + const detailsUri = dashboardId && dashbaordInfoDetailsUriFromIdSelector(state, dashboardId) || mapId && mapInfoDetailsUriFromIdSelector(state, mapId); if (detailsUri) { return {}; } @@ -135,7 +154,8 @@ export default createPlugin('Details', { action: openDetailsPanel, selector: (state) => { const mapId = mapIdSelector(state); - const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId); + const dashboardId = getDashboardId(state); + const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId) || dashboardId && dashbaordInfoDetailsUriFromIdSelector(state, dashboardId); if (detailsUri) { return { bsStyle: state.controls.details && state.controls.details.enabled ? 'primary' : 'tray', diff --git a/web/client/plugins/SidebarMenu.jsx b/web/client/plugins/SidebarMenu.jsx index 538b93b823..7b7115608f 100644 --- a/web/client/plugins/SidebarMenu.jsx +++ b/web/client/plugins/SidebarMenu.jsx @@ -17,6 +17,7 @@ import {bindActionCreators} from "redux"; import ToolsContainer from "./containers/ToolsContainer"; import SidebarElement from "../components/sidebarmenu/SidebarElement"; import {mapLayoutValuesSelector} from "../selectors/maplayout"; +import { isDashboardLoading } from '../selectors/dashboard'; import tooltip from "../components/misc/enhancers/tooltip"; import {setControlProperty} from "../actions/controls"; import {createPlugin} from "../utils/PluginsUtils"; @@ -26,6 +27,7 @@ import './sidebarmenu/sidebarmenu.less'; import {lastActiveToolSelector, sidebarIsActiveSelector} from "../selectors/sidebarmenu"; import {setLastActiveItem} from "../actions/sidebarmenu"; import Message from "../components/I18N/Message"; +import LoadingSpinner from '../components/misc/LoadingSpinner'; const TDropdownButton = tooltip(DropdownButton); @@ -229,7 +231,7 @@ class SidebarMenu extends React.Component { render() { return this.state.hidden ? false : ( -
+
{ ({ height }) => } + {this.props.loading ? : null}
); @@ -270,12 +273,14 @@ const sidebarMenuSelector = createSelector([ state => state, state => lastActiveToolSelector(state), state => mapLayoutValuesSelector(state, {dockSize: true, bottom: true, height: true}), - sidebarIsActiveSelector -], (state, lastActiveTool, style, isActive) => ({ + sidebarIsActiveSelector, + isDashboardLoading +], (state, lastActiveTool, style, isActive, loading ) => ({ style, lastActiveTool, state, - isActive + isActive, + loading })); /** 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/dashboard/dashboardWidgets/AddWidgetDashboard.jsx b/web/client/plugins/dashboard/dashboardWidgets/AddWidgetDashboard.jsx new file mode 100644 index 0000000000..98bcd5dd5e --- /dev/null +++ b/web/client/plugins/dashboard/dashboardWidgets/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 { assign } from 'lodash'; +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: 'primary' }}/>); + } +} + +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: 2000, + tool: ConnectedAddWidget, + priority: 0 + } + } +}); diff --git a/web/client/plugins/dashboard/dashboardWidgets/MapConnectionDashboard.jsx b/web/client/plugins/dashboard/dashboardWidgets/MapConnectionDashboard.jsx new file mode 100644 index 0000000000..c6a0df47e7 --- /dev/null +++ b/web/client/plugins/dashboard/dashboardWidgets/MapConnectionDashboard.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 { Glyphicon } from 'react-bootstrap'; + +import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; +import { connect } from 'react-redux'; +// import Message from '../../../components/I18N/Message'; +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: 2000, + priority: 0 + } + } +}); diff --git a/web/client/plugins/sidebarmenu/sidebarmenu.less b/web/client/plugins/sidebarmenu/sidebarmenu.less index 04fdf249a6..84605cd0cc 100644 --- a/web/client/plugins/sidebarmenu/sidebarmenu.less +++ b/web/client/plugins/sidebarmenu/sidebarmenu.less @@ -35,3 +35,8 @@ } } } + +#mapstore-sidebar-menu-container.fullHightSideBar{ + 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..861f122f72 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/zoomOut' */ '../plugins/dashboard/dashboardWidgets/AddWidgetDashboard')), + MapConnectionDashboardPlugin: toModulePlugin('MapConnectionDashboard', () => import(/* webpackChunkName: 'plugins/zoomOut' */ '../plugins/dashboard/dashboardWidgets/MapConnectionDashboard')) }; const pluginsDefinition = { diff --git a/web/client/reducers/__tests__/config-test.js b/web/client/reducers/__tests__/config-test.js index 81d88c60db..f843c6cb60 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 ef022e6353..74a16041cd 100644 --- a/web/client/reducers/config.js +++ b/web/client/reducers/config.js @@ -98,8 +98,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, { @@ -108,6 +109,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/themes/default/less/panels.less b/web/client/themes/default/less/panels.less index 5f87d294c1..17f9e45100 100644 --- a/web/client/themes/default/less/panels.less +++ b/web/client/themes/default/less/panels.less @@ -64,6 +64,12 @@ } } } + &.ms-dashboard-opened { + > div > div { + left: auto !important; // it is for panel details in case of dashboard only + right:0 !important; // it is for panel details in case of dashboard only + } + } .ql-video { .ms-quill-iframe { display: none;