Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/reducers/userV0.js b/static_src/reducers/userV0.js
new file mode 100644
index 0000000..42a93fc
--- /dev/null
+++ b/static_src/reducers/userV0.js
@@ -0,0 +1,384 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// 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 {DEFAULT_MD_PROJECTS} from 'shared/md-helper.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: String(DEFAULT_MD_PROJECTS.has(projectName)),
+ ...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});
+ }
+};