├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── .yarnrc ├── LICENSE ├── README.md ├── SECURITY.md ├── lerna.json ├── package.json ├── packages ├── dag-history-component │ ├── .gitignore │ ├── .npmignore │ ├── .storybook │ │ ├── config.js │ │ └── webpack.config.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── Bookmark │ │ │ │ ├── Bookmark.tsx │ │ │ │ ├── DragDropBookmark.tsx │ │ │ │ ├── EditBookmark.tsx │ │ │ │ ├── EditableBookmark.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── BookmarkList │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── Branch │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── BranchList │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── BranchListContainer │ │ │ │ └── index.tsx │ │ │ ├── BranchProfile │ │ │ │ ├── calculateSpans.ts │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── Continuation │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── DiscoveryTrail │ │ │ │ ├── calculateSpans.ts │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── ExpandCollapseToggle │ │ │ │ └── index.tsx │ │ │ ├── History │ │ │ │ ├── BranchedHistoryView │ │ │ │ │ └── index.tsx │ │ │ │ ├── ChronologicalHistoryView │ │ │ │ │ └── index.tsx │ │ │ │ ├── SwitchingHistoryView │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── interfaces.ts │ │ │ │ └── styled.ts │ │ │ ├── HistoryContainer │ │ │ │ └── index.ts │ │ │ ├── HistoryTabs │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── HistoryTypeDropdown │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── OptionDropdown │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── PlaybackPane │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── State │ │ │ │ ├── index.tsx │ │ │ │ ├── interfaces.ts │ │ │ │ └── styled.ts │ │ │ ├── StateList │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── StateListContainer │ │ │ │ └── index.tsx │ │ │ ├── StateListManager │ │ │ │ └── index.tsx │ │ │ ├── StoryboardingView │ │ │ │ ├── BookmarkListContainer.tsx │ │ │ │ └── index.tsx │ │ │ ├── Transport │ │ │ │ ├── buttons.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── createHistoryContainer.tsx │ │ │ └── styled.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── palette.ts │ │ ├── state │ │ │ ├── Configuration.ts │ │ │ ├── actions │ │ │ │ ├── creators.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── interfaces.ts │ │ │ └── reducers │ │ │ │ ├── bookmarkEdit.ts │ │ │ │ ├── bookmarks.ts │ │ │ │ ├── configurableReducer.ts │ │ │ │ ├── dragDrop.ts │ │ │ │ ├── index.ts │ │ │ │ ├── isHistoryAction.ts │ │ │ │ ├── pinnedState.ts │ │ │ │ ├── playback.ts │ │ │ │ └── views.ts │ │ └── util │ │ │ ├── Bookmark.ts │ │ │ ├── BookmarkActions.ts │ │ │ ├── calculateIndex.ts │ │ │ ├── isNumber.ts │ │ │ └── spans.ts │ ├── stories │ │ ├── components │ │ │ ├── Bookmark │ │ │ │ └── index.tsx │ │ │ ├── Branch │ │ │ │ └── index.tsx │ │ │ ├── BranchList │ │ │ │ └── index.tsx │ │ │ ├── BranchProfile │ │ │ │ └── index.tsx │ │ │ ├── Continuation │ │ │ │ └── index.tsx │ │ │ ├── DiscoveryTrail │ │ │ │ └── index.tsx │ │ │ ├── History │ │ │ │ └── index.tsx │ │ │ ├── HistoryTypeDropdown │ │ │ │ └── index.tsx │ │ │ └── State │ │ │ │ └── index.tsx │ │ └── index.ts │ ├── styles.css │ ├── test │ │ ├── components │ │ │ ├── Bookmark │ │ │ │ └── index.spec.tsx │ │ │ ├── BookmarkList │ │ │ │ └── index.spec.tsx │ │ │ ├── Branch │ │ │ │ └── index.spec.tsx │ │ │ ├── BranchList │ │ │ │ └── index.spec.tsx │ │ │ ├── Continuation │ │ │ │ └── index.spec.tsx │ │ │ ├── History │ │ │ │ └── index.spec.tsx │ │ │ ├── OptionDropdown │ │ │ │ └── index.spec.tsx │ │ │ ├── PlaybackPane │ │ │ │ └── index.spec.tsx │ │ │ ├── State │ │ │ │ └── index.spec.tsx │ │ │ └── StateList │ │ │ │ └── index.spec.tsx │ │ ├── index.spec.ts │ │ ├── palette.spec.ts │ │ ├── state │ │ │ ├── actions │ │ │ │ ├── creators.spec.ts │ │ │ │ └── types.spec.ts │ │ │ └── reducers │ │ │ │ ├── bookmarks.spec.ts │ │ │ │ ├── dragDrop.spec.ts │ │ │ │ ├── playback.spec.ts │ │ │ │ └── views.spec.ts │ │ └── util │ │ │ ├── Bookmark.spec.ts │ │ │ ├── isNumber.spec.ts │ │ │ ├── percentCalc.spec.ts │ │ │ └── spans.spec.ts │ └── tsconfig.json ├── example │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── app.css │ │ ├── app.tsx │ │ ├── components │ │ │ ├── Application.tsx │ │ │ ├── HistoryPresenter.tsx │ │ │ └── visuals │ │ │ │ ├── VisualA.tsx │ │ │ │ └── VisualB.tsx │ │ ├── persister │ │ │ ├── index.ts │ │ │ └── simulate.ts │ │ ├── state │ │ │ ├── Actions.ts │ │ │ ├── reducers │ │ │ │ ├── app │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── metadata.ts │ │ │ │ │ └── visuals.ts │ │ │ │ └── index.ts │ │ │ └── store.ts │ │ └── util │ │ │ └── hashString.ts │ ├── tsconfig.json │ └── webpack.config.js └── redux-dag-history │ ├── .npmignore │ ├── package.json │ ├── src │ ├── ActionCreators.ts │ ├── ActionTypes.ts │ ├── Configuration.ts │ ├── DagGraph.ts │ ├── DagHistory │ │ ├── clear.ts │ │ ├── createBranch.ts │ │ ├── createHistory.ts │ │ ├── getExistingState.ts │ │ ├── index.ts │ │ ├── insert.ts │ │ ├── jump.ts │ │ ├── jumpToBranch.ts │ │ ├── jumpToLatestOnBranch.ts │ │ ├── jumpToState.ts │ │ ├── jumpToStateLogged.ts │ │ ├── load.ts │ │ ├── log.ts │ │ ├── redo.ts │ │ ├── renameBranch.ts │ │ ├── renameState.ts │ │ ├── replaceCurrentState.ts │ │ ├── skipToEnd.ts │ │ ├── skipToStart.ts │ │ ├── squash.ts │ │ ├── undo.ts │ │ └── unfreeze.ts │ ├── index.ts │ ├── interfaces.ts │ ├── nextId.ts │ └── reducer.ts │ ├── test │ ├── DagGraph.spec.ts │ └── DagHistory.spec.ts │ └── tsconfig.json ├── postcss.config.js ├── scripts └── stub.js ├── tsconfig.jest.json ├── tsconfig.json ├── tslint.json ├── wallaby.conf.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | indent_size = 2 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform EOL normalization 2 | # (`text=auto` means that repo will use LF, `eol=lf` means that Git will ensure LF in the working copy) 3 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | *-debug.log 5 | *-error.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "node" 5 | cache: 6 | yarn: true 7 | directories: 8 | - node_modules 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "msjsdiag.debugger-for-chrome", 5 | "eg2.tslint", 6 | "jpoissonnier.vscode-styled-components", 7 | "naumovs.color-highlight", 8 | "WallabyJs.wallaby-vscode" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.useTabStops": true, 4 | "editor.formatOnSave": true, 5 | "files.eol": "\n", 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/.svn": true, 9 | "**/.DS_Store": true, 10 | "**/*~": true, 11 | "**/#*": true, 12 | "**/lib/": true, 13 | "coverage": true, 14 | "storybook-static": true 15 | }, 16 | "typescript.tsdk": "./node_modules/typescript/lib" 17 | } 18 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Microsoft 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Microsoft/redux-dag-history.svg?branch=master)](https://travis-ci.org/Microsoft/redux-dag-history) 2 | 3 | # Redux DAG History 4 | 5 | Nonlinear History 6 | 7 | This project is a redux middleware that provides an alternative take on application history. Independent threads of user exploration are tracked as 8 | separate "branches" in a state DAG (Directed Acyclic Graph) inspired roughly by Git version control. Some additional concepts have been implemented, 9 | including: 10 | 11 | * Pinning states of interest 12 | * Checking for state equivalency before inserting a new state 13 | * Tracking alternate routes to a state 14 | * Import/Export 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0-beta.38", 3 | "version": "independent", 4 | "npmClient": "yarn", 5 | "packages": [ 6 | "packages/*" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-dag-history-parent", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "precommit": "lint-staged", 7 | "//prepush": "yarn lint", 8 | "build": "lerna run build --stream", 9 | "clean": "lerna run clean", 10 | "prettify": "prettier \"packages/*/**/*.{js,jsx,ts,tsx}\"", 11 | "lint": "tslint 'packages/*/{src,test}/**/*.ts*'", 12 | "jest": "cross-env TS_JEST_DEBUG=true jest", 13 | "jest:watch": "jest --watch", 14 | "jest:coverage": "jest --coverage", 15 | "start:middleware": 16 | "lerna run start --scope @essex/redux-dag-history --stream", 17 | "start:component": 18 | "lerna run start --scope @essex/dag-history-component --stream", 19 | "start:example": 20 | "lerna run start --scope=@essex/dag-history-example --stream", 21 | "packages:test": "lerna run test --stream", 22 | "test": "run-s clean lint build jest:coverage", 23 | "start": "npm-run-all clean build -p start:*" 24 | }, 25 | "author": "Chris Trevino ", 26 | "workspaces": ["packages/*"], 27 | "lint-staged": { 28 | "packages/**/*.{js,tsx}": [ 29 | "prettier --trailingComma=es5 --write", 30 | "git add" 31 | ], 32 | "packages/**/*.{ts,tsx}": [ 33 | "prettier --trailingComma=all --write", 34 | "git add" 35 | ] 36 | }, 37 | "prettier": { 38 | "singleQuote": true, 39 | "trailingComma": "all", 40 | "semi": false, 41 | "useTabs": true 42 | }, 43 | "jest": { 44 | "transform": { 45 | "^.+\\.tsx?$": "ts-jest" 46 | }, 47 | "globals": { 48 | "__TS_CONFIG__": { 49 | "module": "commonjs" 50 | }, 51 | "ts-jest": { 52 | "tsConfigFile": "tsconfig.jest.json" 53 | } 54 | }, 55 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"], 56 | "moduleNameMapper": { 57 | "\\.(css|scss)$": "/scripts/stub.js" 58 | }, 59 | "testRegex": ".*/packages/.*/test/.*/.*\\.spec\\.(ts|tsx|js)$" 60 | }, 61 | "devDependencies": { 62 | "@types/jest": "^22.1.1", 63 | "@types/redux-logger": "^2.6.34", 64 | "cross-env": "^5.1.3", 65 | "husky": "^0.14.3", 66 | "jest": "^22.1.4", 67 | "lerna": "^2.8.0", 68 | "lint-staged": "^6.1.0", 69 | "npm-run-all": "^4.1.2", 70 | "prettier": "^1.10.2", 71 | "redux-logger": "^2.8.2", 72 | "ts-jest": "^22.0.3", 73 | "tslint": "^5.9.1", 74 | "tslint-react": "^3.4.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/dag-history-component/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | dist/ 4 | storybook-static/ 5 | coverage/ 6 | .nyc_output/ 7 | npm-debug.log* 8 | yarn-error.log* 9 | test-results.xml 10 | -------------------------------------------------------------------------------- /packages/dag-history-component/.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | node_modules/ 3 | src/ 4 | test/ 5 | coverage/ 6 | -------------------------------------------------------------------------------- /packages/dag-history-component/.storybook/config.js: -------------------------------------------------------------------------------- 1 | const { configure } = require('@storybook/react') 2 | 3 | function loadStories() { 4 | require('../stories') 5 | } 6 | 7 | configure(loadStories, module) 8 | -------------------------------------------------------------------------------- /packages/dag-history-component/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const lodash = require('lodash') 3 | const get = lodash.get 4 | 5 | const tsxRule = { 6 | test: /\.ts(x|)/, 7 | loaders: ['ts-loader'], 8 | } 9 | const cssRule = { 10 | test: /\.css/, 11 | loaders: ['style-loader', 'css-loader'], 12 | } 13 | 14 | module.exports = (baseConfig, env) => { 15 | const rules = get(baseConfig, 'module.rules', []) 16 | const extensions = get(baseConfig, 'resolve.extensions', ['.js']) 17 | 18 | const config = Object.assign({}, baseConfig, { 19 | resolve: Object.assign({}, get(baseConfig, 'resolve', {}), { 20 | extensions: extensions.concat('.ts', '.tsx'), 21 | }), 22 | module: Object.assign({}, get(baseConfig, 'module', {}), { 23 | rules: rules.concat(tsxRule, cssRule), 24 | }), 25 | }) 26 | return config 27 | } 28 | -------------------------------------------------------------------------------- /packages/dag-history-component/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Microsoft/dag-history-component.svg?branch=master)](https://travis-ci.org/Microsoft/dag-history-component) 2 | 3 | # dag-history-component 4 | 5 | A React component library for representing application histories using the [redux-dag-history](http://github.com/Microsoft/redux-dag-history) middleware. 6 | -------------------------------------------------------------------------------- /packages/dag-history-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@essex/dag-history-component", 3 | "version": "3.0.4", 4 | "description": "A React Component for Dag-History Visualization", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib/ storybook-static/", 8 | "start:storybook": "start-storybook -p 6006", 9 | "start:tsc": "tsc -w", 10 | "start": "run-p start:*", 11 | "build:tsc": "tsc", 12 | "//build:storybook": "build-storybook", 13 | "build": "run-p build:*" 14 | }, 15 | "license": "MIT", 16 | "files": ["package.json", "README.md", "LICENSE", "styles.css", "lib/"], 17 | "devDependencies": { 18 | "@storybook/addon-actions": "^3.3.12", 19 | "@storybook/react": "^3.3.12", 20 | "@types/bluebird": "^3.5.20", 21 | "@types/enzyme": "^3.1.8", 22 | "@types/enzyme-adapter-react-16": "^1.0.1", 23 | "@types/redux-logger": "^3.0.5", 24 | "@types/sinon": "^4.1.3", 25 | "@types/storybook__react": "^3.0.6", 26 | "bluebird": "^3.5.1", 27 | "enzyme": "^3.3.0", 28 | "enzyme-adapter-react-16": "^1.1.1", 29 | "immutable": "^3.8.2", 30 | "npm-run-all": "^4.1.2", 31 | "react-dnd-html5-backend": "^2.5.4", 32 | "redux-logger": "^3.0.6", 33 | "redux-thunk": "^2.2.0", 34 | "rimraf": "^2.6.2", 35 | "sinon": "^4.2.2", 36 | "typescript": "^2.7.1" 37 | }, 38 | "dependencies": { 39 | "@essex/redux-dag-history": "^5.0.1", 40 | "@types/classnames": "^2.2.3", 41 | "@types/react": "^16.0.36", 42 | "@types/react-dnd": "^2.0.34", 43 | "@types/react-dom": "^16.0.3", 44 | "@types/react-redux": "^5.0.14", 45 | "@types/react-tabs": "^1.0.3", 46 | "@types/react-transition-group": "^2.0.6", 47 | "@types/redux": "^3.6.31", 48 | "@types/redux-actions": "^2.2.3", 49 | "classnames": "^2.2.5", 50 | "debug": "^3.1.0", 51 | "lodash": "^4.17.5", 52 | "react": "^16.2.0", 53 | "react-dnd": "^2.5.4", 54 | "react-dom": "^16.2.0", 55 | "react-icons": "^2.2.7", 56 | "react-keydown": "^1.9.6", 57 | "react-redux": "^5.0.6", 58 | "react-simple-dropdown": "^3.2.0", 59 | "react-tabs": "^2.2.1", 60 | "react-transition-group": "^2.2.1", 61 | "redux": "^3.7.2", 62 | "redux-actions": "^2.2.1", 63 | "styled-components": "^3.1.6" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Bookmark/Bookmark.tsx: -------------------------------------------------------------------------------- 1 | import * as classnames from 'classnames' 2 | import * as React from 'react' 3 | import DiscoveryTrail from '../DiscoveryTrail' 4 | import { 5 | Container, 6 | DetailsContainer, 7 | Details, 8 | Title, 9 | Annotation, 10 | } from './styled' 11 | 12 | export interface BookmarkProps { 13 | name: string 14 | index: number 15 | active?: boolean 16 | numLeadInStates?: number 17 | onClick?: () => void 18 | onClickEdit?: () => void 19 | annotation: string 20 | commitPathLength: number 21 | onDiscoveryTrailIndexClicked?: (index: number) => void 22 | } 23 | 24 | function determineHighlight(props: BookmarkProps): number { 25 | /* 26 | TODO: Props here were wrong - fix highlight 27 | const { selectedDepth } = props 28 | if (selectedDepth === undefined && props.active) { 29 | return Math.max(0, (props.shortestCommitPath || []).length - 1) 30 | } 31 | return selectedDepth 32 | */ 33 | return 0 34 | } 35 | 36 | const Bookmark: React.StatelessComponent = ( 37 | props: BookmarkProps, 38 | ) => { 39 | const { 40 | name, 41 | index, 42 | active, 43 | onClick, 44 | onClickEdit, 45 | onDiscoveryTrailIndexClicked, 46 | numLeadInStates, 47 | annotation, 48 | commitPathLength, 49 | } = props 50 | const highlight = determineHighlight(props) 51 | const isDiscoveryTrailVisible = active && numLeadInStates > 0 52 | const discoveryTrail = isDiscoveryTrailVisible ? ( 53 | onDiscoveryTrailIndexClicked(idx)} 60 | /> 61 | ) : null 62 | return ( 63 | 64 | 65 |
onClick() : undefined}> 66 | onClickEdit()} 69 | > 70 | {name} 71 | 72 | onClickEdit()}>{annotation} 73 |
74 | {discoveryTrail} 75 |
76 |
77 | ) 78 | } 79 | 80 | export default Bookmark 81 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Bookmark/DragDropBookmark.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Dispatch } from 'redux' 3 | import EditableBookmark, { EditableBookmarkProps } from './EditableBookmark' 4 | import { Dragged } from './styled' 5 | const flow = require('lodash/flow') 6 | 7 | export interface DragDropBookmarkProps extends EditableBookmarkProps { 8 | // Injected by React DnD: 9 | isDragging?: boolean 10 | connectDragSource?: () => void 11 | connectDropTarget?: () => void 12 | dragIndex?: number 13 | hoverIndex?: number 14 | dragKey?: string 15 | dispatch: Dispatch 16 | stateId: string 17 | } 18 | 19 | export default class DrapDropBookmark extends React.Component< 20 | DragDropBookmarkProps 21 | > { 22 | public render() { 23 | const { connectDragSource, connectDropTarget } = this.props 24 | return flow(connectDragSource, connectDropTarget)(this.renderBookmark()) 25 | } 26 | 27 | private renderBookmark() { 28 | if (this.props.isDragging) { 29 | return ( 30 |
31 | 32 |
33 | ) 34 | } else { 35 | return ( 36 |
37 | 38 |
39 | ) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Bookmark/EditableBookmark.tsx: -------------------------------------------------------------------------------- 1 | import { StateId } from '@essex/redux-dag-history' 2 | import * as classnames from 'classnames' 3 | import * as React from 'react' 4 | import DiscoveryTrail from '../DiscoveryTrail' 5 | import Bookmark, { BookmarkProps } from './Bookmark' 6 | import EditBookmark from './EditBookmark' 7 | 8 | export interface EditableBookmarkProps extends BookmarkProps { 9 | index: number 10 | numLeadInStates?: number 11 | onBookmarkChange?: Function 12 | onBookmarkEdit?: Function 13 | onBookmarkEditDone?: Function 14 | shortestCommitPath?: StateId[] 15 | selectedDepth?: number 16 | onSelectBookmarkDepth?: Function 17 | editMode: boolean 18 | } 19 | 20 | const EditableBookmark: React.StatelessComponent< 21 | EditableBookmarkProps 22 | > = props => { 23 | const { editMode, onBookmarkEdit, onBookmarkEditDone } = props 24 | const innerBookmark = editMode ? ( 25 | 26 | ) : ( 27 | onBookmarkEdit(props.index)} /> 28 | ) 29 | 30 | return
{innerBookmark}
31 | } 32 | EditableBookmark.defaultProps = { 33 | index: null, 34 | editMode: false, 35 | name: '', 36 | annotation: '', 37 | commitPathLength: 0, 38 | shortestCommitPath: [], 39 | } 40 | 41 | export default EditableBookmark 42 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Bookmark/index.tsx: -------------------------------------------------------------------------------- 1 | import * as classnames from 'classnames' 2 | import { debounce } from 'lodash' 3 | import * as React from 'react' 4 | import * as ReactDOM from 'react-dom' 5 | import { connect } from 'react-redux' 6 | import * as state from '../../state' 7 | import { 8 | DragSource, 9 | DropTarget, 10 | DragSourceMonitor, 11 | DropTargetMonitor, 12 | DragSourceCollector, 13 | DropTargetCollector, 14 | } from 'react-dnd' 15 | import { 16 | bookmarkDragCancel, 17 | bookmarkDragDrop, 18 | bookmarkDragHover, 19 | bookmarkDragStart, 20 | } from '../../state/actions/creators' 21 | import { 22 | default as DragDropBookmark, 23 | DragDropBookmarkProps, 24 | } from './DragDropBookmark' 25 | 26 | const flow = require('lodash/flow') 27 | 28 | const dragSource = { 29 | beginDrag(props: DragDropBookmarkProps) { 30 | const { index, dispatch, stateId } = props 31 | dispatch(bookmarkDragStart({ index, key: stateId })) 32 | return { index } 33 | }, 34 | endDrag(props: DragDropBookmarkProps, monitor: DragSourceMonitor) { 35 | const { dispatch, hoverIndex, dragIndex } = props 36 | const item = monitor.getItem() as any 37 | const droppedOn = hoverIndex < dragIndex ? hoverIndex : hoverIndex - 1 38 | dispatch(bookmarkDragDrop({ 39 | index: item.index, 40 | droppedOn, 41 | }) as any) 42 | }, 43 | } 44 | 45 | const fireHoverEvent = debounce((dispatch, index) => 46 | dispatch(bookmarkDragHover({ index })), 47 | ) 48 | 49 | const dropTargetSpec = { 50 | drop(props: DragDropBookmarkProps, monitor: DragSourceMonitor) { 51 | const { index } = props 52 | return { index } 53 | }, 54 | hover( 55 | props: DragDropBookmarkProps, 56 | monitor: DropTargetMonitor, 57 | component: React.ReactInstance, 58 | ) { 59 | if (!monitor.isOver()) { 60 | return 61 | } 62 | const { dispatch, index, hoverIndex, dragIndex, dragKey, stateId } = props 63 | 64 | if (dragKey === stateId) { 65 | return 66 | } 67 | const domNode = ReactDOM.findDOMNode(component) 68 | const { clientWidth: width, clientHeight: height } = domNode 69 | const rect = domNode.getBoundingClientRect() 70 | const clientY = monitor.getClientOffset().y 71 | const midline = rect.top + (rect.bottom - rect.top) / 2 72 | const newHoverIndex = clientY < midline ? index : index + 1 73 | 74 | if (newHoverIndex !== hoverIndex) { 75 | fireHoverEvent(dispatch, newHoverIndex) 76 | } 77 | }, 78 | } 79 | 80 | const connectDragSource: DragSourceCollector = (c, monitor) => ({ 81 | connectDragSource: c.dragSource(), 82 | isDragging: monitor.isDragging(), 83 | }) 84 | 85 | const connectDropTarget: DropTargetCollector = (c, monitor) => ({ 86 | connectDropTarget: c.dropTarget(), 87 | }) 88 | 89 | export default flow( 90 | DragSource('BOOKMARK', dragSource, connectDragSource), 91 | DropTarget('BOOKMARK', dropTargetSpec as any, connectDropTarget), 92 | connect(), 93 | )(DragDropBookmark) 94 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Bookmark/styled.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | const white = '#fff' 5 | const highlight = '#f00' 6 | const background = '#f5e2d4' 7 | const selectedBackground = '#ebc6ab' 8 | const gray = '#d1d1d1' 9 | const charcoal = '#5d6d7e' 10 | const black = '#000' 11 | 12 | export const Dragged = styled.div` 13 | min-height: 30px; 14 | background-color: grey; 15 | ` 16 | 17 | export const Container = styled.div` 18 | border-top: 1px solid ${white}; 19 | align-items: center; 20 | background-color: ${background}; 21 | display: flex; 22 | min-height: 30px; 23 | min-width: 250px; 24 | position: relative; 25 | 26 | &.active { 27 | background-color: ${selectedBackground}; 28 | 29 | &:hover { 30 | border-right: 3px solid ${highlight}; 31 | } 32 | 33 | &:not(:hover) { 34 | border-right: 3px solid ${selectedBackground}; 35 | } 36 | } 37 | 38 | &:not(:hover) { 39 | border-right: 3px solid ${background}; 40 | } 41 | 42 | &:hover { 43 | border-right: 3px solid ${highlight}; 44 | } 45 | 46 | .label { 47 | font-family: sans-serif; 48 | font-size: 11pt; 49 | font-weight: 300; 50 | } 51 | ` 52 | 53 | const DetailsBase = styled.div` 54 | align-self: stretch; 55 | font-family: sans-serif; 56 | left: 0; 57 | top: 0; 58 | ` 59 | 60 | export const DetailsContainer = DetailsBase.extend` 61 | flex-direction: column; 62 | flex: 1; 63 | ` 64 | 65 | export const Details = DetailsBase.extend` 66 | align-items: flex-start; 67 | flex-direction: column; 68 | padding: 4px; 69 | ` 70 | 71 | export const Title = DetailsBase.extend` 72 | &:active { 73 | color: ${black}; 74 | } 75 | 76 | &:not(.active) { 77 | color: ${charcoal}; 78 | } 79 | ` 80 | 81 | export const Annotation = DetailsBase.extend` 82 | color: ${charcoal}; 83 | font-size: 9pt; 84 | margin-top: 5px; 85 | ` 86 | 87 | export const DetailsEditable = DetailsBase.extend` 88 | align-items: stretch; 89 | flex-direction: column; 90 | padding: 10px; 91 | width: 100%; 92 | ` 93 | 94 | export const EditableTitleContainer = styled.div` 95 | display: flex; 96 | justify-content: space-between; 97 | ` 98 | 99 | export const EditAnnotation = styled.textarea` 100 | background-color: $white; 101 | border: 1px solid $gray; 102 | border-radius: 4px; 103 | box-shadow: none; 104 | box-sizing: border-box; 105 | flex: 1; 106 | padding: 6px 10px; 107 | width: 100%; 108 | margin-top: 5px; 109 | ` 110 | 111 | export const ControlsContainer = styled.div` 112 | display: flex; 113 | flex-direction: row; 114 | justify-content: space-between; 115 | 116 | .bookmark-controls { 117 | display: flex; 118 | flex-direction: row; 119 | flex: 1; 120 | } 121 | ` 122 | 123 | export const DiscoveryTrailLabel = styled.span` 124 | font-size: 9pt; 125 | font-weight: lighter; 126 | ` 127 | 128 | export const DiscoveryTrailInfoButton = styled.button` 129 | font-size: 9px; 130 | height: 25px; 131 | line-height: 25px; 132 | padding: 0 15px; 133 | margin-left: 5px; 134 | ` 135 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/BookmarkList/index.tsx: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug' 2 | import * as React from 'react' 3 | import { StateId } from '@essex/redux-dag-history' 4 | import Bookmark from '../Bookmark' 5 | import { Bookmarks, NoBookmarkText } from './styled' 6 | 7 | const { DropTarget } = require('react-dnd') 8 | 9 | const log = debug('@essex/redux-dag-history:BookmarkList') 10 | 11 | export interface BookmarkListProps { 12 | bookmarks: any[] 13 | onBookmarkClick?: Function 14 | onSelectState?: Function 15 | onSelectBookmarkDepth?: Function 16 | bookmarkEditIndex?: number 17 | onBookmarkEdit: Function 18 | onBookmarkEditDone: Function 19 | 20 | dragIndex?: number 21 | hoverIndex?: number 22 | dragKey?: string 23 | } 24 | 25 | const NoBookmarks: React.StatelessComponent<{}> = () => ( 26 | No Bookmarks 27 | ) 28 | 29 | export default class BookmarkList extends React.PureComponent< 30 | BookmarkListProps, 31 | {} 32 | > { 33 | public onBookmarkClick(index: number, stateId: StateId) { 34 | if (this.props.onBookmarkClick) { 35 | this.props.onBookmarkClick(index, stateId) 36 | } 37 | } 38 | 39 | public render() { 40 | const { 41 | bookmarks, 42 | onBookmarkClick, 43 | onSelectState, 44 | onSelectBookmarkDepth, 45 | dragIndex, 46 | hoverIndex, 47 | dragKey, 48 | onBookmarkEdit, 49 | onBookmarkEditDone, 50 | bookmarkEditIndex, 51 | } = this.props 52 | 53 | const bookmarkViews = bookmarks.map((s, index) => ( 54 | this.onBookmarkClick(index, s.stateId)} 67 | onDiscoveryTrailIndexClicked={(selectedIndex: number) => { 68 | const target = s.shortestCommitPath[selectedIndex] 69 | onSelectBookmarkDepth({ target, depth: selectedIndex, state: target }) 70 | onSelectState(target) 71 | }} 72 | /> 73 | )) 74 | 75 | if (dragKey && hoverIndex >= 0 && hoverIndex !== dragIndex) { 76 | const dragged = bookmarkViews[dragIndex] 77 | const adjustedHoverIndex = 78 | hoverIndex < dragIndex ? hoverIndex : hoverIndex - 1 79 | bookmarkViews.splice(dragIndex, 1) 80 | bookmarkViews.splice(adjustedHoverIndex, 0, dragged) 81 | } 82 | return ( 83 | 84 | {bookmarkViews.length > 0 ? bookmarkViews : } 85 | 86 | ) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/BookmarkList/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const Bookmarks = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | flex: 1; 8 | max-height: 100%; 9 | overflow-y: scroll; 10 | ` 11 | 12 | export const NoBookmarkText = styled.div` 13 | font-size: 16pt; 14 | flex: 1; 15 | ` 16 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Branch/index.tsx: -------------------------------------------------------------------------------- 1 | import { BranchId } from '@essex/redux-dag-history' 2 | import * as classnames from 'classnames' 3 | import { BranchType } from '../../interfaces' 4 | import * as React from 'react' 5 | import BranchProfile from '../BranchProfile' 6 | import { Container, Details, Name, ProfileContainer } from './styled' 7 | 8 | export interface BranchProps { 9 | id?: BranchId 10 | label: string 11 | branchType: BranchType 12 | startsAt: number 13 | endsAt: number 14 | currentBranchStart?: number 15 | currentBranchEnd?: number 16 | maxDepth: number 17 | activeStateIndex?: number 18 | onClick?: React.EventHandler> 19 | active?: boolean 20 | } 21 | 22 | const Branch: React.StatelessComponent = ({ 23 | label, 24 | branchType, 25 | startsAt, 26 | endsAt, 27 | currentBranchStart, 28 | currentBranchEnd, 29 | maxDepth, 30 | activeStateIndex, 31 | onClick, 32 | active, 33 | }) => ( 34 | (onClick ? onClick(e) : undefined)}> 35 | 36 | 45 | 46 |
47 | {label} 48 |
49 |
50 | ) 51 | 52 | export default Branch 53 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Branch/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | const black = '#000' 5 | const white = '#fff' 6 | const highlight = '#f93' 7 | const charcoal = '#5d6d7e' 8 | 9 | export const Container = styled.div` 10 | border-top: 1px solid ${white}; 11 | min-height: 30px; 12 | min-width: 250px; 13 | position: relative; 14 | overflow: hidden; 15 | 16 | &:not(:hover) { 17 | border-right: 3px solid transparent; 18 | } 19 | 20 | &:hover { 21 | border-right: 3px solid ${highlight}; 22 | } 23 | ` 24 | 25 | export const Details = styled.div` 26 | align-items: center; 27 | align-self: stretch; 28 | display: flex; 29 | height: 100%; 30 | left: 0; 31 | padding-left: 5px; 32 | position: absolute; 33 | top: 0; 34 | width: 100%; 35 | overflow: hidden; 36 | cursor: pointer; 37 | ` 38 | export const Name = styled.div` 39 | font-weight: 300; 40 | 41 | &:active { 42 | color: ${black}; 43 | } 44 | 45 | &:not(.active) { 46 | color: ${charcoal}; 47 | } 48 | ` 49 | 50 | export const ProfileContainer = styled.div` 51 | align-items: center; 52 | align-self: stretch; 53 | display: flex; 54 | height: 100%; 55 | left: 0; 56 | position: absolute; 57 | top: 0; 58 | width: 100%; 59 | overflow: hidden; 60 | ` 61 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/BranchList/index.tsx: -------------------------------------------------------------------------------- 1 | import { BranchId } from '@essex/redux-dag-history' 2 | import * as React from 'react' 3 | import Branch, { BranchProps } from '../Branch' 4 | import { Container, Branches } from './styled' 5 | 6 | export interface BranchListProps { 7 | activeBranch?: BranchId 8 | branches: BranchProps[] 9 | onBranchClick?: (branchId: BranchId) => void 10 | style?: any 11 | } 12 | 13 | const BranchList: React.StatelessComponent = ({ 14 | activeBranch, 15 | branches, 16 | onBranchClick, 17 | style, 18 | }) => { 19 | const branchViews = branches.map(s => ( 20 | (onBranchClick ? onBranchClick(s.id) : undefined)} 24 | active={activeBranch === s.id} 25 | /> 26 | )) 27 | return ( 28 | 29 | {branchViews} 30 | 31 | ) 32 | } 33 | 34 | export default BranchList 35 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/BranchList/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: flex-end; 8 | overflow: hidden; 9 | ` 10 | 11 | export const Branches = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | overflow-x: hidden; 15 | ` 16 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/BranchProfile/calculateSpans.ts: -------------------------------------------------------------------------------- 1 | import { initialSpans, insertSpan, Span } from '../../util/spans' 2 | 3 | const isNumber = (d: number) => !isNaN(d) && d !== null 4 | const convertArg = (arg: number | undefined, offset: number) => 5 | arg !== null && arg !== undefined ? arg - offset : arg 6 | 7 | export default function calculateSpans( 8 | type: string, 9 | max: number, 10 | startArg: number, 11 | endArg: number, 12 | branchStartArg: number, 13 | branchEndArg: number, 14 | activeIndexArg: number, 15 | ) { 16 | const offset = startArg 17 | const start = startArg - offset 18 | const end = endArg - offset 19 | const branchStart = branchStartArg - offset 20 | const branchEnd = branchEndArg - offset 21 | const activeIndex = convertArg(activeIndexArg, offset) 22 | 23 | // Set up the initial spans ranges; culling out empty ranges 24 | let spans = initialSpans(start, max) 25 | const isCurrent = type === 'current' 26 | spans = insertSpan(spans, new Span(start, end + 1, 'UNRELATED_UNIQUE')) 27 | 28 | if ( 29 | isNumber(branchStart) && 30 | isNumber(branchEnd) && 31 | branchStart >= 0 && 32 | branchEnd >= 0 33 | ) { 34 | const color = isCurrent ? 'CURRENT' : 'ANCESTOR' 35 | const span = new Span(branchStart, branchEnd + 1, color) 36 | spans = insertSpan(spans, span) 37 | } 38 | 39 | if (isNumber(activeIndex)) { 40 | const color = isCurrent ? 'CURRENT_ACTIVE' : 'LEGACY_ACTIVE' 41 | const span = new Span(activeIndex, activeIndex + 1, color) 42 | spans = insertSpan(spans, span) 43 | } 44 | 45 | return spans 46 | } 47 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/BranchProfile/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import colors from '../../palette' 3 | import calculateSpans from './calculateSpans' 4 | import { Container } from './styled' 5 | import { BranchType } from '../../interfaces' 6 | /** 7 | * Gets styling for the branch-info spans 8 | */ 9 | function infoSpanStyle(flex: number, backgroundColor: string) { 10 | if (flex === 0) { 11 | return { display: 'none' } 12 | } 13 | return { backgroundColor, flex } 14 | } 15 | 16 | export interface BranchProfileProps { 17 | start: number 18 | end: number 19 | branchStart?: number 20 | branchEnd?: number 21 | max: number 22 | activeStateIndex?: number 23 | type: BranchType 24 | } 25 | 26 | const BranchProfile: React.StatelessComponent = ({ 27 | type, 28 | start, 29 | end, 30 | max, 31 | branchStart, 32 | branchEnd, 33 | activeStateIndex: activeIndex, 34 | }) => { 35 | const infoSpans = calculateSpans( 36 | type, 37 | max, 38 | start, 39 | end, 40 | branchStart, 41 | branchEnd, 42 | activeIndex, 43 | ) 44 | const spanComponents = infoSpans 45 | .map(s => infoSpanStyle(s.length, colors[s.type.toString()])) 46 | .map((style, index) =>
) 47 | 48 | return {spanComponents} 49 | } 50 | 51 | export default BranchProfile 52 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/BranchProfile/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const Container = styled.div` 5 | align-items: stretch; 6 | align-self: stretch; 7 | display: flex; 8 | flex: 1; 9 | min-height: 10px; 10 | min-width: 100px; 11 | ` 12 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Continuation/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Container } from './styled' 3 | 4 | function getContinuationText(count: number) { 5 | const sanecount = Math.abs(count || 0) 6 | let result 7 | if (sanecount <= 1) { 8 | result = '' 9 | } else if (sanecount < 99) { 10 | result = `${sanecount}` 11 | } else { 12 | result = '99+' 13 | } 14 | return result 15 | } 16 | 17 | function handleClick(handler: Function) { 18 | if (handler) { 19 | return (evt: React.MouseEvent) => { 20 | handler(evt) 21 | evt.stopPropagation() 22 | } 23 | } 24 | } 25 | 26 | export interface ContinuationProps { 27 | count?: number 28 | color?: string 29 | onClick?: Function 30 | } 31 | 32 | const Continuation: React.StatelessComponent = ({ 33 | count, 34 | color: backgroundColor, 35 | onClick, 36 | }) => { 37 | const continuationText = getContinuationText(count) 38 | return ( 39 | 40 | {continuationText} 41 | 42 | ) 43 | } 44 | 45 | export default Continuation 46 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Continuation/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | const grey = '#797d7f' 5 | 6 | export const Container = styled.div` 7 | border: 2px solid ${grey}; 8 | border-radius: 10px; 9 | box-sizing: content-box; 10 | display: inline-block; 11 | font-family: monospace; 12 | font-size: 8pt; 13 | font-weight: 600; 14 | height: 14px; 15 | min-width: 14px; 16 | line-height: 14px; 17 | min-height: 14px; 18 | padding: 2px; 19 | text-align: center; 20 | ` 21 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/DiscoveryTrail/calculateSpans.ts: -------------------------------------------------------------------------------- 1 | import { initialSpans, insertSpan, Span } from '../../util/spans' 2 | 3 | const isNumber = (d: number | null | undefined) => !isNaN(d) && d !== null 4 | 5 | export default function calculateSpans( 6 | depth: number, 7 | highlight: number, 8 | leadIn: number, 9 | active: boolean, 10 | ): Span[] { 11 | if (depth < 0) { 12 | return [] 13 | } 14 | let spans = initialSpans(0, depth, 'empty') 15 | if (isNumber(leadIn) && leadIn !== 0) { 16 | spans = insertSpan(spans, new Span(depth - leadIn, depth + 1, 'leadin')) 17 | } 18 | if (active && depth > 1) { 19 | const type = active ? 'highlighted' : 'highlightedInactive' 20 | const highlightDepth = highlight || depth 21 | spans = insertSpan( 22 | spans, 23 | new Span(highlightDepth, highlightDepth + 1, type), 24 | ) 25 | } 26 | return spans 27 | } 28 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/DiscoveryTrail/index.tsx: -------------------------------------------------------------------------------- 1 | import * as classnames from 'classnames' 2 | import * as debug from 'debug' 3 | import * as React from 'react' 4 | import calculateSpans from './calculateSpans' 5 | import calculateIndex from '../../util/calculateIndex' 6 | import { StatePager, BookmarkPager, PagerState } from './styled' 7 | 8 | const log = debug('dag-history-component:components:DiscoveryTrail') 9 | 10 | export interface DiscoveryTrailProps { 11 | /** 12 | * If true, renders in vertical mode 13 | */ 14 | vertical?: boolean 15 | 16 | /** 17 | * If true, then the path this trail represents is currently active 18 | */ 19 | active?: boolean 20 | 21 | /** 22 | * If true, this trail represents a bookmark list, and does not bother with lead-ins 23 | */ 24 | bookmark?: boolean 25 | 26 | /** 27 | * The highlighted index 28 | */ 29 | highlight?: number 30 | 31 | /** 32 | * The number of states in the trail 33 | */ 34 | depth: number 35 | 36 | /** 37 | * The number of lead-in states to show 38 | */ 39 | leadIn?: number 40 | 41 | /** 42 | * An event handler for when the state pager has clicked an item 43 | */ 44 | onIndexClicked?: Function 45 | 46 | /** 47 | * If not full width, renders curved ends 48 | */ 49 | fullWidth?: boolean 50 | 51 | style?: React.CSSProperties 52 | } 53 | 54 | export default class DiscoveryTrail extends React.Component< 55 | DiscoveryTrailProps, 56 | {} 57 | > { 58 | public static defaultProps = { 59 | vertical: false, 60 | active: false, 61 | depth: 0, 62 | fullWidth: false, 63 | } 64 | 65 | private containerDiv: HTMLDivElement 66 | 67 | private handleClick(evt: React.MouseEvent) { 68 | const { target } = event 69 | const { onIndexClicked } = this.props 70 | const bounds = this.containerDiv.getBoundingClientRect() 71 | 72 | const x = evt.clientX - bounds.left 73 | const width = bounds.right - bounds.left 74 | const percent = x / width 75 | const index = calculateIndex(this.props.depth + 1, percent) 76 | if (onIndexClicked) { 77 | log('selected index %s', index) 78 | onIndexClicked(index) 79 | } 80 | } 81 | 82 | private get pagerComponent() { 83 | return this.props.bookmark ? BookmarkPager : StatePager 84 | } 85 | 86 | private get pagerClass() { 87 | const { vertical, fullWidth: isFullWidth } = this.props 88 | const horizontal = !vertical 89 | const radiusEdges = !isFullWidth 90 | return classnames({ 91 | vertical, 92 | horizontal, 93 | radiusEdges, 94 | }) 95 | } 96 | 97 | public render() { 98 | const { 99 | vertical, 100 | depth, 101 | highlight, 102 | leadIn, 103 | active, 104 | style, 105 | bookmark: isBookmark, 106 | fullWidth: isFullWidth, 107 | } = this.props 108 | const spans = calculateSpans(depth, highlight, leadIn, active) 109 | const Pager = this.pagerComponent 110 | const spanTags = spans.map((s, index) => ( 111 | 119 | )) 120 | 121 | return ( 122 | this.handleClick(e)} 126 | innerRef={(e: HTMLDivElement) => (this.containerDiv = e)} 127 | > 128 | {spanTags} 129 | 130 | ) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/DiscoveryTrail/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | const minPagerWidth = '3px' 5 | const emptyStateColor = 'white' 6 | const highlightedStateInactiveBookmarkColor = 'white' 7 | const highlightedStateColor = '#bbf0d1' 8 | const leadInStateColor = '#dbf7e7' 9 | const emptyBookmarkColor = 'white' 10 | const highlightedBookmarkInactiveBookmarkColor = '#ebc6ab' 11 | const highlightedBookmarkColor = '#555' 12 | const leadInBookmarkColor = '#f5e2d4' 13 | const statePagerBorderColor = 'none' 14 | 15 | export const PagerState = styled.div`` 16 | 17 | const Pager = ( 18 | borderColor: string, 19 | highlightedColor: string, 20 | highlightedInactiveColor: string, 21 | leadInColor: string, 22 | emptyColor: string, 23 | ) => 24 | styled.div` 25 | display: flex; 26 | &.radiusEdges { 27 | border-radius: 4px; 28 | } 29 | 30 | ${PagerState} { 31 | &.highlighted { 32 | background-color: ${highlightedColor}; 33 | } 34 | &.highlightedInactive { 35 | background-color: ${highlightedInactiveColor}; 36 | } 37 | &.leadin { 38 | background-color: ${leadInColor}; 39 | } 40 | &.empty { 41 | background-color: ${emptyColor}; 42 | } 43 | } 44 | 45 | &.horizontal { 46 | ${PagerState} { 47 | &.startItem { 48 | border-bottom-left-radius: 4px; 49 | border-top-left-radius: 4px; 50 | } 51 | &.endItem { 52 | border-bottom-right-radius: 4px; 53 | border-top-right-radius: 4px; 54 | } 55 | } 56 | 57 | border: ${borderColor}; 58 | flex-direction: row; 59 | min-height: ${minPagerWidth}; 60 | width: 100%; 61 | } 62 | 63 | &.vertical { 64 | ${PagerState} { 65 | &.start { 66 | border-top-right-radius: 4px; 67 | border-top-left-radius: 4px; 68 | } 69 | &.end { 70 | border-bottom-left-radius: 4px; 71 | border-bottom-right-radius: 4px; 72 | } 73 | } 74 | border-left: ${borderColor}; 75 | flex-direction: column; 76 | height: 100%; 77 | min-width: ${minPagerWidth}; 78 | } 79 | ` 80 | 81 | export const StatePager = Pager( 82 | statePagerBorderColor, 83 | highlightedStateColor, 84 | highlightedStateInactiveBookmarkColor, 85 | leadInStateColor, 86 | emptyStateColor, 87 | ) 88 | 89 | export const BookmarkPager = Pager( 90 | statePagerBorderColor, 91 | highlightedBookmarkColor, 92 | highlightedBookmarkInactiveBookmarkColor, 93 | leadInBookmarkColor, 94 | emptyBookmarkColor, 95 | ) 96 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/ExpandCollapseToggle/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | const MdExpandMore = require('react-icons/lib/md/expand-more') 3 | const MdExpandLess = require('react-icons/lib/md/expand-less') 4 | 5 | export interface ExpandCollapseToggleProps { 6 | isExpanded?: boolean 7 | onClick: Function 8 | } 9 | 10 | const ExpandCollapseToggle: React.StatelessComponent< 11 | ExpandCollapseToggleProps 12 | > = ({ isExpanded, onClick }) => 13 | isExpanded ? ( 14 | 15 | ) : ( 16 | 17 | ) 18 | 19 | export default ExpandCollapseToggle 20 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/History/BranchedHistoryView/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BranchId, 3 | StateId, 4 | ActionCreators as DagHistoryActions, 5 | } from '@essex/redux-dag-history' 6 | import * as React from 'react' 7 | import { TransitionGroup, CSSTransition } from 'react-transition-group' 8 | import { connect } from 'react-redux' 9 | import { bindActionCreators } from 'redux' 10 | import { Bookmark } from '../../../interfaces' 11 | import * as DagComponentActions from '../../../state/actions/creators' 12 | import ExpandCollapseToggle from '../../ExpandCollapseToggle' 13 | import Transport from '../../Transport' 14 | import { HistoryContainerSharedProps } from '../interfaces' 15 | import BranchListContainer, { 16 | BranchListContainerProps, 17 | } from '../../BranchListContainer' 18 | import StateListContainer, { 19 | StateListContainerProps, 20 | } from '../../StateListContainer' 21 | import { 22 | BranchListContainerEl, 23 | HistoryControlBar, 24 | HistoryControlBarTitle, 25 | } from '../styled' 26 | import HistoryContainer from '../../HistoryContainer' 27 | 28 | export interface BranchedHistoryViewDispatchProps { 29 | onStateSelect: (id: StateId) => void 30 | onAddBookmark: Function 31 | onBranchSelect: (id: BranchId) => void 32 | onRemoveBookmark: Function 33 | onToggleBranchContainer: Function 34 | onPinState: Function 35 | onRenameBranch: Function 36 | } 37 | 38 | export interface BranchedHistoryViewProps 39 | extends BranchedHistoryViewDispatchProps, 40 | HistoryContainerSharedProps { 41 | bookmarks: Bookmark[] 42 | } 43 | 44 | const BranchedHistoryView: React.StatelessComponent< 45 | BranchedHistoryViewProps 46 | > = props => { 47 | const { branchContainerExpanded, onToggleBranchContainer } = props 48 | const branchList = branchContainerExpanded ? ( 49 | 53 | 54 | 55 | ) : null 56 | 57 | return ( 58 | 59 | 60 | 61 | 62 | Paths 63 | 67 | 68 | {branchList} 69 | 70 | 71 | ) 72 | } 73 | 74 | export default connect<{}, BranchedHistoryViewDispatchProps, {}>( 75 | () => ({}), 76 | dispatch => 77 | bindActionCreators( 78 | { 79 | onStateSelect: DagHistoryActions.jumpToState, 80 | onAddBookmark: DagComponentActions.addBookmark, 81 | onBranchSelect: DagHistoryActions.jumpToBranch, 82 | onRemoveBookmark: DagComponentActions.removeBookmark, 83 | onToggleBranchContainer: DagComponentActions.toggleBranchContainer, 84 | onPinState: DagComponentActions.pinState, 85 | onRenameBranch: DagHistoryActions.renameBranch, 86 | }, 87 | dispatch, 88 | ), 89 | )(BranchedHistoryView) 90 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/History/ChronologicalHistoryView/index.tsx: -------------------------------------------------------------------------------- 1 | import { ActionCreators as DagHistoryActions } from '@essex/redux-dag-history' 2 | import * as React from 'react' 3 | import { connect } from 'react-redux' 4 | import { bindActionCreators } from 'redux' 5 | import { Bookmark } from '../../../interfaces' 6 | import * as DagComponentActions from '../../../state/actions/creators' 7 | import { HistoryContainerSharedProps } from '../interfaces' 8 | import StateListContainer from '../../StateListContainer' 9 | import HistoryContainer from '../../HistoryContainer' 10 | 11 | export interface ChronologicalHistoryViewDispatchProps { 12 | onBranchSelect: Function 13 | onStateSelect: Function 14 | onAddBookmark: Function 15 | onRemoveBookmark: Function 16 | onToggleBranchContainer: Function 17 | onPinState: Function 18 | onUndo: Function 19 | onRedo: Function 20 | onSkipToStart: Function 21 | onSkipToEnd: Function 22 | onRenameBranch: Function 23 | } 24 | 25 | export interface ChronologicalHistoryViewOwnProps 26 | extends HistoryContainerSharedProps { 27 | bookmarks: Bookmark[] 28 | } 29 | 30 | export interface ChronologicalHistoryViewProps 31 | extends ChronologicalHistoryViewDispatchProps, 32 | ChronologicalHistoryViewOwnProps {} 33 | 34 | const ChronologicalHistoryView: React.StatelessComponent< 35 | ChronologicalHistoryViewProps 36 | > = props => ( 37 | 38 | 44 | 45 | ) 46 | 47 | export default connect< 48 | {}, 49 | ChronologicalHistoryViewDispatchProps, 50 | ChronologicalHistoryViewOwnProps 51 | >( 52 | () => ({}), 53 | dispatch => 54 | bindActionCreators( 55 | { 56 | onBranchSelect: DagHistoryActions.jumpToLatestOnBranch, 57 | onStateSelect: DagHistoryActions.jumpToState, 58 | onAddBookmark: DagComponentActions.addBookmark, 59 | onRemoveBookmark: DagComponentActions.removeBookmark, 60 | onUndo: DagHistoryActions.undo, 61 | onRedo: DagHistoryActions.redo, 62 | onSkipToStart: DagHistoryActions.skipToStart, 63 | onSkipToEnd: DagHistoryActions.skipToEnd, 64 | onRenameBranch: DagHistoryActions.renameBranch, 65 | onPinState: DagComponentActions.pinState, 66 | onToggleBranchContainer: DagComponentActions.toggleBranchContainer, 67 | }, 68 | dispatch, 69 | ), 70 | )(ChronologicalHistoryView) 71 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/History/SwitchingHistoryView/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import { Bookmark, HistoryType } from '../../../interfaces' 5 | import { selectHistoryType } from '../../../state/actions/creators' 6 | import OptionDropdown from '../../OptionDropdown' 7 | import { HistoryContainerSharedProps } from '../interfaces' 8 | import BranchedHistoryView, { 9 | BranchedHistoryViewProps, 10 | } from '../BranchedHistoryView' 11 | import ChronologicalHistoryView from '../ChronologicalHistoryView' 12 | import { 13 | DropdownOptionRow, 14 | ColumnContainer, 15 | DropdownContainer, 16 | } from '../styled' 17 | const BranchedIcon = require('react-icons/lib/go/git-branch') 18 | const ChronologicalIcon = require('react-icons/lib/go/three-bars') 19 | 20 | const viewLabels: { [key: string]: string } = { 21 | branched: 'Branched', 22 | chronological: 'Chronological', 23 | } 24 | 25 | const viewIcons: { [key: string]: JSX.Element } = { 26 | branched: , 27 | chronological: , 28 | } 29 | 30 | export interface HistoryViewDispatchProps { 31 | onSelectHistoryType: Function 32 | } 33 | 34 | export interface HistoryViewOwnProps extends HistoryContainerSharedProps { 35 | bookmarks: Bookmark[] 36 | } 37 | 38 | export interface HistoryViewProps 39 | extends HistoryViewDispatchProps, 40 | HistoryViewOwnProps {} 41 | 42 | const HistoryView: React.StatelessComponent = props => { 43 | const { historyType, onSelectHistoryType } = props 44 | const historyTypeOption = (name: string) => ({ 45 | label: viewLabels[name], 46 | element: ( 47 | 48 | {viewLabels[name]} 49 | {viewIcons[name]} 50 | 51 | ), 52 | onClick: () => onSelectHistoryType(name), 53 | }) 54 | const label = viewLabels[historyType] 55 | 56 | return ( 57 | 58 | 59 | 67 | 68 | {historyType === HistoryType.CHRONOLOGICAL ? ( 69 | 70 | ) : ( 71 | 72 | )} 73 | 74 | ) 75 | } 76 | 77 | export default connect<{}, HistoryViewDispatchProps, HistoryViewOwnProps>( 78 | () => ({}), 79 | dispatch => 80 | bindActionCreators( 81 | { 82 | onSelectHistoryType: selectHistoryType, 83 | }, 84 | dispatch, 85 | ), 86 | )(HistoryView) 87 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/History/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { BranchId, DagHistory, StateId } from '@essex/redux-dag-history' 2 | import { Bookmark, HistoryType, ComponentView } from '../../interfaces' 3 | 4 | export interface HistoryContainerSharedProps { 5 | history: DagHistory 6 | pinnedStateId?: StateId 7 | mainView: ComponentView 8 | historyType: HistoryType 9 | dragIndex?: number 10 | hoverIndex?: number 11 | bookmarkEditIndex?: number 12 | branchContainerExpanded?: boolean 13 | selectedBookmark?: number 14 | selectedBookmarkDepth?: number 15 | isPlayingBack?: boolean 16 | bookmarks: Bookmark[] 17 | 18 | getSourceFromState: Function 19 | bookmarksEnabled?: boolean 20 | 21 | /** 22 | * ControlBar Configuration Properties 23 | * 24 | * If the controlBar property is undefined, the control bar will be disabled. 25 | */ 26 | controlBar?: { 27 | /** 28 | * A handler to save the history tree out. This is handled by clients. 29 | */ 30 | onSaveHistory: Function 31 | 32 | /** 33 | * A handler to retrieve the history tree. This is handled by clients 34 | */ 35 | onLoadHistory: Function 36 | 37 | /** 38 | * A function that emits a Promise that confirms the clear-history operation. 39 | */ 40 | onConfirmClear: Function 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/History/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const BranchListContainerEl = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: space-between; 8 | margin-top: 3px; 9 | max-height: 35%; 10 | overflow: hidden; 11 | ` 12 | 13 | export const HistoryControlBarTitle = styled.div` 14 | font-weight: bold; 15 | margin-left: 10px; 16 | ` 17 | 18 | export const HistoryControlBar = styled.div` 19 | align-items: center; 20 | cursor: default; 21 | display: flex; 22 | flex-direction: row; 23 | justify-content: space-between; 24 | min-height: 24px; 25 | ` 26 | 27 | export const DropdownOptionRow = styled.div` 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | ` 32 | 33 | export const ColumnContainer = styled.div` 34 | display: flex; 35 | flex: 1; 36 | flex-direction: column; 37 | max-height: 100%; 38 | height: 100%; 39 | ` 40 | 41 | export const DropdownContainer = styled.div` 42 | display: flex; 43 | flex-direction: row; 44 | justify-content: flex-end; 45 | ` 46 | 47 | export const PlaybackContainer = styled.div` 48 | display: flex; 49 | flex-direction: column; 50 | flex: 1; 51 | max-height: 100%; 52 | ` 53 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/HistoryContainer/index.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | const Flexy = styled.div` 5 | display: flex; 6 | flex: 1; 7 | max-height: 100%; 8 | ` 9 | 10 | const HistoryContainer = Flexy.extend` 11 | align-content: space-between; 12 | flex-direction: column; 13 | -ms-overflow-style: none; 14 | overflow: -moz-scrollbars-none; 15 | overflow: hidden; 16 | ::-webkit-scrollbar { 17 | display: none; 18 | } 19 | ` 20 | 21 | export default HistoryContainer 22 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/HistoryTabs/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import OptionDropdown from '../OptionDropdown' 3 | import { Container, OptionMenu, Tabs } from './styled' 4 | import { Tab, TabList, TabPanel } from 'react-tabs' 5 | 6 | const viewNameToIndex: { [key: string]: number } = { 7 | history: 0, 8 | storyboarding: 1, 9 | } 10 | const indexToViewName = ['history', 'storyboarding'] 11 | 12 | function handleTabSelector(onTabSelect: (view: string) => void) { 13 | return (index: number) => onTabSelect(indexToViewName[index]) 14 | } 15 | 16 | export interface HistoryContainerProps { 17 | selectedTab: string 18 | onTabSelect: (view: string) => void 19 | historyView: JSX.Element 20 | storyboardingView: JSX.Element 21 | bookmarksEnabled?: boolean 22 | controlBarEnabled?: boolean 23 | onSaveClicked: Function 24 | onClearClicked: Function 25 | onLoadClicked: Function 26 | } 27 | 28 | const HistoryContainer: React.StatelessComponent = ({ 29 | onTabSelect, 30 | selectedTab, 31 | historyView, 32 | storyboardingView, 33 | controlBarEnabled, 34 | bookmarksEnabled, 35 | onSaveClicked, 36 | onLoadClicked, 37 | onClearClicked, 38 | }) => { 39 | if (!bookmarksEnabled) { 40 | return historyView 41 | } 42 | const controlBar = controlBarEnabled && ( 43 | 44 | 51 | 52 | ) 53 | 54 | return ( 55 | 56 | {controlBar} 57 | 61 | 62 | History 63 | Bookmarks 64 | 65 | {historyView} 66 | {storyboardingView} 67 | 68 | 69 | ) 70 | } 71 | 72 | export default HistoryContainer 73 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/HistoryTabs/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import 'react-tabs/style/react-tabs.css' 3 | import styled, { StyledComponentClass } from 'styled-components' 4 | import { 5 | Tab as TabRaw, 6 | TabList as TabListRaw, 7 | TabPanel as TabPanelRaw, 8 | Tabs as TabsRaw, 9 | TabProps, 10 | TabListProps, 11 | TabPanelProps, 12 | TabsProps, 13 | } from 'react-tabs' 14 | 15 | const Flexy = styled.div` 16 | display: flex; 17 | flex: 1; 18 | max-height: 100%; 19 | ` 20 | 21 | export const Container = Flexy.extend` 22 | align-content: space-between; 23 | flex-direction: column; 24 | -ms-overflow-style: none; 25 | overflow: -moz-scrollbars-none; 26 | overflow: hidden; 27 | 28 | ::-webkit-scrollbar { 29 | display: none; 30 | } 31 | ` 32 | 33 | export const Tab = styled(TabRaw)` 34 | margin: 0 !important; 35 | ` 36 | 37 | export const TabList = styled(TabListRaw)` 38 | margin: 0 !important; 39 | ` 40 | 41 | export const TabPanel = styled(TabPanelRaw)` 42 | display: flex; 43 | max-height: 100%; 44 | height: 100%; 45 | ` 46 | 47 | export const Tabs = styled(TabsRaw)` 48 | display: flex; 49 | flex: 1; 50 | flex-direction: column; 51 | max-height: 100%; 52 | ` 53 | 54 | export const OptionMenu = styled.div` 55 | min-height: 24px; 56 | position: absolute; 57 | right: 0; 58 | top: 0; 59 | ` 60 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/HistoryTypeDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { bindActionCreators, Dispatch } from 'redux' 3 | import { connect } from 'react-redux' 4 | import { HistoryType } from '../../interfaces' 5 | import { DropdownOptionRow } from './styled' 6 | import OptionDropdown from '../OptionDropdown' 7 | import { selectHistoryType } from '../../state/actions/creators' 8 | 9 | const BranchedIcon = require('react-icons/lib/go/git-branch') 10 | const ChronologicalIcon = require('react-icons/lib/go/three-bars') 11 | 12 | const viewLabels: { [key: string]: string } = { 13 | branched: 'Branched', 14 | chronological: 'Chronological', 15 | } 16 | 17 | const viewIcons: { [key: string]: JSX.Element } = { 18 | branched: , 19 | chronological: , 20 | } 21 | 22 | export interface HistoryTypeDropdownOwnProps { 23 | historyType: HistoryType 24 | } 25 | 26 | export interface HistoryTypeDropdownDispatchProps { 27 | onSelectionChanged: (type: HistoryType) => void 28 | } 29 | 30 | export interface HistoryTypeDropdownProps 31 | extends HistoryTypeDropdownOwnProps, 32 | HistoryTypeDropdownDispatchProps {} 33 | 34 | const historyTypeOption = ( 35 | name: HistoryType, 36 | onSelectionChanged: (type: HistoryType) => void, 37 | ) => ({ 38 | label: viewLabels[name], 39 | element: ( 40 | 41 | {viewLabels[name]} 42 | {viewIcons[name]} 43 | 44 | ), 45 | onClick: () => onSelectionChanged(name), 46 | }) 47 | 48 | export const HistoryTypeDropdown: React.StatelessComponent< 49 | HistoryTypeDropdownProps 50 | > = ({ historyType, onSelectionChanged }) => ( 51 | 59 | ) 60 | 61 | export default connect< 62 | {}, 63 | HistoryTypeDropdownDispatchProps, 64 | HistoryTypeDropdownOwnProps 65 | >( 66 | () => ({}), 67 | (dispatch: Dispatch) => 68 | bindActionCreators( 69 | { 70 | onSelectionChanged: selectHistoryType, 71 | }, 72 | dispatch, 73 | ), 74 | )(HistoryTypeDropdown) 75 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/HistoryTypeDropdown/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const DropdownOptionRow = styled.div` 5 | display: flex; 6 | flex-direction: row; 7 | justify-content: space-between; 8 | ` 9 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/OptionDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { 3 | DropdownTrigger, 4 | TriggerContent, 5 | DropdownContent, 6 | OptionList, 7 | ListItem, 8 | } from './styled' 9 | const MdMoreVert = require('react-icons/lib/md/more-vert') 10 | const { default: Dropdown } = require('react-simple-dropdown') 11 | 12 | export interface Option { 13 | element?: JSX.Element 14 | label?: string 15 | onClick: Function 16 | } 17 | 18 | export interface OptionDropdownProps { 19 | label?: string 20 | icon?: JSX.Element 21 | options?: Option[] 22 | } 23 | 24 | export interface OptionDropdownState { 25 | show: boolean 26 | } 27 | 28 | export default class OptionDropdown extends React.Component< 29 | OptionDropdownProps, 30 | OptionDropdownState 31 | > { 32 | public static defaultProps = { 33 | options: [] as Option[], 34 | } 35 | 36 | constructor(props: OptionDropdownProps) { 37 | super(props) 38 | this.state = { show: false } 39 | } 40 | 41 | public render() { 42 | const { label, icon, options } = this.props 43 | let result = null 44 | if (options.length === 0) { 45 | result = label ?
{label}
: null 46 | } else { 47 | const triggerLabel = label ?
{label}
: null 48 | let triggerIcon = icon 49 | if (!triggerIcon && !label) { 50 | triggerIcon = 51 | } 52 | if (triggerIcon) { 53 | triggerIcon =
{triggerIcon}
54 | } 55 | 56 | const optionClicked = (onClick: Function) => { 57 | onClick() 58 | this.setState({ show: false }) 59 | } 60 | 61 | const triggerClicked = () => { 62 | this.setState({ show: true }) 63 | } 64 | 65 | result = ( 66 | 67 | triggerClicked()}> 68 | 69 | {triggerLabel} 70 | {triggerIcon} 71 | 72 | 73 | 74 | 75 | {options.map( 76 | ( 77 | { element: optionElement, label: optionLabel, onClick }, 78 | index, 79 | ) => ( 80 | optionClicked(onClick)} 83 | > 84 | {optionElement || optionLabel} 85 | 86 | ), 87 | )} 88 | 89 | 90 | 91 | ) 92 | } 93 | return result 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/OptionDropdown/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | const { 4 | default: DropdownRaw, 5 | DropdownTrigger: DropdownTriggerRaw, 6 | DropdownContent: DropdownContentRaw, 7 | } = require('react-simple-dropdown') 8 | import 'react-simple-dropdown/styles/Dropdown.css' 9 | 10 | const white = '#fff' 11 | const black = '#000' 12 | const shadow = 'rgba(0, 0, 0, .2)' 13 | 14 | export const DropdownTrigger = styled(DropdownTriggerRaw)` 15 | color: ${black}; 16 | text-decoration: none; 17 | cursor: pointer; 18 | 19 | .dropown-icon-wrapper { 20 | color: ${black}; 21 | display: flex; 22 | justify-content: flex-end; 23 | margin-left: 10px; 24 | } 25 | ` 26 | 27 | export const TriggerContent = styled.div` 28 | display: flex; 29 | flex-direction: row; 30 | margin: 0 5px 5px 0; 31 | ` 32 | 33 | export const DropdownContent = styled(DropdownContentRaw)` 34 | background-color: ${white}; 35 | box-shadow: 0 8px 16px 0 ${shadow}; 36 | display: none; 37 | margin-top: 5px; 38 | right: 5px; 39 | min-width: 160px; 40 | position: relative; 41 | z-index: 1; 42 | ` 43 | 44 | export const OptionList = styled.ul` 45 | list-style: none; 46 | margin: 0; 47 | padding: 0; 48 | ` 49 | 50 | export const ListItem = styled.li` 51 | margin: 0; 52 | padding: 10px 14px; 53 | cursor: pointer; 54 | ` 55 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/PlaybackPane/index.tsx: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug' 2 | import * as React from 'react' 3 | import DiscoveryTrail from '../DiscoveryTrail' 4 | import { Container, Paged, Pane } from './styled' 5 | const log = debug('dag-history-component:components:PlaybackPane') 6 | 7 | export interface PlaybackPaneProps { 8 | text: string 9 | depth: number 10 | highlight: number 11 | bookmarkDepth: number 12 | bookmarkHighlight: number 13 | bookmarkNumLeadInStates?: number 14 | onDiscoveryTrailIndexClicked?: Function 15 | } 16 | 17 | const PlaybackPane: React.StatelessComponent = props => { 18 | const { 19 | text, 20 | depth, 21 | highlight, 22 | bookmarkDepth, 23 | bookmarkHighlight, 24 | bookmarkNumLeadInStates, 25 | onDiscoveryTrailIndexClicked, 26 | } = props 27 | return ( 28 | 29 | 30 | 31 |

{text}

32 |
33 | 41 |
42 | onDiscoveryTrailIndexClicked(idx)} 49 | /> 50 |
51 | ) 52 | } 53 | 54 | export default PlaybackPane 55 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/PlaybackPane/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex: 1; 7 | flex-direction: column; 8 | ` 9 | export const Paged = styled.div` 10 | display: flex; 11 | flex: 1; 12 | flex-direction: row; 13 | ` 14 | 15 | export const Pane = styled.div` 16 | display: flex; 17 | flex: 1; 18 | justify-content: left; 19 | padding: 10px; 20 | ` 21 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/State/index.tsx: -------------------------------------------------------------------------------- 1 | import * as classnames from 'classnames' 2 | import * as React from 'react' 3 | import { TransitionGroup, CSSTransition } from 'react-transition-group' 4 | import colors from '../../palette' 5 | import Continuation from '../Continuation' 6 | import { StateProps } from './interfaces' 7 | import { BranchType } from '../../interfaces' 8 | import { 9 | Container, 10 | Detail, 11 | Source, 12 | Name, 13 | ContinuationContainer, 14 | } from './styled' 15 | 16 | const Bookmark = require('react-icons/lib/io/bookmark') 17 | 18 | const coloring: { [key: string]: { [key: string]: string } } = { 19 | current: { 20 | active: colors.CURRENT_ACTIVE, 21 | nonactive: colors.CURRENT, 22 | }, 23 | legacy: { 24 | active: colors.LEGACY_ACTIVE, 25 | nonactive: colors.ANCESTOR, 26 | }, 27 | unrelated: { 28 | active: colors.UNRELATED, 29 | nonactive: colors.UNRELATED_UNIQUE, 30 | }, 31 | } 32 | 33 | function getBackgroundColor(branchType: BranchType, active: boolean) { 34 | return coloring[branchType.toString()][active ? 'active' : 'nonactive'] 35 | } 36 | 37 | function continuationColor(active: boolean, pinned: boolean) { 38 | let result = colors.CONT_BLANK 39 | if (pinned) { 40 | result = colors.CONT_PINNED 41 | } else if (active) { 42 | result = colors.CONT_ACTIVE 43 | } 44 | return result 45 | } 46 | 47 | export default class State extends React.PureComponent { 48 | public static defaultProps = { 49 | showContinuation: true, 50 | label: '', 51 | branchType: BranchType.CURRENT, 52 | numChildren: 0, 53 | } 54 | 55 | public render() { 56 | const { 57 | id, 58 | source, 59 | label, 60 | branchType, 61 | active, 62 | renderBookmarks, 63 | bookmarked, 64 | numChildren, 65 | onClick, 66 | onContinuationClick, 67 | onBookmarkClick, 68 | successor, 69 | pinned, 70 | showContinuation, 71 | } = this.props 72 | const backgroundColor = getBackgroundColor(branchType, active) 73 | 74 | const handleClick = () => { 75 | if (onClick) { 76 | onClick(id) 77 | } 78 | } 79 | 80 | const handleContinuationClick = () => { 81 | if (onContinuationClick) { 82 | onContinuationClick(id) 83 | } 84 | } 85 | 86 | const handleBookmarkClick = () => { 87 | if (onBookmarkClick) { 88 | onBookmarkClick(id) 89 | } 90 | } 91 | 92 | const continuation = showContinuation ? ( 93 | 97 | 98 | handleContinuationClick()} 102 | /> 103 | 104 | 105 | ) : null 106 | 107 | const bookmark = renderBookmarks ? ( 108 | handleBookmarkClick()} 112 | /> 113 | ) : null 114 | 115 | const marginLeftValue = successor ? 30 : 0 116 | 117 | return ( 118 | handleClick()} 125 | > 126 | {continuation} 127 | 128 | {source || ''} 129 | {label || ''} 130 | 131 | {bookmark} 132 | 133 | ) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/State/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { DagHistory, StateId, DagGraph } from '@essex/redux-dag-history' 2 | import { BranchType } from '../../interfaces' 3 | 4 | export interface StateProps { 5 | id: StateId 6 | active?: boolean 7 | renderBookmarks?: boolean 8 | pinned?: boolean 9 | successor?: boolean 10 | state?: any 11 | source?: string 12 | label: string 13 | numChildren?: number 14 | bookmarked?: boolean 15 | showContinuation?: boolean 16 | branchType?: BranchType 17 | onBookmarkClick?: (state: StateId) => void 18 | onClick?: (state: StateId) => void 19 | onContinuationClick?: (state: StateId) => void 20 | style?: any 21 | } 22 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/State/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | const white = '#fff' 5 | const highlight = '#f93' 6 | const black = '#000' 7 | const charcoal = '#5d6d7e' 8 | 9 | export const ContinuationContainer = styled.div` 10 | overflow: hidden; 11 | display: flex; 12 | justify-content: center; 13 | ` 14 | 15 | export const Container = styled.div` 16 | font-family: sans-serif; 17 | align-items: center; 18 | align-self: stretch; 19 | border-top: 1px solid $white; 20 | cursor: pointer; 21 | display: flex; 22 | height: 30px; 23 | min-height: 30px; 24 | min-width: 250px; 25 | padding: 8px; 26 | position: relative; 27 | overflow: hidden; 28 | transition: all 250ms ease; 29 | 30 | &.successor { 31 | margin-left: 35px; 32 | } 33 | 34 | &:not(:hover) { 35 | border-right: 3px solid transparent; 36 | } 37 | 38 | &:hover { 39 | border-right: 3px solid ${highlight}; 40 | } 41 | ` 42 | 43 | export const Detail = styled.div` 44 | flex: 1; 45 | display: flex; 46 | flex-direction: column; 47 | margin-left: 8px; 48 | overflow: hidden; 49 | ` 50 | 51 | export const Source = styled.div` 52 | font-family: sans-serif; 53 | font-size: 8pt; 54 | overflow: hidden; 55 | 56 | &:active { 57 | color: ${black}; 58 | } 59 | 60 | &:not(.active) { 61 | color: ${charcoal}; 62 | } 63 | ` 64 | 65 | export const Name = styled.div` 66 | font-weight: 300; 67 | font-size: 11pt; 68 | white-space: nowrap; 69 | overflow: hidden; 70 | 71 | &:active { 72 | color: ${black}; 73 | } 74 | 75 | &:not(.active) { 76 | color: ${charcoal}; 77 | } 78 | ` 79 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/StateList/index.tsx: -------------------------------------------------------------------------------- 1 | import { StateId } from '@essex/redux-dag-history' 2 | import * as React from 'react' 3 | import { TransitionGroup, CSSTransition } from 'react-transition-group' 4 | import isNumber from '../../util/isNumber' 5 | import State from '../State' 6 | import { StateProps } from '../State/interfaces' 7 | import { Container } from './styled' 8 | 9 | export interface StateListProps { 10 | states: StateProps[] 11 | activeStateId?: StateId 12 | onStateClick?: Function 13 | onStateContinuationClick?: Function 14 | renderBookmarks?: boolean 15 | onStateBookmarkClick?: Function 16 | } 17 | 18 | export default class StateList extends React.Component { 19 | private containerDiv: HTMLDivElement 20 | 21 | public componentDidUpdate() { 22 | this.containerDiv.scrollTop = this.containerDiv.scrollHeight 23 | } 24 | 25 | public render() { 26 | const { 27 | states, 28 | activeStateId, 29 | onStateClick, 30 | onStateContinuationClick, 31 | renderBookmarks, 32 | onStateBookmarkClick, 33 | } = this.props 34 | 35 | const handleClick = (id: StateId) => { 36 | if (onStateClick) { 37 | onStateClick(id) 38 | } 39 | } 40 | 41 | const handleContinuationClick = (id: StateId) => { 42 | if (onStateContinuationClick) { 43 | onStateContinuationClick(id) 44 | } 45 | } 46 | 47 | const handleBookmarkClick = (id: StateId) => { 48 | if (onStateBookmarkClick) { 49 | onStateBookmarkClick(id) 50 | } 51 | } 52 | 53 | const stateViews = states.map((s, index) => ( 54 | 59 | handleClick(id)} 63 | onContinuationClick={id => handleContinuationClick(id)} 64 | onBookmarkClick={id => handleBookmarkClick(id)} 65 | /> 66 | 67 | )) 68 | return ( 69 | (this.containerDiv = e)}> 70 | {stateViews} 71 | 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/StateList/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex: 1; 7 | max-height: 100%; 8 | flex-direction: column; 9 | width: 100%; 10 | overflow-x: hidden; 11 | overflow-y: scroll; 12 | ` 13 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/StoryboardingView/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DagHistory, 3 | DagGraph, 4 | ActionCreators as DagHistoryActions, 5 | } from '@essex/redux-dag-history' 6 | import * as React from 'react' 7 | import { connect } from 'react-redux' 8 | import { bindActionCreators } from 'redux' 9 | import * as Actions from '../../state/actions/creators' 10 | import Transport from '../Transport' 11 | import makeActions from '../../util/BookmarkActions' 12 | import BookmarkListContainer, { 13 | BookmarkListContainerProps, 14 | } from './BookmarkListContainer' 15 | import { Bookmark as BookmarkData } from '../../interfaces' 16 | import Bookmark from '../../util/Bookmark' 17 | import HistoryContainer from '../HistoryContainer' 18 | 19 | export interface StoryboardingViewDispatchProps { 20 | onStartPlayback: Function 21 | onStopPlayback: () => void 22 | onSelectBookmarkDepth: Function 23 | } 24 | export interface StoryboardingViewOwnProps { 25 | history: DagHistory 26 | selectedBookmark?: number 27 | selectedBookmarkDepth?: number 28 | bookmarks: Bookmark[] 29 | dragIndex?: number 30 | hoverIndex?: number 31 | bookmarkEditIndex?: number 32 | } 33 | 34 | export interface StoryboardingViewProps 35 | extends StoryboardingViewDispatchProps, 36 | StoryboardingViewOwnProps {} 37 | 38 | const StoryboardingView: React.StatelessComponent< 39 | StoryboardingViewProps & BookmarkListContainerProps 40 | > = props => { 41 | const { 42 | history, 43 | bookmarks, 44 | onStartPlayback, 45 | onStopPlayback, 46 | selectedBookmark, 47 | selectedBookmarkDepth, 48 | onSelectBookmarkDepth, 49 | dragIndex, 50 | hoverIndex, 51 | bookmarkEditIndex, 52 | } = props 53 | 54 | const { 55 | handleStepBack, 56 | handleStepForward, 57 | handleNextBookmark, 58 | handlePreviousBookmark, 59 | handleStepBackUnbounded, 60 | } = makeActions( 61 | selectedBookmark, 62 | selectedBookmarkDepth, 63 | history, 64 | bookmarks, 65 | onSelectBookmarkDepth, 66 | ) 67 | 68 | const onPlay = () => { 69 | if (bookmarks.length === 0) { 70 | return 71 | } 72 | const bookmark = new Bookmark(bookmarks[0], new DagGraph(history.graph)) 73 | const initialDepth = bookmark.startingDepth() 74 | const stateId = bookmark.commitPath[bookmark.sanitizeDepth(initialDepth)] 75 | 76 | onStartPlayback({ initialDepth, stateId }) 77 | } 78 | 79 | return ( 80 | 81 | 82 | 90 | 91 | ) 92 | } 93 | 94 | export default connect< 95 | {}, 96 | StoryboardingViewDispatchProps, 97 | StoryboardingViewOwnProps 98 | >( 99 | () => ({}), 100 | dispatch => 101 | bindActionCreators( 102 | { 103 | onStartPlayback: Actions.startPlayback, 104 | onStopPlayback: Actions.stopPlayback, 105 | onSelectBookmarkDepth: Actions.selectBookmarkDepth, 106 | }, 107 | dispatch, 108 | ), 109 | )(StoryboardingView) 110 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Transport/buttons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const LeftIcon = require('react-icons/lib/md/keyboard-arrow-left') 4 | const RightIcon = require('react-icons/lib/md/keyboard-arrow-right') 5 | const UpIcon = require('react-icons/lib/fa/caret-up') 6 | const DownIcon = require('react-icons/lib/fa/caret-down') 7 | const PlayIcon = require('react-icons/lib/md/play-arrow') 8 | const StopIcon = require('react-icons/lib/md/stop') 9 | 10 | export interface ButtonProps { 11 | onClick: () => void 12 | size: number 13 | } 14 | 15 | export const StepBack: React.StatelessComponent = ({ 16 | onClick, 17 | size, 18 | }) => 19 | 20 | export const StepForward: React.StatelessComponent = ({ 21 | onClick, 22 | size, 23 | }) => 24 | 25 | export const JumpBack: React.StatelessComponent = ({ 26 | onClick, 27 | size, 28 | }) => 29 | 30 | export const JumpForward: React.StatelessComponent = ({ 31 | onClick, 32 | size, 33 | }) => 34 | 35 | export interface PlayPauseProps { 36 | size: number 37 | show: boolean 38 | playing: boolean 39 | onStop: () => void 40 | onPlay: () => void 41 | } 42 | 43 | export const PlayPause: React.StatelessComponent = ({ 44 | size, 45 | onStop, 46 | onPlay, 47 | show, 48 | playing, 49 | }) => { 50 | if (!show) { 51 | return
52 | } 53 | return playing ? ( 54 | 55 | ) : ( 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Transport/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Container } from './styled' 3 | import { 4 | StepBack, 5 | StepForward, 6 | JumpBack, 7 | JumpForward, 8 | PlayPause, 9 | } from './buttons' 10 | const debounce = require('lodash/debounce') 11 | const DEFAULT_ICON_SIZE = 30 12 | 13 | export interface TransportCallbackProps { 14 | onPlay?: Function 15 | onStop?: Function 16 | onBack?: Function 17 | onForward?: Function 18 | onStepForward?: Function 19 | onStepBack?: Function 20 | } 21 | 22 | export interface TransportProps extends TransportCallbackProps { 23 | iconSize?: number 24 | playing?: boolean 25 | showPlay?: boolean 26 | 27 | /** 28 | * When this is true, then stepping downwards means stepping into older states 29 | */ 30 | reverseVertical?: boolean 31 | } 32 | 33 | const keys = { 34 | SPACE: 32, 35 | ESC: 27, 36 | UP: 38, 37 | DOWN: 40, 38 | LEFT: 37, 39 | RIGHT: 39, 40 | } 41 | 42 | class Transport extends React.Component { 43 | public static defaultProps = { 44 | iconSize: DEFAULT_ICON_SIZE, 45 | playing: false, 46 | showPlay: true, 47 | } 48 | 49 | private handlers: TransportCallbackProps = null 50 | 51 | public play() { 52 | if (this.handlers.onPlay) { 53 | this.handlers.onPlay() 54 | } 55 | } 56 | 57 | public stepBack() { 58 | if (this.handlers.onStepBack) { 59 | this.handlers.onStepBack() 60 | } 61 | } 62 | 63 | public stepForward() { 64 | if (this.handlers.onStepForward) { 65 | this.handlers.onStepForward() 66 | } 67 | } 68 | 69 | public stop() { 70 | if (this.handlers.onStop) { 71 | this.handlers.onStop() 72 | } 73 | } 74 | 75 | public back() { 76 | if (this.props.reverseVertical) { 77 | this.goForward() 78 | } else { 79 | this.goBack() 80 | } 81 | } 82 | 83 | public forward() { 84 | if (this.props.reverseVertical) { 85 | this.goBack() 86 | } else { 87 | this.goForward() 88 | } 89 | } 90 | 91 | private goBack() { 92 | if (this.handlers.onBack) { 93 | this.handlers.onBack() 94 | } 95 | } 96 | 97 | private goForward() { 98 | if (this.handlers.onForward) { 99 | this.handlers.onForward() 100 | } 101 | } 102 | 103 | public render() { 104 | const { iconSize, playing, showPlay } = this.props 105 | 106 | this.handlers = { 107 | onPlay: this.props.onPlay, 108 | onStop: this.props.onStop, 109 | onBack: this.props.onBack, 110 | onForward: this.props.onForward, 111 | onStepForward: this.props.onStepForward, 112 | onStepBack: this.props.onStepBack, 113 | } 114 | 115 | return ( 116 | undefined} // allows event bubbling 119 | > 120 |
121 | this.stepBack()} /> 122 | this.stepForward()} /> 123 |
124 | this.play()} 126 | onStop={() => this.stop()} 127 | playing={playing} 128 | show={showPlay} 129 | size={iconSize} 130 | /> 131 |
132 | this.back()} /> 133 | this.forward()} /> 134 |
135 |
136 | ) 137 | } 138 | } 139 | export default Transport 140 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/Transport/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const Container = styled.div` 5 | align-items: center; 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: space-around; 9 | 10 | /** 11 | * Remove the blue focus-outline 12 | */ 13 | outline: none; 14 | ` 15 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/createHistoryContainer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BranchId, 3 | DagHistory, 4 | StateId, 5 | ActionCreators as DagHistoryActions, 6 | } from '@essex/redux-dag-history' 7 | import * as React from 'react' 8 | import { connect } from 'react-redux' 9 | import { bindActionCreators, Dispatch } from 'redux' 10 | import { Bookmark, ComponentView, HistoryType } from '../interfaces' 11 | import * as Actions from '../state/actions/creators' 12 | import HistoryComponent from './History' 13 | 14 | export interface HistoryContainerStateProps { 15 | history?: DagHistory 16 | mainView?: ComponentView 17 | historyType?: HistoryType 18 | branchContainerExpanded?: boolean 19 | pinnedStateId?: StateId 20 | selectedBookmark?: number 21 | selectedBookmarkDepth?: number 22 | bookmarks?: Bookmark[] 23 | getSourceFromState: Function 24 | } 25 | 26 | export interface HistoryContainerOwnProps { 27 | bookmarksEnabled?: boolean 28 | 29 | /** 30 | * ControlBar Configuration Properties 31 | */ 32 | controlBar?: { 33 | /** 34 | * A handler to save the history tree out. This is handled by clients. 35 | */ 36 | onSaveHistory: Function 37 | 38 | /** 39 | * A handler to retrieve the history tree. This is handled by clients 40 | */ 41 | onLoadHistory: Function 42 | 43 | /** 44 | * A function that emits a Promise that confirms the clear-history operation. 45 | */ 46 | onConfirmClear: Function 47 | } 48 | } 49 | 50 | export interface HistoryContainerProps 51 | extends HistoryContainerStateProps, 52 | HistoryContainerOwnProps {} 53 | 54 | const HistoryContainer: React.StatelessComponent< 55 | HistoryContainerProps 56 | // TODO: Hacky, figure out the typings here 57 | > = props => 58 | 59 | // HACK: these unused expressions sidesteps webpack's tree-shaking from pruning these imports 60 | bindActionCreators // tslint:disable-line no-unused-expression 61 | Actions // tslint:disable-line no-unused-expression 62 | DagHistoryActions // tslint:disable-line no-unused-expression 63 | 64 | const mapDispatchToProps = (dispatch: Dispatch) => 65 | bindActionCreators( 66 | { 67 | onClear: DagHistoryActions.clear, 68 | onLoad: DagHistoryActions.load, 69 | onSelectMainView: Actions.selectMainView, 70 | onSelectState: DagHistoryActions.jumpToState, 71 | onToggleBranchContainer: Actions.toggleBranchContainer, 72 | onStartPlayback: Actions.startPlayback, 73 | onStopPlayback: Actions.stopPlayback, 74 | onSelectBookmarkDepth: Actions.selectBookmarkDepth, 75 | }, 76 | dispatch, 77 | ) 78 | 79 | export default function createHistoryContainer( 80 | getMiddlewareState: Function, 81 | getComponentState: Function, 82 | getSourceFromState: Function, 83 | ) { 84 | const mapStateToProps = (state: any) => { 85 | const middleware = getMiddlewareState(state) 86 | const component = getComponentState(state) 87 | return { 88 | getSourceFromState, 89 | 90 | // State from the redux-dag-history middleware 91 | history: middleware, 92 | pinnedStateId: component.pinnedState.id, 93 | 94 | // State from the dag-history-component 95 | bookmarks: component.bookmarks, 96 | mainView: component.views.mainView, 97 | historyType: component.views.historyType, 98 | dragIndex: component.dragDrop.sourceIndex, 99 | dragKey: component.dragDrop.sourceKey, 100 | hoverIndex: component.dragDrop.hoverIndex, 101 | bookmarkEditIndex: component.bookmarkEdit.editIndex, 102 | branchContainerExpanded: component.views.branchContainerExpanded, 103 | selectedBookmark: component.playback.bookmark, 104 | selectedBookmarkDepth: component.playback.depth, 105 | isPlayingBack: component.playback.isPlayingBack, 106 | } 107 | } 108 | return connect( 109 | mapStateToProps, 110 | mapDispatchToProps, 111 | )(HistoryContainer) 112 | } 113 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/components/styled.ts: -------------------------------------------------------------------------------- 1 | import * as react from 'react' 2 | import styled, { StyledComponentClass } from 'styled-components' 3 | 4 | export const StateListContainer = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | flex: 2; 8 | overflow-x: hidden; 9 | overflow-y: scroll; 10 | ` 11 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/index.ts: -------------------------------------------------------------------------------- 1 | import createHistoryContainer from './components/createHistoryContainer' 2 | import History from './components/History' 3 | import * as state from './state' 4 | export { History, createHistoryContainer, state } 5 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { StateId } from '@essex/redux-dag-history' 2 | 3 | export interface Bookmark { 4 | stateId: StateId 5 | name: string 6 | data: { [name: string]: any } 7 | } 8 | 9 | export enum BranchType { 10 | CURRENT = 'current', 11 | LEGACY = 'legacy', 12 | UNRELATED = 'unrelated', 13 | } 14 | 15 | export enum HistoryType { 16 | BRANCHED = 'branched', 17 | CHRONOLOGICAL = 'chronological', 18 | } 19 | 20 | export enum ComponentView { 21 | HISTORY = 'history', 22 | STORYBOARDING = 'storyboarding', 23 | } 24 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/palette.ts: -------------------------------------------------------------------------------- 1 | const colors: { [key: string]: string } = { 2 | // Profile Colors 3 | NONE: 'transparent', 4 | CURRENT_ACTIVE: '#70A5D3', 5 | LEGACY_ACTIVE: '#70A5D3', 6 | CURRENT: '#AFCDE7', 7 | ANCESTOR: '#e0f4fb', 8 | UNRELATED_UNIQUE: '#EFEFEF', 9 | UNRELATED: '#FDFDFD', 10 | 11 | // Continuation Cloors 12 | CONT_ACTIVE: '#D5F5E3', 13 | CONT_BLANK: 'white', 14 | CONT_PINNED: '#97E6BA', 15 | } 16 | export default colors 17 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/Configuration.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationImpl } from '@essex/redux-dag-history' 2 | import { ComponentView, HistoryType } from '../interfaces' 3 | import { ComponentConfiguration, RawComponentConfiguration } from './interfaces' 4 | 5 | export default class ComponentConfigurationImpl extends ConfigurationImpl 6 | implements ComponentConfiguration { 7 | constructor(rawConfig: RawComponentConfiguration = {}) { 8 | super(rawConfig as any) 9 | } 10 | 11 | private get config() { 12 | return this.rawConfig as ComponentConfiguration 13 | } 14 | 15 | public get initialViewState() { 16 | return this.config.initialViewState || {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/actions/types.ts: -------------------------------------------------------------------------------- 1 | function action(name: string): string { 2 | return `DAG_HISTORY_COMPONENT_${name.toUpperCase()}` 3 | } 4 | 5 | // Action Types 6 | export const SELECT_MAIN_VIEW = action('SELECT_MAIN_VIEW') 7 | export const SELECT_HISTORY_TYPE = action('SELECT_HISTORY_TYPE') 8 | export const SELECT_BOOKMARK_DEPTH = action('SELECT_BOOKMARK_DEPTH') 9 | export const TOGGLE_BRANCH_CONTAINER = action('TOGGLE_BRANCH_CONTAINER') 10 | export const START_PLAYBACK = action('START_PLAYBACK') 11 | export const STOP_PLAYBACK = action('STOP_PLAYBACK') 12 | export const BOOKMARK_DRAG_START = action('BOOKMARK_DRAG_START') 13 | export const BOOKMARK_DRAG_HOVER = action('BOOKMARK_DRAG_HOVER') 14 | export const BOOKMARK_DRAG_DROP = action('BOOKMARK_DRAG_DROP') 15 | export const BOOKMARK_DRAG_CANCEL = action('BOOKMARK_DRAG_CANCEL') 16 | export const BOOKMARK_EDIT = action('BOOKMARK_EDIT') 17 | export const BOOKMARK_EDIT_DONE = action('BOOKMARK_EDIT_DONE') 18 | 19 | /** 20 | * Pin a state to navigate successors 21 | */ 22 | export const PIN_STATE = action('PIN_STATE') 23 | 24 | /** 25 | * Add a new state bookmark 26 | */ 27 | export const ADD_BOOKMARK = action('add_bookmark') 28 | 29 | /** 30 | * Remove a state bookmark 31 | */ 32 | export const REMOVE_BOOKMARK = action('remove_bookmark') 33 | 34 | /** 35 | * Rename a state bookmark 36 | */ 37 | export const RENAME_BOOKMARK = action('rename_bookmark') 38 | 39 | /** 40 | * Change a state bookmark 41 | */ 42 | export const CHANGE_BOOKMARK = action('change_bookmark') 43 | 44 | /** 45 | * Move a bookmark to a different position 46 | */ 47 | export const MOVE_BOOKMARK = action('move_bookmark') 48 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/index.ts: -------------------------------------------------------------------------------- 1 | import * as Actions from './actions/creators' 2 | import * as ActionTypes from './actions/types' 3 | import Configuration from './Configuration' 4 | import reducer from './reducers' 5 | 6 | export { Actions, ActionTypes, reducer } 7 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from '@essex/redux-dag-history' 2 | import { ComponentView, HistoryType } from '../interfaces' 3 | 4 | export interface ComponentConfiguration extends Configuration { 5 | initialViewState: { 6 | mainView?: ComponentView 7 | historyType?: HistoryType 8 | branchContainerExpanded?: boolean 9 | } 10 | } 11 | 12 | export type RawComponentConfiguration = { 13 | [P in keyof ComponentConfiguration]?: ComponentConfiguration[P] 14 | } 15 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/bookmarkEdit.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux-actions' 2 | import { Configuration } from '@essex/redux-dag-history' 3 | import { 4 | BOOKMARK_EDIT, 5 | MOVE_BOOKMARK, 6 | BOOKMARK_EDIT_DONE, 7 | } from '../actions/types' 8 | import isHistoryAction from './isHistoryAction' 9 | import makeReducer from './configurableReducer' 10 | 11 | export interface State { 12 | editIndex: number 13 | } 14 | 15 | const INITIAL_STATE: State = { 16 | editIndex: undefined, 17 | } 18 | 19 | function reducer( 20 | state: State = INITIAL_STATE, 21 | action: ReduxActions.Action, 22 | config: Configuration, 23 | ) { 24 | if (action.type === BOOKMARK_EDIT) { 25 | // Edit a bookmark 26 | return { editIndex: action.payload } 27 | } else if ( 28 | action.type === MOVE_BOOKMARK && 29 | action.payload.from === state.editIndex 30 | ) { 31 | // If the user is moving the currently edited bookmark, update the editIndex 32 | return { editIndex: action.payload.to } 33 | } else if ( 34 | action.type === BOOKMARK_EDIT_DONE || 35 | (!isHistoryAction(action) && config.actionFilter(action.type)) 36 | ) { 37 | // Clear the edit state 38 | return INITIAL_STATE 39 | } 40 | return state 41 | } 42 | 43 | export default makeReducer(reducer) 44 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/bookmarks.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from '@essex/redux-dag-history' 2 | import { 3 | ADD_BOOKMARK, 4 | CHANGE_BOOKMARK, 5 | MOVE_BOOKMARK, 6 | REMOVE_BOOKMARK, 7 | RENAME_BOOKMARK, 8 | } from '../actions/types' 9 | import { Bookmark } from '../../interfaces' 10 | import { Action } from 'redux-actions' 11 | 12 | export type State = Bookmark[] 13 | export const INITIAL_STATE: State = [] 14 | 15 | export default function reduce( 16 | state: State = INITIAL_STATE, 17 | action: ReduxActions.Action, 18 | ) { 19 | switch (action.type) { 20 | case ADD_BOOKMARK: { 21 | const { name, stateId, data } = action.payload 22 | const newBookmark = { name, stateId, data: data || {} } 23 | return [...state, newBookmark] 24 | } 25 | 26 | case REMOVE_BOOKMARK: { 27 | const stateId = action.payload 28 | return state.filter(element => element.stateId !== stateId) 29 | } 30 | 31 | case RENAME_BOOKMARK: { 32 | const { stateId, name: newName } = action.payload 33 | return state.map(b => { 34 | const isTarget = b.stateId === stateId 35 | const name = isTarget ? newName : b.name 36 | return { ...b, name } 37 | }) 38 | } 39 | 40 | case CHANGE_BOOKMARK: { 41 | const { stateId, name, data } = action.payload 42 | return state.map(b => { 43 | const isTarget = b.stateId === stateId 44 | return isTarget ? { name, stateId, data } : b 45 | }) 46 | } 47 | 48 | case MOVE_BOOKMARK: { 49 | const { from, to } = action.payload 50 | const bookmarks = [...state] 51 | const moved = bookmarks[from] 52 | if (from !== to) { 53 | bookmarks.splice(from, 1) 54 | bookmarks.splice(to, 0, moved) 55 | } 56 | return bookmarks 57 | } 58 | 59 | default: { 60 | return state 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/configurableReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux-actions' 2 | 3 | export type ConfigurableReducer = ( 4 | state: STATE, 5 | action: Action, 6 | config: CONFIG, 7 | ) => STATE 8 | 9 | export default function makeReducer( 10 | reducer: ConfigurableReducer, 11 | ) { 12 | return (config: CONFIG) => (state: STATE, action: Action) => 13 | reducer(state, action, config) 14 | } 15 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/dragDrop.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux-actions' 2 | import { Configuration } from '@essex/redux-dag-history' 3 | import { 4 | BOOKMARK_DRAG_CANCEL, 5 | BOOKMARK_DRAG_DROP, 6 | BOOKMARK_DRAG_HOVER, 7 | BOOKMARK_DRAG_START, 8 | } from '../actions/types' 9 | import makeReducer from './configurableReducer' 10 | 11 | export interface State { 12 | sourceIndex?: number 13 | sourceKey?: string 14 | hoverIndex?: number 15 | } 16 | 17 | export const INITIAL_STATE: State = { 18 | sourceIndex: undefined, 19 | sourceKey: undefined, 20 | hoverIndex: undefined, 21 | } 22 | 23 | function reducer( 24 | state: State = INITIAL_STATE, 25 | action: ReduxActions.Action, 26 | config: Configuration, 27 | ) { 28 | let result = state 29 | if (action.type === BOOKMARK_DRAG_START) { 30 | result = { 31 | ...state, 32 | sourceIndex: action.payload.index, 33 | sourceKey: action.payload.key, 34 | } 35 | } else if (action.type === BOOKMARK_DRAG_HOVER) { 36 | const hoverIndex = action.payload.index 37 | result = { 38 | ...state, 39 | hoverIndex, 40 | } 41 | } else if (action.type === BOOKMARK_DRAG_DROP) { 42 | result = INITIAL_STATE 43 | } else if (action.type === BOOKMARK_DRAG_CANCEL) { 44 | result = INITIAL_STATE 45 | } else if ( 46 | action.type.indexOf('DAG_HISTORY_') !== 0 && 47 | config.actionFilter(action.type) 48 | ) { 49 | result = INITIAL_STATE 50 | } 51 | return result 52 | } 53 | 54 | export default makeReducer(reducer) 55 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import * as redux from 'redux' 2 | import bookmarkEdit from './bookmarkEdit' 3 | import bookmarks from './bookmarks' 4 | import dragDrop from './dragDrop' 5 | import pinnedState from './pinnedState' 6 | import playback from './playback' 7 | import views from './views' 8 | import { ComponentConfiguration } from '../interfaces' 9 | 10 | export default function createReducer(config: ComponentConfiguration) { 11 | return redux.combineReducers({ 12 | bookmarkEdit: bookmarkEdit(config), 13 | dragDrop: dragDrop(config), 14 | views: views(config), 15 | playback: playback(config), 16 | pinnedState: pinnedState(config), 17 | bookmarks, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/isHistoryAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux-actions' 2 | 3 | export default function isHistoryAction( 4 | action: ReduxActions.Action, 5 | ): boolean { 6 | return !!(action && action.type && action.type.startsWith('DAG_HISTORY_')) 7 | } 8 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/pinnedState.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, StateId } from '@essex/redux-dag-history' 2 | import { PIN_STATE } from '../actions/types' 3 | import { Action } from 'redux-actions' 4 | import makeReducer from './configurableReducer' 5 | 6 | export interface State { 7 | id?: StateId 8 | } 9 | 10 | export const INITIAL_STATE = {} 11 | 12 | function reduce( 13 | state: State = INITIAL_STATE, 14 | action: ReduxActions.Action, 15 | config: Configuration, 16 | ) { 17 | let result = state 18 | if (action.type === PIN_STATE) { 19 | const stateId = action.payload 20 | result = { 21 | ...state, 22 | id: state.id === stateId ? undefined : stateId, 23 | } 24 | } else if ( 25 | action.type.indexOf('DAG_HISTORY_') !== 0 && 26 | config.actionFilter(action.type) 27 | ) { 28 | result = INITIAL_STATE 29 | } 30 | return result 31 | } 32 | 33 | export default makeReducer(reduce) 34 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/playback.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from '@essex/redux-dag-history' 2 | import { 3 | SELECT_BOOKMARK_DEPTH, 4 | START_PLAYBACK, 5 | STOP_PLAYBACK, 6 | } from '../actions/types' 7 | import isHistoryAction from './isHistoryAction' 8 | import { Action } from 'redux-actions' 9 | import makeReducer from './configurableReducer' 10 | 11 | export interface State { 12 | isPlayingBack: boolean 13 | bookmark?: number 14 | depth?: number 15 | } 16 | 17 | export const INITIAL_STATE: State = { 18 | isPlayingBack: false, 19 | bookmark: undefined, 20 | depth: undefined, 21 | } 22 | 23 | function reduce( 24 | state: State = INITIAL_STATE, 25 | action: ReduxActions.Action, 26 | config: Configuration, 27 | ) { 28 | let result = state 29 | if (action.type === START_PLAYBACK) { 30 | const { initialDepth } = action.payload 31 | result = { 32 | ...state, 33 | isPlayingBack: true, 34 | bookmark: 0, 35 | depth: initialDepth, 36 | } 37 | } else if (action.type === STOP_PLAYBACK) { 38 | result = INITIAL_STATE 39 | } else if (action.type === SELECT_BOOKMARK_DEPTH) { 40 | const { depth, bookmarkIndex } = action.payload 41 | result = { 42 | ...state, 43 | bookmark: bookmarkIndex === undefined ? state.bookmark : bookmarkIndex, 44 | depth, 45 | } 46 | } else if (!isHistoryAction(action) && config.actionFilter(action.type)) { 47 | // Insertable actions clear the pinned state 48 | result = { 49 | ...state, 50 | isPlayingBack: false, 51 | bookmark: undefined, 52 | depth: undefined, 53 | } 54 | } 55 | return result 56 | } 57 | 58 | export default makeReducer(reduce) 59 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/state/reducers/views.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SELECT_HISTORY_TYPE, 3 | SELECT_MAIN_VIEW, 4 | TOGGLE_BRANCH_CONTAINER, 5 | } from '../actions/types' 6 | import { HistoryType, ComponentView } from '../../interfaces' 7 | import { ComponentConfiguration } from '../interfaces' 8 | import isHistoryAction from './isHistoryAction' 9 | import { Action } from 'redux-actions' 10 | import makeReducer from './configurableReducer' 11 | 12 | export interface State { 13 | mainView: ComponentView 14 | historyType: HistoryType 15 | branchContainerExpanded: boolean 16 | } 17 | 18 | export const INITIAL_STATE: State = { 19 | mainView: ComponentView.HISTORY, 20 | historyType: HistoryType.BRANCHED, 21 | branchContainerExpanded: true, 22 | } 23 | 24 | function reduce( 25 | state: State, 26 | action: ReduxActions.Action, 27 | config: ComponentConfiguration, 28 | ) { 29 | if (!state) { 30 | state = { 31 | ...INITIAL_STATE, 32 | ...config.initialViewState, 33 | } 34 | } 35 | let result = state 36 | if (action.type === SELECT_MAIN_VIEW) { 37 | result = { 38 | ...state, 39 | mainView: action.payload, 40 | } 41 | } else if (action.type === SELECT_HISTORY_TYPE) { 42 | result = { 43 | ...state, 44 | historyType: action.payload, 45 | } 46 | } else if (action.type === TOGGLE_BRANCH_CONTAINER) { 47 | result = { 48 | ...state, 49 | branchContainerExpanded: !state.branchContainerExpanded, 50 | } 51 | } else if (!isHistoryAction(action) && config.actionFilter(action.type)) { 52 | // Insertable actions clear the pinned state 53 | result = { 54 | ...state, 55 | mainView: ComponentView.HISTORY, 56 | } 57 | } 58 | return result 59 | } 60 | 61 | export default makeReducer(reduce) 62 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/util/Bookmark.ts: -------------------------------------------------------------------------------- 1 | import { DagGraph } from '@essex/redux-dag-history' 2 | import { Bookmark as BookmarkData } from '../interfaces' 3 | /** 4 | * Represents bookmark data for our bookmark 5 | */ 6 | export default class Bookmark { 7 | /** 8 | * Constructs a bookmark 9 | * @param bookmark - The bookmark item in state 10 | * @param graph - The source graph for the bookmark 11 | */ 12 | constructor(private bookmark: BookmarkData, private graph: DagGraph) {} 13 | 14 | public get numLeadInStates() { 15 | const bookmark = this.bookmark 16 | return bookmark && bookmark.data && bookmark.data.numLeadInStates 17 | } 18 | 19 | public get annotation() { 20 | const bookmark = this.bookmark 21 | return bookmark && bookmark.data && bookmark.data.annotation 22 | } 23 | 24 | public get name() { 25 | const bookmark = this.bookmark 26 | return bookmark.name 27 | } 28 | 29 | public get stateId() { 30 | const bookmark = this.bookmark 31 | return bookmark.stateId 32 | } 33 | 34 | public get slideText() { 35 | return this.annotation || this.name || 'No slide data' 36 | } 37 | 38 | public get commitPath() { 39 | return this.graph.shortestCommitPath(this.stateId) 40 | } 41 | 42 | public get presentedPath() { 43 | return this.commitPath.slice(this.hiddenPathLength) 44 | } 45 | 46 | public get commitPathLength() { 47 | return this.commitPath.length 48 | } 49 | 50 | public get presentedPathLength() { 51 | // Lead in + final state 52 | return (this.numLeadInStates || 0) + 1 53 | } 54 | 55 | public get hiddenPathLength() { 56 | return this.commitPathLength - this.presentedPathLength 57 | } 58 | 59 | public startingDepth() { 60 | const isLeadInDefined = this.numLeadInStates !== undefined 61 | return isLeadInDefined ? this.hiddenPathLength : undefined 62 | } 63 | 64 | public isDepthAtEnd(depth: number) { 65 | return depth === undefined || depth >= this.commitPathLength - 1 66 | } 67 | 68 | public isDepthAtStart(depth: number) { 69 | if (depth === 0) { 70 | return true 71 | } 72 | if (depth === undefined) { 73 | return this.startingDepth === undefined 74 | } 75 | 76 | let startingDepth = this.startingDepth() 77 | if (startingDepth === undefined) { 78 | startingDepth = this.commitPath.length - 1 79 | } 80 | return depth === startingDepth 81 | } 82 | 83 | public getStateAtDepth(depth?: number) { 84 | if (depth === undefined) { 85 | return this.commitPath[this.commitPath.length - 1] 86 | } 87 | return this.commitPath[depth] 88 | } 89 | 90 | public sanitizeDepth(depth?: number) { 91 | if (depth !== undefined) { 92 | return depth 93 | } 94 | const commitPathLength = this.commitPathLength 95 | return commitPathLength - 1 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/util/BookmarkActions.ts: -------------------------------------------------------------------------------- 1 | import { StateId, DagGraph } from '@essex/redux-dag-history' 2 | import * as debug from 'debug' 3 | import { Bookmark as BookmarkData } from '../interfaces' 4 | import Bookmark from './Bookmark' 5 | 6 | const log = debug('dag-history-component:BookmarkActions') 7 | 8 | export default function makeActions( 9 | rawSelectedBookmark: number, 10 | rawSelectedBookmarkDepth: number, 11 | history: any, 12 | bookmarks: BookmarkData[], 13 | onSelectBookmarkDepth: Function, 14 | ) { 15 | const graph = new DagGraph(history.graph) 16 | const { currentStateId } = graph 17 | const bookmarkAt = (index: number) => { 18 | if (bookmarks.length === 0 || index < 0 || index >= bookmarks.length) { 19 | return null 20 | } 21 | return new Bookmark(bookmarks[index], graph) 22 | } 23 | const jump = (index: number, jumpToDepth?: number) => { 24 | const target: Bookmark = bookmarkAt(index) 25 | const state: StateId = target.getStateAtDepth(jumpToDepth) 26 | onSelectBookmarkDepth({ bookmarkIndex: index, depth: jumpToDepth, state }) 27 | } 28 | const bookmarkIndex = 29 | rawSelectedBookmark !== undefined 30 | ? rawSelectedBookmark 31 | : Math.max(0, bookmarks.findIndex(it => it.stateId === currentStateId)) 32 | const bookmark: Bookmark = bookmarkAt(bookmarkIndex) 33 | const depth = bookmark 34 | ? bookmark.sanitizeDepth(rawSelectedBookmarkDepth) 35 | : null 36 | 37 | const rawStepBack = (isAtBookmarkStart: boolean) => { 38 | const isAtBeginning = bookmarkIndex === 0 && isAtBookmarkStart 39 | 40 | // We're at the start of the presentation, do nothing 41 | if (isAtBeginning) { 42 | return 43 | } 44 | 45 | if (isAtBookmarkStart) { 46 | log('going to previous bookmark') 47 | jump(bookmarkIndex - 1, undefined) 48 | return 49 | } 50 | 51 | log('decrementing depth in current bookmark') 52 | jump(bookmarkIndex, depth - 1) 53 | } 54 | 55 | const handleStepForward = () => { 56 | const isAtBookmarkEnd = bookmark.isDepthAtEnd(depth) 57 | const isAtLastBookmark = bookmarkIndex === bookmarks.length - 1 58 | const isAtEnd = isAtLastBookmark && isAtBookmarkEnd 59 | 60 | // We're at the end of the presentation, do nothing 61 | if (isAtEnd) { 62 | return 63 | } 64 | 65 | // If we're not at the end of this bookmark, just increment the step 66 | if (!isAtBookmarkEnd) { 67 | log('incrementing depth in current bookmark') 68 | jump(bookmarkIndex, depth + 1) 69 | return 70 | } 71 | 72 | // Go to the start of the next bookmark 73 | log('going to next bookmark') 74 | const nextBookmark = new Bookmark(bookmarks[bookmarkIndex + 1], graph) 75 | jump(bookmarkIndex + 1, nextBookmark.startingDepth()) 76 | } 77 | 78 | const handleStepBack = () => rawStepBack(bookmark.isDepthAtStart(depth)) 79 | const handleStepBackUnbounded = () => rawStepBack(depth === 0) 80 | const handleJumpToBookmark = (index: number) => jump(index) 81 | const handlePreviousBookmark = () => 82 | handleJumpToBookmark(Math.max(bookmarkIndex - 1, 0)) 83 | const handleNextBookmark = () => 84 | handleJumpToBookmark(Math.min(bookmarkIndex + 1, bookmarks.length - 1)) 85 | 86 | return { 87 | handleStepBack, 88 | handleStepForward, 89 | handleNextBookmark, 90 | handlePreviousBookmark, 91 | handleStepBackUnbounded, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/util/calculateIndex.ts: -------------------------------------------------------------------------------- 1 | // A small utility function for determining the selected item in a div based on a percentage 2 | export default function calculateIndex(length: number, percent: number) { 3 | const result = Math.floor(percent * length) 4 | return Math.max(0, Math.min(length - 1, result)) 5 | } 6 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/util/isNumber.ts: -------------------------------------------------------------------------------- 1 | export default function isNumber(d: any) { 2 | return ( 3 | d !== null && 4 | d !== undefined && 5 | typeof d === 'number' && 6 | !Number.isNaN(d) && 7 | Number.isFinite(d) 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /packages/dag-history-component/src/util/spans.ts: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug' 2 | 3 | const log = debug('dag-history-component:SpanCalculator') 4 | 5 | export class Span { 6 | constructor(public start: number, public end: number, public type: string) {} 7 | 8 | public toString() { 9 | return `Span::${this.type}[${this.start} => ${this.end}]` 10 | } 11 | 12 | get length() { 13 | return this.end - this.start 14 | } 15 | 16 | public areBoundsEqual(other: Span) { 17 | return other.start === this.start && other.end === this.end 18 | } 19 | 20 | public contains(index: number) { 21 | return index >= this.start && index <= this.end 22 | } 23 | } 24 | 25 | /** 26 | * Gets the initial set of common spans for a branch profile 27 | */ 28 | export function initialSpans( 29 | start: number, 30 | max: number, 31 | type: string = 'NONE', 32 | ) { 33 | return [new Span(start, max + 1, type)] 34 | } 35 | 36 | /** 37 | * Replaces a span at at index with the given span 38 | */ 39 | function replaceSpan(spans: Span[], newSpan: Span, i: number) { 40 | return spans 41 | .slice(0, i) 42 | .concat([newSpan]) 43 | .concat(spans.slice(i + 1)) 44 | } 45 | 46 | /** 47 | * Inserts a span at the tail of an existing span 48 | * [=============] <-- existing 49 | * [++++] <-- new 50 | * becomes 51 | * 52 | * [=======][++++] <-- pruned existing + new 53 | */ 54 | function insertSpanAtTail(spans: Span[], newSpan: Span, i: number) { 55 | const span = spans[i] 56 | return spans 57 | .slice(0, i) 58 | .concat([new Span(span.start, newSpan.start, span.type), newSpan]) 59 | .concat(spans.slice(i + 1)) 60 | .filter(t => t.end > t.start) 61 | } 62 | 63 | /** 64 | * Inserts a span at the bridge-point between two spans 65 | * [aaaaaaaaaaaaaaa][bbbbbbbbbbbbbb] <-- existing 66 | * [cccccc] <-- new 67 | * becomes 68 | * 69 | * [aaaaaaaaaaaa[cccccc][bbbbbbbbbb] 70 | */ 71 | function insertBridgingSpan(spans: Span[], newSpan: Span, i: number) { 72 | const left = spans[i] 73 | const right = spans[i + 1] 74 | 75 | return spans 76 | .slice(0, i) 77 | .concat([ 78 | new Span(left.start, newSpan.start, left.type), 79 | newSpan, 80 | new Span(newSpan.end, right.end, right.type), 81 | ]) 82 | .concat(spans.slice(i + 2)) 83 | .filter(t => t.end > t.start) 84 | } 85 | 86 | /** 87 | * Inserts a span interior to an existing span 88 | * [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] <-- existing 89 | * [cccccc] <-- new 90 | * becomes 91 | * 92 | * [aaaaaaaaaaaa[cccccc][aaaaaaaaaaa] 93 | */ 94 | function insertSplittingSpan(spans: Span[], newSpan: Span, i: number) { 95 | const span = spans[i] 96 | return spans 97 | .slice(0, i) 98 | .concat([ 99 | new Span(span.start, newSpan.start, span.type), 100 | newSpan, 101 | new Span(newSpan.end, span.end, span.type), 102 | ]) 103 | .concat(spans.slice(i + 1)) 104 | .filter(t => t.end > t.start) 105 | } 106 | 107 | export function insertSpan(spans: Span[], newSpan: Span) { 108 | if (!newSpan) { 109 | throw new Error('could not insert span that is undefined/null') 110 | } 111 | 112 | for (let i = 0; i < spans.length; i += 1) { 113 | const span = spans[i] 114 | if (span.areBoundsEqual(newSpan)) { 115 | return replaceSpan(spans, newSpan, i) 116 | } 117 | if (span.contains(newSpan.start)) { 118 | if (newSpan.end === span.end) { 119 | return insertSpanAtTail(spans, newSpan, i) 120 | } else if (newSpan.end > span.end) { 121 | return insertBridgingSpan(spans, newSpan, i) 122 | } 123 | return insertSplittingSpan(spans, newSpan, i) 124 | } 125 | } 126 | log(`Could not insert span ${newSpan} into spanset ${spans}`) 127 | return spans 128 | } 129 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/Bookmark/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import * as React from 'react' 3 | import Bookmark from '../../../src/components/Bookmark/Bookmark' 4 | const { action } = require('@storybook/addon-actions') 5 | 6 | storiesOf('Bookmark', module) 7 | .add('Inactive', () => ( 8 | 15 | )) 16 | .add('Active', () => ( 17 | 25 | )) 26 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/Branch/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import * as React from 'react' 3 | import Branch from '../../../src/components/Branch' 4 | import { BranchType } from '../../../src/interfaces' 5 | const { action } = require('@storybook/addon-actions') 6 | 7 | storiesOf('Branch', module) 8 | .add('Branch with half-depth', () => ( 9 | 17 | )) 18 | .add('Branch with an ative commit', () => ( 19 | 27 | )) 28 | .add('Branch with inactive start', () => ( 29 | 36 | )) 37 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/BranchList/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import * as React from 'react' 3 | import BranchList from '../../../src/components/BranchList' 4 | import { BranchType } from '../../../src/interfaces' 5 | 6 | storiesOf('BranchList', module).add('Basic example', () => ( 7 | 29 | )) 30 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/BranchProfile/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import * as React from 'react' 3 | import BranchProfile from '../../../src/components/BranchProfile' 4 | import { BranchType } from '../../../src/interfaces' 5 | 6 | storiesOf('BranchProfile', module) 7 | .add('1/2 Selected', () => ( 8 | 15 | )) 16 | .add('2/2 Selected', () => ( 17 | 24 | )) 25 | .add('1/3 Selected', () => ( 26 | 33 | )) 34 | .add('2/3 Selected', () => ( 35 | 42 | )) 43 | .add('3/3 Selected', () => ( 44 | 51 | )) 52 | .add('Current Branch with Active State', () => ( 53 | 62 | )) 63 | .add('Legacy Branch with Active State', () => ( 64 | 73 | )) 74 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/Continuation/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import * as React from 'react' 3 | import Continuation from '../../../src/components/Continuation' 4 | 5 | storiesOf('Continuation', module) 6 | .add('Huge Number', () => ) 7 | .add('Selected Empty', () => ( 8 | 9 | )) 10 | .add('Selected with Number', () => ) 11 | .add('Single Digit', () => ) 12 | .add('Triple Digit', () => ) 13 | .add('Unselected and Empty', () => ) 14 | .add('Unselected with Number', () => ) 15 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/DiscoveryTrail/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import * as React from 'react' 3 | import DiscoveryTrail from '../../../src/components/DiscoveryTrail' 4 | const { action } = require('@storybook/addon-actions') 5 | 6 | storiesOf('DiscoveryTrail', module) 7 | .add('Horizontal', () => ) 8 | .add('Horizontal, Full-Width', () => ( 9 | 10 | )) 11 | .add('Vertical', () => ( 12 |
13 | 14 |
15 |
16 | )) 17 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/History/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import { fromJS } from 'immutable' 3 | import { get } from 'lodash' 4 | import * as React from 'react' 5 | import HTML5Backend from 'react-dnd-html5-backend' 6 | import { Provider } from 'react-redux' 7 | import * as redux from 'redux' 8 | import { HistoryType, ComponentView } from '../../../src/interfaces' 9 | import { createLogger } from 'redux-logger' 10 | import thunk from 'redux-thunk' 11 | import createHistoryContainer from '../../../src/components/createHistoryContainer' 12 | const { action } = require('@storybook/addon-actions') 13 | 14 | const DragDropContextProvider = require('react-dnd/lib/DragDropContextProvider') 15 | .default 16 | 17 | const Container = createHistoryContainer( 18 | (state: any) => state.app, 19 | (state: any) => state.component, 20 | (state: any) => get(state, 'metadata.source'), 21 | ) 22 | 23 | function createStore(state: any) { 24 | // A simple static reducer 25 | const reducer = () => state 26 | 27 | // If the redux devtools are available, wire into them 28 | const composeEnhancers = 29 | (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || redux.compose 30 | const logger = createLogger() 31 | 32 | const store: redux.Store = composeEnhancers( 33 | redux.applyMiddleware(thunk, logger), 34 | )(redux.createStore)(reducer) 35 | return store 36 | } 37 | 38 | storiesOf('History Injection', module).add('On Bookmarks view ', () => { 39 | const store = createStore({ 40 | app: { 41 | current: {}, 42 | graph: fromJS({ 43 | current: { 44 | state: '32', 45 | branch: '0', 46 | }, 47 | branches: { 48 | 0: { 49 | latest: '34', 50 | name: 'Branch 0', 51 | first: '1', 52 | committed: '34', 53 | }, 54 | }, 55 | states: { 56 | 27: { 57 | id: '27', 58 | name: 'Initial State', 59 | branch: '0', 60 | }, 61 | 28: { 62 | id: '28', 63 | name: 'Add Empty Visual Container', 64 | branch: '0', 65 | parent: '27', 66 | }, 67 | 29: { id: '29', name: 'Add Field', branch: '0', parent: '28' }, 68 | 30: { 69 | id: '30', 70 | name: 'Update Dimensions', 71 | branch: '0', 72 | parent: '29', 73 | }, 74 | 31: { 75 | id: '31', 76 | name: 'Update Dimensions', 77 | branch: '0', 78 | parent: '30', 79 | }, 80 | 32: { id: '32', name: 'Execute Action', branch: '0', parent: '31' }, 81 | 33: { id: '33', name: 'Execute Action', branch: '0', parent: '32' }, 82 | 34: { id: '34', name: 'Execute Action', branch: '0', parent: '33' }, 83 | }, 84 | physicalStates: { 85 | 27: {}, 86 | 28: {}, 87 | 29: {}, 88 | 30: {}, 89 | 31: {}, 90 | 32: {}, 91 | 33: {}, 92 | 34: {}, 93 | }, 94 | lastStateId: '34', 95 | lastBranchId: '1', 96 | stateHash: {}, 97 | chronologicalStates: ['27', '28', '29', '30', '31', '32', '33', '34'], 98 | }), 99 | }, 100 | component: { 101 | bookmarkEdit: { editIndex: 6 }, 102 | dragDrop: {}, 103 | views: { 104 | mainView: 'storyboarding', 105 | historyType: HistoryType.BRANCHED, 106 | branchContainerExpanded: false, 107 | }, 108 | playback: { isPlayingBack: false, bookmark: 6, depth: 1 }, 109 | pinnedState: { id: '21' }, 110 | bookmarks: [ 111 | { name: 'Execute Action', stateId: '25', data: {} }, 112 | { name: 'Execute Action', stateId: '21', data: {} }, 113 | { name: 'Initial State', stateId: '19', data: {} }, 114 | { name: 'Execute Action', stateId: '23', data: {} }, 115 | { name: 'Execute Action', stateId: '34', data: {} }, 116 | { name: 'Execute Action', stateId: '33', data: {} }, 117 | { name: 'Execute Action', stateId: '32', data: {} }, 118 | ], 119 | }, 120 | }) 121 | return ( 122 | 123 | 124 | 125 | 126 | 127 | ) 128 | }) 129 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/HistoryTypeDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import * as React from 'react' 3 | import { HistoryTypeDropdown } from '../../../src/components/HistoryTypeDropdown' 4 | import { BranchType, HistoryType } from '../../../src/interfaces' 5 | const { action } = require('@storybook/addon-actions') 6 | 7 | storiesOf('HistoryTypeDropdown', module).add('With two options', () => ( 8 |
16 | 20 |
21 | )) 22 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/components/State/index.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import * as React from 'react' 3 | import State from '../../../src/components/State' 4 | import { BranchType } from '../../../src/interfaces' 5 | const { action } = require('@storybook/addon-actions') 6 | 7 | storiesOf('State', module) 8 | .add('Current, Active', () => ( 9 | 19 | )) 20 | .add('Current, Inactive', () => ( 21 | 30 | )) 31 | .add('Legacy, Active', () => ( 32 | 42 | )) 43 | .add('Legacy, Inactive', () => ( 44 | 53 | )) 54 | .add('Current, Unbookmarked', () => ( 55 | 65 | )) 66 | .add('Legacy, Unbookmarked', () => ( 67 | 77 | )) 78 | .add('Current, Bookmarked', () => ( 79 | 90 | )) 91 | .add('Legacy, Bookmarked', () => ( 92 | 103 | )) 104 | .add('Pinned', () => ( 105 | 115 | )) 116 | -------------------------------------------------------------------------------- /packages/dag-history-component/stories/index.ts: -------------------------------------------------------------------------------- 1 | import './components/Bookmark' 2 | import './components/Branch' 3 | import './components/BranchList' 4 | import './components/BranchProfile' 5 | import './components/Continuation' 6 | import './components/DiscoveryTrail' 7 | import './components/History' 8 | import './components/State' 9 | import './components/HistoryTypeDropdown' 10 | -------------------------------------------------------------------------------- /packages/dag-history-component/styles.css: -------------------------------------------------------------------------------- 1 | .react-tabs__tab-panel--selected { 2 | height: 100%; 3 | max-height: 100%; 4 | display: flex; 5 | } 6 | 7 | /** 8 | * State Entry Animation 9 | */ 10 | .state-entry-enter { 11 | opacity: 0; 12 | } 13 | 14 | .state-entry-enter.state-entry-enter-active { 15 | opacity: 1; 16 | transform: translate(0%, 0%); 17 | transition: transform 250ms ease-in-out, opacity 250ms ease-in-out; 18 | } 19 | 20 | .state-entry-leave { 21 | opacity: 1; 22 | transform: translate(0%); 23 | } 24 | 25 | .state-entry-leave.state-entry-leave-active { 26 | opacity: 0; 27 | transform: translate(100%, 0%); 28 | transition: transform 250ms ease-in-out, opacity 250ms ease-in-out; 29 | } 30 | 31 | /** 32 | * Continuation Dissolve Animation 33 | */ 34 | .continuation-dissolve-enter { 35 | opacity: 0; 36 | width: 0; 37 | transition: opacity 250ms ease-in-out, width 250ms ease-in-out; 38 | } 39 | 40 | .continuation-dissolve-enter.continuation-dissolve-enter-active { 41 | opacity: 1; 42 | width: 14px; 43 | transition: opacity 250ms ease-in-out, width 250ms ease-in-out; 44 | } 45 | 46 | .continuation-dissolve-leave { 47 | opacity: 1; 48 | width: 14px; 49 | transition: opacity 250ms ease-in-out, width 250ms ease-in-out; 50 | } 51 | 52 | .continuation-dissolve-leave.continuation-dissolve-leave-active { 53 | opacity: 0; 54 | width: 0px; 55 | transition: opacity 250ms ease-in-out, width 250ms ease-in-out; 56 | } 57 | 58 | /** 59 | * Branch List Dock Animation 60 | */ 61 | .show-docked-under-enter { 62 | transform: translate(0%, 100%); 63 | } 64 | 65 | .show-docked-under-enter.show-docked-under-enter-active { 66 | transform: translate(0%, 0%); 67 | transition: transform 250ms ease-in-out; 68 | } 69 | 70 | .show-docked-under-exit { 71 | transform: translate(0%, 0%); 72 | } 73 | 74 | .show-docked-under-exit.show-docked-under-exit-active { 75 | transform: translate(0%, 100%); 76 | transition: transform 250ms ease-in-out; 77 | } 78 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/components/BookmarkList/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount, configure } from 'enzyme' 2 | import * as React from 'react' 3 | import Bookmark from '../../../src/components/Bookmark' 4 | import BookmarkList from '../../../src/components/BookmarkList' 5 | import * as Adapter from 'enzyme-adapter-react-16' 6 | 7 | configure({ adapter: new Adapter() }) 8 | 9 | xdescribe('The BookmarkList Component', () => { 10 | it('can render a set of bookmarks', () => { 11 | const rendered = mount( 12 | undefined} 14 | onBookmarkEditDone={() => undefined} 15 | bookmarks={[ 16 | { 17 | index: 0, 18 | stateId: 1, 19 | name: 'Bookmark 1', 20 | annotation: 'Anno 1', 21 | active: true, 22 | }, 23 | { 24 | index: 1, 25 | stateId: 2, 26 | name: 'Bookmark 2', 27 | annotation: 'Anno 2', 28 | active: false, 29 | }, 30 | { 31 | index: 2, 32 | stateId: 3, 33 | name: 'Bookmark 3', 34 | annotation: 'Anno 3', 35 | active: false, 36 | }, 37 | ]} 38 | />, 39 | ) 40 | expect(rendered.find('.history-bookmark').length).toEqual(3) 41 | expect(rendered).toBeDefined() 42 | }) 43 | 44 | xit('can propagate up bookmark clicks', () => { 45 | let clickedIndex = null 46 | let clickedStateId = null 47 | const rendered = mount( 48 | undefined} 50 | onBookmarkEditDone={() => undefined} 51 | onBookmarkClick={(index, stateId) => { 52 | clickedIndex = index 53 | clickedStateId = stateId 54 | }} 55 | bookmarks={[ 56 | { 57 | index: 0, 58 | stateId: 1, 59 | name: 'Bookmark 1', 60 | annotation: 'Anno 1', 61 | active: true, 62 | }, 63 | { 64 | index: 1, 65 | stateId: 2, 66 | name: 'Bookmark 2', 67 | annotation: 'Anno 2', 68 | active: false, 69 | }, 70 | { 71 | index: 2, 72 | stateId: 3, 73 | name: 'Bookmark 3', 74 | annotation: 'Anno 3', 75 | active: false, 76 | }, 77 | ]} 78 | />, 79 | ) 80 | 81 | rendered 82 | .find(Bookmark) 83 | .at(1) 84 | .simulate('click') 85 | expect(clickedIndex).toEqual(1) 86 | expect(clickedStateId).toEqual(2) 87 | }) 88 | 89 | it('can handle click events when no handler is defined', () => { 90 | const rendered = mount( 91 | undefined} 93 | onBookmarkEditDone={() => undefined} 94 | bookmarks={[ 95 | { 96 | index: 0, 97 | stateId: 1, 98 | name: 'Bookmark 1', 99 | annotation: 'Anno 1', 100 | active: true, 101 | }, 102 | ]} 103 | />, 104 | ) 105 | rendered 106 | .find(Bookmark) 107 | .at(0) 108 | .simulate('click') 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/components/Branch/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount, configure } from 'enzyme' 2 | import * as React from 'react' 3 | import Branch from '../../../src/components/Branch' 4 | import { BranchType } from '../../../src/interfaces' 5 | import * as Adapter from 'enzyme-adapter-react-16' 6 | 7 | configure({ adapter: new Adapter() }) 8 | 9 | describe('The Branch component', () => { 10 | it('can be rendered', () => { 11 | const rendered = mount( 12 | , 19 | ) 20 | expect(rendered).toBeDefined() 21 | rendered.simulate('click') // no error 22 | }) 23 | 24 | it('can respond to click events rendered', () => { 25 | let clicked = false 26 | const rendered = mount( 27 | (clicked = true)} 34 | />, 35 | ) 36 | rendered.simulate('click') 37 | expect(clicked).toBeDefined() 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/components/BranchList/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount, configure } from 'enzyme' 2 | import * as React from 'react' 3 | import BranchList from '../../../src/components/BranchList' 4 | import Branch from '../../../src/components/Branch' 5 | import { BranchType } from '../../../src/interfaces' 6 | import * as Adapter from 'enzyme-adapter-react-16' 7 | 8 | configure({ adapter: new Adapter() }) 9 | 10 | describe('The BranchList component', () => { 11 | it('can render an empty branch list', () => { 12 | const rendered = mount() 13 | expect(rendered).toBeDefined() 14 | }) 15 | 16 | it('can render an non-empty branch list', () => { 17 | let clickedId = null 18 | const rendered = mount( 19 | (clickedId = id)} 32 | />, 33 | ) 34 | expect(rendered).toBeDefined() 35 | rendered.find(Branch).get(0) 36 | rendered.find(Branch).simulate('click') 37 | expect(clickedId).toEqual('5') 38 | }) 39 | 40 | it('will not throw an error when an branch is clicked without an onClick handler defined', () => { 41 | const rendered = mount( 42 | , 55 | ) 56 | // click should be ok 57 | expect(rendered).toBeDefined() 58 | rendered.find(Branch).get(0) 59 | rendered.find(Branch).simulate('click') 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/components/Continuation/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount, configure } from 'enzyme' 2 | import * as React from 'react' 3 | import Continuation from '../../../src/components/Continuation' 4 | import * as Adapter from 'enzyme-adapter-react-16' 5 | 6 | configure({ adapter: new Adapter() }) 7 | 8 | describe('The Continuation component', () => { 9 | it('can be rendered', () => { 10 | const rendered = mount() 11 | expect(rendered).toBeDefined() 12 | }) 13 | 14 | it('can render a sane continuation count', () => { 15 | const rendered = mount() 16 | expect(rendered).toBeDefined() 17 | const found = rendered.find(Continuation) 18 | expect(found.html().indexOf('10')).toBeGreaterThanOrEqual(0) 19 | expect(found.length).toEqual(1) 20 | }) 21 | 22 | it('can render a high count', () => { 23 | const rendered = mount() 24 | expect(rendered).toBeDefined() 25 | const found = rendered.find(Continuation) 26 | expect(found.html().indexOf('99+')).toBeGreaterThanOrEqual(0) 27 | expect(found.length).toEqual(1) 28 | }) 29 | 30 | it('can be rendered', () => { 31 | let clicked = false 32 | const rendered = mount( (clicked = true)} />) 33 | rendered.simulate('click') 34 | expect(clicked).toBeTruthy() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/components/OptionDropdown/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount, configure } from 'enzyme' 2 | import * as React from 'react' 3 | import OptionDropdown from '../../../src/components/OptionDropdown' 4 | import * as Adapter from 'enzyme-adapter-react-16' 5 | 6 | const { 7 | default: Dropdown, 8 | DropdownTrigger, 9 | DropdownContent, 10 | } = require('react-simple-dropdown') 11 | 12 | configure({ adapter: new Adapter() }) 13 | 14 | describe('The OptionDropdown Component', () => { 15 | it('can be rendered', () => { 16 | let rendered = mount() 17 | expect(rendered).toBeDefined() 18 | 19 | rendered = mount() 20 | expect(rendered).toBeDefined() 21 | }) 22 | 23 | it('can handle on option being clicked', () => { 24 | let clicked = null 25 | const rendered = mount( 26 | (clicked = 'derp') }, 29 | { onClick: () => (clicked = 'herp') }, 30 | { element:

hi

, onClick: () => (clicked = 'flerp') }, 31 | ]} 32 | />, 33 | ) 34 | 35 | rendered 36 | .find(OptionDropdown) 37 | .at(0) 38 | .simulate('click') 39 | 40 | expect(rendered).toBeDefined() 41 | rendered 42 | .find('li') 43 | .at(0) 44 | .simulate('click') 45 | expect(clicked).toEqual('derp') 46 | 47 | rendered 48 | .find('li') 49 | .at(1) 50 | .simulate('click') 51 | expect(clicked).toEqual('herp') 52 | 53 | rendered 54 | .find('li') 55 | .at(2) 56 | .simulate('click') 57 | expect(clicked).toEqual('flerp') 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/components/PlaybackPane/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount, configure } from 'enzyme' 2 | import * as React from 'react' 3 | import PlaybackPane from '../../../src/components/PlaybackPane' 4 | import * as Adapter from 'enzyme-adapter-react-16' 5 | 6 | configure({ adapter: new Adapter() }) 7 | 8 | describe('The PlaybackPane Component', () => { 9 | it('can be mounted', () => { 10 | const rendered = mount( 11 | , 18 | ) 19 | 20 | const html = rendered.html() 21 | expect(html.indexOf('Hello!')).toBeGreaterThanOrEqual(0) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/components/State/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount, configure } from 'enzyme' 2 | import * as React from 'react' 3 | import Continuation from '../../../src/components/Continuation' 4 | import State from '../../../src/components/State' 5 | import { BranchType } from '../../../src/interfaces' 6 | import * as Adapter from 'enzyme-adapter-react-16' 7 | const Bookmark = require('react-icons/lib/io/bookmark') 8 | 9 | configure({ adapter: new Adapter() }) 10 | 11 | describe('The State Component', () => { 12 | it('can be rendered', () => { 13 | let rendered = mount() 14 | expect(rendered).toBeDefined() 15 | 16 | rendered = mount( 17 | , 18 | ) 19 | expect(rendered).toBeDefined() 20 | 21 | rendered = mount( 22 | , 28 | ) 29 | expect(rendered).toBeDefined() 30 | 31 | rendered = mount( 32 | , 39 | ) 40 | expect(rendered).toBeDefined() 41 | 42 | rendered = mount( 43 | , 51 | ) 52 | expect(rendered).toBeDefined() 53 | 54 | rendered = mount( 55 | , 63 | ) 64 | expect(rendered).toBeDefined() 65 | }) 66 | 67 | it('can handle clicks when click handlers are not defined without throwing', () => { 68 | const rendered = mount( 69 | , 70 | ) 71 | rendered.simulate('click') 72 | rendered.find(Continuation).simulate('click') 73 | rendered.find(Bookmark).simulate('click') 74 | }) 75 | 76 | it('can handle clicks', () => { 77 | let clicked = false 78 | let continuationClicked = false 79 | let bookmarkClicked = false 80 | const rendered = mount( 81 | (clicked = true)} 86 | onContinuationClick={() => (continuationClicked = true)} 87 | onBookmarkClick={() => (bookmarkClicked = true)} 88 | />, 89 | ) 90 | rendered.simulate('click') 91 | expect(clicked).toBeTruthy() 92 | rendered.find(Continuation).simulate('click') 93 | expect(continuationClicked).toBeTruthy() 94 | rendered.find(Bookmark).simulate('click') 95 | expect(bookmarkClicked).toBeTruthy() 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as History from '../src' 2 | 3 | describe('The Top-Level Entry Point', () => { 4 | it('exists', () => { 5 | expect(History).toBeDefined() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/palette.spec.ts: -------------------------------------------------------------------------------- 1 | import palette from '../src/palette' 2 | 3 | describe('The Palette Module', () => { 4 | it('exposes a default colors object full of color strings', () => { 5 | Object.keys(palette).forEach(k => 6 | expect(typeof palette[k]).toEqual('string'), 7 | ) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/state/actions/creators.spec.ts: -------------------------------------------------------------------------------- 1 | import { ActionTypes as DagHistoryActions } from '@essex/redux-dag-history' 2 | import * as sinon from 'sinon' 3 | import * as ActionCreators from '../../../src/state/actions/creators' 4 | import * as ActionTypes from '../../../src/state/actions/types' 5 | 6 | describe('The Action Creators Module', () => { 7 | it('has action creators that emit FSA-compliant actions', () => { 8 | expect(ActionCreators.selectMainView('derp')).toEqual({ 9 | type: ActionTypes.SELECT_MAIN_VIEW, 10 | payload: 'derp', 11 | }) 12 | expect(ActionCreators.toggleBranchContainer({ index: 3 })).toEqual({ 13 | type: ActionTypes.TOGGLE_BRANCH_CONTAINER, 14 | payload: { 15 | index: 3, 16 | }, 17 | }) 18 | }) 19 | 20 | describe('the bookmarkDragDrop action', () => { 21 | it('will emit a drop event and a moveBookmark event', () => { 22 | const dispatch = sinon.spy() 23 | ActionCreators.bookmarkDragDrop({ index: 0, droppedOn: 1 })(dispatch) 24 | 25 | expect(dispatch.callCount).toEqual(2) 26 | const firstAction = dispatch.getCall(0).args[0] 27 | expect(firstAction.type).toEqual(ActionTypes.BOOKMARK_DRAG_DROP) 28 | expect(firstAction.payload).toBeUndefined() 29 | 30 | const secondAction = dispatch.getCall(1).args[0] 31 | expect(secondAction.type).toEqual(ActionTypes.MOVE_BOOKMARK) 32 | expect(secondAction.payload).toEqual({ from: 0, to: 1 }) 33 | }) 34 | 35 | it('will only emit a drop event when the to index is invalid', () => { 36 | const dispatch = sinon.spy() 37 | ActionCreators.bookmarkDragDrop({ index: 1, droppedOn: undefined })( 38 | dispatch, 39 | ) 40 | 41 | expect(dispatch.callCount).toEqual(1) 42 | const firstAction = dispatch.getCall(0).args[0] 43 | expect(firstAction.type).toEqual(ActionTypes.BOOKMARK_DRAG_DROP) 44 | }) 45 | }) 46 | 47 | describe('the selectBookmarkDepth action', () => { 48 | it('will emit bookmark selection and jump events', () => { 49 | const bookmarkIndex = 5 50 | const depth = 7 51 | const state = '10' 52 | 53 | const dispatch = sinon.spy() 54 | ActionCreators.selectBookmarkDepth({ bookmarkIndex, depth, state })( 55 | dispatch, 56 | ) 57 | 58 | expect(dispatch.callCount).toEqual(2) 59 | const firstAction = dispatch.getCall(0).args[0] 60 | expect(firstAction.type).toEqual(ActionTypes.SELECT_BOOKMARK_DEPTH) 61 | expect(firstAction.payload).toEqual({ bookmarkIndex, depth }) 62 | 63 | const secondAction = dispatch.getCall(1).args[0] 64 | expect(secondAction.type).toEqual(DagHistoryActions.JUMP_TO_STATE) 65 | expect(secondAction.payload).toEqual(state) 66 | }) 67 | }) 68 | 69 | describe('the selectBookmark action', () => { 70 | it('will emit bookmark selection and jump events', () => { 71 | const bookmarkIndex = 3 72 | const state = '7' 73 | const dispatch = sinon.spy() 74 | ActionCreators.selectBookmark(bookmarkIndex, state)(dispatch) 75 | 76 | expect(dispatch.callCount).toEqual(2) 77 | const firstAction = dispatch.getCall(0).args[0] 78 | expect(firstAction.type).toEqual(ActionTypes.SELECT_BOOKMARK_DEPTH) 79 | expect(firstAction.payload).toEqual({ 80 | bookmarkIndex, 81 | depth: undefined, 82 | }) 83 | 84 | const secondAction = dispatch.getCall(1).args[0] 85 | expect(secondAction.type).toEqual(DagHistoryActions.JUMP_TO_STATE) 86 | expect(secondAction.payload).toEqual(state) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/state/actions/types.spec.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../../../src/state/actions/types' 2 | 3 | describe('Action Types', () => { 4 | it('should all be strings', () => { 5 | Object.keys(types).forEach(key => 6 | expect(typeof types[key]).toEqual('string'), 7 | ) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/state/reducers/bookmarks.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addBookmark, 3 | doChangeBookmark, 4 | moveBookmark, 5 | removeBookmark, 6 | renameBookmark, 7 | } from '../../../src/state/actions/creators' 8 | import reduce from '../../../src/state/reducers/bookmarks' 9 | 10 | const fan = bookmarks => bookmarks.map(b => b.stateId) 11 | 12 | describe('The bookmarks reducer', () => { 13 | it('emits an empty bookmarks array by default', () => { 14 | expect(reduce(undefined, { type: 'derp' })).toEqual([]) 15 | }) 16 | 17 | it('can add a bookmark', () => { 18 | const state = reduce(undefined, addBookmark({ stateId: 1, name: 'derp' })) 19 | expect(state).toEqual([{ stateId: 1, name: 'derp', data: {} }]) 20 | }) 21 | 22 | it('can add a bookmark with data', () => { 23 | const state = reduce( 24 | undefined, 25 | addBookmark({ stateId: 1, name: 'derp', data: { x: 1 } }), 26 | ) 27 | expect(state).toEqual([{ stateId: 1, name: 'derp', data: { x: 1 } }]) 28 | }) 29 | 30 | it('can remove a bookmark', () => { 31 | let state 32 | state = reduce(state, addBookmark({ stateId: 1, name: 'state1' })) 33 | state = reduce(state, addBookmark({ stateId: 2, name: 'state2' })) 34 | state = reduce(state, addBookmark({ stateId: 3, name: 'state3' })) 35 | 36 | expect(state.length).toEqual(3) 37 | 38 | state = reduce(state, removeBookmark(2)) 39 | expect(state.length).toEqual(2) 40 | expect(fan(state)).toEqual([1, 3]) 41 | }) 42 | 43 | it('can rename a bookmark', () => { 44 | let state 45 | state = reduce(state, addBookmark({ stateId: 1, name: 'state1' })) 46 | state = reduce(state, addBookmark({ stateId: 2, name: 'state2' })) 47 | state = reduce(state, addBookmark({ stateId: 3, name: 'state3' })) 48 | 49 | state = reduce(state, renameBookmark({ stateId: 2, name: 'newName' })) 50 | expect(state[1].name).toEqual('newName') 51 | }) 52 | 53 | it('can change a bookmark', () => { 54 | let state 55 | state = reduce(state, addBookmark({ stateId: '1', name: 'state1' })) 56 | state = reduce(state, addBookmark({ stateId: '2', name: 'state2' })) 57 | state = reduce(state, addBookmark({ stateId: '3', name: 'state3' })) 58 | 59 | state = reduce( 60 | state, 61 | doChangeBookmark({ stateId: '2', name: 'newName', data: { x: 1 } }), 62 | ) 63 | expect(state[1].name).toEqual('newName') 64 | expect(state[1].data).toEqual({ x: 1 }) 65 | }) 66 | 67 | it('can move a bookmark', () => { 68 | let state 69 | state = reduce(state, addBookmark({ stateId: '1', name: 'state1' })) 70 | state = reduce(state, addBookmark({ stateId: '2', name: 'state2' })) 71 | state = reduce(state, addBookmark({ stateId: '3', name: 'state3' })) 72 | 73 | state = reduce(state, moveBookmark({ from: 0, to: 2 })) 74 | expect(fan(state)).toEqual(['2', '3', '1']) 75 | 76 | state = reduce(state, moveBookmark({ from: 0, to: 1 })) 77 | expect(fan(state)).toEqual(['3', '2', '1']) 78 | 79 | state = reduce(state, moveBookmark({ from: 2, to: 1 })) 80 | expect(fan(state)).toEqual(['3', '1', '2']) 81 | 82 | state = reduce(state, moveBookmark({ from: 2, to: 0 })) 83 | expect(fan(state)).toEqual(['2', '3', '1']) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/state/reducers/dragDrop.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | default as makeReducer, 3 | INITIAL_STATE, 4 | } from '../../../src/state/reducers/dragDrop' 5 | 6 | import { 7 | bookmarkDragHover, 8 | bookmarkDragStart, 9 | } from '../../../src/state/actions/creators' 10 | import * as types from '../../../src/state/actions/types' 11 | 12 | const defaultConfig = { 13 | actionFilter: () => false, 14 | } 15 | 16 | describe('The DragDrop reducer', () => { 17 | it('will emit an initial dragDrop state', () => { 18 | const state = makeReducer(defaultConfig)(undefined, { type: 'derp' }) 19 | expect(state).toEqual(INITIAL_STATE) 20 | }) 21 | 22 | it('can handle a dragStart event', () => { 23 | let state 24 | const reduce = makeReducer(defaultConfig) 25 | state = reduce(state, { type: 'derp' }) 26 | state = reduce(state, bookmarkDragStart({ index: 3, key: 2 })) 27 | expect(state).toEqual({ 28 | ...INITIAL_STATE, 29 | sourceIndex: 3, 30 | sourceKey: 2, 31 | }) 32 | }) 33 | 34 | it('can handle a dragHover event', () => { 35 | let state 36 | const reduce = makeReducer(defaultConfig) 37 | state = reduce(state, { type: 'derp' }) 38 | state = reduce(state, bookmarkDragStart({ index: 3 })) 39 | state = reduce(state, bookmarkDragHover({ index: 4 })) 40 | expect(state).toEqual({ 41 | ...INITIAL_STATE, 42 | sourceIndex: 3, 43 | hoverIndex: 4, 44 | }) 45 | }) 46 | 47 | it('can handle a dragDrop event', () => { 48 | let state 49 | const reduce = makeReducer(defaultConfig) 50 | state = reduce(state, { type: 'derp' }) 51 | state = reduce(state, bookmarkDragStart({ index: 3 })) 52 | state = reduce(state, bookmarkDragHover({ index: 4 })) 53 | state = reduce(state, { type: types.BOOKMARK_DRAG_DROP }) 54 | expect(state).toEqual(INITIAL_STATE) 55 | }) 56 | 57 | it('can handle a dragCancel event', () => { 58 | let state 59 | const reduce = makeReducer(defaultConfig) 60 | state = reduce(state, { type: 'derp' }) 61 | state = reduce(state, bookmarkDragStart({ index: 3 })) 62 | state = reduce(state, bookmarkDragHover({ index: 4 })) 63 | state = reduce(state, { type: types.BOOKMARK_DRAG_CANCEL }) 64 | expect(state).toEqual(INITIAL_STATE) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/state/reducers/playback.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | default as makeReducer, 3 | INITIAL_STATE, 4 | } from '../../../src/state/reducers/playback' 5 | 6 | import { 7 | doStartPlayback, 8 | stopPlayback, 9 | } from '../../../src/state/actions/creators' 10 | import * as types from '../../../src/state/actions/types' 11 | 12 | const defaultConfig = { 13 | actionFilter: () => false, 14 | } 15 | 16 | describe('The Playback reducer', () => { 17 | it('will emit an initial state', () => { 18 | const state = makeReducer(defaultConfig)(undefined, { type: 'derp' }) 19 | expect(state).toEqual(INITIAL_STATE) 20 | }) 21 | 22 | it('can handle a startPlayback event', () => { 23 | let state 24 | const reduce = makeReducer(defaultConfig) 25 | state = reduce(state, { type: 'derp' }) 26 | state = reduce(state, doStartPlayback({ initialDepth: 3, stateId: '1' })) 27 | expect(state).toEqual({ 28 | isPlayingBack: true, 29 | bookmark: 0, 30 | depth: 3, 31 | }) 32 | }) 33 | 34 | it('can handle a stopPlayback event', () => { 35 | let state 36 | const reduce = makeReducer(defaultConfig) 37 | state = reduce(state, { type: 'derp' }) 38 | state = reduce(state, doStartPlayback({ initialDepth: 3, stateId: '1' })) 39 | state = reduce(state, stopPlayback()) 40 | expect(state).toEqual(INITIAL_STATE) 41 | }) 42 | 43 | it('can handle a selectBookmarkDepth event', () => { 44 | let state 45 | const reduce = makeReducer(defaultConfig) 46 | state = reduce(state, { type: 'derp' }) 47 | state = reduce(state, doStartPlayback({ initialDepth: 3, stateId: '1' })) 48 | 49 | state = reduce(state, { 50 | type: types.SELECT_BOOKMARK_DEPTH, 51 | payload: { 52 | depth: 5, 53 | bookmarkIndex: 1, 54 | }, 55 | }) 56 | expect(state).toEqual({ 57 | isPlayingBack: true, 58 | bookmark: 1, 59 | depth: 5, 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/state/reducers/views.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | default as makeReducer, 3 | INITIAL_STATE, 4 | } from '../../../src/state/reducers/views' 5 | 6 | import { 7 | selectHistoryType, 8 | selectMainView, 9 | toggleBranchContainer, 10 | } from '../../../src/state/actions/creators' 11 | 12 | const defaultConfig = { 13 | actionFilter: () => false, 14 | } 15 | 16 | describe('The Views reducer', () => { 17 | it('will emit an initial dragDrop state', () => { 18 | const state = makeReducer(defaultConfig as any)(undefined, { type: 'derp' }) 19 | expect(state).toEqual(INITIAL_STATE) 20 | }) 21 | 22 | it('can handle a selectMainView event', () => { 23 | const reduce = makeReducer(defaultConfig as any) 24 | const state = reduce(undefined, selectMainView('storyboarding')) 25 | expect(state).toEqual({ 26 | ...INITIAL_STATE, 27 | mainView: 'storyboarding', 28 | }) 29 | }) 30 | 31 | it('can handle a selectHistoryType event', () => { 32 | const reduce = makeReducer(defaultConfig as any) 33 | const state = reduce(undefined, selectHistoryType('derp')) 34 | expect(state).toEqual({ 35 | ...INITIAL_STATE, 36 | historyType: 'derp', 37 | }) 38 | }) 39 | 40 | it('can handle a toggleBranchContainer event', () => { 41 | let state 42 | const reduce = makeReducer(defaultConfig as any) 43 | state = reduce(state, { type: 'derp' }) 44 | expect(state.branchContainerExpanded).toBeTruthy() 45 | 46 | state = reduce(state, toggleBranchContainer({ index: 3 })) 47 | expect(state.branchContainerExpanded).toBeFalsy() 48 | 49 | state = reduce(state, toggleBranchContainer({ index: 3 })) 50 | expect(state.branchContainerExpanded).toBeTruthy() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/util/isNumber.spec.ts: -------------------------------------------------------------------------------- 1 | import isNumber from '../../src/util/isNumber' 2 | 3 | describe('The isNumber module', () => { 4 | it('returns true on numeric inputs', () => { 5 | expect(isNumber(0)).toBeTruthy() 6 | expect(isNumber(1)).toBeTruthy() 7 | expect(isNumber(-1)).toBeTruthy() 8 | expect(isNumber(Number.MAX_VALUE)).toBeTruthy() 9 | expect(isNumber(Number.MIN_VALUE)).toBeTruthy() 10 | }) 11 | 12 | it('returns false on non-numeric inputs', () => { 13 | expect(isNumber(NaN)).toBeFalsy() 14 | expect(isNumber(Infinity)).toBeFalsy() 15 | expect(isNumber({})).toBeFalsy() 16 | expect(isNumber(() => ({}))).toBeFalsy() 17 | expect(isNumber('')).toBeFalsy() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/util/percentCalc.spec.ts: -------------------------------------------------------------------------------- 1 | import calculateIndex from '../../src/util/calculateIndex' 2 | 3 | const adjust = (x: number) => x - 0.001 4 | 5 | describe('Percent calculations (for mouse intercepts in a DOM element)', () => { 6 | it('can determine the item to select based on a percentage range', () => { 7 | // [[--|---|---|---] 8 | expect(calculateIndex(4, -1)).toEqual(0) 9 | 10 | // [[--|---|---|---] 11 | expect(calculateIndex(4, 0)).toEqual(0) 12 | 13 | // [x--|---|---|---] 14 | expect(calculateIndex(4, adjust(1 / 12))).toEqual(0) 15 | 16 | // [-x-|---|---|---] 17 | expect(calculateIndex(4, adjust(2 / 12))).toEqual(0) 18 | 19 | // [--x|---|---|---] 20 | expect(calculateIndex(4, adjust(3 / 12))).toEqual(0) 21 | 22 | // [---|x--|---|---] 23 | expect(calculateIndex(4, adjust(4 / 12))).toEqual(1) 24 | 25 | // [---|-x-|---|---] 26 | expect(calculateIndex(4, adjust(5 / 12))).toEqual(1) 27 | 28 | // [---|--x|---|---] 29 | expect(calculateIndex(4, adjust(6 / 12))).toEqual(1) 30 | 31 | // [---|---|x--|---] 32 | expect(calculateIndex(4, adjust(7 / 12))).toEqual(2) 33 | 34 | // [---|---|-x-|---] 35 | expect(calculateIndex(4, adjust(8 / 12))).toEqual(2) 36 | 37 | // [---|---|--x|---] 38 | expect(calculateIndex(4, adjust(9 / 12))).toEqual(2) 39 | 40 | // [---|---|---|x--] 41 | expect(calculateIndex(4, adjust(10 / 12))).toEqual(3) 42 | 43 | // [---|---|---|-x-] 44 | expect(calculateIndex(4, adjust(11 / 12))).toEqual(3) 45 | 46 | // [---|---|---|--x] 47 | expect(calculateIndex(4, adjust(12 / 12))).toEqual(3) 48 | 49 | // [--|---|---|---]] 50 | expect(calculateIndex(4, 1.0)).toEqual(3) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/dag-history-component/test/util/spans.spec.ts: -------------------------------------------------------------------------------- 1 | import * as SpanCalc from '../../src/util/spans' 2 | 3 | function assertSpan(span: any, start: number, end: number, type: string) { 4 | expect(span.start).toEqual(start) 5 | expect(span.end).toEqual(end) 6 | expect(span.type).toEqual(type) 7 | } 8 | 9 | describe('The span calculator', () => { 10 | it('is exists', () => { 11 | expect(SpanCalc).toBeDefined() 12 | }) 13 | 14 | it('can create initial span', () => { 15 | const spans = SpanCalc.initialSpans(0, 10) 16 | // 'expected one span' 17 | expect(spans.length).toEqual(1) 18 | assertSpan(spans[0], 0, 11, 'NONE') 19 | }) 20 | 21 | it('can insert a span in the middle of the initial span', () => { 22 | let spans = SpanCalc.initialSpans(0, 10) 23 | spans = SpanCalc.insertSpan(spans, new SpanCalc.Span(3, 6, 'test')) 24 | // 'expected 3 spans' 25 | expect(spans.length).toEqual(3) 26 | assertSpan(spans[0], 0, 3, 'NONE') 27 | assertSpan(spans[1], 3, 6, 'test') 28 | assertSpan(spans[2], 6, 11, 'NONE') 29 | }) 30 | 31 | it('can insert a one-length span in the middle of the initial span', () => { 32 | let spans = SpanCalc.initialSpans(0, 10) 33 | spans = SpanCalc.insertSpan(spans, new SpanCalc.Span(3, 4, 'test')) 34 | // 'expected 3 spans' 35 | expect(spans.length).toEqual(3) 36 | assertSpan(spans[0], 0, 3, 'NONE') 37 | assertSpan(spans[1], 3, 4, 'test') 38 | assertSpan(spans[2], 4, 11, 'NONE') 39 | }) 40 | 41 | it('can replace a one-length span', () => { 42 | let spans = SpanCalc.initialSpans(0, 10) 43 | spans = SpanCalc.insertSpan(spans, new SpanCalc.Span(3, 4, 'test')) 44 | spans = SpanCalc.insertSpan(spans, new SpanCalc.Span(3, 4, 'derp')) 45 | // 'expected 3 spans' 46 | expect(spans.length).toEqual(3) 47 | assertSpan(spans[0], 0, 3, 'NONE') 48 | assertSpan(spans[1], 3, 4, 'derp') 49 | assertSpan(spans[2], 4, 11, 'NONE') 50 | }) 51 | 52 | it('can insert a span at the end of the initial span', () => { 53 | let spans = SpanCalc.initialSpans(0, 10) 54 | spans = SpanCalc.insertSpan(spans, new SpanCalc.Span(8, 11, 'test')) 55 | // , `expected 2 spans: ${spans}` 56 | expect(spans.length).toEqual(2) 57 | assertSpan(spans[0], 0, 8, 'NONE') 58 | assertSpan(spans[1], 8, 11, 'test') 59 | }) 60 | 61 | it('can insert a span at the beginning of the initial span', () => { 62 | let spans = SpanCalc.initialSpans(0, 10) 63 | spans = SpanCalc.insertSpan(spans, new SpanCalc.Span(0, 2, 'test')) 64 | // , `expected 2 spans: ${spans}` 65 | expect(spans.length).toEqual(2) 66 | assertSpan(spans[0], 0, 2, 'test') 67 | assertSpan(spans[1], 2, 11, 'NONE') 68 | }) 69 | 70 | it('can insert a span overlapping two spans', () => { 71 | let spans = SpanCalc.initialSpans(0, 10) 72 | spans = SpanCalc.insertSpan(spans, new SpanCalc.Span(0, 6, 'test')) 73 | spans = SpanCalc.insertSpan(spans, new SpanCalc.Span(3, 7, 'test')) 74 | // , 'expected 3 spans' 75 | expect(spans.length).toEqual(3) 76 | assertSpan(spans[0], 0, 3, 'test') 77 | assertSpan(spans[1], 3, 7, 'test') 78 | assertSpan(spans[2], 7, 11, 'NONE') 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /packages/dag-history-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "declaration": true, 6 | "noEmit": false 7 | }, 8 | "files": ["src/index.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/example/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@essex/dag-history-example", 3 | "private": true, 4 | "version": "0.0.4", 5 | "scripts": { 6 | "start": "webpack-dev-server --host 0.0.0.0 --inline --history-api-fallback" 7 | }, 8 | "license": "MIT", 9 | "files": [ 10 | "package.json", 11 | "README.md", 12 | "LICENSE", 13 | "src/" 14 | ], 15 | "devDependencies": { 16 | "circular-dependency-plugin": "^4.4.0", 17 | "css-loader": "^0.28.9", 18 | "exports-loader": "^0.7.0", 19 | "imports-loader": "^0.7.1", 20 | "postcss-loader": "^2.1.0", 21 | "style-loader": "^0.20.1", 22 | "ts-loader": "^3.4.0", 23 | "typescript": "^2.7.1", 24 | "webpack": "^3.10.0", 25 | "webpack-dev-server": "^2.11.1" 26 | }, 27 | "dependencies": { 28 | "@essex/dag-history-component": "^3.0.4", 29 | "@essex/redux-dag-history": "^5.0.1", 30 | "@types/file-saver": "^1.3.0", 31 | "@types/lodash": "^4.14.102", 32 | "@types/react": "^16.0.36", 33 | "@types/react-dnd-html5-backend": "^2.1.8", 34 | "@types/react-dom": "^16.0.3", 35 | "@types/react-redux": "^5.0.14", 36 | "@types/react-router": "^4.0.21", 37 | "@types/react-router-redux": "^5.0.11", 38 | "@types/redux": "^3.6.0", 39 | "@types/redux-actions": "^2.2.3", 40 | "@types/redux-thunk": "^2.1.0", 41 | "debug": "^3.1.0", 42 | "file-saver": "^1.3.3", 43 | "lodash": "^4.17.5", 44 | "react": "^16.2.0", 45 | "react-dnd": "^2.5.4", 46 | "react-dnd-html5-backend": "^2.5.4", 47 | "react-dom": "^16.2.0", 48 | "react-redux": "^5.0.6", 49 | "react-router": "^4.2.0", 50 | "react-router-redux": "^4.0.8", 51 | "redux": "^3.7.2", 52 | "redux-actions": "^2.2.1", 53 | "redux-thunk": "^2.2.0", 54 | "skeleton-css": "^2.0.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/example/src/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | width: 100%; 4 | margin: 0; 5 | padding: 0; 6 | -ms-overflow-style: none; 7 | } 8 | ::-webkit-scrollbar { 9 | display: none; 10 | } 11 | 12 | body { 13 | display: flex; 14 | height: 100%; 15 | margin: 0; 16 | overflow: hidden; 17 | padding: 0; 18 | } 19 | 20 | .app-root { 21 | display: flex; 22 | flex: 1; 23 | padding: 10; 24 | } 25 | 26 | .visual-a { 27 | flex: 1; 28 | padding: 30px; 29 | } 30 | 31 | .visual-b { 32 | flex: 1; 33 | padding: 30px; 34 | } 35 | 36 | .history-viz-container { 37 | display: flex; 38 | flex: 1; 39 | } 40 | 41 | .app-container { 42 | height: 100%; 43 | width: 100%; 44 | position: absolute; 45 | } 46 | 47 | .dashboard { 48 | display: flex; 49 | flex: 1; 50 | flex-direction: row; 51 | height: 100%; 52 | width: 100%; 53 | } 54 | 55 | .visual-pane { 56 | display: flex; 57 | flex: 3; 58 | flex-direction: column; 59 | } 60 | 61 | .history-pane { 62 | display: flex; 63 | flex: 1; 64 | } 65 | -------------------------------------------------------------------------------- /packages/example/src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import 'skeleton-css/css/normalize.css' 4 | import 'skeleton-css/css/skeleton.css' 5 | import './app.css' 6 | import Application from './components/Application' 7 | import store from './state/store' 8 | 9 | const root = document.createElement('div') 10 | document.body.appendChild(root) 11 | 12 | ReactDOM.render(, root) 13 | -------------------------------------------------------------------------------- /packages/example/src/components/Application.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { DragDropContext } from 'react-dnd' 3 | import HTML5Backend from 'react-dnd-html5-backend' 4 | import * as ReactDOM from 'react-dom' 5 | import { Provider } from 'react-redux' 6 | import HistoryPresenter from './HistoryPresenter' 7 | import VisualA from './visuals/VisualA' 8 | import VisualB from './visuals/VisualB' 9 | 10 | export interface ApplicationProps { 11 | store: any 12 | } 13 | 14 | export class Application extends React.Component { 15 | public render() { 16 | const { store } = this.props 17 | return ( 18 | 19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 | ) 32 | } 33 | } 34 | 35 | export default DragDropContext(HTML5Backend)(Application) 36 | -------------------------------------------------------------------------------- /packages/example/src/components/HistoryPresenter.tsx: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash' 2 | import * as React from 'react' 3 | import createHistoryContainer from '@essex/dag-history-component/lib/components/createHistoryContainer' 4 | import { load, save } from '../persister' 5 | 6 | const HistoryContainer = createHistoryContainer( 7 | (state: any) => state.app, 8 | (state: any) => state.component, 9 | (state: any) => get(state, 'metadata.source'), 10 | ) 11 | 12 | const HistoryPresenter: React.StatelessComponent<{}> = () => { 13 | return ( 14 |
15 | Promise.resolve(true), 21 | }} 22 | /> 23 | 29 |
30 | ) 31 | } 32 | 33 | export default HistoryPresenter 34 | -------------------------------------------------------------------------------- /packages/example/src/components/visuals/VisualA.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import { 5 | decrement as doDecrement, 6 | increment as doIncrement, 7 | pickRandomColor as doPickRandomColor, 8 | } from '../../state/Actions' 9 | 10 | export interface VisualAStateProps { 11 | backgroundColor: string 12 | } 13 | 14 | export interface VisualADispatchProps { 15 | actions: { 16 | pickRandomColor: Function 17 | } 18 | } 19 | 20 | export interface VisualAProps extends VisualAStateProps, VisualADispatchProps {} 21 | 22 | const RawVisualA: React.StatelessComponent = ({ 23 | backgroundColor, 24 | actions: { pickRandomColor }, 25 | }) => ( 26 |
pickRandomColor()} 30 | > 31 |

Color: {backgroundColor}

32 |
33 | ) 34 | 35 | export default connect( 36 | ({ app: { current: { visuals: { color: backgroundColor } } } }: any) => ({ 37 | backgroundColor, 38 | }), 39 | dispatch => ({ 40 | actions: bindActionCreators( 41 | { pickRandomColor: doPickRandomColor }, 42 | dispatch, 43 | ), 44 | }), 45 | )(RawVisualA) 46 | -------------------------------------------------------------------------------- /packages/example/src/components/visuals/VisualB.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import { 5 | decrement as doDecrement, 6 | increment as doIncrement, 7 | pickRandomColor as doPickRandomColor, 8 | } from '../../state/Actions' 9 | 10 | export interface VisualBStateProps { 11 | value: number 12 | } 13 | 14 | export interface VisualBDispatchProps { 15 | actions: { 16 | increment: Function 17 | decrement: Function 18 | } 19 | } 20 | 21 | export interface VisualBProps extends VisualBStateProps, VisualBDispatchProps {} 22 | 23 | const RawVisualB: React.StatelessComponent = ({ 24 | value, 25 | actions: { increment, decrement }, 26 | }) => ( 27 |
28 |
29 |

Value: {value}

30 |
31 |
32 | 33 | 34 |
35 |
36 | ) 37 | 38 | export default connect( 39 | ({ app: { current: { visuals: { value } } } }: any) => ({ value }), 40 | dispatch => ({ 41 | actions: bindActionCreators( 42 | { 43 | increment: doIncrement, 44 | decrement: doDecrement, 45 | }, 46 | dispatch, 47 | ), 48 | }), 49 | )(RawVisualB) 50 | -------------------------------------------------------------------------------- /packages/example/src/persister/index.ts: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug' 2 | import simulate from './simulate' 3 | import { saveAs } from 'file-saver' 4 | const log = debug('redux-dag-history:FilePersister') 5 | 6 | function readJson(file: Blob) { 7 | return new Promise((resolve, reject) => { 8 | const reader = new FileReader() 9 | reader.onerror = err => { 10 | log('error reading file', err) 11 | reject(err) 12 | } 13 | reader.onloadend = () => { 14 | log('read loadend', reader) 15 | const { result } = reader 16 | resolve(JSON.parse(result)) 17 | } 18 | reader.readAsText(file) 19 | }) 20 | } 21 | 22 | export function save(history: any) { 23 | const blob = new Blob([JSON.stringify(history)], { 24 | type: 'text/plain;charset=utf-8', 25 | }) 26 | try { 27 | saveAs(blob, 'visual.history') 28 | } catch (err) { 29 | log('Error Saving History', err) 30 | } 31 | } 32 | 33 | export function load() { 34 | log('Loading...', Promise) 35 | return new Promise(resolve => { 36 | log('Loading... (in promise)') 37 | const pickerElem = document.getElementById('pickFileInput') 38 | pickerElem.addEventListener('change', function handleChange() { 39 | this.removeEventListener('change', handleChange, false) 40 | log('Loading... on change!') 41 | // tslint:disable-next-line no-string-literal 42 | const file = (this as any).files[0] 43 | log('history file selected', file) 44 | resolve(readJson(file)) 45 | }) 46 | simulate(document.getElementById('pickFileInput'), 'click') 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /packages/example/src/persister/simulate.ts: -------------------------------------------------------------------------------- 1 | const EVENT_MATCHERS: { [key: string]: RegExp } = { 2 | HTMLEvents: /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, 3 | MouseEvents: /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/, 4 | } 5 | 6 | export interface Options { 7 | pointerX: number 8 | pointerY: number 9 | button: number 10 | ctrlKey: boolean 11 | altKey: boolean 12 | shiftKey: boolean 13 | metaKey: boolean 14 | bubbles: boolean 15 | cancelable: boolean 16 | } 17 | 18 | export type Optional = { [K in keyof T]?: T[K] } 19 | 20 | const DEFAULT_OPTIONS = { 21 | pointerX: 0, 22 | pointerY: 0, 23 | button: 0, 24 | ctrlKey: false, 25 | altKey: false, 26 | shiftKey: false, 27 | metaKey: false, 28 | bubbles: true, 29 | cancelable: true, 30 | } 31 | 32 | // 33 | // From http://stackoverflow.com/questions/6157929/how-to-simulate-a-mouse-click-using-javascript 34 | // 35 | export default function simulate( 36 | element: HTMLElement, 37 | eventName: string, 38 | opts: Optional = {}, 39 | ) { 40 | const options = { ...DEFAULT_OPTIONS, ...opts } 41 | let oEvent: any = null 42 | let eventType: string = null 43 | 44 | for (const name in EVENT_MATCHERS) { 45 | if (EVENT_MATCHERS[name].test(eventName)) { 46 | eventType = name 47 | break 48 | } 49 | } 50 | 51 | if (!eventType) { 52 | throw new SyntaxError( 53 | 'Only HTMLEvents and MouseEvents interfaces are supported', 54 | ) 55 | } 56 | 57 | if (document.createEvent) { 58 | oEvent = document.createEvent(eventType) 59 | if (eventType === 'HTMLEvents') { 60 | oEvent.initEvent(eventName, options.bubbles, options.cancelable) 61 | } else { 62 | oEvent.initMouseEvent( 63 | eventName, 64 | options.bubbles, 65 | options.cancelable, 66 | document.defaultView, 67 | options.button, 68 | options.pointerX, 69 | options.pointerY, 70 | options.pointerX, 71 | options.pointerY, 72 | options.ctrlKey, 73 | options.altKey, 74 | options.shiftKey, 75 | options.metaKey, 76 | options.button, 77 | element, 78 | ) 79 | } 80 | element.dispatchEvent(oEvent) 81 | } else { 82 | // tslint:disable-next-line no-string-literal 83 | const evt = (document as any)['createEventObject']() 84 | const elm = element as any 85 | // tslint:disable-next-line no-string-literal 86 | elm.fireEvent(`on${eventName}`, { 87 | ...evt, 88 | ...options, 89 | clientX: options.pointerX, 90 | clientY: options.pointerY, 91 | }) 92 | } 93 | return element 94 | } 95 | -------------------------------------------------------------------------------- /packages/example/src/state/Actions.ts: -------------------------------------------------------------------------------- 1 | import * as ReduxActions from 'redux-actions' 2 | 3 | // Action Types 4 | export const PICK_RANDOM_COLOR = 'PICK_RANDOM_COLOR' 5 | export const INCREMENT = 'INCREMENT_VALUE' 6 | export const DECREMENT = 'DECREMENT_VALUE' 7 | 8 | // Action Creators 9 | export const pickRandomColor = ReduxActions.createAction(PICK_RANDOM_COLOR) 10 | export const increment = ReduxActions.createAction(INCREMENT) 11 | export const decrement = ReduxActions.createAction(DECREMENT) 12 | -------------------------------------------------------------------------------- /packages/example/src/state/reducers/app/index.ts: -------------------------------------------------------------------------------- 1 | import * as redux from 'redux' 2 | import metadata, { State as MetadataState } from './metadata' 3 | import visuals, { State as VisualsState } from './visuals' 4 | 5 | export interface State { 6 | metadata: MetadataState 7 | visuals: VisualsState 8 | } 9 | 10 | export default redux.combineReducers({ metadata, visuals }) 11 | -------------------------------------------------------------------------------- /packages/example/src/state/reducers/app/metadata.ts: -------------------------------------------------------------------------------- 1 | import * as Actions from '../../Actions' 2 | 3 | const DEFAULT_STATE: State = { 4 | name: 'Initial', 5 | historyIndex: 0, 6 | source: null, 7 | } 8 | 9 | export interface State { 10 | name: string 11 | historyIndex: number 12 | source: string | null 13 | } 14 | 15 | export default function reduce( 16 | state: State = DEFAULT_STATE, 17 | action: ReduxActions.Action, 18 | ) { 19 | if (action.type === Actions.INCREMENT) { 20 | return { 21 | name: 'Increment Value', 22 | source: 'Incrementer', 23 | historyIndex: state.historyIndex + 1, 24 | } 25 | } else if (action.type === Actions.DECREMENT) { 26 | return { 27 | name: 'Decrement Value', 28 | source: 'Incrementer', 29 | historyIndex: state.historyIndex + 1, 30 | } 31 | } else if (action.type === Actions.PICK_RANDOM_COLOR) { 32 | return { 33 | name: 'Pick Random Color', 34 | source: 'Color Picker', 35 | historyIndex: state.historyIndex + 1, 36 | } 37 | } 38 | return state 39 | } 40 | -------------------------------------------------------------------------------- /packages/example/src/state/reducers/app/visuals.ts: -------------------------------------------------------------------------------- 1 | import * as Actions from '../../Actions' 2 | 3 | const INITIAL_STATE = { 4 | value: 0, 5 | color: '#FF0000', 6 | } 7 | 8 | export interface State { 9 | value: number 10 | color: string 11 | } 12 | 13 | function randomColor() { 14 | return `#${Math.floor(Math.random() * 0xffffff).toString(16)}` 15 | } 16 | 17 | export default function reduce( 18 | state: State = INITIAL_STATE, 19 | action: ReduxActions.Action, 20 | ) { 21 | let result = state 22 | if (action.type === Actions.INCREMENT) { 23 | result = { ...state, value: state.value + 1 } 24 | } else if (action.type === Actions.DECREMENT) { 25 | result = { ...state, value: state.value - 1 } 26 | } else if (action.type === Actions.PICK_RANDOM_COLOR) { 27 | result = { ...state, color: randomColor() } 28 | } 29 | return result 30 | } 31 | -------------------------------------------------------------------------------- /packages/example/src/state/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { reducer as dagHistoryReducer } from '@essex/redux-dag-history' 2 | import * as debug from 'debug' 3 | import * as redux from 'redux' 4 | 5 | import Configuration from '@essex/dag-history-component/lib/state/Configuration' 6 | import history from '@essex/dag-history-component/lib/state/reducers' 7 | import app, { State as AppState } from './app' 8 | import hashString from '../../util/hashString' 9 | 10 | const log = debug('app:state') 11 | 12 | export const EXCLUDED_ACTION_NAMES = [ 13 | '@@INIT', 14 | 'INIT', 15 | 'TOGGLE_BRANCH_CONTAINER', 16 | 'SELECT_MAIN_VIEW', 17 | 'SELECT_HISTORY_TYPE', 18 | 'RETRIEVE_INITIAL_STATE_IGNORE_THIS_EVENT', 19 | 'HIGHLIGHT_SUCCESSORS', 20 | ] 21 | 22 | function stateEqualityPredicate(state1: AppState, state2: AppState) { 23 | log('checking equality between states', state1, state2) 24 | const colorsEqual = state1.visuals.color === state2.visuals.color 25 | const valuesEqual = state1.visuals.value === state2.visuals.value 26 | return colorsEqual && valuesEqual 27 | } 28 | 29 | function stateKeyGenerator(state: AppState) { 30 | const { color, value } = state.visuals 31 | const stateString = `${color}:${value}` 32 | return '' + hashString(stateString) 33 | } 34 | 35 | const DAG_HISTORY_CONFIG = new Configuration({ 36 | // Middleware Config 37 | debug: false, 38 | actionName: state => state.metadata.name, 39 | actionFilter: actionType => EXCLUDED_ACTION_NAMES.indexOf(actionType) === -1, 40 | stateEqualityPredicate, 41 | stateKeyGenerator, 42 | 43 | // UI Config 44 | initialViewState: { 45 | branchContainerExpanded: true, 46 | }, 47 | }) 48 | 49 | export interface State { 50 | app: AppState 51 | component: any // TODO: Create a state interface for the component 52 | } 53 | 54 | export default redux.combineReducers({ 55 | app: dagHistoryReducer(app, DAG_HISTORY_CONFIG), 56 | component: history(DAG_HISTORY_CONFIG), 57 | }) 58 | -------------------------------------------------------------------------------- /packages/example/src/state/store.ts: -------------------------------------------------------------------------------- 1 | import * as redux from 'redux' 2 | import thunk from 'redux-thunk' 3 | import reducers from './reducers' 4 | 5 | const composeEnhancers = 6 | // tslint:disable-next-line no-string-literal 7 | (window as any)['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || redux.compose 8 | const createStoreWithMiddleware = composeEnhancers( 9 | redux.applyMiddleware(thunk), 10 | )(redux.createStore) 11 | 12 | export default createStoreWithMiddleware(reducers) 13 | -------------------------------------------------------------------------------- /packages/example/src/util/hashString.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable no-bitwise 2 | 3 | export default function hashString(input: string) { 4 | const { length } = input 5 | let hash = 0 6 | for (let i = 0; i < length; i += 1) { 7 | const chr = input.charCodeAt(i) 8 | hash = (hash << 5) - hash + chr 9 | hash |= 0 10 | } 11 | return hash 12 | } 13 | -------------------------------------------------------------------------------- /packages/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "declaration": true, 6 | "noEmit": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const CircularDependencyPlugin = require('circular-dependency-plugin') 4 | const path = require('path') 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | context: path.join(__dirname), 9 | entry: { 10 | javascript: './src/app.tsx', 11 | }, 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'appbundle.js', 15 | }, 16 | resolve: { 17 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 18 | alias: { 19 | 'react/lib/ReactMount': 'react-dom/lib/ReactMount', 20 | }, 21 | }, 22 | plugins: [ 23 | new HtmlWebpackPlugin({ 24 | title: 'DAG History Component Example', 25 | }), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | new webpack.ProvidePlugin({ 28 | saveAs: 'imports?this=>global!exports?global.saveAs!filesaver.js', 29 | }), 30 | new CircularDependencyPlugin({ 31 | failsOnError: true, 32 | }), 33 | ], 34 | module: { 35 | rules: [ 36 | { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] }, 37 | { test: /\.ts(x|)/, use: ['ts-loader'], exclude: /node_modules/ }, 38 | ], 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /packages/redux-dag-history/.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .eslintrc 3 | .eslintignore 4 | .npmignore 5 | .gitignore 6 | .travis.yml 7 | .vscode/ 8 | src/ 9 | coverage/ 10 | test/ 11 | scripts/ 12 | __test__ 13 | wallaby.conf.js 14 | tsconfig.json 15 | yarn.lock 16 | *~ 17 | -------------------------------------------------------------------------------- /packages/redux-dag-history/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@essex/redux-dag-history", 3 | "version": "5.0.1", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib", 8 | "build": "tsc", 9 | "start": "tsc -w" 10 | }, 11 | "author": "Chris Trevino ", 12 | "typings": "lib/index", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@types/debug": "^0.0.30", 16 | "@types/node": "^9.4.1", 17 | "@types/redux": "^3.6.0", 18 | "@types/redux-actions": "^2.2.3", 19 | "debug": "^3.1.0", 20 | "immutable": "^3.8.2", 21 | "redux-actions": "^2.2.1" 22 | }, 23 | "peerDependencies": { 24 | "redux": "*" 25 | }, 26 | "devDependencies": { 27 | "@types/chai": "^4.1.2", 28 | "chai": "^4.1.2", 29 | "rimraf": "^2.6.2", 30 | "typescript": "^2.7.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/ActionCreators.ts: -------------------------------------------------------------------------------- 1 | import { Action, createAction, ActionFunction0 } from 'redux-actions' 2 | import * as ActionTypes from './ActionTypes' 3 | import { BranchId, RenameBranchPayload, StateId } from './interfaces' 4 | 5 | export const load = createAction(ActionTypes.LOAD) 6 | export const clear = createAction(ActionTypes.CLEAR) 7 | export const undo = createAction(ActionTypes.UNDO) 8 | export const redo = createAction(ActionTypes.REDO) 9 | export const skipToStart = createAction(ActionTypes.SKIP_TO_START) 10 | export const skipToEnd = createAction(ActionTypes.SKIP_TO_END) 11 | export const jumpToState = createAction(ActionTypes.JUMP_TO_STATE) 12 | export const jumpToBranch = createAction(ActionTypes.JUMP_TO_BRANCH) 13 | export const jumpToLatestOnBranch = createAction( 14 | ActionTypes.JUMP_TO_LATEST_ON_BRANCH, 15 | ) 16 | export const renameBranch = createAction( 17 | ActionTypes.RENAME_BRANCH, 18 | ) 19 | export const createBranch = createAction(ActionTypes.CREATE_BRANCH) 20 | 21 | // Explicitly type here because Action needs to be used to satisfy the compiler 22 | export const squash: ActionFunction0> = createAction( 23 | ActionTypes.SQUASH, 24 | ) 25 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/ActionTypes.ts: -------------------------------------------------------------------------------- 1 | const action = (name: string) => `DAG_HISTORY_${name.toUpperCase()}` 2 | 3 | /** 4 | * Loads a history graph, discarding the current history 5 | */ 6 | export const LOAD = action('load') 7 | 8 | /** 9 | * Clears the state graph. The current state will become the root state of 10 | * the new graph. 11 | */ 12 | export const CLEAR = action('clear') 13 | 14 | /** 15 | * Performs an Undo operation. 16 | * 17 | * An Undo operation will move the currentState pointer to being the parent of 18 | * the current state pointer. 19 | * 20 | * If the parent state pointer is null, no operation is performed. 21 | */ 22 | export const UNDO = action('undo') 23 | 24 | /** 25 | * Performs a Redo operation. 26 | * 27 | * A Redo operation will move the currentState pointer to the next ancestor 28 | * the currentBranch. 29 | */ 30 | export const REDO = action('redo') 31 | 32 | /** 33 | * Jumps to a specific state. 34 | * 35 | * If the state being jumped to is not an ancestor of the current branch, then 36 | * we will detach the current branch. 37 | */ 38 | export const JUMP_TO_STATE = action('jump_to_state') 39 | 40 | /** 41 | * Jumps to the latest state in a branch. 42 | */ 43 | export const JUMP_TO_BRANCH = action('jump_to_branch') 44 | 45 | /** 46 | * Jumps to the latest state in a branch. 47 | */ 48 | export const JUMP_TO_LATEST_ON_BRANCH = action('jump_to_latest_on_branch') 49 | 50 | /** 51 | * Creates a new branch. Points the new branch to the current state. 52 | */ 53 | export const CREATE_BRANCH = action('create_branch') 54 | 55 | /** 56 | * Renames a branch 57 | */ 58 | export const RENAME_BRANCH = action('rename_branch') 59 | 60 | /** 61 | * Squashes the ancestors of the current state that do not support multiple branches. 62 | * e.g. squashing [e] will result in 63 | * 64 | * b b 65 | * a < ==> a < 66 | * c -> d -> [e] [e] 67 | */ 68 | export const SQUASH = action('squash') 69 | 70 | /** 71 | * Renames the current state 72 | */ 73 | export const RENAME_STATE = action('rename_state') 74 | 75 | /** 76 | * Skips to the beginning of the current history line 77 | */ 78 | export const SKIP_TO_START = action('skip_to_start') 79 | 80 | /** 81 | * Skips to the end of the current history line 82 | */ 83 | export const SKIP_TO_END = action('skip_to_end') 84 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/clear.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, DagHistory } from '../interfaces' 2 | import createHistory from './createHistory' 3 | import log from './log' 4 | 5 | export default function clear( 6 | history: DagHistory, 7 | config?: Configuration, 8 | ): DagHistory { 9 | log('clearing history') 10 | const { current } = history 11 | return createHistory(current, config) 12 | } 13 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/createBranch.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory } from '../interfaces' 3 | import nextId from '../nextId' 4 | import log from './log' 5 | 6 | export default function createBranch( 7 | branchName: string, 8 | history: DagHistory, 9 | ): DagHistory { 10 | log('creating branch %s', branchName) 11 | const { graph, current } = history 12 | const reader = new DagGraph(graph) 13 | 14 | return { 15 | current, 16 | graph: graph.withMutations(g => { 17 | const { lastBranchId } = reader 18 | const newBranchId = nextId(lastBranchId) 19 | return new DagGraph(g) 20 | .setCurrentBranch(newBranchId) 21 | .setLastBranchId(newBranchId) 22 | .setBranchName(newBranchId, branchName) 23 | .setCommitted(newBranchId, reader.currentStateId) 24 | .setFirst(newBranchId, reader.currentStateId) 25 | .setLatest(newBranchId, reader.currentStateId) 26 | }), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/createHistory.ts: -------------------------------------------------------------------------------- 1 | import { BranchId, Configuration, DagHistory, StateId } from '../interfaces' 2 | import ConfigurationImpl from '../Configuration' 3 | import nextId from '../nextId' 4 | import load from './load' 5 | 6 | import log from './log' 7 | const EMPTY_STATE: any = {} 8 | 9 | export default function createHistory( 10 | initialState: T = EMPTY_STATE, 11 | config: Configuration = new ConfigurationImpl({}), 12 | ): DagHistory { 13 | log('creating history') 14 | const stateId: StateId = nextId() 15 | const branchId: BranchId = nextId() 16 | const { initialStateName, initialBranchName } = config 17 | 18 | // We may need to insert the initial hash into the state data, so construct it here 19 | const initialStateData = { 20 | name: initialStateName, 21 | branch: branchId, 22 | hash: '', 23 | } 24 | 25 | // If possible, hash the initial state 26 | const stateHash: { [key: string]: any } = {} 27 | if (config && config.stateKeyGenerator) { 28 | const initialHash = config.stateKeyGenerator(initialState) 29 | stateHash[initialHash] = stateId 30 | initialStateData.hash = initialHash 31 | } 32 | 33 | return load({ 34 | current: initialState, 35 | graph: { 36 | /** 37 | * The last used state id 38 | */ 39 | lastStateId: stateId, 40 | 41 | /** 42 | * The last used branch id 43 | */ 44 | lastBranchId: branchId, 45 | 46 | /** 47 | * A map of hash-code strings to state IDs. If a has function is defined in 48 | * the configuration file, then these will be inserted. 49 | */ 50 | stateHash, 51 | 52 | /** 53 | * A chronological listing of visited states 54 | */ 55 | chronologicalStates: [stateId], 56 | 57 | /** 58 | * The current state and branch 59 | */ 60 | current: { 61 | state: stateId, 62 | branch: branchId, 63 | }, 64 | 65 | /** 66 | * A map of branch-ids to branch data 67 | */ 68 | branches: { 69 | [branchId]: { 70 | latest: stateId, 71 | name: initialBranchName, 72 | first: stateId, 73 | committed: stateId, 74 | }, 75 | }, 76 | 77 | /** 78 | * A map of state-ids to state metadata 79 | */ 80 | states: { 81 | [stateId]: initialStateData, 82 | }, 83 | 84 | /** 85 | * A map of state ids to physical states 86 | */ 87 | physicalStates: { 88 | [stateId]: initialState, 89 | }, 90 | }, 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/getExistingState.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { Configuration, DagHistory, StateId } from '../interfaces' 3 | import log from './log' 4 | 5 | /** 6 | * When a state is being inserted into the system, we can optionally check to see if it's 7 | * a duplicate of an existing state. We perform a hash check and an equality check to 8 | * determine if the state is, in fact, a duplicate. 9 | */ 10 | export default function getExistingState( 11 | newState: T, 12 | history: DagHistory, 13 | config: Configuration, 14 | ): StateId | null { 15 | if (config.stateKeyGenerator && config.stateEqualityPredicate) { 16 | const dagGraph = new DagGraph(history.graph) 17 | const hash = config.stateKeyGenerator(newState) 18 | const found = dagGraph.getStateForHash(hash) 19 | 20 | if (found) { 21 | const existingState = new DagGraph(history.graph).getState(found) 22 | const areEqual = config.stateEqualityPredicate(newState, existingState) 23 | if (areEqual) { 24 | return found 25 | } 26 | log('found hashed state not equal') 27 | } else { 28 | log('no hashed state found') 29 | } 30 | } else { 31 | log('skip existing state check') 32 | } 33 | return null 34 | } 35 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/index.ts: -------------------------------------------------------------------------------- 1 | import clear from './clear' 2 | import createBranch from './createBranch' 3 | import createHistory from './createHistory' 4 | import getExistingState from './getExistingState' 5 | import insert from './insert' 6 | import jumpToBranch from './jumpToBranch' 7 | import jumpToLatestOnBranch from './jumpToLatestOnBranch' 8 | import jumpToState from './jumpToState' 9 | import jumpToStateLogged from './jumpToStateLogged' 10 | import load from './load' 11 | import redo from './redo' 12 | import renameBranch from './renameBranch' 13 | import renameState from './renameState' 14 | import replaceCurrentState from './replaceCurrentState' 15 | import skipToEnd from './skipToEnd' 16 | import skipToStart from './skipToStart' 17 | import squash from './squash' 18 | import undo from './undo' 19 | 20 | export default { 21 | clear, 22 | createBranch, 23 | createHistory, 24 | getExistingState, 25 | insert, 26 | jumpToBranch, 27 | jumpToLatestOnBranch, 28 | jumpToState, 29 | jumpToStateLogged, 30 | load, 31 | redo, 32 | renameBranch, 33 | renameState, 34 | replaceCurrentState, 35 | skipToEnd, 36 | skipToStart, 37 | squash, 38 | undo, 39 | } 40 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/insert.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { Configuration, DagHistory } from '../interfaces' 3 | import nextId from '../nextId' 4 | import log from './log' 5 | 6 | /** 7 | * Inserts a new state into the history 8 | */ 9 | export default function insert( 10 | state: T, 11 | history: DagHistory, 12 | config: Configuration, 13 | ): DagHistory { 14 | log('inserting new history state') 15 | const { graph } = history 16 | if (!graph) { 17 | throw new Error('History graph is not defined') 18 | } 19 | 20 | const reader = new DagGraph(graph) 21 | const { lastStateId, lastBranchId } = reader 22 | const parentStateId = reader.currentStateId 23 | const currentBranchId = reader.currentBranch 24 | const newStateId = nextId(lastStateId) 25 | const newStateName = config.actionName(state, newStateId) 26 | const cousins = reader.childrenOf(parentStateId) 27 | const isBranching = 28 | cousins.length > 0 || 29 | lastBranchId > currentBranchId || 30 | currentBranchId === undefined 31 | 32 | return { 33 | current: state, 34 | graph: graph.withMutations(g => { 35 | const dg = new DagGraph(g) 36 | .insertState(newStateId, parentStateId, state, newStateName) 37 | .setCurrentStateId(newStateId) 38 | .setLastStateId(newStateId) 39 | 40 | // If the state has a hash code, register the state 41 | if (config.stateKeyGenerator) { 42 | const stateHash = config.stateKeyGenerator(state) 43 | log('inserting state with key', stateHash) 44 | dg.setHashForState(stateHash, newStateId) 45 | } 46 | 47 | if (isBranching) { 48 | const newBranchId = nextId(lastBranchId) 49 | const newBranch = config.branchName( 50 | currentBranchId, 51 | newBranchId, 52 | newStateName, 53 | ) 54 | dg 55 | .setCurrentBranch(newBranchId) 56 | .setLastBranchId(newBranchId) 57 | .setBranchName(newBranchId, newBranch) 58 | .setLatest(newBranchId, newStateId) 59 | .setFirst(newBranchId, newStateId) 60 | .setCommitted(newBranchId, newStateId) 61 | .markStateForBranch(newStateId, newBranchId) 62 | } else { 63 | dg 64 | .setLatest(currentBranchId, newStateId) 65 | .setCommitted(currentBranchId, newStateId) 66 | .markStateForBranch(newStateId, currentBranchId) 67 | } 68 | }), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/jump.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory, StateId } from '../interfaces' 3 | import unfreeze from './unfreeze' 4 | 5 | // 6 | // Provides state jumping without special rules applied. 7 | // This allows us to share common state-jumping code. 8 | // 9 | 10 | /** 11 | * Jumps to a specific state 12 | */ 13 | export function jump( 14 | stateId: StateId, 15 | history: DagHistory, 16 | callback: ((g: DagGraph) => void) = () => ({}), 17 | ): DagHistory { 18 | const { graph } = history 19 | const reader = new DagGraph(graph) 20 | const targetState = reader.getState(stateId) 21 | return { 22 | current: unfreeze(targetState), 23 | graph: graph.withMutations(g => { 24 | const writer = new DagGraph(g).setCurrentStateId(stateId) 25 | callback(writer) 26 | }), 27 | } 28 | } 29 | 30 | /** 31 | * Jumps to a specific state, while logging this visitation in the chronological states array. 32 | */ 33 | export function jumpLog( 34 | stateId: StateId, 35 | history: DagHistory, 36 | callback: ((g: DagGraph) => void) = () => ({}), 37 | ): DagHistory { 38 | const { graph } = history 39 | const { currentStateId: alternateParent } = new DagGraph(graph) 40 | 41 | return jump(stateId, history, writer => { 42 | writer.setAlternateParent(stateId, alternateParent) 43 | callback(writer) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/jumpToBranch.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { BranchId, DagHistory, StateId } from '../interfaces' 3 | import createBranch from './createBranch' 4 | import { jump } from './jump' 5 | 6 | import log from './log' 7 | 8 | export default function jumpToBranch( 9 | branch: BranchId, 10 | history: DagHistory, 11 | ): DagHistory { 12 | log('jumping to branch %s', branch) 13 | const { graph } = history 14 | const reader = new DagGraph(graph) 15 | const branches = reader.branches 16 | 17 | const jumpTo = (state: StateId) => 18 | jump(state, history, writer => writer.setCurrentBranch(branch)) 19 | 20 | if (branches.indexOf(branch) === -1) { 21 | return createBranch(branch, history) 22 | } 23 | return jumpTo(reader.committedOn(branch)) 24 | } 25 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/jumpToLatestOnBranch.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { BranchId, DagHistory, StateId } from '../interfaces' 3 | import createBranch from './createBranch' 4 | import { jump } from './jump' 5 | import log from './log' 6 | 7 | export default function jumpToLatestOnBranch( 8 | branch: BranchId, 9 | history: DagHistory, 10 | ): DagHistory { 11 | log('jumping to latest on branch %s', branch) 12 | const { graph } = history 13 | const reader = new DagGraph(graph) 14 | const branches = reader.branches 15 | 16 | const jumpTo = (state: StateId) => 17 | jump(state, history, writer => writer.setCurrentBranch(branch)) 18 | 19 | if (branches.indexOf(branch) === -1) { 20 | return createBranch(branch, history) 21 | } 22 | return jumpTo(reader.latestOn(branch)) 23 | } 24 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/jumpToState.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory, StateId } from '../interfaces' 3 | import { jump } from './jump' 4 | 5 | import log from './log' 6 | 7 | export default function jumpToState( 8 | stateId: StateId, 9 | history: DagHistory, 10 | ): DagHistory { 11 | log('jumping to state %s', stateId) 12 | const { graph } = history 13 | const reader = new DagGraph(graph) 14 | const branches = reader.branchesOf(stateId) 15 | const branch = reader.currentBranch 16 | 17 | return jump(stateId, history, writer => { 18 | if (branches.indexOf(branch) === -1) { 19 | const stateBranch = reader.branchOf(stateId) 20 | log( 21 | 'current branch %s is not present on commit %s, available are [%s] - setting current branch to %s', 22 | branch, 23 | stateId, 24 | branches.join(', '), 25 | stateBranch, 26 | ) 27 | writer.setCurrentBranch(stateBranch) 28 | } else { 29 | writer.setCommitted(branch, stateId) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/jumpToStateLogged.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory, StateId } from '../interfaces' 3 | import { jumpLog } from './jump' 4 | import log from './log' 5 | 6 | export default function jumpToStateLogged( 7 | stateId: StateId, 8 | history: DagHistory, 9 | ): DagHistory { 10 | log('jumping w/log to state %s', stateId) 11 | const { graph } = history 12 | const reader = new DagGraph(graph) 13 | const branches = reader.branchesOf(stateId) 14 | const branch = reader.currentBranch 15 | 16 | return jumpLog(stateId, history, writer => { 17 | if (branches.indexOf(branch) === -1) { 18 | const stateBranch = reader.branchOf(stateId) 19 | log( 20 | 'current branch %s is not present on commit %s, available are [%s] - setting current branch to %s', 21 | branch, 22 | stateId, 23 | branches.join(', '), 24 | stateBranch, 25 | ) 26 | writer.setCurrentBranch(stateBranch) 27 | } else { 28 | writer.setCommitted(branch, stateId) 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/load.ts: -------------------------------------------------------------------------------- 1 | import { fromJS, Map } from 'immutable' 2 | import { DagHistory } from '../interfaces' 3 | 4 | export default function load(history: any): DagHistory { 5 | // Immutabilize the history graph, sans the physical states that are present 6 | const copiedGraph = { ...history.graph } 7 | delete copiedGraph.physicalStates 8 | let graph = fromJS(copiedGraph) as Map 9 | 10 | // Insert the states into the dag graph, we do this dance so we don't accidentally turn raw JS to immutable 11 | Object.keys(history.graph.physicalStates).forEach(state => { 12 | graph = graph.setIn( 13 | ['physicalStates', `${state}`], 14 | history.graph.physicalStates[state], 15 | ) 16 | }) 17 | 18 | return { 19 | ...history, 20 | graph, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/log.ts: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug' 2 | export default debug('redux-dag-history:DagHistory') 3 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/redo.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory } from '../interfaces' 3 | import jumpToState from './jumpToState' 4 | import log from './log' 5 | 6 | export default function redo(history: DagHistory): DagHistory { 7 | const { graph } = history 8 | const reader = new DagGraph(graph) 9 | const children = reader 10 | .childrenOf(reader.currentStateId) 11 | .filter( 12 | child => reader.branchesOf(child).indexOf(reader.currentBranch) !== -1, 13 | ) 14 | 15 | if (children.length > 0) { 16 | log('redo') 17 | const nextStateId = children[0] 18 | return jumpToState(nextStateId, history) 19 | } 20 | log('cannot redo') 21 | return history 22 | } 23 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/renameBranch.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { BranchId, DagHistory } from '../interfaces' 3 | import log from './log' 4 | 5 | export default function renameBranch( 6 | branchId: BranchId, 7 | branchName: string, 8 | history: DagHistory, 9 | ): DagHistory { 10 | const { graph } = history 11 | log('renaming branch %s => %s', branchId, branchName) 12 | return { 13 | ...history, 14 | graph: graph.withMutations(g => { 15 | new DagGraph(g).setBranchName(branchId, branchName) 16 | }), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/renameState.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory, StateId } from '../interfaces' 3 | import log from './log' 4 | 5 | export default function renameState( 6 | stateId: StateId, 7 | name: string, 8 | history: DagHistory, 9 | ): DagHistory { 10 | log('rename state %s => %s', stateId, name) 11 | const { graph } = history 12 | return { 13 | current: history.current, 14 | graph: graph.withMutations(g => new DagGraph(g).renameState(stateId, name)), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/replaceCurrentState.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | 3 | import { Configuration, DagHistory } from '../interfaces' 4 | 5 | import log from './log' 6 | 7 | export default function replaceCurrentState( 8 | state: any, 9 | history: DagHistory, 10 | config: Configuration, 11 | ): DagHistory { 12 | log('replace current state') 13 | const { graph } = history 14 | const reader = new DagGraph(graph) 15 | const currentStateId = reader.currentStateId 16 | 17 | return { 18 | current: state, 19 | graph: graph.withMutations(g => { 20 | const graphMutate = new DagGraph(g) 21 | 22 | // If the state has a hash code, register the state 23 | if (config.stateKeyGenerator) { 24 | const hash = config.stateKeyGenerator(state) 25 | log('inserting state with key', hash) 26 | graphMutate.setHashForState(hash, currentStateId) 27 | } 28 | 29 | return graphMutate.replaceState(currentStateId, state) 30 | }), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/skipToEnd.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory } from '../interfaces' 3 | import jumpToState from './jumpToState' 4 | import log from './log' 5 | 6 | export default function skipToEnd(history: DagHistory): DagHistory { 7 | log('skip to end') 8 | const { graph } = history 9 | const reader = new DagGraph(graph) 10 | 11 | const path = reader.branchCommitPath(reader.currentBranch) 12 | const result = path[path.length - 1] 13 | return jumpToState(result, history) 14 | } 15 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/skipToStart.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory } from '../interfaces' 3 | import jumpToState from './jumpToState' 4 | import log from './log' 5 | 6 | export default function skipToStart(history: DagHistory): DagHistory { 7 | log('skip to start') 8 | const { graph } = history 9 | const reader = new DagGraph(graph) 10 | 11 | let result = reader.currentStateId 12 | while (reader.parentOf(result) !== null) { 13 | result = reader.parentOf(result) 14 | } 15 | return jumpToState(result, history) 16 | } 17 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/squash.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory } from '../interfaces' 3 | 4 | import log from './log' 5 | 6 | export default function squash(history: DagHistory): DagHistory { 7 | log('squashing history') 8 | const { graph, current } = history 9 | return { 10 | current, 11 | graph: graph.withMutations(g => new DagGraph(g).squashCurrentBranch()), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/undo.ts: -------------------------------------------------------------------------------- 1 | import DagGraph from '../DagGraph' 2 | import { DagHistory } from '../interfaces' 3 | import jumpToState from './jumpToState' 4 | 5 | import log from './log' 6 | 7 | export default function undo(history: DagHistory): DagHistory { 8 | const { graph } = history 9 | const reader = new DagGraph(graph) 10 | const parentId = reader.parentOf(reader.currentStateId) 11 | if (parentId !== null && parentId !== undefined) { 12 | log('undoing %s => %s', reader.currentStateId, parentId) 13 | return jumpToState(parentId, history) 14 | } 15 | log('cannot undo') 16 | return history 17 | } 18 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/DagHistory/unfreeze.ts: -------------------------------------------------------------------------------- 1 | export default function unfreeze(state: any) { 2 | return state && state.toJS ? state.toJS() : state 3 | } 4 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as ActionCreatorsImport from './ActionCreators' 2 | import * as ActionTypesImport from './ActionTypes' 3 | export { default as DagHistoryImpl } from './DagHistory' 4 | export { default as ConfigurationImpl } from './Configuration' 5 | export { default as DagGraph } from './DagGraph' 6 | export { default as reducer } from './reducer' 7 | export * from './interfaces' 8 | export const ActionCreators = ActionCreatorsImport 9 | export const ActionTypes = ActionTypesImport 10 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Map as ImmutableMap } from 'immutable' 2 | export type StateId = KeyType 3 | export type BranchId = KeyType 4 | export type StateHash = string 5 | 6 | export interface Configuration { 7 | /** 8 | * A flag indicating whether or not to print per-action debug information 9 | */ 10 | debug?: boolean 11 | 12 | /** 13 | * A Predicate filter to determine whether an action is insertable into the history view. 14 | * If an action is not insertable, then the new state as the result of the action will replace 15 | * the current state 16 | */ 17 | actionFilter: (actionType: string) => boolean 18 | 19 | /** 20 | * A predicate for determining whether two states are equal 21 | */ 22 | stateEqualityPredicate: (s1: T, s2: T) => boolean 23 | 24 | /** 25 | * A function for generating map keys for states 26 | */ 27 | stateKeyGenerator: (state: T) => StateHash 28 | 29 | /** 30 | * Action Names 31 | */ 32 | loadActionType: string 33 | clearActionType: string 34 | undoActionType: string 35 | redoActionType: string 36 | jumpToStateActionType: string 37 | jumpToBranchActionType: string 38 | jumpToLatestOnBranchActionType: string 39 | createBranchActionType: string 40 | squashActionType: string 41 | renameBranchActionType: string 42 | renameStateActionType: string 43 | initialBranchName: string 44 | initialStateName: string 45 | skipToStartActionType: string 46 | skipToEndActionType: string 47 | 48 | /** 49 | * State and Branch Naming 50 | */ 51 | actionName: (state: any, stateId: StateId) => string 52 | branchName: ( 53 | oldBranch: BranchId, 54 | newBranch: BranchId, 55 | actionName: string, 56 | ) => string 57 | 58 | // Custom Handlers 59 | canHandleAction: (action: any) => boolean 60 | handleAction: (action: any, history: DagHistory) => DagHistory 61 | } 62 | 63 | export type RawConfiguration = { 64 | [P in keyof Configuration]?: Configuration[P] 65 | } 66 | 67 | /** 68 | * This interface represents the state shape of the DAG history in the Redux 69 | * state tree. 70 | */ 71 | export interface DagHistory { 72 | /** 73 | * The current application state 74 | */ 75 | current: T 76 | 77 | /** 78 | * The explored state space, represented as a graph (future and past). 79 | * See DagGraph.ts and createHistory.ts for more details on the structure of this 80 | */ 81 | graph: ImmutableMap 82 | } 83 | 84 | export type StateIdGenerator = (lastStateId: StateId) => StateId 85 | 86 | export type StateNameGenerator = (state: any, id: StateId) => string 87 | 88 | export interface RenameBranchPayload { 89 | branch: BranchId 90 | name: string 91 | } 92 | -------------------------------------------------------------------------------- /packages/redux-dag-history/src/nextId.ts: -------------------------------------------------------------------------------- 1 | export default function nextId(id?: string) { 2 | return id !== undefined ? `${parseInt(id as string, 10) + 1}` : '1' 3 | } 4 | -------------------------------------------------------------------------------- /packages/redux-dag-history/test/DagGraph.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable' 2 | import DagGraph from '../src/DagGraph' 3 | 4 | function makeGraph() { 5 | return new DagGraph( 6 | fromJS({ 7 | current: { 8 | state: '1', 9 | branch: '1', 10 | }, 11 | branches: { 12 | 1: { 13 | latest: '1', 14 | name: 'Initial Branch', 15 | first: '1', 16 | committed: '1', 17 | }, 18 | }, 19 | states: { 20 | 1: { 21 | state: { a: 1, b: 'xyz' }, 22 | name: 'Initial State', 23 | branch: '1', 24 | parent: null, 25 | }, 26 | }, 27 | }), 28 | ) 29 | } 30 | 31 | describe('DagGraph', () => { 32 | it('exists', () => { 33 | expect(DagGraph).toBeDefined() 34 | }) 35 | 36 | describe('construction', () => { 37 | it('will throw an error if constructed without a valid graph', () => { 38 | expect(() => new DagGraph(null)).toThrow(/must be defined/) 39 | }) 40 | 41 | it('will throw in constructed with a plain object', () => { 42 | expect(() => new DagGraph({} as any)).toThrow( 43 | /must be an immutablejs instance/, 44 | ) 45 | }) 46 | }) 47 | 48 | describe('depthIndexOf', () => { 49 | it('can determine depthIndex=0 for a root commit', () => { 50 | const graph = makeGraph() 51 | expect(graph.depthIndexOf('1', '1')).toEqual(0) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/redux-dag-history/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "declaration": true, 6 | "noEmit": false 7 | }, 8 | "files": ["src/index.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/stub.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/redux-dag-history/ac74d6e374aebe0189bbf74620011ff64b280097/scripts/stub.js -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": {} 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "jsx": "react", 5 | // TODO: When we can get jest to work well w/ JS files that use ESM we can set this to es2015 6 | "module": "commonjs", 7 | "noEmit": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "lib": ["esnext", "dom"], 11 | // TODO: Enable this piecemeal eventually 12 | // "strict": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | "strictPropertyInitialization": true, 17 | "strictFunctionTypes": true, 18 | "strictNullChecks": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-react"], 3 | "rules": { 4 | // Since prettier is automatically handling formatting, these rules do not need to be enabled 5 | "eofline": false, 6 | "quotemark": false, 7 | "semicolon": false, 8 | "indent": false, 9 | "trailing-comma": false, 10 | "align": false, 11 | "whitespace": false, 12 | 13 | // Annoying rules, but may be worth re-enabling at some point 14 | "ordered-imports": false, 15 | 16 | // Ignoring this temporarily until we get the refactor up 17 | "ban-types": false, 18 | 19 | // This should only be enabled for test file (stories, specs, etc..) 20 | "no-implicit-dependencies": [true, "dev"], 21 | 22 | "ban": false, 23 | "class-name": true, 24 | "forin": true, 25 | "arrow-parens": false, 26 | "interface-name": [true, "never-prefix"], 27 | "jsx-curly-spacing": false, 28 | "jsdoc-format": true, 29 | "jsx-no-lambda": false, 30 | "jsx-no-multiline-js": false, 31 | "jsx-wrap-multiline": false, 32 | "label-position": true, 33 | "object-literal-sort-keys": false, 34 | "no-submodule-imports": false, 35 | "no-var-requires": false, 36 | "member-ordering": [ 37 | true, 38 | "static-before-instance", 39 | "variables-before-functions" 40 | ], 41 | "no-arg": true, 42 | "no-bitwise": true, 43 | "no-console": true, 44 | "no-construct": true, 45 | "no-debugger": true, 46 | "no-duplicate-variable": true, 47 | "no-empty": true, 48 | "no-eval": true, 49 | "no-shadowed-variable": true, 50 | "no-string-literal": true, 51 | "no-switch-case-fall-through": true, 52 | "radix": true, 53 | "switch-default": true, 54 | "triple-equals": [true, "allow-null-check"], 55 | "typedef": [true, "parameter", "property-declaration"], 56 | "variable-name": [ 57 | true, 58 | "ban-keywords", 59 | "check-format", 60 | "allow-leading-underscore", 61 | "allow-pascal-case" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | debug: true, 3 | files: ['tsconfig.json', 'packages/*/src/**/*.ts*'], 4 | tests: ['packages/*/test/**/*.spec.ts*'], 5 | env: { 6 | type: 'node', 7 | runner: 'node', 8 | }, 9 | testFramework: 'jest', 10 | } 11 | --------------------------------------------------------------------------------