Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/reducers/users.js b/static_src/reducers/users.js
new file mode 100644
index 0000000..af8609c
--- /dev/null
+++ b/static_src/reducers/users.js
@@ -0,0 +1,222 @@
+// Copyright 2020 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.
+
+/**
+ * @fileoverview User actions, selectors, and reducers organized into
+ * a single Redux "Duck" that manages updating and retrieving user state
+ * on the frontend.
+ *
+ * The User data is stored in a normalized format.
+ * `byName` stores all User data indexed by User name.
+ * `user` is a selector that gets the currently viewed User data.
+ *
+ * Reference: https://github.com/erikras/ducks-modular-redux
+ */
+
+import {combineReducers} from 'redux';
+import {createReducer, createKeyedRequestReducer} from './redux-helpers.js';
+import {prpcClient} from 'prpc-client-instance.js';
+import 'shared/typedef.js';
+
+/** @typedef {import('redux').AnyAction} AnyAction */
+
+// Actions
+export const LOG_IN = 'user/LOG_IN';
+
+export const BATCH_GET_START = 'user/BATCH_GET_START';
+export const BATCH_GET_SUCCESS = 'user/BATCH_GET_SUCCESS';
+export const BATCH_GET_FAILURE = 'user/BATCH_GET_FAILURE';
+
+export const FETCH_START = 'user/FETCH_START';
+export const FETCH_SUCCESS = 'user/FETCH_SUCCESS';
+export const FETCH_FAILURE = 'user/FETCH_FAILURE';
+
+export const GATHER_PROJECT_MEMBERSHIPS_START =
+ 'user/GATHER_PROJECT_MEMBERSHIPS_START';
+export const GATHER_PROJECT_MEMBERSHIPS_SUCCESS =
+ 'user/GATHER_PROJECT_MEMBERSHIPS_SUCCESS';
+export const GATHER_PROJECT_MEMBERSHIPS_FAILURE =
+ 'user/GATHER_PROJECT_MEMBERSHIPS_FAILURE';
+
+/* State Shape
+{
+ currentUserName: ?string,
+
+ byName: Object<UserName, User>,
+
+ requests: {
+ batchGet: ReduxRequestState,
+ fetch: ReduxRequestState,
+ gatherProjectMemberships: ReduxRequestState,
+ },
+}
+*/
+
+// Reducers
+
+/**
+ * A reference to the currently logged in user.
+ * @param {?string} state The current user name.
+ * @param {AnyAction} action
+ * @param {User} action.user The user that was logged in.
+ * @return {?string}
+ */
+export const currentUserNameReducer = createReducer(null, {
+ [LOG_IN]: (_state, {user}) => user.name,
+});
+
+/**
+ * All User data indexed by User name.
+ * @param {Object<UserName, User>} state The existing User data.
+ * @param {AnyAction} action
+ * @param {User} action.user The user that was fetched.
+ * @return {Object<UserName, User>}
+ */
+export const byNameReducer = createReducer({}, {
+ [BATCH_GET_SUCCESS]: (state, {users}) => {
+ const newState = {...state};
+ for (const user of users) {
+ newState[user.name] = user;
+ }
+ return newState;
+ },
+ [FETCH_SUCCESS]: (state, {user}) => ({...state, [user.name]: user}),
+});
+
+/**
+ * ProjectMember data indexed by User name.
+ *
+ * Pragma: No normalization for ProjectMember objects. There is never a
+ * situation when we will have access to ProjectMember names but not associated
+ * ProjectMember objects so normalizing is unnecessary.
+ * @param {Object<UserName, Array<ProjectMember>>} state The existing User data.
+ * @param {AnyAction} action
+ * @param {UserName} action.userName The resource name of the user that was
+ * fetched.
+ * @param {Array<ProjectMember>=} action.projectMemberships The project
+ * memberships for the fetched user.
+ * @return {Object<UserName, Array<ProjectMember>>}
+ */
+export const projectMembershipsReducer = createReducer({}, {
+ [GATHER_PROJECT_MEMBERSHIPS_SUCCESS]: (state, {userName,
+ projectMemberships}) => {
+ const newState = {...state};
+
+ newState[userName] = projectMemberships || [];
+ return newState;
+ },
+});
+
+const requestsReducer = combineReducers({
+ batchGet: createKeyedRequestReducer(
+ BATCH_GET_START, BATCH_GET_SUCCESS, BATCH_GET_FAILURE),
+ fetch: createKeyedRequestReducer(
+ FETCH_START, FETCH_SUCCESS, FETCH_FAILURE),
+ gatherProjectMemberships: createKeyedRequestReducer(
+ GATHER_PROJECT_MEMBERSHIPS_START, GATHER_PROJECT_MEMBERSHIPS_SUCCESS,
+ GATHER_PROJECT_MEMBERSHIPS_FAILURE),
+});
+
+export const reducer = combineReducers({
+ currentUserName: currentUserNameReducer,
+ byName: byNameReducer,
+ projectMemberships: projectMembershipsReducer,
+
+ requests: requestsReducer,
+});
+
+// Selectors
+
+/**
+ * Returns the currently logged in user name, or null if there is none.
+ * @param {any} state
+ * @return {?string}
+ */
+export const currentUserName = (state) => state.users.currentUserName;
+
+/**
+ * Returns all the User data in the store as a mapping from name to User.
+ * @param {any} state
+ * @return {Object<UserName, User>}
+ */
+export const byName = (state) => state.users.byName;
+
+/**
+ * Returns all the ProjectMember data in the store, mapped to Users' names.
+ * @param {any} state
+ * @return {Object<UserName, ProjectMember>}
+ */
+export const projectMemberships = (state) => state.users.projectMemberships;
+
+// Action Creators
+
+/**
+ * Action creator to fetch multiple User objects.
+ * @param {Array<UserName>} names The names of the Users to fetch.
+ * @return {function(function): Promise<void>}
+ */
+export const batchGet = (names) => async (dispatch) => {
+ dispatch({type: BATCH_GET_START});
+
+ try {
+ /** @type {{users: Array<User>}} */
+ const {users} = await prpcClient.call(
+ 'monorail.v3.Users', 'BatchGetUsers', {names});
+
+ dispatch({type: BATCH_GET_SUCCESS, users});
+ } catch (error) {
+ dispatch({type: BATCH_GET_FAILURE, error});
+ };
+};
+
+/**
+ * Action creator to fetch a single User object.
+ * TODO(https://crbug.com/monorail/7824): Maybe decouple LOG_IN from
+ * FETCH_SUCCESS once we move away from server-side logins.
+ * @param {UserName} name The resource name of the User to fetch.
+ * @return {function(function): Promise<void>}
+ */
+export const fetch = (name) => async (dispatch) => {
+ dispatch({type: FETCH_START});
+
+ try {
+ /** @type {User} */
+ const user = await prpcClient.call('monorail.v3.Users', 'GetUser', {name});
+ dispatch({type: FETCH_SUCCESS, user});
+ dispatch({type: LOG_IN, user});
+ } catch (error) {
+ dispatch({type: FETCH_FAILURE, error});
+ }
+};
+
+/**
+ * Action creator to fetch ProjectMember objects for a given User.
+ * @param {UserName} name The resource name of the User.
+ * @return {function(function): Promise<void>}
+ */
+export const gatherProjectMemberships = (name) => async (dispatch) => {
+ dispatch({type: GATHER_PROJECT_MEMBERSHIPS_START});
+
+ try {
+ /** @type {{projectMemberships: Array<ProjectMember>}} */
+ const {projectMemberships} = await prpcClient.call(
+ 'monorail.v3.Frontend', 'GatherProjectMembershipsForUser',
+ {user: name});
+
+ dispatch({type: GATHER_PROJECT_MEMBERSHIPS_SUCCESS,
+ userName: name, projectMemberships});
+ } catch (error) {
+ // TODO(crbug.com/monorail/7627): Catch actual API errors.
+ dispatch({type: GATHER_PROJECT_MEMBERSHIPS_FAILURE, error});
+ };
+};
+
+export const users = {
+ currentUserName,
+ byName,
+ projectMemberships,
+ batchGet,
+ fetch,
+ gatherProjectMemberships,
+};