Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | // 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 | |
| 5 | import {combineReducers} from 'redux'; |
| 6 | import {createReducer} from './redux-helpers.js'; |
| 7 | |
| 8 | /** @typedef {import('redux').AnyAction} AnyAction */ |
| 9 | |
| 10 | const 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 | */ |
| 19 | export 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 |
| 30 | const INCREMENT_NAVIGATION_COUNT = 'INCREMENT_NAVIGATION_COUNT'; |
| 31 | const REPORT_DIRTY_FORM = 'REPORT_DIRTY_FORM'; |
| 32 | const CLEAR_DIRTY_FORMS = 'CLEAR_DIRTY_FORMS'; |
| 33 | const SET_FOCUS_ID = 'SET_FOCUS_ID'; |
| 34 | export const SHOW_SNACKBAR = 'SHOW_SNACKBAR'; |
| 35 | const 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 | |
| 55 | const 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 | */ |
| 68 | const 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 | |
| 82 | const 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 | */ |
| 94 | export 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 | |
| 103 | export 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 |
| 115 | export const navigationCount = (state) => state.ui.navigationCount; |
| 116 | export const dirtyForms = (state) => state.ui.dirtyForms; |
| 117 | export 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 | */ |
| 124 | export const snackbars = (state) => state.ui.snackbars; |
| 125 | |
| 126 | // Action Creators |
| 127 | export const incrementNavigationCount = () => { |
| 128 | return {type: INCREMENT_NAVIGATION_COUNT}; |
| 129 | }; |
| 130 | |
| 131 | export const reportDirtyForm = (name, isDirty) => { |
| 132 | return {type: REPORT_DIRTY_FORM, name, isDirty}; |
| 133 | }; |
| 134 | |
| 135 | export const clearDirtyForms = () => ({type: CLEAR_DIRTY_FORMS}); |
| 136 | |
| 137 | export 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 | */ |
| 150 | export 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 | */ |
| 165 | export const hideSnackbar = (id) => { |
| 166 | return { |
| 167 | type: HIDE_SNACKBAR, |
| 168 | id, |
| 169 | }; |
| 170 | }; |