Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
28 changes: 28 additions & 0 deletions web/client/actions/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const TOGGLE_COLLAPSE_ALL = "WIDGET:TOGGLE_COLLAPSE_ALL";
export const TOGGLE_MAXIMIZE = "WIDGET:TOGGLE_MAXIMIZE";
export const TOGGLE_TRAY = "WIDGET:TOGGLE_TRAY";

export const REPLACE_LAYOUT_VIEW = "WIDGET:REPLACE_LAYOUT_VIEW";
export const SET_SELECTED_LAYOUT_VIEW_ID = "WIDGET:SET_SELECTED_LAYOUT_VIEW_ID";

/**
* Intent to create a new Widgets
* @param {object} widget The widget template to start with
Expand Down Expand Up @@ -320,3 +323,28 @@ export const toggleMaximize = (widget, target = DEFAULT_TARGET) => ({
* @param {boolean} value true the tray is present, false if it is not present
*/
export const toggleTray = value => ({ type: TOGGLE_TRAY, value});


/**
* Add a layouts in the provided target
* @param {object} layouts The layouts to replace
* @param {string} [target=floating] the target container of the layouts
* @return {object} action with type `WIDGETS:REPLACE_LAYOUT_VIEW`, the layouts and the target
*/
export const replaceLayoutView = (layouts, target = DEFAULT_TARGET) => ({
type: REPLACE_LAYOUT_VIEW,
target,
layouts
});

/**
* Set the layouts view ID that is selected
* @param {object} viewId The layout view ID
* @param {string} [target=floating] the target container of the layouts
* @return {object} action with type `WIDGETS:SET_SELECTED_LAYOUT_VIEW_ID`, the layout view ID and the target
*/
export const setSelectedLayoutViewId = (viewId, target = DEFAULT_TARGET) => ({
type: SET_SELECTED_LAYOUT_VIEW_ID,
target,
viewId
});
67 changes: 67 additions & 0 deletions web/client/components/dashboard/ConfigureView.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useEffect, useState } from 'react';
import Dialog from '../misc/Dialog';
import { Button, ControlLabel, FormControl, FormGroup, Glyphicon } from 'react-bootstrap';
import Message from '../I18N/Message';
import ColorSelector from '../style/ColorSelector';

const ConfigureView = ({ active, onToggle, name, color, onSave }) => {
const [setting, setSetting] = useState({ name: null, color: null });
useEffect(() => {
setSetting({ name, color });
}, [name, color]);
return (
<div>
{active && (
<Dialog
id="mapstore-export-data-results"
draggable={false}
modal>
<span role="header">
<span className="modal-title about-panel-title"><Message msgId="dashboard.view.configure"/></span>
<button onClick={() => onToggle()} className="settings-panel-close close">
<Glyphicon glyph="1-close"/>
</button>
</span>
<div role="body" className="_padding-lg">
<FormGroup className="_padding-b-sm">
<ControlLabel><Message msgId="dashboard.view.name" /></ControlLabel>
<FormControl
type="text"
value={setting.name}
onChange={event => {
const { value } = event.target || {};
setSetting(prev => ({ ...prev, name: value }));
}}
/>
</FormGroup>
<FormGroup className="ms-flex-box _flex _flex-gap-sm _flex-center-v">
<ControlLabel><Message msgId="dashboard.view.color" /></ControlLabel>
<div className="dashboard-color-picker">
<ColorSelector
format="rgb"
color={setting.color}
onChangeColor={(colorVal) => colorVal && setSetting(prev =>({
...prev,
color: colorVal
}))}
/>
</div>
</FormGroup>
</div>
<div role="footer">
<Button
bsStyle="default"
onClick={() => onToggle()}
>Cancel</Button>
<Button
bsStyle="primary"
onClick={() => onSave(setting)}
>Save</Button>
</div>
</Dialog>
)}
</div>
);
};

export default ConfigureView;
63 changes: 59 additions & 4 deletions web/client/components/dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
* 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 { compose, defaultProps, pure, withProps } from 'recompose';
import { compose, defaultProps, pure, withProps, withStateHandlers, withHandlers, lifecycle } from 'recompose';

import Message from '../I18N/Message';
import { widthProvider } from '../layout/enhancers/gridLayout';
import emptyState from '../misc/enhancers/emptyState';
import withSelection from '../widgets/view/enhancers/withSelection';
import WidgetsView from '../widgets/view/WidgetsView';
import WidgetViewWrapper from './WidgetViewWrapper';
import uuidv1 from 'uuid/v1';

const WIDGET_MOBILE_RIGHT_SPACE = 18;

Expand Down Expand Up @@ -69,5 +69,60 @@ export default compose(
defaultProps({
isWidgetSelectable: () => true
}),
withStateHandlers(
// Initial state to set the Configure view for the selected layout
() => ({ active: false }),
{ setActive: () => (active) => ({ active }) }
),
// Intercept onLayoutChange to inspect and modify data
withHandlers({
onLayoutChange: props => (layout, allLayouts) => {
const currentLayouts = Array.isArray(props.layouts) ? props.layouts : [props.layouts];

// This is updating an existing layout - allLayouts contains breakpoint data
const updatedLayouts = currentLayouts.map(l => {
if (l?.id && l?.id === props.selectedLayoutId) {
// allLayouts contains the grid data for all breakpoints (md, xxs, etc.)
// Merge this with the existing tabbed layout properties
return {
...l,
...allLayouts,
id: l.id,
name: l.name,
color: l.color
};
}
return l;
});

// Call the original onLayoutChange if it exists
// Pass the updated tabbed layouts
if (props.onLayoutChange) {
props.onLayoutChange(layout, updatedLayouts);
}

return { layout, allLayouts: updatedLayouts };
}
}),
lifecycle({
componentDidMount() {
const { layouts, widgets, onLayoutViewReplace = () => {}, onWidgetsReplace = () => {} } = this.props;
// DEFAULT value for the tabbed dashboard (Handling the first load in new dashboard)
const _layout = [{
id: uuidv1(),
name: "Main view",
color: null
}];
if (!layouts) {
// Replace the layout view with default value
onLayoutViewReplace(_layout);
}
if (widgets) {
// Check if the widgets have layoutId or not and assign if missing
// Replace the existing widgets
onWidgetsReplace(widgets.map(w => w.layoutId ? w : { ...w, layoutId: _layout?.[0]?.id }));
}
}
}),
withSelection
)(WidgetsView);
)(WidgetViewWrapper);
161 changes: 161 additions & 0 deletions web/client/components/dashboard/ViewSwitcher.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React, { useRef, useState } from 'react';
import { Button, Dropdown, Glyphicon, MenuItem } from 'react-bootstrap';
import Message from '../I18N/Message';
import withConfirm from '../misc/withConfirm';
import FlexBox from '../layout/FlexBox';
import useCheckScroll from './hooks/useCheckScroll';

const WithConfirmButton = withConfirm(MenuItem);

const View = ({ handleSelect, isSelected, id, color, name, onRemove, onMove, canDelete, onConfigure, canMoveLeft, canMoveRight, canEdit }) => {
const [position, setPosition] = useState({ left: 0, bottom: 0, right: 0 });
const toggleBtnRef = useRef(null);

const handleToggleClick = () => {
handleSelect(id);
if (toggleBtnRef.current) {
const rect = toggleBtnRef.current.getBoundingClientRect();
// align to right if there is space, otherwise align to left
if (window.innerWidth - rect.right < 200) {
setPosition({
right: window.innerWidth - rect.right - 4,
bottom: window.innerHeight - rect.top + 10,
left: 'auto'
});
} else {
setPosition({
left: rect.left - 4,
bottom: window.innerHeight - rect.top + 10,
right: 'auto'
});
}
}
};

return (
<Dropdown
dropup
bsStyle="default"
id={id}
className={`${isSelected ? "is-selected " : ""}ms-layout-view`}
style={{ borderBottom: `2px solid ${color}` }}
>
<Button onClick={() => handleSelect(id)} bsStyle="default">
{name}
</Button>
{canEdit && (
<Dropdown.Toggle onClick={handleToggleClick} noCaret>
<div ref={toggleBtnRef}>
<Glyphicon glyph="option-vertical" />
</div>
</Dropdown.Toggle>
)}
<Dropdown.Menu className="_fixed" style={{ ...position }}>
<WithConfirmButton
confirmTitle={<Message msgId="dashboard.view.removeConfirmTitle" />}
confirmContent={<Message msgId="dashboard.view.removeConfirmContent" />}
onClick={() => onRemove(id)}
disabled={!canDelete}
>
<Glyphicon glyph="trash" />
<Message msgId="dashboard.view.delete" />
</WithConfirmButton>
<MenuItem
onClick={() => {
onConfigure();
}}
>
<Glyphicon glyph="cog" />
<Message msgId="dashboard.view.configure" />
</MenuItem>
<MenuItem
onClick={() => {
onMove(id, 'right');
}}
disabled={!canMoveRight}
>
<Glyphicon glyph="arrow-right" />
<Message msgId="dashboard.view.moveRight" />
</MenuItem>
<MenuItem
onClick={() => {
onMove(id, 'left');
}}
disabled={!canMoveLeft}
>
<Glyphicon glyph="arrow-left" />
<Message msgId="dashboard.view.moveLeft" />
</MenuItem>
</Dropdown.Menu>
</Dropdown>);
};

const ViewSwitcher = ({ layouts = [], selectedLayoutId, onSelect, onAdd, onRemove, onMove, onConfigure, canEdit }) => {
const handleSelect = (id) => {
onSelect?.(id);
};

const [scrollRef, showButtons, isLeftDisabled, isRightDisabled, scroll] = useCheckScroll({ data: layouts });

return (
<FlexBox gap="xs" centerChildrenVertically className="view-switcher-container">
{canEdit && (
<Button
onClick={onAdd}
className="square-button-md _margin-l-xs _margin-tb-xs"
title="Add a layout view to the dashboard"
>
<Glyphicon glyph="plus" />
</Button>
)}

{/* Layouts Tabs */}
<FlexBox centerChildrenVertically ref={scrollRef} className="_overflow-auto _fill view-switcher-tabs">
{layouts.map((layout, idx) => {
const id = layout.id || idx + 1;
return (
<View
key={id}
id={id}
name={layout.name}
color={layout.color}
handleSelect={handleSelect}
isSelected={selectedLayoutId === id}
onRemove={onRemove}
onMove={onMove}
onConfigure={onConfigure}
canDelete={layouts.length > 1}
canMoveRight={idx !== layouts.length - 1}
canMoveLeft={idx !== 0}
canEdit={canEdit}
/>
);
})}
</FlexBox>
{showButtons && (
<FlexBox gap="xs" centerChildrenVertically className="view-scroll-buttons">
<Button
className="square-button-md"
bsStyle="primary"
onClick={() => scroll("left")}
disabled={isLeftDisabled}
title="Scroll left"
>
<Glyphicon glyph="chevron-left" />
</Button>
<Button
className="square-button-md"
bsStyle="primary"
onClick={() => scroll("right")}
disabled={isRightDisabled}
title="Scroll right"
>
<Glyphicon glyph="chevron-right" />
</Button>
</FlexBox>
)}
</FlexBox>
);
};

export default ViewSwitcher;
Loading
Loading