diff --git a/spikes/stacks/redux-loop/.babelrc b/spikes/stacks/redux-loop/.babelrc new file mode 100644 index 00000000..82cc857a --- /dev/null +++ b/spikes/stacks/redux-loop/.babelrc @@ -0,0 +1,15 @@ +{ + "presets": [ + "react", + "es2015" + ], + "plugins": [ + ["transform-object-rest-spread", { + "useBuiltIns": true + }], + "add-module-exports", + "transform-es2015-modules-commonjs", + "react-hot-loader/babel" + ], + "sourceMaps": "both" +} diff --git a/spikes/stacks/redux-loop/.eslintignore b/spikes/stacks/redux-loop/.eslintignore new file mode 100644 index 00000000..683e721c --- /dev/null +++ b/spikes/stacks/redux-loop/.eslintignore @@ -0,0 +1,3 @@ +/node_modules +coverage +.nyc_output diff --git a/spikes/stacks/redux-loop/.eslintrc b/spikes/stacks/redux-loop/.eslintrc new file mode 100644 index 00000000..19bd88dc --- /dev/null +++ b/spikes/stacks/redux-loop/.eslintrc @@ -0,0 +1,29 @@ +{ + "extends": "semistandard", + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 7, + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "plugins": [ + "babel", + "react" + ], + "rules": { + "generator-star-spacing": 0, + "babel/generator-star-spacing": 1, + "space-before-function-paren": [2, "never"], + "react/jsx-uses-react": 2, + "react/jsx-uses-vars": 2, + "react/react-in-jsx-scope": 2, + "object-curly-newline": ["error", { + "minProperties": 1 + }], + "sort-vars": ["error", { + "ignoreCase": true + }] + } +} \ No newline at end of file diff --git a/spikes/stacks/redux-loop/.gitignore b/spikes/stacks/redux-loop/.gitignore new file mode 100644 index 00000000..c21b0ba0 --- /dev/null +++ b/spikes/stacks/redux-loop/.gitignore @@ -0,0 +1,4 @@ +/node_modules +coverage +.nyc_output +npm-debug.log diff --git a/spikes/stacks/redux-loop/.storybook/config.js b/spikes/stacks/redux-loop/.storybook/config.js new file mode 100644 index 00000000..d17d412a --- /dev/null +++ b/spikes/stacks/redux-loop/.storybook/config.js @@ -0,0 +1,5 @@ +const Storybook = require('@kadira/storybook'); + +Storybook.configure(() => { + require('../stories'); +}, module); diff --git a/spikes/stacks/redux-loop/package.json b/spikes/stacks/redux-loop/package.json new file mode 100644 index 00000000..c5c852e4 --- /dev/null +++ b/spikes/stacks/redux-loop/package.json @@ -0,0 +1,75 @@ +{ + "name": "redux-loop-spike", + "private": true, + "license": "private", + "main": "src/server/index.js", + "scripts": { + "start": "node .", + "lint": "eslint .", + "test": "NODE_ENV=test nyc ava test/*.js --fail-fast --verbose --tap", + "open": "nyc report --reporter=html & open coverage/index.html", + "coverage": "nyc check-coverage --statements 100 --functions 100 --lines 100 --branches 100", + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook" + }, + "dependencies": { + "aphrodite": "^0.6.0", + "babel-eslint": "^7.0.0", + "babel-loader": "^6.2.5", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.16.0", + "babel-plugin-transform-object-rest-spread": "^6.16.0", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-preset-es2015": "^6.16.0", + "babel-preset-react": "^6.16.0", + "babel-preset-react-hmre": "^1.1.1", + "babel-runtime": "^6.11.6", + "component-emitter": "^1.2.1", + "crosstab": "^0.2.12", + "express-graphql": "^0.5.4", + "graphql": "^0.7.2", + "graphql-fetch": "^1.0.0", + "json-loader": "^0.5.4", + "lodash.find": "^4.6.0", + "lodash.get": "^4.4.2", + "lodash.values": "^4.3.0", + "node-uuid": "^1.4.7", + "react": "^15.3.2", + "react-dom": "^15.3.2", + "react-hot-loader": "^3.0.0-beta.6", + "react-intl": "^2.1.5", + "react-redux": "^4.4.5", + "react-router": "^4.0.0-alpha.4", + "reduce-reducers": "^0.1.2", + "redux": "^3.6.0", + "redux-actions": "^0.12.0", + "redux-batched-actions": "^0.1.3", + "redux-logger": "^2.7.0", + "redux-loop": "^2.2.2", + "redux-promise-middleware": "^4.1.0", + "reselect": "^2.5.4", + "webpack": "^1.13.2", + "webpack-dev-server": "^1.16.2" + }, + "devDependencies": { + "@kadira/storybook": "^2.24.0", + "ava": "^0.16.0", + "babel-register": "^6.16.3", + "enzyme": "^2.4.1", + "eslint": "^3.8.0", + "eslint-config-semistandard": "^7.0.0", + "eslint-config-standard": "^6.2.0", + "eslint-plugin-babel": "^3.3.0", + "eslint-plugin-promise": "^3.0.0", + "eslint-plugin-react": "^6.4.1", + "eslint-plugin-standard": "^2.0.1", + "nyc": "^8.3.1", + "react-addons-test-utils": "^15.3.2" + }, + "ava": { + "require": [ + "babel-register" + ], + "babel": "inherit" + } +} diff --git a/spikes/stacks/redux-loop/src/client/api.js b/spikes/stacks/redux-loop/src/client/api.js new file mode 100644 index 00000000..6f8e6afc --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/api.js @@ -0,0 +1,49 @@ +var fetch = require('graphql-fetch')(`${document.location.origin}/graphql`); + +exports.fetchChanges = () => { + return fetch(` + query { + changes { + id, + product { + id, + artist, + title, + label, + format, + price, + currency + }, + price, + currency + } + } + `).then(({ + data + }) => { + return data.changes; + }); +}; + +exports.removeChange = (id) => { + console.log(` + mutation { + removeChange(id: "${id}") + } +`); + return fetch(` + mutation { + removeChange(id: "${id}") { + id + } + } + `).then(({ + errors + }) => { + if (!errors) { + return; + } + + throw new Error(errors[0].message); + }); +}; diff --git a/spikes/stacks/redux-loop/src/client/components/change.js b/spikes/stacks/redux-loop/src/client/components/change.js new file mode 100644 index 00000000..3a317d26 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/components/change.js @@ -0,0 +1,14 @@ +const React = require('react'); + +module.exports = ({ + price, + currency, + product, + id +}) => { + return ( +

+ {product.artist}: {product.title} - {product.currencr}{product.price} > {currency}{price} +

+ ); +}; diff --git a/spikes/stacks/redux-loop/src/client/components/changes.js b/spikes/stacks/redux-loop/src/client/components/changes.js new file mode 100644 index 00000000..31bd1fa1 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/components/changes.js @@ -0,0 +1,32 @@ +const ReactRouter = require('react-router'); +const React = require('react'); + +const { + Link +} = ReactRouter; + +module.exports = ({ + changes = [], + pathname +}) => { + const lis = changes.map(({ + price, + currency, + product, + id + }) => { + return ( +
  • + + {product.artist}: {product.title} - {product.currencr}{product.price} > {currency}{price} + +
  • + ); + }); + + return ( + + ); +}; diff --git a/spikes/stacks/redux-loop/src/client/components/loader.js b/spikes/stacks/redux-loop/src/client/components/loader.js new file mode 100644 index 00000000..f9ec9b3f --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/components/loader.js @@ -0,0 +1,51 @@ +const React = require('react'); +const Loader = require('react-loader'); + +module.exports = React.createClass({ + fetch: function() { + const { + fetch, + loading, + loaded + } = this.props; + + if (fetch && !loading && !loaded) { + fetch(); + } + }, + componentDidMount: function() { + this.fetch(); + }, + componentDidUpdate: function(nextProps) { + const updated = ( + nextProps.loaded !== this.props.loaded && + nextProps.loading !== this.props.loading + ); + + if (!updated) { + return; + } + + this.fetch(); + }, + render: function() { + const { + loading, + loaded, + render, + children + } = this.props; + + const _loaded = !loading && !loaded; + const component = _loaded ? (children || render()) : null; + + return ( + + {component} + + ); + } +}); diff --git a/spikes/stacks/redux-loop/src/client/components/printers.js b/spikes/stacks/redux-loop/src/client/components/printers.js new file mode 100644 index 00000000..c5e71317 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/components/printers.js @@ -0,0 +1,39 @@ +const React = require('react'); + +module.exports = ({ + printers = [], + locked = '', + onClick +}) => { + const _onClick = (id) => { + return () => { + onClick(id); + }; + }; + + const lis = printers.map(({ + name, + lock, + id + }) => { + const msg = (() => { + if (!lock) { + return ''; + } + + return (locked === id) ? '(Locked to you)' : `(Locked to ${lock})`; + })(); + + return ( +
  • + {name} {msg} +
  • + ); + }); + + return ( + + ); +}; diff --git a/spikes/stacks/redux-loop/src/client/containers/app.js b/spikes/stacks/redux-loop/src/client/containers/app.js new file mode 100644 index 00000000..ab7463fe --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/containers/app.js @@ -0,0 +1,55 @@ +const ReactRedux = require('react-redux'); +const ReactRouter = require('react-router'); +const React = require('react'); + +const NotFound = require('./not-found'); +const Home = require('./home'); +const Print = require('./print'); + +const actions = require('../state/actions'); + +const { + connect +} = ReactRedux; + +const { + Miss, + Match +} = ReactRouter; + +const { + updateRouter +} = actions; + +const App = connect()(React.createClass({ + componentDidMount: function() { + require('../worker').on('action', this.props.dispatch); + }, + render: function() { + const { + children, + router, + dispatch + } = this.props; + + // ugly hack needed because of a limitation of react-router api + // that doens't pass it's instance to matched routes + dispatch(updateRouter(router)); + + return ( +
    + {children} +
    + ); + } +})); + +module.exports = (props) => { + return ( + + + + + + ); +}; diff --git a/spikes/stacks/redux-loop/src/client/containers/home.js b/spikes/stacks/redux-loop/src/client/containers/home.js new file mode 100644 index 00000000..952a726a --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/containers/home.js @@ -0,0 +1,14 @@ +const ReactRouter = require('react-router'); +const React = require('react'); + +const { + Link +} = ReactRouter; + +module.exports = () => { + return ( +
    + Start +
    + ); +}; diff --git a/spikes/stacks/redux-loop/src/client/containers/not-found.js b/spikes/stacks/redux-loop/src/client/containers/not-found.js new file mode 100644 index 00000000..ed7e90b8 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/containers/not-found.js @@ -0,0 +1,7 @@ +const React = require('react'); + +module.exports = () => { + return ( +

    Not found

    + ); +}; diff --git a/spikes/stacks/redux-loop/src/client/containers/print.js b/spikes/stacks/redux-loop/src/client/containers/print.js new file mode 100644 index 00000000..93730f49 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/containers/print.js @@ -0,0 +1,136 @@ +const ReactRouter = require('react-router'); +const ReactRedux = require('react-redux'); +const React = require('react'); +const find = require('lodash.find'); + +const Loader = require('../components/loader'); +const Printers = require('../components/printers'); +const Changes = require('../components/changes'); +const Change = require('../components/change'); + +const actions = require('../state/actions'); + +const { + fetchChanges, + lockPrinter, + print, + transitionTo +} = actions; + +const { + Match +} = ReactRouter; + +const { + connect +} = ReactRedux; + +const Print = ({ + pathname, + printers = [], + changes = [], + lockPrinter, + fetchChanges, + onPrint, + loaded, + loading, + locked, + router +}) => { + const allChanges = () => { + return ( +
    +

    Changes

    + + + +
    + ); + }; + + const singleChange = ({ + params + }) => { + const change = find(changes, (change) => { + return change.id === params.id; + }); + + if (!change) { + return ( +

    Change not found

    + ); + } + + const _onPrint = (id) => { + return () => { + return onPrint(id); + }; + }; + + // TODO: don't load all changes + return ( +
    +

    Change

    + + + + +
    + ); + }; + + return ( +
    +
    +

    Printers

    + +
    + + + +
    + ); +}; + +const mapStateToProps = (state) => { + return { + loaded: state.ui.changes.loaded, + loading: state.ui.changes.loading, + changes: state.data.changes, + printers: state.data.printers, + locked: state.ui.printers.locked + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + lockPrinter: (id) => { + return dispatch(lockPrinter(id)); + }, + fetchChanges: () => { + return dispatch(fetchChanges()); + }, + onPrint: (id) => { + return dispatch(print(id)).then(() => { + return dispatch(transitionTo('/print')); + }); + } + }; +}; + +module.exports = connect(mapStateToProps, mapDispatchToProps)(Print); diff --git a/spikes/stacks/redux-loop/src/client/index.js b/spikes/stacks/redux-loop/src/client/index.js new file mode 100644 index 00000000..10c5a842 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/index.js @@ -0,0 +1,19 @@ +require('./worker'); // singleton + +const React = require('react'); +const ReactDOM = require('react-dom'); + +const render = () => { + const Root = require('./root'); + + ReactDOM.render( + , + document.getElementById('root') + ); +}; + +render(); + +if (module.hot) { + module.hot.accept('./root', render); +} diff --git a/spikes/stacks/redux-loop/src/client/intl.json b/spikes/stacks/redux-loop/src/client/intl.json new file mode 100644 index 00000000..077404aa --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/intl.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/spikes/stacks/redux-loop/src/client/root.js b/spikes/stacks/redux-loop/src/client/root.js new file mode 100644 index 00000000..4ecda442 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/root.js @@ -0,0 +1,46 @@ +const React = require('react'); +const ReactHotLoader = require('react-hot-loader'); +const ReactRouter = require('react-router'); +const ReactRedux = require('react-redux'); +const ReactIntl = require('react-intl'); + +const App = require('./containers/app'); +const store = require('./state/store'); + +const { + AppContainer +} = ReactHotLoader; + +const { + BrowserRouter +} = ReactRouter; + +const { + Provider +} = ReactRedux; + +const { + IntlProvider +} = ReactIntl; + +// http://stackoverflow.com/a/38150585 +const locale = ( + navigator.languages && navigator.languages[0] || // Chrome / Firefox + navigator.language || // All browsers + navigator.userLanguage // IE <= 10 +); + +module.exports = () => { + return ( + + + + {App} + + + + ); +}; diff --git a/spikes/stacks/redux-loop/src/client/state/actions/app.js b/spikes/stacks/redux-loop/src/client/state/actions/app.js new file mode 100644 index 00000000..65b4371e --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/actions/app.js @@ -0,0 +1,25 @@ +const ReduxActions = require('redux-actions'); +const app = require('../../../../package.json').name; +const selectors = require('../selectors'); + +const { + router +} = selectors; + +const { + createAction +} = ReduxActions; + +const UPDATE_ROUTER = `${app}/changes/UPDATE_ROUTER`; + +const updateRouter = createAction(UPDATE_ROUTER); + +const transitionTo = (pathname) => (dispatch, getState) => { + return router(getState()).transitionTo(pathname); +}; + +module.exports = { + UPDATE_ROUTER, + updateRouter, + transitionTo +}; diff --git a/spikes/stacks/redux-loop/src/client/state/actions/changes.js b/spikes/stacks/redux-loop/src/client/state/actions/changes.js new file mode 100644 index 00000000..389e4249 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/actions/changes.js @@ -0,0 +1,28 @@ +const app = require('../../../../package.json').name; +const api = require('../../api'); + +const FETCH_CHANGES = `${app}/changes/FETCH_CHANGES`; +const REMOVE_CHANGE = `${app}/changes/REMOVE_CHANGE`; + +const fetchChanges = () => { + return { + type: FETCH_CHANGES, + payload: api.fetchChanges() + }; +}; + +const removeChange = (id) => (dispatch) => { + return dispatch({ + type: REMOVE_CHANGE, + payload: api.removeChange(id) + }).then(() => { + return dispatch(fetchChanges()); + }); +}; + +module.exports = { + FETCH_CHANGES, + REMOVE_CHANGE, + fetchChanges, + removeChange +}; diff --git a/spikes/stacks/redux-loop/src/client/state/actions/index.js b/spikes/stacks/redux-loop/src/client/state/actions/index.js new file mode 100644 index 00000000..997b6edd --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/actions/index.js @@ -0,0 +1,5 @@ +module.exports = { + ...require('./app'), + ...require('./printers'), + ...require('./changes') +}; diff --git a/spikes/stacks/redux-loop/src/client/state/actions/printers.js b/spikes/stacks/redux-loop/src/client/state/actions/printers.js new file mode 100644 index 00000000..6bbde8df --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/actions/printers.js @@ -0,0 +1,159 @@ +const ReduxActions = require('redux-actions'); +const app = require('../../../../package.json').name; +const find = require('lodash.find'); +const changes = require('./changes'); + +const { + createAction +} = ReduxActions; + +const { + removeChange +} = changes; + +const UPDATE_PRINTERS = `${app}/printers/UPDATE_PRINTERS`; +const UPDATE_WORKER_ID = `${app}/printers/UPDATE_WORKER_ID`; +const EMIT_LOCK_PRINTER = `${app}/printers/EMIT_LOCK_PRINTER`; +const LOCK_PRINTER = `${app}/printers/LOCK_PRINTER`; +const PRINTER_NOT_FOUND = `${app}/printers/PRINTER_NOT_FOUND`; +const DO_PRINT = `${app}/printers/DO_PRINT`; +const EMIT_PRINT_JOB = `${app}/printers/EMIT_PRINT_JOB`; +const CONFIRM_OVERRIDE = `${app}/printers/CONFIRM_OVERRIDE`; +const CONFIRM_LOCK = `${app}/printers/CONFIRM_LOCK`; +const PROMPT_LOCK = `${app}/printers/PROMPT_LOCK`; +const PROMPT_CONFIRM_LOCK = `${app}/printers/PROMPT_CONFIRM_LOCK`; +const PRINT = `${app}/printers/PRINT`; + +// confirm should be an async op, +// let's mock it that way +const confirm = (msg) => { + return new Promise((resolve, reject) => { + resolve(window.confirm(msg)); + }); +}; + +// prompt should be an async op, +// let's mock it that way +const prompt = (msg) => { + return new Promise((resolve, reject) => { + resolve(window.prompt(msg)); + }); +}; + +// alert should be an async op, +// let's mock it that way +const alert = (msg) => { + return new Promise((resolve, reject) => { + resolve(window.alert(msg)); + }); +}; + +const confirmOverride = createAction(CONFIRM_OVERRIDE); +const confirmLock = createAction(CONFIRM_LOCK); +const promptLock = createAction(PROMPT_LOCK); +const updatePrinters = createAction(UPDATE_PRINTERS); +const updateWorkerId = createAction(UPDATE_WORKER_ID); +const promptOrConfirmLock = createAction(PROMPT_CONFIRM_LOCK); +const lockPrinter = createAction(LOCK_PRINTER); +const printerNotFound = createAction(PRINTER_NOT_FOUND); +const print = createAction(PRINT); + +// const print = (changeId) => (dispatch, getState) => { +// const { +// ui, +// data +// } = getState(); +// +// const { +// printers +// } = data; +// +// const { +// printers: { +// locked +// } +// } = ui; +// +// const worker = require('../../worker'); +// +// const _print = () => { +// return dispatch({ +// type: EMIT_PRINT_JOB, +// payload: worker.dispatch({ +// type: 'PRINT', +// payload: changeId +// }) +// }).then(() => { +// return dispatch(removeChange(changeId)); +// }); +// }; +// +// const lock = (printerId) => { +// return dispatch({ +// type: EMIT_LOCK_PRINTER, +// payload: worker.dispatch({ +// type: 'LOCK_PRINTER', +// payload: printerId +// }) +// }).then(_print); +// }; +// +// const askToOverride = (printerId) => { +// const msg = `Printer ${printerId} already locked! Do you want to override?`; +// return confirm(msg).then((yes) => { +// return yes ? lock(printerId) : null; +// }); +// }; +// +// const askToLock = () => { +// const msg = `Please select a printer to lock: ${ +// printers.map(({ +// id, +// name +// }) => { +// return `\n(${id}) ${name}`; +// }) +// }`; +// +// return prompt(msg).then((printerId) => { +// const printer = find(printers, ['id', printerId]); +// +// if (!printer) { +// return alert(`Printer ${printerId} not found. Try again`).then(() => { +// return print(printerId)(dispatch, getState); +// }); +// } +// +// return printer.lock ? askToOverride(printerId) : lock(printerId); +// }); +// }; +// +// return !locked ? askToLock() : _print(); +// }; + +module.exports = { + UPDATE_PRINTERS, + UPDATE_WORKER_ID, + EMIT_LOCK_PRINTER, + LOCK_PRINTER, + PRINTER_NOT_FOUND, + DO_PRINT, + EMIT_PRINT_JOB, + CONFIRM_OVERRIDE, + CONFIRM_LOCK, + PROMPT_LOCK, + PROMPT_CONFIRM_LOCK, + PRINT, + PRINTER_NOT_FOUND, + confirm, + prompt, + alert, + updatePrinters, + updateWorkerId, + confirmOverride, + confirmLock, + promptLock, + promptOrConfirmLock, + lockPrinter, + print +}; diff --git a/spikes/stacks/redux-loop/src/client/state/loop-promise.js b/spikes/stacks/redux-loop/src/client/state/loop-promise.js new file mode 100644 index 00000000..4be218e4 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/loop-promise.js @@ -0,0 +1,39 @@ +const ReduxActions = require('redux-actions'); +const ReduxLoop = require('redux-loop'); + +const { + Effects +} = ReduxLoop; + +const { + createAction +} = ReduxActions; + +module.exports = ({ + type, + payload, + ctx +}) => { + return Effects.batch([ + Effects.constant({ + type: `${type}_PENDING` + }), + Effects.promise(() => { + return payload + .then((res) => { + return { + type: `${type}_FULFILLED`, + payload: res, + ctx + }; + }) + .catch((err) => { + return { + type: `${type}_REJECTED`, + payload: err, + ctx + }; + }); + }) + ]); +}; diff --git a/spikes/stacks/redux-loop/src/client/state/reduce-reducers.js b/spikes/stacks/redux-loop/src/client/state/reduce-reducers.js new file mode 100644 index 00000000..bf5e143c --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/reduce-reducers.js @@ -0,0 +1,44 @@ +const ReduxLoop = require('redux-loop'); + +const { + loop, + isLoop, + getEffect, + getModel, + Effects +} = ReduxLoop; + +const optimizeBatch = (effects) => { + switch(effects.length) { + case 0: + return Effects.none(); + case 1: + return effects[0]; + default: + return Effects.batch(effects); + } +}; + +module.exports = (...reducers) => { + return (state, action) => { + const effects = []; + + const next = reducers.reduce((sum, reducer) => { + let state = reducer(sum, action); + + if (!isLoop(state)) { + return state; + } + + effects.push(getEffect(state)); + state = getModel(state); + + return state; + }, state); + + return loop( + next, + optimizeBatch(effects) + ); + }; +}; diff --git a/spikes/stacks/redux-loop/src/client/state/reducers/app.js b/spikes/stacks/redux-loop/src/client/state/reducers/app.js new file mode 100644 index 00000000..fc47f8e8 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/reducers/app.js @@ -0,0 +1,19 @@ +const ReduxActions = require('redux-actions'); +const actions = require('../actions'); + +const { + handleActions +} = ReduxActions; + +const { + UPDATE_ROUTER +} = actions; + +exports.ui = handleActions({ + [UPDATE_ROUTER]: (state, action) => { + return { + ...state, + router: action.payload + }; + } +}, {}); diff --git a/spikes/stacks/redux-thunk/src/client/reducers/changes.js b/spikes/stacks/redux-loop/src/client/state/reducers/changes.js similarity index 70% rename from spikes/stacks/redux-thunk/src/client/reducers/changes.js rename to spikes/stacks/redux-loop/src/client/state/reducers/changes.js index ab191a59..7f5155c6 100644 --- a/spikes/stacks/redux-thunk/src/client/reducers/changes.js +++ b/spikes/stacks/redux-loop/src/client/state/reducers/changes.js @@ -1,17 +1,13 @@ const ReduxActions = require('redux-actions'); -const app = require('../../../package.json').name; -const api = require('../api'); +const actions = require('../actions'); const { - createAction, handleActions } = ReduxActions; const { - fetchChanges -} = api; - -const FETCH_CHANGES = `${app}/changes/FETCH_CHANGES`; + FETCH_CHANGES +} = actions; exports.data = handleActions({ [`${FETCH_CHANGES}_FULFILLED`]: (state, action) => { @@ -45,12 +41,3 @@ exports.ui = handleActions({ loading: false, loaded: false }); - -exports.actions = { - fetchChanges: () => { - return { - type: FETCH_CHANGES, - payload: fetchChanges() - }; - } -}; diff --git a/spikes/stacks/redux-loop/src/client/state/reducers/index.js b/spikes/stacks/redux-loop/src/client/state/reducers/index.js new file mode 100644 index 00000000..d21d14fd --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/reducers/index.js @@ -0,0 +1,27 @@ +const reduceReducers = require('../reduce-reducers'); +const ReduxLoop = require('redux-loop'); + +const app = require('./app'); +const printers = require('./printers'); +const changes = require('./changes'); + +const { + combineReducers +} = ReduxLoop; + +module.exports = () => reduceReducers( + // app.global, + printers.global, + // changes.global + combineReducers({ + data: combineReducers({ + printers: printers.data, + changes: changes.data + }), + ui: combineReducers({ + changes: changes.ui, + printers: printers.ui, + app: app.ui + }) + }) +); diff --git a/spikes/stacks/redux-loop/src/client/state/reducers/printers.js b/spikes/stacks/redux-loop/src/client/state/reducers/printers.js new file mode 100644 index 00000000..a0de895f --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/reducers/printers.js @@ -0,0 +1,238 @@ +const loopPromise = require('../loop-promise'); +const ReduxActions = require('redux-actions'); +const ReduxLoop = require('redux-loop'); +const find = require('lodash.find'); +const actions = require('../actions'); + +const { + handleActions +} = ReduxActions; + +const { + loop, + Effects +} = ReduxLoop; + +const { + UPDATE_WORKER_ID, + UPDATE_PRINTERS, + LOCK_PRINTER, + PROMPT_CONFIRM_LOCK, + CONFIRM_LOCK, + PROMPT_LOCK, + CONFIRM_OVERRIDE, + EMIT_LOCK_PRINTER, + PRINTER_NOT_FOUND, + printerNotFound, + confirmOverride, + promptOrConfirmLock, + confirmLock, + promptLock, + confirm, + alert, + prompt +} = actions; + +exports.data = handleActions({ + [UPDATE_PRINTERS]: (state, action) => { + return action.payload; + } +}, []); + +exports.ui = handleActions({ + [UPDATE_WORKER_ID]: (state, action) => { + return { + ...state, + id: action.payload + }; + }, + [UPDATE_PRINTERS]: (state, action) => { + const locked = (find(action.payload, (printer) => { + return ( + printer.lock && + printer.lock === state.id + ); + }) || {}).id || ''; + + return { + ...state, + locked + }; + } +}, { + id: '', + locked: '' +}); + +exports.global = handleActions({ + [LOCK_PRINTER]: (state, { + payload + }) => { + const { + ui, + data + } = state; + + const { + printers + } = data; + + const { + printers: { + locked + } + } = ui; + + if (locked === payload) { + return state; + } + + const printer = find(printers, ['id', payload]); + + if (!printer) { + return loop( + state, + Effects.constant(printerNotFound(payload)) + ); + } + + const action = (printer.lock ? confirmOverride : promptOrConfirmLock)(payload); + + return loop( + state, + Effects.constant(action) + ); + }, + [PROMPT_CONFIRM_LOCK]: (state, { + payload + }) => { + const action = (payload ? confirmLock : promptLock)(payload); + + return loop( + state, + Effects.constant(action) + ); + }, + [CONFIRM_LOCK]: (state, { + payload + }) => { + const msg = `Do you want to lock printer ${payload}?`; + + return loop( + state, + loopPromise({ + type: CONFIRM_LOCK, + payload: confirm(msg), + ctx: payload + }) + ); + }, + [`${CONFIRM_LOCK}_FULFILLED`]: (state, { + payload, + ctx + }) => { + const worker = require('../../worker'); + + return !payload ? state : loop( + state, + loopPromise({ + type: EMIT_LOCK_PRINTER, + payload: worker.dispatch({ + type: 'LOCK_PRINTER', + payload: ctx + }) + }) + ); + }, + [PROMPT_LOCK]: (state, action) => { + const { + ui, + data + } = state; + + const { + printers + } = data; + + const msg = `Please select a printer to lock: ${ + printers.map(({ + id, + name + }) => { + return `\n(${id}) ${name}`; + }) + }`; + + return loop( + state, + loopPromise({ + type: PROMPT_LOCK, + payload: prompt(msg) + }) + ); + }, + [`${PROMPT_LOCK}_FULFILLED`]: (state, { + payload + }) => { + const { + data: { + printers + } + } = state; + + const printer = find(printers, ['id', payload]); + + if (!printer) { + return loop( + state, + Effects.constant(printerNotFound(payload)) + ); + } + + const worker = require('../../worker'); + + return loop( + state, + loopPromise({ + type: EMIT_LOCK_PRINTER, + payload: worker.dispatch({ + type: 'LOCK_PRINTER', + payload: ctx + }) + }) + ); + }, + [CONFIRM_OVERRIDE]: (state, { + payload + }) => { + const msg = `Printer ${payload} already locked! Do you want to override?`; + + return loop( + state, + loopPromise({ + type: CONFIRM_LOCK, + payload: confirm(msg), + ctx: payload + }) + ); + }, + [PRINTER_NOT_FOUND]: (state, action) => { + const msg = `Printer ${printerId} not found. Try again`; + + return loop( + state, + loopPromise({ + type: PRINTER_NOT_FOUND, + payload: alert(msg) + }) + ); + }, + [`${PRINTER_NOT_FOUND}_FULFILLED`]: (state, action) => { + return loop( + state, + Effects.constant({ + type: PROMPT_LOCK + }) + ); + } +}, {}); diff --git a/spikes/stacks/redux-loop/src/client/state/selectors.js b/spikes/stacks/redux-loop/src/client/state/selectors.js new file mode 100644 index 00000000..cac1592c --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/selectors.js @@ -0,0 +1,10 @@ +// const Reselect = require('reselect'); +const get = require('lodash.get'); + +const router = (state) => { + return get(state, 'ui.app.router'); +}; + +module.exports = { + router +}; diff --git a/spikes/stacks/redux-loop/src/client/state/store.js b/spikes/stacks/redux-loop/src/client/state/store.js new file mode 100644 index 00000000..b2ccd018 --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/state/store.js @@ -0,0 +1,30 @@ +const createReducer = require('./reducers'); +const enableBatching = require('redux-batched-actions').enableBatching; +const ReduxLoop = require('redux-loop'); +const promiseMiddleware = require('redux-promise-middleware').default; +const createLogger = require('redux-logger'); +const redux = require('redux'); + +const { + install +} = ReduxLoop; + +const { + createStore, + compose, + applyMiddleware +} = redux; + +module.exports = (initialState = Object.freeze({})) => { + return createStore( + enableBatching(createReducer()), + initialState, + compose( + applyMiddleware( + // createLogger(), + promiseMiddleware() + ), + install() + ) + ); +}; diff --git a/spikes/stacks/redux-loop/src/client/worker.js b/spikes/stacks/redux-loop/src/client/worker.js new file mode 100644 index 00000000..2bd5d47e --- /dev/null +++ b/spikes/stacks/redux-loop/src/client/worker.js @@ -0,0 +1,144 @@ +const uuid = require('node-uuid'); +const Emitter = require('component-emitter'); +const crosstab = require('crosstab'); +const values = require('lodash.values'); +const actions = require('./state/actions'); + +const emitter = module.exports = new Emitter(); +let isMaster = crosstab.util.tabs['MASTER_TAB'].id === crosstab.id; +const bridge = new Map(); + +const { + updatePrinters, + updateWorkerId +} = actions; + +const printers = { + '1': { + id: '1', + name: 'Main printer', + lock: '' + }, + '2': { + id: '2', + name: 'Handled printer', + lock: '' + } +}; + +const handlers = { + 'PRINT': (action, fn) => { + fn(); + }, + 'LOCK_PRINTER': (action, fn) => { + const alreadyLocked = values(printers).filter((printer) => { + return printer.lock === action._origin; + }); + + alreadyLocked.forEach((printer) => { + printers[printer.id] = { + ...printers[printer.id], + lock: '' + }; + }); + + printers[action.payload] = { + ...printers[action.payload], + lock: action._origin + }; + + fn(); + } +}; + +crosstab.util.events.on('message', ({ + data, + origin +}) => { + if (origin === crosstab.id) { + return; + } + + if (!data || !data.type) { + return; + } + + if (!data._id) { + return emitter.emit('action', data); + } + + const b = bridge.get(data._id); + + if (b) { + data.error ? b.reject(new Error(data.error)) : b.resolve(data.payload); + return bridge.delete(data._id); + } + + if (!handlers[data.type]) { + return emitter.emit('action', data); + } + + handlers[data.type]({ + ...data, + _origin: origin + }, (err, res) => { + crosstab.broadcast('message', { + ...data, + error: err && err.message + }, origin); + }); +}); + +crosstab.util.events.on(crosstab.util.eventTypes.becomeMaster, () => { + isMaster = true; +}); + +crosstab.util.events.on(crosstab.util.eventTypes.demoteFromMaster, () => { + isMaster = false; +}); + +const dispatch = module.exports.dispatch = (action, tab) => { + if (isMaster && !tab) { + if (handlers[action.type]) { + return new Promise(function(resolve, reject) { + handlers[action.type]({ + ...action, + _origin: crosstab.id + }, function(err, res) { + return err ? reject(err) : resolve(res); + }); + }); + } + } + + const id = uuid.v4(); + + const then = new Promise((resolve, reject) => { + bridge.set(id, { + resolve, + reject + }); + }); + + crosstab.broadcast('message', { + ...action, + _id: id + }, tab); + + return then; +}; + +setTimeout(function() { + emitter.emit('action', updateWorkerId(crosstab.id)); +}, 450); + +setInterval(() => { + if (!isMaster) { + return; + } + + const action = updatePrinters(values(printers)); + + emitter.emit('action', action); + dispatch(action); +}, 1000); diff --git a/spikes/stacks/redux-loop/src/config/webpack.config.js b/spikes/stacks/redux-loop/src/config/webpack.config.js new file mode 100644 index 00000000..98343222 --- /dev/null +++ b/spikes/stacks/redux-loop/src/config/webpack.config.js @@ -0,0 +1,41 @@ +const webpack = require('webpack'); +const path = require('path'); + +module.exports = { + debug: true, + devtool: 'eval', + context: path.join(__dirname, '../client'), + entry: [ + 'webpack-dev-server/client?http://localhost:3000', + 'webpack/hot/only-dev-server', + 'react-hot-loader/patch', + './index.js' + ], + output: { + path: path.join(__dirname, '../../static'), + publicPath: '/static/', + filename: 'bundle.js' + }, + plugins: [ + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin() + ], + module: { + loaders: [{ + test: /js?$/, + exclude: /node_modules/, + include: [ + path.join(__dirname, '../client') + ], + loaders: ['babel'] + }, { + test: /\.json?$/, + exclude: /node_modules/, + include: [ + path.join(__dirname, '../client'), + path.join(__dirname, '../../') // for package.json + ], + loaders: ['json'] + }] + } +}; diff --git a/spikes/stacks/redux-loop/src/server/data/changes.json b/spikes/stacks/redux-loop/src/server/data/changes.json new file mode 100644 index 00000000..fb3f42b7 --- /dev/null +++ b/spikes/stacks/redux-loop/src/server/data/changes.json @@ -0,0 +1,57 @@ +{ + "1": { + "id": "1", + "format": "LP", + "product": "2267150", + "price": 19, + "currency": "€" + }, + "2": { + "id": "2", + "product": "5347347", + "price": 16, + "currency": "€" + }, + "3": { + "id": "3", + "product": "2485033", + "price": 20, + "currency": "€" + }, + "3": { + "id": "3", + "product": "4389520", + "price": 17, + "currency": "€" + }, + "4": { + "id": "4", + "product": "2942482", + "price": 15, + "currency": "€" + }, + "5": { + "id": "5", + "product": "6549215", + "price": 12, + "currency": "€" + }, + "6": { + "id": "6", + "product": "4192697", + "price": 8, + "currency": "€" + }, + "7": { + "id": "7", + "product": "1248296", + "price": 27, + "currency": "€" + }, + "8": { + "id": "8", + "product": "1174296", + "price": 17, + "currency": "€" + } +} diff --git a/spikes/stacks/redux-loop/src/server/data/products.json b/spikes/stacks/redux-loop/src/server/data/products.json new file mode 100644 index 00000000..a683d718 --- /dev/null +++ b/spikes/stacks/redux-loop/src/server/data/products.json @@ -0,0 +1,83 @@ +{ + "2267150": { + "artist": "The National", + "title": "High Violet", + "label": "4AD, 4AD", + "format": "LP", + "id": "2267150", + "price": 24, + "currency": "€" + }, + "5347347": { + "artist": "Sensible Soccers", + "title": "8", + "label": "Groovement Organic Series", + "format": "LP", + "id": "5347347", + "price": 25, + "currency": "€" + }, + "2485033": { + "artist": "Eddie Vedder", + "title": "Into The Wild", + "label": "Music On Vinyl", + "format": "LP", + "id": "2485033", + "price": 33, + "currency": "€" + }, + "4389520": { + "artist": "Daughter (2)", + "title": "If You Leave", + "label": "4AD", + "format": "LP", + "id": "4389520", + "price": 22, + "currency": "€" + }, + "2942482": { + "artist": "Bon Iver", + "title": "Bon Iver, Bon Iver", + "label": "4AD, Jagjaguwar", + "format": "LP", + "id": "2942482", + "price": 29, + "currency": "€" + }, + "6549215": { + "artist": "Sufjan Stevens", + "title": "Carrie & Lowell", + "label": "Asthmatic Kitty Records", + "format": "LP", + "id": "6549215", + "price": 20, + "currency": "€" + }, + "4192697": { + "artist": "Uzi & Ari", + "title": "It Is Freezing Out", + "label": "Own Records", + "format": "LP", + "id": "4192697", + "price": 17, + "currency": "€" + }, + "1248296": { + "artist": "Various", + "title": "Juno (Music From The Motion Picture)", + "label": "Rhino Records (2), Fox Music, Fox Searchlight Pictures", + "format": "LP", + "id": "1248296", + "price": 36, + "currency": "€" + }, + "1174296": { + "artist": "Radiohead", + "title": "In Rainbows", + "label": "XL Recordings", + "format": "LP", + "id": "1174296", + "price": 19, + "currency": "€" + } +} diff --git a/spikes/stacks/redux-loop/src/server/index.js b/spikes/stacks/redux-loop/src/server/index.js new file mode 100644 index 00000000..a503007e --- /dev/null +++ b/spikes/stacks/redux-loop/src/server/index.js @@ -0,0 +1,28 @@ +const webpack = require('webpack'); +const webpackConfig = require('../config/webpack.config.js'); +const WebpackDevServer = require('webpack-dev-server'); +const graphqlHTTP = require('express-graphql'); +const schema = require('./schema'); + +// WebpackDevServer should only be used for dev +const server = new WebpackDevServer(webpack(webpackConfig), { + hot: true, + historyApiFallback: { + index: './static/index.html' + }, + setup: function(app) { + app.use('/graphql', graphqlHTTP({ + schema: schema, + graphiql: true + })); + }, + publicPath: webpackConfig.output.publicPath +}); + +server.listen(3000, 'localhost', (err) => { + if (err) { + console.log(err); + } + + console.log('Listening at localhost:3000'); +}); diff --git a/spikes/stacks/redux-loop/src/server/schema.js b/spikes/stacks/redux-loop/src/server/schema.js new file mode 100644 index 00000000..521d370b --- /dev/null +++ b/spikes/stacks/redux-loop/src/server/schema.js @@ -0,0 +1,142 @@ +const changes = require('./data/changes.json'); +const products = require('./data/products.json'); +const values = require('lodash.values'); + +const { + GraphQLString, + GraphQLObjectType, + GraphQLInt, + GraphQLID, + GraphQLList, + GraphQLSchema +} = require('graphql'); + +const ProductType = new GraphQLObjectType({ + name: 'ProductType', + fields: { + id: { + type: GraphQLID + }, + artist: { + type: GraphQLString + }, + title: { + type: GraphQLString + }, + label: { + type: GraphQLString + }, + format: { + type: GraphQLString + }, + price: { + type: GraphQLInt + }, + currency: { + type: GraphQLString + } + } +}); + +const ChangeType = new GraphQLObjectType({ + name: 'ChangeType', + fields: { + id: { + type: GraphQLID + }, + product: { + type: ProductType, + resolve: (root, args) => { + return products[root.product]; + } + }, + price: { + type: GraphQLInt + }, + currency: { + type: GraphQLString + } + } +}); + +const query = new GraphQLObjectType({ + name: 'RootQueryType', + fields: { + products: { + type: new GraphQLList(ProductType), + args: { + id: { + type: GraphQLID + } + }, + resolve(root, args, ctx) { + return args.id ? [products[args.id]] : values(products); + } + }, + changes: { + type: new GraphQLList(ChangeType), + args: { + id: { + type: GraphQLID + }, + product: { + type: GraphQLID + } + }, + resolve(root, args, ctx) { + if (args.id) { + return [changes[args.id]]; + } + + if (!args.product) { + return values(changes); + } + + return values(changes).filter((change) => { + return change.product === args.product; + }); + } + } + } +}); + +const mutation = new GraphQLObjectType({ + name: 'RootMutationType', + fields: { + removeChange: { + type: ChangeType, + args: { + id: { + type: GraphQLID + }, + product: { + type: GraphQLID + } + }, + resolve(root, args, ctx) { + const ops = (() => { + if (args.id) { + return [args.id]; + } + + if (!args.product) { + return Object.keys(changes); + } + + return Object.keys(changes).filter((id) => { + return changes[id].product === args.product; + }); + })(); + + ops.forEach((id) => { + delete changes[id]; + }); + } + } + } +}); + +module.exports = new GraphQLSchema({ + query, + mutation +}); diff --git a/spikes/stacks/redux-loop/static/index.html b/spikes/stacks/redux-loop/static/index.html new file mode 100644 index 00000000..d36d2c24 --- /dev/null +++ b/spikes/stacks/redux-loop/static/index.html @@ -0,0 +1,10 @@ + + + + React Boilerplate + + +
    + + + diff --git a/spikes/stacks/redux-loop/stories/index.js b/spikes/stacks/redux-loop/stories/index.js new file mode 100644 index 00000000..3b098180 --- /dev/null +++ b/spikes/stacks/redux-loop/stories/index.js @@ -0,0 +1,10 @@ +const React = require('react'); +const Storybook = require('@kadira/storybook'); + +const Home = require('../src/client/containers/home'); + +const homeStories = Storybook.storiesOf('Home', module); + +homeStories.add('with nothing', () => ( + +)); diff --git a/spikes/stacks/redux-loop/test/index.js b/spikes/stacks/redux-loop/test/index.js new file mode 100644 index 00000000..62cf17c3 --- /dev/null +++ b/spikes/stacks/redux-loop/test/index.js @@ -0,0 +1,13 @@ +const test = require('ava'); +const enzyme = require('enzyme'); +const React = require('react'); + +const { + shallow +} = enzyme; + +test('renders without exploding', (t) => { + const Home = require('../src/client/containers/home'); + const wrapper = shallow(); + t.deepEqual(wrapper.length, 1); +}); diff --git a/spikes/stacks/redux-thunk/.eslintignore b/spikes/stacks/redux-thunk/.eslintignore new file mode 100644 index 00000000..683e721c --- /dev/null +++ b/spikes/stacks/redux-thunk/.eslintignore @@ -0,0 +1,3 @@ +/node_modules +coverage +.nyc_output diff --git a/spikes/stacks/redux-thunk/.gitignore b/spikes/stacks/redux-thunk/.gitignore index 52bbeb64..c21b0ba0 100644 --- a/spikes/stacks/redux-thunk/.gitignore +++ b/spikes/stacks/redux-thunk/.gitignore @@ -1,4 +1,4 @@ /node_modules coverage .nyc_output - +npm-debug.log diff --git a/spikes/stacks/redux-thunk/package.json b/spikes/stacks/redux-thunk/package.json index 27e8365f..0e1452dd 100644 --- a/spikes/stacks/redux-thunk/package.json +++ b/spikes/stacks/redux-thunk/package.json @@ -31,6 +31,7 @@ "graphql-fetch": "^1.0.0", "json-loader": "^0.5.4", "lodash.find": "^4.6.0", + "lodash.get": "^4.4.2", "lodash.values": "^4.3.0", "node-uuid": "^1.4.7", "react": "^15.3.2", @@ -46,6 +47,7 @@ "redux-logger": "^2.7.0", "redux-promise-middleware": "^4.1.0", "redux-thunk": "^2.1.0", + "reselect": "^2.5.4", "webpack": "^1.13.2", "webpack-dev-server": "^1.16.1" }, diff --git a/spikes/stacks/redux-thunk/src/client/actions.js b/spikes/stacks/redux-thunk/src/client/actions.js deleted file mode 100644 index c25f3629..00000000 --- a/spikes/stacks/redux-thunk/src/client/actions.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - ...require('./reducers/printers').actions, - ...require('./reducers/changes').actions -}; diff --git a/spikes/stacks/redux-thunk/src/client/api.js b/spikes/stacks/redux-thunk/src/client/api.js index 1714d755..6f8e6afc 100644 --- a/spikes/stacks/redux-thunk/src/client/api.js +++ b/spikes/stacks/redux-thunk/src/client/api.js @@ -24,3 +24,26 @@ exports.fetchChanges = () => { return data.changes; }); }; + +exports.removeChange = (id) => { + console.log(` + mutation { + removeChange(id: "${id}") + } +`); + return fetch(` + mutation { + removeChange(id: "${id}") { + id + } + } + `).then(({ + errors + }) => { + if (!errors) { + return; + } + + throw new Error(errors[0].message); + }); +}; diff --git a/spikes/stacks/redux-thunk/src/client/components/changes.js b/spikes/stacks/redux-thunk/src/client/components/changes.js index 216cfe4d..31bd1fa1 100644 --- a/spikes/stacks/redux-thunk/src/client/components/changes.js +++ b/spikes/stacks/redux-thunk/src/client/components/changes.js @@ -7,15 +7,8 @@ const { module.exports = ({ changes = [], - pathname, - onClick + pathname }) => { - const _onClick = (id) => { - return () => { - onClick(id); - }; - }; - const lis = changes.map(({ price, currency, diff --git a/spikes/stacks/redux-thunk/src/client/components/loader.js b/spikes/stacks/redux-thunk/src/client/components/loader.js index 0bc9c9c7..f9ec9b3f 100644 --- a/spikes/stacks/redux-thunk/src/client/components/loader.js +++ b/spikes/stacks/redux-thunk/src/client/components/loader.js @@ -30,7 +30,6 @@ module.exports = React.createClass({ }, render: function() { const { - fetch, loading, loaded, render, @@ -38,7 +37,7 @@ module.exports = React.createClass({ } = this.props; const _loaded = !loading && !loaded; - const component = _loaded ? (children ? children : render()) : null; + const component = _loaded ? (children || render()) : null; return ( { const _onClick = (id) => { @@ -12,11 +13,20 @@ module.exports = ({ const lis = printers.map(({ name, + lock, id }) => { + const msg = (() => { + if (!lock) { + return ''; + } + + return (locked === id) ? '(Locked to you)' : `(Locked to ${lock})`; + })(); + return (
  • - {name} + {name} {msg}
  • ); }); diff --git a/spikes/stacks/redux-thunk/src/client/containers/app.js b/spikes/stacks/redux-thunk/src/client/containers/app.js index b00d5e80..ab7463fe 100644 --- a/spikes/stacks/redux-thunk/src/client/containers/app.js +++ b/spikes/stacks/redux-thunk/src/client/containers/app.js @@ -1,27 +1,55 @@ const ReactRedux = require('react-redux'); +const ReactRouter = require('react-router'); const React = require('react'); +const NotFound = require('./not-found'); +const Home = require('./home'); +const Print = require('./print'); + +const actions = require('../state/actions'); + const { connect } = ReactRedux; -const App = React.createClass({ +const { + Miss, + Match +} = ReactRouter; + +const { + updateRouter +} = actions; + +const App = connect()(React.createClass({ componentDidMount: function() { - require('../worker').on('action', (action) => { - this.props.dispatch(action); - }); + require('../worker').on('action', this.props.dispatch); }, render: function() { const { - children + children, + router, + dispatch } = this.props; + // ugly hack needed because of a limitation of react-router api + // that doens't pass it's instance to matched routes + dispatch(updateRouter(router)); + return (
    {children}
    ); } -}); +})); -module.exports = connect()(App); +module.exports = (props) => { + return ( + + + + + + ); +}; diff --git a/spikes/stacks/redux-thunk/src/client/containers/print.js b/spikes/stacks/redux-thunk/src/client/containers/print.js index 9f3d9686..93730f49 100644 --- a/spikes/stacks/redux-thunk/src/client/containers/print.js +++ b/spikes/stacks/redux-thunk/src/client/containers/print.js @@ -8,16 +8,17 @@ const Printers = require('../components/printers'); const Changes = require('../components/changes'); const Change = require('../components/change'); -const actions = require('../actions'); +const actions = require('../state/actions'); const { - fetchChanges + fetchChanges, + lockPrinter, + print, + transitionTo } = actions; const { - BrowserRouter, - Miss, - Match, + Match } = ReactRouter; const { @@ -30,8 +31,11 @@ const Print = ({ changes = [], lockPrinter, fetchChanges, + onPrint, loaded, - loading + loading, + locked, + router }) => { const allChanges = () => { return ( @@ -58,6 +62,18 @@ const Print = ({ return change.id === params.id; }); + if (!change) { + return ( +

    Change not found

    + ); + } + + const _onPrint = (id) => { + return () => { + return onPrint(id); + }; + }; + // TODO: don't load all changes return (
    @@ -69,6 +85,7 @@ const Print = ({ > +
    ); }; @@ -79,6 +96,8 @@ const Print = ({

    Printers

    @@ -93,15 +112,23 @@ const mapStateToProps = (state) => { loaded: state.ui.changes.loaded, loading: state.ui.changes.loading, changes: state.data.changes, - printers: state.data.printers + printers: state.data.printers, + locked: state.ui.printers.locked }; }; const mapDispatchToProps = (dispatch) => { return { - lockPrinter: (id) => {}, + lockPrinter: (id) => { + return dispatch(lockPrinter(id)); + }, fetchChanges: () => { - dispatch(fetchChanges()); + return dispatch(fetchChanges()); + }, + onPrint: (id) => { + return dispatch(print(id)).then(() => { + return dispatch(transitionTo('/print')); + }); } }; }; diff --git a/spikes/stacks/redux-thunk/src/client/index.js b/spikes/stacks/redux-thunk/src/client/index.js index 6ebb1c56..10c5a842 100644 --- a/spikes/stacks/redux-thunk/src/client/index.js +++ b/spikes/stacks/redux-thunk/src/client/index.js @@ -1,4 +1,4 @@ -const worker = require('./worker'); // singleton +require('./worker'); // singleton const React = require('react'); const ReactDOM = require('react-dom'); @@ -16,4 +16,4 @@ render(); if (module.hot) { module.hot.accept('./root', render); -} \ No newline at end of file +} diff --git a/spikes/stacks/redux-thunk/src/client/intl.json b/spikes/stacks/redux-thunk/src/client/intl.json new file mode 100644 index 00000000..077404aa --- /dev/null +++ b/spikes/stacks/redux-thunk/src/client/intl.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/spikes/stacks/redux-thunk/src/client/root.js b/spikes/stacks/redux-thunk/src/client/root.js index 8fceb79f..4ecda442 100644 --- a/spikes/stacks/redux-thunk/src/client/root.js +++ b/spikes/stacks/redux-thunk/src/client/root.js @@ -2,39 +2,44 @@ const React = require('react'); const ReactHotLoader = require('react-hot-loader'); const ReactRouter = require('react-router'); const ReactRedux = require('react-redux'); +const ReactIntl = require('react-intl'); const App = require('./containers/app'); -const NotFound = require('./containers/not-found'); -const Home = require('./containers/home'); -const Print = require('./containers/print'); - -const store = require('./store'); +const store = require('./state/store'); const { AppContainer } = ReactHotLoader; const { - BrowserRouter, - Miss, - Match + BrowserRouter } = ReactRouter; const { Provider } = ReactRedux; +const { + IntlProvider +} = ReactIntl; + +// http://stackoverflow.com/a/38150585 +const locale = ( + navigator.languages && navigator.languages[0] || // Chrome / Firefox + navigator.language || // All browsers + navigator.userLanguage // IE <= 10 +); + module.exports = () => { return ( - - - - - - - + + {App} + ); diff --git a/spikes/stacks/redux-thunk/src/client/state/actions/app.js b/spikes/stacks/redux-thunk/src/client/state/actions/app.js new file mode 100644 index 00000000..65b4371e --- /dev/null +++ b/spikes/stacks/redux-thunk/src/client/state/actions/app.js @@ -0,0 +1,25 @@ +const ReduxActions = require('redux-actions'); +const app = require('../../../../package.json').name; +const selectors = require('../selectors'); + +const { + router +} = selectors; + +const { + createAction +} = ReduxActions; + +const UPDATE_ROUTER = `${app}/changes/UPDATE_ROUTER`; + +const updateRouter = createAction(UPDATE_ROUTER); + +const transitionTo = (pathname) => (dispatch, getState) => { + return router(getState()).transitionTo(pathname); +}; + +module.exports = { + UPDATE_ROUTER, + updateRouter, + transitionTo +}; diff --git a/spikes/stacks/redux-thunk/src/client/state/actions/changes.js b/spikes/stacks/redux-thunk/src/client/state/actions/changes.js new file mode 100644 index 00000000..389e4249 --- /dev/null +++ b/spikes/stacks/redux-thunk/src/client/state/actions/changes.js @@ -0,0 +1,28 @@ +const app = require('../../../../package.json').name; +const api = require('../../api'); + +const FETCH_CHANGES = `${app}/changes/FETCH_CHANGES`; +const REMOVE_CHANGE = `${app}/changes/REMOVE_CHANGE`; + +const fetchChanges = () => { + return { + type: FETCH_CHANGES, + payload: api.fetchChanges() + }; +}; + +const removeChange = (id) => (dispatch) => { + return dispatch({ + type: REMOVE_CHANGE, + payload: api.removeChange(id) + }).then(() => { + return dispatch(fetchChanges()); + }); +}; + +module.exports = { + FETCH_CHANGES, + REMOVE_CHANGE, + fetchChanges, + removeChange +}; diff --git a/spikes/stacks/redux-thunk/src/client/state/actions/index.js b/spikes/stacks/redux-thunk/src/client/state/actions/index.js new file mode 100644 index 00000000..997b6edd --- /dev/null +++ b/spikes/stacks/redux-thunk/src/client/state/actions/index.js @@ -0,0 +1,5 @@ +module.exports = { + ...require('./app'), + ...require('./printers'), + ...require('./changes') +}; diff --git a/spikes/stacks/redux-thunk/src/client/state/actions/printers.js b/spikes/stacks/redux-thunk/src/client/state/actions/printers.js new file mode 100644 index 00000000..b4bbaab9 --- /dev/null +++ b/spikes/stacks/redux-thunk/src/client/state/actions/printers.js @@ -0,0 +1,183 @@ +const ReduxActions = require('redux-actions'); +const app = require('../../../../package.json').name; +const find = require('lodash.find'); +const changes = require('./changes'); + +const { + createAction +} = ReduxActions; + +const { + removeChange +} = changes; + +const UPDATE_PRINTERS = `${app}/printers/UPDATE_PRINTERS`; +const UPDATE_WORKER_ID = `${app}/printers/UPDATE_WORKER_ID`; +const LOCK_PRINTER = `${app}/printers/LOCK_PRINTER`; +const PRINT = `${app}/printers/PRINT`; + +// confirm should be an async op, +// let's mock it that way +const confirm = (msg) => { + return new Promise((resolve, reject) => { + resolve(window.confirm(msg)); + }); +}; + +// prompt should be an async op, +// let's mock it that way +const prompt = (msg) => { + return new Promise((resolve, reject) => { + resolve(window.prompt(msg)); + }); +}; + +// alert should be an async op, +// let's mock it that way +const alert = (msg) => { + return new Promise((resolve, reject) => { + resolve(window.alert(msg)); + }); +}; + +const updatePrinters = createAction(UPDATE_PRINTERS); +const updateWorkerId = createAction(UPDATE_WORKER_ID); + +const lockPrinter = (id) => (dispatch, getState) => { + const { + ui, + data + } = getState(); + + const { + printers + } = data; + + const { + printers: { + locked + } + } = ui; + + if (locked === id) { + return; + } + + const printer = find(printers, ['id', id]); + + if (!printer) { + return window.alert(`Printer ${id} not found`); + } + + const worker = require('../../worker'); + + const lock = () => { + return dispatch({ + type: LOCK_PRINTER, + payload: worker.dispatch({ + type: 'LOCK_PRINTER', + payload: id + }) + }); + }; + + const askToLock = () => { + const msg = `Do you want to lock printer ${id}?`; + return confirm(msg).then((yes) => { + return yes ? lock(id) : null; + }); + }; + + const askToOverride = () => { + const msg = `Printer ${id} already locked! Do you want to override?`; + return confirm(msg).then((yes) => { + return yes ? lock(id) : null; + }); + }; + + return printer.lock ? askToOverride() : askToLock(); +}; + +const print = (changeId) => (dispatch, getState) => { + const { + ui, + data + } = getState(); + + const { + printers + } = data; + + const { + printers: { + locked + } + } = ui; + + const worker = require('../../worker'); + + const _print = () => { + return dispatch({ + type: PRINT, + payload: worker.dispatch({ + type: 'PRINT', + payload: changeId + }) + }).then(() => { + return dispatch(removeChange(changeId)); + }); + }; + + const lock = (printerId) => { + return dispatch({ + type: LOCK_PRINTER, + payload: worker.dispatch({ + type: 'LOCK_PRINTER', + payload: printerId + }) + }).then(_print); + }; + + const askToOverride = (printerId) => { + const msg = `Printer ${printerId} already locked! Do you want to override?`; + return confirm(msg).then((yes) => { + return yes ? lock(printerId) : null; + }); + }; + + const askToLock = () => { + const msg = `Please select a printer to lock: ${ + printers.map(({ + id, + name + }) => { + return `\n(${id}) ${name}`; + }) + }`; + + return prompt(msg).then((printerId) => { + const printer = find(printers, ['id', printerId]); + + if (!printer) { + return alert(`Printer ${printerId} not found. Try again`).then(() => { + return print(printerId)(dispatch, getState); + }); + } + + return printer.lock ? askToOverride(printerId) : lock(printerId); + }); + }; + + return !locked ? askToLock() : _print(); +}; + +module.exports = { + UPDATE_PRINTERS, + UPDATE_WORKER_ID, + LOCK_PRINTER, + PRINT, + updatePrinters, + updateWorkerId, + lockPrinter, + print +}; diff --git a/spikes/stacks/redux-thunk/src/client/state/reducers/app.js b/spikes/stacks/redux-thunk/src/client/state/reducers/app.js new file mode 100644 index 00000000..fc47f8e8 --- /dev/null +++ b/spikes/stacks/redux-thunk/src/client/state/reducers/app.js @@ -0,0 +1,19 @@ +const ReduxActions = require('redux-actions'); +const actions = require('../actions'); + +const { + handleActions +} = ReduxActions; + +const { + UPDATE_ROUTER +} = actions; + +exports.ui = handleActions({ + [UPDATE_ROUTER]: (state, action) => { + return { + ...state, + router: action.payload + }; + } +}, {}); diff --git a/spikes/stacks/redux-thunk/src/client/state/reducers/changes.js b/spikes/stacks/redux-thunk/src/client/state/reducers/changes.js new file mode 100644 index 00000000..7f5155c6 --- /dev/null +++ b/spikes/stacks/redux-thunk/src/client/state/reducers/changes.js @@ -0,0 +1,43 @@ +const ReduxActions = require('redux-actions'); +const actions = require('../actions'); + +const { + handleActions +} = ReduxActions; + +const { + FETCH_CHANGES +} = actions; + +exports.data = handleActions({ + [`${FETCH_CHANGES}_FULFILLED`]: (state, action) => { + return action.payload; + } +}, []); + +exports.ui = handleActions({ + [`${FETCH_CHANGES}_PENDING`]: (state, action) => { + return { + ...state, + loading: true + }; + }, + [`${FETCH_CHANGES}_FULFILLED`]: (state, action) => { + return { + ...state, + loading: false, + loaded: false + }; + }, + [`${FETCH_CHANGES}_REJECTED`]: (state, action) => { + // TODO: deal with error + return { + ...state, + loading: false, + loaded: false + }; + } +}, { + loading: false, + loaded: false +}); diff --git a/spikes/stacks/redux-thunk/src/client/reducers/index.js b/spikes/stacks/redux-thunk/src/client/state/reducers/index.js similarity index 79% rename from spikes/stacks/redux-thunk/src/client/reducers/index.js rename to spikes/stacks/redux-thunk/src/client/state/reducers/index.js index d5565b04..19bc79f9 100644 --- a/spikes/stacks/redux-thunk/src/client/reducers/index.js +++ b/spikes/stacks/redux-thunk/src/client/state/reducers/index.js @@ -1,6 +1,7 @@ // const reduceReducers = require('reduce-reducers'); const Redux = require('redux'); +const app = require('./app'); const printers = require('./printers'); const changes = require('./changes'); @@ -15,7 +16,9 @@ module.exports = () => { changes: changes.data }), ui: combineReducers({ - changes: changes.ui + changes: changes.ui, + printers: printers.ui, + app: app.ui }) }); }; diff --git a/spikes/stacks/redux-thunk/src/client/reducers/printers.js b/spikes/stacks/redux-thunk/src/client/state/reducers/printers.js similarity index 63% rename from spikes/stacks/redux-thunk/src/client/reducers/printers.js rename to spikes/stacks/redux-thunk/src/client/state/reducers/printers.js index d44b332c..88399b49 100644 --- a/spikes/stacks/redux-thunk/src/client/reducers/printers.js +++ b/spikes/stacks/redux-thunk/src/client/state/reducers/printers.js @@ -1,14 +1,15 @@ const ReduxActions = require('redux-actions'); -const app = require('../../../package.json').name; const find = require('lodash.find'); +const actions = require('../actions'); const { - createAction, handleActions } = ReduxActions; -const UPDATE_PRINTERS = `${app}/printers/UPDATE_PRINTERS`; -const UPDATE_WORKER_ID = `${app}/printers/UPDATE_WORKER_ID`; +const { + UPDATE_WORKER_ID, + UPDATE_PRINTERS +} = actions; exports.data = handleActions({ [UPDATE_PRINTERS]: (state, action) => { @@ -27,8 +28,7 @@ exports.ui = handleActions({ const locked = (find(action.payload, (printer) => { return ( printer.lock && - state.locked && - printer.lock === state.locked + printer.lock === state.id ); }) || {}).id || ''; @@ -41,8 +41,3 @@ exports.ui = handleActions({ id: '', locked: '' }); - -exports.actions = { - updatePrinters: createAction(UPDATE_PRINTERS), - updateWorkerId: createAction(UPDATE_WORKER_ID) -}; diff --git a/spikes/stacks/redux-thunk/src/client/state/selectors.js b/spikes/stacks/redux-thunk/src/client/state/selectors.js new file mode 100644 index 00000000..cac1592c --- /dev/null +++ b/spikes/stacks/redux-thunk/src/client/state/selectors.js @@ -0,0 +1,10 @@ +// const Reselect = require('reselect'); +const get = require('lodash.get'); + +const router = (state) => { + return get(state, 'ui.app.router'); +}; + +module.exports = { + router +}; diff --git a/spikes/stacks/redux-thunk/src/client/store.js b/spikes/stacks/redux-thunk/src/client/state/store.js similarity index 88% rename from spikes/stacks/redux-thunk/src/client/store.js rename to spikes/stacks/redux-thunk/src/client/state/store.js index a50f4fce..d177ec37 100644 --- a/spikes/stacks/redux-thunk/src/client/store.js +++ b/spikes/stacks/redux-thunk/src/client/state/store.js @@ -2,7 +2,7 @@ const createReducer = require('./reducers'); const enableBatching = require('redux-batched-actions').enableBatching; const thunk = require('redux-thunk').default; const promiseMiddleware = require('redux-promise-middleware').default; -const createLogger = require('redux-logger'); +// const createLogger = require('redux-logger'); const redux = require('redux'); @@ -18,7 +18,7 @@ module.exports = (state = Object.freeze({})) => { state, compose( applyMiddleware( - createLogger(), +// createLogger(), promiseMiddleware(), thunk ) diff --git a/spikes/stacks/redux-thunk/src/client/worker.js b/spikes/stacks/redux-thunk/src/client/worker.js index 3fe1b68d..e41576e1 100644 --- a/spikes/stacks/redux-thunk/src/client/worker.js +++ b/spikes/stacks/redux-thunk/src/client/worker.js @@ -2,19 +2,51 @@ const uuid = require('node-uuid'); const Emitter = require('component-emitter'); const crosstab = require('crosstab'); const values = require('lodash.values'); -const actions = require('./actions'); +const actions = require('./state/actions'); const emitter = module.exports = new Emitter(); let isMaster = crosstab.util.tabs['MASTER_TAB'].id === crosstab.id; const bridge = new Map(); const { - updatePrinters + updatePrinters, + updateWorkerId } = actions; +const printers = { + '1': { + id: '1', + name: 'Main printer', + lock: '' + }, + '2': { + id: '2', + name: 'Handled printer', + lock: '' + } +}; + const handlers = { - 'PRINT': (action, fn) => {}, + 'PRINT': (action, fn) => { + fn(); + }, 'LOCK_PRINTER': (action, fn) => { + const alreadyLocked = values(printers).filter((printer) => { + return printer.lock === action._origin; + }); + + alreadyLocked.forEach((printer) => { + printers[printer.id] = { + ...printers[printer.id], + lock: '' + }; + }); + + printers[action.payload] = { + ...printers[action.payload], + lock: action._origin + }; + fn(); } }; @@ -46,7 +78,10 @@ crosstab.util.events.on('message', ({ return emitter.emit('action', data); } - handlers[data.type](data, (err, res) => { + handlers[data.type]({ + ...data, + _origin: origin + }, (err, res) => { crosstab.broadcast('message', { ...data, error: err && err.message @@ -66,7 +101,10 @@ const dispatch = module.exports.dispatch = (action, tab) => { if (isMaster && !tab) { if (handlers[action.type]) { return new Promise(function(resolve, reject) { - handlers[action.type](action, function(err, res) { + handlers[action.type]({ + ...action, + _origin: crosstab.id + }, function(err, res) { return err ? reject(err) : resolve(res); }); }); @@ -90,20 +128,9 @@ const dispatch = module.exports.dispatch = (action, tab) => { return then; }; -const printers = { - '1': { - id: '1', - name: 'Main printer', - lock: '' - }, - '2': { - id: '2', - name: 'Handled printer', - lock: '' - } -}; - -emitter.emit('action', updateWorkerId(crosstab.id)); +setTimeout(function() { + emitter.emit('action', updateWorkerId(crosstab.id)); +}, 450); setInterval(() => { if (!isMaster) { diff --git a/spikes/stacks/redux-thunk/src/server/schema.js b/spikes/stacks/redux-thunk/src/server/schema.js index 9e73a578..521d370b 100644 --- a/spikes/stacks/redux-thunk/src/server/schema.js +++ b/spikes/stacks/redux-thunk/src/server/schema.js @@ -47,7 +47,7 @@ const ChangeType = new GraphQLObjectType({ product: { type: ProductType, resolve: (root, args) => { - return products[root.product] + return products[root.product]; } }, price: { @@ -70,7 +70,7 @@ const query = new GraphQLObjectType({ } }, resolve(root, args, ctx) { - return args.id ? [products[args.id]] : values(products) + return args.id ? [products[args.id]] : values(products); } }, changes: { @@ -114,7 +114,7 @@ const mutation = new GraphQLObjectType({ } }, resolve(root, args, ctx) { - const changes = (() => { + const ops = (() => { if (args.id) { return [args.id]; } @@ -128,7 +128,7 @@ const mutation = new GraphQLObjectType({ }); })(); - changes.forEach((id) => { + ops.forEach((id) => { delete changes[id]; }); }