diff --git a/package-lock.json b/package-lock.json index 524666e9f..f4b328c75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,6 @@ "react-use": "^17.5.1", "redux": "^5.0.1", "redux-logger": "^3.0.6", - "redux-promise-middleware": "^6.2.0", "redux-thunk": "^3.1.0", "victory": "37.0.2", "victory-create-container": "37.0.2" @@ -20402,14 +20401,6 @@ "lodash.isplainobject": "^4.0.6" } }, - "node_modules/redux-promise-middleware": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/redux-promise-middleware/-/redux-promise-middleware-6.2.0.tgz", - "integrity": "sha512-TEzfMeLX63gju2WqkdFQlQMvUGYzFvJNePIJJsBlbPHs3Txsbc/5Rjhmtha1XdMU6lkeiIlp1Qx7AR3Zo9he9g==", - "peerDependencies": { - "redux": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" - } - }, "node_modules/redux-thunk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", diff --git a/package.json b/package.json index de464fddb..721832f1b 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,6 @@ "react-use": "^17.5.1", "redux": "^5.0.1", "redux-logger": "^3.0.6", - "redux-promise-middleware": "^6.2.0", "redux-thunk": "^3.1.0", "victory": "37.0.2", "victory-create-container": "37.0.2" diff --git a/src/redux/README.md b/src/redux/README.md index 5826f2ee4..e5a861496 100644 --- a/src/redux/README.md +++ b/src/redux/README.md @@ -19,6 +19,8 @@
MultiActionMiddleware
+
PromiseMiddleware
+
StatusMiddleware
AppReducer
@@ -1062,6 +1064,82 @@ Allow passing an array of actions for batch dispatch. + + +## PromiseMiddleware + +* [PromiseMiddleware](#Middleware.module_PromiseMiddleware) + * [~ActionType](#Middleware.module_PromiseMiddleware..ActionType) : Object + * [~createPromise(config)](#Middleware.module_PromiseMiddleware..createPromise) ⇒ function + * [~promiseMiddleware(config)](#Middleware.module_PromiseMiddleware..promiseMiddleware) ⇒ function + + + +### PromiseMiddleware~ActionType : Object +Redux default action types for promiseMiddleware + +**Kind**: inner constant of [PromiseMiddleware](#Middleware.module_PromiseMiddleware) + + +### PromiseMiddleware~createPromise(config) ⇒ function +Function: createPromise +Description: The main createPromise accepts a configuration +object and returns the middleware. + +**Kind**: inner method of [PromiseMiddleware](#Middleware.module_PromiseMiddleware) + + + + + + + + + + + + + + + + + + + + +
ParamType
configobject
config.isCatchRejectionboolean
config.promiseTypeDelimiterstring
config.promiseTypeSuffixPendingstring
config.promiseTypeSuffixFulfilledstring
config.promiseTypeSuffixRejectedstring
+ + + +### PromiseMiddleware~promiseMiddleware(config) ⇒ function +Promise middleware +Base code, https://github.com/pburtchaell/redux-promise-middleware +Modified to allow configuration and "isCatchRejection". + +**Kind**: inner method of [PromiseMiddleware](#Middleware.module_PromiseMiddleware) + + + + + + + + + + + + + + + + + + + + +
ParamTypeDescription
configobject
config.isCatchRejectionboolean

Catch the returned promise. Helps avoid the "[uncaught in promise]" error

+
config.promiseTypeDelimiterstring
config.promiseTypeSuffixPendingstring
config.promiseTypeSuffixFulfilledstring
config.promiseTypeSuffixRejectedstring
+ ## StatusMiddleware diff --git a/src/redux/actions/__tests__/platformActions.test.js b/src/redux/actions/__tests__/platformActions.test.js index 35631bce5..60c25c29c 100644 --- a/src/redux/actions/__tests__/platformActions.test.js +++ b/src/redux/actions/__tests__/platformActions.test.js @@ -5,7 +5,7 @@ import { platformActions } from '../platformActions'; import { appReducer } from '../../reducers'; describe('PlatformActions', () => { - const middleware = [promiseMiddleware]; + const middleware = [promiseMiddleware()]; const generateStore = () => createStore( combineReducers({ diff --git a/src/redux/actions/__tests__/rhsmActions.test.js b/src/redux/actions/__tests__/rhsmActions.test.js index af41b7fe1..a4338cc6a 100644 --- a/src/redux/actions/__tests__/rhsmActions.test.js +++ b/src/redux/actions/__tests__/rhsmActions.test.js @@ -6,7 +6,7 @@ import { rhsmConstants } from '../../../services/rhsm/rhsmConstants'; import { rhsmActions } from '../rhsmActions'; describe('RhsmActions', () => { - const middleware = [multiActionMiddleware, promiseMiddleware]; + const middleware = [multiActionMiddleware, promiseMiddleware()]; const generateStore = () => createStore( combineReducers({ diff --git a/src/redux/actions/__tests__/userActions.test.js b/src/redux/actions/__tests__/userActions.test.js index 1a0241900..f93a2f1f4 100644 --- a/src/redux/actions/__tests__/userActions.test.js +++ b/src/redux/actions/__tests__/userActions.test.js @@ -5,7 +5,7 @@ import { appReducer } from '../../reducers'; import { userActions } from '../userActions'; describe('UserActions', () => { - const middleware = [promiseMiddleware]; + const middleware = [promiseMiddleware()]; const generateStore = () => createStore( combineReducers({ diff --git a/src/redux/middleware/index.js b/src/redux/middleware/index.js index a74dccd1c..5c10897dd 100644 --- a/src/redux/middleware/index.js +++ b/src/redux/middleware/index.js @@ -1,7 +1,7 @@ import { createLogger } from 'redux-logger'; -import promiseMiddleware from 'redux-promise-middleware'; import { thunk as thunkMiddleware } from 'redux-thunk'; import { notificationsMiddleware } from '@redhat-cloud-services/frontend-components-notifications'; +import { promiseMiddleware } from './promiseMiddleware'; import { multiActionMiddleware } from './multiActionMiddleware'; import { statusMiddleware } from './statusMiddleware'; import { actionRecordMiddleware } from './actionRecordMiddleware'; @@ -34,7 +34,7 @@ const reduxMiddleware = [ thunkMiddleware, statusMiddleware(), multiActionMiddleware, - promiseMiddleware, + promiseMiddleware({ isCatchRejection: true }), actionRecordMiddleware({ id: process.env.REACT_APP_UI_LOGGER_ID, app: { version: process.env.REACT_APP_UI_VERSION } diff --git a/src/redux/middleware/promiseMiddleware.js b/src/redux/middleware/promiseMiddleware.js new file mode 100644 index 000000000..7e6603030 --- /dev/null +++ b/src/redux/middleware/promiseMiddleware.js @@ -0,0 +1,230 @@ +import { helpers } from '../../common/helpers'; + +/** + * @memberof Middleware + * @module PromiseMiddleware + */ + +/** + * Redux default action types for promiseMiddleware + * + * @type {{Fulfilled: string, Rejected: string, Pending: string}} + */ +const ActionType = { + Pending: 'PENDING', + Fulfilled: 'FULFILLED', + Rejected: 'REJECTED' +}; + +/** + * Function: createPromise + * Description: The main createPromise accepts a configuration + * object and returns the middleware. + * + * @param {object} config + * @param {boolean} config.isCatchRejection + * @param {string} config.promiseTypeDelimiter + * @param {string} config.promiseTypeSuffixPending + * @param {string} config.promiseTypeSuffixFulfilled + * @param {string} config.promiseTypeSuffixRejected + * @returns {Function} + */ +const createPromise = ({ + promiseTypeDelimiter: PROMISE_TYPE_DELIMITER = '_', + promiseTypeSuffixPending = ActionType.Pending, + promiseTypeSuffixFulfilled = ActionType.Fulfilled, + promiseTypeSuffixRejected = ActionType.Rejected, + isCatchRejection = false +} = {}) => { + const PROMISE_TYPE_SUFFIXES = [promiseTypeSuffixPending, promiseTypeSuffixFulfilled, promiseTypeSuffixRejected]; + return ref => { + const { dispatch } = ref; + + return next => action => { + /** + * Instantiate variables to hold: + * (1) the promise + * (2) the data for optimistic updates + */ + let promise; + let data; + + /** + * There are multiple ways to dispatch a promise. The first step is to + * determine if the promise is defined: + * (a) explicitly (action.payload.promise is the promise) + * (b) implicitly (action.payload is the promise) + * (c) as an async function (returns a promise when called) + * + * If the promise is not defined in one of these three ways, we don't do + * anything and move on to the next middleware in the middleware chain. + */ + + // Step 1a: Is there a payload? + if (action.payload) { + const PAYLOAD = action.payload; + + // Step 1.1: Is the promise implicitly defined? + if (helpers.isPromise(PAYLOAD)) { + promise = PAYLOAD; + } + + // Step 1.2: Is the promise explicitly defined? + else if (helpers.isPromise(PAYLOAD.promise)) { + promise = PAYLOAD.promise; + data = PAYLOAD.data; + } + + // Step 1.3: Is the promise returned by an async function? + else if (typeof PAYLOAD === 'function' || typeof PAYLOAD.promise === 'function') { + promise = PAYLOAD.promise ? PAYLOAD.promise() : PAYLOAD(); + data = PAYLOAD.promise ? PAYLOAD.data : undefined; + + // Step 1.3.1: Is the return of action.payload a promise? + if (!helpers.isPromise(promise)) { + // If not, move on to the next middleware. + return next({ + ...action, + payload: promise + }); + } + } + + // Step 1.4: If there's no promise, move on to the next middleware. + else { + return next(action); + } + + // Step 1b: If there's no payload, move on to the next middleware. + } else { + return next(action); + } + + /** + * Instantiate and define constants for: + * (1) the action type + * (2) the action meta + */ + const TYPE = action.type; + const META = action.meta; + + /** + * Instantiate and define constants for the action type suffixes. + * These are appended to the end of the action type. + */ + const [PENDING, FULFILLED, REJECTED] = PROMISE_TYPE_SUFFIXES; + + /** + * Function: getAction + * Description: This function constructs and returns a rejected + * or fulfilled action object. The action object is based off the Flux + * Standard Action (FSA). + * + * Given an original action with the type FOO: + * + * The rejected object model will be: + * { + * error: true, + * type: 'FOO_REJECTED', + * payload: ..., + * meta: ... (optional) + * } + * + * The fulfilled object model will be: + * { + * type: 'FOO_FULFILLED', + * payload: ..., + * meta: ... (optional) + * } + * + * @param {unknown} newPayload + * @param {boolean} isRejected + * @returns {{payload: any, meta: any, type: string, error: any}} + */ + const getAction = (newPayload, isRejected) => ({ + // Concatenate the type string property. + type: [TYPE, isRejected ? REJECTED : FULFILLED].join(PROMISE_TYPE_DELIMITER), + + // Include the payload property. + ...(newPayload === null || typeof newPayload === 'undefined' + ? {} + : { + payload: newPayload + }), + + // If the original action includes a meta property, include it. + ...(META !== undefined ? { meta: META } : {}), + + // If the action is rejected, include an error property. + ...(isRejected + ? { + error: true + } + : {}) + }); + + const handleReject = reason => { + const rejectedAction = getAction(reason, true); + dispatch(rejectedAction); + + if (isCatchRejection === false) { + throw reason; + } + }; + + const handleFulfill = (value = null) => { + const resolvedAction = getAction(value, false); + dispatch(resolvedAction); + + return { value, action: resolvedAction }; + }; + + /** + * First, dispatch the pending action: + * This object describes the pending state of a promise and will include + * any data (for optimistic updates) and/or meta from the original action. + */ + next({ + // Concatenate the type string. + type: [TYPE, PENDING].join(PROMISE_TYPE_DELIMITER), + + // Include payload (for optimistic updates) if it is defined. + ...(data !== undefined ? { payload: data } : {}), + + // Include meta data if it is defined. + ...(META !== undefined ? { meta: META } : {}) + }); + + /** + * Second, dispatch a rejected or fulfilled action and move on to the + * next middleware. + */ + return promise.then(handleFulfill, handleReject); + }; + }; +}; + +/** + * Promise middleware + * Base code, https://github.com/pburtchaell/redux-promise-middleware + * Modified to allow configuration and "isCatchRejection". + * + * @param {object} config + * @param {boolean} config.isCatchRejection Catch the returned promise. Helps avoid the "[uncaught in promise]" error + * @param {string} config.promiseTypeDelimiter + * @param {string} config.promiseTypeSuffixPending + * @param {string} config.promiseTypeSuffixFulfilled + * @param {string} config.promiseTypeSuffixRejected + * @returns {Function} + */ +const promiseMiddleware = + config => + ({ dispatch } = {}) => { + if (typeof dispatch === 'function') { + return createPromise(config)({ dispatch }); + } + + return null; + }; + +export { promiseMiddleware as default, promiseMiddleware, createPromise, ActionType }; diff --git a/tests/__snapshots__/dist.test.js.snap b/tests/__snapshots__/dist.test.js.snap index ae644ca9f..25543700c 100644 --- a/tests/__snapshots__/dist.test.js.snap +++ b/tests/__snapshots__/dist.test.js.snap @@ -772,8 +772,6 @@ exports[`Build distribution should match a specific file output 1`] = ` "./dist/js/2438*txt", "./dist/js/2471*js", "./dist/js/2531*js", - "./dist/js/254*js", - "./dist/js/254*txt", "./dist/js/2745*js", "./dist/js/2871*js", "./dist/js/2901*js", @@ -822,6 +820,8 @@ exports[`Build distribution should match a specific file output 1`] = ` "./dist/js/5523*js", "./dist/js/5600*js", "./dist/js/5606*js", + "./dist/js/5683*js", + "./dist/js/5683*txt", "./dist/js/5687*js", "./dist/js/5717*js", "./dist/js/5927*js", @@ -906,7 +906,6 @@ exports[`Build distribution should match a specific file output 1`] = ` "./dist/sourcemaps/2438*map", "./dist/sourcemaps/2471*map", "./dist/sourcemaps/2531*map", - "./dist/sourcemaps/254*map", "./dist/sourcemaps/2745*map", "./dist/sourcemaps/2871*map", "./dist/sourcemaps/2901*map", @@ -948,6 +947,7 @@ exports[`Build distribution should match a specific file output 1`] = ` "./dist/sourcemaps/5523*map", "./dist/sourcemaps/5600*map", "./dist/sourcemaps/5606*map", + "./dist/sourcemaps/5683*map", "./dist/sourcemaps/5687*map", "./dist/sourcemaps/5717*map", "./dist/sourcemaps/5927*map",