Skip to content

Commit

Permalink
Fix #2623. Implemented table widget (#2635)
Browse files Browse the repository at this point in the history
 - Externalized VirtualScroll functionalities to be reused
 - implemented wfsTable enhancer to support auto-data
   fetch/update with virtual scroll
 - Added to RequestBuilder sortBy and propertyName support
 - Add wfs to observable to reuse streams
 - Provided a getLayerJSONFeature (to extend) for a more rational usage
   of parameters ( old requests had to manage
   filterObj was containing sort and pagination options)
 - Set widgetContainer to be traggable only by header
   ( the cursor now changes where the widget is draggable)
 - Add sortable and defaultWidth options to FeatureGrid editor enhancer
 - Add support for columns resize, memorization and reset
 - tabular view of attribute selection
  • Loading branch information
offtherailz authored Feb 27, 2018
1 parent 3dc8b5a commit 62ef950
Show file tree
Hide file tree
Showing 49 changed files with 1,469 additions and 178 deletions.
74 changes: 74 additions & 0 deletions TableWidget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 Message = require('../../I18N/Message');
const loadingState = require('../../misc/enhancers/loadingState');
const FeatureGrid = loadingState(({ describeFeatureType }) => !describeFeatureType)(require('../../data/featuregrid/FeatureGrid'));
const InfoPopover = require('./InfoPopover');

const WidgetContainer = require('./WidgetContainer');
const {
Glyphicon,
ButtonToolbar,
DropdownButton,
MenuItem
} = require('react-bootstrap');

const renderHeaderLeftTopItem = ({ title, description }) => {
return title || description ? <InfoPopover placement="top" title={title} text={description} /> : null;
};


module.exports = ({
id,
title,
description,
loading,
confirmDelete = false,
toggleTableView = () => { },
toggleDeleteConfirm = () => { },
exportCSV = () => { },
onEdit = () => { },
onDelete = () => { },
pageEvents = {
moreFeatures: () => {}
},
describeFeatureType,
features,
size,
pages,
pagination = {},
virtualScroll = true
}) =>
(<WidgetContainer
id={`widget-chart-${id}`}
title={title}
topLeftItems={renderHeaderLeftTopItem({ loading, title, description, toggleTableView })}
confirmDelete={confirmDelete}
onDelete={onDelete}
toggleDeleteConfirm={toggleDeleteConfirm}
topRightItems={<ButtonToolbar>
<DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
<MenuItem onClick={() => toggleTableView()} eventKey="1"><Glyphicon glyph="features-grid" />&nbsp;<Message msgId="widgets.widget.menu.showChartData" /></MenuItem>
<MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil" />&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>
<MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash" />&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>
<MenuItem onClick={() => exportCSV({ title })} eventKey="4"><Glyphicon className="exportCSV" glyph="download" />&nbsp;<Message msgId="widgets.widget.menu.downloadData" /></MenuItem>
</DropdownButton>
</ButtonToolbar>}>
<FeatureGrid
pageEvents={pageEvents}
virtualScroll={virtualScroll}
features={features}
pages={pages}
size={size}
rowKey="id"
describeFeatureType={describeFeatureType}
pagination={pagination} />
</WidgetContainer>

);
11 changes: 11 additions & 0 deletions web/client/actions/__tests__/widgets-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
NEW,
INSERT,
UPDATE,
UPDATE_PROPERTY,
DELETE,
CHANGE_LAYOUT,
EDIT,
Expand All @@ -27,6 +28,7 @@ const {
createWidget,
insertWidget,
updateWidget,
updateWidgetProperty,
deleteWidget,
changeLayout,
editWidget,
Expand Down Expand Up @@ -84,6 +86,15 @@ describe('Test correctness of the widgets actions', () => {
expect(retval.widget).toBe(widget);
expect(retval.target).toBe(DEFAULT_TARGET);
});
it('updateWidgetProperty', () => {
const retval = updateWidgetProperty("id", "key", "value");
expect(retval).toExist();
expect(retval.type).toBe(UPDATE_PROPERTY);
expect(retval.id).toBe("id");
expect(retval.key).toBe("key");
expect(retval.value).toBe("value");
expect(retval.target).toBe(DEFAULT_TARGET);
});
it('deleteWidget', () => {
const widget = {};
const retval = deleteWidget(widget);
Expand Down
19 changes: 18 additions & 1 deletion web/client/actions/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const EDIT_NEW = "WIDGETS:EDIT_NEW";
const EDITOR_CHANGE = "WIDGETS:EDITOR_CHANGE";
const EDITOR_SETTING_CHANGE = "WIGETS:EDITOR_SETTING_CHANGE";
const UPDATE = "WIDGETS:UPDATE";
const UPDATE_PROPERTY = "WIDGETS:UPDATE_PROPERTY";
const CHANGE_LAYOUT = "WIDGETS:CHANGE_LAYOUT";
const DELETE = "WIDGETS:DELETE";
const CLEAR_WIDGETS = "WIDGETS:CLEAR_WIDGETS";
Expand Down Expand Up @@ -56,6 +57,20 @@ const updateWidget = (widget, target = DEFAULT_TARGET) => ({
target,
widget
});
/**
* Update a widget property in the provided target
* @param {string} id The widget id to update
* @param {string} key The widget property name or path to update
* @param {any} value the widget value to update
* @return {object} action with type `WIDGETS:UPDATE_PROPERTY`, the widget and the target
*/
const updateWidgetProperty = (id, key, value, target = DEFAULT_TARGET) => ({
type: UPDATE_PROPERTY,
id,
target,
key,
value
});
/**
* Deletes a widget from the passed target
* @param {object} widget The widget template to start with
Expand Down Expand Up @@ -100,7 +115,7 @@ const editWidget = (widget) => ({
* Edit new widget. Initializes the widget builder properly
* @param {object} widget The widget template
* @param {object} settings The settings for the template
* @return {object} the action of type `WIGETS:EDIT_NEW`
* @return {object} the action of type `WIGETS:EDIT_NEW`
*/
const editNewWidget = (widget, settings) => ({
type: EDIT_NEW,
Expand Down Expand Up @@ -157,6 +172,7 @@ module.exports = {
NEW,
INSERT,
UPDATE,
UPDATE_PROPERTY,
DELETE,
CLEAR_WIDGETS,
CHANGE_LAYOUT,
Expand All @@ -174,6 +190,7 @@ module.exports = {
createWidget,
insertWidget,
updateWidget,
updateWidgetProperty,
deleteWidget,
clearWidgets,
changeLayout,
Expand Down
39 changes: 39 additions & 0 deletions web/client/components/data/featuregrid/AttributeTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 ReactDataGrid = require('react-data-grid');
const Message = require('../../I18N/Message');

module.exports = ({
style = {},
titleMsg = "featuregrid.columns",
onChange = () => { },
attributes = []
} = {}) => (
<div className="bg-body data-attribute-selector" style={style}>
<h4 className="text-center"><strong><Message msgId={titleMsg} /></strong></h4>
<ReactDataGrid
rowKey="id"
columns={[{
name: '',
key: 'attribute'
}]}
rowGetter={idx => attributes[idx]}
rowsCount={attributes.length}
rowSelection={{
showCheckbox: true,
enableShiftSelect: true,
onRowsSelected: rows => onChange(rows.map(row => attributes[row.rowIdx].name), false),
onRowsDeselected: rows => onChange(rows.map(row => attributes[row.rowIdx].name), true),
selectBy: {
indexes: attributes.reduce( (acc, a, idx) => [...acc, ...(a.hide ? [] : [idx] )], [])
}
}} />
</div>
);
2 changes: 2 additions & 0 deletions web/client/components/data/featuregrid/FeatureGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class FeatureGrid extends React.PureComponent {
};
static defaultProps = {
editingAllowedRoles: ["ADMIN"],
initPlugin: () => {},
autocompleteEnabled: false,
gridComponent: AdaptiveGrid,
changes: {},
Expand All @@ -73,6 +74,7 @@ class FeatureGrid extends React.PureComponent {
constructor(props) {
super(props);
}
// TODO: externalize initPlugin
componentDidMount() {
this.props.initPlugin({virtualScroll: this.props.virtualScroll, editingAllowedRoles: this.props.editingAllowedRoles, maxStoredPages: this.props.maxStoredPages});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2017, 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.
*/
var React = require('react');
var ReactDOM = require('react-dom');
var AttributeSelector = require('../AttributeTable');
var expect = require('expect');
const spyOn = expect.spyOn;


describe('Test for AttributeTable component', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});

afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
document.body.innerHTML = '';
setTimeout(done);
});
it('render with defaults', () => {
ReactDOM.render(<AttributeSelector/>, document.getElementById("container"));
const el = document.getElementsByClassName("data-attribute-selector")[0];
expect(el).toExist();
});
it('render with attributes, checked by default', () => {
ReactDOM.render(<AttributeSelector attributes={[{label: "label", attribute: "attr"}]}/>, document.getElementById("container"));
const check = document.getElementsByTagName("input")[1];
expect(check).toExist();
expect(check.checked).toBe(true);
});
it('check hide is not selected', () => {
ReactDOM.render(<AttributeSelector attributes={[{label: "label", attribute: "attr", hide: true}]}/>, document.getElementById("container"));
const checks = document.getElementsByTagName("input");
expect(checks.length).toBe(2);
expect(checks[0].checked).toBe(false);
});
it('click event', () => {
const events = {
onChange: () => {}
};
spyOn(events, "onChange");
ReactDOM.render(<AttributeSelector onChange={events.onChange} attributes={[{label: "label", attribute: "attr", hide: true}]}/>, document.getElementById("container"));
const checks = document.getElementsByTagName("input");
expect(checks.length).toBe(2);
checks[0].click();
expect(events.onChange).toHaveBeenCalled();

});

});
12 changes: 7 additions & 5 deletions web/client/components/data/featuregrid/enhancers/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const dataStreamFactory = $props => {

const featuresToGrid = compose(
defaultProps({
sortable: true,
autocompleteEnabled: false,
initPlugin: () => {},
url: "",
Expand Down Expand Up @@ -88,7 +89,7 @@ const featuresToGrid = compose(
withPropsOnChange(
["features", "newFeatures", "changes"],
props => ({
rows: ( [...props.newFeatures, ...props.features] : props.features)
rows: (props.newFeatures ? [...props.newFeatures, ...props.features] : props.features)
.filter(props.focusOnEdit ? createNewAndEditingFilter(props.changes && Object.keys(props.changes).length > 0, props.newFeatures, props.changes) : () => true)
.map(orig => applyAllChanges(orig, props.changes)).map(result =>
({...result,
Expand All @@ -112,12 +113,13 @@ const featuresToGrid = compose(
),
withHandlers({rowGetter: props => props.virtualScroll && (i => getRowVirtual(i, props.rows, props.pages, props.size)) || (i => getRow(i, props.rows))}),
withPropsOnChange(
["describeFeatureType", "columnSettings", "tools", "actionOpts", "mode", "isFocused"],
["describeFeatureType", "columnSettings", "tools", "actionOpts", "mode", "isFocused", "sortable"],
props => ({
columns: getToolColumns(props.tools, props.rowGetter, props.describeFeatureType, props.actionOpts)
.concat(featureTypeToGridColumns(props.describeFeatureType, props.columnSettings, {
editable: props.mode === "EDIT",
sortable: !props.isFocused
sortable: props.sortable && !props.isFocused,
defaultSize: props.defaultSize
}, {
getEditor: (desc) => {
const generalProps = {
Expand Down Expand Up @@ -147,7 +149,7 @@ const featuresToGrid = compose(
})
),
withPropsOnChange(
["gridOpts", "describeFeatureType", "actionOpts", "mode", "select"],
["gridOpts", "describeFeatureType", "actionOpts", "mode", "select", "columns"],
props => {
// bind proper events and setup the colums array
// bind and get proper grid events from gridEvents object
Expand All @@ -156,7 +158,7 @@ const featuresToGrid = compose(
onRowsDeselected = () => {},
onRowsToggled = () => {},
hasTemporaryChanges = () => {},
...gridEvents} = getGridEvents(props.gridEvents, props.rowGetter, props.describeFeatureType, props.actionOpts);
...gridEvents} = getGridEvents(props.gridEvents, props.rowGetter, props.describeFeatureType, props.actionOpts, props.columns);

// setup gridOpts setting app selection events binded
let gridOpts = props.gridOpts;
Expand Down
4 changes: 3 additions & 1 deletion web/client/components/data/grid/DataGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ class DataGrid extends Grid {
const visibleRows = Math.ceil(this.canvas.clientHeight / this.props.rowHeight);
const firstRowIdx = Math.floor(this.canvas.scrollTop / this.props.rowHeight);
const lastRowIdx = firstRowIdx + visibleRows;
this.props.onGridScroll({firstRowIdx, lastRowIdx});
if (this.props.onGridScroll) {
this.props.onGridScroll({ firstRowIdx, lastRowIdx });
}
}
setCanvasListner = () => {
this.canvas = ReactDOM.findDOMNode(this).querySelector('.react-grid-Canvas');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
const {wizardHanlders} = require('../enhancers');
const {wizardHandlers} = require('../enhancers');

const React = require('react');
const ReactDOM = require('react-dom');
const ReactTestUtils = require('react-dom/test-utils');

const expect = require('expect');
const WizardContainer = wizardHanlders(require('../WizardContainer'));
const WizardContainer = wizardHandlers(require('../WizardContainer'));

describe('wizard enhancers ', () => {
beforeEach((done) => {
Expand Down
10 changes: 5 additions & 5 deletions web/client/components/misc/wizard/enhancers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


const {compose, withState, withPropsOnChange, withHandlers} = require('recompose');
const wizardHanlders = compose(
const wizardHandlers = compose(
withPropsOnChange(["step"], ({skipButtonsOnSteps = [], step, hideButtons} = {}) => {
if (skipButtonsOnSteps && skipButtonsOnSteps.indexOf(step) >= 0) {
return {hideButtons: true};
Expand All @@ -26,18 +26,18 @@ const wizardHanlders = compose(
);
module.exports = {
/**
* Apply this enhancer to the WizarContainer to make it controlled.
* It controls the step and the hideButtons paroperties
* Apply this enhancer to the WizardContainer to make it controlled.
* It controls the step and the hideButtons properties
*/
controlledWizard: compose(
withState(
"step", "setPage", 0
),
wizardHanlders
wizardHandlers
),
/**
* Use this enhancer if you want to change step and use setPage as handler
*/
wizardHanlders
wizardHandlers

};
5 changes: 5 additions & 0 deletions web/client/components/widgets/builder/WidgetTypeSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const DEFAULT_TYPES = [{
type: "text",
glyph: "sheet",
caption: <Message msgId={"widgets.types.text.caption"} />
}, {
title: <Message msgId={"widgets.types.table.title"} />,
type: "table",
glyph: "features-grid",
caption: <Message msgId={"widgets.types.table.caption"} />
}];

module.exports = ({widgetTypes = DEFAULT_TYPES, typeFilter = () => true, onSelect= () => {}}) =>
Expand Down
Loading

0 comments on commit 62ef950

Please sign in to comment.