diff --git a/web/app/components/thread/thread.getters.test.js b/web/app/components/thread/thread.getters.test.js
new file mode 100644
index 00000000..e8fc6791
--- /dev/null
+++ b/web/app/components/thread/thread.getters.test.js
@@ -0,0 +1,31 @@
+import store from 'common/store';
+import { getThreadIsCollapsed } from './thread.getters';
+
+describe('collapsedThreads', () => {
+ const notScoredComment = { id: 1 };
+ const goodComment = { id: 1, score: 3 };
+ const badComment = { id: 1, score: -1 };
+
+ it('takes value from the state', () => {
+ const state = { collapsedThreads: { [notScoredComment.id]: true } };
+ const collapsed = getThreadIsCollapsed(state, notScoredComment);
+ expect(collapsed).toEqual(true);
+ });
+
+ beforeAll(() => {
+ const config = { critical_score: 2 };
+ store.set('config', config);
+ });
+
+ it('returns true when score is less then critical_score', () => {
+ const state = { collapsedThreads: {} };
+ const collapsed = getThreadIsCollapsed(state, badComment);
+ expect(collapsed).toEqual(true);
+ });
+
+ it('returns true when score is less then critical_score', () => {
+ const state = { collapsedThreads: {} };
+ const collapsed = getThreadIsCollapsed(state, goodComment);
+ expect(collapsed).toEqual(false);
+ });
+});
diff --git a/web/app/components/thread/thread.reducers.test.js b/web/app/components/thread/thread.reducers.test.js
index f48e8931..954feaee 100644
--- a/web/app/components/thread/thread.reducers.test.js
+++ b/web/app/components/thread/thread.reducers.test.js
@@ -1,29 +1,18 @@
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);
+ const action = setCollapse(comment, true);
+ const newState = collapsedThreads({}, action);
+ expect(newState).toEqual({ [comment.id]: true });
});
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);
+ const action = setCollapse(comment, false);
+ const newState = collapsedThreads({}, action);
+ expect(newState).toEqual({ [comment.id]: false });
});
});
diff --git a/web/app/components/user-info/index.js b/web/app/components/user-info/index.js
index 6f9f4544..142fba6e 100644
--- a/web/app/components/user-info/index.js
+++ b/web/app/components/user-info/index.js
@@ -1,3 +1,7 @@
+import { userComments, isLoadingUserComments } from './user-info.reducers';
+
+export const userInfoReducers = { userComments, isLoadingUserComments };
+
export { default } from './user-info';
require('./user-info.scss');
diff --git a/web/app/components/user-info/user-info.actions.js b/web/app/components/user-info/user-info.actions.js
new file mode 100644
index 00000000..403ca402
--- /dev/null
+++ b/web/app/components/user-info/user-info.actions.js
@@ -0,0 +1,12 @@
+export const USER_INFO_FETCH_COMMENTS = 'USER_INFO/FETCH_COMMENTS';
+export const fetchComments = userId => ({
+ type: USER_INFO_FETCH_COMMENTS,
+ userId,
+});
+
+export const USER_INFO_COMPLETE_FETCH_COMMENTS = 'USER_INFO/COMPLETE_FETCH_COMMENTS';
+export const completeFetchComments = (userId, comments) => ({
+ type: USER_INFO_COMPLETE_FETCH_COMMENTS,
+ userId,
+ comments,
+});
diff --git a/web/app/components/user-info/user-info.getters.js b/web/app/components/user-info/user-info.getters.js
new file mode 100644
index 00000000..c5067547
--- /dev/null
+++ b/web/app/components/user-info/user-info.getters.js
@@ -0,0 +1,2 @@
+export const getUserComments = (state, userId) => state.userComments[userId] || null;
+export const getIsLoadingUserComments = (state, userId) => state.isLoadingUserComments[userId] || false;
diff --git a/web/app/components/user-info/user-info.getters.test.js b/web/app/components/user-info/user-info.getters.test.js
new file mode 100644
index 00000000..c59118ac
--- /dev/null
+++ b/web/app/components/user-info/user-info.getters.test.js
@@ -0,0 +1,40 @@
+import { getUserComments, getIsLoadingUserComments } from './user-info.getters';
+
+describe('getUserComments', () => {
+ const userId = 1;
+ const comments = [{ id: 1 }];
+
+ it('returns null when comments have not been loaded', () => {
+ const state = { userComments: {} };
+ const userComments = getUserComments(state, userId);
+ expect(userComments).toEqual(null);
+ });
+
+ it('returns comments when comments have been loaded', () => {
+ const state = { userComments: { [userId]: comments } };
+ const userComments = getUserComments(state, userId);
+ expect(userComments).toEqual(comments);
+ });
+});
+
+describe('getIsLoadingUserComments', () => {
+ const userId = 1;
+
+ it('returns false when state is empty', () => {
+ const state = { isLoadingUserComments: {} };
+ const loading = getIsLoadingUserComments(state, userId);
+ expect(loading).toEqual(false);
+ });
+
+ it('returns false when comments are not loading', () => {
+ const state = { isLoadingUserComments: { [userId]: false } };
+ const loading = getIsLoadingUserComments(state, userId);
+ expect(loading).toEqual(false);
+ });
+
+ it('returns true when comments are loading', () => {
+ const state = { isLoadingUserComments: { [userId]: true } };
+ const loading = getIsLoadingUserComments(state, userId);
+ expect(loading).toEqual(true);
+ });
+});
diff --git a/web/app/components/user-info/user-info.jsx b/web/app/components/user-info/user-info.jsx
index 2959ecf1..1e91ffde 100644
--- a/web/app/components/user-info/user-info.jsx
+++ b/web/app/components/user-info/user-info.jsx
@@ -1,36 +1,40 @@
/** @jsx h */
import { h, Component } from 'preact';
+import { connect } from 'preact-redux';
import api from 'common/api';
import { getHandleClickProps } from 'common/accessibility';
import LastCommentsList from './last-comments-list';
import Avatar from 'components/avatar-icon';
+import { fetchComments, completeFetchComments } from './user-info.actions';
+import { getUserComments, getIsLoadingUserComments } from './user-info.getters';
class UserInfo extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- comments: [],
- isLoading: true,
- };
- }
-
componentWillMount() {
const {
user: { id },
+ comments,
+ isLoading,
+ fetchComments,
+ completeFetchComments,
} = this.props;
- api
- .getUserComments({ user: id, limit: 10 })
- .then(({ comments = [] }) => this.setState({ comments }))
- .finally(() => this.setState({ isLoading: false }));
+ if (!comments && !isLoading) {
+ fetchComments(id);
+
+ api
+ .getUserComments({ user: id, limit: 10 })
+ .then(({ comments }) => completeFetchComments(id, comments))
+ .catch(() => completeFetchComments(id, []));
+ }
}
- render(props, { comments, isLoading }) {
+ render(props) {
const {
user: { name, id, isDefaultPicture, picture },
+ comments = [],
+ isLoading,
onClose,
} = props;
@@ -40,7 +44,7 @@ class UserInfo extends Component {
Last comments by {name}
{id}
-
+ {!!comments && }
Close ✖
@@ -50,4 +54,10 @@ class UserInfo extends Component {
}
}
-export default UserInfo;
+export default connect(
+ (state, props) => ({
+ comments: getUserComments(state, props.user.id),
+ isLoading: getIsLoadingUserComments(state, props.user.id),
+ }),
+ { fetchComments, completeFetchComments }
+)(UserInfo);
diff --git a/web/app/components/user-info/user-info.reducers.js b/web/app/components/user-info/user-info.reducers.js
new file mode 100644
index 00000000..23e58b7d
--- /dev/null
+++ b/web/app/components/user-info/user-info.reducers.js
@@ -0,0 +1,37 @@
+import { USER_INFO_FETCH_COMMENTS, USER_INFO_COMPLETE_FETCH_COMMENTS } from './user-info.actions';
+
+export const userComments = (state = {}, action) => {
+ switch (action.type) {
+ case USER_INFO_FETCH_COMMENTS: {
+ return {
+ ...state,
+ [action.userId]: [],
+ };
+ }
+ case USER_INFO_COMPLETE_FETCH_COMMENTS:
+ return {
+ ...state,
+ [action.userId]: action.comments,
+ };
+ default:
+ return state;
+ }
+};
+
+export const isLoadingUserComments = (state = {}, action) => {
+ switch (action.type) {
+ case USER_INFO_FETCH_COMMENTS: {
+ return {
+ ...state,
+ [action.userId]: true,
+ };
+ }
+ case USER_INFO_COMPLETE_FETCH_COMMENTS:
+ return {
+ ...state,
+ [action.userId]: false,
+ };
+ default:
+ return state;
+ }
+};
diff --git a/web/app/components/user-info/user-info.reducers.test.js b/web/app/components/user-info/user-info.reducers.test.js
new file mode 100644
index 00000000..12d6c8b4
--- /dev/null
+++ b/web/app/components/user-info/user-info.reducers.test.js
@@ -0,0 +1,45 @@
+import { fetchComments, completeFetchComments } from './user-info.actions';
+import { userComments, isLoadingUserComments } from './user-info.reducers';
+
+const userId = 1;
+const comments = [{ id: 1 }];
+
+describe('userComments', () => {
+ it('should return {} by default', () => {
+ const action = { type: 'OTHER' };
+ const newState = userComments({}, action);
+ expect(newState).toEqual({});
+ });
+
+ it('should return [] on USER_INFO_FETCH_COMMENTS', () => {
+ const action = fetchComments(userId);
+ const newState = userComments({}, action);
+ expect(newState).toEqual({ [userId]: [] });
+ });
+
+ it('should return comments on USER_INFO_COMPLETE_FETCH_COMMENTS', () => {
+ const action = completeFetchComments(userId, comments);
+ const newState = userComments({}, action);
+ expect(newState).toEqual({ [userId]: comments });
+ });
+});
+
+describe('isLoadingUserComments', () => {
+ it('should return {} by default', () => {
+ const action = { type: 'OTHER' };
+ const newState = isLoadingUserComments({}, action);
+ expect(newState).toEqual({});
+ });
+
+ it('should return [] on USER_INFO_FETCH_COMMENTS', () => {
+ const action = fetchComments(userId);
+ const newState = isLoadingUserComments({}, action);
+ expect(newState).toEqual({ [userId]: true });
+ });
+
+ it('should return comments on USER_INFO_COMPLETE_FETCH_COMMENTS', () => {
+ const action = completeFetchComments(userId, comments);
+ const newState = isLoadingUserComments({}, action);
+ expect(newState).toEqual({ [userId]: false });
+ });
+});
diff --git a/web/app/remark.js b/web/app/remark.js
index c8bc6a27..8fa7b05c 100644
--- a/web/app/remark.js
+++ b/web/app/remark.js
@@ -22,12 +22,6 @@ loadPolyfills().then(() => {
}
});
-const Main = () => (
-
-
-
-);
-
function init() {
const node = document.getElementById(NODE_ID);
@@ -63,13 +57,21 @@ function init() {
render(
,
node.parentElement,
node
);
} else {
- render(, node.parentElement, node);
+ render(
+
+
+ ,
+ node.parentElement,
+ node
+ );
}
}
diff --git a/web/app/store.js b/web/app/store.js
index 1604ef3b..fd570b1c 100644
--- a/web/app/store.js
+++ b/web/app/store.js
@@ -2,9 +2,11 @@ import { createStore, applyMiddleware } from 'redux';
import { combineReducers } from 'redux';
import { threadReducers, threadMiddlewares } from './components/thread';
+import { userInfoReducers } from './components/user-info';
const reducers = combineReducers({
...threadReducers,
+ ...userInfoReducers,
});
const middlewares = applyMiddleware(...threadMiddlewares);
diff --git a/web/package.json b/web/package.json
index 8a8b0255..4bedf734 100644
--- a/web/package.json
+++ b/web/package.json
@@ -6,6 +6,7 @@
"start": "webpack-dev-server --progress --hot --inline --config ./webpack.config.js",
"lint": "eslint --ext=.js,.jsx .",
"test": "jest",
+ "test:coverage": "jest --coverage",
"prettier": "prettier --write \"./**/*.{js,jsx,scss}\"",
"precommit": "./node_modules/.bin/lint-staged"
},
diff --git a/web/yarn-error.log b/web/yarn-error.log
deleted file mode 100644
index 2e432a7f..00000000
--- a/web/yarn-error.log
+++ /dev/null
@@ -1,128 +0,0 @@
-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?$": "/fileTransformer.js"
- },
- "setupFiles": [
- "/injectGlobalVariable.js",
- "jest-localstorage-mock"
- ],
- "moduleDirectories": [
- "node_modules",
- "/app"
- ],
- "testMatch": [
- "/**/*.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. (/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)