* Break apart bookmark component into a few low-level renderings

* Improve drag-and-drop

* Update dragKey proptype

* Update libraries, correct linting and tests

* Add skeleton-css dep; remove index.html, use webpack plugin

* Upgrade to webpack 2

* Switch to Jest

* Lib Updates

* Unconnect the top-level History component, allow tthe createHistoryContainer function to wire in redux action bindings

* Extend the redux-dag-history configuration class so that we can inject UI configuration

* Add clean task, make some tasks parallel
This commit is contained in:
Chris Trevino 2017-02-08 13:14:55 -08:00 коммит произвёл GitHub
Родитель 3b4be2e16c
Коммит 90e5ff223e
34 изменённых файлов: 1768 добавлений и 1034 удалений

Просмотреть файл

@ -13,7 +13,14 @@
// workaround for incomplete types
"dot-notation": "off",
// due to TypeScript
//
// TypeScript
//
// ctors with field initializers
"no-useless-constructor": "off",
"no-empty-function": "off",
"brace-style": "off",
"no-undef": "off",
"import/no-unresolved": "off",
"import/extensions": "off",

1
.gitignore поставляемый
Просмотреть файл

@ -1,5 +1,6 @@
node_modules/
lib/
dist/
storybook-static/
coverage/
.nyc_output/

Просмотреть файл

@ -1,13 +1,14 @@
.eslintrc
.babelrc
.npmignore
node_modules/
src/
test/
dist/
variations/
scripts/
storybook-static/
coverage/
webpack.conf.js
gulpfile.babel.js
.eslintrc
.babelrc
.nycrc
server.js
.npmignore

Просмотреть файл

@ -1,7 +1,12 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import store from './state/store';
import 'skeleton-css/css/normalize.css';
import 'skeleton-css/css/skeleton.css';
import './app.scss';
import store from './state/store';
import Application from './components/Application';
ReactDOM.render(<Application store={store} />, document.getElementById('root'));
const root = document.createElement('div');
document.body.appendChild(root);
ReactDOM.render(<Application store={store} />, root);

Просмотреть файл

@ -1,5 +1,4 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { save, load } from '../persister';
import { IBookmark } from '../../src/interfaces';
import '../../src/daghistory.scss';

Просмотреть файл

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<link href="https://cdn.jsdelivr.net/skeleton/2.0.4/css/skeleton.css" type="text/css" rel="stylesheet">
<title>DAG History Demo</title>
</head>
<body>
<div id="root" class="app-root"></div>
<script src="appbundle.js"></script>
</body>
</html>

Просмотреть файл

@ -1,6 +1,6 @@
import * as redux from 'redux';
import dagHistory from '@essex/redux-dag-history/lib/reducer';
import Configuration from '@essex/redux-dag-history/lib/Configuration';
import Configuration from '../../../src/state/Configuration';
import app from './app';
import history from '../../../src/state/reducers';
@ -46,11 +46,17 @@ function stateKeyGenerator(state) {
}
const DAG_HISTORY_CONFIG = new Configuration({
// Middleware Config
debug: false,
actionName: state => state.metadata.name,
actionFilter: actionType => EXCLUDED_ACTION_NAMES.indexOf(actionType) === -1,
stateEqualityPredicate,
stateKeyGenerator,
// UI Config
initialViewState: {
branchContainerExpanded: false,
},
});
export default redux.combineReducers({

Просмотреть файл

@ -4,23 +4,24 @@
"description": "A React Component for Dag-History Visualization",
"main": "lib/index.js",
"scripts": {
"webpack:server": "webpack-dev-server --host 0.0.0.0 --hot --inline --history-api-fallback --publicPath ./",
"clean": "rimraf lib/ storybook-static/",
"serve:webpack": "webpack-dev-server --host 0.0.0.0 --hot --inline --history-api-fallback",
"serve:storybook": "start-storybook -p 6006",
"build:tsc": "tsc",
"build:sass": "node-sass src/daghistory.scss --output lib",
"build:assets": "cpx 'src/**/*.scss' lib",
"build:storybook": "build-storybook",
"build": "npm-run-all --parallel 'build:*'",
"lint": "eslint '{src,test,stories}/**/*.ts'",
"tsc": "tsc",
"sass": "node-sass src/daghistory.scss --output lib",
"copysass": "cpx 'src/**/*.scss' lib",
"mocha:coverage": "nyc ./node_modules/.bin/_mocha",
"mocha": "mocha",
"test": "npm-run-all mocha:coverage lint --parallel tsc sass copysass",
"watch:tsc": "npm run tsc -- -w",
"watch:sass": "npm run sass -- --watch",
"watch:mocha": "npm run mocha -- -w",
"watch:copysass": "npm run copysass -- --watch",
"watch": "npm-run-all --parallel 'watch:*'",
"develop": "npm-run-all --parallel watch webpack:server storybook",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
"start": "npm run develop"
"unit_test": "jest",
"unit_test:coverage": "jest --coverage",
"watch:tsc": "tsc -w",
"watch:unit_test": "jest --watch",
"watch:sass": "npm run build:sass -- --watch",
"watch:copysass": "npm run build:assets -- --watch",
"verify": "npm-run-all --parallel lint unit_test:coverage",
"test": "npm-run-all clean --parallel verify build",
"start": "npm-run-all --parallel 'serve:*' 'watch:*'"
},
"author": "Chris Trevino <chris.trevino@atsid.com>",
"license": "MIT",
@ -31,111 +32,96 @@
"lib/"
],
"devDependencies": {
"@kadira/storybook": "^2.33.0",
"@kadira/storybook": "^2.35.3",
"@types/bluebird": "^3.0.37",
"@types/chai": "^3.4.34",
"@types/enzyme": "^2.7.1",
"@types/enzyme": "^2.7.2",
"@types/jest": "^18.1.1",
"@types/jsdom": "^2.0.29",
"@types/mocha": "^2.2.36",
"@types/node": "^6.0.58",
"@types/react-dom": "^0.14.20",
"@types/react-router": "^2.0.41",
"@types/react-router-redux": "^4.0.36",
"@types/redux-logger": "^2.6.32",
"@types/react-dom": "^0.14.22",
"@types/react-router": "^3.0.0",
"@types/react-router-redux": "^4.0.39",
"@types/redux-thunk": "^2.1.32",
"@types/sinon": "^1.16.34",
"autoprefixer": "^6.6.1",
"babel-polyfill": "^6.16.0",
"autoprefixer": "^6.7.2",
"babel-polyfill": "^6.22.0",
"bluebird": "^3.4.7",
"chai": "^3.5.0",
"cpx": "^1.3.1",
"css-loader": "^0.26.1",
"electron": "^1.4.10",
"enzyme": "^2.7.0",
"eslint": "^3.13.0",
"eslint-config-airbnb": "^14.0.0",
"electron": "^1.4.15",
"enzyme": "^2.7.1",
"eslint": "^3.15.0",
"eslint-config-airbnb": "^14.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^3.0.2",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^6.9.0",
"file-loader": "^0.9.0",
"filesaver.js": "^0.2.0",
"html-webpack-plugin": "^2.28.0",
"imports-loader": "^0.7.0",
"jsdom": "^9.5.0",
"jest": "^18.1.0",
"jsdom": "^9.10.0",
"json-loader": "^0.5.4",
"mocha": "^3.0.2",
"mocha-junit-reporter": "^1.13.0",
"mocha-multi": "^0.10.0",
"mocha-osx-reporter": "^0.1.2",
"node-sass": "^4.2.0",
"npm-run-all": "^4.0.0",
"nyc": "^10.0.0",
"postcss-loader": "^1.0.0",
"node-sass": "^4.5.0",
"npm-run-all": "^4.0.1",
"nyc": "^10.1.2",
"postcss-loader": "^1.2.2",
"react-addons-test-utils": "^15.4.2",
"react-dnd-html5-backend": "^2.1.2",
"react-dnd-html5-backend": "^2.2.3",
"react-dom": "^15.4.2",
"react-hot-loader": "^1.3.0",
"react-router": "^3.0.0",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.2",
"redux-logger": "^2.6.1",
"redux-thunk": "^2.0.1",
"redux-thunk": "^2.2.0",
"rimraf": "^2.5.4",
"sass-lint": "^1.8.2",
"sass-loader": "^4.1.1",
"sass-loader": "^5.0.0",
"sinon": "2.0.0-pre",
"skeleton-css": "^2.0.4",
"style-loader": "^0.13.1",
"ts-loader": "^1.2.2",
"ts-loader": "^2.0.0",
"ts-node": "^2.0.0",
"typescript": "^2.1.4",
"typescript-eslint-parser": "^1.0.1",
"wallaby-webpack": "^0.0.30",
"webpack": "^1.13.1",
"webpack-dev-middleware": "^1.6.1",
"webpack-dev-server": "^1.14.1",
"webpack-hot-middleware": "^2.12.0"
"typescript": "^2.1.5",
"typescript-eslint-parser": "^1.0.2",
"wallaby-webpack": "^0.0.32",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.3.0"
},
"dependencies": {
"@essex/redux-dag-history": "^2.0.0",
"@essex/redux-dag-history": "^2.0.1",
"@types/classnames": "^0.0.32",
"@types/react": "^0.0.0",
"@types/react-dnd": "^2.0.31",
"@types/react-redux": "^4.4.33",
"@types/react-tabs": "^0.5.21",
"@types/react": "^15.0.6",
"@types/react-dnd": "^2.0.32",
"@types/react-redux": "^4.4.36",
"@types/react-tabs": "^0.5.22",
"@types/redux": "^3.6.31",
"@types/redux-actions": "^1.2.2",
"classnames": "^2.2.5",
"debug": "^2.6.0",
"lodash": "^4.17.4",
"react": "^15.4.2",
"react-dnd": "^2.1.4",
"react-dnd": "^2.2.3",
"react-icons": "^2.2.3",
"react-keydown": "^1.6.2",
"react-redux": "^5.0.1",
"react-redux": "^5.0.2",
"react-simple-dropdown": "^1.1.4",
"react-tabs": "^0.8.2",
"redux": "^3.3.1",
"redux-actions": "^1.2.0"
"redux-actions": "^1.2.1"
},
"nyc": {
"include": [
"src/**/*.ts",
"src/**/*.tsx"
"jest": {
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"exclude": [
"node_modules/",
"lib/",
"stories/"
],
"extension": [
".ts",
".tsx"
],
"require": [
"ts-node/register"
],
"reporter": [
"text-summary",
"html"
],
"sourceMap": true,
"instrument": true
"moduleNameMapper": {
"\\.(css|scss)$": "<rootDir>/scripts/stub.js"
},
"transform": {
"^.+\\.(ts|tsx)$": "<rootDir>/scripts/preprocessor.js"
},
"testRegex": ".*/test/.*/.*\\.spec\\.(ts|tsx|js)$"
}
}

16
scripts/preprocessor.js Normal file
Просмотреть файл

@ -0,0 +1,16 @@
// From: https://github.com/facebook/jest/blob/master/examples/typescript/preprocessor.js
const tsc = require('typescript');
const tsConfig = require('../tsconfig.json');
module.exports = {
process(src, path) {
if (path.endsWith('.ts') || path.endsWith('.tsx')) {
return tsc.transpile(
src,
tsConfig.compilerOptions,
path,
[]
);
}
return src;
},
};

1
scripts/stub.js Normal file
Просмотреть файл

@ -0,0 +1 @@
module.exports = {};

Просмотреть файл

@ -0,0 +1,73 @@
import * as React from "react";
import * as classnames from "classnames";
import './Bookmark.scss';
import DiscoveryTrail from '../DiscoveryTrail';
const { PropTypes } = React;
export interface IBookmarkProps {
name: string;
active?: boolean;
numLeadInStates?: number;
onClick?: Function;
onClickEdit?: Function;
annotation: string;
onDiscoveryTrailIndexClicked?: (index: number) => void;
}
const determineHighlight = (props) => {
const { selectedDepth } = props;
if (selectedDepth === undefined && props.active) {
return Math.max(0, (props.shortestCommitPath || []).length - 1);
}
return selectedDepth;
}
const Bookmark: React.StatelessComponent<IBookmarkProps> = (props) => {
const {
name,
active,
onClick,
onClickEdit,
onDiscoveryTrailIndexClicked,
numLeadInStates,
annotation,
} = props;
const highlight = determineHighlight(props);
const isDiscoveryTrailVisible = active && numLeadInStates > 0;
const discoveryTrail = isDiscoveryTrailVisible ? (
<DiscoveryTrail
fullWidth
depth={this.commitPathLength - 1}
highlight={highlight}
leadIn={numLeadInStates}
active={active}
onIndexClicked={idx => onDiscoveryTrailIndexClicked(idx)}
/>
) : null;
return (
<div
className={`history-bookmark ${active ? 'selected' : ''}`}
>
<div className="bookmark-details-container">
<div className="bookmark-details" onClick={onClick ? () => onClick() : undefined}>
<div
className={classnames('bookmark-title', { active })}
onClick={() => onClickEdit('title')}
>
{name}
</div>
<div
className="bookmark-annotation"
onClick={() => onClickEdit('annotation')}
>
{annotation}
</div>
</div>
{discoveryTrail}
</div>
</div>
);
};
export default Bookmark;

Просмотреть файл

@ -0,0 +1,44 @@
import * as React from "react";
import {default as Bookmark, IEditableBookmarkProps} from "./EditableBookmark";
const flow = require('lodash/flow');
export interface IDragDropBookmarkProps extends IEditableBookmarkProps {
// Injected by React DnD:
isDragging?: boolean;
connectDragSource?: Function;
connectDropTarget?: Function;
dragIndex?: number;
hoverIndex?: number;
dragKey?: string;
dispatch: Function;
stateId: string;
}
export interface IDragDropBookmarkState {}
export default class DrapDropBookmark extends React.Component<IDragDropBookmarkProps, IDragDropBookmarkState> {
private renderBookmark() {
if (this.props.isDragging) {
return (<div className="bookmark-dragged" />);
} else {
return (
<div className="bookmark-dragdrop-wrapper">
<Bookmark {...this.props} />
</div>
);
}
}
public render() {
const {
connectDragSource,
connectDropTarget,
} = this.props;
return flow(
connectDragSource,
connectDropTarget,
)(
this.renderBookmark(),
);
}
}

Просмотреть файл

@ -26,7 +26,6 @@ export interface IEditBookmarkProps {
// Injected by React DnD:
isDragging?: boolean;
connectDragSource?: Function;
}
export interface IEditBookmarkState {}
@ -119,14 +118,13 @@ export default class EditBookmark extends React.Component<IEditBookmarkProps, IE
selectedDepth,
numLeadInStates,
onDiscoveryTrailIndexClicked,
connectDragSource,
} = this.props;
const leadInStatesValue = numLeadInStates !== undefined ? `${numLeadInStates}` : 'all';
const isIntroSet = numLeadInStates !== undefined;
log('rendering commitPathLength=%s, selectedDepth=%s', this.props.commitPathLength, this.props.selectedDepth);
return connectDragSource(
return (
<div
className={`history-bookmark ${active ? 'selected' : ''}`}
data-index={index}

Просмотреть файл

@ -0,0 +1,79 @@
import * as React from "react";
import * as classnames from "classnames";
import './Bookmark.scss';
import EditBookmark from './EditBookmark';
import {default as Bookmark, IBookmarkProps } from './Bookmark';
import DiscoveryTrail from '../DiscoveryTrail';
const { PropTypes } = React;
export interface IEditableBookmarkProps extends IBookmarkProps {
index: number;
numLeadInStates?: number;
onBookmarkChange?: Function;
shortestCommitPath?: number[];
selectedDepth?: number;
onSelectBookmarkDepth?: Function;
}
export interface IEditableBookmarkState {
editMode: boolean;
focusOn?: string;
}
export default class EditableBookmark extends React.PureComponent<IEditableBookmarkProps, IEditableBookmarkState> {
public static propTypes = {
index: PropTypes.number,
name: PropTypes.string.isRequired,
annotation: PropTypes.string.isRequired,
numLeadInStates: PropTypes.number,
active: PropTypes.bool,
onClick: PropTypes.func,
onBookmarkChange: PropTypes.func,
onDiscoveryTrailIndexClicked: PropTypes.func,
shortestCommitPath: PropTypes.arrayOf(PropTypes.number),
selectedDepth: PropTypes.number,
onSelectBookmarkDepth: PropTypes.func,
};
public static defaultProps = {
shortestCommitPath: [],
}
constructor() {
super();
this.state = { editMode: false };
}
onClickEdit(focusOn) {
this.setState({ editMode: true, focusOn });
}
onDoneEditing() {
this.setState({ editMode: false });
}
render() {
const {
editMode,
focusOn,
} = this.state;
if (editMode) {
return (
<EditBookmark
{...this.props}
focusOn={focusOn}
onDoneEditing={() => this.onDoneEditing()}
/>
);
} else {
return (
<Bookmark
{...this.props}
onClickEdit={t => this.onClickEdit(t)}
/>
);
}
}
}

Просмотреть файл

@ -1,9 +1,9 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as classnames from "classnames";
import './Bookmark.scss';
import EditBookmark from './EditBookmark';
import DiscoveryTrail from '../DiscoveryTrail';
import * as state from '../../state';
import {default as DragDropBookmark, IDragDropBookmarkProps} from "./DragDropBookmark";
import { connect } from "react-redux";
import {
bookmarkDragStart,
bookmarkDragHover,
@ -16,75 +16,18 @@ const {
DragSource,
DropTarget,
} = require('react-dnd');
const { connect } = require('react-redux');
const { PropTypes } = React;
export interface IBookmarkDragProps {
// Injected by React DnD:
isDragging?: boolean;
connectDragSource?: Function;
connectDropTarget?: Function;
hoverIndex?: number,
}
export interface IBookmarkProps extends IBookmarkDragProps {
name: string;
onClick?: Function;
active?: boolean;
onDiscoveryTrailIndexClicked?: (index: number) => void;
onSelectBookmarkDepth?: Function;
index: number;
numLeadInStates?: number;
annotation: string;
onBookmarkChange?: Function;
shortestCommitPath?: number[];
selectedDepth?: number;
}
export interface IBookmarkState {
editMode: boolean;
focusOn?: string;
}
const fireHoverEvent = debounce((dispatch, index) => dispatch(bookmarkDragHover({ index })));
const dropTargetSpec = {
drop(props, monitor, component) {
const { index } = props;
return { index };
},
hover(props, monitor, component) {
if (!monitor.isOver()) {
return;
}
const { dispatch, index } = props;
const domNode = ReactDOM.findDOMNode(component);
const { clientWidth: width, clientHeight: height } = domNode;
const rect = domNode.getBoundingClientRect();
const clientY = monitor.getClientOffset().y;
const midline = rect.top + ((rect.bottom - rect.top) / 2);
if (clientY < midline) {
// insert hover into the hover slot, pushing the hovered item forward
fireHoverEvent(dispatch, index);
} else {
// insert hover into next slot
fireHoverEvent(dispatch, index + 1);
}
}
};
const flow = require('lodash/flow');
const dragSource = {
beginDrag(props) {
const { index, dispatch } = props;
dispatch(bookmarkDragStart({ index }));
beginDrag(props: IDragDropBookmarkProps) {
const { index, dispatch, stateId } = props;
dispatch(bookmarkDragStart({ index, key: stateId }));
return { index };
},
endDrag(props, monitor, component) {
endDrag(props: IDragDropBookmarkProps, monitor, component) {
const { dispatch } = props;
const item = monitor.getItem();
// TODO: use key instead for this?
dispatch(bookmarkDragDrop({
index: item.index,
droppedOn: props.hoverIndex,
@ -92,150 +35,53 @@ const dragSource = {
},
};
@connect()
@DragSource("BOOKMARK", dragSource, (connect, monitor) => ({
const fireHoverEvent = debounce((dispatch, index) => dispatch(bookmarkDragHover({ index })));
const dropTargetSpec = {
drop(props: IDragDropBookmarkProps, monitor, component) {
const { index } = props;
return { index };
},
hover(props: IDragDropBookmarkProps, monitor, component) {
if (!monitor.isOver()) {
return;
}
const {
dispatch,
index,
hoverIndex,
dragIndex,
dragKey,
stateId,
} = props;
if (dragKey === stateId) {
return;
}
const domNode = ReactDOM.findDOMNode(component);
const { clientWidth: width, clientHeight: height } = domNode;
const rect = domNode.getBoundingClientRect();
const clientY = monitor.getClientOffset().y;
const midline = rect.top + ((rect.bottom - rect.top) / 2);
const newHoverIndex = clientY < midline ? index : index + 1;
if (newHoverIndex !== hoverIndex) {
fireHoverEvent(dispatch, newHoverIndex);
}
}
};
const connectDragSource = (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))
@DropTarget("BOOKMARK", dropTargetSpec, (connect, monitor) => {
return { connectDropTarget: connect.dropTarget() };
})
export default class Bookmark extends React.PureComponent<IBookmarkProps, IBookmarkState> {
public static propTypes = {
index: PropTypes.number,
name: PropTypes.string.isRequired,
annotation: PropTypes.string.isRequired,
numLeadInStates: PropTypes.number,
active: PropTypes.bool,
onClick: PropTypes.func,
onBookmarkChange: PropTypes.func,
onDiscoveryTrailIndexClicked: PropTypes.func,
shortestCommitPath: PropTypes.arrayOf(PropTypes.number),
selectedDepth: PropTypes.number,
onSelectBookmarkDepth: PropTypes.func,
});
// Injected by React DnD:
isDragging: PropTypes.bool.isRequired,
connectDragSource: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func,
const connectDropTarget = (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
});
// D&D Related
hoverIndex: PropTypes.number,
};
public static defaultProps = {
shortestCommitPath: [],
}
constructor() {
super();
this.state = { editMode: false };
}
onClickEdit(focusOn) {
this.setState({ editMode: true, focusOn });
}
onDoneEditing() {
this.setState({ editMode: false });
}
onDiscoveryTrailIndexClicked(index) {
if (this.props.onDiscoveryTrailIndexClicked) {
this.props.onDiscoveryTrailIndexClicked(index);
}
}
onBookmarkChangeDone(payload) {
if (this.props.onBookmarkChange) {
this.props.onBookmarkChange(payload);
}
}
private get commitPathLength() {
const { shortestCommitPath } = this.props;
return shortestCommitPath.length;
}
private get highlight() {
const { selectedDepth } = this.props;
if (selectedDepth === undefined && this.props.active) {
return this.commitPathLength - 1;
}
return selectedDepth;
}
render() {
const {
name,
onClick,
active,
index,
isDragging,
annotation,
onBookmarkChange,
shortestCommitPath,
selectedDepth,
numLeadInStates,
connectDragSource,
connectDropTarget,
} = this.props;
const {
editMode,
focusOn,
} = this.state;
const { highlight } = this;
const isDiscoveryTrailVisible = active && numLeadInStates > 0;
const discoveryTrail = isDiscoveryTrailVisible ? (
<DiscoveryTrail
fullWidth
depth={this.commitPathLength - 1}
highlight={highlight}
leadIn={numLeadInStates}
active={active}
onIndexClicked={idx => this.onDiscoveryTrailIndexClicked(idx)}
/>
) : null;
if (isDragging) {
return <div className="bookmark-dragged" />
} else if (editMode) {
return (
<EditBookmark
{...this.props}
commitPathLength={this.commitPathLength - 1}
selectedDepth={this.highlight}
focusOn={focusOn}
onDoneEditing={() => this.onDoneEditing()}
onBookmarkChange={p => this.onBookmarkChangeDone(p)}
onDiscoveryTrailIndexClicked={idx => this.onDiscoveryTrailIndexClicked(idx)}
/>
);
} else {
return connectDropTarget(connectDragSource(
<div
className={`history-bookmark ${active ? 'selected' : ''}`}
>
<div className="bookmark-details-container">
<div className="bookmark-details" onClick={onClick ? () => onClick() : undefined}>
<div
className={classnames('bookmark-title', { active })}
onClick={() => this.onClickEdit('title')}
>
{name}
</div>
<div
className="bookmark-annotation"
onClick={() => this.onClickEdit('annotation')}
>
{annotation}
</div>
</div>
{ discoveryTrail }
</div>
</div>
));
}
}
}
export default flow(
DragSource("BOOKMARK", dragSource, connectDragSource),
DropTarget("BOOKMARK", dropTargetSpec, connectDropTarget),
connect(),
)(DragDropBookmark);

Просмотреть файл

@ -12,10 +12,10 @@ export interface IBookmarkListProps {
onBookmarkClick?: Function;
onSelectState?: Function;
onSelectBookmarkDepth?: Function;
connectDropTarget?: Function;
dragIndex?: number;
hoverIndex?: number;
dragKey?: number;
}
export default class BookmarkList extends React.PureComponent<IBookmarkListProps, {}> {
@ -40,17 +40,20 @@ export default class BookmarkList extends React.PureComponent<IBookmarkListProps
onBookmarkClick,
onSelectState,
onSelectBookmarkDepth,
connectDropTarget,
dragIndex,
hoverIndex,
dragKey,
} = this.props;
let bookmarkViews = bookmarks.map((s, index) => (
<Bookmark
{...s}
hoverIndex={hoverIndex}
dragIndex={dragIndex}
dragKey={dragKey}
key={`bookmark::${s.stateId}`}
index={index}
stateId={s.stateId}
onSelectBookmarkDepth={onSelectBookmarkDepth}
onClick={() => this.onBookmarkClick(index, s.stateId)}
onDiscoveryTrailIndexClicked={selectedIndex => {
@ -61,7 +64,7 @@ export default class BookmarkList extends React.PureComponent<IBookmarkListProps
/>
));
if (hoverIndex >= 0 && hoverIndex !== dragIndex) {
if (dragKey && hoverIndex >= 0 && hoverIndex !== dragIndex) {
const dragged = bookmarkViews[dragIndex];
const adjustedHoverIndex = hoverIndex < dragIndex ? hoverIndex : hoverIndex - 1;
bookmarkViews.splice(dragIndex, 1);

Просмотреть файл

@ -13,6 +13,7 @@ const log = require('debug')('dag-history-component:components:StoryboardingView
export interface IBookmarkListContainerStateProps {
dragIndex?: number;
hoverIndex?: number;
dragKey?: number;
}
export interface IBookmarkListContainerDispatchProps {
@ -49,6 +50,7 @@ const BookmarkListContainer: React.StatelessComponent<IBookmarkListContainerProp
selectedBookmarkDepth: selectedBookmarkDepthIndex,
dragIndex,
hoverIndex,
dragKey,
} = props;
const historyGraph = new DagGraph(graph);
const { currentStateId } = historyGraph;
@ -82,6 +84,7 @@ const BookmarkListContainer: React.StatelessComponent<IBookmarkListContainerProp
<BookmarkList
dragIndex={dragIndex}
hoverIndex={hoverIndex}
dragKey={dragKey}
bookmarks={bookmarkData}
onBookmarkClick={(index, state) => onSelectBookmark(index, state)}
onSelectState={onSelectState}
@ -98,6 +101,7 @@ BookmarkListContainer.propTypes = {
selectedBookmark: React.PropTypes.number,
selectedBookmarkDepth: React.PropTypes.number,
dragIndex: React.PropTypes.number,
dragKey: React.PropTypes.number,
hoverIndex: React.PropTypes.number,
};

Просмотреть файл

@ -79,6 +79,7 @@ StoryboardingView.propTypes = {
selectedBookmark: PropTypes.number,
selectedBookmarkDepth: PropTypes.number,
dragIndex: PropTypes.number,
dragKey: PropTypes.number,
hoverIndex: PropTypes.number,
/* User Interaction Handlers - loaded by redux */

Просмотреть файл

@ -1,8 +1,4 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as DagHistoryActions from '@essex/redux-dag-history/lib/ActionCreators';
import * as Actions from '../../state/actions/creators';
import { IDagHistory } from '@essex/redux-dag-history/lib/interfaces';
import DagGraph from '@essex/redux-dag-history/lib/DagGraph';
import HistoryTabs from './HistoryTabs';
@ -24,25 +20,27 @@ const log = require('debug')('dag-history-component:components:History');
export interface IHistoryStateProps {}
export interface IHistoryDispatchProps {
onLoad: Function;
onClear: Function;
onSelectMainView: Function;
onToggleBranchContainer: Function;
onStartPlayback: Function;
onStopPlayback: Function;
onSelectBookmarkDepth: Function;
onSelectState: Function;
onLoad?: Function;
onClear?: Function;
onSelectMainView?: Function;
onToggleBranchContainer?: Function;
onStartPlayback?: Function;
onStopPlayback?: Function;
onSelectBookmarkDepth?: Function;
onSelectState?: Function;
}
export interface IHistoryOwnProps extends IHistoryContainerSharedProps {
}
export interface IHistoryProps extends IHistoryStateProps, IHistoryDispatchProps, IHistoryOwnProps {}
export class History extends React.Component<IHistoryProps, {}> {
export default class History extends React.Component<IHistoryProps, {}> {
public static propTypes = {
bookmarks: PropTypes.array.isRequired,
dragIndex: PropTypes.number,
dragKey: PropTypes.number,
hoverIndex: PropTypes.number,
isPlayingBack: PropTypes.bool,
/**
* The Dag-History Object
@ -221,19 +219,3 @@ export class History extends React.Component<IHistoryProps, {}> {
);
}
}
export default connect<IHistoryStateProps, IHistoryDispatchProps, IHistoryOwnProps>(
() => ({}),
dispatch => bindActionCreators({
onClear: DagHistoryActions.clear,
onLoad: DagHistoryActions.load,
onSelectMainView: Actions.selectMainView,
onSelectState: DagHistoryActions.jumpToState,
onToggleBranchContainer: Actions.toggleBranchContainer,
onStartPlayback: Actions.startPlayback,
onStopPlayback: Actions.stopPlayback,
onSelectBookmarkDepth: Actions.selectBookmarkDepth,
}, dispatch)
)(History);

Просмотреть файл

@ -1,8 +1,11 @@
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { IBookmark } from '../interfaces';
import HistoryComponent from './History';
import {default as HistoryComponent, IHistoryOwnProps, IHistoryDispatchProps } from './History';
import '../daghistory.scss';
import * as DagHistoryActions from '@essex/redux-dag-history/lib/ActionCreators';
import * as Actions from '../state/actions/creators';
const { PropTypes } = React;
@ -17,79 +20,46 @@ export interface IHistoryContainerStateProps {
bookmarks?: IBookmark[];
}
export interface IHistoryContainerDispatchProps {
export interface IHistoryContainerDispatchProps extends IHistoryDispatchProps {
}
export interface IHistoryContainerOwnProps extends IHistoryContainerStateProps {
controlBar: any;
export interface IHistoryContainerOwnProps {
bookmarksEnabled?: boolean;
/**
* ControlBar Configuration Properties
*/
controlBar?: {
/**
* A handler to save the history tree out. This is handled by clients.
*/
onSaveHistory: Function;
/**
* A handler to retrieve the history tree. This is handled by clients
*/
onLoadHistory: Function;
/**
* A function that emits a Promise<boolean> that confirms the clear-history operation.
*/
onConfirmClear: Function;
};
getSourceFromState: Function;
}
export interface IHistoryContainerProps extends IHistoryContainerStateProps, IHistoryContainerDispatchProps, IHistoryContainerOwnProps {
export interface IHistoryContainerProps extends
IHistoryContainerStateProps,
IHistoryContainerDispatchProps,
IHistoryContainerOwnProps {
}
const HistoryContainer: React.StatelessComponent<IHistoryContainerProps> = (props) => {
return (<HistoryComponent {...props} />);
};
HistoryContainer.propTypes = {
/**
* ControlBar Configuration Properties
*/
controlBar: PropTypes.shape({
/**
* A handler to save the history tree out. This is handled by clients.
*/
onSaveHistory: PropTypes.func,
/**
* A handler to retrieve the history tree. This is handled by clients
*/
onLoadHistory: PropTypes.func,
/**
* A function that emits a Promise<boolean> that confirms the clear-history operation.
*/
onConfirmClear: PropTypes.func,
}),
/**
* Bookbark Configuration Properties
*/
bookmarksEnabled: PropTypes.bool,
getSourceFromState: PropTypes.func.isRequired,
// Props injected from redux state
history: PropTypes.object,
highlightSuccessorsOf: PropTypes.number,
mainView: PropTypes.string,
historyType: PropTypes.string,
dragIndex: PropTypes.number,
hoverIndex: PropTypes.number,
branchContainerExpanded: PropTypes.bool,
selectedBookmark: PropTypes.number,
selectedBookmarkDepth: PropTypes.number,
isPlayingBack: PropTypes.bool,
};
const mapStateToProps = state => {
return {
// State from the redux-dag-history middleware
history: state.app,
highlightSuccessorsOf: state.app.pinnedStateId,
// State from the dag-history-component
bookmarks: state.component.bookmarks,
mainView: state.component.views.mainView,
historyType: state.component.views.historyType,
dragIndex: state.component.dragDrop.sourceIndex,
hoverIndex: state.component.dragDrop.hoverIndex,
branchContainerExpanded: state.component.views.branchContainerExpanded,
selectedBookmark: state.component.playback.bookmark,
selectedBookmarkDepth: state.component.playback.depth,
isPlayingBack: state.component.playback.isPlayingBack,
};
...HistoryComponent.propTypes,
};
export default function createHistoryContainer(getMiddlewareState: Function, getComponentState: Function) {
@ -106,6 +76,7 @@ export default function createHistoryContainer(getMiddlewareState: Function, get
mainView: component.views.mainView,
historyType: component.views.historyType,
dragIndex: component.dragDrop.sourceIndex,
dragKey: state.component.dragDrop.sourceKey,
hoverIndex: component.dragDrop.hoverIndex,
branchContainerExpanded: component.views.branchContainerExpanded,
selectedBookmark: component.playback.bookmark,
@ -113,5 +84,18 @@ export default function createHistoryContainer(getMiddlewareState: Function, get
isPlayingBack: component.playback.isPlayingBack,
};
};
return connect<IHistoryContainerStateProps, IHistoryContainerDispatchProps, IHistoryContainerOwnProps>(mapStateToProps)(HistoryContainer);
const mapDispatchToProps = (dispatch) => bindActionCreators({
onClear: DagHistoryActions.clear,
onLoad: DagHistoryActions.load,
onSelectMainView: Actions.selectMainView,
onSelectState: DagHistoryActions.jumpToState,
onToggleBranchContainer: Actions.toggleBranchContainer,
onStartPlayback: Actions.startPlayback,
onStopPlayback: Actions.stopPlayback,
onSelectBookmarkDepth: Actions.selectBookmarkDepth,
}, dispatch);
return connect<IHistoryContainerStateProps, IHistoryContainerDispatchProps, IHistoryContainerOwnProps>(
mapStateToProps,
mapDispatchToProps,
)(HistoryContainer);
};

Просмотреть файл

@ -0,0 +1,18 @@
import Configuration from '@essex/redux-dag-history/lib/configuration';
import { IComponentConfiguration } from './interfaces'; // eslint-disable-line no-unused-vars
export default class ComponentConfiguration<T>
extends Configuration<T>
implements IComponentConfiguration<T> {
constructor(rawConfig: IComponentConfiguration<T>) {
super(rawConfig);
}
private get config() {
return this.rawConfig as IComponentConfiguration<T>;
}
public get initialViewState() {
return this.config.initialViewState || {};
}
}

Просмотреть файл

@ -1,8 +1,9 @@
import { StateId } from '@essex/redux-dag-history/lib/interfaces';
import * as DagHistoryActions from '@essex/redux-dag-history/lib/ActionCreators';
import { createAction } from 'redux-actions';
import * as ReduxActions from 'redux-actions';
import * as Types from './types';
const { createAction } = ReduxActions;
// Simple Action Creators
const doSelectBookmarkDepth = createAction<BookmarkDepthSelection>(Types.SELECT_BOOKMARK_DEPTH);
@ -66,6 +67,7 @@ export interface AddBookmarkPayload {
export interface BookmarkDragStartPayload {
index: number;
key: string;
}
export interface BookmarkDragHoverPayload {

11
src/state/interfaces.ts Normal file
Просмотреть файл

@ -0,0 +1,11 @@
import {
IConfiguration, // eslint-disable-line no-unused-vars
} from '@essex/redux-dag-history/lib/interfaces';
export interface IComponentConfiguration<T> extends IConfiguration<T> {
initialViewState?: {
mainView?: string;
historyType?: string;
branchContainerExpanded?: boolean;
};
}

Просмотреть файл

@ -10,6 +10,7 @@ import {
export const INITIAL_STATE = {
sourceIndex: undefined,
sourceKey: undefined,
hoverIndex: undefined,
};
@ -20,31 +21,20 @@ export default function makeReducer(config: IConfiguration<any>) {
result = {
...state,
sourceIndex: action.payload.index,
sourceKey: action.payload.key,
};
} else if (action.type === BOOKMARK_DRAG_HOVER) {
const hoverIndex = action.payload.index;
result = {
...state,
hoverIndex: action.payload.index,
hoverIndex,
};
} else if (action.type === BOOKMARK_DRAG_DROP) {
result = {
...state,
sourceIndex: undefined,
hoverIndex: undefined,
};
result = INITIAL_STATE;
} else if (action.type === BOOKMARK_DRAG_CANCEL) {
result = {
...state,
sourceIndex: undefined,
hoverIndex: undefined,
};
result = INITIAL_STATE;
} else if (action.type.indexOf('DAG_HISTORY_') !== 0 && config.actionFilter(action.type)) {
// Insertable actions clear the pinned state
result = {
...state,
sourceIndex: undefined,
hoverIndex: undefined,
};
result = INITIAL_STATE;
}
return result;
};

Просмотреть файл

@ -1,13 +1,11 @@
import {
IConfiguration, // eslint-disable-line no-unused-vars
} from '@essex/redux-dag-history/lib/interfaces';
import * as redux from 'redux';
import dragDrop from './dragDrop';
import views from './views';
import playback from './playback';
import bookmarks from './bookmarks';
import { IComponentConfiguration } from '../interfaces'; // eslint-disable-line no-unused-vars
export default function createReducer(config: IConfiguration<any>) {
export default function createReducer<T>(config: IComponentConfiguration<T>) {
return redux.combineReducers({
dragDrop: dragDrop(config),
views: views(config),

Просмотреть файл

@ -1,6 +1,6 @@
import {
IConfiguration, // eslint-disable-line no-unused-vars
} from '@essex/redux-dag-history/lib/interfaces';
IComponentConfiguration, // eslint-disable-line no-unused-vars
} from '../interfaces';
import {
SELECT_MAIN_VIEW,
SELECT_HISTORY_TYPE,
@ -13,8 +13,12 @@ export const INITIAL_STATE = {
branchContainerExpanded: true,
};
export default function (config: IConfiguration<any>) {
return function reduce(state = INITIAL_STATE, action) {
export default function makeReducer<T>(config: IComponentConfiguration<T>) {
const initialState = {
...INITIAL_STATE,
...config.initialViewState,
};
return function reduce(state = initialState, action) {
let result = state;
if (action.type === SELECT_MAIN_VIEW) {
result = {

Просмотреть файл

@ -1,14 +1,11 @@
const log = require('debug')('dag-history-component:SpanCalculator');
export class Span {
public start: number;
public end: number;
public type: string;
constructor(start, end, type) {
this.start = start;
this.end = end;
this.type = type;
constructor(
public start: number,
public end: number,
public type: string, // eslint-disable-line no-unused-vars
) {
}
toString() {

Просмотреть файл

@ -1,24 +1,20 @@
import * as React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import Bookmark from '../../../src/components/Bookmark';
import Bookmark from '../../../src/components/Bookmark/Bookmark';
storiesOf('Bookmark', module)
.add('Inactive', () => (
<Bookmark
name="Some Bookmark Name"
annotation="Some Bookmark Annotation Text. Derp Depr Derp Derp"
index={1}
onClick={action('click')}
onBookmarkChange={action('bookmarkChange')}
/>
))
.add('Active', () => (
<Bookmark
name="Some Bookmark Name"
annotation="Some Bookmark Annotation Text. Derp Depr Derp Derp"
index={1}
onClick={action('click')}
onBookmarkChange={action('bookmarkChange')}
active
/>
));

Просмотреть файл

@ -2,5 +2,7 @@ import { expect } from 'chai';
import * as History from '../src';
describe('The Top-Level Entry Point', () => {
expect(History).to.be.ok;
it('exists', () => {
expect(History).to.be.ok;
});
});

Просмотреть файл

@ -1,5 +0,0 @@
--reporter mocha-multi
--reporter-options spec=-,mocha-junit-reporter=-,mocha-osx-reporter=-
--require ts-node/register
--require test/setup
test/**/*.spec.*

Просмотреть файл

@ -1,5 +1,8 @@
import { expect } from 'chai';
import makeReducer from '../../../src/state/reducers/dragDrop';
import {
default as makeReducer,
INITIAL_STATE,
} from '../../../src/state/reducers/dragDrop';
import {
bookmarkDragStart,
@ -14,20 +17,18 @@ const defaultConfig = {
describe('The DragDrop reducer', () => {
it('will emit an initial dragDrop state', () => {
const state = makeReducer(defaultConfig)(undefined, { type: 'derp' });
expect(state).to.deep.equal({
sourceIndex: undefined,
hoverIndex: undefined,
});
expect(state).to.deep.equal(INITIAL_STATE);
});
it('can handle a dragStart event', () => {
let state;
const reduce = makeReducer(defaultConfig);
state = reduce(state, { type: 'derp' });
state = reduce(state, bookmarkDragStart({ index: 3 }));
state = reduce(state, bookmarkDragStart({ index: 3, key: 2 }));
expect(state).to.deep.equal({
...INITIAL_STATE,
sourceIndex: 3,
hoverIndex: undefined,
sourceKey: 2,
});
});
@ -38,6 +39,7 @@ describe('The DragDrop reducer', () => {
state = reduce(state, bookmarkDragStart({ index: 3 }));
state = reduce(state, bookmarkDragHover({ index: 4 }));
expect(state).to.deep.equal({
...INITIAL_STATE,
sourceIndex: 3,
hoverIndex: 4,
});
@ -50,11 +52,7 @@ describe('The DragDrop reducer', () => {
state = reduce(state, bookmarkDragStart({ index: 3 }));
state = reduce(state, bookmarkDragHover({ index: 4 }));
state = reduce(state, { type: types.BOOKMARK_DRAG_DROP });
expect(state).to.deep.equal({
sourceIndex: undefined,
hoverIndex: undefined,
});
expect(state).to.deep.equal(INITIAL_STATE);
});
it('can handle a dragCancel event', () => {
@ -64,10 +62,6 @@ describe('The DragDrop reducer', () => {
state = reduce(state, bookmarkDragStart({ index: 3 }));
state = reduce(state, bookmarkDragHover({ index: 4 }));
state = reduce(state, { type: types.BOOKMARK_DRAG_CANCEL });
expect(state).to.deep.equal({
sourceIndex: undefined,
hoverIndex: undefined,
});
expect(state).to.deep.equal(INITIAL_STATE);
});
});

Просмотреть файл

@ -1,74 +0,0 @@
/*
import { expect } from 'chai';
import {
INITIAL_STATE,
default as reduceFactory, // eslint-disable-line
} from '../src/reducer';
const reducer = () => reduceFactory({
actionFilter: () => false,
});
describe('The Dag-History Component Reducer', () => {
it('is a function that returns a function', () => {
expect(reduceFactory).to.be.a('function');
expect(reduceFactory({} as any)).to.be.a('function');
});
it('can generate an initial state', () => {
const state = reducer()(undefined, { type: 'DERP' });
expect(state).to.deep.equal(INITIAL_STATE);
});
it('can respond to a SELECT_MAIN_VIEW action', () => {
const state = reducer()(undefined, { type: 'SELECT_MAIN_VIEW', payload: 'abc123' });
expect(state).to.deep.equal({
...INITIAL_STATE,
mainView: 'abc123',
});
});
it('can respond to a SELECT_HISTORY_TYPE action', () => {
const state = reducer()(undefined, { type: 'SELECT_HISTORY_TYPE', payload: 'derp' });
expect(state).to.deep.equal({
...INITIAL_STATE,
mainView: 'history',
historyType: 'derp',
});
});
it('can respond to a TOGGLE_BRANCH_CONTAINER action', () => {
let state = reducer()(undefined, { type: 'TOGGLE_BRANCH_CONTAINER' });
expect(state).to.deep.equal({
...INITIAL_STATE,
branchContainerExpanded: false,
});
state = reducer()(state, { type: 'TOGGLE_BRANCH_CONTAINER' });
expect(state).to.deep.equal({
...INITIAL_STATE,
branchContainerExpanded: true,
});
});
it('will not reset the main view to history if DAG_HISTORY_* actions are taken', () => {
const initialState = {
...INITIAL_STATE,
mainView: 'bookmarks',
};
const state = reducer()(initialState, { type: 'DAG_HISTORY_DERP' });
expect(state).to.deep.equal(initialState);
});
it('will reset the main view to history if an insertable action occurs', () => {
const initialState = {
...INITIAL_STATE,
mainView: 'bookmarks',
};
const reduce = reduceFactory({
actionFilter: () => true,
});
const state = reduce(initialState, { type: 'DERP' });
expect(state.mainView).to.equal('history');
});
});
*/

Просмотреть файл

@ -1,41 +1,40 @@
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const CSS_LOADERS = ['style-loader', 'css-loader', 'postcss-loader'];
module.exports = {
devtool: 'source-map',
context: path.join(__dirname),
devtool: 'inline-source-map',
entry: {
javascript: './example/app.tsx',
html: './example/index.html',
},
output: {
filename: './appbundle.js',
path: path.join(__dirname, 'dist'),
filename: 'appbundle.js',
},
resolve: {
extensions: ['', '.js', '.jsx', '.ts', '.tsx'],
modulesDirectories: [path.join(__dirname, 'node_modules')],
fallback: path.join(__dirname, 'node_modules'),
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'react/lib/ReactMount': 'react-dom/lib/ReactMount',
sinon: 'sinon/pkg/sinon',
},
},
resolveLoader: {
modulesDirectories: [path.join(__dirname, 'node_modules')],
fallback: path.join(__dirname, 'node_modules'),
},
plugins: [
new webpack.NoErrorsPlugin(),
new HtmlWebpackPlugin({
title: 'DAG History Component Example',
}),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.ProvidePlugin({
saveAs: 'imports?this=>global!exports?global.saveAs!filesaver.js',
}),
],
module: {
loaders: [
{ test: /\.html$/, loader: 'file-loader?name=[name].[ext]' },
{ test: /\.css$/, loader: 'style-loader!css-loader!postcss-loader' },
{ test: /\.scss$/, loader: 'style-loader!css-loader!postcss-loader!sass-loader' },
{ test: /\.ts(x|)/, loaders: ['ts-loader'], exclude: /node_modules/ },
rules: [
{ test: /\.css$/, use: CSS_LOADERS },
{ test: /\.scss$/, use: [...CSS_LOADERS, 'sass-loader'] },
{ test: /\.ts(x|)/, use: ['ts-loader'], exclude: /node_modules/ },
],
},
};

1720
yarn.lock

Разница между файлами не показана из-за своего большого размера Загрузить разницу