blob: af8609c99ed6225ac88e5bd07fefd4206e7dd973 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview User actions, selectors, and reducers organized into
7 * a single Redux "Duck" that manages updating and retrieving user state
8 * on the frontend.
9 *
10 * The User data is stored in a normalized format.
11 * `byName` stores all User data indexed by User name.
12 * `user` is a selector that gets the currently viewed User data.
13 *
14 * Reference: https://github.com/erikras/ducks-modular-redux
15 */
16
17import {combineReducers} from 'redux';
18import {createReducer, createKeyedRequestReducer} from './redux-helpers.js';
19import {prpcClient} from 'prpc-client-instance.js';
20import 'shared/typedef.js';
21
22/** @typedef {import('redux').AnyAction} AnyAction */
23
24// Actions
25export const LOG_IN = 'user/LOG_IN';
26
27export const BATCH_GET_START = 'user/BATCH_GET_START';
28export const BATCH_GET_SUCCESS = 'user/BATCH_GET_SUCCESS';
29export const BATCH_GET_FAILURE = 'user/BATCH_GET_FAILURE';
30
31export const FETCH_START = 'user/FETCH_START';
32export const FETCH_SUCCESS = 'user/FETCH_SUCCESS';
33export const FETCH_FAILURE = 'user/FETCH_FAILURE';
34
35export const GATHER_PROJECT_MEMBERSHIPS_START =
36 'user/GATHER_PROJECT_MEMBERSHIPS_START';
37export const GATHER_PROJECT_MEMBERSHIPS_SUCCESS =
38 'user/GATHER_PROJECT_MEMBERSHIPS_SUCCESS';
39export const GATHER_PROJECT_MEMBERSHIPS_FAILURE =
40 'user/GATHER_PROJECT_MEMBERSHIPS_FAILURE';
41
42/* State Shape
43{
44 currentUserName: ?string,
45
46 byName: Object<UserName, User>,
47
48 requests: {
49 batchGet: ReduxRequestState,
50 fetch: ReduxRequestState,
51 gatherProjectMemberships: ReduxRequestState,
52 },
53}
54*/
55
56// Reducers
57
58/**
59 * A reference to the currently logged in user.
60 * @param {?string} state The current user name.
61 * @param {AnyAction} action
62 * @param {User} action.user The user that was logged in.
63 * @return {?string}
64 */
65export const currentUserNameReducer = createReducer(null, {
66 [LOG_IN]: (_state, {user}) => user.name,
67});
68
69/**
70 * All User data indexed by User name.
71 * @param {Object<UserName, User>} state The existing User data.
72 * @param {AnyAction} action
73 * @param {User} action.user The user that was fetched.
74 * @return {Object<UserName, User>}
75 */
76export const byNameReducer = createReducer({}, {
77 [BATCH_GET_SUCCESS]: (state, {users}) => {
78 const newState = {...state};
79 for (const user of users) {
80 newState[user.name] = user;
81 }
82 return newState;
83 },
84 [FETCH_SUCCESS]: (state, {user}) => ({...state, [user.name]: user}),
85});
86
87/**
88 * ProjectMember data indexed by User name.
89 *
90 * Pragma: No normalization for ProjectMember objects. There is never a
91 * situation when we will have access to ProjectMember names but not associated
92 * ProjectMember objects so normalizing is unnecessary.
93 * @param {Object<UserName, Array<ProjectMember>>} state The existing User data.
94 * @param {AnyAction} action
95 * @param {UserName} action.userName The resource name of the user that was
96 * fetched.
97 * @param {Array<ProjectMember>=} action.projectMemberships The project
98 * memberships for the fetched user.
99 * @return {Object<UserName, Array<ProjectMember>>}
100 */
101export const projectMembershipsReducer = createReducer({}, {
102 [GATHER_PROJECT_MEMBERSHIPS_SUCCESS]: (state, {userName,
103 projectMemberships}) => {
104 const newState = {...state};
105
106 newState[userName] = projectMemberships || [];
107 return newState;
108 },
109});
110
111const requestsReducer = combineReducers({
112 batchGet: createKeyedRequestReducer(
113 BATCH_GET_START, BATCH_GET_SUCCESS, BATCH_GET_FAILURE),
114 fetch: createKeyedRequestReducer(
115 FETCH_START, FETCH_SUCCESS, FETCH_FAILURE),
116 gatherProjectMemberships: createKeyedRequestReducer(
117 GATHER_PROJECT_MEMBERSHIPS_START, GATHER_PROJECT_MEMBERSHIPS_SUCCESS,
118 GATHER_PROJECT_MEMBERSHIPS_FAILURE),
119});
120
121export const reducer = combineReducers({
122 currentUserName: currentUserNameReducer,
123 byName: byNameReducer,
124 projectMemberships: projectMembershipsReducer,
125
126 requests: requestsReducer,
127});
128
129// Selectors
130
131/**
132 * Returns the currently logged in user name, or null if there is none.
133 * @param {any} state
134 * @return {?string}
135 */
136export const currentUserName = (state) => state.users.currentUserName;
137
138/**
139 * Returns all the User data in the store as a mapping from name to User.
140 * @param {any} state
141 * @return {Object<UserName, User>}
142 */
143export const byName = (state) => state.users.byName;
144
145/**
146 * Returns all the ProjectMember data in the store, mapped to Users' names.
147 * @param {any} state
148 * @return {Object<UserName, ProjectMember>}
149 */
150export const projectMemberships = (state) => state.users.projectMemberships;
151
152// Action Creators
153
154/**
155 * Action creator to fetch multiple User objects.
156 * @param {Array<UserName>} names The names of the Users to fetch.
157 * @return {function(function): Promise<void>}
158 */
159export const batchGet = (names) => async (dispatch) => {
160 dispatch({type: BATCH_GET_START});
161
162 try {
163 /** @type {{users: Array<User>}} */
164 const {users} = await prpcClient.call(
165 'monorail.v3.Users', 'BatchGetUsers', {names});
166
167 dispatch({type: BATCH_GET_SUCCESS, users});
168 } catch (error) {
169 dispatch({type: BATCH_GET_FAILURE, error});
170 };
171};
172
173/**
174 * Action creator to fetch a single User object.
175 * TODO(https://crbug.com/monorail/7824): Maybe decouple LOG_IN from
176 * FETCH_SUCCESS once we move away from server-side logins.
177 * @param {UserName} name The resource name of the User to fetch.
178 * @return {function(function): Promise<void>}
179 */
180export const fetch = (name) => async (dispatch) => {
181 dispatch({type: FETCH_START});
182
183 try {
184 /** @type {User} */
185 const user = await prpcClient.call('monorail.v3.Users', 'GetUser', {name});
186 dispatch({type: FETCH_SUCCESS, user});
187 dispatch({type: LOG_IN, user});
188 } catch (error) {
189 dispatch({type: FETCH_FAILURE, error});
190 }
191};
192
193/**
194 * Action creator to fetch ProjectMember objects for a given User.
195 * @param {UserName} name The resource name of the User.
196 * @return {function(function): Promise<void>}
197 */
198export const gatherProjectMemberships = (name) => async (dispatch) => {
199 dispatch({type: GATHER_PROJECT_MEMBERSHIPS_START});
200
201 try {
202 /** @type {{projectMemberships: Array<ProjectMember>}} */
203 const {projectMemberships} = await prpcClient.call(
204 'monorail.v3.Frontend', 'GatherProjectMembershipsForUser',
205 {user: name});
206
207 dispatch({type: GATHER_PROJECT_MEMBERSHIPS_SUCCESS,
208 userName: name, projectMemberships});
209 } catch (error) {
210 // TODO(crbug.com/monorail/7627): Catch actual API errors.
211 dispatch({type: GATHER_PROJECT_MEMBERSHIPS_FAILURE, error});
212 };
213};
214
215export const users = {
216 currentUserName,
217 byName,
218 projectMemberships,
219 batchGet,
220 fetch,
221 gatherProjectMemberships,
222};