Skip to content

Commit 7dbd79e

Browse files
committed
Implementing user syncing (SIS -> ReCodEx).
1 parent 7dafcef commit 7dbd79e

File tree

8 files changed

+111
-150
lines changed

8 files changed

+111
-150
lines changed

src/containers/LayoutContainer/LayoutContainer.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import moment from 'moment';
77
import { setLang } from '../../redux/modules/app.js';
88
import { getLang, anyPendingFetchOperations } from '../../redux/selectors/app.js';
99
import { isLoggedIn } from '../../redux/selectors/auth.js';
10-
import { getLoggedInUserSettings } from '../../redux/selectors/users.js';
1110

1211
import Layout from '../../components/layout/Layout';
1312
import { messages } from '../../locales';
@@ -69,12 +68,7 @@ class LayoutContainer extends Component {
6968
* Get messages for the given language or the deafult - English
7069
*/
7170

72-
getDefaultLang = () => {
73-
const { userSettings } = this.props;
74-
return userSettings && userSettings.defaultLanguage ? userSettings.defaultLanguage : 'en';
75-
};
76-
77-
getMessages = lang => messages[lang] || messages[this.getDefaultLang()];
71+
getMessages = lang => messages[lang] || messages.en;
7872

7973
render() {
8074
const {
@@ -116,7 +110,6 @@ LayoutContainer.propTypes = {
116110
sidebarIsCollapsed: PropTypes.bool,
117111
sidebarIsOpen: PropTypes.bool,
118112
location: withRouterProps.location,
119-
userSettings: PropTypes.object,
120113
};
121114

122115
export default withRouter(
@@ -125,7 +118,6 @@ export default withRouter(
125118
lang: getLang(state),
126119
isLoggedIn: isLoggedIn(state),
127120
pendingFetchOperations: anyPendingFetchOperations(state),
128-
userSettings: getLoggedInUserSettings(state),
129121
}),
130122
dispatch => ({
131123
setLang: lang => {
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { connect } from 'react-redux';
22
import Sidebar from '../../components/layout/Sidebar/Sidebar.js';
3-
import { loggedInUserSelector, notificationsSelector } from '../../redux/selectors/users.js';
3+
import { loggedInUserSelector } from '../../redux/selectors/users.js';
44

55
export default connect(state => ({
66
loggedInUser: loggedInUserSelector(state),
7-
notifications: notificationsSelector(state),
87
}))(Sidebar);

src/locales/cs.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@
116116
"app.user.sisUserLoadedCallout": "Data ze SISu byla úspěšně načtena.",
117117
"app.user.syncButton": "Přepsat ReCodEx daty ze SISu",
118118
"app.user.title": "Osobní údaje",
119+
"app.user.userSyncFailedCallout": "Synchronizace uživatele selhala. Prosím, znovu načťete stránku a opakujte akci později.",
120+
"app.user.userUpdatedCallout": "Uživatelská data v ReCodExu byla úspěšně aktualizována daty ze SISu.",
119121
"app.userName.externalIds": "Externí identifikátory",
120122
"app.userName.externalIdsClickInfo": "klikutím zkopírujete ID do schránky",
121123
"app.userName.userDeactivated": "Uživatelský účet byl deaktivován. Uživatel se nemůže přihlásit.",

src/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@
116116
"app.user.sisUserLoadedCallout": "The SIS user data were successfully (re)loaded.",
117117
"app.user.syncButton": "Overwrite ReCodEx with SIS Data",
118118
"app.user.title": "Personal Data",
119+
"app.user.userSyncFailedCallout": "User sync operation failed. Reload the page and try again later.",
120+
"app.user.userUpdatedCallout": "The ReCodEx user data were successfully updated by current SIS data.",
119121
"app.userName.externalIds": "External identifiers",
120122
"app.userName.externalIdsClickInfo": "click to copy the ID(s) to clipboard",
121123
"app.userName.userDeactivated": "The user account was deactivated. The user may not sign in.",

src/pages/User/User.js

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import Box from '../../components/widgets/Box';
1111
import DateTime from '../../components/widgets/DateTime';
1212
import Button, { TheButtonGroup } from '../../components/widgets/TheButton';
1313
import Icon, { LoadingIcon, RefreshIcon, UserProfileIcon, WarningIcon } from '../../components/icons';
14-
import { fetchUserIfNeeded } from '../../redux/modules/users.js';
14+
import { fetchUserIfNeeded, syncUser, syncUserReset } from '../../redux/modules/users.js';
1515
import { fetchSisUser, fetchSisUserIfNeeded } from '../../redux/modules/sisUsers.js';
16-
import { loggedInUserSelector } from '../../redux/selectors/users.js';
16+
import { loggedInUserSelector, isUserSyncing, isUserUpdated, isUserSyncFailed } from '../../redux/selectors/users.js';
1717
import { loggedInSisUserSelector } from '../../redux/selectors/sisUsers.js';
1818
import { isLoading, hasFailed, getJsData } from '../../redux/helpers/resourceManager';
1919

@@ -83,7 +83,16 @@ class User extends Component {
8383
Promise.all([dispatch(fetchUserIfNeeded(userId)), dispatch(fetchSisUserIfNeeded(userId, 30))]);
8484

8585
render() {
86-
const { loggedInUser, loggedInSisUser, fetchSisUser } = this.props;
86+
const {
87+
loggedInUser,
88+
loggedInSisUser,
89+
fetchSisUser,
90+
syncUser,
91+
syncReset,
92+
isUserSyncing = false,
93+
isUserUpdated = false,
94+
isUserSyncFailed = false,
95+
} = this.props;
8796
const sisUserData = getJsData(loggedInSisUser)?.sisUser;
8897
const sisUserUpdated = Boolean(getJsData(loggedInSisUser)?.updated);
8998
const sisUserUpdateFailed = Boolean(getJsData(loggedInSisUser)?.failed);
@@ -111,6 +120,23 @@ class User extends Component {
111120
</Callout>
112121
)}
113122

123+
{isUserUpdated && (
124+
<Callout variant="success">
125+
<FormattedMessage
126+
id="app.user.userUpdatedCallout"
127+
defaultMessage="The ReCodEx user data were successfully updated by current SIS data."
128+
/>
129+
</Callout>
130+
)}
131+
{isUserSyncFailed && (
132+
<Callout variant="danger">
133+
<FormattedMessage
134+
id="app.user.userSyncFailedCallout"
135+
defaultMessage="User sync operation failed. Reload the page and try again later."
136+
/>
137+
</Callout>
138+
)}
139+
114140
<Table bordered>
115141
<thead>
116142
<tr>
@@ -189,16 +215,22 @@ class User extends Component {
189215

190216
<div className="text-center">
191217
<TheButtonGroup>
192-
{Object.values(diffIndex).some(v => v) && (
193-
<Button variant="success">
194-
<Icon icon="left-long" gapRight />
218+
{(isUserSyncing || Object.values(diffIndex).some(v => v)) && (
219+
<Button
220+
variant={isUserSyncFailed ? 'danger' : 'success'}
221+
disabled={isUserSyncing}
222+
onClick={() => syncUser(user.id)}>
223+
{isUserSyncing ? <LoadingIcon gapRight /> : <Icon icon="left-long" gapRight />}
195224
<FormattedMessage id="app.user.syncButton" defaultMessage="Overwrite ReCodEx with SIS Data" />
196225
</Button>
197226
)}
198227
<Button
199228
variant={sisUserUpdateFailed ? 'danger' : 'primary'}
200229
disabled={isLoading(loggedInSisUser)}
201-
onClick={() => fetchSisUser(user.id, 0)}>
230+
onClick={() => {
231+
syncReset(user.id);
232+
return fetchSisUser(user.id, 0);
233+
}}>
202234
{isLoading(loggedInSisUser) ? <LoadingIcon gapRight /> : <RefreshIcon gapRight />}
203235
<FormattedMessage id="app.user.fetchSisButton" defaultMessage="Refresh SIS Data" />
204236
</Button>
@@ -215,19 +247,32 @@ class User extends Component {
215247
User.propTypes = {
216248
loggedInUser: ImmutablePropTypes.map,
217249
loggedInSisUser: ImmutablePropTypes.map,
250+
isUserSyncing: PropTypes.bool,
251+
isUserUpdated: PropTypes.bool,
252+
isUserSyncFailed: PropTypes.bool,
218253
loadAsync: PropTypes.func.isRequired,
219254
fetchSisUser: PropTypes.func.isRequired,
220255
fetchSisUserIfNeeded: PropTypes.func.isRequired,
256+
syncUser: PropTypes.func.isRequired,
257+
syncReset: PropTypes.func.isRequired,
221258
};
222259

223260
export default connect(
224-
state => ({
225-
loggedInUser: loggedInUserSelector(state),
226-
loggedInSisUser: loggedInSisUserSelector(state),
227-
}),
261+
state => {
262+
const loggedInUser = loggedInUserSelector(state);
263+
return {
264+
loggedInUser,
265+
loggedInSisUser: loggedInSisUserSelector(state),
266+
isUserSyncing: isUserSyncing(state, loggedInUser && loggedInUser.getIn(['data', 'id'], '')),
267+
isUserUpdated: isUserUpdated(state, loggedInUser && loggedInUser.getIn(['data', 'id'], '')),
268+
isUserSyncFailed: isUserSyncFailed(state, loggedInUser && loggedInUser.getIn(['data', 'id'], '')),
269+
};
270+
},
228271
dispatch => ({
229272
loadAsync: userId => User.loadAsync({ userId }, dispatch),
230273
fetchSisUser: (userId, expiration = null) => dispatch(fetchSisUser(userId, expiration)),
231274
fetchSisUserIfNeeded: (userId, expiration = null) => dispatch(fetchSisUserIfNeeded(userId, expiration)),
275+
syncUser: userId => dispatch(syncUser(userId)),
276+
syncReset: userId => dispatch(syncUserReset(userId)),
232277
})
233278
)(User);

src/redux/modules/sisUsers.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@ import { handleActions } from 'redux-actions';
33

44
import factory, {
55
initialState,
6-
// createRecord,
7-
// resourceStatus,
8-
createActionsWithPostfixes,
96
getJsData,
107
isLoading,
118
defaultNeedsRefetching,
9+
createRecord,
10+
resourceStatus,
1211
} from '../helpers/resourceManager';
1312
import { defaultSelectorFactory } from '../helpers/resourceManager/utils.js';
1413
import { createApiAction } from '../middleware/apiMiddleware.js';
1514

16-
export const additionalActionTypes = {
17-
// createActionsWithPostfixes generates all 4 constants for async operations
18-
...createActionsWithPostfixes('FETCH_BY_IDS', 'siscodex/users'),
19-
};
15+
import { additionalActionTypes as userActionTypes } from './users.js';
16+
17+
export const additionalActionTypes = {};
2018

2119
const resourceName = 'sisUsers';
2220
const apiEndpointFactory = id => `/users/${id}/sisuser`;
@@ -60,7 +58,11 @@ export const fetchSisUserIfNeeded =
6058

6159
const reducer = handleActions(
6260
Object.assign({}, reduceActions, {
63-
//
61+
[userActionTypes.SYNC_FULFILLED]: (state, { payload: { sisUser }, meta: { id } }) =>
62+
state.setIn(
63+
['resources', id],
64+
createRecord({ state: resourceStatus.FULFILLED, data: { sisUser, updated: false, failed: false } })
65+
),
6466
}),
6567
initialState
6668
);

src/redux/modules/users.js

Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import { handleActions } from 'redux-actions';
2-
import { fromJS } from 'immutable';
1+
import { createAction, handleActions } from 'redux-actions';
32

43
import factory, {
54
initialState,
65
createRecord,
76
resourceStatus,
87
createActionsWithPostfixes,
98
} from '../helpers/resourceManager';
9+
import { createApiAction } from '../middleware/apiMiddleware.js';
1010

1111
import { actionTypes as authActionTypes } from './authTypes.js';
1212

1313
export const additionalActionTypes = {
14-
// createActionsWithPostfixes generates all 4 constants for async operations
15-
...createActionsWithPostfixes('FETCH_BY_IDS', 'siscodex/users'),
14+
...createActionsWithPostfixes('SYNC', 'siscodex/users'),
15+
RESET_SYNC: 'siscodex/users/RESET_SYNC',
1616
};
1717

1818
const resourceName = 'users';
@@ -29,55 +29,43 @@ export const fetchManyEndpoint = '/users';
2929
export const fetchUser = actions.fetchResource;
3030
export const fetchUserIfNeeded = actions.fetchOneIfNeeded;
3131

32+
export const syncUser = id =>
33+
createApiAction({
34+
type: additionalActionTypes.SYNC,
35+
endpoint: `/users/${id}/sync`,
36+
method: 'POST',
37+
meta: { id },
38+
});
39+
40+
export const syncUserReset = createAction(additionalActionTypes.RESET_SYNC, id => ({ id }));
41+
3242
/**
3343
* Reducer
3444
*/
3545
const reducer = handleActions(
3646
Object.assign({}, reduceActions, {
37-
[actionTypes.UPDATE_FULFILLED]: (state, { payload, meta: { id } }) =>
38-
state.setIn(
39-
['resources', id, 'data'],
40-
fromJS(payload.user && typeof payload.user === 'object' ? payload.user : payload)
41-
),
42-
43-
[additionalActionTypes.FETCH_BY_IDS_PENDING]: (state, { meta: { ids } }) =>
44-
state.update('resources', users => {
45-
ids.forEach(id => {
46-
if (!users.has(id)) {
47-
users = users.set(id, createRecord());
48-
}
49-
});
50-
return users;
51-
}),
52-
53-
[additionalActionTypes.FETCH_BY_IDS_FULFILLED]: (state, { payload, meta: { ids } }) =>
54-
state.update('resources', users => {
55-
payload.forEach(user => {
56-
users = users.set(user.id, createRecord({ state: resourceStatus.FULFILLED, data: user }));
57-
});
58-
// in case some of the users were not returned in payload
59-
ids.forEach(id => {
60-
if (users.has(id) && users.getIn([id, 'data'], null) === null) {
61-
users = users.delete(id);
62-
}
63-
});
64-
return users;
65-
}),
66-
67-
[additionalActionTypes.FETCH_BY_IDS_REJECTED]: (state, { meta: { ids } }) =>
68-
state.update('resources', users => {
69-
ids.forEach(id => {
70-
if (users.has(id) && users.getIn([id, 'data'], null) === null) {
71-
users = users.delete(id);
72-
}
73-
});
74-
return users;
75-
}),
47+
[additionalActionTypes.SYNC_PENDING]: (state, { meta: { id } }) =>
48+
state
49+
.setIn(['resources', id, 'syncing'], true)
50+
.removeIn(['resources', id, 'updated'])
51+
.removeIn(['resources', id, 'syncFailed']),
52+
53+
[additionalActionTypes.SYNC_FULFILLED]: (state, { payload: { user, updated }, meta: { id } }) =>
54+
state
55+
.setIn(['resources', id], createRecord({ state: resourceStatus.FULFILLED, data: user }))
56+
.setIn(['resources', id, 'syncing'], false)
57+
.setIn(['resources', id, 'updated'], updated),
58+
59+
[additionalActionTypes.SYNC_REJECTED]: (state, { meta: { id } }) =>
60+
state.setIn(['resources', id, 'syncing'], false).setIn(['resources', id, 'syncFailed'], true),
7661

7762
[authActionTypes.LOGIN_FULFILLED]: (state, { payload: { user } }) =>
7863
user && user.id
7964
? state.setIn(['resources', user.id], createRecord({ state: resourceStatus.FULFILLED, data: user }))
8065
: state,
66+
67+
[additionalActionTypes.RESET_SYNC]: (state, { payload: { id } }) =>
68+
state.setIn(['resources', id, 'updated'], false).setIn(['resources', id, 'syncFailed'], false),
8169
}),
8270
initialState
8371
);

0 commit comments

Comments
 (0)