diff --git a/BREAKING_CHANGES_LOG.md b/BREAKING_CHANGES_LOG.md index ba680b64e7e..2b43aa2b9f5 100644 --- a/BREAKING_CHANGES_LOG.md +++ b/BREAKING_CHANGES_LOG.md @@ -3,6 +3,19 @@ Before 1.0, the stack do NOT follow semver version in releases. This document aims to ease the WIP migration from a version to another by providing intels about what to do to migrate. ## v0.156.0 +* cmf: selectors +* PR: https://github.com/Talend/ui/pull/1055 +* Change: move to collections + +| name | new location | +|---|---| +| getCollectionFromPath | selectors.collections.find +| findCollectionPathListItem | selectors.collections.findListItem + +* cmf: putActionCreator +* PR: https://github.com/Talend/ui/pull/1055 +* Change: move from api.saga.putActionCreator to api.sagas.putActionCreator + * Container: DeleteResource * PR: https://github.com/Talend/ui/pull/1053 * Changes: deleteResource Saga params has changed diff --git a/output/cmf.eslint.txt b/output/cmf.eslint.txt index 9b41053cae5..b4c6a826e86 100644 --- a/output/cmf.eslint.txt +++ b/output/cmf.eslint.txt @@ -5,7 +5,7 @@ The react/require-extension rule is deprecated. Please use the import/extensions rule from eslint-plugin-import instead. /home/travis/build/Talend/ui/packages/cmf/src/cmfConnect.js - 151:4 warning Unexpected console statement no-console + 152:4 warning Unexpected console statement no-console /home/travis/build/Talend/ui/packages/cmf/src/componentState.js 88:3 warning Unexpected console statement no-console @@ -21,5 +21,8 @@ The react/require-extension rule is deprecated. Please use the import/extensions 163:54 error There should be no spaces inside this paren space-in-parens 177:51 error There should be no spaces inside this paren space-in-parens -✖ 7 problems (2 errors, 5 warnings) +/home/travis/build/Talend/ui/packages/cmf/src/sagas/collection.js + 9:1 error Prefer default export import/prefer-default-export + +✖ 8 problems (3 errors, 5 warnings) diff --git a/packages/cmf/__tests__/actions/saga.test.js b/packages/cmf/__tests__/actions/saga.test.js new file mode 100644 index 00000000000..db6c87d73de --- /dev/null +++ b/packages/cmf/__tests__/actions/saga.test.js @@ -0,0 +1,25 @@ +import { + start, + stop, +} from '../../src/actions/saga'; +import CONST from '../../src/constant'; + +describe('actions.saga', () => { + it('start should return action object with DID_MOUNT_SAGA_START', () => { + const event = { type: 'DID_MOUNT' }; + const data = { saga: 'mySaga' }; + expect(start(event, data)).toEqual({ + type: CONST.DID_MOUNT_SAGA_START, + saga: data.saga, + event, + }); + }); + it('start should return action object with WILL_UNMUNT_SAGA_STOP', () => { + const event = { type: 'WILL_UNMOUNT' }; + const data = { saga: 'mySaga' }; + expect(stop(event, data)).toEqual({ + type: `${CONST.WILL_UNMOUNT_SAGA_STOP}_${data.saga}`, + event, + }); + }); +}); diff --git a/packages/cmf/__tests__/api.test.js b/packages/cmf/__tests__/api.test.js index 54b823c4c7f..b875a529dd6 100644 --- a/packages/cmf/__tests__/api.test.js +++ b/packages/cmf/__tests__/api.test.js @@ -3,5 +3,25 @@ import api from '../src/api'; describe('CMF api', () => { it('provide action, route access', () => { expect(typeof api.action).toBe('object'); + expect(api.action).toBeDefined(); + expect(api.actions).toBeDefined(); + expect(api.actionCreator).toBeDefined(); + expect(api.component).toBeDefined(); + expect(api.expression).toBeDefined(); + expect(api.route).toBeDefined(); + expect(api.registry).toBeDefined(); + expect(api.registerInternals).toBeDefined(); + expect(api.sagas).toBeDefined(); + expect(api.saga).toBeDefined(); + }); + it('registerInternals should add internal actionCreators to the registry', () => { + const context = { + registry: {}, + }; + expect(() => api.actionCreator.get(context, 'cmf.saga.start')).toThrow(); + expect(() => api.actionCreator.get(context, 'cmf.saga.stop')).toThrow(); + api.registerInternals(context); + expect(api.actionCreator.get(context, 'cmf.saga.start')).toBeDefined(); + expect(api.actionCreator.get(context, 'cmf.saga.stop')).toBeDefined(); }); }); diff --git a/packages/cmf/__tests__/cmfConnect.test.js b/packages/cmf/__tests__/cmfConnect.test.js index a19fdc8f4b4..02151c9acd2 100644 --- a/packages/cmf/__tests__/cmfConnect.test.js +++ b/packages/cmf/__tests__/cmfConnect.test.js @@ -269,6 +269,45 @@ describe('cmfConnect', () => { expect(props.initState.mock.calls[0][0]).toBe(props.initialState); }); + it('should componentDidMount support saga', () => { + const TestComponent = jest.fn(); + TestComponent.displayName = 'TestComponent'; + const CMFConnected = cmfConnect({})(TestComponent); + const props = { + saga: 'hello', + dispatchActionCreator: jest.fn(), + }; + const context = mock.context(); + const instance = new CMFConnected.CMFContainer(props, context); + instance.componentDidMount(); + expect(props.dispatchActionCreator).toHaveBeenCalledWith( + 'cmf.saga.start', + { type: 'DID_MOUNT' }, + instance.props, + instance.context + ); + }); + + it('should componentWillUnmount support saga', () => { + const TestComponent = jest.fn(); + TestComponent.displayName = 'TestComponent'; + const CMFConnected = cmfConnect({})(TestComponent); + const props = { + saga: 'hello', + dispatchActionCreator: jest.fn(), + deleteState: jest.fn(), + }; + const context = mock.context(); + const instance = new CMFConnected.CMFContainer(props, context); + instance.componentWillUnmount(); + expect(props.dispatchActionCreator).toHaveBeenCalledWith( + 'cmf.saga.stop', + { type: 'WILL_UNMOUNT' }, + instance.props, + instance.context + ); + }); + it('should componentWillUnMount dispatchActionCreator', () => { const TestComponent = jest.fn(); TestComponent.displayName = 'TestComponent'; diff --git a/packages/cmf/__tests__/sagas/component.test.js b/packages/cmf/__tests__/sagas/component.test.js new file mode 100644 index 00000000000..9c17077251a --- /dev/null +++ b/packages/cmf/__tests__/sagas/component.test.js @@ -0,0 +1,33 @@ +import { fork, take, takeEvery, cancel } from 'redux-saga/effects'; +import { createMockTask } from 'redux-saga/utils'; +import registry from '../../src/registry'; +import { onSagaStart, handle } from '../../src/sagas/component'; +import CONST from '../../src/constant'; + +describe('sagas.component', () => { + it('should onSagaStart fork action.saga and wait for unmount to cancel', () => { + // given + const testAction = { type: 'TEST', saga: 'my-saga' }; + function* saga() {} + const reg = registry.getRegistry(); + reg['SAGA:my-saga'] = saga; + const task = createMockTask(); + // when + const gen = onSagaStart(testAction); + + // then + expect(gen.next().value).toEqual(fork(saga)); + expect(gen.next(task).value).toEqual(take(`${CONST.WILL_UNMOUNT_SAGA_STOP}_my-saga`)); + expect(gen.next().value).toEqual(cancel(task)); + }); + it('should handle takeEvery didmount', () => { + // given + const gen = handle(); + const didMountAction = { type: CONST.DID_MOUNT_SAGA_START }; + + // then + expect(gen.next(didMountAction).value).toEqual( + takeEvery(CONST.DID_MOUNT_SAGA_START, onSagaStart) + ); + }); +}); diff --git a/packages/cmf/src/saga.test.js b/packages/cmf/__tests__/sagas/putActionCreator.test.js similarity index 87% rename from packages/cmf/src/saga.test.js rename to packages/cmf/__tests__/sagas/putActionCreator.test.js index df3de1063c5..92e3bbf53cd 100644 --- a/packages/cmf/src/saga.test.js +++ b/packages/cmf/__tests__/sagas/putActionCreator.test.js @@ -1,6 +1,6 @@ import { put, select } from 'redux-saga/effects'; -import registry from './registry'; -import saga from './saga'; +import registry from '../../src/registry'; +import putActionCreator from '../../src/sagas/putActionCreator'; describe('saga', () => { it('should putActionCreator call put of a registred actionCreator without context', () => { @@ -13,7 +13,7 @@ describe('saga', () => { const event = { type: 'click', source: 'MyComponent' }; // when - const gen = saga.putActionCreator('myActionCreator', event, data); + const gen = putActionCreator('myActionCreator', event, data); // then expect(gen.next().value).toEqual(select()); @@ -41,7 +41,7 @@ describe('saga', () => { const event = { type: 'click', source: 'MyComponent' }; // when - const gen = saga.putActionCreator('myActionCreator', event, data, context); + const gen = putActionCreator('myActionCreator', event, data, context); // then expect(gen.next().value).toEqual(select()); diff --git a/packages/cmf/__tests__/selectors/index.test.js b/packages/cmf/__tests__/selectors/index.test.js index 4b30834875f..a03e546df32 100644 --- a/packages/cmf/__tests__/selectors/index.test.js +++ b/packages/cmf/__tests__/selectors/index.test.js @@ -1,8 +1,8 @@ import { Map, List } from 'immutable'; import cases from 'jest-in-case'; -import { getCollectionFromPath, findCollectionPathListItem } from '../../src/selectors'; +import selectors from '../../src/selectors'; -describe('getCollectionFromPath', () => { +describe('selectors.collections.get', () => { const collection = new Map({ id: 'id' }); const collectionSubset = new Map({ subset: 'subset' }); const collectionWithSubset = new Map({ collectionSubset }); @@ -15,18 +15,18 @@ describe('getCollectionFromPath', () => { }, }; it('try to find the collection if collectionPath is a string', () => { - expect(getCollectionFromPath(state, 'collection')).toEqual(collection); + expect(selectors.collections.get(state, 'collection')).toEqual(collection); }); it('try to find the collection subset if collectionPath is an array', () => { - expect(getCollectionFromPath(state, ['collectionWithSubset', 'collectionSubset'])).toEqual( + expect(selectors.collections.get(state, ['collectionWithSubset', 'collectionSubset'])).toEqual( collectionSubset, ); }); it('throw an exception if collection path is neither a string or an array', () => { expect(() => { - getCollectionFromPath(state, {}); + selectors.collections.get(state, {}); }).toThrowError(`Type mismatch: collectionPath should be a string or an array of string got [object Object]`); }); @@ -47,9 +47,9 @@ const state = { }; cases( - 'findCollectionPathListItem(state, pathDescriptor, resourceId)', + 'find(state, pathDescriptor, resourceId)', opts => { - expect(findCollectionPathListItem(opts.state, opts.pathDescriptor, opts.resourceId)).toBe( + expect(selectors.collections.findListItem(opts.state, opts.pathDescriptor, opts.resourceId)).toBe( opts.result, ); }, @@ -79,10 +79,10 @@ cases( ); cases( - 'findCollectionPathListItem(state, pathDescriptor, resourceId)', + 'selectors.collections.findListItem(state, pathDescriptor, resourceId)', opts => { expect(() => { - findCollectionPathListItem(opts.state, opts.pathDescriptor, opts.resourceId); + selectors.collections.findListItem(opts.state, opts.pathDescriptor, opts.resourceId); }).toThrow(opts.result); }, [ @@ -91,7 +91,7 @@ cases( state, pathDescriptor: 'isNotList', resourceId: id, - result: `Type mismatch: isNotList does not resolve as an instance of Immutable.List, + result: `Type mismatch: isNotList does not resolve as an instance of Immutable.List, got Map { "id": Map { "id": "id" } }`, }, { @@ -99,7 +99,7 @@ got Map { "id": Map { "id": "id" } }`, state, pathDescriptor: 'notFound', resourceId: id, - result: `Type mismatch: notFound does not resolve as an instance of Immutable.List, + result: `Type mismatch: notFound does not resolve as an instance of Immutable.List, got undefined`, }, ], diff --git a/packages/cmf/src/actions/index.js b/packages/cmf/src/actions/index.js index 188ac47089b..471c0b6eb34 100644 --- a/packages/cmf/src/actions/index.js +++ b/packages/cmf/src/actions/index.js @@ -7,6 +7,7 @@ import * as collectionsActions from './collectionsActions'; import * as componentsActions from './componentsActions'; import * as settingsActions from './settingsActions'; +import * as saga from './saga'; import http from './http'; /** @@ -27,4 +28,5 @@ export default { collections: collectionsActions, components: componentsActions, settings: settingsActions, + saga, }; diff --git a/packages/cmf/src/actions/saga.js b/packages/cmf/src/actions/saga.js new file mode 100644 index 00000000000..047d1928cdb --- /dev/null +++ b/packages/cmf/src/actions/saga.js @@ -0,0 +1,16 @@ +import CONST from '../constant'; + +export function start(event = {}, data) { + return { + type: CONST.DID_MOUNT_SAGA_START, + saga: data.saga, + event, + }; +} + +export function stop(event, data) { + return { + type: `${CONST.WILL_UNMOUNT_SAGA_STOP}_${data.saga}`, + event, + }; +} diff --git a/packages/cmf/src/api.js b/packages/cmf/src/api.js index a238dd6f959..32d4c3c0bb7 100644 --- a/packages/cmf/src/api.js +++ b/packages/cmf/src/api.js @@ -27,9 +27,15 @@ import action from './action'; import actions from './actions'; import actionCreator from './actionCreator'; import expression from './expression'; -import saga from './saga'; +import sagas from './sagas'; +import selectors from './selectors'; import component from './component'; +function registerInternals(context) { + actionCreator.register('cmf.saga.start', actions.saga.start, context); + actionCreator.register('cmf.saga.stop', actions.saga.stop, context); +} + export default { action, actions, @@ -38,5 +44,8 @@ export default { expression, route, registry, - saga, + registerInternals, + saga: sagas, + sagas, + selectors, }; diff --git a/packages/cmf/src/api.md b/packages/cmf/src/api.md index faa0a90a457..14cefbdf729 100644 --- a/packages/cmf/src/api.md +++ b/packages/cmf/src/api.md @@ -103,11 +103,25 @@ api.expressions.register('myExpression', myExpression); Expressions can be used for props resolution. In this case, the payload is the current props. -api.saga +api.sagas -- +You can register your saga in the cmf registry to be able to use the saga props +supported by `cmfConnect`. + +```javascript +function* mySaga(action) { + //... +} +api.sagas.register('mySaga', mySaga); +``` + +This is related to the `component` saga that you must initialize. + +Most of them are documented [here](sagas/index.md) + ```javascript -api.saga.putActionCreator('myAction', event, data, optionalContext); +api.sagas.putActionCreator('myaction', event, data, optionalContext); ``` This will call the registered `myAction` action creator. diff --git a/packages/cmf/src/cmfConnect.js b/packages/cmf/src/cmfConnect.js index 5e0bab51930..f6a2be7299a 100644 --- a/packages/cmf/src/cmfConnect.js +++ b/packages/cmf/src/cmfConnect.js @@ -46,6 +46,7 @@ const CMF_PROPS = [ 'didMountActionCreator', // componentDidMount action creator id in registry 'keepComponentState', // redux state management on unmount 'view', // view component id in registry + 'saga', 'willUnMountActionCreator', // componentWillUnmount action creator id in registry ]; @@ -247,6 +248,9 @@ export default function cmfConnect({ if (this.props.didMountActionCreator) { this.dispatchActionCreator(this.props.didMountActionCreator, null, this.props); } + if (this.props.saga) { + this.dispatchActionCreator('cmf.saga.start', { type: 'DID_MOUNT' }, this.props); + } } componentWillUnmount() { @@ -260,6 +264,9 @@ export default function cmfConnect({ ) { this.props.deleteState(); } + if (this.props.saga) { + this.dispatchActionCreator('cmf.saga.stop', { type: 'WILL_UNMOUNT' }, this.props); + } } dispatchActionCreator(actionCreatorId, event, data, context) { diff --git a/packages/cmf/src/constant.js b/packages/cmf/src/constant.js index ae0cfb8a21a..9b569128cf5 100644 --- a/packages/cmf/src/constant.js +++ b/packages/cmf/src/constant.js @@ -3,4 +3,7 @@ export default { REGISTRY_COMPONENT_PREFIX: '_.route.component', REGISTRY_HOOK_PREFIX: '_.route.hook', REGISTRY_ACTION_CREATOR_PREFIX: 'actionCreator', + SAGA_PREFIX: 'saga', + DID_MOUNT_SAGA_START: 'DID_MOUNT_SAGA_START', + WILL_UNMOUNT_SAGA_STOP: 'WILL_UNMOUNT_SAGA_STOP', }; diff --git a/packages/cmf/src/sagas/collection.js b/packages/cmf/src/sagas/collection.js new file mode 100644 index 00000000000..990de78aafe --- /dev/null +++ b/packages/cmf/src/sagas/collection.js @@ -0,0 +1,19 @@ +import { call, select } from 'redux-saga/effects'; +import { delay } from 'redux-saga'; +import selectors from '../selectors'; + +/** + * this saga ends when the collection is available + * @param {string} id of the collection to wait for + * @param {number} interval in ms + */ +export function* waitFor(id, interval = 10) { + // eslint-disable-next-line no-constant-condition + while (true) { + const collection = yield select(selectors.collections.get); + if (collection !== undefined) { + break; + } + yield call(delay, interval); + } +} diff --git a/packages/cmf/src/sagas/component.js b/packages/cmf/src/sagas/component.js new file mode 100644 index 00000000000..d5e03f95b6a --- /dev/null +++ b/packages/cmf/src/sagas/component.js @@ -0,0 +1,42 @@ +import invariant from 'invariant'; +import { fork, cancel, take, takeEvery } from 'redux-saga/effects'; +import CONST from '../constant'; +import registry from '../registry'; + +/** + * This function register a saga in the cmf registry + * @param {string} id the saga id you want + * @param {generator} saga the saga generator + * @param {object} context optional context to get the registry + */ +export function register(id, saga, context) { + registry.addToRegistry(`SAGA:${id}`, saga, context); +} + +/** + * This function allow to get a saga from the registry + * @param {string} id the saga id you want + * @param {object} context optional context to get the registry + */ +export function get(id, context) { + return registry.getFromRegistry(`SAGA:${id}`, context); +} + +export const registerMany = registry.getRegisterMany(register); + + +export function* onSagaStart(action) { + const saga = get(action.saga); + if (!saga) { + invariant(process.env.NODE_ENV !== 'production', `The saga ${action.saga} is not registred`); + } else { + const task = yield fork(saga); + yield take(`${CONST.WILL_UNMOUNT_SAGA_STOP}_${action.saga}`); + yield cancel(task); + } +} + +export function* handle() { + yield takeEvery(CONST.DID_MOUNT_SAGA_START, onSagaStart); + yield take('DO_NOT_QUIT'); +} diff --git a/packages/cmf/src/sagas/index.js b/packages/cmf/src/sagas/index.js index 5a482836355..e3148a3eeb2 100644 --- a/packages/cmf/src/sagas/index.js +++ b/packages/cmf/src/sagas/index.js @@ -1,7 +1,18 @@ import changeDocumentTitle from './documentTitle'; import http from './http'; +import putActionCreator from './putActionCreator'; +import * as collection from './collection'; +import * as component from './component'; export default { + // shortcut + get: component.get, + register: component.register, + registerMany: component.registerMany, + changeDocumentTitle, + collection, + component, http, + putActionCreator, }; diff --git a/packages/cmf/src/saga.js b/packages/cmf/src/sagas/putActionCreator.js similarity index 83% rename from packages/cmf/src/saga.js rename to packages/cmf/src/sagas/putActionCreator.js index 5c539a2124a..f77b66bf552 100644 --- a/packages/cmf/src/saga.js +++ b/packages/cmf/src/sagas/putActionCreator.js @@ -9,8 +9,8 @@ api.saga.putActionCreator('myaction', {}, {}); */ import { put, select } from 'redux-saga/effects'; -import actionCreatorAPI from './actionCreator'; -import registry from './registry'; +import actionCreatorAPI from '../actionCreator'; +import registry from '../registry'; function* putActionCreator(actionCreatorId, event, data, optContext) { const state = yield select(); @@ -24,6 +24,4 @@ function* putActionCreator(actionCreatorId, event, data, optContext) { yield put(actionCreator(event, data, context)); } -export default { - putActionCreator, -}; +export default putActionCreator; diff --git a/packages/cmf/src/selectors/collections.js b/packages/cmf/src/selectors/collections.js new file mode 100644 index 00000000000..8603dff8d7d --- /dev/null +++ b/packages/cmf/src/selectors/collections.js @@ -0,0 +1,44 @@ +import { List } from 'immutable'; + +export function getAll(state) { + return state.cmf.collections; +} + +/** + * return a collection or subset of a collection from a cmf store + * @param {Object} state + * @param {String or Array} collectionPath + * @example + * get('foo.bar', true) === state.cmf.collections.getIn(['foo', 'bar'], true) + */ +export function get(state, collectionPath, defaultValue) { + let path; + if (typeof collectionPath === 'string') { + path = collectionPath.split('.'); + } else if (Array.isArray(collectionPath)) { + path = collectionPath; + } + if (path) { + return state.cmf.collections.getIn(path, defaultValue); + } + throw Error(`Type mismatch: collectionPath should be a string or an array of string +got ${collectionPath}`); +} + +/** + * for a collectionId and an id find and return the an item from this + * collection if it is a list + * @param {Object} state + * @param {String} collectionId + * @param {String} itemId + */ +export function findListItem(state, collectionPath, itemId) { + const collectionOrCollectionSubset = get(state, collectionPath); + if (List.isList(collectionOrCollectionSubset)) { + return collectionOrCollectionSubset.find(element => element && element.get('id') === itemId); + } + throw Error( + `Type mismatch: ${collectionPath} does not resolve as an instance of Immutable.List, +got ${collectionOrCollectionSubset}`, + ); +} diff --git a/packages/cmf/src/selectors/index.js b/packages/cmf/src/selectors/index.js index 94bbf41ee55..467e7091133 100644 --- a/packages/cmf/src/selectors/index.js +++ b/packages/cmf/src/selectors/index.js @@ -1,34 +1,5 @@ -import { List } from 'immutable'; +import * as collections from './collections'; -/** - * return a collection or subset of a collection from a cmf store - * @param {Object} state - * @param {String or Array} collectionPath - */ -export function getCollectionFromPath(state, collectionPath) { - if (typeof collectionPath === 'string') { - return state.cmf.collections.get(collectionPath); - } else if (Array.isArray(collectionPath)) { - return state.cmf.collections.getIn(collectionPath); - } - throw Error(`Type mismatch: collectionPath should be a string or an array of string -got ${collectionPath}`); -} - -/** - * for a collectionId and an id find and return the an item from this - * collection if it is a list - * @param {Object} state - * @param {String} collectionId - * @param {String} itemId - */ -export function findCollectionPathListItem(state, collectionPath, itemId) { - const collectionOrCollectionSubset = getCollectionFromPath(state, collectionPath); - if (List.isList(collectionOrCollectionSubset)) { - return collectionOrCollectionSubset.find(element => element && element.get('id') === itemId); - } - throw Error( - `Type mismatch: ${collectionPath} does not resolve as an instance of Immutable.List, -got ${collectionOrCollectionSubset}`, - ); -} +export default { + collections, +}; diff --git a/packages/containers/src/DeleteResource/deleteResource.sagas.js b/packages/containers/src/DeleteResource/deleteResource.sagas.js index a377714d507..db84a9a85dd 100644 --- a/packages/containers/src/DeleteResource/deleteResource.sagas.js +++ b/packages/containers/src/DeleteResource/deleteResource.sagas.js @@ -1,7 +1,6 @@ import invariant from 'invariant'; import { take, put, race, call, select } from 'redux-saga/effects'; -import { actions } from '@talend/react-cmf'; -import { findCollectionPathListItem } from '@talend/react-cmf/lib/selectors'; +import { api, actions } from '@talend/react-cmf'; import deleteResourceConst from './deleteResource.constants'; /** @@ -56,7 +55,7 @@ got ${resourcePath}`, export function* deleteResourceValidate(uri, resourceType, itemId, resourcePath) { yield take(deleteResourceConst.DIALOG_BOX_DELETE_RESOURCE_OK); const resourceLocator = getResourceLocator(resourceType, resourcePath); - const resource = yield select(findCollectionPathListItem, resourceLocator, itemId); + const resource = yield select(api.selectors.collections.findListItem, resourceLocator, itemId); if (resource) { yield put( buildHttpDelete( @@ -104,7 +103,8 @@ export default function deleteResource({ }), }); } catch (error) { - invariant(`DeleteResource race failed :${error}`); + console.error(error); + invariant(process.env.NODE_ENV !== 'production', `DeleteResource race failed :${error}`); } finally { yield put({ type: deleteResourceConst.DIALOG_BOX_DELETE_RESOURCE_CLOSE,