Basic redux integration for thread component (#141)

This commit is contained in:
Dmitry Tsepelev
2018-07-10 14:05:39 +04:00
committed by Aleksei Gurianov
parent b6cda91dd9
commit a02b1502a6
13 changed files with 286 additions and 55 deletions
@@ -0,0 +1,28 @@
import { siteId, url } from 'common/settings';
import { THREAD_SET_COLLAPSE } from './thread.actions';
import getCollapsedComments from './getCollapsedComments';
import saveCollapsedComments from './saveCollapsedComments';
const collapsedCommentsMiddleware = ({ getState }) => next => action => {
if (action.type === THREAD_SET_COLLAPSE) {
const state = getState();
const currentCollapsed = state[action.comment.id];
if (action.collapsed !== currentCollapsed) {
const lsCollapsedID = `${siteId}_${url}_${action.comment.id}`;
let collapsedComments = getCollapsedComments();
if (action.collapsed) {
collapsedComments = [...new Set(collapsedComments.concat(lsCollapsedID))];
} else {
collapsedComments = collapsedComments.filter(id => id !== lsCollapsedID);
}
saveCollapsedComments(collapsedComments);
}
}
next(action);
};
export default collapsedCommentsMiddleware;
@@ -0,0 +1,5 @@
import { LS_COLLAPSE_KEY } from 'common/constants';
const getCollapsedComments = () => JSON.parse(localStorage.getItem(LS_COLLAPSE_KEY) || '[]');
export default getCollapsedComments;
+7
View File
@@ -1 +1,8 @@
import { collapsedThreads } from './thread.reducers';
import collapsedCommentsMiddleware from './collapsedCommentsMiddleware';
export const threadReducers = { collapsedThreads };
export const threadMiddlewares = [collapsedCommentsMiddleware];
export { default } from './thread';
@@ -0,0 +1,5 @@
import { LS_COLLAPSE_KEY } from 'common/constants';
const saveCollapsedComments = comments => localStorage.setItem(LS_COLLAPSE_KEY, JSON.stringify(comments));
export default saveCollapsedComments;
@@ -0,0 +1,6 @@
export const THREAD_SET_COLLAPSE = 'THREAD/COLLAPSE_SET';
export const setCollapse = (comment, collapsed) => ({
type: THREAD_SET_COLLAPSE,
comment,
collapsed,
});
@@ -0,0 +1,14 @@
import store from 'common/store';
export const getThreadIsCollapsed = (state, comment) => {
let collapsed = state.collapsedThreads[comment.id];
if (collapsed !== null && collapsed !== undefined) {
return collapsed;
}
const config = store.get('config') || {};
const score = comment.score || 0;
return score <= config.critical_score;
};
+13 -53
View File
@@ -1,63 +1,24 @@
/** @jsx h */
import { h, Component } from 'preact';
import { LS_COLLAPSE_KEY } from 'common/constants';
import { siteId, url } from 'common/settings';
import store from 'common/store';
import { connect } from 'preact-redux';
import Comment from 'components/comment';
import { setCollapse } from './thread.actions';
import { getThreadIsCollapsed } from './thread.getters';
export default class Thread extends Component {
class Thread extends Component {
constructor(props) {
super(props);
if (this.props.data && this.props.data.comment) {
this.updateCollapsedState(this.props.data.comment);
}
this.onCollapseToggle = this.onCollapseToggle.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.data && nextProps.data.comment) {
this.updateCollapsedState(nextProps.data.comment);
}
}
updateCollapsedState(comment) {
const config = store.get('config') || {};
const score = comment.score || 0;
this.lsCollapsedID = `${siteId}_${url}_${comment.id}`;
this.state = {
collapsed:
(!this.state.isCollapsedChanged && score <= config.critical_score) ||
getCollapsedComments().includes(this.lsCollapsedID),
isCollapsedChanged: true,
};
}
onCollapseToggle() {
const collapsed = !this.state.collapsed;
this.setState({ collapsed: !this.state.collapsed });
let collapsedComments = getCollapsedComments();
if (collapsed) {
if (!collapsedComments.includes(this.lsCollapsedID)) {
collapsedComments = collapsedComments.concat(this.lsCollapsedID);
}
} else {
collapsedComments = collapsedComments.filter(id => id !== this.lsCollapsedID);
}
saveCollapsedComments(collapsedComments);
this.props.setCollapse(this.props.data.comment, !this.props.collapsed);
}
render(props, { collapsed }) {
render(props) {
const {
collapsed,
data: { comment, replies = [] },
mods = {},
} = props;
@@ -79,7 +40,7 @@ export default class Thread extends Component {
{!collapsed &&
!!replies.length &&
replies.map(thread => (
<Thread
<ConnectedThread
key={thread.comment.id}
data={thread}
mods={{ level: mods.level < 5 ? mods.level + 1 : mods.level }}
@@ -92,10 +53,9 @@ export default class Thread extends Component {
}
}
function getCollapsedComments() {
return JSON.parse(localStorage.getItem(LS_COLLAPSE_KEY) || '[]');
}
const ConnectedThread = connect(
(state, props) => ({ collapsed: getThreadIsCollapsed(state, props.data.comment) }),
{ setCollapse }
)(Thread);
function saveCollapsedComments(comments) {
localStorage.setItem(LS_COLLAPSE_KEY, JSON.stringify(comments));
}
export default ConnectedThread;
@@ -0,0 +1,22 @@
import { siteId, url } from 'common/settings';
import { THREAD_SET_COLLAPSE } from './thread.actions';
import getCollapsedComments from './getCollapsedComments';
const collapsedCommentIds = getCollapsedComments()
.map(comment => comment.split('_'))
.filter(components => components[0] === siteId && components[1] === url)
.map(component => component[2]);
const initialState = collapsedCommentIds.reduce((acc, id) => ({ ...acc, [id]: true }), {});
export const collapsedThreads = (state = initialState, action) => {
switch (action.type) {
case THREAD_SET_COLLAPSE:
return {
...state,
[action.comment.id]: action.collapsed,
};
default:
return state;
}
};
@@ -0,0 +1,29 @@
import { setCollapse } from './thread.actions';
import { collapsedThreads } from './thread.reducers';
import { getThreadIsCollapsed } from './thread.getters';
describe('collapsedThreads', () => {
const comment = { id: 1 };
it('should set collapsed to true', () => {
const collapsed = true;
const action = setCollapse(comment, collapsed);
const newState = {
collapsedThreads: collapsedThreads({}, action),
};
expect(getThreadIsCollapsed(newState, comment)).toEqual(collapsed);
});
it('should set collapsed to false', () => {
const collapsed = false;
const action = setCollapse(comment, collapsed);
const newState = {
collapsedThreads: collapsedThreads({}, action),
};
expect(getThreadIsCollapsed(newState, comment)).toEqual(collapsed);
});
});
+9 -1
View File
@@ -3,9 +3,11 @@
import loadPolyfills from 'common/polyfills';
import { h, render } from 'preact';
import { Provider } from 'preact-redux';
import Root from './components/root';
import UserInfo from 'components/user-info';
import store from 'common/store';
import reduxStore from './store';
// eslint-disable-next-line no-unused-vars
import ListComments from './components/list-comments'; // TODO: temp solution for extracting styles
@@ -20,6 +22,12 @@ loadPolyfills().then(() => {
}
});
const Main = () => (
<Provider store={reduxStore}>
<Root />
</Provider>
);
function init() {
const node = document.getElementById(NODE_ID);
@@ -62,6 +70,6 @@ function init() {
node
);
} else {
render(<Root />, node.parentElement, node);
render(<Main />, node.parentElement, node);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { createStore, applyMiddleware } from 'redux';
import { combineReducers } from 'redux';
import { threadReducers, threadMiddlewares } from './components/thread';
const reducers = combineReducers({
...threadReducers,
});
const middlewares = applyMiddleware(...threadMiddlewares);
export default createStore(reducers, middlewares);
+8 -1
View File
@@ -43,6 +43,7 @@
"html-webpack-plugin": "^2.30.1",
"husky": "^0.14.3",
"jest": "^23.1.0",
"jest-localstorage-mock": "^2.2.0",
"lint-staged": "^7.2.0",
"npm-run-all": "^4.1.3",
"postcss-calc": "^6.0.1",
@@ -53,7 +54,9 @@
"postcss-simple-vars": "^4.1.0",
"postcss-url": "^6.3.1",
"postcss-wrap": "0.0.4",
"preact-redux": "^2.0.3",
"prettier": "^1.13.7",
"redux": "^4.0.0",
"style-loader": "^0.19.1",
"webpack": "^3.12.0",
"webpack-bundle-analyzer": "^2.13.1",
@@ -72,11 +75,15 @@
"^.+\\.jsx?$": "<rootDir>/fileTransformer.js"
},
"setupFiles": [
"<rootDir>/injectGlobalVariable.js"
"<rootDir>/injectGlobalVariable.js",
"jest-localstorage-mock"
],
"moduleDirectories": [
"node_modules",
"<rootDir>/app"
],
"testMatch": [
"<rootDir>/**/*.test.js"
]
},
"engines": {
+128
View File
@@ -0,0 +1,128 @@
Arguments:
/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/bin/node /Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/bin/yarn start
PATH:
/anaconda2/bin:/Users/dmitrytsepelev/.rvm/gems/ruby-2.5.1/bin:/Users/dmitrytsepelev/.rvm/gems/ruby-2.5.1@global/bin:/Users/dmitrytsepelev/.rvm/rubies/ruby-2.5.1/bin:/usr/local/heroku/bin:/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/bin:./bin:bin:/Users/dmitrytsepelev/bin:/usr/local/bin:/usr/local/sbin:/usr/local/share/npm/bin:/usr/bin:/bin:/usr/sbin:/sbin::/Users/dmitrytsepelev/.rvm/bin:/usr/local/go/bin:/Users/dmitrytsepelev/go/bin:/Users/dmitrytsepelev/Library/Android/sdk/tools:/Users/dmitrytsepelev/Library/Android/sdk/platform-tools:/Users/dmitrytsepelev/Library/Android/sdk/tools/proguard/bin
Yarn version:
1.5.1
Node version:
9.2.0
Platform:
darwin x64
npm manifest:
{
"name": "remark-ui",
"version": "0.1.0",
"scripts": {
"build": "cross-env NODE_ENV=production webpack --config ./webpack.config.js",
"start": "webpack-dev-server --progress --hot --inline --config ./webpack.config.js",
"lint": "eslint --ext=.js,.jsx .",
"test": "jest",
"prettier": "prettier --write \"./**/*.{js,jsx,scss}\"",
"precommit": "./node_modules/.bin/lint-staged"
},
"lint-staged": {
"./**/*.{js,jsx}": [
"eslint --fix",
"git add"
],
"./**/*.scss": [
"prettier --write",
"git add"
]
},
"devDependencies": {
"autoprefixer": "^7.2.6",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.5",
"babel-loader": "^7.1.4",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-jsx": "^6.24.1",
"babel-preset-env": "^1.7.0",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.1",
"core-js": "^2.5.7",
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-jsx-a11y": "^6.1.0",
"eslint-plugin-prettier": "^2.6.1",
"eslint-plugin-react": "^7.10.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^0.11.1",
"html-webpack-plugin": "^2.30.1",
"husky": "^0.14.3",
"jest": "^23.1.0",
"jest-localstorage-mock": "^2.2.0",
"lint-staged": "^7.2.0",
"postcss-calc": "^6.0.1",
"postcss-csso": "^2.0.0",
"postcss-for": "^2.1.1",
"postcss-loader": "^2.1.5",
"postcss-nested": "^3.0.0",
"postcss-simple-vars": "^4.1.0",
"postcss-url": "^6.3.1",
"postcss-wrap": "0.0.4",
"preact-redux": "^2.0.3",
"prettier": "^1.13.7",
"redux": "^4.0.0",
"style-loader": "^0.19.1",
"webpack": "^3.12.0",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-dev-server": "^2.7.1"
},
"dependencies": {
"axios": "^0.18.0",
"bem-react-helper": "^1.1.2",
"preact": "^8.2.9"
},
"eslintIgnore": [
"public"
],
"jest": {
"transform": {
"^.+\\.jsx?$": "<rootDir>/fileTransformer.js"
},
"setupFiles": [
"<rootDir>/injectGlobalVariable.js",
"jest-localstorage-mock"
],
"moduleDirectories": [
"node_modules",
"<rootDir>/app"
],
"testMatch": [
"<rootDir>/**/*.test.js"
]
},
"engines": {
"node": ">=8"
}
}
yarn manifest:
No manifest
Lockfile:
No lockfile
Trace:
Error: Command failed.
Exit code: 1
Command: sh
Arguments: -c webpack-dev-server --progress --hot --inline --config ./webpack.config.js
Directory: /Users/dmitrytsepelev/dev/remark/web
Output:
at ProcessTermError.MessageError (/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/lib/node_modules/yarn/lib/cli.js:186:110)
at new ProcessTermError (/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/lib/node_modules/yarn/lib/cli.js:226:113)
at ChildProcess.<anonymous> (/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/lib/node_modules/yarn/lib/cli.js:30281:17)
at ChildProcess.emit (events.js:159:13)
at maybeClose (internal/child_process.js:943:16)
at Process.ChildProcess._handle.onexit (internal/child_process.js:220:5)