Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add empty map to map selector #2813

Merged
merged 3 commits into from
Apr 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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