From 155a923cd009f00d098d43dbbd27320d0db593fc Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Mon, 12 Dec 2016 18:42:26 -0500 Subject: [PATCH] Added preferences to redux store (#124) * Disabled broken ESLint rule * Added preferences to redux store * Re-enabled all unit tests * Fixed code referencing users.myPreferences * Stopped exporting dispatcher from actions/helpers --- .eslintrc.json | 2 +- service/actions/channels.js | 6 +- service/actions/preferences.js | 70 ++++++++++ service/actions/users.js | 6 +- service/client/client.js | 45 ++++++- service/constants/index.js | 2 + service/constants/preferences.js | 22 ++++ service/constants/users.js | 5 +- service/reducers/entities/index.js | 4 +- service/reducers/entities/preferences.js | 43 ++++++ service/reducers/entities/users.js | 27 ---- service/reducers/requests/index.js | 4 +- service/reducers/requests/preferences.js | 43 ++++++ test/service/actions/channels.test.js | 11 +- test/service/actions/preferences.test.js | 160 +++++++++++++++++++++++ test/service/actions/users.test.js | 14 +- 16 files changed, 406 insertions(+), 58 deletions(-) create mode 100644 service/actions/preferences.js create mode 100644 service/constants/preferences.js create mode 100644 service/reducers/entities/preferences.js create mode 100644 service/reducers/requests/preferences.js create mode 100644 test/service/actions/preferences.test.js diff --git a/.eslintrc.json b/.eslintrc.json index 6cfbf7cc9..b15d50539 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,7 +53,7 @@ "func-call-spacing": [2, "never"], "func-names": 2, "func-style": [2, "declaration"], - "generator-star-spacing": [2, {"before": false, "after": true}], + "generator-star-spacing": [0, {"before": false, "after": true}], "global-require": 2, "guard-for-in": 2, "id-blacklist": 0, diff --git a/service/actions/channels.js b/service/actions/channels.js index 7415f93c2..433c6abe4 100644 --- a/service/actions/channels.js +++ b/service/actions/channels.js @@ -1,7 +1,7 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {Constants, ChannelTypes, UsersTypes} from 'service/constants'; +import {Constants, ChannelTypes, PreferencesTypes, UsersTypes} from 'service/constants'; import {forceLogoutIfNecessary} from './helpers'; import {batchActions} from 'redux-batched-actions'; import Client from 'service/client'; @@ -89,8 +89,8 @@ export function createDirectChannel(userId, otherUserId) { data: member }, { - type: UsersTypes.RECEIVED_PREFERENCE, - data: {category: Constants.CATEGORY_DIRECT_CHANNEL_SHOW, name: otherUserId, value: 'true'} + type: PreferencesTypes.RECEIVED_PREFERENCES, + data: [{category: Constants.CATEGORY_DIRECT_CHANNEL_SHOW, name: otherUserId, value: 'true'}] }, { type: ChannelTypes.CREATE_CHANNEL_SUCCESS diff --git a/service/actions/preferences.js b/service/actions/preferences.js new file mode 100644 index 000000000..c28ad681b --- /dev/null +++ b/service/actions/preferences.js @@ -0,0 +1,70 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Client from 'service/client'; + +import {PreferencesTypes} from 'service/constants'; +import {bindClientFunc, forceLogoutIfNecessary, requestData, requestFailure} from './helpers'; + +import {batchActions} from 'redux-batched-actions'; + +export function getMyPreferences() { + return bindClientFunc( + Client.getMyPreferences, + PreferencesTypes.MY_PREFERENCES_REQUEST, + [PreferencesTypes.RECEIVED_PREFERENCES, PreferencesTypes.MY_PREFERENCES_SUCCESS], + PreferencesTypes.MY_PREFERENCES_FAILURE + ); +} + +export function savePreferences(preferences) { + return async (dispatch, getState) => { + dispatch(requestData(PreferencesTypes.SAVE_PREFERENCES_REQUEST), getState); + + try { + await Client.savePreferences(preferences); + + dispatch(batchActions([ + { + type: PreferencesTypes.RECEIVED_PREFERENCES, + data: preferences + }, + { + type: PreferencesTypes.SAVE_PREFERENCES_SUCCESS + } + ]), getState); + } catch (err) { + forceLogoutIfNecessary(err, dispatch); + dispatch(requestFailure(PreferencesTypes.SAVE_PREFERENCES_FAILURE, err), getState); + } + }; +} + +export function deletePreferences(preferences) { + return async (dispatch, getState) => { + dispatch(requestData(PreferencesTypes.DELETE_PREFERENCES_REQUEST), getState); + + try { + await Client.deletePreferences(preferences); + + dispatch(batchActions([ + { + type: PreferencesTypes.DELETED_PREFERENCES, + data: preferences + }, + { + type: PreferencesTypes.DELETE_PREFERENCES_SUCCESS + } + ]), getState); + } catch (err) { + forceLogoutIfNecessary(err, dispatch); + dispatch(requestFailure(PreferencesTypes.DELETE_PREFERENCES_FAILURE, err), getState); + } + }; +} + +export default { + getMyPreferences, + savePreferences, + deletePreferences +}; diff --git a/service/actions/users.js b/service/actions/users.js index 712226ded..c4276aa04 100644 --- a/service/actions/users.js +++ b/service/actions/users.js @@ -5,8 +5,8 @@ import {batchActions} from 'redux-batched-actions'; import Client from 'service/client'; // TODO: uncomment when PLT-4167 is merged -// import {Constants, UsersTypes, TeamsTypes} from 'constants'; -import {Constants, UsersTypes} from 'service/constants'; +// import {Constants, PreferencesTypes, UsersTypes, TeamsTypes} from 'constants'; +import {Constants, PreferencesTypes, UsersTypes} from 'service/constants'; import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; export function login(loginId, password, mfaToken = '') { @@ -25,7 +25,7 @@ export function login(loginId, password, mfaToken = '') { data }, { - type: UsersTypes.RECEIVED_PREFERENCES, + type: PreferencesTypes.RECEIVED_PREFERENCES, data: await preferences }, diff --git a/service/client/client.js b/service/client/client.js index 1dd77218e..17e101cdb 100644 --- a/service/client/client.js +++ b/service/client/client.js @@ -242,13 +242,6 @@ export default class Client { ); }; - getMyPreferences = async () => { - return this.doFetch( - `${this.getPreferencesRoute()}/`, - {method: 'get'} - ); - }; - getProfiles = async (offset, limit) => { return this.doFetch( `${this.getUsersRoute()}/${offset}/${limit}`, @@ -506,6 +499,7 @@ export default class Client { }; // Post routes + fetchPosts = (teamId, channelId, onRequest, onSuccess, onFailure) => { return this.doFetch( `${this.getPostsRoute(teamId, channelId)}/page/0/60`, @@ -523,6 +517,43 @@ export default class Client { ); }; + // Preferences routes + + getMyPreferences = async () => { + return this.doFetch( + `${this.getPreferencesRoute()}/`, + {method: 'get'} + ); + }; + + savePreferences = async (preferences) => { + return this.doFetch( + `${this.getPreferencesRoute()}/save`, + {method: 'post', body: JSON.stringify(preferences)} + ); + }; + + deletePreferences = async (preferences) => { + return this.doFetch( + `${this.getPreferencesRoute()}/delete`, + {method: 'post', body: JSON.stringify(preferences)} + ); + }; + + getPreferenceCategory = async (category) => { + return this.doFetch( + `${this.getPreferencesRoute()}/${category}`, + {method: 'get'} + ); + }; + + getPreference = async (category, name) => { + return this.doFetch( + `${this.getPreferencesRoute()}/${category}/${name}`, + {method: 'get'} + ); + }; + doFetch = async (url, options) => { const {data} = await this.doFetchWithResponse(url, options); diff --git a/service/constants/index.js b/service/constants/index.js index 0eb2369f3..ae257a294 100644 --- a/service/constants/index.js +++ b/service/constants/index.js @@ -7,6 +7,7 @@ import GeneralTypes from './general'; import UsersTypes from './users'; import TeamsTypes from './teams'; import PostsTypes from './posts'; +import PreferencesTypes from './preferences'; import RequestStatus from './request_status'; export { @@ -16,5 +17,6 @@ export { TeamsTypes, ChannelTypes, PostsTypes, + PreferencesTypes, RequestStatus }; diff --git a/service/constants/preferences.js b/service/constants/preferences.js new file mode 100644 index 000000000..264773b88 --- /dev/null +++ b/service/constants/preferences.js @@ -0,0 +1,22 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import keymirror from 'keymirror'; + +export default keymirror({ + MY_PREFERENCES_REQUEST: null, + MY_PREFERENCES_SUCCESS: null, + MY_PREFERENCES_FAILURE: null, + + SAVE_PREFERENCES_REQUEST: null, + SAVE_PREFERENCES_SUCCESS: null, + SAVE_PREFERENCES_FAILURE: null, + + DELETE_PREFERENCES_REQUEST: null, + DELETE_PREFERENCES_SUCCESS: null, + DELETE_PREFERENCES_FAILURE: null, + + RECEIVED_PREFERENCE: null, + RECEIVED_PREFERENCES: null, + DELETED_PREFERENCES: null +}); diff --git a/service/constants/users.js b/service/constants/users.js index 91900254e..c46425e15 100644 --- a/service/constants/users.js +++ b/service/constants/users.js @@ -54,10 +54,7 @@ const UserTypes = keymirror({ RECEIVED_SESSIONS: null, RECEIVED_REVOKED_SESSION: null, RECEIVED_AUDITS: null, - RECEIVED_STATUSES: null, - RECEIVED_PREFERENCE: null, - RECEIVED_PREFERENCES: null, - DELETED_PREFERENCES: null + RECEIVED_STATUSES: null }); export default UserTypes; diff --git a/service/reducers/entities/index.js b/service/reducers/entities/index.js index 2b241ad10..03753e6c9 100644 --- a/service/reducers/entities/index.js +++ b/service/reducers/entities/index.js @@ -8,11 +8,13 @@ import general from './general'; import users from './users'; import teams from './teams'; import posts from './posts'; +import preferences from './preferences'; export default combineReducers({ general, users, teams, channels, - posts + posts, + preferences }); diff --git a/service/reducers/entities/preferences.js b/service/reducers/entities/preferences.js new file mode 100644 index 000000000..55940d820 --- /dev/null +++ b/service/reducers/entities/preferences.js @@ -0,0 +1,43 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {combineReducers} from 'redux'; +import {PreferencesTypes, UsersTypes} from 'service/constants'; + +function getKey(preference) { + return `${preference.category}-${preference.name}`; +} + +function myPreferences(state = {}, action) { + switch (action.type) { + case PreferencesTypes.RECEIVED_PREFERENCES: { + const nextState = {...state}; + + for (const preference of action.data) { + nextState[getKey(preference)] = preference; + } + + return nextState; + } + case PreferencesTypes.DELETED_PREFERENCES: { + const nextState = {...state}; + + for (const preference of action.data) { + Reflect.deleteProperty(nextState, getKey(preference)); + } + + return nextState; + } + + case UsersTypes.LOGOUT_SUCCESS: + return {}; + default: + return state; + } +} + +export default combineReducers({ + + // object where the key is the category-name and has the corresponding value + myPreferences +}); diff --git a/service/reducers/entities/users.js b/service/reducers/entities/users.js index 817409a8e..03faea9eb 100644 --- a/service/reducers/entities/users.js +++ b/service/reducers/entities/users.js @@ -18,30 +18,6 @@ function currentId(state = '', action) { return state; } -function myPreferences(state = {}, action) { - const nextState = {...state}; - - switch (action.type) { - case UsersTypes.RECEIVED_PREFERENCES: { - const preferences = action.data; - for (const p of preferences) { - nextState[`${p.category}--${p.name}`] = p.value; - } - return nextState; - } - case UsersTypes.RECEIVED_PREFERENCE: { - const p = action.data; - nextState[`${p.category}--${p.name}`] = p.value; - return nextState; - } - case UsersTypes.LOGOUT_SUCCESS: - return {}; - - default: - return state; - } -} - function mySessions(state = [], action) { switch (action.type) { case UsersTypes.RECEIVED_SESSIONS: @@ -171,9 +147,6 @@ export default combineReducers({ // the current selected user currentId, - // object where the key is the category-name and has the corresponding value - myPreferences, - // array with the user's sessions mySessions, diff --git a/service/reducers/requests/index.js b/service/reducers/requests/index.js index eaa89fb6a..ba9db05ab 100644 --- a/service/reducers/requests/index.js +++ b/service/reducers/requests/index.js @@ -7,10 +7,12 @@ import general from './general'; import channels from './channels'; import users from './users'; import teams from './teams'; +import preferences from './preferences'; export default combineReducers({ general, channels, users, - teams + teams, + preferences }); diff --git a/service/reducers/requests/preferences.js b/service/reducers/requests/preferences.js new file mode 100644 index 000000000..72f83482e --- /dev/null +++ b/service/reducers/requests/preferences.js @@ -0,0 +1,43 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {handleRequest, initialRequestState} from './helpers'; +import {PreferencesTypes} from 'service/constants'; + +import {combineReducers} from 'redux'; + +function getMyPreferences(state = initialRequestState(), action) { + return handleRequest( + PreferencesTypes.MY_PREFERENCES_REQUEST, + PreferencesTypes.MY_PREFERENCES_SUCCESS, + PreferencesTypes.MY_PREFERENCES_FAILURE, + state, + action + ); +} + +function savePreferences(state = initialRequestState(), action) { + return handleRequest( + PreferencesTypes.SAVE_PREFERENCES_REQUEST, + PreferencesTypes.SAVE_PREFERENCES_SUCCESS, + PreferencesTypes.SAVE_PREFERENCES_FAILURE, + state, + action + ); +} + +function deletePreferences(state = initialRequestState(), action) { + return handleRequest( + PreferencesTypes.DELETE_PREFERENCES_REQUEST, + PreferencesTypes.DELETE_PREFERENCES_SUCCESS, + PreferencesTypes.DELETE_PREFERENCES_FAILURE, + state, + action + ); +} + +export default combineReducers({ + getMyPreferences, + savePreferences, + deletePreferences +}); diff --git a/test/service/actions/channels.test.js b/test/service/actions/channels.test.js index 52f8219ca..67128449a 100644 --- a/test/service/actions/channels.test.js +++ b/test/service/actions/channels.test.js @@ -63,12 +63,13 @@ describe('Actions.Channels', () => { ); store.subscribe(() => { - const channels = store.getState().entities.channels.channels; - const members = store.getState().entities.channels.myMembers; - const profiles = store.getState().entities.users.profiles; - const preferences = store.getState().entities.users.myPreferences; + const state = store.getState(); + const channels = state.entities.channels.channels; + const members = state.entities.channels.myMembers; + const profiles = state.entities.users.profiles; + const preferences = state.entities.preferences.myPreferences; - const createRequest = store.getState().requests.channels.createChannel; + const createRequest = state.requests.channels.createChannel; if (createRequest.status === RequestStatus.SUCCESS || createRequest.status === RequestStatus.FAILURE) { if (createRequest.error) { diff --git a/test/service/actions/preferences.test.js b/test/service/actions/preferences.test.js new file mode 100644 index 000000000..91387bb08 --- /dev/null +++ b/test/service/actions/preferences.test.js @@ -0,0 +1,160 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import assert from 'assert'; + +import * as Actions from 'service/actions/preferences'; +import Client from 'service/client'; +import configureStore from 'app/store'; +import {RequestStatus} from 'service/constants'; +import TestHelper from 'test/test_helper'; + +describe('Actions.Preferences', () => { + it('getMyPreferences', (done) => { + TestHelper.initBasic(Client).then(async ({user}) => { + const store = configureStore(); + + const existingPreferences = [ + { + user_id: user.id, + category: 'test', + name: 'test1', + value: 'test' + }, + { + user_id: user.id, + category: 'test', + name: 'test2', + value: 'test' + } + ]; + + await Client.savePreferences(existingPreferences); + + store.subscribe(() => { + const state = store.getState(); + const request = state.requests.preferences.getMyPreferences; + + if (request.status === RequestStatus.SUCCESS) { + const myPreferences = state.entities.preferences.myPreferences; + + assert.ok(myPreferences['test-test1']); + assert.deepEqual(existingPreferences[0], myPreferences['test-test1']); + assert.ok(myPreferences['test-test2']); + assert.deepEqual(existingPreferences[1], myPreferences['test-test2']); + + done(); + } else if (request.status === RequestStatus.FAILURE) { + done(new Error(JSON.stringify(request.error))); + } + }); + + Actions.getMyPreferences('1234')(store.dispatch, store.getState); + }); + }); + + it('savePrefrences', (done) => { + TestHelper.initBasic(Client).then(async ({user}) => { + const store = configureStore(); + + const existingPreferences = [ + { + user_id: user.id, + category: 'test', + name: 'test1', + value: 'test' + } + ]; + + await Client.savePreferences(existingPreferences); + Actions.getMyPreferences()(store.dispatch, store.getState); + + const preferences = [ + { + user_id: user.id, + category: 'test', + name: 'test2', + value: 'test' + }, + { + user_id: user.id, + category: 'test', + name: 'test3', + value: 'test' + } + ]; + + store.subscribe(() => { + const state = store.getState(); + const request = state.requests.preferences.savePreferences; + + if (request.status === RequestStatus.SUCCESS) { + const myPreferences = state.entities.preferences.myPreferences; + + assert.ok(myPreferences['test-test1']); + assert.deepEqual(existingPreferences[0], myPreferences['test-test1']); + assert.ok(myPreferences['test-test2']); + assert.deepEqual(preferences[0], myPreferences['test-test2']); + assert.ok(myPreferences['test-test3']); + assert.deepEqual(preferences[1], myPreferences['test-test3']); + + done(); + } else if (request.status === RequestStatus.FAILURE) { + done(new Error(JSON.stringify(request.error))); + } + }); + + Actions.savePreferences(preferences)(store.dispatch, store.getState); + }); + }); + + it('deletePreferences', (done) => { + TestHelper.initBasic(Client).then(async ({user}) => { + const store = configureStore(); + + const existingPreferences = [ + { + user_id: user.id, + category: 'test', + name: 'test1', + value: 'test' + }, + { + user_id: user.id, + category: 'test', + name: 'test2', + value: 'test' + }, + { + user_id: user.id, + category: 'test', + name: 'test3', + value: 'test' + } + ]; + + await Client.savePreferences(existingPreferences); + Actions.getMyPreferences()(store.dispatch, store.getState); + + store.subscribe(() => { + const state = store.getState(); + const request = state.requests.preferences.deletePreferences; + + if (request.status === RequestStatus.SUCCESS) { + const myPreferences = state.entities.preferences.myPreferences; + + assert.ok(!myPreferences['test-test1']); + assert.ok(myPreferences['test-test2']); + assert.deepEqual(existingPreferences[1], myPreferences['test-test2']); + assert.ok(!myPreferences['test-test3']); + + done(); + } else if (request.status === RequestStatus.FAILURE) { + done(new Error(JSON.stringify(request.error))); + } + }); + + Actions.deletePreferences([existingPreferences[0], existingPreferences[2]])(store.dispatch, store.getState); + }); + }); +}); diff --git a/test/service/actions/users.test.js b/test/service/actions/users.test.js index b80b1e39f..6a3650fdf 100644 --- a/test/service/actions/users.test.js +++ b/test/service/actions/users.test.js @@ -16,13 +16,14 @@ describe('Actions.Users', () => { const store = configureStore(); store.subscribe(() => { - const loginRequest = store.getState().requests.users.login; - const currentUserId = store.getState().entities.users.currentId; - const profiles = store.getState().entities.users.profiles; - const preferences = store.getState().entities.users.myPreferences; + const state = store.getState(); + const loginRequest = state.requests.users.login; + const currentUserId = state.entities.users.currentId; + const profiles = state.entities.users.profiles; + const preferences = state.entities.preferences.myPreferences; // TODO: uncomment when PLT-4167 is merged - // const teamMembers = store.getState().entities.teams.myMembers; + // const teamMembers = state.entities.teams.myMembers; if (loginRequest.status === RequestStatus.SUCCESS || loginRequest.status === RequestStatus.FAILURE) { if (loginRequest.error) { @@ -63,6 +64,7 @@ describe('Actions.Users', () => { const teams = state.entities.teams; const channels = state.entities.channels; const posts = state.entities.posts; + const preferences = state.entities.preferences; const navigation = state.navigation; if (logoutRequest.status === RequestStatus.SUCCESS || logoutRequest.status === RequestStatus.FAILURE) { @@ -72,7 +74,6 @@ describe('Actions.Users', () => { assert.deepStrictEqual(general.config, {}, 'config not empty'); assert.deepStrictEqual(general.license, {}, 'license not empty'); assert.strictEqual(users.currentId, '', 'current user id not empty'); - assert.deepStrictEqual(users.myPreferences, {}, 'user preferences not empty'); assert.deepStrictEqual(users.mySessions, [], 'user sessions not empty'); assert.deepStrictEqual(users.myAudits, [], 'user audits not empty'); assert.deepStrictEqual(users.profiles, {}, 'user profiles not empty'); @@ -96,6 +97,7 @@ describe('Actions.Users', () => { assert.strictEqual(posts.currentFocusedPostId, '', 'current focused post id is not empty'); assert.deepStrictEqual(posts.postsInfo, {}, 'posts info is not empty'); assert.deepStrictEqual(posts.latestPageTime, {}, 'posts latest page time is not empty'); + assert.deepStrictEqual(preferences.myPreferences, {}, 'user preferences not empty'); assert.strictEqual(navigation.index, 0, 'navigation not reset to first element of stack'); assert.deepStrictEqual(navigation.routes, [Routes.Root], 'navigation not reset to root route');