blob: 871cf875f63bebc2b6e92d071196e18b055b04c6 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 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
5import {combineReducers} from 'redux';
6import {createReducer} from './redux-helpers.js';
7
8/** @typedef {import('redux').AnyAction} AnyAction */
9
10const DEFAULT_SNACKBAR_TIMEOUT_MS = 10 * 1000;
11
12
13/**
14 * Object of various constant strings used to uniquely identify
15 * snackbar instances used in the app.
16 * TODO(https://crbug.com/monorail/7491): Avoid using this Object.
17 * @type {Object<string, string>}
18 */
19export const snackbarNames = Object.freeze({
20 // Issue list page snackbars.
21 FETCH_ISSUE_LIST_ERROR: 'FETCH_ISSUE_LIST_ERROR',
22 FETCH_ISSUE_LIST: 'FETCH_ISSUE_LIST',
23 UPDATE_HOTLISTS_SUCCESS: 'UPDATE_HOTLISTS_SUCCESS',
24
25 // Issue detail page snackbars.
26 ISSUE_COMMENT_ADDED: 'ISSUE_COMMENT_ADDED',
27});
28
29// Actions
30const INCREMENT_NAVIGATION_COUNT = 'INCREMENT_NAVIGATION_COUNT';
31const REPORT_DIRTY_FORM = 'REPORT_DIRTY_FORM';
32const CLEAR_DIRTY_FORMS = 'CLEAR_DIRTY_FORMS';
33const SET_FOCUS_ID = 'SET_FOCUS_ID';
34export const SHOW_SNACKBAR = 'SHOW_SNACKBAR';
35const HIDE_SNACKBAR = 'HIDE_SNACKBAR';
36
37/**
38 * @typedef {Object} Snackbar
39 * @param {string} id Unique string identifying the snackbar.
40 * @param {string} text The text to show in the snackbar.
41 */
42
43/* State Shape
44{
45 navigationCount: number,
46 dirtyForms: Array,
47 focusId: String,
48 snackbars: Array<Snackbar>,
49}
50*/
51
52// Reducers
53
54
55const navigationCountReducer = createReducer(0, {
56 [INCREMENT_NAVIGATION_COUNT]: (state) => state + 1,
57});
58
59/**
60 * Saves state on which forms have been edited, to warn the user
61 * about possible data loss when they navigate away from a page.
62 * @param {Array<string>} state Dirty form names.
63 * @param {AnyAction} action
64 * @param {string} action.name The name of the form being updated.
65 * @param {boolean} action.isDirty Whether the form is dirty or not dirty.
66 * @return {Array<string>}
67 */
68const dirtyFormsReducer = createReducer([], {
69 [REPORT_DIRTY_FORM]: (state, {name, isDirty}) => {
70 const newState = [...state];
71 const index = state.indexOf(name);
72 if (isDirty && index === -1) {
73 newState.push(name);
74 } else if (!isDirty && index !== -1) {
75 newState.splice(index, 1);
76 }
77 return newState;
78 },
79 [CLEAR_DIRTY_FORMS]: () => [],
80});
81
82const focusIdReducer = createReducer(null, {
83 [SET_FOCUS_ID]: (_state, action) => action.focusId,
84});
85
86/**
87 * Updates snackbar state.
88 * @param {Array<Snackbar>} state A snackbar-shaped slice of Redux state.
89 * @param {AnyAction} action
90 * @param {string} action.text The text to display in the snackbar.
91 * @param {string} action.id A unique global ID for the snackbar.
92 * @return {Array<Snackbar>} New snackbar state.
93 */
94export const snackbarsReducer = createReducer([], {
95 [SHOW_SNACKBAR]: (state, {text, id}) => {
96 return [...state, {text, id}];
97 },
98 [HIDE_SNACKBAR]: (state, {id}) => {
99 return state.filter((snackbar) => snackbar.id !== id);
100 },
101});
102
103export const reducer = combineReducers({
104 // Count of "page" navigations.
105 navigationCount: navigationCountReducer,
106 // Forms to be checked for user changes before leaving the page.
107 dirtyForms: dirtyFormsReducer,
108 // The ID of the element to be focused, as given by the hash part of the URL.
109 focusId: focusIdReducer,
110 // Array of snackbars to render on the page.
111 snackbars: snackbarsReducer,
112});
113
114// Selectors
115export const navigationCount = (state) => state.ui.navigationCount;
116export const dirtyForms = (state) => state.ui.dirtyForms;
117export const focusId = (state) => state.ui.focusId;
118
119/**
120 * Retrieves snackbar data from the Redux store.
121 * @param {any} state Redux state.
122 * @return {Array<Snackbar>} All the snackbars in the store.
123 */
124export const snackbars = (state) => state.ui.snackbars;
125
126// Action Creators
127export const incrementNavigationCount = () => {
128 return {type: INCREMENT_NAVIGATION_COUNT};
129};
130
131export const reportDirtyForm = (name, isDirty) => {
132 return {type: REPORT_DIRTY_FORM, name, isDirty};
133};
134
135export const clearDirtyForms = () => ({type: CLEAR_DIRTY_FORMS});
136
137export const setFocusId = (focusId) => {
138 return {type: SET_FOCUS_ID, focusId};
139};
140
141/**
142 * Displays a snackbar.
143 * @param {string} id Unique identifier for a given snackbar. We depend on
144 * snackbar users to keep this unique.
145 * @param {string} text The text to be shown in the snackbar.
146 * @param {number} timeout An optional timeout in milliseconds for how
147 * long to wait to dismiss a snackbar.
148 * @return {function(function): Promise<void>}
149 */
150export const showSnackbar = (id, text,
151 timeout = DEFAULT_SNACKBAR_TIMEOUT_MS) => (dispatch) => {
152 dispatch({type: SHOW_SNACKBAR, text, id});
153
154 if (timeout) {
155 window.setTimeout(() => dispatch(hideSnackbar(id)),
156 timeout);
157 }
158};
159
160/**
161 * Hides a snackbar.
162 * @param {string} id The unique name of the snackbar to be hidden.
163 * @return {any} A Redux action.
164 */
165export const hideSnackbar = (id) => {
166 return {
167 type: HIDE_SNACKBAR,
168 id,
169 };
170};