| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {combineReducers} from 'redux'; |
| import {createSelector} from 'reselect'; |
| import {createReducer, createRequestReducer} from './redux-helpers.js'; |
| import {prpcClient} from 'prpc-client-instance.js'; |
| import {objectToMap} from 'shared/helpers.js'; |
| import {userRefToId, userToUserRef} from 'shared/convertersV0.js'; |
| import loadGapi, {fetchGapiEmail} from 'shared/gapi-loader.js'; |
| import {viewedProjectName} from 'reducers/projectV0.js'; |
| |
| // Actions |
| const FETCH_START = 'userV0/FETCH_START'; |
| const FETCH_SUCCESS = 'userV0/FETCH_SUCCESS'; |
| const FETCH_FAILURE = 'userV0/FETCH_FAILURE'; |
| |
| export const FETCH_PROJECTS_START = 'userV0/FETCH_PROJECTS_START'; |
| export const FETCH_PROJECTS_SUCCESS = 'userV0/FETCH_PROJECTS_SUCCESS'; |
| export const FETCH_PROJECTS_FAILURE = 'userV0/FETCH_PROJECTS_FAILURE'; |
| |
| const FETCH_HOTLISTS_START = 'userV0/FETCH_HOTLISTS_START'; |
| const FETCH_HOTLISTS_SUCCESS = 'userV0/FETCH_HOTLISTS_SUCCESS'; |
| const FETCH_HOTLISTS_FAILURE = 'userV0/FETCH_HOTLISTS_FAILURE'; |
| |
| const FETCH_PREFS_START = 'userV0/FETCH_PREFS_START'; |
| const FETCH_PREFS_SUCCESS = 'userV0/FETCH_PREFS_SUCCESS'; |
| const FETCH_PREFS_FAILURE = 'userV0/FETCH_PREFS_FAILURE'; |
| |
| export const SET_PREFS_START = 'userV0/SET_PREFS_START'; |
| export const SET_PREFS_SUCCESS = 'userV0/SET_PREFS_SUCCESS'; |
| export const SET_PREFS_FAILURE = 'userV0/SET_PREFS_FAILURE'; |
| |
| const GAPI_LOGIN_START = 'GAPI_LOGIN_START'; |
| export const GAPI_LOGIN_SUCCESS = 'GAPI_LOGIN_SUCCESS'; |
| const GAPI_LOGIN_FAILURE = 'GAPI_LOGIN_FAILURE'; |
| |
| const GAPI_LOGOUT_START = 'GAPI_LOGOUT_START'; |
| export const GAPI_LOGOUT_SUCCESS = 'GAPI_LOGOUT_SUCCESS'; |
| const GAPI_LOGOUT_FAILURE = 'GAPI_LOGOUT_FAILURE'; |
| |
| |
| // Monorial UserPrefs are stored as plain strings in Monorail's backend. |
| // We want boolean preferences to be converted into booleans for convenience. |
| // Currently, there are no user prefs in Monorail that are NOT booleans, so |
| // we default to converting all user prefs to booleans unless otherwise |
| // specified. |
| // See: https://source.chromium.org/chromium/infra/infra/+/main:appengine/monorail/framework/framework_bizobj.py;l=409 |
| const NON_BOOLEAN_PREFS = new Set(['test_non_bool']); |
| |
| |
| /* State Shape |
| { |
| currentUser: { |
| ...user: Object, |
| groups: Array, |
| hotlists: Array, |
| prefs: Object, |
| gapiEmail: String, |
| }, |
| requests: { |
| fetch: Object, |
| fetchHotlists: Object, |
| fetchPrefs: Object, |
| }, |
| } |
| */ |
| |
| // Reducers |
| const USER_DEFAULT = { |
| groups: [], |
| hotlists: [], |
| projects: {}, |
| prefs: {}, |
| prefsLoaded: false, |
| }; |
| |
| const gapiEmailReducer = (user, action) => { |
| return { |
| ...user, |
| gapiEmail: action.email || '', |
| }; |
| }; |
| |
| export const currentUserReducer = createReducer(USER_DEFAULT, { |
| [FETCH_SUCCESS]: (_user, action) => { |
| return { |
| ...USER_DEFAULT, |
| ...action.user, |
| groups: action.groups, |
| }; |
| }, |
| [FETCH_HOTLISTS_SUCCESS]: (user, action) => { |
| return {...user, hotlists: action.hotlists}; |
| }, |
| [FETCH_PREFS_SUCCESS]: (user, action) => { |
| return { |
| ...user, |
| prefs: action.prefs, |
| prefsLoaded: true, |
| }; |
| }, |
| [SET_PREFS_SUCCESS]: (user, action) => { |
| const newPrefs = action.newPrefs; |
| const prefs = Object.assign({}, user.prefs); |
| newPrefs.forEach(({name, value}) => { |
| prefs[name] = value; |
| }); |
| return { |
| ...user, |
| prefs, |
| }; |
| }, |
| [GAPI_LOGIN_SUCCESS]: gapiEmailReducer, |
| [GAPI_LOGOUT_SUCCESS]: gapiEmailReducer, |
| }); |
| |
| export const usersByIdReducer = createReducer({}, { |
| [FETCH_PROJECTS_SUCCESS]: (state, action) => { |
| const newState = {...state}; |
| |
| action.usersProjects.forEach((userProjects) => { |
| const {userRef, ownerOf = [], memberOf = [], contributorTo = [], |
| starredProjects = []} = userProjects; |
| |
| const userId = userRefToId(userRef); |
| |
| newState[userId] = { |
| ...newState[userId], |
| projects: { |
| ownerOf, |
| memberOf, |
| contributorTo, |
| starredProjects, |
| }, |
| }; |
| }); |
| |
| return newState; |
| }, |
| }); |
| |
| const requestsReducer = combineReducers({ |
| // Request for getting backend metadata related to a user, such as |
| // which groups they belong to and whether they're a site admin. |
| fetch: createRequestReducer(FETCH_START, FETCH_SUCCESS, FETCH_FAILURE), |
| // Requests for fetching projects a user is related to. |
| fetchProjects: createRequestReducer( |
| FETCH_PROJECTS_START, FETCH_PROJECTS_SUCCESS, FETCH_PROJECTS_FAILURE), |
| // Request for getting a user's hotlists. |
| fetchHotlists: createRequestReducer( |
| FETCH_HOTLISTS_START, FETCH_HOTLISTS_SUCCESS, FETCH_HOTLISTS_FAILURE), |
| // Request for getting a user's prefs. |
| fetchPrefs: createRequestReducer( |
| FETCH_PREFS_START, FETCH_PREFS_SUCCESS, FETCH_PREFS_FAILURE), |
| // Request for setting a user's prefs. |
| setPrefs: createRequestReducer( |
| SET_PREFS_START, SET_PREFS_SUCCESS, SET_PREFS_FAILURE), |
| }); |
| |
| export const reducer = combineReducers({ |
| currentUser: currentUserReducer, |
| usersById: usersByIdReducer, |
| requests: requestsReducer, |
| }); |
| |
| // Selectors |
| export const requests = (state) => state.userV0.requests; |
| export const currentUser = (state) => state.userV0.currentUser || {}; |
| // TODO(zhangtiff): Replace custom logic to check if the user is logged in |
| // across the frontend. |
| export const isLoggedIn = createSelector( |
| currentUser, (user) => user && user.userId); |
| export const userRef = createSelector( |
| currentUser, (user) => userToUserRef(user)); |
| export const prefs = createSelector( |
| currentUser, viewedProjectName, (user, projectName = '') => { |
| const prefs = { |
| // Make Markdown default to true for projects who have opted in. |
| render_markdown: 'true', |
| ...user.prefs |
| }; |
| for (let prefName of Object.keys(prefs)) { |
| if (!NON_BOOLEAN_PREFS.has(prefName)) { |
| // Monorail user preferences are stored as strings. |
| prefs[prefName] = prefs[prefName] === 'true'; |
| } |
| } |
| return objectToMap(prefs); |
| }); |
| |
| const _usersById = (state) => state.userV0.usersById || {}; |
| export const usersById = createSelector(_usersById, |
| (usersById) => objectToMap(usersById)); |
| |
| export const projectsPerUser = createSelector(usersById, (usersById) => { |
| const map = new Map(); |
| for (const [key, value] of usersById.entries()) { |
| if (value.projects) { |
| map.set(key, value.projects); |
| } |
| } |
| return map; |
| }); |
| |
| // Projects for just the current user. |
| export const projects = createSelector(projectsPerUser, userRef, |
| (projectsMap, userRef) => projectsMap.get(userRefToId(userRef)) || {}); |
| |
| // Action Creators |
| /** |
| * Fetches the data required to view the logged in user. |
| * @param {string} displayName The display name of the logged in user. Note that |
| * while usernames may be hidden for users in Monorail, the logged in user |
| * will always be able to view their own name. |
| * @return {function(function): Promise<void>} |
| */ |
| export const fetch = (displayName) => async (dispatch) => { |
| dispatch({type: FETCH_START}); |
| |
| const message = { |
| userRef: {displayName}, |
| }; |
| |
| try { |
| const resp = await Promise.all([ |
| prpcClient.call( |
| 'monorail.Users', 'GetUser', message), |
| prpcClient.call( |
| 'monorail.Users', 'GetMemberships', message), |
| ]); |
| |
| const user = resp[0]; |
| |
| dispatch({ |
| type: FETCH_SUCCESS, |
| user, |
| groups: resp[1].groupRefs || [], |
| }); |
| |
| const userRef = userToUserRef(user); |
| |
| dispatch(fetchProjects([userRef])); |
| const hotlistsPromise = dispatch(fetchHotlists(userRef)); |
| dispatch(fetchPrefs()); |
| |
| hotlistsPromise.then((hotlists) => { |
| // TODO(crbug.com/monorail/5828): Remove |
| // window.TKR_populateHotlistAutocomplete once the old |
| // autocomplete code is deprecated. |
| window.TKR_populateHotlistAutocomplete(hotlists); |
| }); |
| } catch (error) { |
| dispatch({type: FETCH_FAILURE, error}); |
| }; |
| }; |
| |
| export const fetchProjects = (userRefs) => async (dispatch) => { |
| dispatch({type: FETCH_PROJECTS_START}); |
| try { |
| const resp = await prpcClient.call( |
| 'monorail.Users', 'GetUsersProjects', {userRefs}); |
| dispatch({type: FETCH_PROJECTS_SUCCESS, usersProjects: resp.usersProjects}); |
| } catch (error) { |
| dispatch({type: FETCH_PROJECTS_FAILURE, error}); |
| } |
| }; |
| |
| /** |
| * Fetches all of a given user's hotlists. |
| * @param {UserRef} userRef The user to fetch hotlists for. |
| * @return {function(function): Promise<Array<HotlistV0>>} |
| */ |
| export const fetchHotlists = (userRef) => async (dispatch) => { |
| dispatch({type: FETCH_HOTLISTS_START}); |
| |
| try { |
| const resp = await prpcClient.call( |
| 'monorail.Features', 'ListHotlistsByUser', {user: userRef}); |
| |
| const hotlists = resp.hotlists || []; |
| hotlists.sort((hotlistA, hotlistB) => { |
| return hotlistA.name.localeCompare(hotlistB.name); |
| }); |
| dispatch({type: FETCH_HOTLISTS_SUCCESS, hotlists}); |
| |
| return hotlists; |
| } catch (error) { |
| dispatch({type: FETCH_HOTLISTS_FAILURE, error}); |
| }; |
| }; |
| |
| /** |
| * Fetches user preferences for the logged in user. |
| * @return {function(function): Promise<void>} |
| */ |
| export const fetchPrefs = () => async (dispatch) => { |
| dispatch({type: FETCH_PREFS_START}); |
| |
| try { |
| const resp = await prpcClient.call( |
| 'monorail.Users', 'GetUserPrefs', {}); |
| const prefs = {}; |
| (resp.prefs || []).forEach(({name, value}) => { |
| prefs[name] = value; |
| }); |
| dispatch({type: FETCH_PREFS_SUCCESS, prefs}); |
| } catch (error) { |
| dispatch({type: FETCH_PREFS_FAILURE, error}); |
| }; |
| }; |
| |
| /** |
| * Action creator for setting a user's preferences. |
| * |
| * @param {Object} newPrefs |
| * @param {boolean} saveChanges |
| * @return {function(function): Promise<void>} |
| */ |
| export const setPrefs = (newPrefs, saveChanges = true) => async (dispatch) => { |
| if (!saveChanges) { |
| dispatch({type: SET_PREFS_SUCCESS, newPrefs}); |
| return; |
| } |
| |
| dispatch({type: SET_PREFS_START}); |
| |
| try { |
| const message = {prefs: newPrefs}; |
| await prpcClient.call( |
| 'monorail.Users', 'SetUserPrefs', message); |
| dispatch({type: SET_PREFS_SUCCESS, newPrefs}); |
| |
| // Re-fetch the user's prefs after saving to prevent prefs from |
| // getting out of sync. |
| dispatch(fetchPrefs()); |
| } catch (error) { |
| dispatch({type: SET_PREFS_FAILURE, error}); |
| } |
| }; |
| |
| /** |
| * Action creator to initiate the gapi.js login flow. |
| * |
| * @return {Promise} Resolved only when gapi.js login succeeds. |
| */ |
| export const initGapiLogin = () => (dispatch) => { |
| dispatch({type: GAPI_LOGIN_START}); |
| |
| return new Promise(async (resolve) => { |
| try { |
| await loadGapi(); |
| gapi.auth2.getAuthInstance().signIn().then(async () => { |
| const email = await fetchGapiEmail(); |
| dispatch({type: GAPI_LOGIN_SUCCESS, email: email}); |
| resolve(); |
| }); |
| } catch (error) { |
| // TODO(jeffcarp): Pop up a message that signIn failed. |
| dispatch({type: GAPI_LOGIN_FAILURE, error}); |
| } |
| }); |
| }; |
| |
| /** |
| * Action creator to log the user out of gapi.js |
| * |
| * @return {undefined} |
| */ |
| export const initGapiLogout = () => async (dispatch) => { |
| dispatch({type: GAPI_LOGOUT_START}); |
| |
| try { |
| await loadGapi(); |
| gapi.auth2.getAuthInstance().signOut().then(() => { |
| dispatch({type: GAPI_LOGOUT_SUCCESS, email: ''}); |
| }); |
| } catch (error) { |
| // TODO(jeffcarp): Pop up a message that signOut failed. |
| dispatch({type: GAPI_LOGOUT_FAILURE, error}); |
| } |
| }; |