Skip to content

Commit

Permalink
Feat: Page copying and delete confirmation (elastic#172)
Browse files Browse the repository at this point in the history
* fix: globally set the confirm modal font color

* chore: move getId into its own module

* feat: add page duplication

action, reducer, and the UI controls

* fix: middleware to update elements

when duplicating a page, all the elements on that page need to be loaded

* feat: add confirmation to page delete

* chore: bump react dependencies
  • Loading branch information
w33ble authored Sep 15, 2017
1 parent 7658cc3 commit 9481db2
Show file tree
Hide file tree
Showing 16 changed files with 143 additions and 45 deletions.
26 changes: 15 additions & 11 deletions public/components/confirm_modal/confirm_modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ConfirmModal = (props) => {
onCancel,
confirmButtonText,
cancelButtonText,
className,
} = props;

const confirm = (ev) => {
Expand All @@ -28,17 +29,19 @@ export const ConfirmModal = (props) => {
if (!isOpen) return null;

return (
<KuiModalOverlay>
<KuiConfirmModal
message={message}
title={title}
onConfirm={confirm}
onCancel={cancel}
cancelButtonText={cancelButtonText || 'Cancel'}
confirmButtonText={confirmButtonText || 'Confirm'}
defaultFocusedButton="confirm"
/>
</KuiModalOverlay>
<div className={`canvas__confirm_modal ${className || ''}`}>
<KuiModalOverlay>
<KuiConfirmModal
message={message}
title={title}
onConfirm={confirm}
onCancel={cancel}
cancelButtonText={cancelButtonText || 'Cancel'}
confirmButtonText={confirmButtonText || 'Confirm'}
defaultFocusedButton="confirm"
/>
</KuiModalOverlay>
</div>
);
};

Expand All @@ -50,4 +53,5 @@ ConfirmModal.propTypes = {
onCancel: PropTypes.func.isRequired,
cancelButtonText: PropTypes.string,
confirmButtonText: PropTypes.string,
className: PropTypes.string,
};
4 changes: 2 additions & 2 deletions public/components/function_form/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import uuid from 'uuid/v4';
import { FunctionForm as Component } from './function_form';
import { findExpressionType } from '../../lib/find_expression_type';
import { getId } from '../../lib/get_id.js';
import { createAsset } from '../../state/actions/assets';
import {
fetchContext,
Expand Down Expand Up @@ -48,7 +48,7 @@ const mapDispatchToProps = (dispatch, { expressionIndex }) => ({
},
onAssetAdd: (type, content) => {
// make the ID here and pass it into the action
const assetId = `asset-${uuid()}`;
const assetId = getId('asset');
dispatch(createAsset(type, content, assetId));

// then the id, so the caller knows the id that will be created
Expand Down
6 changes: 4 additions & 2 deletions public/components/page_manager/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { compose, withState } from 'recompose';
import { PageManager as Component } from './page_manager';
import { addPage, loadPage, movePage, removePage } from '../../state/actions/pages';
import { addPage, loadPage, movePage, removePage, duplicatePage } from '../../state/actions/pages';
import { getSelectedPage, getPages } from '../../state/selectors/workpad';

const mapStateToProps = (state) => ({
Expand All @@ -13,10 +13,12 @@ const mapDispatchToProps = (dispatch) => ({
addPage: () => dispatch(addPage()),
loadPage: (id) => dispatch(loadPage(id)),
movePage: (id, position) => dispatch(movePage(id, position)),
duplicatePage: (id) => dispatch(duplicatePage(id)),
removePage: (id) => dispatch(removePage(id)),
});

export const PageManager = compose(
connect(mapStateToProps, mapDispatchToProps),
withState('withControls', 'showControls', false)
withState('withControls', 'showControls', false),
withState('deleteId', 'setDeleteId', null),
)(Component);
18 changes: 18 additions & 0 deletions public/components/page_manager/page_controls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';

export const PageControls = ({ pageId, movePage, duplicatePage, removePage }) => (
<div className="canvas__page-manager--page-controls">
<span className="fa fa-angle-double-left move-left" onClick={() => movePage(pageId, -1)} />
<span className="fa fa-trash delete" onClick={() => removePage(pageId)} />
<span className="fa fa-files-o duplicate" onClick={() => duplicatePage(pageId)} />
<span className="fa fa-angle-double-right move-right" onClick={() => movePage(pageId, 1)} />
</div>
);

PageControls.propTypes = {
pageId: PropTypes.string.isRequired,
movePage: PropTypes.func.isRequired,
duplicatePage: PropTypes.func.isRequired,
removePage: PropTypes.func.isRequired,
};
38 changes: 32 additions & 6 deletions public/components/page_manager/page_manager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-bootstrap';
import { PageControls } from './page_controls';
import { ConfirmModal } from '../confirm_modal';
import './page_manager.less';

export const PageManager = (props) => {
Expand All @@ -11,11 +13,23 @@ export const PageManager = (props) => {
loadPage,
addPage,
movePage,
duplicatePage,
removePage,
withControls,
showControls,
deleteId,
setDeleteId,
} = props;

const confirmDelete = (pageId) => setDeleteId(pageId);
const resetDelete = () => setDeleteId(null);
const doDelete = () => {
resetDelete();
removePage(deleteId);
};

const pageName = page => page.id.split('-')[1].substring(0, 6);

return (
<div className="canvas__page-manager">
<div className="canvas__page-manager--pages">
Expand All @@ -30,14 +44,15 @@ export const PageManager = (props) => {
className={`body ${page.id === selectedPage ? 'selected' : ''}`}
onClick={() => loadPage(page.id)}
>
<div className="title">Page<br />{page.id.split('-')[1].substring(0, 6)}</div>
<div className="title">Page<br />{pageName(page)}</div>
</div>
{(withControls === page.id) && (
<div className="canvas__page-manager--page-controls">
<span className="fa fa-angle-double-left move-left" onClick={() => movePage(page.id, -1)} />
<span className="fa fa-trash delete" onClick={() => removePage(page.id)} />
<span className="fa fa-angle-double-right move-right" onClick={() => movePage(page.id, 1)} />
</div>
<PageControls
pageId={page.id}
movePage={movePage}
duplicatePage={duplicatePage}
removePage={confirmDelete}
/>
)}
</div>
))}
Expand All @@ -46,6 +61,14 @@ export const PageManager = (props) => {
<Button bsStyle="success" onClick={addPage}>Add Page</Button>
<Button onClick={done}>Done</Button>
</div>

<ConfirmModal
isOpen={deleteId != null}
message={'Are you sure you want to remove this page?'}
confirmButtonText="Remove"
onConfirm={doDelete}
onCancel={resetDelete}
/>
</div>
);
};
Expand All @@ -56,11 +79,14 @@ PageManager.propTypes = {
loadPage: PropTypes.func.isRequired,
addPage: PropTypes.func.isRequired,
movePage: PropTypes.func.isRequired,
duplicatePage: PropTypes.func.isRequired,
removePage: PropTypes.func.isRequired,
selectedPage: PropTypes.string,
withControls: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]),
showControls: PropTypes.func,
deleteId: PropTypes.string,
setDeleteId: PropTypes.func.isRequired,
};
16 changes: 10 additions & 6 deletions public/components/page_manager/page_manager.less
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,21 @@
.canvas__page-manager--page-controls {
text-align: center;

.delete {
color: @blue;
> * {
padding: @spacingXS;
}

.delete,
.duplicate {
font-size: @textMedium;
}

.move-left {
padding-right: @spacingM;
.delete {
color: @red;
}

.move-right {
padding-left: @spacingM;
.duplicate {
color: @blue;
}
}

Expand Down
4 changes: 0 additions & 4 deletions public/components/workpad_loader/workpad_loader.less
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
@import "../../style/variables.less";

.canvas__workpad_loader {
.kuiModal {
color: @trueBlack;
}

.canvas__workpad_loader--search {
background-color: @lightestGrey;
color: @trueBlack;
Expand Down
5 changes: 5 additions & 0 deletions public/lib/get_id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import uuid from 'uuid/v4';

export function getId(type) {
return `${type}-${uuid()}`;
}
1 change: 1 addition & 0 deletions public/state/actions/pages.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createAction } from 'redux-actions';

export const addPage = createAction('addPage');
export const duplicatePage = createAction('duplicatePage');
export const loadPage = createAction('loadPage');
export const movePage = createAction('movePage', (id, position) => ({ id, position }));
export const removePage = createAction('removePage');
Expand Down
14 changes: 5 additions & 9 deletions public/state/defaults.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import uuid from 'uuid/v4';

function getID(type) {
return `${type}-${uuid()}`;
}
import { getId } from '../lib/get_id.js';

export const getDefaultElement = () => {
return {
id: getID('element'),
id: getId('element'),
position: {
top: 20,
left: 20,
Expand All @@ -17,15 +13,15 @@ export const getDefaultElement = () => {
expression: `
demodata
| pointseries y="median(cost)" x=time color="project"
| plot defaultStyle={seriesStyle points=5}
| plot defaultStyle={seriesStyle points=5}
`,
filter: null,
};
};

export const getDefaultPage = () => {
return {
id: getID('page'),
id: getId('page'),
style: {
background: '#fff',
},
Expand All @@ -37,7 +33,7 @@ export const getDefaultWorkpad = () => {
const page = getDefaultPage();
return {
name: 'Untitled Workpad',
id: getID('workpad'),
id: getId('workpad'),
width: 600,
height: 720,
page: 0,
Expand Down
10 changes: 9 additions & 1 deletion public/state/middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import lzString from 'lz-string';
import { esPersistMiddleware } from './es_persist';
import { historyMiddleware } from './history';
import { inFlight } from './in_flight';
import { workpadUpdate } from './workpad_update';
import { appReady } from './app_ready';

const storageKey = 'canvas';
Expand All @@ -28,7 +29,14 @@ const serializer = (function () {
}());

const middlewares = [
applyMiddleware(thunkMiddleware, esPersistMiddleware, historyMiddleware(window), inFlight, appReady),
applyMiddleware(
thunkMiddleware,
esPersistMiddleware,
historyMiddleware(window),
inFlight,
appReady,
workpadUpdate,
),
persistState('persistent', { key: storageKey }),
persistState('assets', {
key: `${storageKey}-assets`,
Expand Down
14 changes: 14 additions & 0 deletions public/state/middleware/workpad_update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { duplicatePage } from '../actions/pages';
import { fetchRenderable } from '../actions/elements';
import { getPages } from '../selectors/workpad';

export const workpadUpdate = ({ dispatch, getState }) => next => (action) => {
next(action);

if (action.type === duplicatePage.toString()) {
const pages = getPages(getState());
const newPage = pages[pages.length - 1];

return newPage.elements.forEach(element => dispatch(fetchRenderable(element)));
}
};
4 changes: 2 additions & 2 deletions public/state/reducers/assets.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { handleActions, combineActions } from 'redux-actions';
import { set, assign, del } from 'object-path-immutable';
import { get } from 'lodash';
import uuid from 'uuid/v4';
import { createAsset, setAssetValue, removeAsset, setAssets, resetAssets } from '../actions/assets';
import { getId } from '../../lib/get_id.js';

export default handleActions({
[createAsset]: (assetState, { payload }) => {
const asset = {
id: `asset-${uuid()}`,
id: getId('asset'),
'@created': (new Date()).toISOString(),
...payload,
};
Expand Down
20 changes: 20 additions & 0 deletions public/state/reducers/pages.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { handleActions } from 'redux-actions';
import { push, set, del, insert } from 'object-path-immutable';
import { findIndex } from 'lodash';
import { getId } from '../../lib/get_id.js';
import { getDefaultPage } from '../defaults';
import * as actions from '../actions/pages';

Expand All @@ -17,12 +18,31 @@ function addPage(workpadState, payload) {
return push(workpadState, 'pages', payload || getDefaultPage());
}

function clonePage(page) {
// TODO: would be nice if we could more reliably know which parameters need to get a unique id
// this makes a pretty big assumption about the shape of the page object
return {
...page,
id: getId('page'),
elements: page.elements.map(element => ({ ...element, id: getId('element') })),
};
}

export default handleActions({
[actions.addPage]: (workpadState, { payload }) => {
const withNewPage = addPage(workpadState, payload);
return setPageIndex(withNewPage, withNewPage.pages.length - 1);
},

[actions.duplicatePage]: (workpadState, { payload }) => {
const srcPage = workpadState.pages.find(page => page.id === payload);

// if the page id is invalid, don't change the state
if (!srcPage) return workpadState;

return addPage(workpadState, clonePage(srcPage));
},

[actions.nextPage]: (workpadState) => {
return setPageIndex(workpadState, workpadState.page + 1);
},
Expand Down
4 changes: 4 additions & 0 deletions public/style/main.less
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@
linear-gradient(-45deg, transparent 75%, #ddd 75%);
background-size: 8px 8px,
}

.canvas__confirm_modal {
color: @trueBlack;
}
}
Loading

0 comments on commit 9481db2

Please sign in to comment.