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
This commit is contained in:
Harrison Healey 2016-12-12 18:42:26 -05:00 committed by enahum
parent e2f3f7b803
commit 155a923cd0
16 changed files with 406 additions and 58 deletions

View file

@ -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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

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

View file

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