From da3575cb260688d1a6b4fc3f4f904d697a4f2097 Mon Sep 17 00:00:00 2001 From: Vladislav Shatilenya Date: Fri, 6 Mar 2020 18:34:50 +0300 Subject: [PATCH 1/2] Adds new plugin to MapStore MapCatalog --- web/client/actions/mapcatalog.js | 25 +++ .../components/mapcatalog/MapCatalogPanel.jsx | 168 ++++++++++++++++++ .../__tests__/MapCatalogPanel-test.jsx | 32 ++++ .../mapcatalog/enhancers/withDelete.jsx | 50 ++++++ .../mapcatalog/enhancers/withEdit.jsx | 59 ++++++ .../mapcatalog/enhancers/withFilter.jsx | 30 ++++ web/client/components/maps/MapCatalog.jsx | 19 +- .../misc/enhancers/infiniteScroll/loadMore.js | 9 +- .../infiniteScroll/withInfiniteScroll.js | 3 +- .../enhancers/infiniteScroll/withScrollSpy.js | 6 + .../misc/enhancers/loadingState.jsx | 2 +- web/client/epics/mapcatalog.js | 52 ++++++ web/client/plugins/MapCatalog.jsx | 95 ++++++++++ .../plugins/__tests__/MapCatalog-test.jsx | 42 +++++ web/client/product/plugins.js | 1 + web/client/reducers/mapcatalog.js | 23 +++ web/client/selectors/mapcatalog.js | 9 + .../themes/default/less/mapcatalog.less | 48 +++++ web/client/themes/default/ms2-theme.less | 1 + web/client/translations/data.de-DE.json | 20 +++ web/client/translations/data.en-US.json | 20 +++ web/client/translations/data.es-ES.json | 20 +++ web/client/translations/data.fr-FR.json | 20 +++ web/client/translations/data.it-IT.json | 20 +++ 24 files changed, 765 insertions(+), 9 deletions(-) create mode 100644 web/client/actions/mapcatalog.js create mode 100644 web/client/components/mapcatalog/MapCatalogPanel.jsx create mode 100644 web/client/components/mapcatalog/__tests__/MapCatalogPanel-test.jsx create mode 100644 web/client/components/mapcatalog/enhancers/withDelete.jsx create mode 100644 web/client/components/mapcatalog/enhancers/withEdit.jsx create mode 100644 web/client/components/mapcatalog/enhancers/withFilter.jsx create mode 100644 web/client/epics/mapcatalog.js create mode 100644 web/client/plugins/MapCatalog.jsx create mode 100644 web/client/plugins/__tests__/MapCatalog-test.jsx create mode 100644 web/client/reducers/mapcatalog.js create mode 100644 web/client/selectors/mapcatalog.js create mode 100644 web/client/themes/default/less/mapcatalog.less diff --git a/web/client/actions/mapcatalog.js b/web/client/actions/mapcatalog.js new file mode 100644 index 0000000000..dd2bc1f1e1 --- /dev/null +++ b/web/client/actions/mapcatalog.js @@ -0,0 +1,25 @@ +/** + * Copyright 2020, 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. + */ + +export const DELETE_MAP = 'MAPCATALOG:DELETE_MAP'; +export const SAVE_MAP = 'MAPCATALOG:SAVE_MAP'; +export const TRIGGER_RELOAD = 'MAPCATALOG:TRIGGER_RELOAD'; + +export const deleteMap = (resource) => ({ + type: DELETE_MAP, + resource +}); + +export const saveMap = (resource) => ({ + type: SAVE_MAP, + resource +}); + +export const triggerReload = () => ({ + type: TRIGGER_RELOAD +}); diff --git a/web/client/components/mapcatalog/MapCatalogPanel.jsx b/web/client/components/mapcatalog/MapCatalogPanel.jsx new file mode 100644 index 0000000000..69f68aab45 --- /dev/null +++ b/web/client/components/mapcatalog/MapCatalogPanel.jsx @@ -0,0 +1,168 @@ +/** + * Copyright 2020, 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 Rx from 'rxjs'; +import { isArray, zip } from 'lodash'; +import { compose, getContext } from 'recompose'; +import { Glyphicon } from 'react-bootstrap'; + +import { getResource } from '../../api/persistence'; +import Api from '../../api/GeoStoreDAO'; + +import withInfiniteScroll from '../misc/enhancers/infiniteScroll/withInfiniteScroll'; +import withShareTool from '../resources/enhancers/withShareTool'; +import withFilter from './enhancers/withFilter'; +import withDelete from './enhancers/withDelete'; +import withEdit from './enhancers/withEdit'; +import LocaleUtils from '../../utils/LocaleUtils'; +import Toolbar from '../misc/toolbar/Toolbar'; +import Filter from '../misc/Filter'; +import MapCatalog from '../maps/MapCatalog'; + +const getContextNames = ({results, ...other}) => { + const maps = isArray(results) ? results : (results === "" ? [] : [results]); + return maps.length === 0 ? + Rx.Observable.of({results, ...other}) : + Rx.Observable.forkJoin( + maps.map(({context}) => context ? + getResource(context, {includeAttributes: false, withData: false, withPermissions: false}) + .switchMap(resource => Rx.Observable.of(resource.name)) : + Rx.Observable.of(null)) + ).map(contextNames => ({ + results: zip(maps, contextNames).map( + ([curMap, contextName]) => ({...curMap, contextName})), + ...other + })); +}; + +const searchMaps = ({searchText, opts}) => Rx.Observable.defer(() => Api.getResourcesByCategory( + 'MAP', + searchText || '*', + opts +)).switchMap(response => getContextNames(response)) + .map(result => ({ + items: result.results, + total: result.totalCount, + loading: false + })).catch(() => Rx.Observable.of({ + items: [], + total: 0, + loading: false + })); + +const loadPage = ({searchText = '', limit = 12} = {}, page = 0) => searchMaps({ + searchText, + opts: { + params: { + includeAttributes: true, + start: page * limit, + limit + } + } +}); + +const MapCatalogPanel = ({ + loading, + mapType, + items = [], + filterText, + onFilter = () => {}, + onDelete = () => {}, + onEdit = () => {}, + onShare = () => {}, + messages = {}, + router = {} +}) => { + const mapToItem = (map) => ({ + title: map.name, + description: map.description, + tools: { + e.stopPropagation(); + onDelete(map); + } + }, { + glyph: 'wrench', + bsStyle: 'primary', + tooltipId: 'mapCatalog.tooltips.edit', + visible: map.canEdit, + onClick: (e) => { + e.stopPropagation(); + onEdit(map); + } + }, { + glyph: 'share-alt', + bsStyle: 'primary', + className: 'square-button-md', + tooltipId: 'mapCatalog.tooltips.share', + onClick: (e) => { + e.stopPropagation(); + onShare(map); + } + }]}/>, + preview: +
+ {map.thumbnail && map.thumbnail !== 'NODATA' ? + : + } +
, + onClick: () => router.history.push(map.contextName ? + "/context/" + map.contextName + "/" + map.id : + "/viewer/" + mapType + "/" + map.id + ) + }); + + return ( +
+ + } + items={items.map(mapToItem)}/> +
+ ); +}; + +export default compose( + getContext({ + messages: PropTypes.object, + router: PropTypes.object + }), + withInfiniteScroll({ + loadPage, + loadMoreStreamOptions: { + initialStreamDebounce: 300 + }, + scrollSpyOptions: { + querySelector: '.map-catalog-panel > .map-catalog > .ms2-border-layout-body', + pageSize: 12 + }, + hasMore: ({items = [], total = 0}) => items.length < total + }), + withFilter, + withDelete, + withEdit, + withShareTool +)(MapCatalogPanel); diff --git a/web/client/components/mapcatalog/__tests__/MapCatalogPanel-test.jsx b/web/client/components/mapcatalog/__tests__/MapCatalogPanel-test.jsx new file mode 100644 index 0000000000..6b4cfa83e8 --- /dev/null +++ b/web/client/components/mapcatalog/__tests__/MapCatalogPanel-test.jsx @@ -0,0 +1,32 @@ +/* + * Copyright 2019, 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 ReactDOM from 'react-dom'; +import expect from 'expect'; + +import MapCatalogPanel from '../MapCatalogPanel'; + +describe('MapCatalogPanel component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('MapCatalogPanel with defaults', () => { + ReactDOM.render(, document.getElementById('container')); + const rootDiv = document.getElementsByClassName('map-catalog-panel')[0]; + expect(rootDiv).toExist(); + }); +}); diff --git a/web/client/components/mapcatalog/enhancers/withDelete.jsx b/web/client/components/mapcatalog/enhancers/withDelete.jsx new file mode 100644 index 0000000000..c7c9d49f8b --- /dev/null +++ b/web/client/components/mapcatalog/enhancers/withDelete.jsx @@ -0,0 +1,50 @@ +/** + * Copyright 2020, 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 Message from '../../I18N/Message'; +import ConfirmDialog from '../../misc/ConfirmDialog'; + +export default (Component) => ({ + onDelete = () => {}, + ...props +}) => { + const [showConfirm, setShowConfirm] = React.useState(false); + const [resourceToDelete, setResourceToDelete] = React.useState(); + + return ( + <> + { + setResourceToDelete(resource); + setShowConfirm(true); + }}/> + { + setResourceToDelete(); + setShowConfirm(false); + }} + onConfirm={() => { + setShowConfirm(false); + onDelete(resourceToDelete); + setResourceToDelete(); + }} + confirmButtonBSStyle="default" + confirmButtonContent={} + closeText={} + closeGlyph="1-close"> + + + + ); +}; diff --git a/web/client/components/mapcatalog/enhancers/withEdit.jsx b/web/client/components/mapcatalog/enhancers/withEdit.jsx new file mode 100644 index 0000000000..944e5633f3 --- /dev/null +++ b/web/client/components/mapcatalog/enhancers/withEdit.jsx @@ -0,0 +1,59 @@ +/** + * Copyright 2020, 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 SaveModal from '../../resources/modals/Save'; +import handleSaveModal from '../../resources/modals/enhancers/handleSaveModal'; + +const SaveDialog = handleSaveModal(SaveModal); + +export default (Component) => ({ + user, + saveDialogTitle = 'resources.resource.editResource', + onSave = () => {}, + ...props +}) => { + const [showSaveDialog, setShowSaveDialog] = React.useState(false); + const [resource, setResource] = React.useState(); + + return ( + <> + { + setResource(resourceToEdit); + setShowSaveDialog(true); + }}/> + { + onSave(resourceToSave); + setResource(); + setShowSaveDialog(false); + }} + onClose={() => { + setResource(); + setShowSaveDialog(false); + }}/> + + ); +}; diff --git a/web/client/components/mapcatalog/enhancers/withFilter.jsx b/web/client/components/mapcatalog/enhancers/withFilter.jsx new file mode 100644 index 0000000000..86035f320a --- /dev/null +++ b/web/client/components/mapcatalog/enhancers/withFilter.jsx @@ -0,0 +1,30 @@ +/** + * Copyright 2020, 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'; + +export default (Component) => ({ + triggerReloadValue = false, + onTriggerReload = () => {}, + loadFirst = () => {}, + ...props +}) => { + const [filterText, setFilterText] = React.useState(''); + + React.useEffect(() => { + loadFirst({searchText: filterText}); + }, [filterText, triggerReloadValue, loadFirst]); + + return ( + onTriggerReload()}/> + ); +}; diff --git a/web/client/components/maps/MapCatalog.jsx b/web/client/components/maps/MapCatalog.jsx index 683df480b3..29e2946900 100644 --- a/web/client/components/maps/MapCatalog.jsx +++ b/web/client/components/maps/MapCatalog.jsx @@ -25,17 +25,30 @@ const SideGrid = compose( }) )(require('../misc/cardgrids/SideGrid')); -module.exports = ({ setSearchText = () => { }, selected, skip = 0, onSelected, loading, searchText, items = [], total, title = }) => { +module.exports = ({ + setSearchText = () => { }, + selected, + skip = 0, + onSelected, + loading, + searchText, + items = [], + loaderProps = {}, + total, + header, + title = +}) => { return (} + header={header || } footer={
- {loading ? : null} + {loading && items.length > 0 ? : null} {!isNil(total) ? : null}
}> i === selected || selected diff --git a/web/client/components/misc/enhancers/infiniteScroll/loadMore.js b/web/client/components/misc/enhancers/infiniteScroll/loadMore.js index cb021e1670..adaf59e35b 100644 --- a/web/client/components/misc/enhancers/infiniteScroll/loadMore.js +++ b/web/client/components/misc/enhancers/infiniteScroll/loadMore.js @@ -16,8 +16,8 @@ const Rx = require('rxjs'); * @param {function} loadPage A function that returns the observable that emits the page loaded. the event emitted must have at least { items [ ...items of the new page], total: the total number of results} * @return {Observable} Stream of props {with items, loading, error, total} */ -const loadMoreStream = (initialStream$, loadMore$, loadPage, {dataProp = "items", throttleTime = 500} = {}) => - initialStream$.switchMap(searchParams => +const loadMoreStream = (initialStream$, loadMore$, loadPage, {dataProp = "items", initialStreamDebounce = 0, throttleTime = 500} = {}) => + initialStream$.take(1).concat(initialStream$.debounceTime(initialStreamDebounce)).switchMap(searchParams => loadPage(searchParams, 0) .startWith({ loading: true }) .concat(Rx.Observable.of({loading: false})) @@ -77,13 +77,14 @@ const loadMoreStream = (initialStream$, loadMore$, loadPage, {dataProp = "items" * @param {function} loadPage the function that returns the stream. It must accept 2 params: `searchParams`, `page`. */ -module.exports = (loadPage = () => Rx.Observable.empty()) => mapPropsStream((props$) => { +module.exports = (loadPage = () => Rx.Observable.empty(), options) => mapPropsStream((props$) => { const { handler: onLoadMore, stream: loadMore$ } = createEventHandler(); const { handler: loadFirst, stream: initialStream$ } = createEventHandler(); return props$.combineLatest(loadMoreStream( initialStream$, loadMore$, - loadPage + loadPage, + options ).startWith({}), (a, b) => ({ ...a, ...b, diff --git a/web/client/components/misc/enhancers/infiniteScroll/withInfiniteScroll.js b/web/client/components/misc/enhancers/infiniteScroll/withInfiniteScroll.js index 1be898b255..e7a8162c43 100644 --- a/web/client/components/misc/enhancers/infiniteScroll/withInfiniteScroll.js +++ b/web/client/components/misc/enhancers/infiniteScroll/withInfiniteScroll.js @@ -27,10 +27,11 @@ module.exports = ({ loadPage, scrollSpyOptions, loadStreamOptions, + loadMoreStreamOptions, hasMore, isScrolled }) => compose( - loadMore(loadPage), + loadMore(loadPage, loadMoreStreamOptions), defaultProps({hasMore, isScrolled}), withScrollSpy(scrollSpyOptions, loadStreamOptions) diff --git a/web/client/components/misc/enhancers/infiniteScroll/withScrollSpy.js b/web/client/components/misc/enhancers/infiniteScroll/withScrollSpy.js index fcb5c5d6ab..211b6d3a26 100644 --- a/web/client/components/misc/enhancers/infiniteScroll/withScrollSpy.js +++ b/web/client/components/misc/enhancers/infiniteScroll/withScrollSpy.js @@ -65,12 +65,17 @@ module.exports = ({ this.isMounded = true; const div = this.findScrollDomNode(); if (div) { + this.listenerAdded = true; div.addEventListener('scroll', this.onScroll, false); this.onScroll(); } } componentDidUpdate(prevProps) { const div = this.findScrollDomNode(); + if (div && !this.listenerAdded) { + div.addEventListener('scroll', this.onScroll, false); + this.listenerAdded = true; + } if (div && (dataProp && this.props[dataProp] ? this.props[dataProp] !== prevProps[dataProp] @@ -80,6 +85,7 @@ module.exports = ({ } componentWillUnmount() { this.isMounded = false; + this.listenerAdded = false; const div = this.findScrollDomNode(); if (div) { div.removeEventListener('scroll', this.onScroll, false); diff --git a/web/client/components/misc/enhancers/loadingState.jsx b/web/client/components/misc/enhancers/loadingState.jsx index 92b6965f96..24517d83a2 100644 --- a/web/client/components/misc/enhancers/loadingState.jsx +++ b/web/client/components/misc/enhancers/loadingState.jsx @@ -27,4 +27,4 @@ const defaultTest = ({loading, isLoading}) => loading || isLoading && ((typeof i module.exports = (isLoading = defaultTest, loaderProps = {}, LoadingComponent = DefaultLoadingComponent) => branch( isLoading, // TODO return proper HOC - () => () => ); + () => ({loaderProps: dynamicLoaderProps}) => ); diff --git a/web/client/epics/mapcatalog.js b/web/client/epics/mapcatalog.js new file mode 100644 index 0000000000..30320f4d88 --- /dev/null +++ b/web/client/epics/mapcatalog.js @@ -0,0 +1,52 @@ +/** + * Copyright 2020, 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 Rx from 'rxjs'; + +import { basicSuccess, basicError } from '../utils/NotificationUtils'; +import { deleteResource, updateResource } from '../api/persistence'; + +import { + DELETE_MAP, + SAVE_MAP, + triggerReload +} from '../actions/mapcatalog'; + +export const deleteMapEpic = (action$) => action$ + .ofType(DELETE_MAP) + .switchMap(({resource}) => + deleteResource(resource) + .switchMap(() => + Rx.Observable.of(basicSuccess({ + title: 'mapCatalog.deletedMap.title', + message: 'mapCatalog.deletedMap.message', + autoDismiss: 6, + position: 'tc' + }), triggerReload()) + ) + .catch(() => Rx.Observable.of(basicError({ + message: 'mapCatalog.deleteError' + }))) + ); + +export const saveMapEpic = (action$) => action$ + .ofType(SAVE_MAP) + .switchMap(({resource}) => + updateResource(resource) + .switchMap(() => + Rx.Observable.of(basicSuccess({ + title: 'mapCatalog.updatedMap.title', + message: 'mapCatalog.updatedMap.message', + autoDismiss: 6, + position: 'tc' + }), triggerReload()) + ) + .catch(() => Rx.Observable.of(basicError({ + message: 'mapCatalog.updateError' + }))) + ); diff --git a/web/client/plugins/MapCatalog.jsx b/web/client/plugins/MapCatalog.jsx new file mode 100644 index 0000000000..b4ecc7457f --- /dev/null +++ b/web/client/plugins/MapCatalog.jsx @@ -0,0 +1,95 @@ +/** + * Copyright 2020, 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 { connect } from 'react-redux'; +import { createStructuredSelector } from 'reselect'; + +import { toggleControl } from '../actions/controls'; +import { + triggerReload, + saveMap, + deleteMap +} from '../actions/mapcatalog'; +import { mapTypeSelector } from '../selectors/maptype'; +import { userSelector } from '../selectors/security'; +import { triggerReloadValueSelector } from '../selectors/mapcatalog'; + +import MapCatalogPanel from '../components/mapcatalog/MapCatalogPanel'; +import DockPanel from '../components/misc/panels/DockPanel'; +import Message from '../components/I18N/Message'; +import { createPlugin } from '../utils/PluginsUtils'; + +import mapcatalog from '../reducers/mapcatalog'; +import * as epics from '../epics/mapcatalog'; + +const MapCatalogComponent = ({ + active, + mapType, + user, + triggerReloadValue, + onToggleControl = () => {}, + onTriggerReload = () => {}, + onDelete = () => {}, + onSave = () => {} +}) => { + return ( + } + onClose={() => onToggleControl()} + style={{ height: 'calc(100% - 30px)' }}> + map.contextName ? + `context/${map.contextName}/${map.id}` : + `viewer/${mapType}/${map.id}` + } + shareApi/> + + ); +}; + +export default createPlugin('MapCatalog', { + component: connect(createStructuredSelector({ + active: state => state.controls?.mapCatalog?.enabled, + mapType: mapTypeSelector, + user: userSelector, + triggerReloadValue: triggerReloadValueSelector + }), { + onToggleControl: toggleControl.bind(null, 'mapCatalog', 'enabled'), + onTriggerReload: triggerReload, + onDelete: deleteMap, + onSave: saveMap + })(MapCatalogComponent), + containers: { + BurgerMenu: { + name: 'mapcatalog', + position: 6, + text: , + icon: , + action: () => toggleControl('mapCatalog', 'enabled'), + priority: 2, + doNotHide: true + } + }, + reducers: { + mapcatalog + }, + epics +}); diff --git a/web/client/plugins/__tests__/MapCatalog-test.jsx b/web/client/plugins/__tests__/MapCatalog-test.jsx new file mode 100644 index 0000000000..091893746a --- /dev/null +++ b/web/client/plugins/__tests__/MapCatalog-test.jsx @@ -0,0 +1,42 @@ +/** + * Copyright 2020, 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 ReactDOM from 'react-dom'; +import expect from 'expect'; + +import { getPluginForTest } from './pluginsTestUtils'; + +import MapCatalog from '../MapCatalog'; + +describe('MapCatalog plugin', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('MapCatalog with defaults', () => { + const { Plugin } = getPluginForTest(MapCatalog, { + controls: { + mapcatalog: { + enabled: true + } + } + }); + + ReactDOM.render(, document.getElementById('container')); + const rootDiv = document.getElementsByClassName('map-catalog-dock-panel')[0]; + expect(rootDiv).toExist(); + }); +}); diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index c7f86f1abb..5850cc78e9 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -78,6 +78,7 @@ module.exports = { MapPlugin: require('../plugins/Map'), MapSearchPlugin: require('../plugins/MapSearch'), MapsPlugin: require('../plugins/Maps'), + MapCatalogPlugin: require('../plugins/MapCatalog').default, MapTemplatesPlugin: require('../plugins/MapTemplates').default, MeasurePlugin: require('../plugins/Measure'), MediaEditorPlugin: require('../plugins/MediaEditor').default, diff --git a/web/client/reducers/mapcatalog.js b/web/client/reducers/mapcatalog.js new file mode 100644 index 0000000000..49a81e7580 --- /dev/null +++ b/web/client/reducers/mapcatalog.js @@ -0,0 +1,23 @@ +/** + * Copyright 2020, 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 { + TRIGGER_RELOAD +} from '../actions/mapcatalog'; + +import { set } from '../utils/ImmutableUtils'; + +export default (state = {}, action) => { + switch (action.type) { + case TRIGGER_RELOAD: { + return set('triggerReloadValue', !(state.triggerReloadValue || false), state); + } + default: + return state; + } +}; diff --git a/web/client/selectors/mapcatalog.js b/web/client/selectors/mapcatalog.js new file mode 100644 index 0000000000..a83e556c65 --- /dev/null +++ b/web/client/selectors/mapcatalog.js @@ -0,0 +1,9 @@ +/** + * Copyright 2020, 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. + */ + +export const triggerReloadValueSelector = state => state.mapcatalog?.triggerReloadValue; diff --git a/web/client/themes/default/less/mapcatalog.less b/web/client/themes/default/less/mapcatalog.less new file mode 100644 index 0000000000..d893d30904 --- /dev/null +++ b/web/client/themes/default/less/mapcatalog.less @@ -0,0 +1,48 @@ +.map-catalog-dock-panel .ms2-border-layout-body > .ms2-border-layout-content { + display: flex; +} + +.map-catalog-dock-panel .ms2-border-layout-body > .ms2-border-layout-content > div { + flex-grow: 1; +} + +.map-catalog-panel { + height: 100%; + + .loader-container { + height: 100%; + display: flex; + } + + .ms2-border-layout-body > .ms2-border-layout-content { + display: block; + } + + .msSideGrid { + position: relative; + } + + .map-catalog > .mapstore-filter { + margin-bottom: 8px; + padding: 0 15px 0 15px; + } + + .map-catalog-preview { + display: flex; + position: relative; + width: 100%; + height: 100%; + background-color: #ddd; + + img { + width: 100%; + height: 100%; + } + + .glyphicon { + font-size: @square-btn-size; + margin: auto; + color: white; + } + } +} \ No newline at end of file diff --git a/web/client/themes/default/ms2-theme.less b/web/client/themes/default/ms2-theme.less index f5b9c0eb69..eae5a601a7 100644 --- a/web/client/themes/default/ms2-theme.less +++ b/web/client/themes/default/ms2-theme.less @@ -27,6 +27,7 @@ @import "./less/manager.less"; @import "./less/map-footer.less"; @import "./less/map-search-bar.less"; +@import "./less/mapcatalog.less"; @import "./less/maptemplates.less"; @import "./less/map-toolbar.less"; @import "./less/measure.less"; diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 19d991b237..4f88978ea0 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -2439,6 +2439,26 @@ "confirmReplaceMessage": "Sie ersetzen alle Layer und die Kartenkonfiguration durch die ausgewählte Vorlage, indem Sie auf die Schaltfläche Ersetzen klicken", "confirmReplaceConfirmButton": "Ersetzen" }, + "mapCatalog": { + "title": "Kartenkatalog", + "filterPlaceholder": "Karten suchen...", + "deleteConfirmContent": "Möchten Sie die Karte \"{mapName}\" wirklich löschen?", + "tooltips": { + "edit": "Eigenschaften bearbeiten", + "delete": "Ressource löschen", + "share": "Teilen" + }, + "deletedMap": { + "title": "Karte gelöscht", + "message": "Die Karte wurde erfolgreich gelöscht" + }, + "updatedMap": { + "title": "Gespeicherte Karte", + "message": "Die Karte wurde erfolgreich gespeichert" + }, + "deleteError": "Beim Löschen der Karte ist ein unerwarteter Fehler aufgetreten", + "updateError": "Beim Speichern der Karte ist ein unerwarteter Fehler aufgetreten" + }, "tutorial": { "title": "Anleitung", "back": "zurück", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 8373a2de48..6a7c6d2193 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -2442,6 +2442,26 @@ "confirmReplaceMessage": "You will replace all layers and map configuration with the selected template by clicking on replace button", "confirmReplaceConfirmButton": "Replace" }, + "mapCatalog": { + "title": "Map Catalog", + "filterPlaceholder": "Search maps...", + "deleteConfirmContent": "Are you sure you want to delete \"{mapName}\" map?", + "tooltips": { + "edit": "Edit properties", + "delete": "Delete resource", + "share": "Share" + }, + "deletedMap": { + "title": "Deleted Map", + "message": "The map was successfully deleted" + }, + "updatedMap": { + "title": "Saved Map", + "message": "The map was successfully saved" + }, + "deleteError": "Unexpected error occurred while deleting the map", + "updateError": "Unexpected error occurred while saving the map" + }, "tutorial": { "title": "Tutorial", "back": "Back", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index a7cfa26a04..af719cc7b0 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -2438,6 +2438,26 @@ "confirmReplaceMessage": "Reemplazará todas las capas y la configuración del mapa con la plantilla seleccionada haciendo clic en el botón Reemplazar", "confirmReplaceConfirmButton": "Reemplazar" }, + "mapCatalog": { + "title": "Catálogo de mapas", + "filterPlaceholder": "Buscar mapas...", + "deleteConfirmContent": "¿Estás seguro de que quieres eliminar el mapa \"{mapName}\"?", + "tooltips": { + "edit": "Editar propiedades", + "delete": "Eliminar recurso", + "share": "Compartir" + }, + "deletedMap": { + "title": "Mapa eliminado", + "message": "El mapa se eliminó correctamente" + }, + "updatedMap": { + "title": "Mapa guardado", + "message": "El mapa se guardó correctamente" + }, + "deleteError": "Se produjo un error inesperado al eliminar el mapa", + "updateError": "Se produjo un error inesperado al guardar el mapa" + }, "tutorial": { "title": "Tutorial", "back": "Atrás", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 1854812872..a5bc5efeb4 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -2438,6 +2438,26 @@ "confirmReplaceMessage": "Vous allez remplacer toutes les couches et la configuration de la carte par le modèle sélectionné en cliquant sur le bouton remplacer", "confirmReplaceConfirmButton": "Remplacer" }, + "mapCatalog": { + "title": "Catalogue de cartes", + "filterPlaceholder": "Rechercher des cartes...", + "deleteConfirmContent": "Voulez-vous vraiment supprimer la carte \"{mapName}\"?", + "tooltips": { + "edit": "Modifier les propriétés", + "delete": "Supprimer la ressource", + "share": "Partager" + }, + "deletedMap": { + "title": "Carte supprimée", + "message": "La carte a été supprimée avec succès" + }, + "updatedMap": { + "title": "Carte sauvegardée", + "message": "La carte a été enregistrée avec succès" + }, + "deleteError": "Une erreur inattendue s'est produite lors de la suppression de la carte", + "updateError": "Une erreur inattendue s'est produite lors de l'enregistrement de la carte" + }, "tutorial": { "title": "Tutoriel", "back": "Retour", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 679c8cc070..79dd8a2846 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -2441,6 +2441,26 @@ "confirmReplaceMessage": "Sostituirai tutti i livelli e la configurazione della mappa con il modello selezionato facendo clic sul pulsante Sostituisci", "confirmReplaceConfirmButton": "Sostituire" }, + "mapCatalog": { + "title": "Catalogo mappe", + "filterPlaceholder": "Cerca mappe...", + "deleteConfirmContent": "Sei sicuro di voler eliminare la mappa \"{mapName}\"?", + "tooltips": { + "edit": "Modifica proprietà", + "delete": "Elimina risorsa", + "share": "Condividi" + }, + "deletedMap": { + "title": "Mappa cancellata", + "message": "La mappa è stata eliminata correttamente" + }, + "updatedMap": { + "title": "Mappa Salvata", + "message": "La mappa è stata salvata correttamente" + }, + "deleteError": "Si è verificato un errore imprevisto durante l'eliminazione della mappa", + "updateError": "Si è verificato un errore imprevisto durante il salvataggio della mappa" + }, "tutorial": { "title": "Tutorial", "back": "Indietro", From 4619fe1364fc57931a4e1b62f12c4717a90fc0cf Mon Sep 17 00:00:00 2001 From: Vladislav Shatilenya Date: Mon, 9 Mar 2020 20:58:58 +0300 Subject: [PATCH 2/2] made plugin available in context creator, added tests --- .../actions/__tests__/mapcatalog-test.js | 32 +++++++++ .../infiniteScroll/__tests__/loadMore-test.js | 30 ++++++++ web/client/epics/__tests__/mapcatalog-test.js | 71 +++++++++++++++++++ web/client/pluginsConfig.json | 9 +++ .../reducers/__tests__/mapcatalog-test.js | 26 +++++++ web/client/reducers/mapcatalog.js | 6 ++ .../selectors/__tests__/mapcatalog-test.js | 24 +++++++ web/client/translations/data.de-DE.json | 4 ++ web/client/translations/data.en-US.json | 4 ++ web/client/translations/data.es-ES.json | 4 ++ web/client/translations/data.fr-FR.json | 4 ++ web/client/translations/data.it-IT.json | 4 ++ 12 files changed, 218 insertions(+) create mode 100644 web/client/actions/__tests__/mapcatalog-test.js create mode 100644 web/client/epics/__tests__/mapcatalog-test.js create mode 100644 web/client/reducers/__tests__/mapcatalog-test.js create mode 100644 web/client/selectors/__tests__/mapcatalog-test.js diff --git a/web/client/actions/__tests__/mapcatalog-test.js b/web/client/actions/__tests__/mapcatalog-test.js new file mode 100644 index 0000000000..1d7336a913 --- /dev/null +++ b/web/client/actions/__tests__/mapcatalog-test.js @@ -0,0 +1,32 @@ +/* + * Copyright 2020, 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 expect from 'expect'; + +import { + deleteMap, DELETE_MAP, + saveMap, SAVE_MAP, + triggerReload, TRIGGER_RELOAD +} from '../mapcatalog'; + +describe('mapcatalog actions', () => { + it('deleteMap', () => { + const retval = deleteMap('map'); + expect(retval.type).toBe(DELETE_MAP); + expect(retval.resource).toBe('map'); + }); + it('saveMap', () => { + const retval = saveMap('map'); + expect(retval.type).toBe(SAVE_MAP); + expect(retval.resource).toBe('map'); + }); + it('triggerReload', () => { + const retval = triggerReload(); + expect(retval.type).toBe(TRIGGER_RELOAD); + }); +}); diff --git a/web/client/components/misc/enhancers/infiniteScroll/__tests__/loadMore-test.js b/web/client/components/misc/enhancers/infiniteScroll/__tests__/loadMore-test.js index dbdde95d74..cad58d3ffb 100644 --- a/web/client/components/misc/enhancers/infiniteScroll/__tests__/loadMore-test.js +++ b/web/client/components/misc/enhancers/infiniteScroll/__tests__/loadMore-test.js @@ -43,4 +43,34 @@ describe('loadMore enhancer', () => { })); ReactDOM.render(, document.getElementById("container")); }); + it('loadMore test debounce on loadFirst', (done) => { + const Sink = loadMore( + ({text = ''} = {}, page) => { + return Observable.of({ items: Array(10), page, text }).catch( e => { done(e); }); + }, { + initialStreamDebounce: 50 + } + )(createSink(props => { + if (props.page === undefined) { + props.loadFirst(); + setTimeout(() => { + props.loadFirst({text: 't'}); + }, 15); + setTimeout(() => { + props.loadFirst({text: 'te'}); + }, 20); + setTimeout(() => { + props.loadFirst({text: 'tex'}); + }, 25); + } else if (props.text) { + try { + expect(props.text).toBe('tex'); + done(); + } catch (e) { + done(e); + } + } + })); + ReactDOM.render(, document.getElementById("container")); + }); }); diff --git a/web/client/epics/__tests__/mapcatalog-test.js b/web/client/epics/__tests__/mapcatalog-test.js new file mode 100644 index 0000000000..238c215fbf --- /dev/null +++ b/web/client/epics/__tests__/mapcatalog-test.js @@ -0,0 +1,71 @@ +/** + * Copyright 2020, 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 expect from 'expect'; + +import { testEpic } from './epicTestUtils'; +import axios from "../../libs/ajax"; +import MockAdapter from "axios-mock-adapter"; + +import { + TRIGGER_RELOAD, + deleteMap, + saveMap +} from '../../actions/mapcatalog'; + +import { + SHOW_NOTIFICATION +} from '../../actions/notifications'; + +import { + deleteMapEpic, + saveMapEpic +} from '../mapcatalog'; + +const testMap = { + id: 10, + metadata: { + name: 'testmap', + description: 'testmap' + } +}; + +describe('mapcatalog epics', () => { + let mockAxios; + + beforeEach(() => { + mockAxios = new MockAdapter(axios); + }); + + afterEach(() => { + mockAxios.restore(); + }); + + it('deleteMapEpic', (done) => { + mockAxios.onDelete().reply(200, {}); + mockAxios.onGet().reply(200, { + AttributeList: {} + }); + testEpic(deleteMapEpic, 2, deleteMap(testMap), actions => { + expect(actions.length).toBe(2); + expect(actions[0].type).toBe(SHOW_NOTIFICATION); + expect(actions[0].level).toBe('success'); + expect(actions[1].type).toBe(TRIGGER_RELOAD); + }, {}, done); + }); + + it('saveMapEpic', (done) => { + mockAxios.onPut().reply(200, {}); + testEpic(saveMapEpic, 2, saveMap(testMap), actions => { + expect(actions.length).toBe(2); + expect(actions[0].type).toBe(SHOW_NOTIFICATION); + expect(actions[0].level).toBe('success'); + expect(actions[1].type).toBe(TRIGGER_RELOAD); + }, {}, done); + }); +}); diff --git a/web/client/pluginsConfig.json b/web/client/pluginsConfig.json index 4a1be4e76f..66737714b7 100644 --- a/web/client/pluginsConfig.json +++ b/web/client/pluginsConfig.json @@ -280,6 +280,15 @@ "BurgerMenu" ] }, + { + "name": "MapCatalog", + "glyph": "1-map", + "title": "plugins.MapCatalog.title", + "description": "plugins.MapCatalog.description", + "dependencies": [ + "BurgerMenu" + ] + }, { "name": "MapImport", "glyph": "upload", diff --git a/web/client/reducers/__tests__/mapcatalog-test.js b/web/client/reducers/__tests__/mapcatalog-test.js new file mode 100644 index 0000000000..e796a8176c --- /dev/null +++ b/web/client/reducers/__tests__/mapcatalog-test.js @@ -0,0 +1,26 @@ +/* + * Copyright 2019, 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 expect from 'expect'; + +import { + triggerReload +} from '../../actions/mapcatalog'; + +import mapcatalog from '../mapcatalog'; + +describe('mapcatalog reducer', () => { + it('triggerReload', () => { + const state = mapcatalog(undefined, triggerReload()); + expect(state).toExist(); + expect(state.triggerReloadValue).toBe(true); + const newState = mapcatalog(state, triggerReload()); + expect(newState).toExist(); + expect(newState.triggerReloadValue).toBe(false); + }); +}); diff --git a/web/client/reducers/mapcatalog.js b/web/client/reducers/mapcatalog.js index 49a81e7580..6638b14119 100644 --- a/web/client/reducers/mapcatalog.js +++ b/web/client/reducers/mapcatalog.js @@ -12,6 +12,12 @@ import { import { set } from '../utils/ImmutableUtils'; + +/** + * Manages the state of the MapCatalog plugin + * @prop {boolean} triggerReloadValue triggers the reload of maps with current search text + * @memberof reducers + */ export default (state = {}, action) => { switch (action.type) { case TRIGGER_RELOAD: { diff --git a/web/client/selectors/__tests__/mapcatalog-test.js b/web/client/selectors/__tests__/mapcatalog-test.js new file mode 100644 index 0000000000..9038904bf3 --- /dev/null +++ b/web/client/selectors/__tests__/mapcatalog-test.js @@ -0,0 +1,24 @@ +/** + * Copyright 2020, 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 expect from 'expect'; +import { + triggerReloadValueSelector +} from '../mapcatalog'; + +const testState = { + mapcatalog: { + triggerReloadValue: true + } +}; + +describe('mapcatalog selectors', () => { + it('triggerReloadValueSelector', () => { + expect(triggerReloadValueSelector(testState)).toBe(true); + }); +}); diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 4f88978ea0..4d9a208a32 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -2219,6 +2219,10 @@ "description": "Login Tool", "title": "Login" }, + "MapCatalog": { + "description": "Ermöglicht das Durchsuchen, Bearbeiten, Löschen und Laden von Karten, die auf dem Server verfügbar sind", + "title": "Kartenkatalog" + }, "MapExport": { "description": "Ermöglicht das Exportieren einer Karte in eine Datei", "title": "Kartenexport" diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 6a7c6d2193..16ee79f360 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -2222,6 +2222,10 @@ "description": "Login tool", "title": "Login" }, + "MapCatalog": { + "description": "Allows browsing, editing, deleting and loading of maps that are available on the server", + "title": "Map Catalog" + }, "MapExport": { "description": "Allows to export map in a file", "title": "Map Export" diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index af719cc7b0..557900c0df 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -2218,6 +2218,10 @@ "description": "Login Tool", "title": "Login" }, + "MapCatalog": { + "description": "Permite navegar, editar, borrar y cargar mapas que están disponibles en el servidor", + "title": "Catálogo de mapas" + }, "MapExport": { "description": "Permite exportar el mapa en un archivo", "title": "Exportación de mapas" diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index a5bc5efeb4..c194b98f5c 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -2218,6 +2218,10 @@ "description": "Login Tool", "title": "Login" }, + "MapCatalog": { + "description": "Permet la navigation, l'édition, la suppression et le chargement des cartes disponibles sur le serveur", + "title": "Catalogue de cartes" + }, "MapExport": { "description": "Permet d'exporter la carte dans un fichier", "title": "Exportation de carte" diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 79dd8a2846..1db1e51840 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -2219,6 +2219,10 @@ "description": "Login Tool", "title": "Login" }, + "MapCatalog": { + "description": "Consente la navigazione, la modifica, l'eliminazione e il caricamento di mappe disponibili sul server", + "title": "Catalogo mappe" + }, "MapExport": { "description": "Permette di esportare la mappa in un file", "title": "Esporta Mappa"