wip redux-loop

This commit is contained in:
Sérgio Ramos 2016-10-17 16:52:00 +01:00
parent cce5998bfc
commit 7c7e8f4a66
39 changed files with 1791 additions and 0 deletions

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
/node_modules
coverage
.nyc_output

View File

@ -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
}]
}
}

4
spikes/stacks/redux-loop/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules
coverage
.nyc_output
npm-debug.log

View File

@ -0,0 +1,5 @@
const Storybook = require('@kadira/storybook');
Storybook.configure(() => {
require('../stories');
}, module);

View File

@ -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"
}
}

View File

@ -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);
});
};

View File

@ -0,0 +1,14 @@
const React = require('react');
module.exports = ({
price,
currency,
product,
id
}) => {
return (
<p>
{product.artist}: {product.title} - {product.currencr}{product.price} > {currency}{price}
</p>
);
};

View File

@ -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 (
<li key={id}>
<Link to={`${pathname}/${id}`}>
{product.artist}: {product.title} - {product.currencr}{product.price} > {currency}{price}
</Link>
</li>
);
});
return (
<ul>
{lis}
</ul>
);
};

View File

@ -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 (
<Loader
{...this.props}
loaded={_loaded}
>
{component}
</Loader>
);
}
});

View File

@ -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 (
<li key={id}>
<a onClick={_onClick(id)}>{name} {msg}</a>
</li>
);
});
return (
<ul>
{lis}
</ul>
);
};

View File

@ -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 (
<div>
{children}
</div>
);
}
}));
module.exports = (props) => {
return (
<App {...props}>
<Match exactly pattern='/' component={Home} />
<Match pattern='/print' component={Print} />
<Miss component={NotFound}/>
</App>
);
};

View File

@ -0,0 +1,14 @@
const ReactRouter = require('react-router');
const React = require('react');
const {
Link
} = ReactRouter;
module.exports = () => {
return (
<div>
<Link to='/print'>Start</Link>
</div>
);
};

View File

@ -0,0 +1,7 @@
const React = require('react');
module.exports = () => {
return (
<h1>Not found</h1>
);
};

View File

@ -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 (
<div>
<p>Changes</p>
<Loader
loaded={loaded}
loading={loading}
fetch={fetchChanges}
>
<Changes
pathname={pathname}
changes={changes}
/>
</Loader>
</div>
);
};
const singleChange = ({
params
}) => {
const change = find(changes, (change) => {
return change.id === params.id;
});
if (!change) {
return (
<p>Change not found</p>
);
}
const _onPrint = (id) => {
return () => {
return onPrint(id);
};
};
// TODO: don't load all changes
return (
<div>
<p>Change</p>
<Loader
loaded={loaded}
loading={loading}
fetch={fetchChanges}
>
<Change {...change} />
</Loader>
<button onClick={_onPrint(params.id)}>Print</button>
</div>
);
};
return (
<div>
<div>
<p>Printers</p>
<Printers
printers={printers}
onClick={lockPrinter}
locked={locked}
/>
</div>
<Match pattern={`${pathname}/:id`} render={singleChange} />
<Match exactly pattern={pathname} render={allChanges} />
</div>
);
};
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);

View File

@ -0,0 +1,19 @@
require('./worker'); // singleton
const React = require('react');
const ReactDOM = require('react-dom');
const render = () => {
const Root = require('./root');
ReactDOM.render(
<Root />,
document.getElementById('root')
);
};
render();
if (module.hot) {
module.hot.accept('./root', render);
}

View File

@ -0,0 +1,3 @@
{
}

View File

@ -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 (
<AppContainer>
<Provider store={store()}>
<IntlProvider
locale={locale}
defaultLocale='en'
>
<BrowserRouter>{App}</BrowserRouter>
</IntlProvider>
</Provider>
</AppContainer>
);
};

View File

@ -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
};

View File

@ -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
};

View File

@ -0,0 +1,5 @@
module.exports = {
...require('./app'),
...require('./printers'),
...require('./changes')
};

View File

@ -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
};

View File

@ -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
};
});
})
]);
};

View File

@ -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)
);
};
};

View File

@ -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
};
}
}, {});

View File

@ -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
});

View File

@ -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
})
})
);

View File

@ -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
})
);
}
}, {});

View File

@ -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
};

View File

@ -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()
)
);
};

View File

@ -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);

View File

@ -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']
}]
}
};

View File

@ -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": "€"
}
}

View File

@ -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": "€"
}
}

View File

@ -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');
});

View File

@ -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
});

View File

@ -0,0 +1,10 @@
<!doctype html>
<html lang='en-US'>
<head>
<title>React Boilerplate</title>
</head>
<body>
<div id='root'></div>
<script src='/static/bundle.js'></script>
</body>
</html>

View File

@ -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', () => (
<Home />
));

View File

@ -0,0 +1,13 @@
const test = require('ava');
const enzyme = require('enzyme');
const React = require('react');
const {
shallow
} = enzyme;
test('renders <Home> without exploding', (t) => {
const Home = require('../src/client/containers/home');
const wrapper = shallow(<Home />);
t.deepEqual(wrapper.length, 1);
});