Skip to content

Commit

Permalink
Add empty map to map selector (#2813)
Browse files Browse the repository at this point in the history
  • Loading branch information
offtherailz authored Apr 11, 2018
1 parent 37367ae commit e82ea72
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 79 deletions.
6 changes: 4 additions & 2 deletions web/client/components/maps/MapCatalog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ const SideGrid = compose(
})

)(require('../misc/cardgrids/SideGrid'));
module.exports = ({ setSearchText = () => { }, selected, onSelected, loading, searchText, items = [], total, title = <Message msgId={"maps.title"} /> }) => {
module.exports = ({ setSearchText = () => { }, selected, skip = 0, onSelected, loading, searchText, items = [], total, title = <Message msgId={"maps.title"} /> }) => {
return (<BorderLayout
className="map-catalog"
header={<MapCatalogForm title={title} searchText={searchText} onSearchTextChange={setSearchText} />}
footer={<div className="catalog-footer">
<span>{loading ? <LoadingSpinner /> : null}</span>
{!isNil(total) ? <span className="res-info"><Message msgId="catalog.pageInfoInfinite" msgParams={{ loaded: items.length, total }} /></span> : null}
{!isNil(total) ?
<span className="res-info"><Message msgId="catalog.pageInfoInfinite"
msgParams={{ loaded: items.length - skip, total }} /></span> : null}
</div>}>
<SideGrid
items={items.map(i =>
Expand Down
19 changes: 17 additions & 2 deletions web/client/components/maps/__tests__/MapCatalog-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const rxjsConfig = require('recompose/rxjsObservableConfig').default;
setObservableConfig(rxjsConfig);
const expect = require('expect');
const MapCatalog = require('../MapCatalog');
const enhancer = require('../enhancers/mapCatalog');
const mapCatalog = require('../enhancers/mapCatalog');
const mapCatalogWithEmptymap = require('../enhancers/mapCatalogWithEmptyMap');
const GeoStoreDAO = require('../../../api/GeoStoreDAO');

describe('MapCatalog component', () => {
Expand Down Expand Up @@ -49,10 +50,24 @@ describe('MapCatalog component', () => {
});

it('mapCatalog enhancer', (done) => {
const Sink = enhancer(createSink( props => {
const Sink = mapCatalog(createSink( props => {
if (props.items && props.items.length > 0) {
expect(props).toExist();
const item = props.items[0];
expect(props.skip).toNotExist();
expect(item).toExist();
expect(item.title).toExist();
done();
}
}));
ReactDOM.render(<Sink />, document.getElementById("container"));
});
it('mapCatalogWithEmptyMap enhancer', (done) => {
const Sink = mapCatalogWithEmptymap(createSink(props => {
if (props.items && props.items.length > 0) {
expect(props).toExist();
const item = props.items[0];
expect(props.skip).toBe(1);
expect(item).toExist();
expect(item.title).toExist();
done();
Expand Down
123 changes: 123 additions & 0 deletions web/client/components/maps/enhancers/enhancers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2018, 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.
*/
const React = require('react');
const Rx = require('rxjs');
const { compose, withProps, mapPropsStream } = require('recompose');
const { castArray } = require('lodash');
const GeoStoreApi = require('../../../api/GeoStoreDAO');
const Message = require('../../I18N/Message');

const Icon = require('../../misc/FitIcon');

const withControllableState = require('../../misc/enhancers/withControllableState');
const withVirtualScroll = require('../../misc/enhancers/infiniteScroll/withInfiniteScroll');

const defaultPreview = <Icon glyph="geoserver" padding={20} />;

/*
* converts record item into a item for SideGrid
*/
const resToProps = ({ results, totalCount }) => ({
items: (results !== "" && castArray(results) || []).map((r = {}) => ({
id: r.id,
title: r.name,
description: r.description,
preview: r.thumbnail ? <img src={decodeURIComponent(r.thumbnail)} /> : defaultPreview,
map: r
})),
total: totalCount
});
const PAGE_SIZE = 10;
/*
* retrieves data from a catalog service and converts to props
*/
const loadPage = ({ text, options = {} }, page = 0) => Rx.Observable
.fromPromise(GeoStoreApi.getResourcesByCategory("MAP", text, {
params: {
start: page * PAGE_SIZE,
limit: PAGE_SIZE
},
options
}))
.map(resToProps)
.catch(e => Rx.Observable.of({
error: e,
items: [],
total: 0
}));
const scrollSpyOptions = { querySelector: ".ms2-border-layout-body", pageSize: PAGE_SIZE };

/**
* transforms loadPage to add the empty map item on top
* @param {function} fn the original loadPage function
*/
const emptyMap = fn => (opts, page) => {
if (page === 0 && opts && !opts.text) {
return fn(opts, page).map(({ items, total, ...props}) => ({
...props,
total,
items: [{
id: "EMPTY_MAP",
title: <Message msgId="widgets.selectMap.emptyMap.title" />,
description: <Message msgId="widgets.selectMap.emptyMap.description" />,
preview: defaultPreview,
map: {
id: "new.json"
}
}, ...items]
}));
}
return fn(opts, page);
};

/**
* Transforms withVirtualScroll to add an empty map on top.
* @param {object} options
*/
const withEmptyMapVirtualScrollProperties = ({ loadPage: lp, scrollSpyOptions: sso, ...options}) => ({
...options,
scrollSpyOptions: {
skip: 1,
...sso
},
loadPage: emptyMap(lp),
hasMore: ({total, items}) => {
if (items && items.length >= 1 && items[0].id === "EMPTY_MAP") {
return total > (items.length + 1);
}
return total > items.length;
}
});
/**
* Enhances the map catalog with virtual scroll including the support for empty map initial entry.
* Provides skip property to allow widget's footer correct count and modifies properly the loadPage properties to use virtual scroll
* To remove the the empty map you should use simply withVirtualScroll instead of `withEmptyVirtualScrollProperties` transformation and withProps enhancer
*/
const withEmptyMapVirtualScroll = compose(
withVirtualScroll(withEmptyMapVirtualScrollProperties({ loadPage: loadPage, scrollSpyOptions, hasMore: ({ total, items = [] } = {}) => total > items.length })),
withProps(({ items }) => ({ skip: items && items[0] && items[0].id === "EMPTY_MAP" ? 1 : 0}))
);
module.exports = {
// manage local search text
withSearchTextState: withControllableState('searchText', "setSearchText", ""),
// add virtual virtual scroll running loadPage stream to get data
withVirtualScroll: withVirtualScroll(({ loadPage: loadPage, scrollSpyOptions, hasMore: ({ total, items = [] } = {}) => total > items.length })),
// same as above, but with empty map
withEmptyMapVirtualScroll,
// trigger loadFirst on text change
searchOnTextChange: mapPropsStream(props$ =>
props$.merge(props$.take(1).switchMap(({ loadFirst = () => { } }) =>
props$
.debounceTime(500)
.startWith({ searchText: "" })
.distinctUntilKeyChanged('searchText', (a, b) => a === b)
.do(({ searchText, options } = {}) => loadFirst({ text: searchText, options }))
.ignoreElements() // don't want to emit props
)))

};
72 changes: 5 additions & 67 deletions web/client/components/maps/enhancers/mapCatalog.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,8 @@
/*
* Copyright 2018, 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.
*/
const React = require('react');
const Rx = require('rxjs');
const { compose, mapPropsStream } = require('recompose');
const { castArray } = require('lodash');
const GeoStoreApi = require('../../../api/GeoStoreDAO');

const Icon = require('../../misc/FitIcon');

const withControllableState = require('../../misc/enhancers/withControllableState');
const withVirtualScroll = require('../../misc/enhancers/infiniteScroll/withInfiniteScroll');

const defaultPreview = <Icon glyph="geoserver" padding={20} />;

/*
* converts record item into a item for SideGrid
*/
const resToProps = ({ results, totalCount }) => ({
items: (results !== "" && castArray(results) || []).map((r = {}) => ({
id: r.id,
title: r.name,
description: r.description,
preview: r.thumbnail ? <img src={decodeURIComponent(r.thumbnail)} /> : defaultPreview,
map: r
})),
total: totalCount
});
const PAGE_SIZE = 10;
/*
* retrieves data from a catalog service and converts to props
*/
const loadPage = ({ text, options = {} }, page = 0) => Rx.Observable
.fromPromise(GeoStoreApi.getResourcesByCategory("MAP", text, {
params: {
start: page * PAGE_SIZE,
limit: PAGE_SIZE
},
options
}))
.map(resToProps)
.catch(e => Rx.Observable.of({
error: e,
items: [],
total: 0
}));
const scrollSpyOptions = { querySelector: ".ms2-border-layout-body", pageSize: PAGE_SIZE };


const {compose} = require('recompose');
const { withSearchTextState, withVirtualScroll, searchOnTextChange} = require('./enhancers');
module.exports = compose(
// manage local search text
withControllableState('searchText', "setSearchText", ""),
// add virtual virtual scroll running loadPage stream to get data
withVirtualScroll({ loadPage, scrollSpyOptions, hasMore: ({ total, items = [] } = {}) => total > items.length }),
mapPropsStream(props$ =>
props$.merge(props$.take(1).switchMap(({ loadFirst = () => { } }) =>
props$
.debounceTime(500)
.startWith({ searchText: "" })
.distinctUntilKeyChanged('searchText', (a, b) => a === b)
.do(({ searchText, options } = {}) => loadFirst({ text: searchText, options }))
.ignoreElements() // don't want to emit props
)))

withSearchTextState,
withVirtualScroll,
searchOnTextChange
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

const {compose} = require('recompose');
const { withSearchTextState, withEmptyMapVirtualScroll, searchOnTextChange} = require('./enhancers');
module.exports = compose(
withSearchTextState,
withEmptyMapVirtualScroll,
searchOnTextChange
);
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require('rxjs');
* @param {String} [loadingProp="loading"] property to check for loading status. If props[loadingProp] is true, the scroll events will be stopped
* @param {Number} [pageSize=10] page size. It is used to count items and guess the next page number.
* @param {Number} [offsetSize=200] offset, in pixels, before the end of page to call the scroll spy. If the user scrolls the `onLoadMore` handler will be colled when he reaches the end_of_the_page - offsetSize pixels.
* @param {Number} [skip=0] optional number of items to skip in count
* @return {HOC} An Higher Order Component that will call `onLoadMore` when you should load more elements in the context, for example, of an infinite scroll.
* @example
* const Cmp = withScrollSpy({items: "items", querySelector: "div"})(MyComponent);
Expand All @@ -34,6 +35,7 @@ module.exports = ({
querySelector,
closest = false,
loadingProp = "loading",
skip = 0,
pageSize = 10,
offsetSize = 200
} = {}) => (Component) =>
Expand Down Expand Up @@ -93,7 +95,7 @@ module.exports = ({
: true)
&& this.props.hasMore(this.props)) {
this.props.onLoadMore(dataProp
? Math.ceil(this.props[dataProp].length / pageSize)
? Math.ceil((this.props[dataProp].length - skip) / pageSize)
: null);
}
}
Expand Down
10 changes: 9 additions & 1 deletion web/client/components/widgets/builder/wizard/map/MapOptions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@
*/
const React = require('react');
const StepHeader = require('../../../../misc/wizard/StepHeader');
const emptyState = require('../../../../misc/enhancers/emptyState');
const Message = require('../../../../I18N/Message');
const TOC = require('./TOC');
const TOC = emptyState(
({ map = {} } = {}) => !map.layers || (map.layers || []).filter(l => l.group !== 'background').length === 0,
() => ({
glyph: "1-layer",
title: <Message msgId="widgets.selectMap.TOC.noLayerTitle" />,
description: <Message msgId="widgets.selectMap.TOC.noLayerDescription" />
})
)(require('./TOC'));
const nodeEditor = require('./enhancers/nodeEditor');
const Editor = nodeEditor(require('./NodeEditor'));

Expand Down
15 changes: 9 additions & 6 deletions web/client/components/widgets/builder/wizard/map/MapSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const React = require('react');

require('rxjs');
const GeoStoreDAO = require('../../../../../api/GeoStoreDAO');
const axios = require('../../../../../libs/ajax');
const ConfigUtils = require('../../../../../utils/ConfigUtils');

const BorderLayout = require('../../../../layout/BorderLayout');
Expand All @@ -18,20 +19,22 @@ const Toolbar = require('../../../../misc/toolbar/Toolbar');
const BuilderHeader = require('../../BuilderHeader');

const { compose, withState, mapPropsStream, withHandlers } = require('recompose');
const mcEnhancer = require('../../../../maps/enhancers/mapCatalog');
const mcEnhancer = require('../../../../maps/enhancers/mapCatalogWithEmptyMap');
const MapCatalog = mcEnhancer(require('../../../../maps/MapCatalog'));
/**
* Builder page that allows layer's selection
*/
module.exports = compose(
withState('selected', "setSelected", null),
withHandlers({
onMapChoice: ({ onMapSelected = () => { } } = {}) => map => GeoStoreDAO
.getData(map.id)
.then((config => {
let mapState = !config.version ? ConfigUtils.convertFromLegacy(config) : ConfigUtils.normalizeConfig(config.map);
onMapChoice: ({ onMapSelected = () => { } } = {}) => map =>
(typeof map.id === 'string'
? axios.get(map.id).then(response => response.data)
: GeoStoreDAO.getData(map.id)
).then((config => {
let mapState = (!config.version && typeof map.id !== 'string') ? ConfigUtils.convertFromLegacy(config) : ConfigUtils.normalizeConfig(config.map);
return {
...mapState,
...(mapState && mapState.map || {}),
layers: mapState.layers.map(l => {
if (l.group === "background" && (l.type === "ol" || l.type === "OpenLayers.Layer")) {
l.type = "empty";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,22 @@ describe('MapOptions component', () => {
const container = document.getElementById('container');
expect(container.querySelector('.mapstore-step-title')).toExist();
// renders the TOC
expect(container.querySelector('#mapstore-layers')).toNotExist();
expect(container.querySelector('.empty-state-container')).toExist();
// not the Editor
expect(container.querySelector('.ms-row-tab')).toNotExist();
});
it('MapOptions rendering layers', () => {
ReactDOM.render(<MapOptions
map={{ groups: [{ id: 'GGG' }], layers: [{ id: "LAYER", group: "GGG", options: {} }] }}
nodes={[{ id: 'GGG', nodes: [{ id: "LAYER", group: "GGG", options: {} }] }]}
layers={[{ id: "LAYER", group: "GGG", options: {} }]}
/>, document.getElementById("container"));
const container = document.getElementById('container');
expect(container.querySelector('.mapstore-step-title')).toExist();
// renders the TOC
expect(container.querySelector('#mapstore-layers')).toExist();
expect(container.querySelector('.empty-state-container')).toNotExist();
// not the Editor
expect(container.querySelector('.ms-row-tab')).toNotExist();
});
Expand Down
10 changes: 10 additions & 0 deletions web/client/translations/data.de-DE
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,16 @@
"selectChartType": {
"title": "Wählen Sie den Diagrammtyp aus"
},
"selectMap": {
"TOC": {
"noLayerTitle": "Keine Ebenen",
"noLayerDescription": "Es gibt keine Ebenen in der Karte. Wenn Sie eine Ebene aus dem Katalog hinzufügen möchten, klicken Sie auf die Schaltfläche '+' in der Symbolleiste oben"
},
"emptyMap": {
"title": "Leere Karte",
"description": "Start von einer leeren Karte"
}
},
"title": "Titel",
"description": "Beschreibung",
"errors": {
Expand Down
10 changes: 10 additions & 0 deletions web/client/translations/data.en-US
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,16 @@
"selectChartType": {
"title": "Select the Chart type"
},
"selectMap": {
"TOC": {
"noLayerTitle": "No Layers",
"noLayerDescription": "There are no layers in the map. If you want to add a layer from the catalog click on the '+' button in the toolbar on top"
},
"emptyMap": {
"title": "Empty Map",
"description": "Start from an empty map"
}
},
"title": "Title",
"description": "Description",
"errors": {
Expand Down
Loading

0 comments on commit e82ea72

Please sign in to comment.