diff --git a/.changeset/remove-immutable-cmf-cqrs.md b/.changeset/remove-immutable-cmf-cqrs.md new file mode 100644 index 00000000000..3d2440db582 --- /dev/null +++ b/.changeset/remove-immutable-cmf-cqrs.md @@ -0,0 +1,9 @@ +--- +'@talend/react-cmf-cqrs': major +--- + +feat: remove immutable dependency + +## Breaking changes + +`state.ack` is now a plain object. Consumers reading ack state directly must migrate `.get(key)` → `[key]`. diff --git a/.changeset/remove-immutable-cmf.md b/.changeset/remove-immutable-cmf.md new file mode 100644 index 00000000000..4bbad8061f3 --- /dev/null +++ b/.changeset/remove-immutable-cmf.md @@ -0,0 +1,17 @@ +--- +'@talend/react-cmf': major +--- + +feat: remove immutable dependency + +## Breaking changes + +The CMF Redux store no longer uses ImmutableJS. `state.cmf.collections` and `state.cmf.components` are now plain objects. + +Migrate: +- `.get(key)` → `[key]` +- `.getIn([a, b])` → `lodash.get(state, [a, b])` +- `.toJS()` → identity (already plain JS) +- `.size` → `Object.keys(x).length` + +The `cmf.selectors.collections.*` and `cmf.selectors.components.*` APIs still work and are the recommended way to access CMF state. diff --git a/.changeset/remove-immutable-components.md b/.changeset/remove-immutable-components.md new file mode 100644 index 00000000000..4945ac0052e --- /dev/null +++ b/.changeset/remove-immutable-components.md @@ -0,0 +1,11 @@ +--- +'@talend/react-components': major +--- + +feat: remove immutable dependency + +## Breaking changes + +The `Iterable.isIterable` backward-compat guard was removed from `ActionDropdown`. Consumers passing an Immutable List as `items` will no longer work — migrate to plain arrays. + +`immutable` and `react-immutable-proptypes` removed from published `dependencies`. diff --git a/.changeset/remove-immutable-containers.md b/.changeset/remove-immutable-containers.md new file mode 100644 index 00000000000..502157df11c --- /dev/null +++ b/.changeset/remove-immutable-containers.md @@ -0,0 +1,15 @@ +--- +'@talend/react-containers': major +--- + +feat: remove immutable dependency + +## Breaking changes + +`defaultState` is now a plain object. Selectors updated accordingly. + +Migrate: +- `.get(key)` → `[key]` +- `.getIn([a, b])` → `lodash.get(state, [a, b])` +- `.toJS()` → identity (already plain JS) +- `.size` → `Object.keys(x).length` diff --git a/.changeset/remove-immutable-sagas.md b/.changeset/remove-immutable-sagas.md new file mode 100644 index 00000000000..e47dfe12a5f --- /dev/null +++ b/.changeset/remove-immutable-sagas.md @@ -0,0 +1,9 @@ +--- +'@talend/react-sagas': major +--- + +feat: remove immutable dependency + +## Breaking changes + +Exported functions `findPenders(state)` and `findPenderById(state, id)` now return plain JS values instead of Immutable structures. Any consumer calling `.get()`, `.set()`, `.toJS()`, or `.size` on the return values must migrate to plain object access. diff --git a/.changeset/remove-immutable-stepper.md b/.changeset/remove-immutable-stepper.md new file mode 100644 index 00000000000..e53c70f8abb --- /dev/null +++ b/.changeset/remove-immutable-stepper.md @@ -0,0 +1,5 @@ +--- +'@talend/react-stepper': patch +--- + +feat: remove immutable dependency diff --git a/.gitignore b/.gitignore index ed9f9e57949..4fc76121a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,10 @@ eslint-report.json stylelint-report.json i18n-extract .test-cache + +# TMP +_bmad +_bmad-* +.github/agents/bmad-* +.github/prompts/bmad_* +.github/prompts/bmad-* diff --git a/docs/migration-guides/migration-guide-remove-immutable.md b/docs/migration-guides/migration-guide-remove-immutable.md new file mode 100644 index 00000000000..291651d8ffc --- /dev/null +++ b/docs/migration-guides/migration-guide-remove-immutable.md @@ -0,0 +1,52 @@ +# Migration Guide: Removal of ImmutableJS + +This is the index for per-package migration guides covering the breaking changes introduced by the removal of [ImmutableJS](https://immutable-js.com/) from the Talend UI monorepo. + +## Affected packages + +| Package | Version bump | Guide | +| -------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------- | +| `@talend/react-cmf` | **MAJOR** | [migration-guide-remove-immutable-cmf.md](./remove-immutable/migration-guide-remove-immutable-cmf.md) | +| `@talend/react-containers` | **MAJOR** | [migration-guide-remove-immutable-containers.md](./remove-immutable/migration-guide-remove-immutable-containers.md) | +| `@talend/react-cmf-cqrs` | **MAJOR** | [migration-guide-remove-immutable-cmf-cqrs.md](./remove-immutable/migration-guide-remove-immutable-cmf-cqrs.md) | +| `@talend/react-sagas` | **MAJOR** | [migration-guide-remove-immutable-sagas.md](./remove-immutable/migration-guide-remove-immutable-sagas.md) | +| `@talend/react-components` | **MAJOR** | [migration-guide-remove-immutable-components.md](./remove-immutable/migration-guide-remove-immutable-components.md) | +| `@talend/react-stepper` | patch | [migration-guide-remove-immutable-stepper.md](./remove-immutable/migration-guide-remove-immutable-stepper.md) | + +--- + +## Quick reference: ImmutableJS → Plain JS + +| ImmutableJS | Plain JS | +| ------------------------ | ---------------------------------------------------------- | +| `map.get(key)` | `obj[key]` | +| `map.getIn([a, b, c])` | `lodash.get(obj, [a, b, c])` | +| `map.set(key, val)` | `{ ...obj, [key]: val }` | +| `map.setIn([a, b], val)` | `produce(obj, d => set(d, [a, b], val))` (immer + lodash) | +| `map.merge(other)` | `{ ...obj, ...other }` | +| `map.delete(key)` | `const { [key]: _, ...rest } = obj; return rest;` | +| `map.has(key)` | `key in obj` | +| `list.push(item)` | `[...arr, item]` | +| `list.filter(fn)` | `arr.filter(fn)` | +| `list.filterNot(fn)` | `arr.filter(item => !fn(item))` | +| `list.find(fn)` | `arr.find(fn)` | +| `list.includes(val)` | `arr.includes(val)` | +| `Map.isMap(x)` | `typeof x === 'object' && x !== null && !Array.isArray(x)` | +| `List.isList(x)` | `Array.isArray(x)` | +| `Iterable.isIterable(x)` | N/A — remove the guard | +| `fromJS(obj)` | `obj` (already plain) | +| `x.toJS()` | `x` (already plain) | +| `x.size` | `Array.isArray(x) ? x.length : Object.keys(x).length` | + +## Useful grep patterns + +```bash +# Find all Immutable imports +grep -r "from 'immutable'" src/ --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" + +# Find react-immutable-proptypes usage +grep -r "react-immutable-proptypes" src/ --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" + +# Find residual Immutable method calls +grep -rE "\.(getIn|setIn|toJS|fromJS|isMap|isList|isIterable)\(" src/ +``` diff --git a/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-cmf-cqrs.md b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-cmf-cqrs.md new file mode 100644 index 00000000000..3d05be79692 --- /dev/null +++ b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-cmf-cqrs.md @@ -0,0 +1,103 @@ +# Migration Guide: `@talend/react-cmf-cqrs` — Removal of ImmutableJS + +**Version bump**: MAJOR + +--- + +## What changed + +`state.ack` is now a plain JavaScript object. The Immutable methods previously used to read/write acknowledgment state have been replaced with standard object operations. + +Removed dependency: `immutable`. + +--- + +## Breaking changes + +### 1. `state.ack` is now a plain object + +The ACK reducer now stores state as `{ [requestId]: { data, actionCreator, received } }` — a plain JS object keyed by `requestId`. + +**Before** + +```js +// state.ack was an Immutable.Map +const ackEntry = state.ack.getIn([requestId, 'data']); +const received = state.ack.getIn([requestId, 'received']); +const actionCreator = state.ack.getIn([requestId, 'actionCreator']); + +// checking if an entry exists +const exists = state.ack.has(requestId); +``` + +**After** + +```js +// state.ack is now a plain object: { [requestId]: { data, actionCreator, received } } +const ackEntry = state.ack[requestId]?.data; +const received = state.ack[requestId]?.received; +const actionCreator = state.ack[requestId]?.actionCreator; + +// checking if an entry exists +const exists = requestId in state.ack; +``` + +--- + +### 2. `ACKDispatcher` — `acks` prop is a plain object + +The `ACKDispatcher` component now iterates over `props.acks` using `Object.entries()`. Previously it was iterated as an Immutable-style iterable. + +If you use `ACKDispatcher` through `cmfConnect` (the standard usage), this is handled automatically — `state.ack` is now a plain object and is passed as `acks` directly. **No action required** for this pattern. + +If you pass `acks` **manually as a prop**, it must be a plain object: + +```js +// Before — custom acks prop (Immutable) +import { Map } from 'immutable'; + + +// After — custom acks prop (plain object) + +``` + +--- + +### 3. Test fixtures + +Tests that build mock ACK state with Immutable must be updated: + +```js +// Before +import Immutable from 'immutable'; +const state = { + ack: Immutable.fromJS({ + 'req-123': { received: false, data: null, actionCreator: 'dataset:fetch' }, + }), +}; + +// After +const state = { + ack: { + 'req-123': { received: false, data: null, actionCreator: 'dataset:fetch' }, + }, +}; +``` + +--- + +## Checklist + +- [ ] Replace `state.ack.getIn([requestId, 'key'])` → `state.ack[requestId]?.key` +- [ ] Replace `state.ack.has(requestId)` → `requestId in state.ack` +- [ ] Ensure manually-passed `acks` props are plain objects +- [ ] Update test fixtures to use plain objects instead of `Immutable.fromJS({...})` +- [ ] Remove `immutable` from your own `package.json` if transitively relied on diff --git a/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-cmf.md b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-cmf.md new file mode 100644 index 00000000000..4d5debb8d5f --- /dev/null +++ b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-cmf.md @@ -0,0 +1,209 @@ +# Migration Guide: `@talend/react-cmf` — Removal of ImmutableJS + +**Version bump**: MAJOR + +--- + +## What changed + +The CMF Redux store no longer uses ImmutableJS. Both `state.cmf.collections` and `state.cmf.components` are now plain JavaScript objects. + +Removed published dependencies: `immutable`, `react-immutable-proptypes`. + +--- + +## Breaking changes + +### 1. `state.cmf.collections` is now a plain object + +**Before** + +```js +// Immutable.Map +const items = state.cmf.collections.get('myCollection'); +const nested = state.cmf.collections.getIn(['myCollection', 'subKey']); +const all = state.cmf.collections.toJS(); +const count = state.cmf.collections.size; +const exists = state.cmf.collections.has('myCollection'); +``` + +**After** + +```js +// Plain object {} +const items = state.cmf.collections['myCollection']; + +import _get from 'lodash/get'; +const nested = _get(state.cmf.collections, ['myCollection', 'subKey']); + +const all = state.cmf.collections; // already plain, no-op +const count = Object.keys(state.cmf.collections).length; +const exists = 'myCollection' in state.cmf.collections; +``` + +**Recommended**: use the CMF selector API instead of direct access (see [New selectors](#3-new-selectors-added)). + +--- + +### 2. `state.cmf.components` is now a plain object + +**Before** + +```js +// Immutable.Map (via fromJS) +const componentState = state.cmf.components.getIn(['MyComponent', 'default'])?.toJS(); +``` + +**After** + +```js +// Plain object +const componentState = state.cmf.components?.['MyComponent']?.['default']; +``` + +**Recommended**: use `cmf.selectors.components.*` (see [New selectors](#3-new-selectors-added)). + +--- + +### 3. New selectors added + +This release adds new selectors to `cmf.selectors.collections` and introduces `cmf.selectors.components` for the first time. + +#### Collections selectors (new additions) + +```js +import cmf from '@talend/react-cmf'; + +// Get a collection as a plain value (object or array); returns undefined if absent +const raw = cmf.selectors.collections.getCollectionPlain(state, 'myCollection'); + +// Get the items array from a collection — handles both direct-array and +// Map-wrapped { items: [] } forms; returns undefined if absent +const items = cmf.selectors.collections.getCollectionItems(state, 'myCollection'); + +// Find an item in a collection by its `id` field; returns plain object or undefined +const found = cmf.selectors.collections.getCollectionItem(state, 'myCollection', itemId); +``` + +Pre-existing collections selectors still work unchanged: + +```js +cmf.selectors.collections.get(state, 'myCollection'); +cmf.selectors.collections.get(state, ['my', 'nested', 'path']); +cmf.selectors.collections.findListItem(state, 'myCollection', itemId); +cmf.selectors.collections.getAll(state); +``` + +> **Note**: `findListItem` now expects a plain array in the store. Passing an +> Immutable.List here will throw a type-mismatch error. + +#### Components selectors (new) + +```js +import cmf from '@talend/react-cmf'; + +// Get state of one component instance +const compState = cmf.selectors.components.getComponentState(state, 'MyComponent', 'default'); + +// Get all instances of a component +const allInstances = cmf.selectors.components.getAllComponentStates(state, 'MyComponent'); + +// Get a single property with a default value +const expanded = cmf.selectors.components.getComponentStateProperty( + state, + 'MyComponent', + 'default', + 'expanded', + false, +); +``` + +--- + +### 4. `cmfConnect` prop types changed + +The `state` and `initialState` props injected by `cmfConnect` no longer accept Immutable objects. + +| Prop | Before | After | +| -------------- | -------------------------------------------- | ------------------ | +| `state` | `ImmutablePropTypes.map` | `PropTypes.object` | +| `initialState` | `ImmutablePropTypes.map \| PropTypes.object` | `PropTypes.object` | + +If your component used `this.props.state.get('key')` or `this.props.state.toJS()`, migrate: + +```js +// Before +const expanded = this.props.state.get('expanded'); +this.props.setState(({ state }) => state.set('expanded', true)); + +// After +const expanded = this.props.state?.expanded; +this.props.setState({ expanded: true }); +``` + +If you used `spreadCMFState`, the internal `.toJS()` call is gone — the spread now works directly on the plain state object. + +--- + +### 5. `defaultState` in `cmfConnect` + +If you were using `new Map({})` as `defaultState`, migrate to a plain object: + +```js +// Before +import { Map } from 'immutable'; +cmfConnect({ defaultState: new Map({ expanded: false }) })(MyComponent); + +// After +cmfConnect({ defaultState: { expanded: false } })(MyComponent); +``` + +--- + +### 6. `mock.store.getState()` returns plain objects + +If your tests call `.get()`, `.getIn()`, or `.toJS()` on the mock store collections or components, update them: + +```js +// Before +const state = mock.store.getState(); +state.cmf.collections.get('myCollection'); // Immutable.Map + +// After +state.cmf.collections['myCollection']; // plain object +``` + +--- + +### 7. localStorage hydration + +If you previously called `Immutable.fromJS(storedState)` when hydrating Redux state from `localStorage`, remove that call. The data from `JSON.parse` is already a plain object — exactly what the store now expects. + +```js +// Before +const hydrated = Immutable.fromJS(JSON.parse(localStorage.getItem('state'))); + +// After +const hydrated = JSON.parse(localStorage.getItem('state')); +``` + +--- + +### 8. `selectors.toJS` — no-op wrapper kept for backward compatibility + +`cmf.selectors.collections.toJS(state, path)` is retained for backward compatibility and memoization. It no longer calls `.toJS()` internally because the store is already plain JS. No migration needed if you were calling it — it still returns the same value. + +--- + +## Checklist + +- [ ] Replace all `.get('key')` / `.getIn([...])` calls on `state.cmf.collections` / `state.cmf.components` with plain object access or CMF selectors +- [ ] Replace `.toJS()` calls with identity (already plain JS) +- [ ] Update `.size` → `Object.keys(x).length` or `Array.isArray(x) ? x.length : Object.keys(x).length` +- [ ] Update `.has(key)` → `key in obj` +- [ ] Migrate `defaultState: new Map({...})` → `defaultState: {...}` +- [ ] Update `this.props.state.get('key')` → `this.props.state?.key` in cmfConnect components +- [ ] Update `this.props.setState(s => s.set('k', v))` → `this.props.setState({ k: v })` +- [ ] Update `mock.store.getState()` usages in tests +- [ ] Remove `Immutable.fromJS(...)` from localStorage hydration if present +- [ ] Remove `immutable` and `react-immutable-proptypes` from your own `package.json` if you relied on them transitively diff --git a/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-components.md b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-components.md new file mode 100644 index 00000000000..f79927a5662 --- /dev/null +++ b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-components.md @@ -0,0 +1,85 @@ +# Migration Guide: `@talend/react-components` — Removal of ImmutableJS + +**Version bump**: MAJOR + +--- + +## What changed + +Two backward-compatibility guards for Immutable objects have been removed from `ActionDropdown`. Passing an Immutable `List` as the `items` prop will no longer work silently. + +Removed published **dependencies**: `immutable`, `react-immutable-proptypes`. If your project relied on these being transitively available through this package, you must either add them explicitly to your own `package.json`, or — preferably — remove ImmutableJS from your codebase entirely. + +--- + +## Breaking changes + +### 1. `ActionDropdown` — Immutable List no longer accepted as `items` + +Two guards have been removed: + +- The `Iterable.isIterable(item)` check that automatically called `.toJS()` on each item. +- The `!items.size` empty-state check (`.size` is an Immutable property; plain arrays use `.length`). + +**Before** + +```js +import { List } from 'immutable'; + +; +``` + +**After** + +```js + +``` + +--- + +### 2. `ActionDropdown` — `items` prop type is now strictly an array + +The `items` prop type definition has changed: + +| Before | After | +| ---------------------------------------------------------------------- | ------------------------------------------- | +| `PropTypes.arrayOf(PropTypes.shape({...})) \| ImmutablePropTypes.list` | `PropTypes.arrayOf(PropTypes.shape({...}))` | + +If you have PropTypes validation in your own code that mirrors the old definition, update it accordingly. + +--- + +### 3. `immutable` and `react-immutable-proptypes` removed from `dependencies` + +These packages are no longer listed as direct published dependencies of `@talend/react-components`. If you were relying on them being resolved transitively from this package: + +1. **Add them explicitly** to your own `package.json` as a temporary bridge, **or** +2. **Remove all ImmutableJS usage** from your codebase (recommended path). + +```bash +# Find all Immutable imports in your source +grep -r "from 'immutable'" src/ --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" +grep -r "react-immutable-proptypes" src/ --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" +``` + +--- + +## Checklist + +- [ ] Replace Immutable `List` passed as `items` to `ActionDropdown` with a plain array `[...]` +- [ ] Update any PropTypes definitions that reference `ImmutablePropTypes.list` for `ActionDropdown.items` +- [ ] Audit for transitive use of `immutable` or `react-immutable-proptypes` coming from this package and add explicit dependencies or remove ImmutableJS usage diff --git a/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-containers.md b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-containers.md new file mode 100644 index 00000000000..f01a3cae934 --- /dev/null +++ b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-containers.md @@ -0,0 +1,169 @@ +# Migration Guide: `@talend/react-containers` — Removal of ImmutableJS + +**Version bump**: MAJOR + +--- + +## What changed + +All shipped container components that used `new Immutable.Map({})` as `defaultState` have been migrated to plain objects `{}`. Component state is now accessed as a plain JavaScript object. + +Removed published dependencies: `immutable`, `react-immutable-proptypes`. + +--- + +## Breaking changes + +### 1. `defaultState` is now a plain object in all containers + +Every container that previously set `defaultState: new Map({...})` now uses `defaultState: {...}`. + +**Before** + +```js +import { Map } from 'immutable'; + +cmfConnect({ + defaultState: new Map({ expanded: false, loading: false }), +})(MyContainer); +``` + +**After** + +```js +cmfConnect({ + defaultState: { expanded: false, loading: false }, +})(MyContainer); +``` + +If you extend or override any shipped container's `defaultState`, make sure your overrides are also plain objects. + +--- + +### 2. Component state access: `.get()` / `.set()` → plain property access + +Any container component that reads or writes its CMF state via `this.props.state.get()` or `this.props.setState(s => s.set(...))` must be updated. + +**Before** + +```js +// reading +const expanded = this.props.state.get('expanded'); +const loading = this.props.state.get('loading', false); + +// writing +this.props.setState(({ state }) => state.set('expanded', !state.get('expanded'))); +``` + +**After** + +```js +// reading +const expanded = this.props.state?.expanded; +const loading = this.props.state?.loading ?? false; + +// writing +this.props.setState(({ state }) => ({ expanded: !state?.expanded })); +``` + +--- + +### 3. `ActionDropdown` container — `items` prop type narrowed + +The `items` prop in `containers/src/ActionDropdown` now only accepts `PropTypes.arrayOf(PropTypes.object)`. Passing `ImmutablePropTypes.list` is no longer supported. + +```js +// Before +import { List } from 'immutable'; + + +// After + +``` + +--- + +### 4. `AppLoader` — `hasCollections` check + +The `AppLoader` container previously checked for collection existence using the Immutable `.has()` method. It now uses the `in` operator on the plain object. + +This is an **internal change** — no API change is exposed. However, if you forked or extended `AppLoader.connect.jsx`, update: + +```js +// Before +state.cmf.collections.has(collectionName); + +// After +collectionName in state.cmf.collections; +``` + +--- + +### 5. `List` container selector — collection format + +The `List` container's selector previously handled both Immutable.Map-wrapped collections (`Map({ items: [...] })`) and plain arrays. The Immutable path has been removed. + +**Collections stored in CMF must now be either:** + +- A plain array: `['item1', 'item2']` +- A plain object with an `items` key: `{ items: ['item1', 'item2'] }` + +The selector uses `cmf.selectors.collections.getCollectionItems()` internally. No migration needed if your data was already plain JS; if your collection was an Immutable Map, replace it with a plain object. + +--- + +### 6. `ComponentForm` — state access without Immutable methods + +`ComponentForm` reads its state as a plain object. If you customized `ComponentForm` behavior and relied on the state being Immutable: + +**Before** + +```js +const jsonSchema = this.props.state.get('jsonSchema'); +const properties = this.props.state.getIn(['initialState', 'properties']).toJS(); +this.props.setState(prev => prev.state.set('dirty', false).set('jsonSchema', newSchema)); +``` + +**After** + +```js +const jsonSchema = this.props.state.jsonSchema; +const properties = this.props.state.initialState?.properties || {}; +this.props.setState({ + dirty: false, + jsonSchema: newSchema, +}); +``` + +--- + +### 7. `toJS()` helper — identity function + +The internal `toJS(immutableObject)` helper in `ComponentForm.selectors.js` now returns the object as-is (it was previously calling `.toJS()`). If you imported this helper, it still works but no longer performs any conversion. + +--- + +### 8. Test fixtures + +All test factories that created Immutable structures for `state.cmf.collections` or container `defaultState` must be updated: + +```js +// Before +import Immutable from 'immutable'; +const state = { cmf: { collections: Immutable.Map() } }; + +// After +const state = { cmf: { collections: {} } }; +``` + +--- + +## Checklist + +- [ ] Update `defaultState: new Map({...})` → `defaultState: {...}` in all `cmfConnect` containers +- [ ] Replace `this.props.state.get('key')` → `this.props.state?.key` +- [ ] Replace `this.props.setState(s => s.set('k', v))` → `this.props.setState({ k: v })` +- [ ] Ensure `items` props passed to `ActionDropdown` are plain arrays, not Immutable Lists +- [ ] Ensure collection data stored via CMF is a plain array or `{ items: [...] }` object +- [ ] Update test fixtures to use plain objects instead of `Immutable.Map()` / `Immutable.List()` +- [ ] Remove `immutable` and `react-immutable-proptypes` from your own `package.json` if transitively relied on diff --git a/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-sagas.md b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-sagas.md new file mode 100644 index 00000000000..bfc9f3660e7 --- /dev/null +++ b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-sagas.md @@ -0,0 +1,104 @@ +# Migration Guide: `@talend/react-sagas` — Removal of ImmutableJS + +**Version bump**: MAJOR + +--- + +## What changed + +The exported functions `findPenders` and `findPenderById` now return plain JavaScript values instead of Immutable structures. The internal pending-collection management has been migrated from `Immutable.Map` to plain objects. + +Removed dependency: `immutable`. + +--- + +## Breaking changes + +### 1. `findPenders(state)` returns a plain object + +**Before** + +```js +import { findPenders } from '@talend/react-sagas'; + +const penders = findPenders(state); +// penders was an Immutable.Map +const count = penders ? penders.size : 0; +const entry = penders ? penders.get(asyncActionId) : undefined; +``` + +**After** + +```js +import { findPenders } from '@talend/react-sagas'; + +const penders = findPenders(state); +// penders is now a plain object or undefined +const count = penders ? Object.keys(penders).length : 0; +const entry = penders ? penders[asyncActionId] : undefined; +``` + +--- + +### 2. `findPenderById(state, asyncActionId)` returns a plain value + +**Before** + +```js +import { findPenderById } from '@talend/react-sagas'; + +const pender = findPenderById(state, asyncActionId); +// returned an Immutable value — callers that called .get(), .toJS(), etc. +``` + +**After** + +```js +import { findPenderById } from '@talend/react-sagas'; + +const pender = findPenderById(state, asyncActionId); +// returns a plain JS value (string constant or undefined) +// e.g. 'SHOW_PENDING' or undefined +if (pender) { + // show loading indicator +} +``` + +Remove any `.get()`, `.set()`, `.toJS()`, or `.size` calls on the return values. + +--- + +### 3. Test fixtures using sagas state + +If your tests construct a Redux state object with an Immutable pending collection: + +```js +// Before +import Immutable from 'immutable'; +const state = { + cmf: { + collections: Immutable.Map({ + __PENDING__: Immutable.Map({ 'action-id-1': 'SHOW_PENDING' }), + }), + }, +}; + +// After +const state = { + cmf: { + collections: { + __PENDING__: { 'action-id-1': 'SHOW_PENDING' }, + }, + }, +}; +``` + +--- + +## Checklist + +- [ ] Replace `.size` → `Object.keys(penders).length` on `findPenders` return value +- [ ] Replace `.get(id)` → `penders[id]` on `findPenders` return value +- [ ] Remove `.toJS()` calls on `findPenders` / `findPenderById` return values +- [ ] Update test fixtures for the pending collection to plain objects +- [ ] Remove `immutable` from your own `package.json` if transitively relied on diff --git a/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-stepper.md b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-stepper.md new file mode 100644 index 00000000000..deb4c80280e --- /dev/null +++ b/docs/migration-guides/remove-immutable/migration-guide-remove-immutable-stepper.md @@ -0,0 +1,17 @@ +# Migration Guide: `@talend/react-stepper` — Removal of ImmutableJS + +**Version bump**: patch + +--- + +## What changed + +`immutable` has been removed from `devDependencies`. This was an unused declared dependency with no functional impact. + +--- + +## No action required + +There are no breaking changes in this release. The `immutable` package was only declared as a development dependency and was not used in the package's source or tests. + +If you were somehow relying on `immutable` being available transitively from this package (which would have been purely coincidental), add it explicitly to your own `package.json`. diff --git a/docs/migration-guides/vote-presentation-immutable-strategy.md b/docs/migration-guides/vote-presentation-immutable-strategy.md new file mode 100644 index 00000000000..0c333372c79 --- /dev/null +++ b/docs/migration-guides/vote-presentation-immutable-strategy.md @@ -0,0 +1,60 @@ +# ImmutableJS Strategy — Decision Proposal + +The Talend UI monorepo uses **ImmutableJS v3** (`^3.8.1`), which has been in maintenance mode since 2017. Two PRs address this technical debt with different strategies. + +--- + +## Option A — Upgrade to ImmutableJS v5 + +**PR:** [#5743](https://github.com/Talend/ui/pull/5743) + +TUI packages are updated to ImmutableJS v5 (native TypeScript, improved performance). The CMF Redux store keeps its current shape — no store-level breaking change. + +**Consumer project impact:** + +- Must upgrade their own `immutable` dependency from `^3.x` to `^5.x` +- Must audit own Immutable code for v3→v5 breaks: `Record` becomes class-based, `Iterable` is removed +- CMF store access patterns (`.get()`, `.getIn()`) are unchanged +- A migration guide covers the new TUI package versions, but **provides no guidance for migrating project-level Immutable v3 code to v5** + +--- + +## Option B — Remove ImmutableJS entirely + +**PR:** [#5755](https://github.com/Talend/ui/pull/5755) + +ImmutableJS is completely removed from TUI. All internal data structures become plain JavaScript objects. A new CMF selector API is introduced as a stable access layer. + +**Affected TUI packages** (all MAJOR bumps): `react-cmf`, `react-containers`, `react-cmf-cqrs`, `react-sagas`, `react-components`, `react-flow-designer`. + +**Consumer project impact:** + +- CMF store access (`.get()` / `.getIn()`), container `defaultState`, component state read/write, and test fixtures must be migrated — covered by [per-package migration guides](./migration-guide-remove-immutable.md) +- **Only TUI-related Immutable usage is affected.** Project-internal code that uses ImmutableJS independently of TUI is untouched +- After migration, each project is free to keep, upgrade, or remove Immutable at its own pace + +--- + +## Comparison + +| Criterion | Option A | Option B | +| ------------------------------------- | -------------------------------- | ----------------------- | +| TUI breaking changes | Limited | 6 MAJOR version bumps | +| Consumer: own `immutable` dep | Must upgrade to `^5.x` | Unaffected | +| Consumer: store / state access | Unchanged | Must migrate (guided) | +| Consumer: project-internal Immutable | Must audit and update (unguided) | Not affected | +| Long-term TUI dependency on Immutable | Yes (v5) | Eliminated | +| Migration guide coverage | TUI packages only | Full per-package guides | + +--- + +## Recommendation + +**Option B** is recommended: + +- Option A defers the problem — projects still need to migrate their own Immutable v3 code without any guide, and TUI would remain tied to ImmutableJS long-term +- Option B is a finite, documented migration: effort is higher but bounded, and projects gain full independence from ImmutableJS after migration + +> The main argument for Option A: if projects have very heavy own Immutable usage, they would need a v3→v5 upgrade regardless — Option A allows doing it in one pass. + +--- diff --git a/packages/cmf-cqrs/package.json b/packages/cmf-cqrs/package.json index af60330ca68..52e172787c5 100644 --- a/packages/cmf-cqrs/package.json +++ b/packages/cmf-cqrs/package.json @@ -43,7 +43,6 @@ "dependencies": { "@talend/react-cmf": "^12.1.1", "@talend/utils": "^3.7.1", - "immutable": "^3.8.2", "redux-saga": "^1.4.2" }, "devDependencies": { diff --git a/packages/cmf-cqrs/src/components/ACKDispatcher/ACKDispatcher.container.js b/packages/cmf-cqrs/src/components/ACKDispatcher/ACKDispatcher.container.js index 40ad69670bf..14dab60cf43 100644 --- a/packages/cmf-cqrs/src/components/ACKDispatcher/ACKDispatcher.container.js +++ b/packages/cmf-cqrs/src/components/ACKDispatcher/ACKDispatcher.container.js @@ -1,12 +1,11 @@ import { useState } from 'react'; import PropTypes from 'prop-types'; -import { Map } from 'immutable'; import cmf, { cmfConnect, useCMFContext } from '@talend/react-cmf'; import { randomUUID } from '@talend/utils'; import { deleteACK } from '../../actions/ack'; -export const DEFAULT_STATE = new Map({}); +export const DEFAULT_STATE = {}; /** * { @@ -40,14 +39,11 @@ function ACKDispatcher(props) { cache[uuid].push(requestId); } } - (props.acks || []) - .filter(ack => ack.get('received') === true && ack.get('actionCreator')) - .forEach((ack, requestId) => { - let data = ack.get('data'); - if (data === undefined) { - data = {}; - } - dispatchAndUpdateAck(ack.get('actionCreator'), data, requestId); + Object.entries(props.acks || {}) + .filter(([, ack]) => ack.received === true && ack.actionCreator) + .forEach(([requestId, ack]) => { + const data = ack.data !== undefined ? ack.data : {}; + dispatchAndUpdateAck(ack.actionCreator, data, requestId); }); return null; diff --git a/packages/cmf-cqrs/src/components/ACKDispatcher/ACKDispatcher.test.jsx b/packages/cmf-cqrs/src/components/ACKDispatcher/ACKDispatcher.test.jsx index 5eab8cb186f..7feba7100a7 100644 --- a/packages/cmf-cqrs/src/components/ACKDispatcher/ACKDispatcher.test.jsx +++ b/packages/cmf-cqrs/src/components/ACKDispatcher/ACKDispatcher.test.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Map } from 'immutable'; import { render } from '@testing-library/react'; import { mock } from '@talend/react-cmf'; @@ -16,23 +15,23 @@ describe('Container ACKDispatcher', () => { it('should render nothing', () => { const { container } = render( - + , ); expect(container.firstChild).toBeEmptyDOMElement(); }); it('should processACK call dispatch', () => { - const acks = Map({ - 123: new Map({ + const acks = { + 123: { actionCreator: 'actionCreator', data: { foo: 'bar' }, received: true, - }), - 456: new Map({ + }, + 456: { actionCreator: 'actionCreatorBis', data: { foo: 'baz' }, - }), - }); + }, + }; const registry = mock.store.registry(); const mocked = vi.fn(); @@ -70,14 +69,14 @@ describe('Container ACKDispatcher', () => { registry['actionCreator:myActionCreator'] = myActionCreator; const { rerender } = render( - + , ); rerender( , ); @@ -95,20 +94,20 @@ describe('Container ACKDispatcher', () => { }); it(`should dispatch call props.dispatch even when ack only when both received is true, and action exist meaning that we can receive ack before creation request is resolve`, () => { - const acks = Map({ - 42: new Map({ + const acks = { + 42: { received: true, - }), - 123: new Map({ + }, + 123: { actionCreator: 'actionCreator', data: { foo: 'bar' }, received: true, - }), - 456: new Map({ + }, + 456: { actionCreator: 'actionCreatorBis', data: { foo: 'baz' }, - }), - }); + }, + }; const registry = mock.store.registry(); const mocked = vi.fn(); @@ -141,19 +140,15 @@ describe('Connected ACKDispatcher', () => { it('should mapStateToProps', () => { const state = { cmf: { - components: new Map({ - ACKDispatcher: { - ACKDispatcher: DEFAULT_STATE.toJS(), - }, - }), + components: {}, }, - ack: new Map({ - 123: new Map({ + ack: { + 123: { actionCreator: 'test', data: { foo: 'bar' }, received: true, - }), - }), + }, + }, }; const props = mapStateToProps(state); expect(typeof props).toBe('object'); diff --git a/packages/cmf-cqrs/src/components/ACKDispatcher/__snapshots__/ACKDispatcher.test.jsx.snap b/packages/cmf-cqrs/src/components/ACKDispatcher/__snapshots__/ACKDispatcher.test.jsx.snap index ceff06e09ae..aad99a2f177 100644 --- a/packages/cmf-cqrs/src/components/ACKDispatcher/__snapshots__/ACKDispatcher.test.jsx.snap +++ b/packages/cmf-cqrs/src/components/ACKDispatcher/__snapshots__/ACKDispatcher.test.jsx.snap @@ -2,8 +2,8 @@ exports[`Connected ACKDispatcher > should mapStateToProps 1`] = ` { - "acks": Immutable.Map { - "123": Immutable.Map { + "acks": { + "123": { "actionCreator": "test", "data": { "foo": "bar", diff --git a/packages/cmf-cqrs/src/middleware/socketMiddleware.test.js b/packages/cmf-cqrs/src/middleware/socketMiddleware.test.js index 08400f83baf..b5c1472a7f9 100644 --- a/packages/cmf-cqrs/src/middleware/socketMiddleware.test.js +++ b/packages/cmf-cqrs/src/middleware/socketMiddleware.test.js @@ -1,5 +1,4 @@ import configureStore from 'redux-mock-store'; -import { Map } from 'immutable'; import createStatePatchLogger, { isAbsoluteWebSocketUrl } from './socketMiddleware'; import smartWebsocket from './smartWebsocket'; @@ -17,15 +16,15 @@ const mockStore = configureStore([createStatePatchLogger([], [])]); const state = { path: {}, - test: new Map({ + test: { in: {}, out: {}, parents: {}, childrens: {}, - nodes: new Map(), + nodes: {}, transforms: {}, nodeTypes: {}, - }), + }, routing: { locationBeforeTransitions: { pathname: '/datastream-designer/50/', @@ -65,7 +64,7 @@ describe('pathToServer', () => { call = 1; return state; } - return { ...state, test: state.test.setIn(['nodes', 'test'], 'test') }; + return { ...state, test: { ...state.test, nodes: { ...state.test.nodes, test: 'test' } } }; }); const dispatch = vi.fn(); const middleware = createStatePatchLogger('test', []); diff --git a/packages/cmf-cqrs/src/reducers/__snapshots__/ack.test.js.snap b/packages/cmf-cqrs/src/reducers/__snapshots__/ack.test.js.snap index 408d3520256..48a7b2098fb 100644 --- a/packages/cmf-cqrs/src/reducers/__snapshots__/ack.test.js.snap +++ b/packages/cmf-cqrs/src/reducers/__snapshots__/ack.test.js.snap @@ -1,31 +1,31 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`reducers.ack.ackReducer > should reduce addContext 1`] = ` -Immutable.Map { - "123": Immutable.Map { +{ + "123": { + "actionCreator": "my super action creator", "data": { "foo": "bar", }, - "actionCreator": "my super action creator", }, } `; exports[`reducers.ack.ackReducer > should reduce deleteACK 1`] = ` -Immutable.Map { - "456": Immutable.Map { +{ + "456": { "foo": "bar", }, } `; exports[`reducers.ack.ackReducer > should reduce receiveMessage 1`] = ` -Immutable.Map { - "123": Immutable.Map { +{ + "123": { "foo": "bar", "received": true, }, - "456": Immutable.Map { + "456": { "foo": "bar", }, } @@ -33,12 +33,12 @@ Immutable.Map { exports[`reducers.ackProcessed higher order reducer > should reduce addContext 1`] = ` { - "ack": Immutable.Map { - "123": Immutable.Map { + "ack": { + "123": { + "actionCreator": "my super action creator", "data": { "foo": "bar", }, - "actionCreator": "my super action creator", }, }, } @@ -46,8 +46,8 @@ exports[`reducers.ackProcessed higher order reducer > should reduce addContext 1 exports[`reducers.ackProcessed higher order reducer > should reduce deleteACK 1`] = ` { - "ack": Immutable.Map { - "456": Immutable.Map { + "ack": { + "456": { "foo": "bar", }, }, @@ -56,12 +56,12 @@ exports[`reducers.ackProcessed higher order reducer > should reduce deleteACK 1` exports[`reducers.ackProcessed higher order reducer > should reduce receiveMessage 1`] = ` { - "ack": Immutable.Map { - "123": Immutable.Map { + "ack": { + "123": { "foo": "bar", "received": true, }, - "456": Immutable.Map { + "456": { "foo": "bar", }, }, diff --git a/packages/cmf-cqrs/src/reducers/ack.js b/packages/cmf-cqrs/src/reducers/ack.js index a7d45120f54..8608ce7a15c 100644 --- a/packages/cmf-cqrs/src/reducers/ack.js +++ b/packages/cmf-cqrs/src/reducers/ack.js @@ -1,7 +1,6 @@ -import { Map } from 'immutable'; import { ACK_ADD_CONTEXT, ACK_RECEIVE_MESSAGE, ACK_DELETE } from '../constants/index'; -const DEFAULT_STATE = new Map({}); +const DEFAULT_STATE = {}; /** * ackReducer @@ -12,13 +11,26 @@ const DEFAULT_STATE = new Map({}); export default function ackReducer(state = DEFAULT_STATE, action) { switch (action.type) { case ACK_ADD_CONTEXT: - return state - .setIn([action.requestId, 'data'], action.data) - .setIn([action.requestId, 'actionCreator'], action.actionCreator); + return { + ...state, + [action.requestId]: { + ...(state[action.requestId] || {}), + data: action.data, + actionCreator: action.actionCreator, + }, + }; case ACK_RECEIVE_MESSAGE: - return state.setIn([action.requestId, 'received'], true); - case ACK_DELETE: - return state.remove(action.requestId); + return { + ...state, + [action.requestId]: { + ...(state[action.requestId] || {}), + received: true, + }, + }; + case ACK_DELETE: { + const { [action.requestId]: _, ...rest } = state; + return rest; + } default: return state; } diff --git a/packages/cmf-cqrs/src/reducers/ack.test.js b/packages/cmf-cqrs/src/reducers/ack.test.js index af7ff0121b0..00563b0cb05 100644 --- a/packages/cmf-cqrs/src/reducers/ack.test.js +++ b/packages/cmf-cqrs/src/reducers/ack.test.js @@ -1,4 +1,3 @@ -import { Map, fromJS } from 'immutable'; import ackReducer, { ackProcessed } from './ack'; import { addContext, receiveMessage, deleteACK } from '../actions/ack'; @@ -16,10 +15,10 @@ describe('reducers.ack.ackReducer', () => { }); it('should reduce receiveMessage', () => { const state = ackReducer( - fromJS({ + { 123: { foo: 'bar' }, 456: { foo: 'bar' }, - }), + }, receiveMessage(null, { requestId: '123', }), @@ -28,10 +27,10 @@ describe('reducers.ack.ackReducer', () => { }); it('should reduce deleteACK', () => { const state = ackReducer( - fromJS({ + { 123: { foo: 'bar' }, 456: { foo: 'bar' }, - }), + }, deleteACK(null, { requestId: '123', }), @@ -41,11 +40,13 @@ describe('reducers.ack.ackReducer', () => { it('should return default state', () => { const state = ackReducer(undefined, { type: 'notsupported' }); - expect(Map.isMap(state)).toBe(true); + expect(typeof state).toBe('object'); + expect(state).not.toBeNull(); + expect(Array.isArray(state)).toBe(false); }); it('should return input state on non supported action', () => { - const state = new Map(); + const state = {}; const newState = ackReducer(state, { type: 'notsupported' }); expect(state).toBe(newState); }); @@ -73,10 +74,10 @@ describe('reducers.ackProcessed higher order reducer', () => { it('should reduce receiveMessage', () => { const state = ackProcessed( { - ack: fromJS({ + ack: { 123: { foo: 'bar' }, 456: { foo: 'bar' }, - }), + }, }, { ack: receiveMessage(null, { @@ -89,10 +90,10 @@ describe('reducers.ackProcessed higher order reducer', () => { it('should reduce deleteACK', () => { const state = ackProcessed( { - ack: fromJS({ + ack: { 123: { foo: 'bar' }, 456: { foo: 'bar' }, - }), + }, }, { ack: deleteACK(null, { diff --git a/packages/cmf/__tests__/App.test.js b/packages/cmf/__tests__/App.test.js index dd459b91b17..c3c45169f98 100644 --- a/packages/cmf/__tests__/App.test.js +++ b/packages/cmf/__tests__/App.test.js @@ -1,4 +1,5 @@ import { Provider } from 'react-redux'; +import { vi } from 'vitest'; import { render, screen } from '@testing-library/react'; @@ -6,18 +7,20 @@ import App from '../src/App'; import ErrorBoundary from '../src/components/ErrorBoundary/ErrorBoundary.component'; import RegistryProvider from '../src/RegistryProvider'; -jest.mock('react-redux', () => ({ - esModule: true, - Provider: jest.fn(props =>
{props.children}
), - connect: jest.requireActual('react-redux').connect, -})); +vi.mock('react-redux', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + Provider: vi.fn(props =>
{props.children}
), + }; +}); -jest.mock('../src/RegistryProvider', () => - jest.fn(props =>
{props.children}
), -); -jest.mock('../src/components/ErrorBoundary/ErrorBoundary.component', () => - jest.fn(props =>
{props.children}
), -); +vi.mock('../src/RegistryProvider', () => ({ + default: vi.fn(props =>
{props.children}
), +})); +vi.mock('../src/components/ErrorBoundary/ErrorBoundary.component', () => ({ + default: vi.fn(props =>
{props.children}
), +})); describe('CMF App', () => { it('App should init stuff', () => { diff --git a/packages/cmf/__tests__/Dispatcher.test.js b/packages/cmf/__tests__/Dispatcher.test.js index baad5e8654c..5a38063531c 100644 --- a/packages/cmf/__tests__/Dispatcher.test.js +++ b/packages/cmf/__tests__/Dispatcher.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { fireEvent, createEvent, render, screen } from '@testing-library/react'; @@ -9,20 +10,20 @@ const noopRId = `${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:noOp`; describe('Testing ', () => { let registry; - const onError = jest.fn(); + const onError = vi.fn(); beforeEach(() => { registry = { - [`${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:existingActionCreator:id`]: jest.fn(), - [`${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:actionCreator:id`]: jest.fn(), - [`${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:noOp`]: jest.fn(), - [`${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:another:actionCreator:id`]: jest.fn(), + [`${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:existingActionCreator:id`]: vi.fn(), + [`${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:actionCreator:id`]: vi.fn(), + [`${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:noOp`]: vi.fn(), + [`${CONST.REGISTRY_ACTION_CREATOR_PREFIX}:another:actionCreator:id`]: vi.fn(), }; - jest.resetAllMocks(); + vi.resetAllMocks(); }); it('should add onclick event handler to its children', () => { - const dispatchActionCreator = jest.fn(); + const dispatchActionCreator = vi.fn(); render( @@ -38,7 +39,7 @@ describe('Testing ', () => { render( - + @@ -85,8 +86,8 @@ describe('Testing ', () => { ); it('should not prevent event propagation by default', () => { - const dispatchActionCreator = jest.fn(); - const onClick = jest.fn(); + const dispatchActionCreator = vi.fn(); + const onClick = vi.fn(); render(
@@ -105,8 +106,8 @@ describe('Testing ', () => { }); it('should prevent event propagation if stopPropagation is set', () => { - const dispatchActionCreator = jest.fn(); - const onClick = jest.fn(); + const dispatchActionCreator = vi.fn(); + const onClick = vi.fn(); render(
@@ -126,7 +127,7 @@ describe('Testing ', () => { }); it('should preventDefault if props is set', () => { - const dispatchActionCreator = jest.fn(); + const dispatchActionCreator = vi.fn(); render( @@ -136,13 +137,13 @@ describe('Testing ', () => { ); const el = screen.getByText('foo'); const event = createEvent.click(el, {}); - event.preventDefault = jest.fn(); + event.preventDefault = vi.fn(); fireEvent(el, event); expect(event.preventDefault).toHaveBeenCalled(); }); it('should dispatch actionCreator with props as data', () => { - const dispatchActionCreator = jest.fn(); + const dispatchActionCreator = vi.fn(); const props = { dispatchActionCreator, preventDefault: true, @@ -160,7 +161,7 @@ describe('Testing ', () => { const el = screen.getByText('foo'); const event = createEvent.click(el, {}); - event.preventDefault = jest.fn(); + event.preventDefault = vi.fn(); expect(event.type).toBe('click'); fireEvent(el, event); diff --git a/packages/cmf/__tests__/Inject.test.js b/packages/cmf/__tests__/Inject.test.js index 1419d98b56f..a76413d409a 100644 --- a/packages/cmf/__tests__/Inject.test.js +++ b/packages/cmf/__tests__/Inject.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import Inject from '../src/Inject.component'; @@ -6,7 +7,7 @@ import { mock } from '../src'; describe('Inject', () => { it('should render', () => { // given - const MyComponent = jest.fn(props => Hello); + const MyComponent = vi.fn(props => Hello); MyComponent.displayName = 'MyComponent'; const registry = { '_.route.component:MyComponent': MyComponent, @@ -27,7 +28,7 @@ describe('Inject', () => { it('should render error if component not found', () => { // given - const MyComponent = jest.fn(); + const MyComponent = vi.fn(); MyComponent.displayName = 'MyComponent'; // when diff --git a/packages/cmf/__tests__/__snapshots__/cmfConnect.test.jsx.snap b/packages/cmf/__tests__/__snapshots__/cmfConnect.test.jsx.snap index 6e1743dba80..7d9c71eedb4 100644 --- a/packages/cmf/__tests__/__snapshots__/cmfConnect.test.jsx.snap +++ b/packages/cmf/__tests__/__snapshots__/cmfConnect.test.jsx.snap @@ -1,11 +1,11 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`cmfConnect Higher Order Component should not remove the component state when unmount and cmfConnect keepComponentState is true 1`] = ` +exports[`cmfConnect > Higher Order Component > should not remove the component state when unmount and cmfConnect keepComponentState is true 1`] = ` { "cmf": { "componentState": { "componentName": "TestComponent", - "initialComponentState": Immutable.Map {}, + "initialComponentState": {}, "key": "default", "type": "REACT_CMF.COMPONENT_ADD_STATE", }, @@ -15,12 +15,12 @@ exports[`cmfConnect Higher Order Component should not remove the component state } `; -exports[`cmfConnect Higher Order Component should not remove the component state when unmount and props keepComponentState is true 1`] = ` +exports[`cmfConnect > Higher Order Component > should not remove the component state when unmount and props keepComponentState is true 1`] = ` { "cmf": { "componentState": { "componentName": "TestComponent", - "initialComponentState": Immutable.Map {}, + "initialComponentState": {}, "key": "default", "type": "REACT_CMF.COMPONENT_ADD_STATE", }, @@ -30,12 +30,12 @@ exports[`cmfConnect Higher Order Component should not remove the component state } `; -exports[`cmfConnect Higher Order Component should remove the component state when unmount 1`] = ` +exports[`cmfConnect > Higher Order Component > should remove the component state when unmount 1`] = ` { "cmf": { "componentState": { "componentName": "TestComponent", - "initialComponentState": Immutable.Map {}, + "initialComponentState": {}, "key": "default", "type": "REACT_CMF.COMPONENT_ADD_STATE", }, @@ -45,7 +45,7 @@ exports[`cmfConnect Higher Order Component should remove the component state whe } `; -exports[`cmfConnect Higher Order Component should remove the component state when unmount 2`] = ` +exports[`cmfConnect > Higher Order Component > should remove the component state when unmount 2`] = ` { "cmf": { "componentState": { @@ -59,12 +59,12 @@ exports[`cmfConnect Higher Order Component should remove the component state whe } `; -exports[`cmfConnect Higher Order Component should remove the component state when unmount and props keepComponentState is false 1`] = ` +exports[`cmfConnect > Higher Order Component > should remove the component state when unmount and props keepComponentState is false 1`] = ` { "cmf": { "componentState": { "componentName": "TestComponent", - "initialComponentState": Immutable.Map {}, + "initialComponentState": {}, "key": "default", "type": "REACT_CMF.COMPONENT_ADD_STATE", }, @@ -74,7 +74,7 @@ exports[`cmfConnect Higher Order Component should remove the component state whe } `; -exports[`cmfConnect Higher Order Component should remove the component state when unmount and props keepComponentState is false 2`] = ` +exports[`cmfConnect > Higher Order Component > should remove the component state when unmount and props keepComponentState is false 2`] = ` { "cmf": { "componentState": { diff --git a/packages/cmf/__tests__/__snapshots__/componentState.test.js.snap b/packages/cmf/__tests__/__snapshots__/componentState.test.js.snap index 28b0ee89b18..73f098cc420 100644 --- a/packages/cmf/__tests__/__snapshots__/componentState.test.js.snap +++ b/packages/cmf/__tests__/__snapshots__/componentState.test.js.snap @@ -1,11 +1,11 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`state should getStateAccessors return accessors 1`] = ` +exports[`state > should getStateAccessors return accessors 1`] = ` { "cmf": { "componentState": { "componentName": "name", - "initialComponentState": Immutable.Map { + "initialComponentState": { "foo": "bar", }, "key": "id", @@ -17,7 +17,7 @@ exports[`state should getStateAccessors return accessors 1`] = ` } `; -exports[`state should getStateAccessors return accessors 2`] = ` +exports[`state > should getStateAccessors return accessors 2`] = ` { "cmf": { "componentState": { @@ -34,7 +34,7 @@ exports[`state should getStateAccessors return accessors 2`] = ` } `; -exports[`state should getStateAccessors return accessors 3`] = ` +exports[`state > should getStateAccessors return accessors 3`] = ` { "cmf": { "componentState": { @@ -48,7 +48,7 @@ exports[`state should getStateAccessors return accessors 3`] = ` } `; -exports[`state should getStateAccessors should support no DEFAULT_STATE 1`] = ` +exports[`state > should getStateAccessors should support no DEFAULT_STATE 1`] = ` { "cmf": { "componentState": { diff --git a/packages/cmf/__tests__/action.test.js b/packages/cmf/__tests__/action.test.js index 6d70df4c024..f783773702b 100644 --- a/packages/cmf/__tests__/action.test.js +++ b/packages/cmf/__tests__/action.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import actionAPI from '../src/action'; import { mock } from '../src'; @@ -116,7 +117,7 @@ describe('CMF action', () => { actionCreator: 'myActionCreator', }; context.registry = {}; - context.registry['actionCreator:myActionCreator'] = jest.fn(() => ({ type: 'MY_ACTION_TYPE' })); + context.registry['actionCreator:myActionCreator'] = vi.fn(() => ({ type: 'MY_ACTION_TYPE' })); const action = actionAPI.getActionObject(context, obj); expect(context.registry['actionCreator:myActionCreator']).toHaveBeenCalled(); expect(action.type).toBe('MY_ACTION_TYPE'); diff --git a/packages/cmf/__tests__/actionCreator.test.js b/packages/cmf/__tests__/actionCreator.test.js index 93c3b3eacdb..6e2d291a7dd 100644 --- a/packages/cmf/__tests__/actionCreator.test.js +++ b/packages/cmf/__tests__/actionCreator.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { mock } from '../src'; import actionCreatorAPI from '../src/actionCreator'; @@ -33,7 +34,7 @@ describe('CMF action', () => { }); it('should register an actionCreator in context', () => { - const creator = jest.fn(); + const creator = vi.fn(); const id = 'myactioncreator'; context.registry = {}; actionCreatorAPI.register(id, creator, context); diff --git a/packages/cmf/__tests__/actions/__snapshots__/componentsActions.test.js.snap b/packages/cmf/__tests__/actions/__snapshots__/componentsActions.test.js.snap index 232cf61ba89..ec617cc14bb 100644 --- a/packages/cmf/__tests__/actions/__snapshots__/componentsActions.test.js.snap +++ b/packages/cmf/__tests__/actions/__snapshots__/componentsActions.test.js.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`test component state management action creators addState dispatch well formed action object 1`] = ` +exports[`test component state management action creators > addState dispatch well formed action object 1`] = ` { "componentName": "componentName", "initialComponentState": { @@ -11,7 +11,7 @@ exports[`test component state management action creators addState dispatch well } `; -exports[`test component state management action creators mergeState dispatch well formed acton object 1`] = ` +exports[`test component state management action creators > mergeState dispatch well formed acton object 1`] = ` { "componentName": "componentName", "componentState": { @@ -22,7 +22,7 @@ exports[`test component state management action creators mergeState dispatch wel } `; -exports[`test component state management action creators removeState dispatch well formed acton object 1`] = ` +exports[`test component state management action creators > removeState dispatch well formed acton object 1`] = ` { "componentName": "componentName", "key": "key", diff --git a/packages/cmf/__tests__/actions/collectionsActions.test.js b/packages/cmf/__tests__/actions/collectionsActions.test.js index 6be7bd7b94e..5ba63afc7bf 100644 --- a/packages/cmf/__tests__/actions/collectionsActions.test.js +++ b/packages/cmf/__tests__/actions/collectionsActions.test.js @@ -1,12 +1,7 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { Map } from 'immutable'; -import { - addOrReplace, - remove, - mutate, -} from '../../src/actions/collectionsActions'; +import { addOrReplace, remove, mutate } from '../../src/actions/collectionsActions'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); @@ -22,7 +17,7 @@ describe('test collection management action creators', () => { it('addOrReplaceCollection dispatch with path to nested collections (path can be either strings separated with dots or array of strings)', () => { expect( - addOrReplace('collectionId.nestedCollection.nestedObjField', 'data can be anything') + addOrReplace('collectionId.nestedCollection.nestedObjField', 'data can be anything'), ).toEqual({ type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', collectionId: 'collectionId.nestedCollection.nestedObjField', @@ -31,14 +26,16 @@ describe('test collection management action creators', () => { }); it('removeCollection dispatch well formed action object', () => { - const expectedActions = [{ - type: 'REACT_CMF.COLLECTION_REMOVE', - collectionId: 'collectionId', - }]; + const expectedActions = [ + { + type: 'REACT_CMF.COLLECTION_REMOVE', + collectionId: 'collectionId', + }, + ]; const store = mockStore({ cmf: { - collections: new Map().set('collectionId', 'data'), + collections: { collectionId: 'data' }, }, }); diff --git a/packages/cmf/__tests__/actions/http.test.js b/packages/cmf/__tests__/actions/http.test.js index ed119bfa184..6fd27a9e16e 100644 --- a/packages/cmf/__tests__/actions/http.test.js +++ b/packages/cmf/__tests__/actions/http.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import http from '../../src/actions/http'; import { HTTP_METHODS, @@ -107,7 +108,7 @@ describe('actions.http', () => { expect(newAction.type).toBe('CALL_ME_BACK'); expect(newAction.error).toBe(error); - action.onError = jest.fn(); + action.onError = vi.fn(); http.onActionError(action, error); expect(action.onError.mock.calls.length).toBe(1); expect(action.onError.mock.calls[0][0]).toBe(error); @@ -120,14 +121,14 @@ describe('actions.http', () => { onResponse: 'CALL_ME_BACK', }; const headers = { - 'content-type': 'application/json' + 'content-type': 'application/json', }; const newAction = http.onActionResponse(action, response, headers); expect(newAction.type).toBe('CALL_ME_BACK'); expect(newAction.response).toBe(response); expect(newAction.headers).toBe(headers); - action.onResponse = jest.fn(); + action.onResponse = vi.fn(); http.onActionResponse(action, response, headers); expect(action.onResponse.mock.calls.length).toBe(1); expect(action.onResponse.mock.calls[0][0]).toBe(response); diff --git a/packages/cmf/__tests__/bootstrap.test.jsx b/packages/cmf/__tests__/bootstrap.test.jsx index 5ca1ca77e86..6943fabcb91 100644 --- a/packages/cmf/__tests__/bootstrap.test.jsx +++ b/packages/cmf/__tests__/bootstrap.test.jsx @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import ReactDOM from 'react-dom/client'; import createSagaMiddleware from 'redux-saga'; @@ -12,59 +13,75 @@ import registry from '../src/registry'; import sagas from '../src/sagas'; import storeAPI from '../src/store'; -jest.mock('react-dom/client', () => ({ - createRoot: jest.fn().mockImplementation(() => ({ - render: jest.fn(), - })), +vi.mock('react-dom/client', () => ({ + default: { + createRoot: vi.fn().mockImplementation(() => ({ + render: vi.fn(), + })), + }, })); -jest.mock('redux-saga', () => ({ +vi.mock('redux-saga', () => ({ __esModule: true, // this property makes it work default: (() => { - const run = jest.fn(); - const middleware = jest.fn(() => ({ reduxSagaMocked: true, run })); + const run = vi.fn(); + const middleware = vi.fn(() => ({ reduxSagaMocked: true, run })); middleware.run = run; middleware.clearRun = () => run.mockClear(); return middleware; })(), effects: { - spawn: jest.fn(), + spawn: vi.fn(), }, })); -jest.mock('../src/onError', () => ({ - report: jest.fn(), - bootstrap: jest.fn(), +vi.mock('../src/onError', () => ({ + default: { + report: vi.fn(), + bootstrap: vi.fn(), + }, })); -jest.mock('../src/registry', () => ({ - registerMany: jest.fn(), - getRegistry: jest.fn(), +vi.mock('../src/registry', () => ({ + default: { + registerMany: vi.fn(), + getRegistry: vi.fn(), + }, })); -jest.mock('../src/actionCreator', () => ({ - registerMany: jest.fn(), +vi.mock('../src/actionCreator', () => ({ + default: { + registerMany: vi.fn(), + }, })); -jest.mock('../src/component', () => ({ - registerMany: jest.fn(), +vi.mock('../src/component', () => ({ + default: { + registerMany: vi.fn(), + }, })); -jest.mock('../src/expression', () => ({ - registerMany: jest.fn(), +vi.mock('../src/expression', () => ({ + default: { + registerMany: vi.fn(), + }, })); -jest.mock('../src/sagas', () => ({ - registerMany: jest.fn(), +vi.mock('../src/sagas', () => ({ + default: { + registerMany: vi.fn(), + }, })); -jest.mock('../src/register', () => ({ - registerInternals: jest.fn(), +vi.mock('../src/register', () => ({ + registerInternals: vi.fn(), })); -jest.mock('../src/store', () => ({ - addPreReducer: jest.fn(), - setHttpMiddleware: jest.fn(), - initialize: jest.fn(() => ({ dispatch: jest.fn(), applyMiddleware: jest.fn() })), +vi.mock('../src/store', () => ({ + default: { + addPreReducer: vi.fn(), + setHttpMiddleware: vi.fn(), + initialize: vi.fn(() => ({ dispatch: vi.fn(), applyMiddleware: vi.fn() })), + }, })); describe('bootstrap', () => { beforeEach(() => { onError.bootstrap.mockClear(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('error management', () => { it('should bootstrap onError', async () => { @@ -97,7 +114,7 @@ describe('bootstrap', () => { registry.registerMany.mockClear(); const options = { registry: { - foo: jest.fn(), + foo: vi.fn(), }, }; await bootstrap(options); @@ -107,7 +124,7 @@ describe('bootstrap', () => { component.registerMany.mockClear(); const options = { components: { - foo: jest.fn(), + foo: vi.fn(), }, }; await bootstrap(options); @@ -117,7 +134,7 @@ describe('bootstrap', () => { expression.registerMany.mockClear(); const options = { expressions: { - foo: jest.fn(), + foo: vi.fn(), }, }; await bootstrap(options); @@ -127,7 +144,7 @@ describe('bootstrap', () => { actionCreator.registerMany.mockClear(); const options = { actionCreators: { - foo: jest.fn(), + foo: vi.fn(), }, }; await bootstrap(options); @@ -137,7 +154,7 @@ describe('bootstrap', () => { sagas.registerMany.mockClear(); const options = { sagas: { - foo: jest.fn(), + foo: vi.fn(), }, }; await bootstrap(options); @@ -156,14 +173,14 @@ describe('bootstrap', () => { describe('redux', () => { it('should call storeAPI.addPreReducer if options.preReducer', async () => { const options = { - preReducer: jest.fn(), + preReducer: vi.fn(), }; await bootstrap(options); expect(storeAPI.addPreReducer).toHaveBeenCalledWith(options.preReducer); }); it('should call storeAPI.setHttpMiddleware if options.preReducer', async () => { const options = { - httpMiddleware: jest.fn(), + httpMiddleware: vi.fn(), }; await bootstrap(options); expect(storeAPI.setHttpMiddleware).toHaveBeenCalledWith(options.httpMiddleware); @@ -172,7 +189,7 @@ describe('bootstrap', () => { storeAPI.initialize.mockClear(); createSagaMiddleware.mockClear(); const options = { - reducer: { app: jest.fn() }, + reducer: { app: vi.fn() }, preloadedState: {}, middlewares: [], }; diff --git a/packages/cmf/__tests__/cmfConnect.test.jsx b/packages/cmf/__tests__/cmfConnect.test.jsx index 99b910f8731..d366f690359 100644 --- a/packages/cmf/__tests__/cmfConnect.test.jsx +++ b/packages/cmf/__tests__/cmfConnect.test.jsx @@ -1,10 +1,10 @@ +import { vi } from 'vitest'; /** * @jest-environment jsdom */ import { Component } from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; -import { fromJS, Map } from 'immutable'; import PropTypes from 'prop-types'; import { mock } from '../src'; @@ -54,25 +54,25 @@ describe('cmfConnect', () => { describe('#getStateToProps', () => { it('should call getStateProps', () => { const state = mock.store.state(); - state.cmf.components = fromJS({ + state.cmf.components = { TestComponent: { testId: { foo: 'bar', }, }, - }); + }; const props = getStateToProps({ componentId: 'testId', ownProps: {}, state, WrappedComponent: { displayName: 'TestComponent' }, }); - expect(props.state.get('foo')).toBe('bar'); + expect(props.state.foo).toBe('bar'); }); it('should inject view settings using props.view', () => { const state = mock.store.state(); - state.cmf.components = fromJS({}); + state.cmf.components = {}; const props = getStateToProps({ componentId: 'testId', ownProps: { view: 'homepage' }, @@ -84,7 +84,7 @@ describe('cmfConnect', () => { it('should inject view settings using displayName and componentId', () => { const state = mock.store.state(); - state.cmf.components = fromJS({}); + state.cmf.components = {}; state.cmf.settings.props['TestComponent#default'] = { foo: 'from-displayName' }; state.cmf.settings.props['TestComponent#props-id'] = { foo: 'from-props-componentId' }; state.cmf.settings.props['TestComponent#connect-id'] = { foo: 'from-connect-componentId' }; @@ -115,7 +115,7 @@ describe('cmfConnect', () => { }); it('should evaluate expression using all props', () => { const state = mock.store.state(); - state.cmf.components = fromJS({}); + state.cmf.components = {}; expression.register('hasModel', ({ payload }) => payload.model !== undefined); const props = getStateToProps({ ownProps: { @@ -130,7 +130,7 @@ describe('cmfConnect', () => { }); it('should pass view settings together with own props when calling mapStateToProps', () => { const state = mock.store.state(); - const mapStateToProps = jest.fn(); + const mapStateToProps = vi.fn(); const ownProps = { view: 'simple' }; getStateToProps({ state, @@ -161,7 +161,7 @@ describe('cmfConnect', () => { const stateProps = { id: 'stateProps', stateProps: true }; const dispatchProps = { id: 'dispatchProps', dispatchProps: true }; const ownProps = { id: 'ownProps', ownProps: true }; - const mergeProps = jest.fn(); + const mergeProps = vi.fn(); getMergeProps({ mergeProps, stateProps, @@ -174,8 +174,8 @@ describe('cmfConnect', () => { describe('#getDispatchToProps', () => { it('should call getStateAccessors', () => { - const dispatch = jest.fn(); - const mapDispatchToProps = jest.fn(); + const dispatch = vi.fn(); + const mapDispatchToProps = vi.fn(); const ownProps = {}; const props = getDispatchToProps({ componentId: 'testId', @@ -213,7 +213,7 @@ describe('cmfConnect', () => { Button.displayName = 'Button'; const CMFConnectedButton = cmfConnect({})(Button); it('should create a connected component even without params', () => { - const TestComponent = jest.fn(); + const TestComponent = vi.fn(); TestComponent.displayName = 'TestComponent'; mapStateToViewProps.cache.clear(); const CMFConnected = cmfConnect()(TestComponent); @@ -222,7 +222,7 @@ describe('cmfConnect', () => { }); it('should create a connected component', () => { - const TestComponent = jest.fn(props =>
); + const TestComponent = vi.fn(props =>
); TestComponent.displayName = 'TestComponent'; mapStateToViewProps.cache.clear(); const CMFConnected = cmfConnect({})(TestComponent); @@ -239,18 +239,18 @@ describe('cmfConnect', () => { it('should expose getState static function to get the state', () => { expect(typeof CMFConnectedButton.getState).toBe('function'); const state = mock.store.state(); - state.cmf.components = fromJS({ + state.cmf.components = { Button: { default: { foo: 'bar' }, other: { foo: 'baz' }, }, - }); - expect(CMFConnectedButton.getState(state).get('foo')).toBe('bar'); - expect(CMFConnectedButton.getState(state, 'other').get('foo')).toBe('baz'); + }; + expect(CMFConnectedButton.getState(state).foo).toBe('bar'); + expect(CMFConnectedButton.getState(state, 'other').foo).toBe('baz'); }); it('should expose setStateAction static function to get the redux action to setState', () => { expect(typeof CMFConnectedButton.setStateAction).toBe('function'); - const state = new Map({ foo: 'bar' }); + const state = { foo: 'bar' }; let action = CMFConnectedButton.setStateAction(state); expect(action).toEqual({ type: 'Button.setState', @@ -271,15 +271,16 @@ describe('cmfConnect', () => { it('should expose setStateAction static function to get the redux action to setState', () => { expect(typeof CMFConnectedButton.setStateAction).toBe('function'); const state = mock.store.state(); - state.cmf.components = fromJS({ + state.cmf.components = { Button: { default: { foo: 'foo' }, other: { foo: 'baz' }, }, - }); - let actionCreator = CMFConnectedButton.setStateAction(prevState => - prevState.set('foo', 'bar'), - ); + }; + let actionCreator = CMFConnectedButton.setStateAction(prevState => ({ + ...prevState, + foo: 'bar', + })); expect(typeof actionCreator).toBe('function'); let action = actionCreator(null, () => state); expect(action).toMatchObject({ @@ -292,16 +293,16 @@ describe('cmfConnect', () => { }, }, }); - expect(action.cmf.componentState.componentState.get('foo')).toBe('bar'); + expect(action.cmf.componentState.componentState.foo).toBe('bar'); actionCreator = CMFConnectedButton.setStateAction( - prevState => prevState.set('foo', 'baz'), + prevState => ({ ...prevState, foo: 'baz' }), 'other', 'MY_ACTION', ); action = actionCreator(null, () => state); expect(action.type).toBe('MY_ACTION'); expect(action.cmf.componentState.key).toBe('other'); - expect(action.cmf.componentState.componentState.get('foo')).toBe('baz'); + expect(action.cmf.componentState.componentState.foo).toBe('baz'); }); it('should support no context in dispatchActionCreator', () => { const event = {}; @@ -326,8 +327,8 @@ describe('cmfConnect', () => { withDispatchActionCreator: true, })(TestComponent); const props = { - dispatchActionCreator: jest.fn(), - deleteState: jest.fn(), + dispatchActionCreator: vi.fn(), + deleteState: vi.fn(), }; const context = mock.store.context(); render( @@ -346,12 +347,12 @@ describe('cmfConnect', () => { }); it('should pass defaultState when there is no component state in store', () => { - const TestComponent = props => ; + const TestComponent = props => ; TestComponent.displayName = 'MyComponentWithoutStateInStore'; TestComponent.propTypes = { state: PropTypes.any, }; - const defaultState = new Map({ toto: 'lol' }); + const defaultState = { toto: 'lol' }; const CMFConnected = cmfConnect({ defaultState })(TestComponent); render( @@ -363,15 +364,15 @@ describe('cmfConnect', () => { }); it('should componentDidMount initState and dispatchActionCreator after the saga', () => { - const TestComponent = jest.fn(() => null); + const TestComponent = vi.fn(() => null); TestComponent.displayName = 'TestComponent'; - const STATE = new Map(); + const STATE = {}; const CMFConnected = cmfConnect({})(TestComponent); const props = { didMountActionCreator: 'hello', - deleteState: jest.fn(), - dispatchActionCreator: jest.fn(), - initState: jest.fn(), + deleteState: vi.fn(), + dispatchActionCreator: vi.fn(), + initState: vi.fn(), initialState: STATE, foo: 'bar', saga: 'saga', @@ -401,13 +402,13 @@ describe('cmfConnect', () => { }); it('should componentDidMount support saga', () => { - const TestComponent = jest.fn(() => null); + const TestComponent = vi.fn(() => null); TestComponent.displayName = 'TestComponent'; const CMFConnected = cmfConnect({})(TestComponent); const props = { saga: 'hello', - dispatchActionCreator: jest.fn(), - deleteState: jest.fn(), + dispatchActionCreator: vi.fn(), + deleteState: vi.fn(), }; const context = mock.store.context(); render( @@ -430,13 +431,13 @@ describe('cmfConnect', () => { }); it('should componentWillUnmount support saga', () => { - const TestComponent = jest.fn(() => null); + const TestComponent = vi.fn(() => null); TestComponent.displayName = 'TestComponent'; const CMFConnected = cmfConnect({})(TestComponent); const props = { saga: 'hello', - dispatchActionCreator: jest.fn(), - deleteState: jest.fn(), + dispatchActionCreator: vi.fn(), + deleteState: vi.fn(), }; const context = mock.store.context(); const { unmount } = render( @@ -457,18 +458,18 @@ describe('cmfConnect', () => { }); it('should componentWillUnMount dispatchActionCreator', () => { - const TestComponent = jest.fn(() => null); + const TestComponent = vi.fn(() => null); TestComponent.displayName = 'TestComponent'; const CMFConnected = cmfConnect({})(TestComponent); const props = { willUnmountActionCreator: 'bye', - dispatchActionCreator: jest.fn(), - deleteState: jest.fn(), + dispatchActionCreator: vi.fn(), + deleteState: vi.fn(), foo: 'bar', }; const context = mock.store.context(); context.registry = { - 'actionCreator:bye': jest.fn(), + 'actionCreator:bye': vi.fn(), }; const { unmount } = render( @@ -493,12 +494,12 @@ describe('cmfConnect', () => { const TestComponent = () =>
; TestComponent.displayName = 'TestComponent'; const CMFConnected = cmfConnect({ - defaultState: new Map(), + defaultState: {}, })(TestComponent); expect(CMFConnected.displayName).toBe('Connect(CMF(TestComponent))'); expect(CMFConnected.WrappedComponent).toBe(TestComponent); const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); const { unmount } = render( @@ -519,13 +520,13 @@ describe('cmfConnect', () => { const TestComponent = () =>
; TestComponent.displayName = 'TestComponent'; const CMFConnected = cmfConnect({ - defaultState: new Map(), + defaultState: {}, keepComponentState: true, })(TestComponent); expect(CMFConnected.displayName).toBe('Connect(CMF(TestComponent))'); expect(CMFConnected.WrappedComponent).toBe(TestComponent); const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); const { unmount } = render( @@ -551,7 +552,7 @@ describe('cmfConnect', () => { expect(CMFConnected.displayName).toBe('Connect(CMF(TestComponent))'); expect(CMFConnected.WrappedComponent).toBe(TestComponent); const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); const { unmount } = render( @@ -577,7 +578,7 @@ describe('cmfConnect', () => { expect(CMFConnected.displayName).toBe('Connect(CMF(TestComponent))'); expect(CMFConnected.WrappedComponent).toBe(TestComponent); const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); const { unmount } = render( @@ -606,7 +607,7 @@ describe('cmfConnect', () => { TestComponent.displayName = 'TestComponent'; const CMFConnected = cmfConnect({})(TestComponent); const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); render( @@ -648,7 +649,7 @@ describe('cmfConnect', () => { type: 'MY_BUTTON_CLICKED', }; const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); render( @@ -666,7 +667,7 @@ describe('cmfConnect', () => { it('should transform onEventActionCreator props to onEvent handler', () => { const onClickActionCreator = 'myactionCreator'; const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); context.registry = { 'actionCreator:myactionCreator': event => ({ type: 'FETCH_STUFF', event }), }; @@ -693,7 +694,7 @@ describe('cmfConnect', () => { }, }; const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); context.registry = { 'actionCreator:myfetch': (event, data) => ({ type: 'FETCH_CONFIGURED', @@ -721,7 +722,7 @@ describe('cmfConnect', () => { disabled: true, }; const context = mock.store.context(); - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); render( @@ -752,21 +753,21 @@ describe('cmfConnect', () => { return { cmf: { ...state.cmf, - components: fromJS({ + components: { Button: { default: { inProgress: false, }, }, - }), + }, }, }; }; - context.store.dispatch = jest.fn(); + context.store.dispatch = vi.fn(); render( - + , ); const btn = screen.getByRole('button'); diff --git a/packages/cmf/__tests__/cmfModule.merge.test.jsx b/packages/cmf/__tests__/cmfModule.merge.test.jsx index 282082ec28d..9a62d191e8d 100644 --- a/packages/cmf/__tests__/cmfModule.merge.test.jsx +++ b/packages/cmf/__tests__/cmfModule.merge.test.jsx @@ -1,7 +1,7 @@ /* eslint-disable no-empty-function */ /* eslint-disable react/prop-types */ import { render, screen } from '@testing-library/react'; - +import { vi } from 'vitest'; import mergeModules from '../src/cmfModule.merge'; describe('mergeModule', () => { @@ -11,8 +11,8 @@ describe('mergeModule', () => { // eslint-disable-next-line no-console global.console = { ...originalLog, - warn: jest.fn(), - log: jest.fn(), + warn: vi.fn(), + log: vi.fn(), }; }); afterEach(() => { @@ -65,8 +65,8 @@ describe('mergeModule', () => { ttt: function* foo() {}, }, preReducer: [], - enhancer: jest.fn(), - saga: jest.fn(), + enhancer: vi.fn(), + saga: vi.fn(), }; const b = { appId: undefined, @@ -135,8 +135,8 @@ describe('mergeModule', () => { }); it('should merge enhancer functions', () => { - const fn1 = jest.fn(); - const fn2 = jest.fn(); + const fn1 = vi.fn(); + const fn2 = vi.fn(); const config = mergeModules({ enhancer: fn1 }, { enhancer: fn2 }); expect(typeof config.enhancer).toBe('function'); config.enhancer('foo'); @@ -145,8 +145,8 @@ describe('mergeModule', () => { }); it('should merge saga', () => { - const fn1 = jest.fn(); - const fn2 = jest.fn(); + const fn1 = vi.fn(); + const fn2 = vi.fn(); const config = mergeModules({ saga: fn1 }, { saga: fn2 }); expect(typeof config.saga).toBe('function'); expect(config.saga).not.toBe(fn1); @@ -154,8 +154,8 @@ describe('mergeModule', () => { }); it('should merge middlewares', () => { - const mid1 = [jest.fn()]; - const mid2 = [jest.fn()]; + const mid1 = [vi.fn()]; + const mid2 = [vi.fn()]; const config = mergeModules({ middlewares: mid1 }, { middlewares: mid2 }); expect(config.middlewares.length).toBe(2); expect(config.middlewares[0]).toBe(mid1[0]); @@ -163,8 +163,8 @@ describe('mergeModule', () => { }); it('should merge storeCallback fns', () => { - const storeCallback1 = jest.fn(); - const storeCallback2 = jest.fn(); + const storeCallback1 = vi.fn(); + const storeCallback2 = vi.fn(); const config = mergeModules( { storeCallback: storeCallback1 }, { storeCallback: storeCallback2 }, @@ -176,8 +176,8 @@ describe('mergeModule', () => { }); it('should merge reducer', () => { - const ob1 = { foo: jest.fn(), composed: { composed1: jest.fn() } }; - const ob2 = { bar: jest.fn(), composed: { composed2: jest.fn() } }; + const ob1 = { foo: vi.fn(), composed: { composed1: vi.fn() } }; + const ob2 = { bar: vi.fn(), composed: { composed2: vi.fn() } }; const config = mergeModules({ reducer: ob1 }, { reducer: ob2 }); expect(typeof config.reducer).toBe('object'); @@ -190,10 +190,10 @@ describe('mergeModule', () => { }); it('should merge preReducer', () => { - const fn1 = jest.fn(); - const fn2 = jest.fn(); - const array1 = [jest.fn()]; - const array2 = [jest.fn()]; + const fn1 = vi.fn(); + const fn2 = vi.fn(); + const array1 = [vi.fn()]; + const array2 = [vi.fn()]; let config = mergeModules({ preReducer: fn1 }, { preReducer: fn2 }); expect(Array.isArray(config.preReducer)).toBe(true); @@ -221,8 +221,8 @@ describe('mergeModule', () => { }); it('should merge httpInterceptors', () => { - const fn1 = jest.fn(); - const fn2 = jest.fn(); + const fn1 = vi.fn(); + const fn2 = vi.fn(); const config = mergeModules({ httpInterceptors: [fn1] }, { httpInterceptors: [fn2] }); expect(Array.isArray(config.httpInterceptors)).toBeTruthy(); expect(config.httpInterceptors.length).toBe(2); diff --git a/packages/cmf/__tests__/cmfModule.test.js b/packages/cmf/__tests__/cmfModule.test.js index d57affec71a..9eddcf4d5d8 100644 --- a/packages/cmf/__tests__/cmfModule.test.js +++ b/packages/cmf/__tests__/cmfModule.test.js @@ -1,10 +1,11 @@ +import { vi } from 'vitest'; import mergeModulesAndApp from '../src/cmfModule'; describe('cmfModule', () => { it('should merge modules', async () => { - const fooModule = { id: 'foo', components: { foo: jest.fn() } }; - const barModule = { id: 'bar', components: { bar: jest.fn() } }; - const bazModule = { id: 'baz', components: { baz: jest.fn() } }; + const fooModule = { id: 'foo', components: { foo: vi.fn() } }; + const barModule = { id: 'bar', components: { bar: vi.fn() } }; + const bazModule = { id: 'baz', components: { baz: vi.fn() } }; const withModule = { id: 'with', modules: [barModule] }; const config = await mergeModulesAndApp({ modules: [fooModule, bazModule, withModule], @@ -14,7 +15,7 @@ describe('cmfModule', () => { expect(config.components.baz).toBe(bazModule.components.baz); }); it('should throw if module has no id', async () => { - const fooModule = { components: { foo: jest.fn() } }; + const fooModule = { components: { foo: vi.fn() } }; try { await mergeModulesAndApp({ modules: [fooModule], diff --git a/packages/cmf/__tests__/componentState.test.js b/packages/cmf/__tests__/componentState.test.js index 04609ddee19..124de0e34ca 100644 --- a/packages/cmf/__tests__/componentState.test.js +++ b/packages/cmf/__tests__/componentState.test.js @@ -1,5 +1,5 @@ +import { vi } from 'vitest'; import PropTypes from 'prop-types'; -import Immutable, { Map } from 'immutable'; import state, { getStateAccessors, @@ -17,8 +17,8 @@ describe('state', () => { }); it('should getStateAccessors should support no DEFAULT_STATE', () => { - const dispatch = jest.fn(); - const props = getStateAccessors(dispatch, 'name', 'id', new Map()); + const dispatch = vi.fn(); + const props = getStateAccessors(dispatch, 'name', 'id', {}); expect(typeof props.setState).toBe('function'); props.setState(); @@ -30,8 +30,8 @@ describe('state', () => { }); it('should getStateAccessors return accessors', () => { - const dispatch = jest.fn(); - const DEFAULT_STATE = new Map({ foo: 'bar' }); + const dispatch = vi.fn(); + const DEFAULT_STATE = { foo: 'bar' }; const props = getStateAccessors(dispatch, 'name', 'id', DEFAULT_STATE); expect(typeof props.setState).toBe('function'); expect(typeof props.initState).toBe('function'); @@ -54,9 +54,9 @@ describe('state', () => { it(`should call state if state is a function, via applyCallback`, () => { - const dispatch = jest.fn(); - const callBack = jest.fn(); - const DEFAULT_STATE = new Map({ foo: 'bar' }); + const dispatch = vi.fn(); + const callBack = vi.fn(); + const DEFAULT_STATE = { foo: 'bar' }; const props = getStateAccessors(dispatch, 'name', 'id', DEFAULT_STATE); props.setState(callBack); @@ -67,22 +67,22 @@ describe('state', () => { it('should getStateProps return state', () => { const storeState = { cmf: { - components: Immutable.fromJS({ + components: { foo: { bar: { open: true, }, }, - }), + }, }, }; const props = getStateProps(storeState, 'foo', 'bar'); - expect(props.state.get('open')).toBe(true); + expect(props.state.open).toBe(true); }); it('should initState call props.initState with initialState', () => { const props = { - initState: jest.fn(), + initState: vi.fn(), state: undefined, initialState: undefined, }; @@ -90,7 +90,7 @@ describe('state', () => { expect(props.initState.mock.calls.length).toBe(1); expect(props.initState.mock.calls[0][0]).toBe(); - props.initialState = new Map({ foo: 'bar' }); + props.initialState = { foo: 'bar' }; initState(props); expect(props.initState.mock.calls[1][0]).toBe(props.initialState); }); diff --git a/packages/cmf/__tests__/expression.test.js b/packages/cmf/__tests__/expression.test.js index 4ab0671bc31..afa6409505f 100644 --- a/packages/cmf/__tests__/expression.test.js +++ b/packages/cmf/__tests__/expression.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import cmf, { mock } from '../src'; @@ -9,7 +10,7 @@ describe('expression', () => { }); it('should register in registry', () => { - const test = jest.fn(); + const test = vi.fn(); const context = { registry: {}, }; @@ -18,7 +19,7 @@ describe('expression', () => { }); it('should get from registry', () => { - const test = jest.fn(); + const test = vi.fn(); const context = { registry: { 'expression:test': test, @@ -28,7 +29,7 @@ describe('expression', () => { }); it('should call with simple string (no args)', () => { - const test = jest.fn(); + const test = vi.fn(); const context = { registry: { 'expression:test': test, @@ -39,7 +40,7 @@ describe('expression', () => { }); it('should call with object (args)', () => { - const test = jest.fn(); + const test = vi.fn(); const context = { registry: { 'expression:test': test, @@ -131,7 +132,7 @@ describe('getProps', () => { describe('mapStateToProps', () => { it('should check first level props keys and call expression on it', () => { - const isCalled = jest.fn(() => true); + const isCalled = vi.fn(() => true); // eslint-disable-next-line import/no-named-as-default-member const registry = cmf.registry.getRegistry(); registry['expression:isCalled'] = isCalled; diff --git a/packages/cmf/__tests__/expressions/index.test.js b/packages/cmf/__tests__/expressions/index.test.js index f13a854920d..aa274999e8e 100644 --- a/packages/cmf/__tests__/expressions/index.test.js +++ b/packages/cmf/__tests__/expressions/index.test.js @@ -1,4 +1,3 @@ -import Immutable from 'immutable'; import expressions from '../../src/expressions'; import { mock } from '../../src'; @@ -13,11 +12,11 @@ describe('expressions', () => { it('should get collection content', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'my title', - }), - }); + }, + }; context.store.getState = () => state; expect(expressions['cmf.collections.get']({ context }, 'article.title', 'no title')).toBe( 'my title', @@ -27,7 +26,7 @@ describe('expressions', () => { const context = mock.store.context(); const state = mock.store.state(); context.store.getState = () => state; - state.cmf.collections = new Immutable.Map({}); + state.cmf.collections = {}; expect(expressions['cmf.collections.get']({ context }, 'article.title', 'no title')).toBe( 'no title', ); @@ -38,12 +37,12 @@ describe('expressions', () => { it('should return true if the value is present in the list', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'title', - tags: new Immutable.List(['test', 'test2', 'test3']), - }), - }); + tags: ['test', 'test2', 'test3'], + }, + }; context.store.getState = () => state; expect(expressions['cmf.collections.includes']({ context }, 'article.tags', 'test2')).toBe( true, @@ -52,12 +51,12 @@ describe('expressions', () => { it('should return false if the value is not present in the list', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'title', - tags: new Immutable.List(['test', 'test2', 'test3']), - }), - }); + tags: ['test', 'test2', 'test3'], + }, + }; context.store.getState = () => state; expect(expressions['cmf.collections.includes']({ context }, 'article.tags', 'test4')).toBe( false, @@ -67,7 +66,7 @@ describe('expressions', () => { const context = mock.store.context(); const state = mock.store.state(); context.store.getState = () => state; - state.cmf.collections = new Immutable.Map({}); + state.cmf.collections = {}; expect(expressions['cmf.collections.includes']({ context }, 'article.tags', 'test')).toBe( false, ); @@ -77,12 +76,12 @@ describe('expressions', () => { it('should return true if one of the values is present in the list', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'title', - tags: new Immutable.List(['test', 'test2', 'test3']), - }), - }); + tags: ['test', 'test2', 'test3'], + }, + }; context.store.getState = () => state; expect( expressions['cmf.collections.oneOf']({ context }, 'article.tags', ['test2', 'test4']), @@ -91,12 +90,12 @@ describe('expressions', () => { it('should return false if all values are not present in the list', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'title', - tags: new Immutable.List(['test', 'test2', 'test3']), - }), - }); + tags: ['test', 'test2', 'test3'], + }, + }; context.store.getState = () => state; expect( expressions['cmf.collections.oneOf']({ context }, 'article.tags', ['test4', 'test5']), @@ -106,7 +105,7 @@ describe('expressions', () => { const context = mock.store.context(); const state = mock.store.state(); context.store.getState = () => state; - state.cmf.collections = new Immutable.Map({}); + state.cmf.collections = {}; expect( expressions['cmf.collections.oneOf']({ context }, 'article.tags', ['test0', 'test1']), ).toBe(false); @@ -115,12 +114,12 @@ describe('expressions', () => { const context = mock.store.context(); const state = mock.store.state(); context.store.getState = () => state; - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'title', - tags: new Immutable.List(['test', 'test2', 'test3']), - }), - }); + tags: ['test', 'test2', 'test3'], + }, + }; expect(() => expressions['cmf.collections.oneOf']({ context }, 'article.tags', 'test'), ).toThrow(/^You should pass an array of values to check if one of them is present$/); @@ -130,12 +129,12 @@ describe('expressions', () => { it('should return true if all of the values are present in the list', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'title', - tags: new Immutable.List(['test', 'test2', 'test3']), - }), - }); + tags: ['test', 'test2', 'test3'], + }, + }; context.store.getState = () => state; expect( expressions['cmf.collections.allOf']({ context }, 'article.tags', [ @@ -148,12 +147,12 @@ describe('expressions', () => { it('should return false if not all values are not present in the list', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'title', - tags: new Immutable.List(['test', 'test2', 'test3']), - }), - }); + tags: ['test', 'test2', 'test3'], + }, + }; context.store.getState = () => state; expect( expressions['cmf.collections.allOf']({ context }, 'article.tags', ['test2', 'test3']), @@ -163,7 +162,7 @@ describe('expressions', () => { const context = mock.store.context(); const state = mock.store.state(); context.store.getState = () => state; - state.cmf.collections = new Immutable.Map({}); + state.cmf.collections = {}; expect( expressions['cmf.collections.allOf']({ context }, 'article.tags', ['test0', 'test1']), ).toBe(false); @@ -172,12 +171,12 @@ describe('expressions', () => { const context = mock.store.context(); const state = mock.store.state(); context.store.getState = () => state; - state.cmf.collections = new Immutable.Map({ - article: new Immutable.Map({ + state.cmf.collections = { + article: { title: 'title', - tags: new Immutable.List(['test', 'test2', 'test3']), - }), - }); + tags: ['test', 'test2', 'test3'], + }, + }; expect(() => expressions['cmf.collections.allOf']({ context }, 'article.tags', 'test'), ).toThrow(/^You should pass an array of values to check if all of them are present$/); @@ -188,13 +187,13 @@ describe('expressions', () => { it('should get component state', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.components = new Immutable.Map({ - MyComponent: new Immutable.Map({ - default: new Immutable.Map({ + state.cmf.components = { + MyComponent: { + default: { show: true, - }), - }), - }); + }, + }, + }; context.store.getState = () => state; expect( expressions['cmf.components.get']({ context }, 'MyComponent.default.show', false), @@ -203,7 +202,7 @@ describe('expressions', () => { it('should return default value if no component state', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.components = new Immutable.Map({}); + state.cmf.components = {}; context.store.getState = () => state; expect( expressions['cmf.components.get']({ context }, 'MyComponent.default.show', false), @@ -215,14 +214,14 @@ describe('expressions', () => { it('should return true if the value is present in the list', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.components = new Immutable.Map({ - MyComponent: new Immutable.Map({ - default: new Immutable.Map({ - tags: new Immutable.List(['tag1', 'tag2', 'tag3']), + state.cmf.components = { + MyComponent: { + default: { + tags: ['tag1', 'tag2', 'tag3'], show: true, - }), - }), - }); + }, + }, + }; context.store.getState = () => state; expect( expressions['cmf.components.includes']({ context }, 'MyComponent.default.tags', 'tag1'), @@ -231,7 +230,7 @@ describe('expressions', () => { it('should return default false if there is no component state', () => { const context = mock.store.context(); const state = mock.store.state(); - state.cmf.components = new Immutable.Map({}); + state.cmf.components = {}; context.store.getState = () => state; expect( expressions['cmf.components.includes']({ context }, 'MyComponent.default.tags', 'tag1'), diff --git a/packages/cmf/__tests__/httpInterceptors.test.js b/packages/cmf/__tests__/httpInterceptors.test.js index 6552a7a0d2c..84e059ab52f 100644 --- a/packages/cmf/__tests__/httpInterceptors.test.js +++ b/packages/cmf/__tests__/httpInterceptors.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import interceptors from '../src/httpInterceptors'; /* eslint-disable no-underscore-dangle */ @@ -5,8 +6,8 @@ describe('interceptors', () => { let interceptor; beforeEach(() => { interceptor = { - request: jest.fn(config => Object.assign({ foo: 'foo' }, config)), - response: jest.fn(resp => Object.assign({ bar: 'bar' }, resp)), + request: vi.fn(config => Object.assign({ foo: 'foo' }, config)), + response: vi.fn(resp => Object.assign({ bar: 'bar' }, resp)), }; interceptors.push(interceptor); }); @@ -14,80 +15,73 @@ describe('interceptors', () => { interceptors._clear(); }); - it('should onRequest call interceptors every request', done => { - interceptors.onRequest({}).then(config => { + it('should onRequest call interceptors every request', () => { + return interceptors.onRequest({}).then(config => { expect(config.foo).toBe('foo'); - done(); }); }); - it('should onResponse in interceptors', done => { - interceptors.onResponse({}).then(response => { + it('should onResponse in interceptors', () => { + return interceptors.onResponse({}).then(response => { expect(response.bar).toBe('bar'); - done(); }); }); - it('should interceptor requestError be called if JS Error has been thrown', done => { + it('should interceptor requestError be called if JS Error has been thrown', () => { const error = new Error('ERROR fail in interceptor'); const failInterceptor = { request: () => { throw error; }, - requestError: jest.fn(e => e), + requestError: vi.fn(e => e), }; interceptors.push(failInterceptor); - interceptors.onRequest({}).finally(() => { + return interceptors.onRequest({}).finally(() => { expect(failInterceptor.requestError).toHaveBeenCalledWith(error); - done(); }); }); - it('should interceptor requestError be called if interceptor.request promise rejected', done => { + it('should interceptor requestError be called if interceptor.request promise rejected', () => { const msg = 'reject in interceptor'; const failInterceptor = { request: () => new Promise((resolve, reject) => { return reject(msg); }), - requestError: jest.fn((e, v) => v), + requestError: vi.fn((e, v) => v), }; interceptors.push(failInterceptor); - interceptors.onRequest({}).then(() => { + return interceptors.onRequest({}).then(() => { expect(failInterceptor.requestError).toHaveBeenCalledWith(msg); - done(); }); }); - it('should interceptor response be called onResponse', done => { + it('should interceptor response be called onResponse', () => { const res = { data: 'foo' }; - interceptors.onResponse(res).then(response => { + return interceptors.onResponse(res).then(response => { expect(interceptor.response).toHaveBeenCalledWith(res); expect(response.bar).toBe('bar'); expect(response.data).toBe('foo'); - done(); }); }); - it('should interceptor responseError be called if JS Error has been thrown', done => { + it('should interceptor responseError be called if JS Error has been thrown', () => { const error = new Error('ERROR fail in interceptor'); const failInterceptor = { response: () => { throw error; }, - responseError: jest.fn(e => e), + responseError: vi.fn(e => e), }; interceptors.push(failInterceptor); - interceptors.onResponse({}).finally(() => { + return interceptors.onResponse({}).finally(() => { expect(failInterceptor.responseError).toHaveBeenCalledWith(error); - done(); }); }); - it('should interceptor responseError be called if response reject', done => { + it('should interceptor responseError be called if response reject', () => { const msg = 'reject in interceptor response'; const failInterceptor = { response: () => new Promise((resolve, reject) => reject(msg)), - responseError: jest.fn((e, v) => v), + responseError: vi.fn((e, v) => v), }; interceptors.push(failInterceptor); - interceptors.onResponse({}).then(() => { + return interceptors.onResponse({}).then(() => { expect(failInterceptor.responseError).toHaveBeenCalledWith(msg); - done(); }); }); }); diff --git a/packages/cmf/__tests__/localStorage.test.js b/packages/cmf/__tests__/localStorage.test.js index 6c31efa4beb..02532382117 100644 --- a/packages/cmf/__tests__/localStorage.test.js +++ b/packages/cmf/__tests__/localStorage.test.js @@ -1,4 +1,4 @@ -import Immutable from 'immutable'; +import { vi } from 'vitest'; import localStorageAPI from '../src/localStorage'; const PATHS = [ @@ -12,21 +12,6 @@ const state = { app: { extra: true, }, - components: new Immutable.Map({ - Foo: new Immutable.Map({ - default: new Immutable.Map({ - foo: 'foo', - }), - }), - }), - collections: new Immutable.Map({ - data: new Immutable.Map({}), - }), - }, -}; - -const serializedState = JSON.stringify(Object.assign({}, state, { - cmf: { components: { Foo: { default: { @@ -38,13 +23,30 @@ const serializedState = JSON.stringify(Object.assign({}, state, { data: {}, }, }, -})); +}; + +const serializedState = JSON.stringify( + Object.assign({}, state, { + cmf: { + components: { + Foo: { + default: { + foo: 'foo', + }, + }, + }, + collections: { + data: {}, + }, + }, + }), +); const KEY = 'test-cmf-localStorage'; describe('reduxLocalStorage', () => { const realEventListener = window.addEventListener; beforeEach(() => { - window.addEventListener = jest.fn(); + window.addEventListener = vi.fn(); }); afterAll(() => { window.addEventListener = realEventListener; @@ -56,8 +58,8 @@ describe('reduxLocalStorage', () => { it('should getState return parsed state from localStorage', () => { localStorage.setItem(KEY, serializedState); const initialState = localStorageAPI.getState(KEY); - expect(initialState.cmf.components.getIn(['Foo', 'default', 'foo'])).toBe('foo'); - expect(initialState.cmf.collections.getIn(['data']).toJS()).toEqual({}); + expect(initialState.cmf.components?.Foo?.default?.foo).toBe('foo'); + expect(initialState.cmf.collections?.data).toEqual({}); localStorage.setItem(KEY, undefined); }); it('should getStoreCallback return a function', () => { diff --git a/packages/cmf/__tests__/middlewares/__snapshots__/http.test.js.snap b/packages/cmf/__tests__/middlewares/__snapshots__/http.test.js.snap index 3964c23495f..557f8e2b6d3 100644 --- a/packages/cmf/__tests__/middlewares/__snapshots__/http.test.js.snap +++ b/packages/cmf/__tests__/middlewares/__snapshots__/http.test.js.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`HTTPError should create a new instance 1`] = ` +exports[`HTTPError > should create a new instance 1`] = ` { "message": "Internal Server Error", "name": "HTTP 500", @@ -17,7 +17,7 @@ exports[`HTTPError should create a new instance 1`] = ` } `; -exports[`json function should reject if no json function on response 1`] = ` +exports[`json function > should reject if no json function on response 1`] = ` { "name": "HTTP 502", "stack": { @@ -29,7 +29,7 @@ exports[`json function should reject if no json function on response 1`] = ` } `; -exports[`json function should resolve if attr json is on response 1`] = ` +exports[`json function > should resolve if attr json is on response 1`] = ` { "data": { "foo": "bar", @@ -40,9 +40,9 @@ exports[`json function should resolve if attr json is on response 1`] = ` } `; -exports[`json function should resolve status HTTP_STATUS.NO_CONTENT but without json function 1`] = `{}`; +exports[`json function > should resolve status HTTP_STATUS.NO_CONTENT but without json function 1`] = `{}`; -exports[`status function should reject if status >= 300 1`] = ` +exports[`status function > should reject if status >= 300 1`] = ` { "name": "HTTP 500", "stack": { diff --git a/packages/cmf/__tests__/middlewares/cmf.test.js b/packages/cmf/__tests__/middlewares/cmf.test.js index ef7db027324..af1a9ca7931 100644 --- a/packages/cmf/__tests__/middlewares/cmf.test.js +++ b/packages/cmf/__tests__/middlewares/cmf.test.js @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import cmfMiddleware from '../../src/middlewares/cmf'; -import onError from '../../src/onError'; import CONSTANT from '../../src/constant'; -jest.mock('../../src/onError', () => ({ - addAction: jest.fn(), +vi.mock('../../src/onError', () => ({ + addAction: vi.fn(), })); describe('CMF middleware', () => { @@ -12,9 +12,9 @@ describe('CMF middleware', () => { let middleware; beforeEach(() => { store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - next = jest.fn(); + next = vi.fn(); middleware = cmfMiddleware(store)(next); }); it('should be a middleware', () => { diff --git a/packages/cmf/__tests__/middlewares/error.test.js b/packages/cmf/__tests__/middlewares/error.test.js index 3be2eac9979..0b364a8b181 100644 --- a/packages/cmf/__tests__/middlewares/error.test.js +++ b/packages/cmf/__tests__/middlewares/error.test.js @@ -1,19 +1,16 @@ -import getErrorMiddleware, { - URL_REQUIRED_MESSAGE, -} from '../../src/middlewares/error'; -import { - HTTP_METHODS, -} from '../../src/middlewares/http/constants'; +import { vi } from 'vitest'; +import getErrorMiddleware, { URL_REQUIRED_MESSAGE } from '../../src/middlewares/error'; +import { HTTP_METHODS } from '../../src/middlewares/http/constants'; describe('CMF error middleware getErrorMiddleware', () => { it('should return a middleware using slug', () => { const middlewareFactory = getErrorMiddleware('/api/errors'); expect(typeof middlewareFactory).toBe('function'); const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), state: {}, }; - const next = (action) => { + const next = action => { if (action.type === 'THROW') { throw new Error('message'); } diff --git a/packages/cmf/__tests__/middlewares/http.test.js b/packages/cmf/__tests__/middlewares/http.test.js index b4f6aa0b4d9..97ebb78d4b2 100644 --- a/packages/cmf/__tests__/middlewares/http.test.js +++ b/packages/cmf/__tests__/middlewares/http.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import http from '../../src/middlewares/http'; import { @@ -17,7 +18,8 @@ import { HTTP_METHODS, HTTP_STATUS } from '../../src/middlewares/http/constants' describe('CMF http middleware', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); + global.fetch = vi.fn((url, config) => Promise.resolve(config.response)); }); it('should be available from middlewares/http', () => { @@ -165,22 +167,22 @@ describe('CMF http middleware', () => { it('should httpMiddleware return function', () => { const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const middleware = httpMiddleware(store)(next); expect(typeof middleware).toBe('function'); }); - it('return a promise when is given an action', done => { + it('return a promise when is given an action', () => { function json() { return new Promise(resolve => resolve({ foo: 'bar' })); } const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const action = { url: 'foo', type: HTTP_METHODS.POST, @@ -213,24 +215,23 @@ describe('CMF http middleware', () => { url: 'foo', }; - newState.then(() => { + return newState.then(() => { expect(global.fetch.mock.calls[0]).toEqual(['foo', config]); expect(next.mock.calls.length).toBe(1); const newAction = next.mock.calls[0][0]; expect(newAction.response.foo).toBe('bar'); - done(); }); }); - it('pass FormData to the fetch function without tempering if given as action body', done => { + it('pass FormData to the fetch function without tempering if given as action body', () => { function json() { return new Promise(resolve => resolve({ foo: 'bar' })); } const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const formData = new FormData(); const action = { url: 'foo', @@ -266,20 +267,19 @@ describe('CMF http middleware', () => { url: 'foo', }; - newState.then(() => { + return newState.then(() => { expect(global.fetch.mock.calls[0]).toEqual(['foo', config]); expect(next.mock.calls.length).toBe(1); const newAction = next.mock.calls[0][0]; expect(newAction.response.foo).toBe('bar'); - done(); }); }); - it('should httpMiddleware handle response promise with error', done => { + it('should httpMiddleware handle response promise with error', () => { const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const action = { type: HTTP_METHODS.POST, body: { label: 'great test' }, @@ -299,7 +299,7 @@ describe('CMF http middleware', () => { const middleware = httpMiddleware()(store)(next); expect(typeof middleware).toBe('function'); const newState = middleware(action); - newState.then(() => { + return newState.then(() => { expect(store.dispatch.mock.calls.length).toBe(3); const errorHTTPAction = store.dispatch.mock.calls[2][0]; expect(errorHTTPAction.type).toBe('@@HTTP/ERRORS'); @@ -309,15 +309,14 @@ describe('CMF http middleware', () => { expect(errorHTTPAction.error.stack.messageObject).toEqual({ foo: 'bar', }); - done(); }); }); - it('should httpMiddleware handle response promise with error if the body is not a JSON', done => { + it('should httpMiddleware handle response promise with error if the body is not a JSON', () => { const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const action = { type: HTTP_METHODS.POST, body: { label: 'great test' }, @@ -337,25 +336,22 @@ describe('CMF http middleware', () => { const middleware = httpMiddleware()(store)(next); expect(typeof middleware).toBe('function'); const newState = middleware(action); - newState - .then(() => { - expect(store.dispatch.mock.calls.length).toBe(3); - const errorHTTPAction = store.dispatch.mock.calls[2][0]; - expect(errorHTTPAction.type).toBe('@@HTTP/ERRORS'); - expect(errorHTTPAction.error.stack.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR); - expect(errorHTTPAction.error.stack.statusText).toBe('Internal Server Error'); - expect(errorHTTPAction.error.stack.messageObject).toBe(undefined); - expect(errorHTTPAction.error.stack.response).toBe('invalid json'); - }) - .catch(error => console.error(error)) - .finally(done); + return newState.then(() => { + expect(store.dispatch.mock.calls.length).toBe(3); + const errorHTTPAction = store.dispatch.mock.calls[2][0]; + expect(errorHTTPAction.type).toBe('@@HTTP/ERRORS'); + expect(errorHTTPAction.error.stack.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR); + expect(errorHTTPAction.error.stack.statusText).toBe('Internal Server Error'); + expect(errorHTTPAction.error.stack.messageObject).toBe(undefined); + expect(errorHTTPAction.error.stack.response).toBe('invalid json'); + }); }); - it('should handle onError callback if this action property is a typeof function', done => { + it('should handle onError callback if this action property is a typeof function', () => { const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const action = { type: HTTP_METHODS.POST, body: { label: 'great test' }, @@ -378,11 +374,10 @@ describe('CMF http middleware', () => { const middleware = httpMiddleware()(store)(next); expect(typeof middleware).toBe('function'); const newState = middleware(action); - newState.then(() => { + return newState.then(() => { expect(store.dispatch.mock.calls.length).toBe(3); const errorCallbackAction = store.dispatch.mock.calls[2][0]; expect(errorCallbackAction.type).toBe('CUSTOM_ACTION'); - done(); }); }); }); @@ -454,10 +449,10 @@ describe('json function', () => { describe('httpMiddleware configuration', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); - it('should use its parameter for CSRF handling if a security configuration is given', done => { + it('should use its parameter for CSRF handling if a security configuration is given', () => { // given function json() { return new Promise(resolve => resolve({ foo: 'bar' })); @@ -470,9 +465,9 @@ describe('httpMiddleware configuration', () => { }; const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const action = { url: 'foo', type: HTTP_METHODS.POST, @@ -503,7 +498,7 @@ describe('httpMiddleware configuration', () => { // when const middleware = httpMiddleware(httpDefaultConfig)(store)(next); expect(typeof middleware).toBe('function'); - middleware(action).then(() => { + return middleware(action).then(() => { // then const firstCall = global.fetch.mock.calls[0]; const firstCallSecondParam = firstCall[1]; @@ -525,21 +520,20 @@ describe('httpMiddleware configuration', () => { expect(next.mock.calls.length).toBe(1); const newAction = next.mock.calls[0][0]; expect(newAction.response.foo).toBe('bar'); - done(); }); document.cookie = `cookieKey=${expectedCSRFKeyValue}; dwf_section_edit=True; Max-Age=0`; }); - it('should use defaults CSRF handling parameter if no security configuration is given', done => { + it('should use defaults CSRF handling parameter if no security configuration is given', () => { // given function json() { return new Promise(resolve => resolve({ foo: 'bar' })); } const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const action = { url: 'foo', type: HTTP_METHODS.POST, @@ -570,7 +564,7 @@ describe('httpMiddleware configuration', () => { // when const middleware = httpMiddleware()(store)(next); expect(typeof middleware).toBe('function'); - middleware(action).then(() => { + return middleware(action).then(() => { // then const firstCall = global.fetch.mock.calls[0]; const firstCallSecondParam = firstCall[1]; @@ -593,11 +587,10 @@ describe('httpMiddleware configuration', () => { const newAction = next.mock.calls[0][0]; expect(newAction.response.foo).toBe('bar'); document.cookie = ''; - done(); }); }); - it('should call interceptor at every levels', done => { + it('should call interceptor at every levels', () => { // given const response = { foo: 'bar' }; function json() { @@ -605,9 +598,9 @@ describe('httpMiddleware configuration', () => { } const store = { - dispatch: jest.fn(), + dispatch: vi.fn(), }; - const next = jest.fn(); + const next = vi.fn(); const action = { url: 'foo', type: HTTP_METHODS.POST, @@ -620,8 +613,8 @@ describe('httpMiddleware configuration', () => { }, }; const interceptor = { - request: jest.fn(config => config), - response: jest.fn(r => r), + request: vi.fn(config => config), + response: vi.fn(r => r), }; interceptors.push(interceptor); // when @@ -629,14 +622,13 @@ describe('httpMiddleware configuration', () => { const result = middleware(action); // then - result.then(() => { + return result.then(() => { expect(interceptor.request).toHaveBeenCalled(); const augmentedConfig = interceptor.request.mock.calls[0][0]; expect(augmentedConfig.url).toBe(action.url); expect(interceptor.response).toHaveBeenCalledWith({ data: response, headers: {} }); // eslint-disable-next-line no-underscore-dangle interceptors._clear(); - done(); }); }); }); diff --git a/packages/cmf/__tests__/onError.test.js b/packages/cmf/__tests__/onError.test.js index a0e5eec4114..c502d5cbd80 100644 --- a/packages/cmf/__tests__/onError.test.js +++ b/packages/cmf/__tests__/onError.test.js @@ -1,19 +1,20 @@ +import { vi } from 'vitest'; import onError from '../src/onError'; import { store as mock } from '../src/mock'; -window.addEventListener = jest.fn(); -window.removeEventListener = jest.fn(); +window.addEventListener = vi.fn(); +window.removeEventListener = vi.fn(); function activateSentry() { window.Sentry = { - captureException: jest.fn(), - configureScope: jest.fn(), - init: jest.fn(c => { + captureException: vi.fn(), + configureScope: vi.fn(), + init: vi.fn(c => { if (c.dsn === 'fail') { throw new Error('mock fail'); } }), - withScope: jest.fn(), + withScope: vi.fn(), }; } @@ -25,8 +26,8 @@ describe('onError', () => { state = { foo: { ok: 'should be kept', password: 'secret', keyUndefined: undefined } }; store = mock.store(state); window.addEventListener.mockClear(); - store.dispatch = jest.fn(); - console.error = jest.fn(); + store.dispatch = vi.fn(); + console.error = vi.fn(); config = { onError: { reportURL: '/api/v1/report', @@ -34,7 +35,7 @@ describe('onError', () => { }; process.env.NODE_ENV = 'production'; onError.bootstrap(config, store); - jest.clearAllMocks(); + vi.clearAllMocks(); }); afterEach(() => { @@ -73,7 +74,7 @@ describe('onError', () => { describe('middleware', () => { it('should let normal action happens', () => { const mid = onError.middleware(); - const next = jest.fn(); + const next = vi.fn(); const action = { type: 'TEST', }; @@ -99,7 +100,7 @@ describe('onError', () => { }); it('should add action in singleton', () => { const mid = onError.middleware(); - const next = jest.fn(); + const next = vi.fn(); const action = { type: 'FOO', sensitive: true, @@ -112,7 +113,7 @@ describe('onError', () => { it('should keep last 20 actions', () => { // eslint-disable-next-line no-plusplus const mid = onError.middleware(); - const next = jest.fn(); + const next = vi.fn(); for (let index = 0; index < 30; index++) { mid(next)({ type: `FOO ${index}`, password: 'secret' }); } @@ -123,7 +124,7 @@ describe('onError', () => { }); describe('createObjectURL', () => { it('should use window.URL.createObjectURL', () => { - window.URL.createObjectURL = jest.fn(); + window.URL.createObjectURL = vi.fn(); const error = new Error('sth bad 2'); onError.createObjectURL(error); expect(window.URL.createObjectURL).toHaveBeenCalled(); @@ -137,7 +138,7 @@ describe('onError', () => { describe('revokeObjectURL', () => { it('should use window.URL.revokeObjectURL', () => { const url = {}; - window.URL.revokeObjectURL = jest.fn(); + window.URL.revokeObjectURL = vi.fn(); onError.revokeObjectURL(url); expect(window.URL.revokeObjectURL).toHaveBeenCalledWith(url); }); @@ -162,7 +163,7 @@ describe('onError', () => { onError.bootstrap(config, store); const options = { tags: [{ key: 'tag', value: 'value' }] }; const error = new Error('foo'); - const setTag = jest.fn(); + const setTag = vi.fn(); onError.report(error, options); expect(window.Sentry.withScope).toHaveBeenCalled(); const onScope = window.Sentry.withScope.mock.calls[0][0]; diff --git a/packages/cmf/__tests__/onEvent.test.js b/packages/cmf/__tests__/onEvent.test.js index 4d7a0d3d769..bef6980c76e 100644 --- a/packages/cmf/__tests__/onEvent.test.js +++ b/packages/cmf/__tests__/onEvent.test.js @@ -1,4 +1,4 @@ -import Immutable from 'immutable'; +import { vi } from 'vitest'; import onEvent from '../src/onEvent'; describe('onEvent', () => { @@ -9,12 +9,12 @@ describe('onEvent', () => { beforeEach(() => { instance = { props: { - setState: jest.fn(), - state: new Immutable.Map({ docked: false }), + setState: vi.fn(), + state: { docked: false }, }, }; config = {}; - currentHandler = jest.fn(); + currentHandler = vi.fn(); }); it('should return a function', () => { const handler = onEvent.getOnEventSetStateHandler(instance, {}, config, currentHandler); diff --git a/packages/cmf/__tests__/reducers/collectionsReducers.test.js b/packages/cmf/__tests__/reducers/collectionsReducers.test.js index 09fda3399e9..74c2417dc6a 100644 --- a/packages/cmf/__tests__/reducers/collectionsReducers.test.js +++ b/packages/cmf/__tests__/reducers/collectionsReducers.test.js @@ -1,17 +1,25 @@ -import { Map, List, fromJS } from 'immutable'; - -import collectionsReducers, { defaultState, getId, getActionWithCollectionIdAsArray } from '../../src/reducers/collectionsReducers'; - -const initialState = defaultState.set('collection1', 'super data'); - -const listInitialState = defaultState.set( - 'collectionid', - new List().set(0, { id: 0, label: 'test data 0' }).set(1, { id: 1, label: 'test data 1' }) -); -const mapInitialState = defaultState.set( - 'collectionid', - new Map().set('test0', 'test data 0').set('test1', 'test data 1') -); +import collectionsReducers, { + defaultState, + getId, + getActionWithCollectionIdAsArray, +} from '../../src/reducers/collectionsReducers'; + +const initialState = { ...defaultState, collection1: 'super data' }; + +const listInitialState = { + ...defaultState, + collectionid: [ + { id: 0, label: 'test data 0' }, + { id: 1, label: 'test data 1' }, + ], +}; +const mapInitialState = { + ...defaultState, + collectionid: { + test0: 'test data 0', + test1: 'test data 1', + }, +}; describe('check collection management reducer', () => { it('should return state if no action passed', () => { @@ -19,97 +27,127 @@ describe('check collection management reducer', () => { }); it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly add data into store', () => { - expect(collectionsReducers(initialState, { - type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', - collectionId: ['collectionId'], - data: 'data can be anything', - })).toEqual(new Map() - .set('collection1', 'super data') - .set('collectionId', 'data can be anything')); + expect( + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: ['collectionId'], + data: 'data can be anything', + }), + ).toEqual({ + collection1: 'super data', + collectionId: 'data can be anything', + }); }); it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly replace data into store', () => { - expect(collectionsReducers(initialState, { - type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', - collectionId: ['collection1'], - data: 'data can be anything', - })).toEqual(new Map().set('collection1', 'data can be anything')); + expect( + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: ['collection1'], + data: 'data can be anything', + }), + ).toEqual({ collection1: 'data can be anything' }); }); it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly add nested collection into store', () => { - const initState = fromJS({ + const initState = { collection1: { data: 'data can be anything', }, - }); - const expectedResult = initState.setIn(['collection1', 'nestedCollection'], fromJS(['item 1', 'item 2'])); - expect(collectionsReducers(initState, { - type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', - collectionId: ['collection1', 'nestedCollection'], - data: ['item 1', 'item 2'], - })).toEqual(expectedResult); + }; + const expectedResult = { + collection1: { + data: 'data can be anything', + nestedCollection: ['item 1', 'item 2'], + }, + }; + expect( + collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: ['collection1', 'nestedCollection'], + data: ['item 1', 'item 2'], + }), + ).toEqual(expectedResult); }); it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly replace nested collection into store', () => { - const initState = fromJS({ + const initState = { collection1: { nestedCollection: 'data can be anything', }, - }); - const expectedResult = initState.setIn(['collection1', 'nestedCollection'], fromJS(['item 1', 'item 2'])); - expect(collectionsReducers(initState, { - type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', - collectionId: ['collection1', 'nestedCollection'], - data: ['item 1', 'item 2'], - })).toEqual(expectedResult); + }; + const expectedResult = { + collection1: { + nestedCollection: ['item 1', 'item 2'], + }, + }; + expect( + collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: ['collection1', 'nestedCollection'], + data: ['item 1', 'item 2'], + }), + ).toEqual(expectedResult); }); it('REACT_CMF.COLLECTION_REMOVE should properly remove collection from the store', () => { - expect(collectionsReducers(initialState, { - type: 'REACT_CMF.COLLECTION_REMOVE', - collectionId: ['collection1'], - })).toEqual(new Map()); + expect( + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_REMOVE', + collectionId: ['collection1'], + }), + ).toEqual({}); }); it('REACT_CMF.COLLECTION_REMOVE should properly remove nested collection from the store', () => { - const initState = fromJS({ + const initState = { collection: { nestedCollection: { list: ['item 1', 'item 2'], }, }, - }); - const expectedResult = initState.deleteIn(['collection', 'nestedCollection', 'list']); - expect(collectionsReducers(initState, { - type: 'REACT_CMF.COLLECTION_REMOVE', - collectionId: ['collection', 'nestedCollection', 'list'], - })).toEqual(expectedResult); + }; + const expectedResult = { + collection: { + nestedCollection: {}, + }, + }; + expect( + collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_REMOVE', + collectionId: ['collection', 'nestedCollection', 'list'], + }), + ).toEqual(expectedResult); }); - it('REACT_CMF.COLLECTION_REMOVE should throw when collection doesn\'t exist', () => { + it("REACT_CMF.COLLECTION_REMOVE should throw when collection doesn't exist", () => { expect(() => { collectionsReducers(initialState, { type: 'REACT_CMF.COLLECTION_REMOVE', collectionId: ['unknown collection'], }); - }).toThrowError('Can\'t remove collection unknown collection since it doesn\'t exist.'); + }).toThrowError("Can't remove collection unknown collection since it doesn't exist."); }); }); describe('REACT_CMF.COLLECTION_MUTATE', () => { - it('shouldn\'t mutate if id doesn\'t exist', () => { - expect(collectionsReducers(mapInitialState, { - type: 'REACT_CMF.COLLECTION_MUTATE', - id: ['wrongCollectionid'], - operation: {}, - })).toEqual(mapInitialState); + it("shouldn't mutate if id doesn't exist", () => { + expect( + collectionsReducers(mapInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['wrongCollectionid'], + operation: {}, + }), + ).toEqual(mapInitialState); }); - it('shouldn\'t mutate if no operations', () => { - expect(collectionsReducers(mapInitialState, { - type: 'REACT_CMF.COLLECTION_MUTATE', - id: ['collectionid'], - })).toEqual(mapInitialState); + it("shouldn't mutate if no operations", () => { + expect( + collectionsReducers(mapInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + }), + ).toEqual(mapInitialState); }); describe('#add', () => { @@ -121,7 +159,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { add: [{ id: 2, label: 'test data 2' }], }, }); - expect(nextState.get('collectionid').toJS()).toEqual([ + expect(nextState.collectionid).toEqual([ { id: 0, label: 'test data 0' }, { id: 1, label: 'test data 1' }, { id: 2, label: 'test data 2' }, @@ -129,15 +167,13 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { }); it('should insert elements to nested List properly', () => { - const initState = fromJS({ + const initState = { collection: { nestedCollection: { - list: [ - { id: 0, label: 'test data 0' }, - ], + list: [{ id: 0, label: 'test data 0' }], }, }, - }); + }; const nextState = collectionsReducers(initState, { type: 'REACT_CMF.COLLECTION_MUTATE', id: ['collection', 'nestedCollection', 'list'], @@ -148,7 +184,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { ], }, }); - expect(nextState.getIn(['collection', 'nestedCollection', 'list']).toJS()).toEqual([ + expect(nextState.collection.nestedCollection.list).toEqual([ { id: 0, label: 'test data 0' }, { id: 1, label: 'test data 1' }, { id: 2, label: 'test data 2' }, @@ -163,17 +199,17 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { add: [{ test2: 'test data 2' }], }, }); - expect(nextState).toEqual( - new Map().set('collectionid', fromJS({ + expect(nextState).toEqual({ + collectionid: { test0: 'test data 0', test1: 'test data 1', test2: 'test data 2', - })) - ); + }, + }); }); it('should insert elements to nested Map properly', () => { - const initState = fromJS({ + const initState = { collection: { nestedCollection: { obj: { @@ -182,7 +218,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { }, }, }, - }); + }; const nextState = collectionsReducers(initState, { type: 'REACT_CMF.COLLECTION_MUTATE', id: ['collection', 'nestedCollection', 'obj'], @@ -190,19 +226,17 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { add: [{ test2: 'test data 2' }], }, }); - expect(nextState).toEqual( - fromJS({ - collection: { - nestedCollection: { - obj: { - test0: 'test data 0', - test1: 'test data 1', - test2: 'test data 2', - }, + expect(nextState).toEqual({ + collection: { + nestedCollection: { + obj: { + test0: 'test data 0', + test1: 'test data 1', + test2: 'test data 2', }, }, - }), - ); + }, + }); }); }); @@ -215,13 +249,11 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { delete: [0], }, }); - expect(nextState.get('collectionid').toJS()).toEqual([ - { id: 1, label: 'test data 1' }, - ]); + expect(nextState.collectionid).toEqual([{ id: 1, label: 'test data 1' }]); }); it('should delete elements from nested List properly', () => { - const initState = fromJS({ + const initState = { collection: { nestedCollection: { list: [ @@ -231,7 +263,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { ], }, }, - }); + }; const nextState = collectionsReducers(initState, { type: 'REACT_CMF.COLLECTION_MUTATE', id: ['collection', 'nestedCollection', 'list'], @@ -239,9 +271,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { delete: [0, 1], }, }); - expect(nextState.getIn(['collection', 'nestedCollection', 'list']).toJS()).toEqual([ - { id: 2, label: 'test data 2' }, - ]); + expect(nextState.collection.nestedCollection.list).toEqual([{ id: 2, label: 'test data 2' }]); }); it('should delete elements from Map properly', () => { @@ -252,15 +282,15 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { delete: ['test0'], }, }); - expect(nextState).toEqual( - new Map().set('collectionid', fromJS({ + expect(nextState).toEqual({ + collectionid: { test1: 'test data 1', - })) - ); + }, + }); }); it('should delete elements from nested Map properly', () => { - const initState = fromJS({ + const initState = { collection: { nestedCollection: { obj: { @@ -270,7 +300,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { }, }, }, - }); + }; const nextState = collectionsReducers(initState, { type: 'REACT_CMF.COLLECTION_MUTATE', id: ['collection', 'nestedCollection', 'obj'], @@ -278,12 +308,12 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { delete: ['test0', 'test1'], }, }); - expect(nextState.getIn(['collection', 'nestedCollection', 'obj']).toJS()).toEqual({ + expect(nextState.collection.nestedCollection.obj).toEqual({ test2: 'test data 2', }); }); - it('should delete nothing when ids don\'t match in List', () => { + it("should delete nothing when ids don't match in List", () => { const nextState = collectionsReducers(listInitialState, { type: 'REACT_CMF.COLLECTION_MUTATE', id: ['collectionid'], @@ -294,7 +324,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { expect(nextState).toEqual(listInitialState); }); - it('should delete nothing when ids don\'t match in Map', () => { + it("should delete nothing when ids don't match in Map", () => { const nextState = collectionsReducers(mapInitialState, { type: 'REACT_CMF.COLLECTION_MUTATE', id: ['collectionid'], @@ -317,14 +347,14 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { }, }, }); - expect(nextState.get('collectionid').toJS()).toEqual([ + expect(nextState.collectionid).toEqual([ { id: 0, label: 'new test data 0' }, { id: 1, label: 'test data 1' }, ]); }); it('should update elements of nested List properly', () => { - const initState = fromJS({ + const initState = { collection: { nestedCollection: { list: [ @@ -334,7 +364,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { ], }, }, - }); + }; const nextState = collectionsReducers(initState, { type: 'REACT_CMF.COLLECTION_MUTATE', id: ['collection', 'nestedCollection', 'list'], @@ -345,7 +375,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { }, }, }); - expect(nextState.getIn(['collection', 'nestedCollection', 'list']).toJS()).toEqual([ + expect(nextState.collection.nestedCollection.list).toEqual([ { id: 0, label: 'new test data 0' }, { id: 1, label: 'new test data 1' }, { id: 2, label: 'test data 2' }, @@ -362,16 +392,16 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { }, }, }); - expect(nextState).toEqual( - new Map().set('collectionid', fromJS({ + expect(nextState).toEqual({ + collectionid: { test0: 'new test data 0', test1: 'test data 1', - })) - ); + }, + }); }); it('should update elements of nested Map properly', () => { - const initState = fromJS({ + const initState = { collection: { nestedCollection: { obj: { @@ -381,7 +411,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { }, }, }, - }); + }; const nextState = collectionsReducers(initState, { type: 'REACT_CMF.COLLECTION_MUTATE', id: ['collection', 'nestedCollection', 'obj'], @@ -392,7 +422,7 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { }, }, }); - expect(nextState.getIn(['collection', 'nestedCollection', 'obj']).toJS()).toEqual({ + expect(nextState.collection.nestedCollection.obj).toEqual({ test0: 'new test data 0', test1: 'new test data 1', test2: 'test data 2', @@ -403,28 +433,37 @@ describe('REACT_CMF.COLLECTION_MUTATE', () => { describe('should properly perform all operations if collectionId is string', () => { it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly add data into store', () => { - expect(collectionsReducers(initialState, { - type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', - collectionId: 'collectionId', - data: 'data can be anything', - })).toEqual(new Map() - .set('collection1', 'super data') - .set('collectionId', 'data can be anything')); + expect( + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: 'collectionId', + data: 'data can be anything', + }), + ).toEqual({ + collection1: 'super data', + collectionId: 'data can be anything', + }); }); it('REACT_CMF.COLLECTION_REMOVE should properly remove collection from the store', () => { - const initState = fromJS({ + const initState = { collection: { nestedCollection: { list: ['item 1', 'item 2'], }, }, - }); - const expectedResult = initState.deleteIn(['collection', 'nestedCollection', 'list']); - expect(collectionsReducers(initState, { - type: 'REACT_CMF.COLLECTION_REMOVE', - collectionId: 'collection.nestedCollection.list', - })).toEqual(expectedResult); + }; + const expectedResult = { + collection: { + nestedCollection: {}, + }, + }; + expect( + collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_REMOVE', + collectionId: 'collection.nestedCollection.list', + }), + ).toEqual(expectedResult); }); it('REACT_CMF.COLLECTION_MUTATE should mutate List properly', () => { @@ -435,7 +474,7 @@ describe('should properly perform all operations if collectionId is string', () add: [{ id: 2, label: 'test data 2' }], }, }); - expect(nextState.get('collectionid').toJS()).toEqual([ + expect(nextState.collectionid).toEqual([ { id: 0, label: 'test data 0' }, { id: 1, label: 'test data 1' }, { id: 2, label: 'test data 2' }, @@ -444,7 +483,7 @@ describe('should properly perform all operations if collectionId is string', () }); describe('getId', () => { - it('should return mutable element id', () => { + it('should return element id', () => { // given const element = { id: 'toto' }; @@ -454,17 +493,6 @@ describe('getId', () => { // then expect(id).toBe('toto'); }); - - it('should return immutable element id', () => { - // given - const element = fromJS({ id: 'toto' }); - - // when - const id = getId(element); - - // then - expect(id).toBe('toto'); - }); }); describe('getActionWithCollectionIdAsArray', () => { diff --git a/packages/cmf/__tests__/reducers/componentsReducers.test.js b/packages/cmf/__tests__/reducers/componentsReducers.test.js index 4a70df80df0..53af81f62e8 100644 --- a/packages/cmf/__tests__/reducers/componentsReducers.test.js +++ b/packages/cmf/__tests__/reducers/componentsReducers.test.js @@ -1,17 +1,16 @@ -import { Map } from 'immutable'; - +import { vi } from 'vitest'; import reducer, { defaultState } from '../../src/reducers/componentsReducers'; -global.console = { warn: jest.fn() }; +global.console = { warn: vi.fn() }; describe('check component management reducer', () => { - const initialState = defaultState.set( - 'component1', - new Map().set('key1', new Map().set('searchQuery', '')), - ); + const initialState = { + ...defaultState, + component1: { key1: { searchQuery: '' } }, + }; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it(`REACT_CMF.COMPONENT_ADD_STATE should properly add component/collection @@ -23,11 +22,10 @@ describe('check component management reducer', () => { key: 'key', initialComponentState: { searchQuery: 'data' }, }), - ).toEqual( - new Map() - .set('component1', new Map().set('key1', new Map().set('searchQuery', ''))) - .set('componentName', new Map().set('key', new Map().set('searchQuery', 'data'))), - ); + ).toEqual({ + component1: { key1: { searchQuery: '' } }, + componentName: { key: { searchQuery: 'data' } }, + }); }); it(`REACT_CMF.COMPONENT_ADD_STATE should properly add component/collection @@ -40,11 +38,10 @@ describe('check component management reducer', () => { key: 'key', initialComponentState: undefined, }), - ).toEqual( - new Map() - .set('component1', new Map().set('key1', new Map().set('searchQuery', ''))) - .set('componentName', new Map().set('key', new Map())), - ); + ).toEqual({ + component1: { key1: { searchQuery: '' } }, + componentName: { key: {} }, + }); }); it(`REACT_CMF.COMPONENT_ADD_STATE should properly add component/collection @@ -56,12 +53,9 @@ describe('check component management reducer', () => { key: 'key', initialComponentState: 'initialState', }), - ).toEqual( - new Map().set( - 'component1', - new Map().set('key1', new Map().set('searchQuery', '')).set('key', 'initialState'), - ), - ); + ).toEqual({ + component1: { key1: { searchQuery: '' }, key: 'initialState' }, + }); }); it('REACT_CMF.COMPONENT_ADD_STATE throw when a couple of componentName, key already exist', () => { @@ -86,9 +80,9 @@ describe('check component management reducer', () => { key: 'key1', componentState: { searchQuery: 'data' }, }), - ).toEqual( - new Map().set('component1', new Map().set('key1', new Map().set('searchQuery', 'data'))), - ); + ).toEqual({ + component1: { key1: { searchQuery: 'data' } }, + }); }); it(`REACT_CMF.COMPONENT_MERGE_STATE should throw when a couple of @@ -112,7 +106,7 @@ describe('check component management reducer', () => { componentName: 'component1', key: 'key1', }), - ).toEqual(new Map().set('component1', new Map())); + ).toEqual({ component1: {} }); }); it(`removeComponentState throw when a couple of componentName, collectionId doesn't exist`, () => { diff --git a/packages/cmf/__tests__/reducers/settingsReducers.test.js b/packages/cmf/__tests__/reducers/settingsReducers.test.js index 1113959ab90..751e781a9cb 100644 --- a/packages/cmf/__tests__/reducers/settingsReducers.test.js +++ b/packages/cmf/__tests__/reducers/settingsReducers.test.js @@ -1,9 +1,6 @@ +import { vi } from 'vitest'; import CONSTANT from '../../src/constant'; -import reducer, { - defaultState, - attachRef, - attachRefs, -} from '../../src/reducers/settingsReducers'; +import reducer, { defaultState, attachRef, attachRefs } from '../../src/reducers/settingsReducers'; /* eslint-disable no-console */ @@ -29,7 +26,7 @@ describe('settingsReducers.attachRef', () => { const shouldThrow = () => { attachRefs(ref, props); }; - expect(shouldThrow).toThrow(new Error('CMF/Settings: Reference \'myref\' not found')); + expect(shouldThrow).toThrow("CMF/Settings: Reference 'myref' not found"); }); it('should not do anything if obj parameter is not an object', () => { const testFunction = () => ''; @@ -44,9 +41,7 @@ describe('settingsReducers.attachRef', () => { it('should try to resolve _ref if obj is an object', () => { const ref = { stuff: 'res' }; - expect(attachRef(ref, { _ref: 'stuff' })).toEqual( - { 0: 'r', 1: 'e', 2: 's' } - ); + expect(attachRef(ref, { _ref: 'stuff' })).toEqual({ 0: 'r', 1: 'e', 2: 's' }); }); }); @@ -72,7 +67,7 @@ describe('CMF settinsReducers', () => { }); it('should understand REQUEST_KO on 404', () => { const oldError = console.error; - console.error = jest.fn(); + console.error = vi.fn(); const action = { type: CONSTANT.REQUEST_KO, error: { @@ -93,7 +88,7 @@ describe('CMF settinsReducers', () => { const state = reducer(undefined, action); expect(state).not.toBe(undefined); expect(state.initialized).toBe(false); - expect(console.error).toHaveBeenCalledWith('Settings can\'t be loaded Not Found', action.error); + expect(console.error).toHaveBeenCalledWith("Settings can't be loaded Not Found", action.error); console.error = oldError.bind(console); }); }); diff --git a/packages/cmf/__tests__/registry.test.js b/packages/cmf/__tests__/registry.test.js index 5d7969c217b..45918e7b258 100644 --- a/packages/cmf/__tests__/registry.test.js +++ b/packages/cmf/__tests__/registry.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; /* eslint no-underscore-dangle: ["error", {"allow": ["_registry", "_isLocked"] }] */ import registry from '../src/registry'; @@ -49,7 +50,7 @@ describe('CMF registry', () => { it('addToRegistry should warn that a registered item is overridden', () => { // given - console.warn = jest.fn(); + console.warn = vi.fn(); registry.addToRegistry('jso', 'value'); expect(console.warn).not.toHaveBeenCalled(); @@ -76,7 +77,7 @@ describe('CMF registry', () => { }); it('should test the registerMany function', () => { - const register = jest.fn(); + const register = vi.fn(); const registerMany = registry.getRegisterMany(register); const context = { registry: {} }; diff --git a/packages/cmf/__tests__/sagas/collection.test.js b/packages/cmf/__tests__/sagas/collection.test.js index 3cc85213091..4e77615fdb5 100644 --- a/packages/cmf/__tests__/sagas/collection.test.js +++ b/packages/cmf/__tests__/sagas/collection.test.js @@ -1,18 +1,15 @@ -import { delay, call, select } from 'redux-saga/effects'; -import Immutable from 'immutable'; +import { delay, select } from 'redux-saga/effects'; import selectors from '../../src/selectors'; -import { - waitFor, -} from '../../src/sagas/collection'; +import { waitFor } from '../../src/sagas/collection'; describe('waitFor', () => { it('should waitFor wait for a collection to exists', () => { const withoutCollection = { cmf: { - collections: new Immutable.Map({}), + collections: {}, }, }; - const withCollection = withoutCollection.cmf.collections.set('foo', new Immutable.Map({})); + const withCollection = { ...withoutCollection.cmf.collections, foo: {} }; const gen = waitFor('foo'); expect(gen.next().value).toEqual(select(selectors.collections.get, 'foo')); expect(gen.next().value).toEqual(delay(10)); diff --git a/packages/cmf/__tests__/sagas/http.test.js b/packages/cmf/__tests__/sagas/http.test.js index 461ec5052a2..d6a9ce242c7 100644 --- a/packages/cmf/__tests__/sagas/http.test.js +++ b/packages/cmf/__tests__/sagas/http.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Headers, Response } from 'node-fetch'; import { call, put } from 'redux-saga/effects'; @@ -19,7 +20,6 @@ import http, { HTTPError, httpFetch, httpGet, - httpHead, httpPatch, httpPost, httpPut, @@ -31,7 +31,7 @@ import http, { const CSRFToken = 'hNjmdpuRgQClwZnb2c59F9gZhCi8jv9x'; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('http.get', () => { @@ -170,19 +170,18 @@ describe('http.delete', () => { }); describe('handleBody', () => { - it('should manage the body of the response like text if no header', done => { - handleBody(new Response('{"foo": 42}', {})).then(({ data, response }) => { + it('should manage the body of the response like text if no header', () => { + return handleBody(new Response('{"foo": 42}', {})).then(({ data, response }) => { expect(data).toBe('{"foo": 42}'); expect(response instanceof Response).toBeTruthy(); - done(); }); }); - it('should manage the body of the response like a json', done => { + it('should manage the body of the response like a json', () => { const headers = new Headers(); headers.append('Content-Type', 'application/json'); - handleBody( + return handleBody( new Response('{"foo": 42}', { headers, }), @@ -191,52 +190,48 @@ describe('handleBody', () => { foo: 42, }); expect(response instanceof Response).toBe(true); - done(); }); }); - it('should manage the body of the response like a blob', done => { + it('should manage the body of the response like a blob', () => { const headers = new Headers(); headers.append('Content-Type', 'application/zip'); - const blob = jest.fn(() => Promise.resolve()); + const blob = vi.fn(() => Promise.resolve()); - handleBody({ blob, headers }).then(() => { + return handleBody({ blob, headers }).then(() => { expect(blob).toHaveBeenCalled(); - done(); }); }); - it('should manage the body of the response like a text', done => { + it('should manage the body of the response like a text', () => { const headers = new Headers(); headers.append('Content-Type', 'text/plain'); - handleBody( + return handleBody( new Response('foo', { headers, }), ).then(({ data, response }) => { expect(data).toBe('foo'); expect(response instanceof Response).toBe(true); - done(); }); }); - it('should manage the body of the response like a text by default', done => { - handleBody(new Response('')).then(({ data, response }) => { + it('should manage the body of the response like a text by default', () => { + return handleBody(new Response('')).then(({ data, response }) => { expect(data).toBe(''); expect(response instanceof Response).toBe(true); - done(); }); }); }); describe('#handleHttpResponse', () => { - it('should handle the response with 2xx code', done => { + it('should handle the response with 2xx code', () => { const headers = new Headers(); headers.append('Content-Type', 'application/json'); - handleHttpResponse( + return handleHttpResponse( new Response('{"foo": 42}', { status: HTTP_STATUS.OK, headers, @@ -246,39 +241,36 @@ describe('#handleHttpResponse', () => { foo: 42, }); expect(response instanceof Response).toBe(true); - done(); }); }); - it('should handle the response with a code different of 2xx', done => { + it('should handle the response with a code different of 2xx', () => { const headers = new Headers(); headers.append('Content-Type', 'application/json'); - handleHttpResponse( + return handleHttpResponse( new Response('{"foo": 42}', { status: HTTP_STATUS.FORBIDDEN, headers, }), ).catch(response => { expect(response instanceof Response).toBe(true); - done(); }); }); - it('should handle the response with NO_CONTENT code', done => { - handleHttpResponse( + it('should handle the response with NO_CONTENT code', () => { + return handleHttpResponse( new Response('', { status: HTTP_STATUS.NO_CONTENT, }), ).then(({ data, response }) => { expect(data).toBe(''); expect(response instanceof Response).toBe(true); - done(); }); }); - it('should handle the response for HEAD requests', done => { - handleHttpResponse( + it('should handle the response for HEAD requests', () => { + return handleHttpResponse( new Response('{"foo": 42}', { status: HTTP_STATUS.OK, }), @@ -286,17 +278,16 @@ describe('#handleHttpResponse', () => { ).then(({ data, response }) => { expect(data).toBe(''); expect(response instanceof Response).toBe(true); - done(); }); }); }); describe('#handleError', () => { - it('should manage the error', done => { + it('should manage the error', () => { const headers = new Headers(); headers.append('Content-Type', 'application/json'); - handleError( + return handleError( new Response('{"foo": 42}', { status: HTTP_STATUS.FORBIDDEN, statusText: 'Forbidden', @@ -308,7 +299,6 @@ describe('#handleError', () => { foo: 42, }); expect(error.response instanceof Response).toBe(true); - done(); }); }); }); @@ -872,6 +862,9 @@ it('should wrap the request and not notify with generic http error if silent opt }); describe('#httpFetch with CRSF token', () => { + beforeEach(() => { + global.fetch = vi.fn((url, config) => Promise.resolve(config.response)); + }); beforeAll(() => { document.cookie = `csrfToken=${CSRFToken}; dwf_section_edit=True;`; }); @@ -879,7 +872,7 @@ describe('#httpFetch with CRSF token', () => { afterAll(() => { document.cookie = `csrfToken=${CSRFToken}; dwf_section_edit=True; Max-Age=0`; }); - it('should get the CRFS token', done => { + it('should get the CRFS token', () => { const url = '/foo'; const headers = new Headers(); headers.append('Content-Type', 'application/json'); @@ -894,13 +887,7 @@ describe('#httpFetch with CRSF token', () => { bar: 42, }; - httpFetch(url, config, HTTP_METHODS.GET, payload).then(body => { - expect(body.data).toEqual({ - foo: 42, - }); - expect(body.response instanceof Response).toBe(true); - done(); - }); + const promise = httpFetch(url, config, HTTP_METHODS.GET, payload); expect(fetch).toHaveBeenCalledWith(url, { body: '{"bar":42}', @@ -913,6 +900,13 @@ describe('#httpFetch with CRSF token', () => { method: HTTP_METHODS.GET, response: config.response, }); + + return promise.then(body => { + expect(body.data).toEqual({ + foo: 42, + }); + expect(body.response instanceof Response).toBe(true); + }); }); }); @@ -924,6 +918,9 @@ describe('#httpFetch with CSRF handling configuration', () => { }, }; + beforeEach(() => { + global.fetch = vi.fn((url, config) => Promise.resolve(config.response)); + }); beforeAll(() => { HTTP.defaultConfig = null; @@ -936,7 +933,7 @@ describe('#httpFetch with CSRF handling configuration', () => { document.cookie = `${defaultHttpConfiguration.security.CSRFTokenCookieKey}=${CSRFToken}; dwf_section_edit=True; Max-Age=0`; }); - it('check if httpFetch is called with the security configuration', done => { + it('check if httpFetch is called with the security configuration', () => { setDefaultConfig(defaultHttpConfiguration); expect(getDefaultConfig()).toEqual(defaultHttpConfiguration); @@ -954,13 +951,7 @@ describe('#httpFetch with CSRF handling configuration', () => { bar: 42, }; - httpFetch(url, config, HTTP_METHODS.GET, payload).then(body => { - expect(body.data).toEqual({ - foo: 42, - }); - expect(body.response instanceof Response).toBe(true); - done(); - }); + const promise = httpFetch(url, config, HTTP_METHODS.GET, payload); expect(fetch).toHaveBeenCalledWith(url, { ...defaultHttpConfiguration, @@ -974,15 +965,25 @@ describe('#httpFetch with CSRF handling configuration', () => { method: HTTP_METHODS.GET, response: config.response, }); + + return promise.then(body => { + expect(body.data).toEqual({ + foo: 42, + }); + expect(body.response instanceof Response).toBe(true); + }); }); }); describe('#httpFetch', () => { + beforeEach(() => { + global.fetch = vi.fn((url, config) => Promise.resolve(config.response)); + }); afterEach(() => { HTTP.defaultConfig = null; }); - it('should fetch the request', done => { + it('should fetch the request', () => { const url = '/foo'; const headers = new Headers(); headers.append('Content-Type', 'application/json'); @@ -997,13 +998,7 @@ describe('#httpFetch', () => { bar: 42, }; - httpFetch(url, config, HTTP_METHODS.GET, payload).then(body => { - expect(body.data).toEqual({ - foo: 42, - }); - expect(body.response instanceof Response).toBe(true); - done(); - }); + const promise = httpFetch(url, config, HTTP_METHODS.GET, payload); expect(fetch).toHaveBeenCalledWith(url, { body: '{"bar":42}', @@ -1015,9 +1010,16 @@ describe('#httpFetch', () => { method: HTTP_METHODS.GET, response: config.response, }); + + return promise.then(body => { + expect(body.data).toEqual({ + foo: 42, + }); + expect(body.response instanceof Response).toBe(true); + }); }); - it('should fetch the request with the default settings', done => { + it('should fetch the request with the default settings', () => { const url = '/foo'; const headers = new Headers(); headers.append('Content-Type', 'application/json'); @@ -1038,13 +1040,7 @@ describe('#httpFetch', () => { }, }); - httpFetch(url, config, HTTP_METHODS.GET, payload).then(body => { - expect(body.data).toEqual({ - foo: 42, - }); - expect(body.response instanceof Response).toBe(true); - done(); - }); + const promise = httpFetch(url, config, HTTP_METHODS.GET, payload); expect(fetch).toHaveBeenCalledWith(url, { body: '{"bar":42}', @@ -1057,9 +1053,16 @@ describe('#httpFetch', () => { method: HTTP_METHODS.GET, response: config.response, }); + + return promise.then(body => { + expect(body.data).toEqual({ + foo: 42, + }); + expect(body.response instanceof Response).toBe(true); + }); }); - it('should fetch the request with a FormData', done => { + it('should fetch the request with a FormData', () => { const url = '/foo'; const headers = new Headers(); headers.append('Content-Type', 'application/json'); @@ -1072,13 +1075,7 @@ describe('#httpFetch', () => { }; const payload = new FormData(); - httpFetch(url, config, HTTP_METHODS.GET, payload).then(body => { - expect(body.data).toEqual({ - foo: 42, - }); - expect(body.response instanceof Response).toBe(true); - done(); - }); + const promise = httpFetch(url, config, HTTP_METHODS.GET, payload); expect(fetch).toHaveBeenCalledWith(url, { body: payload, @@ -1089,9 +1086,16 @@ describe('#httpFetch', () => { method: HTTP_METHODS.GET, response: config.response, }); + + return promise.then(body => { + expect(body.data).toEqual({ + foo: 42, + }); + expect(body.response instanceof Response).toBe(true); + }); }); - it('should fail the request', done => { + it('should fail the request', () => { const url = '/foo'; const headers = new Headers(); headers.append('Content-Type', 'application/json'); @@ -1106,14 +1110,7 @@ describe('#httpFetch', () => { bar: 42, }; - httpFetch(url, config, HTTP_METHODS.GET, payload).then(body => { - expect(body instanceof Error).toBe(true); - expect(body.data).toEqual({ - foo: 42, - }); - expect(body.response instanceof Response).toBe(true); - done(); - }); + const promise = httpFetch(url, config, HTTP_METHODS.GET, payload); expect(fetch).toHaveBeenCalledWith(url, { body: '{"bar":42}', @@ -1125,6 +1122,14 @@ describe('#httpFetch', () => { method: HTTP_METHODS.GET, response: config.response, }); + + return promise.then(body => { + expect(body instanceof Error).toBe(true); + expect(body.data).toEqual({ + foo: 42, + }); + expect(body.response instanceof Response).toBe(true); + }); }); }); @@ -1321,7 +1326,7 @@ describe('setDefaultLanguage', () => { it('should not redefine the Accept Language if no defaultConfig', () => { expect(() => { setDefaultLanguage('ja'); - }).toThrow(''); + }).toThrow(); }); it('should redefine the Accept Language', () => { diff --git a/packages/cmf/__tests__/sagas/putActionCreator.test.js b/packages/cmf/__tests__/sagas/putActionCreator.test.js index 92e3bbf53cd..08ee6c46415 100644 --- a/packages/cmf/__tests__/sagas/putActionCreator.test.js +++ b/packages/cmf/__tests__/sagas/putActionCreator.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { put, select } from 'redux-saga/effects'; import registry from '../../src/registry'; import putActionCreator from '../../src/sagas/putActionCreator'; @@ -6,7 +7,7 @@ describe('saga', () => { it('should putActionCreator call put of a registred actionCreator without context', () => { // given const testAction = { type: 'TEST' }; - const actionCreator = jest.fn(() => testAction); + const actionCreator = vi.fn(() => testAction); const reg = registry.getRegistry(); reg['actionCreator:myActionCreator'] = actionCreator; const data = { foo: 'bar' }; @@ -31,7 +32,7 @@ describe('saga', () => { it('should putActionCreator call put of a registred actionCreator or using context', () => { // given const testAction = { type: 'TEST' }; - const actionCreator = jest.fn(() => testAction); + const actionCreator = vi.fn(() => testAction); const context = { registry: { 'actionCreator:myActionCreator': actionCreator, diff --git a/packages/cmf/__tests__/selectors/collections.test.js b/packages/cmf/__tests__/selectors/collections.test.js deleted file mode 100644 index d2fdf5f0d2d..00000000000 --- a/packages/cmf/__tests__/selectors/collections.test.js +++ /dev/null @@ -1,120 +0,0 @@ -import { Map, List } from 'immutable'; -import selectors from '../../src/selectors'; - -describe('selectors.collections', () => { - describe('toJS', () => { - const state = { - cmf: { - collections: new Map({ - foo: new Map({ - bar: new Map({ - hello: 'world', - }), - }), - }), - }, - }; - const result1 = selectors.collections.toJS(state, 'foo.bar'); - const result2 = selectors.collections.toJS(state, 'foo.bar'); - expect(result1).toEqual({ hello: 'world' }); - expect(result1).toBe(result2); - }); - describe('get', () => { - const collection = new Map({ id: 'id' }); - const collectionSubset = new Map({ subset: 'subset' }); - const collectionWithSubset = new Map({ collectionSubset }); - const state = { - cmf: { - collections: new Map({ - collection, - collectionWithSubset, - }), - }, - }; - it('try to find the collection if collectionPath is a string', () => { - expect(selectors.collections.get(state, 'collection')).toEqual(collection); - }); - - it('try to find the collection subset if collectionPath is an array', () => { - expect( - selectors.collections.get(state, ['collectionWithSubset', 'collectionSubset']), - ).toEqual(collectionSubset); - }); - - it('throw an exception if collection path is neither a string or an array', () => { - expect(() => { - selectors.collections.get(state, {}); - }).toThrowError(`Type mismatch: collectionPath should be a string or an array of string -got [object Object]`); - }); - }); -}); - -const id = 'id'; -const item = new Map({ id }); -const collectionSubset = new List([item]); -const collectionWithSubset = new Map({ collectionSubset }); -const state = { - cmf: { - collections: new Map({ - isList: new List([item]), - isNotList: new Map({ id: item }), - collectionWithSubset, - }), - }, -}; - -describe('find(state, pathDescriptor, resourceId)', () => { - it.each([ - { - name: 'work if collection path is a string', - state, - pathDescriptor: 'isList', - resourceId: id, - result: item, - }, - { - name: 'work if collection path is a Array', - state, - pathDescriptor: ['collectionWithSubset', 'collectionSubset'], - resourceId: id, - result: item, - }, - { - name: "undefined if id doens't match", - state, - pathDescriptor: 'isList', - resourceId: 'notFound', - result: undefined, - }, - ])('$name', opts => { - expect( - selectors.collections.findListItem(opts.state, opts.pathDescriptor, opts.resourceId), - ).toBe(opts.result); - }); -}); - -describe('selectors.collections.findListItem(state, pathDescriptor, resourceId)', () => { - it.each([ - { - name: 'throw if collection path is not a List', - state, - pathDescriptor: 'isNotList', - resourceId: id, - result: `Type mismatch: isNotList does not resolve as an instance of Immutable.List, -got Map { "id": Map { "id": "id" } }`, - }, - { - name: "throw if collection can't be found", - state, - pathDescriptor: 'notFound', - resourceId: id, - result: `Type mismatch: notFound does not resolve as an instance of Immutable.List, -got undefined`, - }, - ])('$name', opts => { - expect(() => { - selectors.collections.findListItem(opts.state, opts.pathDescriptor, opts.resourceId); - }).toThrow(opts.result); - }); -}); diff --git a/packages/cmf/__tests__/selectors/index.test.js b/packages/cmf/__tests__/selectors/index.test.js deleted file mode 100644 index ad1bb8fb54c..00000000000 --- a/packages/cmf/__tests__/selectors/index.test.js +++ /dev/null @@ -1,13 +0,0 @@ -import selectors from '../../src/selectors'; -import * as collections from '../../src/selectors/collections'; -import toJS from '../../src/selectors/toJS'; - -describe('selectors', () => { - it('should expose collections selectors', () => { - expect(selectors.collections).toEqual(collections); - }); - it('should expose toJS selectors', () => { - expect(selectors.toJS).toEqual(toJS); - }); -}); - diff --git a/packages/cmf/__tests__/selectors/toJS.test.js b/packages/cmf/__tests__/selectors/toJS.test.js index 553be88fcef..902c154be90 100644 --- a/packages/cmf/__tests__/selectors/toJS.test.js +++ b/packages/cmf/__tests__/selectors/toJS.test.js @@ -1,18 +1,18 @@ -import Immutable from 'immutable'; +import { vi } from 'vitest'; import toJS from '../../src/selectors/toJS'; describe('toJS', () => { let selector; beforeEach(() => { - selector = jest.fn(state => state.foo); + selector = vi.fn(state => state.foo); }); it('should return a function', () => { expect(typeof toJS(selector)).toBe('function'); }); - it('the returned function should call toJS on the results', () => { + it('the returned function should return the result of the selector', () => { const myselector = toJS(selector); const state = { - foo: new Immutable.Map({ bar: 'bar' }), + foo: { bar: 'bar' }, }; const result = myselector(state); expect(result).toEqual({ bar: 'bar' }); @@ -20,38 +20,30 @@ describe('toJS', () => { it('the returned function should return same reference on multiple calls', () => { const myselector = toJS(selector); const state = { - foo: new Immutable.Map({ bar: 'bar' }), + foo: { bar: 'bar' }, }; const result1 = myselector(state); const result2 = myselector(state); expect(result1).toBe(result2); }); - it('the returned function should return a different result if store is has been modified', () => { + it('the returned function should return a different result if store has been modified', () => { const myselector = toJS(selector); const state = { - foo: new Immutable.Map({ bar: 'bar' }), + foo: { bar: 'bar' }, }; const result1 = myselector(state); - state.foo = state.foo.set('bar', 'baz'); + state.foo = { bar: 'baz' }; const result2 = myselector(state); expect(result1).not.toBe(result2); expect(result2.bar).toBe('baz'); }); - it('the returned function should throw an error if the selector return a not immutable data', () => { + it('the returned function should return undefined if the selector doesn t return data', () => { const myselector = toJS(selector); - const state = { - foo: { bar: 'bar' }, - }; - const toThrow = () => myselector(state); - expect(toThrow).toThrow(); + const state = {}; + expect(myselector(state)).toBeUndefined(); }); it('should throw if selector is not a function', () => { const toThrow = () => toJS({}); expect(toThrow).toThrow(); }); - it('the returned function should return undefined if the selector doesn t return data', () => { - const myselector = toJS(selector); - const state = {}; - expect(myselector(state)).toBeUndefined(); - }); }); diff --git a/packages/cmf/__tests__/store.test.js b/packages/cmf/__tests__/store.test.js index 6e32f4c2939..6747dfdfaa2 100644 --- a/packages/cmf/__tests__/store.test.js +++ b/packages/cmf/__tests__/store.test.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import store from '../src/store'; describe('CMF store', () => { @@ -43,7 +44,7 @@ describe('CMF store', () => { describe('addPreReducer', () => { it('should add a reducer called by the cmf reducer', () => { - const myreducer = jest.fn(); + const myreducer = vi.fn(); store.addPreReducer(myreducer); const reducer = store.getReducer(); reducer(undefined, {}); @@ -57,13 +58,13 @@ describe('getMiddlewares', () => { expect(Array.isArray(middlewares)).toBe(true); }); it('should support first attr as function', () => { - const fn = jest.fn(); + const fn = vi.fn(); const middlewares = store.getMiddlewares(fn); expect(middlewares).toContain(fn); }); it('should support first attr as array', () => { - const fn1 = jest.fn(); - const fn2 = jest.fn(); + const fn1 = vi.fn(); + const fn2 = vi.fn(); const middlewares = store.getMiddlewares([fn1, fn2]); expect(middlewares).toContain(fn1); expect(middlewares).toContain(fn2); diff --git a/packages/cmf/package.json b/packages/cmf/package.json index 2e12bd6de07..cfe13934099 100644 --- a/packages/cmf/package.json +++ b/packages/cmf/package.json @@ -46,13 +46,11 @@ "@talend/utils": "^3.7.1", "commander": "^6.2.1", "hoist-non-react-statics": "^3.3.2", - "immutable": "^3.8.2", "invariant": "^2.2.4", "lodash": "^4.17.23", "nested-combine-reducers": "^1.2.2", "path-to-regexp": "^8.3.0", "prop-types": "^15.8.1", - "react-immutable-proptypes": "^2.2.0", "react-redux": "^7.2.9", "redux": "^4.2.1", "redux-batched-actions": "^0.5.0", diff --git a/packages/cmf/src/cmfConnect.jsx b/packages/cmf/src/cmfConnect.jsx index 3950c99b989..7bb3bd51d24 100644 --- a/packages/cmf/src/cmfConnect.jsx +++ b/packages/cmf/src/cmfConnect.jsx @@ -24,7 +24,6 @@ export default cmfConnect({ import PropTypes from 'prop-types'; import { useState, useContext, useEffect, forwardRef } from 'react'; import hoistStatics from 'hoist-non-react-statics'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect, useStore } from 'react-redux'; import { randomUUID } from '@talend/utils'; import actions from './actions'; @@ -219,7 +218,7 @@ export default function cmfConnect({ ); } function getState(state, id = 'default') { - return state.cmf.components.getIn([getComponentName(WrappedComponent), id], defaultState); + return state.cmf.components?.[getComponentName(WrappedComponent)]?.[id] ?? defaultState; } function getSetStateAction(state, id, type) { return { @@ -306,7 +305,7 @@ export default function cmfConnect({ } let spreadedState = {}; if ((spreadCMFState || props.spreadCMFState) && props.state) { - spreadedState = props.state.toJS(); + spreadedState = { ...props.state }; } const newProps = { @@ -392,8 +391,8 @@ cmfConnect.omit = omit; cmfConnect.omitAllProps = props => cmfConnect.omit(props, cmfConnect.ALL_INJECTED_PROPS); cmfConnect.propTypes = { - state: ImmutablePropTypes.map, - initialState: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.object]), + state: PropTypes.object, + initialState: PropTypes.object, getComponent: PropTypes.func, setState: PropTypes.func, initState: PropTypes.func, diff --git a/packages/cmf/src/componentState.js b/packages/cmf/src/componentState.js index d63c1417f81..66114fb2fc6 100644 --- a/packages/cmf/src/componentState.js +++ b/packages/cmf/src/componentState.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import Immutable from 'immutable'; import actions from './actions'; /** @@ -23,7 +22,7 @@ export default cmfConnect({})(MyComponent); export function getStateProps(state, name, id = 'default') { return { - state: state.cmf.components.getIn([name, id]), + state: state.cmf.components?.[name]?.[id], }; } @@ -67,9 +66,9 @@ export function getStateAccessors(dispatch, name, id, DEFAULT_STATE) { initState(initialState) { let state; if (DEFAULT_STATE) { - state = DEFAULT_STATE.merge(initialState); + state = { ...DEFAULT_STATE, ...(initialState || {}) }; } else if (initialState) { - state = Immutable.Map.isMap(initialState) ? initialState : Immutable.fromJS(initialState); + state = initialState; } if (state) { const componentState = actions.components.addState(name, id, state); diff --git a/packages/cmf/src/expressions/allOf.js b/packages/cmf/src/expressions/allOf.js index 38e1b28c964..53a1aeffe0b 100644 --- a/packages/cmf/src/expressions/allOf.js +++ b/packages/cmf/src/expressions/allOf.js @@ -1,15 +1,13 @@ import get from 'lodash/get'; -import Immutable from 'immutable'; export default function getAllOfFunction(statePath) { - return function includes({ context }, immutablePath, values) { + return function includes({ context }, path, values) { if (!Array.isArray(values)) { throw new Error('You should pass an array of values to check if all of them are present'); } - const arr = get(context.store.getState(), statePath, new Immutable.Map()).getIn( - immutablePath.split('.'), - new Immutable.List(), - ); - return arr.size > 0 && arr.every(value => values.includes(value)); + const stateSlice = get(context.store.getState(), statePath, {}); + const arr = get(stateSlice, path.split('.'), []); + // Checks that every item in the stored array is among the provided values (subset check) + return Array.isArray(arr) && arr.length > 0 && arr.every(value => values.includes(value)); }; } diff --git a/packages/cmf/src/expressions/getInState.js b/packages/cmf/src/expressions/getInState.js index 67eadc66f23..89d7face6fb 100644 --- a/packages/cmf/src/expressions/getInState.js +++ b/packages/cmf/src/expressions/getInState.js @@ -1,12 +1,9 @@ import _get from 'lodash/get'; -import Immutable from 'immutable'; import curry from 'lodash/curry'; -function getInState(statePath, { context }, immutablePath, defaultValue) { - return _get(context.store.getState(), statePath, new Immutable.Map()).getIn( - immutablePath.split('.'), - defaultValue, - ); +function getInState(statePath, { context }, path, defaultValue) { + const stateSlice = _get(context.store.getState(), statePath, {}); + return _get(stateSlice, path.split('.'), defaultValue); } export default curry(getInState); diff --git a/packages/cmf/src/expressions/includes.js b/packages/cmf/src/expressions/includes.js index ed94cadb2a5..f61c35b1d8c 100644 --- a/packages/cmf/src/expressions/includes.js +++ b/packages/cmf/src/expressions/includes.js @@ -1,10 +1,9 @@ import _get from 'lodash/get'; -import Immutable from 'immutable'; export default function getIncludesFunction(statePath) { - return function includes({ context }, immutablePath, value) { - return _get(context.store.getState(), statePath, new Immutable.Map()) - .getIn(immutablePath.split('.'), new Immutable.List()) - .includes(value); + return function includes({ context }, path, value) { + const stateSlice = _get(context.store.getState(), statePath, {}); + const arr = _get(stateSlice, path.split('.'), []); + return Array.isArray(arr) && arr.includes(value); }; } diff --git a/packages/cmf/src/expressions/index.md b/packages/cmf/src/expressions/index.md index be4a5737b2f..041490aa639 100644 --- a/packages/cmf/src/expressions/index.md +++ b/packages/cmf/src/expressions/index.md @@ -16,12 +16,11 @@ For all the following example we take this component as example: ```javascript import React from 'react'; import PropTypes from 'prop-types'; -import Immutable from 'immutable'; import { cmfConnect } from '@talend/react-cmf'; -const DEFAULT_STATE = new Immutable.Map({ +const DEFAULT_STATE = { like: false, -}); +}; class Article extends React.Component { static propTypes = { @@ -35,11 +34,11 @@ class Article extends React.Component { } onLike() { - this.props.setState({ like: !this.props.state.get('like') }); + this.props.setState({ like: !this.props.state.like }); } render() { - const like = this.props.state.get('like'); + const like = this.props.state.like; return (

{props.title}

@@ -51,7 +50,8 @@ class Article extends React.Component { } function mapStateToProps(state) { return { - model: state.cmf.collections.get('article'); + model: state.cmf.collections['article'], + }; }; } export cmfConnect({mapStateToProps})(MyComponent) @@ -101,6 +101,7 @@ export cmfConnect({mapStateToProps})(MyComponent) } } ``` + ### allOf ```json diff --git a/packages/cmf/src/expressions/oneOf.js b/packages/cmf/src/expressions/oneOf.js index a040c6bc92d..dd4a91425a4 100644 --- a/packages/cmf/src/expressions/oneOf.js +++ b/packages/cmf/src/expressions/oneOf.js @@ -1,15 +1,12 @@ import get from 'lodash/get'; -import Immutable from 'immutable'; export default function getOneOfFunction(statePath) { - return function includes({ context }, immutablePath, values) { + return function includes({ context }, path, values) { if (!Array.isArray(values)) { throw new Error('You should pass an array of values to check if one of them is present'); } - const arr = get(context.store.getState(), statePath, new Immutable.Map()).getIn( - immutablePath.split('.'), - new Immutable.List(), - ); - return values.some(value => arr.includes(value)); + const stateSlice = get(context.store.getState(), statePath, {}); + const arr = get(stateSlice, path.split('.'), []); + return Array.isArray(arr) && values.some(value => arr.includes(value)); }; } diff --git a/packages/cmf/src/localStorage.js b/packages/cmf/src/localStorage.js index 595be0a4adc..ed6dd7b4478 100644 --- a/packages/cmf/src/localStorage.js +++ b/packages/cmf/src/localStorage.js @@ -1,4 +1,4 @@ -import Immutable from 'immutable'; +import get from 'lodash/get'; import set from 'lodash/set'; /** @@ -12,14 +12,6 @@ function getState(key) { return {}; } source = JSON.parse(source); - if (source.cmf) { - if (source.cmf.components) { - source.cmf.components = Immutable.fromJS(source.cmf.components); - } - if (source.cmf.collections) { - source.cmf.collections = Immutable.fromJS(source.cmf.collections); - } - } return source; } @@ -47,14 +39,14 @@ function getStoreCallback(key, paths) { paths.forEach(path => { if (path.length > 2) { if (path[1] === 'components') { - const value = state.cmf.components.getIn(path.slice(2)); + const value = get(state.cmf.components, path.slice(2)); if (value) { - set(toKeep, path, value.toJS()); + set(toKeep, path, value); } } else if (path[1] === 'collections') { - const value = state.cmf.collections.getIn(path.slice(2)); + const value = get(state.cmf.collections, path.slice(2)); if (value) { - set(toKeep, path, value.toJS()); + set(toKeep, path, value); } } } diff --git a/packages/cmf/src/mock/collections.js b/packages/cmf/src/mock/collections.js index b619773c076..ff8b4c56321 100644 --- a/packages/cmf/src/mock/collections.js +++ b/packages/cmf/src/mock/collections.js @@ -1,3 +1 @@ -import { Map } from 'immutable'; - -export default new Map(); +export default {}; diff --git a/packages/cmf/src/mock/components.js b/packages/cmf/src/mock/components.js index 4d3f2ae6970..c8201800b20 100644 --- a/packages/cmf/src/mock/components.js +++ b/packages/cmf/src/mock/components.js @@ -1,5 +1,3 @@ -import { fromJS } from 'immutable'; - const components = { componentName: { componentKey: { @@ -9,4 +7,4 @@ const components = { }, }; -export default fromJS(components); +export default components; diff --git a/packages/cmf/src/mock/index.js b/packages/cmf/src/mock/index.js index b9cc6507b9e..e6ab2ee0a15 100644 --- a/packages/cmf/src/mock/index.js +++ b/packages/cmf/src/mock/index.js @@ -9,6 +9,12 @@ * You have to import the mock from the complete path, ie: * * ```import mock from 'react-cmf/lib/mock';``` + * + * MIGRATION NOTE (Immutable removal): + * `mock.store.getState().cmf.collections` is now a plain object `{}` (was Immutable.Map). + * `mock.store.getState().cmf.components` is now a plain object (was Immutable.Map via fromJS()). + * Consumers should use plain object access (`collections['key']`) instead of `.get('key')`. + * This is aligned with the Epic 5 direction where the real store will also use plain objects. */ import store from './store'; import Provider from './provider'; diff --git a/packages/cmf/src/onEvent.js b/packages/cmf/src/onEvent.js index 9540e1c6c6b..97a97d82c33 100644 --- a/packages/cmf/src/onEvent.js +++ b/packages/cmf/src/onEvent.js @@ -1,5 +1,4 @@ import get from 'lodash/get'; -import Immutable from 'immutable'; import CONSTANT from './constant'; function serializeEvent(event) { @@ -59,9 +58,7 @@ function getOnEventSetStateHandler(instance, props, config, currentHandler) { } } else if (value === 'toggle') { // because toggle need to read the state we dispatch it with a function - instance.props.setState(_props => - instance.props.setState({ [key]: !_props.state.get(key) }), - ); + instance.props.setState(_props => instance.props.setState({ [key]: !_props.state[key] })); } else { // eslint-disable-next-line no-param-reassign acc[key] = value; @@ -84,7 +81,7 @@ const ACTION_CREATOR = 'ACTION_CREATOR'; const DISPATCH = 'DISPATCH'; const SETSTATE = 'SETSTATE'; -const INITIAL_STATE = new Immutable.Map(); +const INITIAL_STATE = {}; function addOnEventSupport(handlerType, instance, props, key) { if (CONSTANT[`IS_HANDLER_${handlerType}_REGEX`].test(key)) { diff --git a/packages/cmf/src/reducers/collectionsReducers.js b/packages/cmf/src/reducers/collectionsReducers.js index a1fdbfe1391..15481c4c7b9 100644 --- a/packages/cmf/src/reducers/collectionsReducers.js +++ b/packages/cmf/src/reducers/collectionsReducers.js @@ -1,21 +1,33 @@ /** * @module react-cmf/lib/reducers/collectionsReducers */ -import { Map, List, fromJS } from 'immutable'; +import get from 'lodash/get'; +import has from 'lodash/has'; +import cloneDeep from 'lodash/cloneDeep'; +import set from 'lodash/set'; +import unset from 'lodash/unset'; import invariant from 'invariant'; import CONSTANTS from '../constant'; -export const defaultState = new Map(); +export const defaultState = {}; + +function setIn(state, path, value) { + const cloned = cloneDeep(state); + set(cloned, path, value); + return cloned; +} + +function deleteIn(state, path) { + const cloned = cloneDeep(state); + unset(cloned, path); + return cloned; +} /** - * Get element id. If it doesn't have "id" property, we consider it as immutable. + * Get element id. */ export function getId(element) { - const id = element.id; - if (id === undefined) { - return element.get('id'); - } - return id; + return element.id; } /* @@ -43,14 +55,14 @@ export function getActionWithCollectionIdAsArray(action) { function addCollectionElement(state, action) { if (action.operations.add) { return action.operations.add.reduce((s, e) => { - const element = s.getIn(action.collectionId); - if (List.isList(element)) { - return s.setIn(action.collectionId, element.push(e)); + const element = get(s, action.collectionId); + if (Array.isArray(element)) { + return setIn(s, action.collectionId, [...element, e]); } - if (Map.isMap(element)) { - return s.setIn(action.collectionId, element.merge(e)); + if (typeof element === 'object' && !Array.isArray(element) && element !== null) { + return setIn(s, action.collectionId, { ...element, ...e }); } - return state; + return s; }, state); } return state; @@ -61,22 +73,26 @@ function deleteListElements(state, action) { return action.operations.delete.indexOf(getId(element)) >= 0; } - const collection = state.getIn(action.collectionId); + const collection = get(state, action.collectionId); if (collection.some(shouldBeRemoved)) { - return state.setIn(action.collectionId, collection.filterNot(shouldBeRemoved)); + return setIn( + state, + action.collectionId, + collection.filter(e => !shouldBeRemoved(e)), + ); } return state; } function deleteMapElements(state, action) { - const collection = state.getIn(action.collectionId); - - if (action.operations.delete.some(id => collection.has(id))) { - const changedCollection = action.operations.delete.reduce( - (collectionAccu, element) => collectionAccu.delete(element), - collection, - ); - return state.setIn(action.collectionId, changedCollection); + const collection = get(state, action.collectionId); + + if (action.operations.delete.some(id => id in collection)) { + const changedCollection = action.operations.delete.reduce((collectionAccu, element) => { + const { [element]: _, ...rest } = collectionAccu; + return rest; + }, collection); + return setIn(state, action.collectionId, changedCollection); } return state; @@ -91,33 +107,33 @@ function deleteMapElements(state, action) { */ function deleteCollectionElement(state, action) { if (action.operations.delete) { - const collection = state.getIn(action.collectionId); - if (Map.isMap(collection)) { + const collection = get(state, action.collectionId); + if (!Array.isArray(collection) && typeof collection === 'object' && collection !== null) { return deleteMapElements(state, action); - } else if (List.isList(collection)) { + } else if (Array.isArray(collection)) { return deleteListElements(state, action); } - throw new Error('CMF collection deletion is only compatible with ImmutableJs List and Map'); + throw new Error('CMF collection deletion is only compatible with plain arrays and objects'); } return state; } function updateListElements(state, action) { const updates = action.operations.update; - - const changedCollection = state - .getIn(action.collectionId) - .map(element => updates[getId(element)] || element); - return state.setIn(action.collectionId, changedCollection); + const changedCollection = get(state, action.collectionId).map(element => + getId(element) in updates ? updates[getId(element)] : element, + ); + return setIn(state, action.collectionId, changedCollection); } function updateMapElements(state, action) { const updates = action.operations.update; + const currentCollection = get(state, action.collectionId); const changedCollection = Object.keys(updates).reduce( - (collectionAccu, id) => collectionAccu.set(id, updates[id]), - state.getIn(action.collectionId), + (collectionAccu, id) => ({ ...collectionAccu, [id]: updates[id] }), + currentCollection, ); - return state.setIn(action.collectionId, changedCollection); + return setIn(state, action.collectionId, changedCollection); } /** @@ -129,13 +145,13 @@ function updateMapElements(state, action) { */ function updateCollectionElement(state, action) { if (action.operations.update) { - const collection = state.getIn(action.collectionId); - if (Map.isMap(collection)) { + const collection = get(state, action.collectionId); + if (!Array.isArray(collection) && typeof collection === 'object' && collection !== null) { return updateMapElements(state, action); - } else if (List.isList(collection)) { + } else if (Array.isArray(collection)) { return updateListElements(state, action); } - throw new Error('CMF collection update is only compatible with ImmutableJs List and Map'); + throw new Error('CMF collection update is only compatible with plain arrays and objects'); } return state; } @@ -148,7 +164,7 @@ function updateCollectionElement(state, action) { * @returns {object} the new state */ function mutateCollection(state, action) { - if (!action.operations || !state.hasIn(action.collectionId) || state.isEmpty()) { + if (!action.operations || !has(state, action.collectionId)) { return state; } let newState = addCollectionElement(state, action); @@ -165,16 +181,16 @@ function collectionsReducers(state = defaultState, action = { type: '' }) { const newAction = getActionWithCollectionIdAsArray(action); switch (newAction.type) { case CONSTANTS.COLLECTION_ADD_OR_REPLACE: - return state.setIn(newAction.collectionId, fromJS(newAction.data)); + return setIn(state, newAction.collectionId, newAction.data); case CONSTANTS.COLLECTION_REMOVE: - if (!state.getIn(newAction.collectionId)) { + if (!has(state, newAction.collectionId)) { invariant( process.env.NODE_ENV === 'production', `Can't remove collection ${newAction.collectionId} since it doesn't exist.`, ); return state; } - return state.deleteIn(newAction.collectionId); + return deleteIn(state, newAction.collectionId); case CONSTANTS.COLLECTION_MUTATE: return mutateCollection(state, newAction); default: diff --git a/packages/cmf/src/reducers/collectionsReducers.test.js b/packages/cmf/src/reducers/collectionsReducers.test.js new file mode 100644 index 00000000000..751b1533db9 --- /dev/null +++ b/packages/cmf/src/reducers/collectionsReducers.test.js @@ -0,0 +1,558 @@ +import collectionsReducers, { + defaultState, + getId, + getActionWithCollectionIdAsArray, +} from './collectionsReducers'; + +const initialState = { ...defaultState, collection1: 'super data' }; + +const listInitialState = { + ...defaultState, + collectionid: [ + { id: 0, label: 'test data 0' }, + { id: 1, label: 'test data 1' }, + ], +}; +const mapInitialState = { + ...defaultState, + collectionid: { + test0: 'test data 0', + test1: 'test data 1', + }, +}; + +describe('check collection management reducer', () => { + it('should return state if no action passed', () => { + expect(collectionsReducers(initialState)).toEqual(initialState); + }); + + it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly add data into store', () => { + expect( + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: ['collectionId'], + data: 'data can be anything', + }), + ).toEqual({ + collection1: 'super data', + collectionId: 'data can be anything', + }); + }); + + it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly replace data into store', () => { + expect( + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: ['collection1'], + data: 'data can be anything', + }), + ).toEqual({ collection1: 'data can be anything' }); + }); + + it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly add nested collection into store', () => { + const initState = { + collection1: { + data: 'data can be anything', + }, + }; + const expectedResult = { + collection1: { + data: 'data can be anything', + nestedCollection: ['item 1', 'item 2'], + }, + }; + expect( + collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: ['collection1', 'nestedCollection'], + data: ['item 1', 'item 2'], + }), + ).toEqual(expectedResult); + }); + + it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly replace nested collection into store', () => { + const initState = { + collection1: { + nestedCollection: 'data can be anything', + }, + }; + const expectedResult = { + collection1: { + nestedCollection: ['item 1', 'item 2'], + }, + }; + expect( + collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: ['collection1', 'nestedCollection'], + data: ['item 1', 'item 2'], + }), + ).toEqual(expectedResult); + }); + + it('REACT_CMF.COLLECTION_REMOVE should properly remove collection from the store', () => { + expect( + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_REMOVE', + collectionId: ['collection1'], + }), + ).toEqual({}); + }); + + it('REACT_CMF.COLLECTION_REMOVE should properly remove nested collection from the store', () => { + const initState = { + collection: { + nestedCollection: { + list: ['item 1', 'item 2'], + }, + }, + }; + const expectedResult = { + collection: { + nestedCollection: {}, + }, + }; + expect( + collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_REMOVE', + collectionId: ['collection', 'nestedCollection', 'list'], + }), + ).toEqual(expectedResult); + }); + + it("REACT_CMF.COLLECTION_REMOVE should throw when collection doesn't exist", () => { + expect(() => { + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_REMOVE', + collectionId: ['unknown collection'], + }); + }).toThrowError("Can't remove collection unknown collection since it doesn't exist."); + }); +}); + +describe('REACT_CMF.COLLECTION_MUTATE', () => { + it("shouldn't mutate if id doesn't exist", () => { + expect( + collectionsReducers(mapInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['wrongCollectionid'], + operation: {}, + }), + ).toEqual(mapInitialState); + }); + + it("shouldn't mutate if no operations", () => { + expect( + collectionsReducers(mapInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + }), + ).toEqual(mapInitialState); + }); + + describe('#add', () => { + it('should insert elements to List properly', () => { + const nextState = collectionsReducers(listInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + add: [{ id: 2, label: 'test data 2' }], + }, + }); + expect(nextState.collectionid).toEqual([ + { id: 0, label: 'test data 0' }, + { id: 1, label: 'test data 1' }, + { id: 2, label: 'test data 2' }, + ]); + }); + + it('should insert elements to nested List properly', () => { + const initState = { + collection: { + nestedCollection: { + list: [{ id: 0, label: 'test data 0' }], + }, + }, + }; + const nextState = collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collection', 'nestedCollection', 'list'], + operations: { + add: [ + { id: 1, label: 'test data 1' }, + { id: 2, label: 'test data 2' }, + ], + }, + }); + expect(nextState.collection.nestedCollection.list).toEqual([ + { id: 0, label: 'test data 0' }, + { id: 1, label: 'test data 1' }, + { id: 2, label: 'test data 2' }, + ]); + }); + + it('should insert elements to Map properly', () => { + const nextState = collectionsReducers(mapInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + add: [{ test2: 'test data 2' }], + }, + }); + expect(nextState).toEqual({ + collectionid: { + test0: 'test data 0', + test1: 'test data 1', + test2: 'test data 2', + }, + }); + }); + + it('should insert elements to nested Map properly', () => { + const initState = { + collection: { + nestedCollection: { + obj: { + test0: 'test data 0', + test1: 'test data 1', + }, + }, + }, + }; + const nextState = collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collection', 'nestedCollection', 'obj'], + operations: { + add: [{ test2: 'test data 2' }], + }, + }); + expect(nextState).toEqual({ + collection: { + nestedCollection: { + obj: { + test0: 'test data 0', + test1: 'test data 1', + test2: 'test data 2', + }, + }, + }, + }); + }); + + it('should leave state unchanged when add target is neither array nor object (fallback branch)', () => { + const initState = { ...defaultState, collectionid: 'primitive-value' }; + const nextState = collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + add: [{ id: 0, label: 'new' }], + }, + }); + expect(nextState).toEqual(initState); + }); + }); + + describe('#delete', () => { + it('should delete elements from List properly', () => { + const nextState = collectionsReducers(listInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + delete: [0], + }, + }); + expect(nextState.collectionid).toEqual([{ id: 1, label: 'test data 1' }]); + }); + + it('should delete elements from nested List properly', () => { + const initState = { + collection: { + nestedCollection: { + list: [ + { id: 0, label: 'test data 0' }, + { id: 1, label: 'test data 1' }, + { id: 2, label: 'test data 2' }, + ], + }, + }, + }; + const nextState = collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collection', 'nestedCollection', 'list'], + operations: { + delete: [0, 1], + }, + }); + expect(nextState.collection.nestedCollection.list).toEqual([{ id: 2, label: 'test data 2' }]); + }); + + it('should delete elements from Map properly', () => { + const nextState = collectionsReducers(mapInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + delete: ['test0'], + }, + }); + expect(nextState).toEqual({ + collectionid: { + test1: 'test data 1', + }, + }); + }); + + it('should delete elements from nested Map properly', () => { + const initState = { + collection: { + nestedCollection: { + obj: { + test0: 'test data 0', + test1: 'test data 1', + test2: 'test data 2', + }, + }, + }, + }; + const nextState = collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collection', 'nestedCollection', 'obj'], + operations: { + delete: ['test0', 'test1'], + }, + }); + expect(nextState.collection.nestedCollection.obj).toEqual({ + test2: 'test data 2', + }); + }); + + it("should delete nothing when ids don't match in List", () => { + const nextState = collectionsReducers(listInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + delete: ['unknown'], + }, + }); + expect(nextState).toEqual(listInitialState); + }); + + it("should delete nothing when ids don't match in Map", () => { + const nextState = collectionsReducers(mapInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + delete: ['unknown'], + }, + }); + expect(nextState).toEqual(mapInitialState); + }); + }); + + describe('#update', () => { + it('should update elements of List properly', () => { + const nextState = collectionsReducers(listInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + update: { + 0: { id: 0, label: 'new test data 0' }, + }, + }, + }); + expect(nextState.collectionid).toEqual([ + { id: 0, label: 'new test data 0' }, + { id: 1, label: 'test data 1' }, + ]); + }); + + it('should update elements of nested List properly', () => { + const initState = { + collection: { + nestedCollection: { + list: [ + { id: 0, label: 'test data 0' }, + { id: 1, label: 'test data 1' }, + { id: 2, label: 'test data 2' }, + ], + }, + }, + }; + const nextState = collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collection', 'nestedCollection', 'list'], + operations: { + update: { + 0: { id: 0, label: 'new test data 0' }, + 1: { id: 1, label: 'new test data 1' }, + }, + }, + }); + expect(nextState.collection.nestedCollection.list).toEqual([ + { id: 0, label: 'new test data 0' }, + { id: 1, label: 'new test data 1' }, + { id: 2, label: 'test data 2' }, + ]); + }); + + it('should update elements of Map properly', () => { + const nextState = collectionsReducers(mapInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + update: { + test0: 'new test data 0', + }, + }, + }); + expect(nextState).toEqual({ + collectionid: { + test0: 'new test data 0', + test1: 'test data 1', + }, + }); + }); + + it('should update elements of nested Map properly', () => { + const initState = { + collection: { + nestedCollection: { + obj: { + test0: 'test data 0', + test1: 'test data 1', + test2: 'test data 2', + }, + }, + }, + }; + const nextState = collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collection', 'nestedCollection', 'obj'], + operations: { + update: { + test0: 'new test data 0', + test1: 'new test data 1', + }, + }, + }); + expect(nextState.collection.nestedCollection.obj).toEqual({ + test0: 'new test data 0', + test1: 'new test data 1', + test2: 'test data 2', + }); + }); + + it('should update elements of List to falsy update values properly', () => { + const nextState = collectionsReducers(listInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: ['collectionid'], + operations: { + update: { + 0: null, + }, + }, + }); + expect(nextState.collectionid).toEqual([null, { id: 1, label: 'test data 1' }]); + }); + }); +}); + +describe('should properly perform all operations if collectionId is string', () => { + it('REACT_CMF.COLLECTION_ADD_OR_REPLACE should properly add data into store', () => { + expect( + collectionsReducers(initialState, { + type: 'REACT_CMF.COLLECTION_ADD_OR_REPLACE', + collectionId: 'collectionId', + data: 'data can be anything', + }), + ).toEqual({ + collection1: 'super data', + collectionId: 'data can be anything', + }); + }); + + it('REACT_CMF.COLLECTION_REMOVE should properly remove collection from the store', () => { + const initState = { + collection: { + nestedCollection: { + list: ['item 1', 'item 2'], + }, + }, + }; + const expectedResult = { + collection: { + nestedCollection: {}, + }, + }; + expect( + collectionsReducers(initState, { + type: 'REACT_CMF.COLLECTION_REMOVE', + collectionId: 'collection.nestedCollection.list', + }), + ).toEqual(expectedResult); + }); + + it('REACT_CMF.COLLECTION_MUTATE should mutate List properly', () => { + const nextState = collectionsReducers(listInitialState, { + type: 'REACT_CMF.COLLECTION_MUTATE', + id: 'collectionid', + operations: { + add: [{ id: 2, label: 'test data 2' }], + }, + }); + expect(nextState.collectionid).toEqual([ + { id: 0, label: 'test data 0' }, + { id: 1, label: 'test data 1' }, + { id: 2, label: 'test data 2' }, + ]); + }); +}); + +describe('getId', () => { + it('should return element id', () => { + // given + const element = { id: 'toto' }; + + // when + const id = getId(element); + + // then + expect(id).toBe('toto'); + }); +}); + +describe('getActionWithCollectionIdAsArray', () => { + it('should return action if there is not collectionId or id fields in action object', () => { + const action = { + type: 'SOME_ACTION', + }; + const result = getActionWithCollectionIdAsArray(action); + expect(result).toEqual(action); + }); + + it('should return new formed action if path to collection is represented by "collectionId" field', () => { + const action = { + type: 'SOME_ACTION', + collectionId: 'collection.nestedCollection', + }; + const expectedResult = { + type: 'SOME_ACTION', + collectionId: ['collection', 'nestedCollection'], + }; + const result = getActionWithCollectionIdAsArray(action); + expect(result).toEqual(expectedResult); + }); + + it('should return new formed action if path to collection is represented by "id" field (mutateCollection action creator)', () => { + const action = { + type: 'SOME_ACTION', + id: 'collection.nestedCollection', + }; + const expectedResult = { + type: 'SOME_ACTION', + id: 'collection.nestedCollection', + collectionId: ['collection', 'nestedCollection'], + }; + const result = getActionWithCollectionIdAsArray(action); + expect(result).toEqual(expectedResult); + }); +}); diff --git a/packages/cmf/src/reducers/componentsReducers.js b/packages/cmf/src/reducers/componentsReducers.js index 9017c5ac09e..42ac40b862e 100644 --- a/packages/cmf/src/reducers/componentsReducers.js +++ b/packages/cmf/src/reducers/componentsReducers.js @@ -3,11 +3,10 @@ * @module react-cmf/lib/reducers/componentsReducers */ import get from 'lodash/get'; -import { Map, fromJS } from 'immutable'; import invariant from 'invariant'; import CONSTANTS from '../constant'; -export const defaultState = new Map(); +export const defaultState = {}; /** * given the state and action, determine if another component try to bind to a specific @@ -17,7 +16,7 @@ export const defaultState = new Map(); */ export function warnIfAnotherComponentBind(state, action) { if (process.env.NODE_ENV !== 'production') { - if (state.getIn([action.componentName, action.key])) { + if (state[action.componentName]?.[action.key]) { console.warn(`Beware component ${action.componentName} try to recreate an existing State namespace ${action.key}, meaning that the original one will be overloaded`); } @@ -32,7 +31,7 @@ export function warnIfAnotherComponentBind(state, action) { */ export function warnIfRemovingStateDoesntExist(state, action) { if (process.env.NODE_ENV !== 'production') { - if (!state.getIn([action.componentName, action.key])) { + if (!state[action.componentName]?.[action.key]) { console.warn(`Beware the component ${action.componentName} try to remove a non existing State namespace ${action.key}, it isn't a normal behavior execpt if two component are binded to this specific namespace`); @@ -48,7 +47,7 @@ export function warnIfRemovingStateDoesntExist(state, action) { * @param {Object} action a redux action */ export function errorIfMergingStateDoesntExist(state, action) { - if (!state.getIn([action.componentName, action.key])) { + if (!state[action.componentName]?.[action.key]) { invariant( process.env.NODE_ENV === 'production', `Error, the component ${action.componentName} try to mutate a non existing @@ -65,22 +64,38 @@ export function errorIfMergingStateDoesntExist(state, action) { */ export function componentsReducers(state = defaultState, action) { switch (action.type) { - case CONSTANTS.COMPONENT_ADD_STATE: + case CONSTANTS.COMPONENT_ADD_STATE: { warnIfAnotherComponentBind(state, action); - if (action.initialComponentState) { - return state.setIn( - [action.componentName, action.key], - fromJS(action.initialComponentState), - ); - } - return state.setIn([action.componentName, action.key], new Map()); + const componentState = + action.initialComponentState !== undefined ? action.initialComponentState : {}; + return { + ...state, + [action.componentName]: { + ...state[action.componentName], + [action.key]: componentState, + }, + }; + } case CONSTANTS.COMPONENT_MERGE_STATE: errorIfMergingStateDoesntExist(state, action); - - return state.mergeIn([action.componentName, action.key], fromJS(action.componentState)); - case CONSTANTS.COMPONENT_REMOVE_STATE: + return { + ...state, + [action.componentName]: { + ...state[action.componentName], + [action.key]: { + ...(state[action.componentName]?.[action.key] || {}), + ...action.componentState, + }, + }, + }; + case CONSTANTS.COMPONENT_REMOVE_STATE: { warnIfRemovingStateDoesntExist(state, action); - return state.deleteIn([action.componentName, action.key]); + const { [action.key]: _removed, ...remainingKeys } = state[action.componentName] || {}; + return { + ...state, + [action.componentName]: remainingKeys, + }; + } default: { const subAction = get(action, 'cmf.componentState'); if (subAction) { diff --git a/packages/cmf/src/reducers/componentsReducers.test.js b/packages/cmf/src/reducers/componentsReducers.test.js new file mode 100644 index 00000000000..30d81cf0766 --- /dev/null +++ b/packages/cmf/src/reducers/componentsReducers.test.js @@ -0,0 +1,142 @@ +import reducer, { defaultState } from './componentsReducers'; + +global.console = { warn: vi.fn() }; + +describe('check component management reducer', () => { + const initialState = { + ...defaultState, + component1: { key1: { searchQuery: '' } }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it(`REACT_CMF.COMPONENT_ADD_STATE should properly add component/collection + state tracking to the store if nor the component/key exist`, () => { + expect( + reducer(initialState, { + type: 'REACT_CMF.COMPONENT_ADD_STATE', + componentName: 'componentName', + key: 'key', + initialComponentState: { searchQuery: 'data' }, + }), + ).toEqual({ + component1: { key1: { searchQuery: '' } }, + componentName: { key: { searchQuery: 'data' } }, + }); + }); + + it(`REACT_CMF.COMPONENT_ADD_STATE should properly add component/collection + state tracking to the store if nor the component/key exist + event if initialState is undefined`, () => { + expect( + reducer(initialState, { + type: 'REACT_CMF.COMPONENT_ADD_STATE', + componentName: 'componentName', + key: 'key', + initialComponentState: undefined, + }), + ).toEqual({ + component1: { key1: { searchQuery: '' } }, + componentName: { key: {} }, + }); + }); + + it(`REACT_CMF.COMPONENT_ADD_STATE should properly add component/collection + state tracking to the store if the key don't exist`, () => { + expect( + reducer(initialState, { + type: 'REACT_CMF.COMPONENT_ADD_STATE', + componentName: 'component1', + key: 'key', + initialComponentState: 'initialState', + }), + ).toEqual({ + component1: { key1: { searchQuery: '' }, key: 'initialState' }, + }); + }); + + it('REACT_CMF.COMPONENT_ADD_STATE throw when a couple of componentName, key already exist', () => { + const action = { + type: 'REACT_CMF.COMPONENT_ADD_STATE', + componentName: 'component1', + key: 'key1', + initialComponentState: 'initialState', + }; + reducer(initialState, action); + expect(console.warn).toHaveBeenCalled(); + expect(console.warn.mock.calls[0][0]) + .toEqual(`Beware component component1 try to recreate an existing + State namespace key1, meaning that the original one will be overloaded`); + }); + + it(`REACT_CMF.COMPONENT_MERGE_STATE should properly merge + component/key state into the store`, () => { + expect( + reducer(initialState, { + type: 'REACT_CMF.COMPONENT_MERGE_STATE', + componentName: 'component1', + key: 'key1', + componentState: { searchQuery: 'data' }, + }), + ).toEqual({ + component1: { key1: { searchQuery: 'data' } }, + }); + }); + + it(`REACT_CMF.COMPONENT_MERGE_STATE should throw when a couple of + componentName, keyId doesn't exist`, () => { + const action = { + type: 'REACT_CMF.COMPONENT_MERGE_STATE', + componentName: 'component', + key: 'key', + componentState: { searchQuery: 'data' }, + }; + expect(() => reducer(initialState, action)) + .toThrow(`Error, the component component try to mutate a non existing + State namespace key, this namespace may be not yet created or already removed.`); + }); + + it(`REACT_CMF.COMPONENT_REMOVE_STATE should properly add + component/key state tracking to the store`, () => { + expect( + reducer(initialState, { + type: 'REACT_CMF.COMPONENT_REMOVE_STATE', + componentName: 'component1', + key: 'key1', + }), + ).toEqual({ component1: {} }); + }); + + it(`removeComponentState throw when a couple of componentName, + collectionId doesn't exist`, () => { + const action = { + type: 'REACT_CMF.COMPONENT_REMOVE_STATE', + componentName: 'component', + key: 'key', + }; + reducer(initialState, action); + expect(console.warn).toHaveBeenCalled(); + expect(console.warn.mock.calls[0][0]) + .toEqual(`Beware the component component try to remove a non existing + State namespace key, it isn't a normal behavior execpt if two component are binded + to this specific namespace`); + }); + + it('should recall itself on action.cmf.componentState', () => { + const action = { + type: 'REACT_CMF.COMPONENT_ADD_STATE', + componentName: 'componentName', + key: 'key', + initialComponentState: { searchQuery: 'data' }, + }; + const subAction = { + type: 'WHAT_EVER', + cmf: { + componentState: action, + }, + }; + expect(reducer(initialState, action)).toEqual(reducer(initialState, subAction)); + }); +}); diff --git a/packages/cmf/src/selectors/collections.js b/packages/cmf/src/selectors/collections.js index 92c483b2e7b..59b4deae4f3 100644 --- a/packages/cmf/src/selectors/collections.js +++ b/packages/cmf/src/selectors/collections.js @@ -1,4 +1,4 @@ -import { List } from 'immutable'; +import _get from 'lodash/get'; import getToJSMemoized from './toJS'; export function getAll(state) { @@ -13,14 +13,8 @@ export function getAll(state) { * 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); + if (typeof collectionPath === 'string' || Array.isArray(collectionPath)) { + return _get(state.cmf.collections, collectionPath, defaultValue); } throw Error(`Type mismatch: collectionPath should be a string or an array of string got ${collectionPath}`); @@ -35,15 +29,17 @@ got ${collectionPath}`); */ export function findListItem(state, collectionPath, itemId) { const collectionOrCollectionSubset = get(state, collectionPath); - if (List.isList(collectionOrCollectionSubset)) { - return collectionOrCollectionSubset.find(element => element && element.get('id') === itemId); + if (Array.isArray(collectionOrCollectionSubset)) { + return collectionOrCollectionSubset.find(element => element && element.id === itemId); } throw Error( - `Type mismatch: ${collectionPath} does not resolve as an instance of Immutable.List, + `Type mismatch: ${collectionPath} does not resolve as an Array, got ${collectionOrCollectionSubset}`, ); } +// Cache keys are joined path strings; bounded in practice by the finite set of +// distinct paths used in the application. const selectors = {}; export function toJS(state, path) { @@ -53,3 +49,57 @@ export function toJS(state, path) { } return selectors[joinedPath](state); } + +/** + * Get a collection as a plain JS object/array (no Immutable types leaked). + * @param {Object} state + * @param {String} collectionId + * @returns {Object|Array|undefined} + */ +export function getCollectionPlain(state, collectionId) { + const collection = state.cmf.collections[collectionId]; + if (collection == null) return undefined; + return collection; +} + +/** + * Get the items from a collection, handling both Map-wrapped and direct List forms. + * Covers the `Map.isMap(collection) ? collection.get('items') : collection` pattern + * from containers/src/List/selector.js. + * @param {Object} state + * @param {String} collectionId + * @returns {Array|undefined} + */ +function extractItems(collection) { + if (collection !== null && typeof collection === 'object' && !Array.isArray(collection)) { + return collection.items; + } + return collection; +} + +export function getCollectionItems(state, collectionId) { + const collection = state.cmf.collections[collectionId]; + if (collection == null) return undefined; + const items = extractItems(collection); + if (items == null) return undefined; + return items; +} + +/** + * Find an item in a collection by its id field, returning a plain JS object. + * Covers the `.find(r => r.get('id') === id)` pattern from + * containers/src/DeleteResource/DeleteResource.connect.js. + * @param {Object} state + * @param {String} collectionId + * @param {String} itemId + * @returns {Object|undefined} + */ +export function getCollectionItem(state, collectionId, itemId) { + const collection = state.cmf.collections[collectionId]; + if (collection == null) return undefined; + const items = extractItems(collection); + if (!items || !Array.isArray(items)) return undefined; + const found = items.find(item => item && item.id === itemId); + if (found == null) return undefined; + return found; +} diff --git a/packages/cmf/src/selectors/collections.test.js b/packages/cmf/src/selectors/collections.test.js new file mode 100644 index 00000000000..6426b779a26 --- /dev/null +++ b/packages/cmf/src/selectors/collections.test.js @@ -0,0 +1,178 @@ +import { describe, it, expect } from 'vitest'; +import { + get, + getAll, + findListItem, + toJS, + getCollectionPlain, + getCollectionItems, + getCollectionItem, +} from './collections'; + +describe('collections.get', () => { + const collection = { id: 'id' }; + const collectionSubset = { subset: 'subset' }; + const collectionWithSubset = { collectionSubset }; + const state = { + cmf: { + collections: { + collection, + collectionWithSubset, + }, + }, + }; + + it('returns collection when path is a string', () => { + expect(get(state, 'collection')).toEqual(collection); + }); + + it('returns collection subset when path is an array', () => { + expect(get(state, ['collectionWithSubset', 'collectionSubset'])).toEqual(collectionSubset); + }); + + it('throws when path is not a string or array', () => { + expect(() => get(state, {})).toThrow( + 'Type mismatch: collectionPath should be a string or an array of string', + ); + }); +}); + +describe('collections.getAll', () => { + const collectionsMap = { foo: { bar: 'baz' } }; + const state = { cmf: { collections: collectionsMap } }; + + it('returns the entire collections object', () => { + expect(getAll(state)).toBe(collectionsMap); + }); +}); + +describe('collections.findListItem', () => { + const id = 'id'; + const item = { id }; + const state = { + cmf: { + collections: { + isList: [item], + isNotList: { id: item }, + }, + }, + }; + + it('finds an item by id in an Array collection', () => { + expect(findListItem(state, 'isList', id)).toBe(item); + }); + + it('returns undefined when id does not match', () => { + expect(findListItem(state, 'isList', 'notFound')).toBeUndefined(); + }); + + it('throws when collection is not an Array', () => { + expect(() => findListItem(state, 'isNotList', id)).toThrow('Type mismatch'); + }); +}); + +describe('collections.toJS', () => { + const state = { + cmf: { + collections: { + foo: { bar: { hello: 'world' } }, + }, + }, + }; + + it('returns the plain JS value at the given path', () => { + expect(toJS(state, 'foo.bar')).toEqual({ hello: 'world' }); + }); + + it('memoizes the result across identical state references', () => { + const result1 = toJS(state, 'foo.bar'); + const result2 = toJS(state, 'foo.bar'); + expect(result1).toBe(result2); + }); +}); + +describe('collections.getCollectionPlain', () => { + const state = { + cmf: { + collections: { + myList: [{ id: '1', name: 'Alice' }], + }, + }, + }; + + it('returns the collection as-is when collectionId is found', () => { + const result = getCollectionPlain(state, 'myList'); + expect(result).toEqual([{ id: '1', name: 'Alice' }]); + expect(result).not.toHaveProperty('get'); + }); + + it('returns undefined when collectionId is not found', () => { + expect(getCollectionPlain(state, 'notFound')).toBeUndefined(); + }); +}); + +describe('collections.getCollectionItems', () => { + const state = { + cmf: { + collections: { + directList: [{ id: '1' }], + wrappedList: { items: [{ id: '2' }] }, + emptyWrapped: { other: 'value' }, + }, + }, + }; + + it('returns array when collection is a direct Array', () => { + const result = getCollectionItems(state, 'directList'); + expect(result).toEqual([{ id: '1' }]); + expect(Array.isArray(result)).toBe(true); + }); + + it('returns array from items key when collection is a plain object with items', () => { + const result = getCollectionItems(state, 'wrappedList'); + expect(result).toEqual([{ id: '2' }]); + expect(Array.isArray(result)).toBe(true); + }); + + it('returns undefined when collection is a plain object without items key', () => { + expect(getCollectionItems(state, 'emptyWrapped')).toBeUndefined(); + }); + + it('returns undefined when collectionId is not found', () => { + expect(getCollectionItems(state, 'notFound')).toBeUndefined(); + }); +}); + +describe('collections.getCollectionItem', () => { + const items = [ + { id: 'a', label: 'Alpha' }, + { id: 'b', label: 'Beta' }, + ]; + const state = { + cmf: { + collections: { + directList: items, + wrappedList: { items }, + }, + }, + }; + + it('finds item by id in a direct Array collection', () => { + const result = getCollectionItem(state, 'directList', 'a'); + expect(result).toEqual({ id: 'a', label: 'Alpha' }); + expect(result).not.toHaveProperty('get'); + }); + + it('finds item by id in an object-wrapped collection', () => { + const result = getCollectionItem(state, 'wrappedList', 'b'); + expect(result).toEqual({ id: 'b', label: 'Beta' }); + }); + + it('returns undefined when item id is not found', () => { + expect(getCollectionItem(state, 'directList', 'xxx')).toBeUndefined(); + }); + + it('returns undefined when collection is not found', () => { + expect(getCollectionItem(state, 'notFound', 'a')).toBeUndefined(); + }); +}); diff --git a/packages/cmf/src/selectors/components.js b/packages/cmf/src/selectors/components.js new file mode 100644 index 00000000000..83e1748ddaf --- /dev/null +++ b/packages/cmf/src/selectors/components.js @@ -0,0 +1,45 @@ +/** + * Selectors for CMF component state (state.cmf.components). + * All selectors return plain JS values — no Immutable objects are leaked. + */ + +/** + * Get the state of a specific component instance as a plain JS object. + * @param {Object} state - Redux state + * @param {String} componentName - Component name (e.g., 'Container(Notification)') + * @param {String} instanceId - Component instance id (e.g., 'default') + * @returns {Object|undefined} + */ +export function getComponentState(state, componentName, instanceId) { + return state.cmf.components?.[componentName]?.[instanceId]; +} + +/** + * Get all instances of a component as a map of instanceId → plain JS state. + * @param {Object} state - Redux state + * @param {String} componentName - Component name + * @returns {Object|undefined} + */ +export function getAllComponentStates(state, componentName) { + return state.cmf.components?.[componentName]; +} + +/** + * Get a specific property from a component instance state as a plain value. + * @param {Object} state - Redux state + * @param {String} componentName - Component name + * @param {String} instanceId - Instance id + * @param {String} property - Property key + * @param {*} defaultValue - Default value if not found + * @returns {*} plain JS value + */ +export function getComponentStateProperty( + state, + componentName, + instanceId, + property, + defaultValue, +) { + const val = state.cmf.components?.[componentName]?.[instanceId]?.[property]; + return val ?? defaultValue; +} diff --git a/packages/cmf/src/selectors/components.test.js b/packages/cmf/src/selectors/components.test.js new file mode 100644 index 00000000000..66ea1bda828 --- /dev/null +++ b/packages/cmf/src/selectors/components.test.js @@ -0,0 +1,104 @@ +import { describe, it, expect } from 'vitest'; +import { getComponentState, getAllComponentStates, getComponentStateProperty } from './components'; + +const makeState = componentsMap => ({ + cmf: { + components: componentsMap, + }, +}); + +describe('components.getComponentState', () => { + const state = makeState({ + 'Container(List)': { + default: { searchQuery: 'hello', sortBy: 'name' }, + other: { searchQuery: 'world' }, + }, + }); + + it('returns plain JS object for a known component instance', () => { + const result = getComponentState(state, 'Container(List)', 'default'); + expect(result).toEqual({ searchQuery: 'hello', sortBy: 'name' }); + expect(typeof result.get).toBe('undefined'); + }); + + it('returns undefined for an unknown component name', () => { + expect(getComponentState(state, 'Unknown', 'default')).toBeUndefined(); + }); + + it('returns undefined for an unknown instance id', () => { + expect(getComponentState(state, 'Container(List)', 'missing')).toBeUndefined(); + }); +}); + +describe('components.getAllComponentStates', () => { + const state = makeState({ + 'Container(List)': { + default: { searchQuery: '' }, + secondary: { searchQuery: 'test' }, + }, + }); + + it('returns all instances as plain JS object', () => { + const result = getAllComponentStates(state, 'Container(List)'); + expect(result).toEqual({ + default: { searchQuery: '' }, + secondary: { searchQuery: 'test' }, + }); + expect(typeof result.get).toBe('undefined'); + }); + + it('returns undefined for an unknown component name', () => { + expect(getAllComponentStates(state, 'Unknown')).toBeUndefined(); + }); +}); + +describe('components.getComponentStateProperty', () => { + const state = makeState({ + 'Container(Notification)': { + Notification: { + notifications: [{ id: '1', message: 'Hi' }], + }, + }, + 'Container(List)': { + default: { + searchQuery: 'test', + }, + }, + }); + + it('returns plain scalar value for a string property', () => { + const result = getComponentStateProperty(state, 'Container(List)', 'default', 'searchQuery'); + expect(result).toBe('test'); + }); + + it('returns plain array for a notifications property', () => { + const result = getComponentStateProperty( + state, + 'Container(Notification)', + 'Notification', + 'notifications', + ); + expect(result).toEqual([{ id: '1', message: 'Hi' }]); + expect(Array.isArray(result)).toBe(true); + }); + + it('returns undefined for an unknown component', () => { + expect(getComponentStateProperty(state, 'Unknown', 'default', 'foo')).toBeUndefined(); + }); + + it('returns undefined for a missing property', () => { + expect( + getComponentStateProperty(state, 'Container(List)', 'default', 'notExist'), + ).toBeUndefined(); + }); + + it('returns defaultValue when component state is not found', () => { + expect(getComponentStateProperty(state, 'Unknown', 'default', 'foo', [])).toEqual([]); + }); + + it('returns defaultValue when property is not found', () => { + expect( + getComponentStateProperty(state, 'Container(List)', 'default', 'notExist', 'fallback'), + ).toBe('fallback'); + }); +}); diff --git a/packages/cmf/src/selectors/index.js b/packages/cmf/src/selectors/index.js index cbedd0f3a45..aedec3e58a7 100644 --- a/packages/cmf/src/selectors/index.js +++ b/packages/cmf/src/selectors/index.js @@ -1,7 +1,9 @@ import * as collections from './collections'; +import * as components from './components'; import toJS from './toJS'; export default { collections, + components, toJS, }; diff --git a/packages/cmf/src/selectors/index.test.js b/packages/cmf/src/selectors/index.test.js new file mode 100644 index 00000000000..aa9a35f6151 --- /dev/null +++ b/packages/cmf/src/selectors/index.test.js @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import selectors from './index'; +import * as collections from './collections'; +import * as components from './components'; +import toJS from './toJS'; + +describe('selectors index', () => { + it('exposes collections selectors', () => { + expect(selectors.collections).toEqual(collections); + }); + + it('exposes components selectors', () => { + expect(selectors.components).toEqual(components); + }); + + it('exposes toJS selector', () => { + expect(selectors.toJS).toEqual(toJS); + }); +}); diff --git a/packages/cmf/src/selectors/toJS.js b/packages/cmf/src/selectors/toJS.js index a83dbdfe6d7..5e38e0fe858 100644 --- a/packages/cmf/src/selectors/toJS.js +++ b/packages/cmf/src/selectors/toJS.js @@ -1,19 +1,15 @@ function toJS(data) { - if (data) { - if (typeof data.toJS === 'function') { - return data.toJS(); - } - throw new Error('the selector return a data which is not an immutable'); - } - return undefined; + return data; } /** - * toJS is an higher order selector. - * It modify a given selector to return the value as a POJO + * toJS is a higher order selector. + * The store is now plain JS — this wrapper is retained for backward compatibility + * and memoization only (prevents unnecessary re-renders when selector result is the + * same reference). No conversion is performed. * Note: your selector must use only one selector * @param {function} selector the selector - * @returns the POJO associated to the given selector + * @returns the value returned by the given selector (already a plain JS value) */ export default function getToJSMemoized(selector) { if (typeof selector !== 'function') { diff --git a/packages/cmf/src/test-setup.ts b/packages/cmf/src/test-setup.ts index 5ef1d222349..020160b3a1e 100644 --- a/packages/cmf/src/test-setup.ts +++ b/packages/cmf/src/test-setup.ts @@ -8,3 +8,14 @@ vi.mock('@talend/utils', async () => { randomUUID: () => '42', }; }); + +// Node v25+ declares `localStorage` on globalThis, which prevents vitest/jsdom from +// overriding it with jsdom's Storage. Use the JSDOM instance directly to fix this. +// @ts-expect-error - jsdom is set by vitest's jsdom environment +const jsdomLocalStorage = globalThis.jsdom?.window?.localStorage; +if (jsdomLocalStorage) { + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: jsdomLocalStorage, + }); +} diff --git a/packages/cmf/vitest.config.ts b/packages/cmf/vitest.config.ts index fa9bc4d0176..a4e455ba00b 100644 --- a/packages/cmf/vitest.config.ts +++ b/packages/cmf/vitest.config.ts @@ -5,7 +5,8 @@ export default defineConfig({ plugins: [react({ include: /\.[jt]sx?$/ })], esbuild: { loader: 'tsx', - include: /src\/.*\.[jt]sx?$/, + include: /(src|__tests__)\/.*\.[jt]sx?$/, + exclude: /node_modules/, jsx: 'automatic', tsconfigRaw: { compilerOptions: { @@ -16,8 +17,13 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', + environmentOptions: { + jsdom: { + url: 'http://localhost', + }, + }, setupFiles: ['src/test-setup.ts'], - include: ['src/**/*.test.{js,jsx,ts,tsx}'], + include: ['src/**/*.test.{js,jsx,ts,tsx}', '__tests__/**/*.test.{js,jsx,ts,tsx}'], exclude: ['lib/**', 'lib-esm/**'], coverage: { provider: 'v8', diff --git a/packages/components/package.json b/packages/components/package.json index 5fdeb746307..c2d8ff24530 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -59,7 +59,6 @@ "date-fns": "^3.6.0", "dom-helpers": "^3.4.0", "focus-outline-manager": "^1.0.2", - "immutable": "^3.8.2", "invariant": "^2.2.4", "lodash": "^4.17.23", "memoize-one": "^6.0.0", @@ -68,7 +67,6 @@ "react-debounce-input": "^3.3.0", "react-draggable": "^4.5.0", "react-grid-layout": "^1.5.3", - "react-immutable-proptypes": "^2.2.0", "react-is": "^18.3.1", "react-popper": "^2.3.0", "react-transition-group": "^2.9.0", diff --git a/packages/components/src/Actions/ActionDropdown/ActionDropdown.component.jsx b/packages/components/src/Actions/ActionDropdown/ActionDropdown.component.jsx index 36c0350e3c3..d15143b801e 100644 --- a/packages/components/src/Actions/ActionDropdown/ActionDropdown.component.jsx +++ b/packages/components/src/Actions/ActionDropdown/ActionDropdown.component.jsx @@ -1,10 +1,8 @@ import { Component } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import get from 'lodash/get'; import classNames from 'classnames'; -import { Iterable } from 'immutable'; import { DropdownButton, MenuItem } from '@talend/react-bootstrap'; import { withTranslation } from 'react-i18next'; import omit from 'lodash/omit'; @@ -96,10 +94,6 @@ function renderMutableMenuItem(item, index, getComponent) { } function getMenuItem(item, index, getComponent) { - if (Iterable.isIterable(item)) { - return renderMutableMenuItem(item.toJS(), index, getComponent); - } - return renderMutableMenuItem(item, index, getComponent); } @@ -265,7 +259,8 @@ class ActionDropdown extends Component { }} noCaret > - {!children && !items.length && !items.size && !loading && !components && ( + {/* items.size: backward-compat guard removed — items is always a plain array */} + {!children && !items.length && !loading && !components && ( {t('ACTION_DROPDOWN_EMPTY', { defaultValue: 'No options' })} @@ -315,16 +310,13 @@ ActionDropdown.propTypes = { pullRight: PropTypes.bool, icon: PropTypes.string, iconTransform: PropTypes.string, - items: PropTypes.oneOfType([ - PropTypes.arrayOf( - PropTypes.shape({ - icon: PropTypes.string, - label: PropTypes.string, - ...MenuItem.propTypes, - }), - ), - ImmutablePropTypes.list, - ]), + items: PropTypes.arrayOf( + PropTypes.shape({ + icon: PropTypes.string, + label: PropTypes.string, + ...MenuItem.propTypes, + }), + ), badge: PropTypes.shape({ className: PropTypes.string, label: PropTypes.string, diff --git a/packages/components/src/Actions/ActionDropdown/ActionDropdown.snapshot.test.jsx b/packages/components/src/Actions/ActionDropdown/ActionDropdown.snapshot.test.jsx index b0e3ba26459..92a26dae74d 100644 --- a/packages/components/src/Actions/ActionDropdown/ActionDropdown.snapshot.test.jsx +++ b/packages/components/src/Actions/ActionDropdown/ActionDropdown.snapshot.test.jsx @@ -1,4 +1,3 @@ -import Immutable from 'immutable'; import { render, screen, within } from '@testing-library/react'; import ActionDropdown from './ActionDropdown.component'; @@ -13,7 +12,6 @@ const items = [ onClick: jest.fn(), }, ]; -const immutableItems = Immutable.fromJS(items); describe('ActionDropdown', () => { it('should render a button dropdown with its menu', () => { @@ -32,26 +30,6 @@ describe('ActionDropdown', () => { expect(screen.getByRole('menu')).toBeInTheDocument(); }); - it('should render the same as when plain object or immutable list', () => { - // given - const props = { - id: 'dropdown-id', - label: 'related items', - items, - }; - const immutableProps = { - ...props, - items: immutableItems, - }; - - // when - render(); - - // then - expect(screen.getByRole('button')).toBeInTheDocument(); - expect(screen.getByRole('menu')).toBeInTheDocument(); - }); - it('should render a button with icon and label', () => { // given const props = { diff --git a/packages/components/src/Actions/ActionDropdown/ActionDropdown.test.jsx b/packages/components/src/Actions/ActionDropdown/ActionDropdown.test.jsx index 521ba6be240..8e470fd7ae6 100644 --- a/packages/components/src/Actions/ActionDropdown/ActionDropdown.test.jsx +++ b/packages/components/src/Actions/ActionDropdown/ActionDropdown.test.jsx @@ -126,6 +126,23 @@ describe('getMenuItem', () => { }); }); +// Verify items accepts plain arrays. +describe('ActionDropdown — plain array items', () => { + it('should render all items from a plain JS array', () => { + const props = { + id: 'dropdown-id', + label: 'Dropdown', + items: [ + { id: 'item1', label: 'Alpha' }, + { id: 'item2', label: 'Beta' }, + ], + }; + render(); + expect(screen.getByRole('menuitem', { name: 'Alpha' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Beta' })).toBeInTheDocument(); + }); +}); + describe('InjectDropdownMenuItem', () => { it('should render MenuItem with props divider', () => { render( diff --git a/packages/components/src/Actions/ActionDropdown/Dropdown.stories.jsx b/packages/components/src/Actions/ActionDropdown/Dropdown.stories.jsx index e2b9783bcf8..3484f01c117 100644 --- a/packages/components/src/Actions/ActionDropdown/Dropdown.stories.jsx +++ b/packages/components/src/Actions/ActionDropdown/Dropdown.stories.jsx @@ -1,6 +1,4 @@ /* eslint-disable no-console */ -import Immutable from 'immutable'; - import FilterBar from '../../FilterBar'; import Action from '../Action'; import ActionDropdown from './ActionDropdown.component'; @@ -59,10 +57,10 @@ const contentAndLoadingAdditionalContent = { ], }; -const withImmutable = { +const withPlainItems = { id: 'context-dropdown-related-items', - label: 'related immutable items', - items: Immutable.fromJS([ + label: 'related items', + items: [ { id: 'context-dropdown-item-document-1', icon: 'talend-file-json-o', @@ -79,10 +77,10 @@ const withImmutable = { 'data-feature': 'actiondropdown.items', onClick: () => console.log('document 2 click'), }, - ]), + ], }; -const openWithImmutable = { ...withImmutable, open: true }; +const openWithPlainItems = { ...withPlainItems, open: true }; const withComponents = { id: 'context-dropdown-custom-items', @@ -288,9 +286,9 @@ export const Default = {
-

With immutable items :

+

With plain object items :

- +

Loading additional content

@@ -300,9 +298,9 @@ export const Default = {
-

Opened and with immutable items :

-
- +

Opened and with plain object items :

+
+
), diff --git a/packages/components/src/DataViewer/Managers/TreeManager/TreeManager.container.js b/packages/components/src/DataViewer/Managers/TreeManager/TreeManager.container.js index c1907aafce6..1491d849fd3 100644 --- a/packages/components/src/DataViewer/Managers/TreeManager/TreeManager.container.js +++ b/packages/components/src/DataViewer/Managers/TreeManager/TreeManager.container.js @@ -1,16 +1,12 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Immutable from 'immutable'; export function addPathsToCollection(index, collection, paths, jsonpath) { - return collection.set(index, paths.push(jsonpath)); + return { ...collection, [index]: [...paths, jsonpath] }; } export function removePathsFromCollection(index, collection, paths, jsonpath) { - return collection.set( - index, - paths.filter(path => path !== jsonpath), - ); + return { ...collection, [index]: paths.filter(path => path !== jsonpath) }; } /** @@ -51,23 +47,23 @@ export default class TreeManager extends Component { this.state = { isAllExpanded: props.isAllExpanded || false, - collapsedNodes: props.collapsedNodes || Immutable.Map(), - expandedNodes: props.expandedNodes || Immutable.Map().set(0, Immutable.List(['$'])), + collapsedNodes: props.collapsedNodes || {}, + expandedNodes: props.expandedNodes || { 0: ['$'] }, }; } onExpandAll = () => { - this.setState(oldState => ({ + this.setState({ isAllExpanded: true, - collapsedNodes: oldState.collapsedNodes.clear(), - })); + collapsedNodes: {}, + }); }; onCollapseAll = () => { - this.setState(oldState => ({ + this.setState({ isAllExpanded: false, - expandedNodes: oldState.expandedNodes.clear(), - })); + expandedNodes: {}, + }); }; onToggle = (event, options, index) => { @@ -88,7 +84,7 @@ export default class TreeManager extends Component { index, collection, isAllExpanded, - collection.get(index, Immutable.List()), + collection[index] ?? [], options, ), }); @@ -98,7 +94,7 @@ export default class TreeManager extends Component { index, collection, isAllExpanded, - collection.get(index, Immutable.List()), + collection[index] ?? [], options, ), }); @@ -115,7 +111,7 @@ export default class TreeManager extends Component { onToggle: this.onToggle, onCollapseAll: this.onCollapseAll, onExpandAll: this.onExpandAll, - paths: isAllExpanded ? this.state.collapsedNodes.toJS() : this.state.expandedNodes.toJS(), + paths: isAllExpanded ? this.state.collapsedNodes : this.state.expandedNodes, highlighted: this.props.highlighted, isAllExpanded, }; diff --git a/packages/components/src/DataViewer/Managers/TreeManager/TreeManager.test.jsx b/packages/components/src/DataViewer/Managers/TreeManager/TreeManager.test.jsx index a9d4ce332ca..d483b56576d 100644 --- a/packages/components/src/DataViewer/Managers/TreeManager/TreeManager.test.jsx +++ b/packages/components/src/DataViewer/Managers/TreeManager/TreeManager.test.jsx @@ -1,7 +1,5 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import Immutable from 'immutable'; - import TreeManager, { addPathsToCollection, removePathsFromCollection, @@ -9,19 +7,19 @@ import TreeManager, { describe('addPathsToCollection', () => { it('should add the jsonpath to the paths collection', () => { - const myMap = Immutable.Map(); - const myList = Immutable.List(); + const myMap = {}; + const myList = []; const newMap = addPathsToCollection(0, myMap, myList, 'jsonpath'); - expect(newMap.get(0).toJS()).toEqual(['jsonpath']); + expect(newMap[0]).toEqual(['jsonpath']); }); }); describe('removePathsFromCollection', () => { it('should remove the jsonpath to the paths collection', () => { - const myList = Immutable.List(['jsonpath', 'somestuff']); - const myMap = Immutable.Map({ 0: myList }); + const myList = ['jsonpath', 'somestuff']; + const myMap = { 0: myList }; const newCollection = removePathsFromCollection(0, myMap, myList, 'jsonpath'); - expect(newCollection.get(0).toJS()).toEqual(['somestuff']); + expect(newCollection[0]).toEqual(['somestuff']); }); }); diff --git a/packages/components/src/HeaderBar/HeaderBar.stories.jsx b/packages/components/src/HeaderBar/HeaderBar.stories.jsx index a314d23c18f..b75d94664de 100644 --- a/packages/components/src/HeaderBar/HeaderBar.stories.jsx +++ b/packages/components/src/HeaderBar/HeaderBar.stories.jsx @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import Immutable from 'immutable'; import assetsApi from '@talend/assets-api'; import tokens from '@talend/design-tokens'; import AppSwitcher from '../AppSwitcher'; @@ -102,7 +101,7 @@ export default meta; export const Default = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); return ; }, parameters: { info: { styles: infoStyle } }, @@ -110,7 +109,7 @@ export const Default = { export const WithFullLogo = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); headerProps.logo.isFull = true; return ; }, @@ -119,10 +118,10 @@ export const WithFullLogo = { export const WithoutProducts = { render: () => { - const headerProps = Immutable.fromJS({ + const headerProps = structuredClone({ ...props, products: null, - }).toJS(); + }); headerProps.logo.isFull = true; return ; }, @@ -131,13 +130,13 @@ export const WithoutProducts = { export const WithBrandIcon = { render: () => { - const headerProps = Immutable.fromJS({ + const headerProps = structuredClone({ ...props, brand: { ...props.brand, icon: 'talend-tmc-negative', }, - }).toJS(); + }); return ; }, parameters: { info: { styles: infoStyle } }, @@ -145,13 +144,13 @@ export const WithBrandIcon = { export const WithBrandIconUrl = { render: () => { - const headerProps = Immutable.fromJS({ + const headerProps = structuredClone({ ...props, brand: { ...props.brand, iconUrl: assetsApi.getURL('/src/svg/products/tmc-negative.svg', '@talend/icons'), }, - }).toJS(); + }); return ; }, parameters: { info: { styles: infoStyle } }, @@ -159,7 +158,7 @@ export const WithBrandIconUrl = { export const WithEnvironmentDropdown = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); headerProps.env = { id: 'header-environment', items: [ @@ -177,7 +176,7 @@ export const WithEnvironmentDropdown = { export const WithUnreadNotifications = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); headerProps.notification = { hasUnread: true, }; @@ -188,7 +187,7 @@ export const WithUnreadNotifications = { export const WithReadNotifications = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); headerProps.notification = { hasUnread: false, }; @@ -199,7 +198,7 @@ export const WithReadNotifications = { export const WithHelpSplitDropdown = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); headerProps.help.items = [ { icon: 'talend-board', @@ -219,7 +218,7 @@ export const WithHelpSplitDropdown = { export const WithCallToAction = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); headerProps.callToAction = { bsStyle: 'info', className: 'btn-inverse', @@ -234,7 +233,7 @@ export const WithCallToAction = { export const WithGenericAction = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); headerProps.genericAction = { bsStyle: 'link', id: 'header-generic-action', @@ -249,7 +248,7 @@ export const WithGenericAction = { export const WithoutUserAndWithInformation = { render: () => { - const headerProps = Immutable.fromJS(props).toJS(); + const headerProps = structuredClone(props); headerProps.user = null; headerProps.information = { id: 'header-info', diff --git a/packages/components/src/test-setup.js b/packages/components/src/test-setup.js index 330c81b7444..3051abaa1c6 100644 --- a/packages/components/src/test-setup.js +++ b/packages/components/src/test-setup.js @@ -21,6 +21,16 @@ vi.mock('@talend/utils', async () => { globalThis.jest = vi; globalThis.xit = it.skip; +// Node v25+ declares `localStorage` on globalThis, which prevents vitest/jsdom from +// overriding it with jsdom's Storage. Use the JSDOM instance directly to fix this. +const jsdomLocalStorage = globalThis.jsdom?.window?.localStorage; +if (jsdomLocalStorage) { + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: jsdomLocalStorage, + }); +} + // Suppress React warnings in tests, as they are not relevant to the test results and can clutter the output. const originalConsoleError = console.error; vi.spyOn(console, 'error').mockImplementation((...args) => { diff --git a/packages/containers/.storybook/cmfModule/index.js b/packages/containers/.storybook/cmfModule/index.js index 080161a2cb6..2998b5065a2 100644 --- a/packages/containers/.storybook/cmfModule/index.js +++ b/packages/containers/.storybook/cmfModule/index.js @@ -1,4 +1,3 @@ -import { fromJS } from 'immutable'; import actionCreators from './actionCreators'; import components from './components'; import expressions from './expressions'; @@ -15,7 +14,7 @@ const cmfModule = { modules: [containersModule], preloadedState: { cmf: { - collections: fromJS({ + collections: { with: { data: [ { @@ -122,7 +121,7 @@ const cmfModule = { }, ], }, - }), + }, }, }, }; diff --git a/packages/containers/package.json b/packages/containers/package.json index 8b0f12847aa..2a1c5de5461 100644 --- a/packages/containers/package.json +++ b/packages/containers/package.json @@ -51,11 +51,9 @@ "@talend/react-forms": "^16.1.1", "@talend/utils": "^3.7.1", "classnames": "^2.5.1", - "immutable": "^3.8.2", "invariant": "^2.2.4", "lodash": "^4.17.23", "memoize-one": "^6.0.0", - "react-immutable-proptypes": "^2.2.0", "redux-saga": "^1.4.2", "reselect": "^2.5.4" }, diff --git a/packages/containers/src/AboutDialog/AboutDialog.container.jsx b/packages/containers/src/AboutDialog/AboutDialog.container.jsx index 589390db39b..568ceb1c276 100644 --- a/packages/containers/src/AboutDialog/AboutDialog.container.jsx +++ b/packages/containers/src/AboutDialog/AboutDialog.container.jsx @@ -1,14 +1,13 @@ import { Component as RComponent } from 'react'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; -import { Map } from 'immutable'; import { cmfConnect } from '@talend/react-cmf'; import Component from '@talend/react-components/lib/AboutDialog'; import Constants from './AboutDialog.constant'; -export const DEFAULT_STATE = new Map({ +export const DEFAULT_STATE = { expanded: false, -}); +}; class AboutDialog extends RComponent { static displayName = 'Container(AboutDialog)'; @@ -26,7 +25,7 @@ class AboutDialog extends RComponent { } toggle() { - this.props.setState(({ state }) => ({ expanded: !state.get('expanded') })); + this.props.setState(({ state }) => ({ expanded: !state?.expanded })); } hide() { @@ -39,9 +38,9 @@ class AboutDialog extends RComponent { ); diff --git a/packages/containers/src/AboutDialog/AboutDialog.test.js b/packages/containers/src/AboutDialog/AboutDialog.test.js index 3ace64157b7..56a49584ad7 100644 --- a/packages/containers/src/AboutDialog/AboutDialog.test.js +++ b/packages/containers/src/AboutDialog/AboutDialog.test.js @@ -1,6 +1,5 @@ -import { Map, List } from 'immutable'; - import Container from './AboutDialog.container'; + import Connected, { mapStateToProps } from './AboutDialog.connect'; import Constants from './AboutDialog.constant'; @@ -13,11 +12,7 @@ describe('Connected AboutDialog', () => { it('should mapStateToProps with an empty list of products', () => { const state = { cmf: { - collections: new Map({ - AboutDialog: { - [Constants.COLLECTION_ID]: new List(), - }, - }), + collections: {}, }, }; const ownProps = {}; diff --git a/packages/containers/src/ActionDropdown/ActionDropdown.connect.jsx b/packages/containers/src/ActionDropdown/ActionDropdown.connect.jsx index 14e8bdd992c..7e9c11bf6cf 100644 --- a/packages/containers/src/ActionDropdown/ActionDropdown.connect.jsx +++ b/packages/containers/src/ActionDropdown/ActionDropdown.connect.jsx @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import cmf, { cmfConnect } from '@talend/react-cmf'; import { ActionDropdown } from '@talend/react-components/lib/Actions'; import omit from 'lodash/omit'; @@ -50,7 +49,7 @@ export function ContainerActionDropdown({ items, ...props }) { ContainerActionDropdown.displayName = 'Container(ActionDropdown)'; ContainerActionDropdown.propTypes = { - items: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), ImmutablePropTypes.list]), + items: PropTypes.arrayOf(PropTypes.object), noCaret: PropTypes.bool, pullRight: PropTypes.bool, hideLabel: PropTypes.bool, diff --git a/packages/containers/src/ActionDropdown/ActionDropdown.stories.jsx b/packages/containers/src/ActionDropdown/ActionDropdown.stories.jsx index 7acc636b1da..4df59b65ab2 100644 --- a/packages/containers/src/ActionDropdown/ActionDropdown.stories.jsx +++ b/packages/containers/src/ActionDropdown/ActionDropdown.stories.jsx @@ -1,5 +1,4 @@ /* eslint-disable react/prop-types */ -import Immutable from 'immutable'; import { action } from 'storybook/actions'; import ActionDropdown from '.'; @@ -45,7 +44,7 @@ export function Default({ onSelect }) { displayMode: 'dropdown', label: 'my immutable items', onSelect, - items: Immutable.fromJS([ + items: [ { id: 'item1', label: 'First immutable label', @@ -54,7 +53,7 @@ export function Default({ onSelect }) { id: 'item2', label: '2nd immutable', }, - ]), + ], }; return ( diff --git a/packages/containers/src/AppLoader/AppLoader.connect.jsx b/packages/containers/src/AppLoader/AppLoader.connect.jsx index 23b2a2c8569..43796e71fd5 100644 --- a/packages/containers/src/AppLoader/AppLoader.connect.jsx +++ b/packages/containers/src/AppLoader/AppLoader.connect.jsx @@ -51,8 +51,8 @@ AppLoaderContainer.propTypes = { */ export function mapStateToProps(state, ownProps) { return { - loading: !get(ownProps, 'hasCollections', []).every(collectionName => - state.cmf.collections.has(collectionName), + loading: !get(ownProps, 'hasCollections', []).every( + collectionName => collectionName in state.cmf.collections, ), }; } diff --git a/packages/containers/src/AppLoader/AppLoader.connect.test.js b/packages/containers/src/AppLoader/AppLoader.connect.test.js index f2feea27f5a..c8aac1d2a0c 100644 --- a/packages/containers/src/AppLoader/AppLoader.connect.test.js +++ b/packages/containers/src/AppLoader/AppLoader.connect.test.js @@ -1,4 +1,3 @@ -import Immutable from 'immutable'; import { render, screen } from '@testing-library/react'; import { AppLoaderContainer, mapStateToProps } from './AppLoader.connect'; @@ -27,7 +26,7 @@ describe('AppLoader container', () => { describe('mapStateToProps', () => { it('should return loading to false if we have nothing to wait', () => { // given - const state = { cmf: { collections: Immutable.Map() } }; + const state = { cmf: { collections: {} } }; const ownProps = {}; // when const result = mapStateToProps(state, ownProps); @@ -37,7 +36,7 @@ describe('AppLoader container', () => { it('should return loading to true if there is something to wait', () => { // given - const state = { cmf: { collections: Immutable.Map({ test2: Immutable.Map() }) } }; + const state = { cmf: { collections: { test2: null } } }; const ownProps = { hasCollections: ['test', 'test2'] }; // when const result = mapStateToProps(state, ownProps); @@ -49,7 +48,7 @@ describe('AppLoader container', () => { // given const state = { cmf: { - collections: Immutable.Map({ test2: Immutable.Map(), test: Immutable.Map() }), + collections: { test2: null, test: null }, }, }; const ownProps = { hasCollections: ['test', 'test2'] }; diff --git a/packages/containers/src/Breadcrumbs/Breadcrumbs.connect.jsx b/packages/containers/src/Breadcrumbs/Breadcrumbs.connect.jsx index 33c006c34e0..d8b6013dbe6 100644 --- a/packages/containers/src/Breadcrumbs/Breadcrumbs.connect.jsx +++ b/packages/containers/src/Breadcrumbs/Breadcrumbs.connect.jsx @@ -1,11 +1,10 @@ import { cmfConnect } from '@talend/react-cmf'; -import { Map } from 'immutable'; import Breadcrumbs from '@talend/react-components/lib/Breadcrumbs'; -const DEFAULT_STATE = Map(); +const DEFAULT_STATE = {}; export function ContainerBreadcrumbs({ state = DEFAULT_STATE, dispatchActionCreator, ...props }) { - const items = state.get('items', props.items); + const items = state?.items ?? props.items; const newProps = { ...props, items: @@ -14,8 +13,8 @@ export function ContainerBreadcrumbs({ state = DEFAULT_STATE, dispatchActionCrea ...item, onClick: (event, data) => dispatchActionCreator(item.actionCreator, event, data), })), - loading: state.get('loading', props.loading), - maxItems: state.get('maxItems', props.maxItems), + loading: state?.loading ?? props.loading, + maxItems: state?.maxItems ?? props.maxItems, }; return ; @@ -28,7 +27,7 @@ ContainerBreadcrumbs.propTypes = { }; export default cmfConnect({ - defaultState: Map({ items: [], maxItems: 10 }), + defaultState: { items: [], maxItems: 10 }, omitCMFProps: true, withComponentRegistry: true, withDispatchActionCreator: true, diff --git a/packages/containers/src/Breadcrumbs/Breadcrumbs.stories.jsx b/packages/containers/src/Breadcrumbs/Breadcrumbs.stories.jsx index 9eb97fb4039..a84c24c91da 100644 --- a/packages/containers/src/Breadcrumbs/Breadcrumbs.stories.jsx +++ b/packages/containers/src/Breadcrumbs/Breadcrumbs.stories.jsx @@ -1,7 +1,6 @@ -import { Map } from 'immutable'; import Breadcrumbs from '.'; -const initialState = new Map({ +const initialState = { items: [ { text: 'Text A', title: 'Text title A', actionCreator: 'breadcrumb:folder:openA' }, { text: 'Text B', title: 'Text title B', actionCreator: 'breadcrumb:folder:openB' }, @@ -12,7 +11,7 @@ const initialState = new Map({ }, ], maxItems: 3, -}); +}; export default { title: 'Breadcrumb', diff --git a/packages/containers/src/ComponentForm/ComponentForm.component.jsx b/packages/containers/src/ComponentForm/ComponentForm.component.jsx index 02c01815e5b..7af449e4e26 100644 --- a/packages/containers/src/ComponentForm/ComponentForm.component.jsx +++ b/packages/containers/src/ComponentForm/ComponentForm.component.jsx @@ -4,7 +4,6 @@ import cmf, { cmfConnect } from '@talend/react-cmf'; import Form from '@talend/react-forms'; import omit from 'lodash/omit'; import get from 'lodash/get'; -import { Map } from 'immutable'; import memoizeOne from 'memoize-one'; import kit from './kit'; import tcompFieldsWidgets from './fields'; @@ -20,19 +19,19 @@ const TO_OMIT = [ ...cmfConnect.INJECTED_PROPS, ]; -export const DEFAULT_STATE = new Map({ +export const DEFAULT_STATE = { dirty: false, - initialState: new Map(), -}); + initialState: {}, +}; /** - * Convert immutable object to js object + * Returns object or null */ -export function toJS(immutableObject) { - if (!immutableObject) { +export function toJS(object) { + if (!object) { return null; } - return immutableObject.toJS(); + return object; } /** @@ -102,9 +101,9 @@ export class TCompForm extends Component { } componentDidUpdate(prevProps) { - const nextProperties = this.props.state.get('properties'); - if (prevProps.state.get('properties') !== nextProperties) { - this.setState({ properties: nextProperties?.toJS() || {} }); + const nextProperties = this.props.state.properties; + if (prevProps.state.properties !== nextProperties) { + this.setState({ properties: nextProperties || {} }); } if ( @@ -123,7 +122,7 @@ export class TCompForm extends Component { } onChange(_, payload) { - if (!this.props.state.get('dirty')) { + if (!this.props.state.dirty) { this.props.setState({ dirty: true }); } @@ -184,15 +183,15 @@ export class TCompForm extends Component { } onReset() { - this.props.setState(prev => - prev.state - .set('jsonSchema', this.props.state.getIn(['initialState', 'jsonSchema'])) - .set('uiSchema', this.props.state.getIn(['initialState', 'uiSchema'])) - .set('properties', this.props.state.getIn(['initialState', 'properties'])) - .set('dirty', false), - ); + const { initialState } = this.props.state; + this.props.setState({ + jsonSchema: initialState?.jsonSchema, + uiSchema: initialState?.uiSchema, + properties: initialState?.properties, + dirty: false, + }); this.setState({ - properties: this.props.state.getIn(['initialState', 'properties']).toJS(), + properties: initialState?.properties || {}, }); } @@ -213,8 +212,8 @@ export class TCompForm extends Component { getUISpec() { return { properties: this.state.properties, - jsonSchema: this.getMemoizedJsonSchema(this.props.state.get('jsonSchema')), - uiSchema: this.getMemoizedUiSchema(this.props.state.get('uiSchema')), + jsonSchema: this.getMemoizedJsonSchema(this.props.state.jsonSchema), + uiSchema: this.getMemoizedUiSchema(this.props.state.uiSchema), }; } @@ -222,9 +221,9 @@ export class TCompForm extends Component { const uiSpecs = this.getUISpec(); if (!uiSpecs.jsonSchema) { - const response = this.props.state.get('response'); + const response = this.props.state.response; if (response) { - return

{response.get('statusText')}

; + return

{response.statusText}

; } return
; } @@ -232,7 +231,7 @@ export class TCompForm extends Component { const props = { ...omit(this.props, TO_OMIT), data: uiSpecs, - initialData: this.getMemoizedInitialState(this.props.state.get('initialState')), + initialData: this.getMemoizedInitialState(this.props.state.initialState), onTrigger: this.onTrigger, onChange: this.onChange, onSubmit: this.onSubmit, diff --git a/packages/containers/src/ComponentForm/ComponentForm.saga.test.js b/packages/containers/src/ComponentForm/ComponentForm.saga.test.js index 7af55f47d89..69ca2a5a9b9 100644 --- a/packages/containers/src/ComponentForm/ComponentForm.saga.test.js +++ b/packages/containers/src/ComponentForm/ComponentForm.saga.test.js @@ -1,9 +1,8 @@ import { call, select, put } from 'redux-saga/effects'; -import { fromJS, Map } from 'immutable'; import cmf from '@talend/react-cmf'; import * as sagas from './ComponentForm.sagas'; -import ConnectedTCompForm, { TCompForm } from './ComponentForm.component'; +import { TCompForm } from './ComponentForm.component'; describe('ComponentForm saga', () => { describe('*checkFormComponentId', () => { @@ -72,14 +71,14 @@ describe('ComponentForm saga', () => { const selector = selectJsonSchema.payload.selector; const jsonSchemaSelection = selector({ cmf: { - components: fromJS({ + components: { [TCompForm.displayName]: { [props.componentId]: { jsonSchema } }, - }), + }, }, }); // then - expect(jsonSchemaSelection.toJS()).toEqual(jsonSchema); + expect(jsonSchemaSelection).toEqual(jsonSchema); }); it('should NOT fetch uiSpec when it is already fetched', () => { @@ -174,9 +173,9 @@ describe('ComponentForm saga', () => { function getReduxStore() { return { cmf: { - components: fromJS({ + components: { [TCompForm.displayName]: { [componentId]: {} }, - }), + }, }, }; } @@ -231,19 +230,19 @@ describe('ComponentForm saga', () => { const errorStep = gen.next({ response }).value; expect(errorStep.payload).toBeDefined(); expect(errorStep.type).toBe('PUT'); - const setStateAction = errorStep.payload.action(null, getReduxStore); + const setStateAction = errorStep.payload.action; // then expect(setStateAction).toEqual({ cmf: { componentState: { componentName: 'ComponentForm', - componentState: new Map({ + componentState: { jsonSchema: undefined, uiSchema: undefined, response, dirty: false, - }), + }, key: 'MyComponentId', type: 'REACT_CMF.COMPONENT_MERGE_STATE', }, @@ -348,15 +347,15 @@ describe('ComponentForm saga', () => { describe('onFormSubmit', () => { const componentId = 'form'; const prevState = { - cmf: { components: fromJS({ [TCompForm.displayName]: { [componentId]: {} } }) }, + cmf: { components: { [TCompForm.displayName]: { [componentId]: {} } } }, }; const mergeStatePayload = { cmf: { componentState: { componentName: 'ComponentForm', - componentState: fromJS({ + componentState: { initialState: { jsonSchema: undefined, uiSchema: undefined, properties: 'prop' }, - }), + }, key: 'form', type: 'REACT_CMF.COMPONENT_MERGE_STATE', }, @@ -441,15 +440,14 @@ describe('ComponentForm saga', () => { // when const gen = sagas.handleSetDirtyState({ componentId, dirty }); // then - expect(gen.next().value).toEqual(select(ConnectedTCompForm.getState, componentId)); - expect(gen.next(Map({})).value).toEqual( + expect(gen.next().value).toEqual( put({ cmf: { componentState: { componentName: 'ComponentForm', - componentState: Map({ + componentState: { dirty: true, - }), + }, key: 'myId', type: 'REACT_CMF.COMPONENT_MERGE_STATE', }, diff --git a/packages/containers/src/ComponentForm/ComponentForm.sagas.js b/packages/containers/src/ComponentForm/ComponentForm.sagas.js index eab521a4e2c..d258d2e4960 100644 --- a/packages/containers/src/ComponentForm/ComponentForm.sagas.js +++ b/packages/containers/src/ComponentForm/ComponentForm.sagas.js @@ -1,6 +1,5 @@ import { call, put, select, take, takeEvery, takeLatest } from 'redux-saga/effects'; import cmf from '@talend/react-cmf'; -import { fromJS } from 'immutable'; import get from 'lodash/get'; import Component from './ComponentForm.component'; import { COMPONENT_FORM_SET_DIRTY } from './ComponentForm.actions'; @@ -13,8 +12,7 @@ export function* fetchDefinition(action) { if (!response.ok) { yield put( Component.setStateAction( - prev => - prev.set('jsonSchema').set('uiSchema').set('response', response).set('dirty', false), + { jsonSchema: undefined, uiSchema: undefined, response, dirty: false }, action.componentId, ), ); @@ -45,9 +43,7 @@ export function* onDidMount({ uiSpecPath, data, }) { - const jsonSchema = yield select(state => - Component.getState(state, componentId).get('jsonSchema'), - ); + const jsonSchema = yield select(state => Component.getState(state, componentId)?.jsonSchema); if (!jsonSchema) { if (definition) { yield put( @@ -74,11 +70,15 @@ export function* onFormSubmit(componentId, submitURL, action) { } yield put( Component.setStateAction( - prev => - prev - .setIn(['initialState', 'jsonSchema'], prev.get('jsonSchema')) - .setIn(['initialState', 'uiSchema'], prev.get('uiSchema')) - .setIn(['initialState', 'properties'], fromJS(action.properties)), + prev => ({ + ...prev, + initialState: { + ...(prev.initialState ?? {}), + jsonSchema: prev.jsonSchema, + uiSchema: prev.uiSchema, + properties: action.properties, + }, + }), componentId, )(undefined, getReduxState), ); @@ -111,8 +111,7 @@ export function checkFormComponentId(componentId, actionType) { * @param {object} reduxAction with a componentId (string) & the dirtyState (boolean) to apply */ export function* handleSetDirtyState({ componentId, dirty }) { - const componentFormState = yield select(Component.getState, componentId); - yield put(Component.setStateAction(componentFormState.set('dirty', !!dirty), componentId)); + yield put(Component.setStateAction({ dirty: !!dirty }, componentId)); } export function* handle(props) { diff --git a/packages/containers/src/ComponentForm/ComponentForm.selectors.js b/packages/containers/src/ComponentForm/ComponentForm.selectors.js index fe4120b1073..9c36cdede70 100644 --- a/packages/containers/src/ComponentForm/ComponentForm.selectors.js +++ b/packages/containers/src/ComponentForm/ComponentForm.selectors.js @@ -1,7 +1,7 @@ import ConnectedComponentForm from './ComponentForm.component'; export function isComponentFormDirty(state, componentName) { - return ConnectedComponentForm.getState(state, componentName).get('dirty', false); + return ConnectedComponentForm.getState(state, componentName)?.dirty ?? false; } export default isComponentFormDirty; diff --git a/packages/containers/src/ComponentForm/ComponentForm.selectors.test.js b/packages/containers/src/ComponentForm/ComponentForm.selectors.test.js index a96d2fa17b6..2f530b5ee30 100644 --- a/packages/containers/src/ComponentForm/ComponentForm.selectors.test.js +++ b/packages/containers/src/ComponentForm/ComponentForm.selectors.test.js @@ -1,4 +1,3 @@ -import Immutable from 'immutable'; import { isComponentFormDirty } from './ComponentForm.selectors'; import { TCompForm } from './ComponentForm.component'; @@ -9,11 +8,9 @@ describe('ComponentForm selectors', () => { // given const state = { cmf: { - components: Immutable.fromJS({ - [TCompForm.displayName]: { - [componentName]: {}, - }, - }), + components: { + [TCompForm.displayName]: { [componentName]: {} }, + }, }, }; // when @@ -26,11 +23,9 @@ describe('ComponentForm selectors', () => { // given const state = { cmf: { - components: Immutable.fromJS({ - [TCompForm.displayName]: { - [componentName]: { dirty: false }, - }, - }), + components: { + [TCompForm.displayName]: { [componentName]: { dirty: false } }, + }, }, }; // when @@ -43,11 +38,9 @@ describe('ComponentForm selectors', () => { // given const state = { cmf: { - components: Immutable.fromJS({ - [TCompForm.displayName]: { - [componentName]: { dirty: true }, - }, - }), + components: { + [TCompForm.displayName]: { [componentName]: { dirty: true } }, + }, }, }; // when diff --git a/packages/containers/src/ComponentForm/ComponentForm.test.js b/packages/containers/src/ComponentForm/ComponentForm.test.js index c928fff7cdf..3c18e0da754 100644 --- a/packages/containers/src/ComponentForm/ComponentForm.test.js +++ b/packages/containers/src/ComponentForm/ComponentForm.test.js @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/react'; -import { fromJS, Map } from 'immutable'; import cmf, { mock } from '@talend/react-cmf'; @@ -47,10 +46,10 @@ describe('ComponentForm', () => { it('should return js object', () => { // given - const immutableObject = new Map({ a: 1, b: 2 }); + const anObject = { a: 1, b: 2 }; // when - const result = toJS(immutableObject); + const result = toJS(anObject); // then expect(result).toEqual({ a: 1, b: 2 }); @@ -313,7 +312,7 @@ describe('ComponentForm', () => { describe('#render', () => { it("should render a CircularProgress when we don't have the schema", () => { // given - const state = new Map({}); + const state = {}; // when render( @@ -334,7 +333,7 @@ describe('ComponentForm', () => { it('should render a response status', () => { // given - const state = fromJS({ response: { statusText: 'we had an error' } }); + const state = { response: { statusText: 'we had an error' } }; // when render( @@ -353,7 +352,7 @@ describe('ComponentForm', () => { it('should render a UIForm', () => { // given - const state = fromJS(addSchemaMock.ui); + const state = { ...addSchemaMock.ui }; // when const { container } = render( @@ -374,7 +373,7 @@ describe('ComponentForm', () => { describe('#security', () => { it('should pass security props to createTrigger', () => { - const state = fromJS(addSchemaMock.ui); + const state = { ...addSchemaMock.ui }; const instance = new TCompForm({ state, triggerURL: 'http://trigger', @@ -391,7 +390,7 @@ describe('ComponentForm', () => { describe('#update', () => { it('should recreate trigger if triggerURL or customTriggers props change', () => { // given - const state = fromJS(addSchemaMock.ui); + const state = { ...addSchemaMock.ui }; const oldTriggerURL = 'http://old'; const newTriggerURL = 'http://new'; const oldCustomTriggers = { oldCustomReload: () => {} }; @@ -433,7 +432,7 @@ describe('ComponentForm', () => { it('should dispatch new definitionURL props', () => { // given - const state = fromJS(addSchemaMock.ui); + const state = { ...addSchemaMock.ui }; const dispatch = jest.fn(); const oldUrl = 'http://old'; const newUrl = 'http://new'; @@ -460,7 +459,7 @@ describe('ComponentForm', () => { }); describe('events', () => { - const state = fromJS({ ...addSchemaMock.ui, initialState: addSchemaMock.ui }); + const state = { ...addSchemaMock.ui, initialState: addSchemaMock.ui }; // extract type field schema const typeSchema = { @@ -501,10 +500,7 @@ describe('ComponentForm', () => { it('should NOT dispatch dirty state if it is already dirty', () => { // given - const dirtyState = fromJS({ - ...addSchemaMock.ui, - dirty: true, - }); + const dirtyState = { ...addSchemaMock.ui, dirty: true }; const setState = jest.fn(); const instance = new TCompForm({ state: dirtyState, diff --git a/packages/containers/src/ConfirmDialog/ConfirmDialog.connect.js b/packages/containers/src/ConfirmDialog/ConfirmDialog.connect.js index 4bf305a6e8e..54d973731ee 100644 --- a/packages/containers/src/ConfirmDialog/ConfirmDialog.connect.js +++ b/packages/containers/src/ConfirmDialog/ConfirmDialog.connect.js @@ -9,9 +9,9 @@ export function mapStateToProps(state, props, cmfProps) { getState: () => state, }, }; - const validateAction = cmfProps.state ? cmfProps.state.get('validateAction') : undefined; - const cancelAction = cmfProps.state ? cmfProps.state.get('cancelAction') : undefined; - const model = cmfProps.state ? cmfProps.state.get('model') : cmfProps.model; + const validateAction = cmfProps.state?.validateAction; + const cancelAction = cmfProps.state?.cancelAction; + const model = cmfProps.state?.model ?? cmfProps.model; return { validateAction: getActionsProps(context, validateAction, model), cancelAction: getActionsProps(context, cancelAction, model), diff --git a/packages/containers/src/ConfirmDialog/ConfirmDialog.container.jsx b/packages/containers/src/ConfirmDialog/ConfirmDialog.container.jsx index 1752a20072c..8b61a03a67e 100644 --- a/packages/containers/src/ConfirmDialog/ConfirmDialog.container.jsx +++ b/packages/containers/src/ConfirmDialog/ConfirmDialog.container.jsx @@ -1,18 +1,17 @@ -import { Map } from 'immutable'; import omit from 'lodash/omit'; import Component from '@talend/react-components/lib/ConfirmDialog'; import { cmfConnect, useCMFContext } from '@talend/react-cmf'; import { getActionsProps } from '../actionAPI'; -export const DEFAULT_STATE = new Map({ +export const DEFAULT_STATE = { show: false, -}); +}; // eslint-disable-next-line react/prefer-stateless-function function ConfirmDialog(props) { const context = useCMFContext(); - const state = (props.state || DEFAULT_STATE).toJS(); + const state = props.state || DEFAULT_STATE; if (!state.validateAction || !state.cancelAction) { return null; } diff --git a/packages/containers/src/ConfirmDialog/ConfirmDialog.stories.jsx b/packages/containers/src/ConfirmDialog/ConfirmDialog.stories.jsx index 1ee0f3f3eb8..4a2687b1641 100644 --- a/packages/containers/src/ConfirmDialog/ConfirmDialog.stories.jsx +++ b/packages/containers/src/ConfirmDialog/ConfirmDialog.stories.jsx @@ -1,14 +1,13 @@ -import { Map } from 'immutable'; import ConfirmDialog from '.'; -const initialState = new Map({ +const initialState = { size: 'small', header: 'DO SOMETHING', show: true, children: 'Confirm this !', validateAction: 'confirm-dialog:validate', cancelAction: 'confirm-dialog:cancel', -}); +}; export default { title: 'ConfirmDialog', diff --git a/packages/containers/src/ConfirmDialog/ConfirmDialog.test.jsx b/packages/containers/src/ConfirmDialog/ConfirmDialog.test.jsx index 46ac3c92b66..bc2cd1c9c8f 100644 --- a/packages/containers/src/ConfirmDialog/ConfirmDialog.test.jsx +++ b/packages/containers/src/ConfirmDialog/ConfirmDialog.test.jsx @@ -1,6 +1,5 @@ /* eslint-disable react/prop-types */ /* eslint-disable react/display-name */ -import { fromJS, Map } from 'immutable'; import cmf, { mock } from '@talend/react-cmf'; import { render } from '@testing-library/react'; @@ -37,12 +36,12 @@ describe('Container ConfirmDialog', () => { App = config.App; }); it('should not render', () => { - const state = new Map({ + const state = { size: 'small', header: 'DO SOMETHING', show: true, children: 'Confirm this !', - }); + }; const { container } = render( @@ -51,7 +50,7 @@ describe('Container ConfirmDialog', () => { expect(container).toBeEmptyDOMElement(); }); it('should render', () => { - const state = new Map({ + const state = { size: 'small', header: 'DO SOMETHING', show: true, @@ -59,7 +58,7 @@ describe('Container ConfirmDialog', () => { validateAction: 'menu:demo', cancelAction: 'menu:demo', model: { foo: 'bar' }, - }); + }; const { container } = render( @@ -77,14 +76,14 @@ describe('Connected ConfirmDialog', () => { }); it('should set validateAction and cancelAction', () => { - const cmfState = new Map({ + const cmfState = { size: 'small', header: 'DO SOMETHING', show: true, children: 'Confirm this !', validateAction: 'object:validate', cancelAction: 'object:cancel', - }); + }; const state = mock.store.state(); state.cmf.settings.actions['object:validate'] = { name: 'foo' }; state.cmf.settings.actions['object:cancel'] = { name: 'foo1' }; @@ -98,15 +97,15 @@ describe('Connected ConfirmDialog', () => { describe('ConfirmDialog.show/hide', () => { it('should change the visibility to true in the state', () => { const state = mock.store.state(); - state.cmf.components = fromJS({ + state.cmf.components = { ConfirmDialog: { ConfirmDialog: { show: false, }, }, - }); + }; - const dialog = new Map({ + const dialog = { size: 'small', header: 'REMOVE SEMANTIC TYPE', children: 'Are you sure you want to remove the semantic type ?', @@ -114,7 +113,7 @@ describe('ConfirmDialog.show/hide', () => { // these two actions are contained in show:remove:semantic action payload validateAction: '', cancelAction: '', - }); + }; const action = { confirmDialogConf: dialog, @@ -123,31 +122,25 @@ describe('ConfirmDialog.show/hide', () => { const newState = showConfirmDialog(state, action); expect(newState).not.toBe(state); - const confirmDialoVisibility = newState.cmf.components.getIn([ - 'CMFContainer(ConfirmDialog)', - 'ConfirmDialog', - 'show', - ]); + const confirmDialoVisibility = + newState.cmf.components?.['CMFContainer(ConfirmDialog)']?.['ConfirmDialog']?.show; expect(confirmDialoVisibility).toBeTruthy(); }); it('should change the visibility to false in the state', () => { const state = mock.store.state(); - state.cmf.components = fromJS({ + state.cmf.components = { ConfirmDialog: { ConfirmDialog: { show: true, }, }, - }); + }; const newState = hideConfirmDialog(state); expect(newState).not.toBe(state); - const confirmDialogVisibility = newState.cmf.components.getIn([ - 'CMFContainer(ConfirmDialog)', - 'ConfirmDialog', - 'show', - ]); + const confirmDialogVisibility = + newState.cmf.components?.['CMFContainer(ConfirmDialog)']?.['ConfirmDialog']?.show; expect(confirmDialogVisibility).toBeFalsy(); }); }); diff --git a/packages/containers/src/ConfirmDialog/showHideConfirmDialog.js b/packages/containers/src/ConfirmDialog/showHideConfirmDialog.js index 01d92a4b6ff..51935094269 100644 --- a/packages/containers/src/ConfirmDialog/showHideConfirmDialog.js +++ b/packages/containers/src/ConfirmDialog/showHideConfirmDialog.js @@ -1,18 +1,40 @@ export function showConfirmDialog(state, action) { // adding conf and showing modal const path = ['CMFContainer(ConfirmDialog)', 'ConfirmDialog']; - const newState = { ...state }; - newState.cmf.components = state.cmf.components.setIn( - path, - action.confirmDialogConf.set('show', true), - ); - return newState; + const [containerName, dialogKey] = path; + return { + ...state, + cmf: { + ...state.cmf, + components: { + ...state.cmf.components, + [containerName]: { + ...(state.cmf.components[containerName] ?? {}), + [dialogKey]: { ...action.confirmDialogConf, show: true }, + }, + }, + }, + }; } export function hideConfirmDialog(state) { // hiding the modal - const path = ['CMFContainer(ConfirmDialog)', 'ConfirmDialog', 'show']; - const newState = { ...state }; - newState.cmf.components = state.cmf.components.setIn(path, false); - return newState; + const containerName = 'CMFContainer(ConfirmDialog)'; + const dialogKey = 'ConfirmDialog'; + return { + ...state, + cmf: { + ...state.cmf, + components: { + ...state.cmf.components, + [containerName]: { + ...(state.cmf.components[containerName] ?? {}), + [dialogKey]: { + ...(state.cmf.components[containerName]?.[dialogKey] ?? {}), + show: false, + }, + }, + }, + }, + }; } diff --git a/packages/containers/src/DeleteResource/DeleteResource.connect.js b/packages/containers/src/DeleteResource/DeleteResource.connect.js index 6f3970497e1..03355cb5ca3 100644 --- a/packages/containers/src/DeleteResource/DeleteResource.connect.js +++ b/packages/containers/src/DeleteResource/DeleteResource.connect.js @@ -1,5 +1,4 @@ -import { cmfConnect } from '@talend/react-cmf'; -import Immutable from 'immutable'; +import cmf, { cmfConnect } from '@talend/react-cmf'; import get from 'lodash/get'; import Container from './DeleteResource.container'; @@ -16,9 +15,7 @@ export function mapStateToProps(state, ownProps) { } else { const collectionId = ownProps.collectionId || ownProps.resourceType; if (collectionId) { - props.resource = state.cmf.collections - .get(collectionId, new Immutable.Map()) - .find(currentResource => currentResource.get('id') === resourceId); + props.resource = cmf.selectors.collections.getCollectionItem(state, collectionId, resourceId); } } diff --git a/packages/containers/src/DeleteResource/DeleteResource.container.jsx b/packages/containers/src/DeleteResource/DeleteResource.container.jsx index 0738759d131..946f37dd127 100644 --- a/packages/containers/src/DeleteResource/DeleteResource.container.jsx +++ b/packages/containers/src/DeleteResource/DeleteResource.container.jsx @@ -14,7 +14,7 @@ import CONSTANTS from './constants'; */ function getLabel(resource) { if (resource) { - return resource.get('label') || resource.get('name') || ''; + return resource.label || resource.name || ''; } return ''; } diff --git a/packages/containers/src/DeleteResource/DeleteResource.test.js b/packages/containers/src/DeleteResource/DeleteResource.test.js index e57643bbf4b..2935e6a3758 100644 --- a/packages/containers/src/DeleteResource/DeleteResource.test.js +++ b/packages/containers/src/DeleteResource/DeleteResource.test.js @@ -1,18 +1,14 @@ import { render, screen } from '@testing-library/react'; import cmf, { mock } from '@talend/react-cmf'; -import Immutable from 'immutable'; import { DeleteResource } from './DeleteResource.container'; import Connected, { mapStateToProps } from './DeleteResource.connect'; const state = mock.store.state(); const settings = {}; -state.cmf = { - settings, -}; -state.cmf.collections = new Immutable.Map({ - foo: new Immutable.List([new Immutable.Map({ id: '123' })]), -}); +state.cmf = { settings }; +const fooItem = { id: '123' }; +state.cmf.collections = { foo: [fooItem] }; describe('Container DeleteResource', () => { let App; @@ -27,7 +23,7 @@ describe('Container DeleteResource', () => { const props = { uri: '/myEndpoint', resourceType: 'myResourceType', - resource: new Immutable.Map({ label: 'myLabel' }), + resource: { label: 'myLabel' }, header: 'My header title', params: { id: 'myResourceID' }, resourceTypeLabel: 'resourceLabel', @@ -79,12 +75,12 @@ describe('Connected DeleteResource', () => { }); it('should return the props.resource corresponding to resourceId', () => { expect(mapStateToProps(state, { resourceType: 'foo', resourceId: '123' }).resource).toBe( - state.cmf.collections.get('foo').get(0), + fooItem, ); }); it('should return the props.resource corresponding to routeParams.id', () => { expect(mapStateToProps(state, { resourceType: 'foo', params: { id: '123' } }).resource).toBe( - state.cmf.collections.get('foo').get(0), + fooItem, ); }); diff --git a/packages/containers/src/DeleteResource/sagas.js b/packages/containers/src/DeleteResource/sagas.js index 87c96e0c760..b2b7ba0989d 100644 --- a/packages/containers/src/DeleteResource/sagas.js +++ b/packages/containers/src/DeleteResource/sagas.js @@ -85,7 +85,7 @@ export function* deleteResourceValidate( model: { ...get(action, 'data.model', {}), id: safeId, - labelResource: resource.get('label') || resource.get('name', ''), + labelResource: resource.label || resource.name || '', }, }); yield call(redirect, get(action, 'data.model.redirectUrl')); diff --git a/packages/containers/src/DeleteResource/sagas.test.js b/packages/containers/src/DeleteResource/sagas.test.js index 0e988b34d9d..6eff040cad0 100644 --- a/packages/containers/src/DeleteResource/sagas.test.js +++ b/packages/containers/src/DeleteResource/sagas.test.js @@ -1,5 +1,4 @@ // import SagaTester from 'redux-saga-tester'; -import { Map } from 'immutable'; import cmf from '@talend/react-cmf'; import { take, put } from 'redux-saga/effects'; import CONSTANTS from './constants'; @@ -54,7 +53,7 @@ describe('internals', () => { }, }, }; - const resource = new Map({ id: '123', label: 'Foo' }); + const resource = { id: '123', label: 'Foo' }; const gen = internals.deleteResourceValidate(); let effect = gen.next().value; @@ -97,7 +96,11 @@ describe('internals', () => { }, }; - const resource = new Map({ id: 'profileId', type: 'advanced', name: 'deleteThisRunProfile' }); + const resource = { + id: 'profileId', + type: 'advanced', + name: 'deleteThisRunProfile', + }; const gen = internals.deleteResourceValidate(); gen.next(); @@ -122,11 +125,11 @@ describe('internals', () => { }, }, }; - const resource = new Map({ + const resource = { id: 'profileId', type: 'advanced', name: 'deleteThisRunProfile', - }); + }; const gen = internals.deleteResourceValidate(); gen.next(); diff --git a/packages/containers/src/EditableText/EditableText.container.jsx b/packages/containers/src/EditableText/EditableText.container.jsx index 7a46fb2a420..c84356bbdd2 100644 --- a/packages/containers/src/EditableText/EditableText.container.jsx +++ b/packages/containers/src/EditableText/EditableText.container.jsx @@ -1,14 +1,13 @@ import { Component as RComponent } from 'react'; import PropTypes from 'prop-types'; import Component from '@talend/react-components/lib/EditableText'; -import Immutable from 'immutable'; import omit from 'lodash/omit'; import { cmfConnect } from '@talend/react-cmf'; export const DISPLAY_NAME = 'Container(EditableText)'; -export const DEFAULT_STATE = new Immutable.Map({ +export const DEFAULT_STATE = { editMode: false, -}); +}; class EditableText extends RComponent { static displayName = DISPLAY_NAME; @@ -91,7 +90,7 @@ class EditableText extends RComponent { onCancel: this.onCancel, onSubmit: this.onSubmit, onChange: this.onChange, - ...state.toJS(), + ...state, }; return ; } diff --git a/packages/containers/src/EditableText/EditableText.selectors.js b/packages/containers/src/EditableText/EditableText.selectors.js index d8337be26ad..4a04f19cd86 100644 --- a/packages/containers/src/EditableText/EditableText.selectors.js +++ b/packages/containers/src/EditableText/EditableText.selectors.js @@ -6,5 +6,5 @@ import EditableText from './EditableText.connect'; * @param {string} idComponent */ export function getEditMode(state, idComponent) { - return EditableText.getState(state, idComponent).get('editMode', false); + return EditableText.getState(state, idComponent)?.editMode ?? false; } diff --git a/packages/containers/src/EditableText/EditableText.test.js b/packages/containers/src/EditableText/EditableText.test.js index 09a50864909..9fb6cc26642 100644 --- a/packages/containers/src/EditableText/EditableText.test.js +++ b/packages/containers/src/EditableText/EditableText.test.js @@ -1,6 +1,5 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Map } from 'immutable'; import cmf, { mock } from '@talend/react-cmf'; @@ -35,7 +34,7 @@ describe('EditableText container', () => { it('should setState when submit event trigger', async () => { let state; const props = { - state: Map({ editMode: true }), + state: { editMode: true }, setState: jest.fn(fn => { state = fn; }), @@ -53,11 +52,10 @@ describe('EditableText container', () => { }); }); it('should call ActionCreatorSubmit when submit event trigger', async () => { - const event = {}; const props = { actionCreatorSubmit: 'mySubmitActionCreator', dispatchActionCreator: jest.fn(), - state: Map({ editMode: true }), + state: { editMode: true }, setState: jest.fn(), text: 'my text', }; @@ -78,7 +76,7 @@ describe('EditableText container', () => { it('should setState when cancel event trigger', async () => { let state; const props = { - state: Map({ editMode: true }), + state: { editMode: true }, setState: jest.fn(fn => { state = fn; }), @@ -93,7 +91,7 @@ describe('EditableText container', () => { it('should call onCancel when cancel event trigger', async () => { const props = { setState: jest.fn(), - state: Map({ editMode: true }), + state: { editMode: true }, onCancel: jest.fn(), text: 'my text', }; @@ -104,7 +102,7 @@ describe('EditableText container', () => { it('should call actionCreatorCancel when cancel event trigger', async () => { const props = { setState: jest.fn(), - state: Map({ editMode: true }), + state: { editMode: true }, actionCreatorCancel: 'myCancelActionCreator', dispatchActionCreator: jest.fn(), text: 'my text', @@ -122,7 +120,7 @@ describe('EditableText container', () => { it('should call setState when edit event trigger', async () => { let state; const props = { - state: Map({ editMode: false }), + state: { editMode: false }, setState: jest.fn(fn => { state = fn; }), @@ -137,7 +135,7 @@ describe('EditableText container', () => { it('should call onEdit when edit event trigger', async () => { const props = { setState: jest.fn(), - state: Map({ editMode: false }), + state: { editMode: false }, onEdit: jest.fn(), text: 'my text', }; @@ -145,10 +143,10 @@ describe('EditableText container', () => { await userEvent.click(screen.getByLabelText('Rename')); expect(props.onEdit).toHaveBeenCalledWith(expect.anything()); }); - it('should call onEdit when edit event trigger', async () => { + it('should call actionCreatorEdit via dispatchActionCreator when edit event trigger', async () => { const props = { setState: jest.fn(), - state: Map({ editMode: false }), + state: { editMode: false }, dispatchActionCreator: jest.fn(), actionCreatorEdit: 'myEditActionCreator', text: 'my text', @@ -168,7 +166,7 @@ describe('EditableText container', () => { const props = { setState: jest.fn(), - state: Map({ editMode: true }), + state: { editMode: true }, onChange: jest.fn(), text: 'my text', }; @@ -184,7 +182,7 @@ describe('EditableText container', () => { const props = { setState: jest.fn(), - state: Map({ editMode: true }), + state: { editMode: true }, dispatchActionCreator: jest.fn(), actionCreatorChange: 'myChangeActionCreator', text: 'my text', @@ -207,11 +205,11 @@ describe('EditableText container', () => { describe('EditableText selectors', () => { let mockState; - const componentState = Map({ editMode: true }); + const componentState = { editMode: true }; beforeEach(() => { mockState = { cmf: { - components: Map({ [DISPLAY_NAME]: Map({ myEditableText: componentState }) }), + components: { [DISPLAY_NAME]: { myEditableText: componentState } }, }, }; }); diff --git a/packages/containers/src/FilterBar/FilterBar.container.jsx b/packages/containers/src/FilterBar/FilterBar.container.jsx index 34f1d6d4e03..03d4087afc0 100644 --- a/packages/containers/src/FilterBar/FilterBar.container.jsx +++ b/packages/containers/src/FilterBar/FilterBar.container.jsx @@ -2,14 +2,13 @@ import { cmfConnect } from '@talend/react-cmf'; import { Component as RComponent } from 'react'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; -import Immutable from 'immutable'; import Component from '@talend/react-components/lib/FilterBar'; export const QUERY_ATTR = 'query'; -export const DEFAULT_STATE = new Immutable.Map({ +export const DEFAULT_STATE = { [QUERY_ATTR]: '', docked: true, -}); +}; export const DISPLAY_NAME = 'Container(FilterBar)'; @@ -49,13 +48,9 @@ class FilterBar extends RComponent { } onToggle(event) { - this.props.setState(prevState => { - let state = prevState.state; - if (this.props.dockable) { - state = state.set('docked', !this.props.state.get('docked')); - } - return state; - }); + if (this.props.dockable) { + this.props.setState({ docked: !this.props.state?.docked }); + } if (this.props.onToggle) { this.props.onToggle(event); } @@ -65,8 +60,8 @@ class FilterBar extends RComponent { const state = this.props.state || DEFAULT_STATE; const props = { ...omit(this.props, cmfConnect.INJECTED_PROPS), - docked: this.props.docked != null ? this.props.docked : state.get(DOCKED_ATTR), - value: this.props.value ? this.props.value : state.get(QUERY_ATTR, ''), + docked: this.props.docked != null ? this.props.docked : state?.[DOCKED_ATTR], + value: this.props.value ? this.props.value : (state?.[QUERY_ATTR] ?? ''), onToggle: this.onToggle, onFilter: this.onFilter, }; diff --git a/packages/containers/src/FilterBar/FilterBar.selectors.js b/packages/containers/src/FilterBar/FilterBar.selectors.js index 5e2d915456f..e6b5a1a55f1 100644 --- a/packages/containers/src/FilterBar/FilterBar.selectors.js +++ b/packages/containers/src/FilterBar/FilterBar.selectors.js @@ -6,10 +6,7 @@ import { DEFAULT_STATE, DISPLAY_NAME } from './FilterBar.container'; * @param {string} idComponent */ export function getComponentState(state, idComponent) { - if (state.cmf.components.hasIn([DISPLAY_NAME, idComponent])) { - return state.cmf.components.getIn([DISPLAY_NAME, idComponent]); - } - return DEFAULT_STATE; + return state.cmf.components?.[DISPLAY_NAME]?.[idComponent] ?? DEFAULT_STATE; } /** @@ -18,5 +15,5 @@ export function getComponentState(state, idComponent) { * @param {string} idComponent */ export function getQuery(state, idComponent) { - return getComponentState(state, idComponent).get('query', ''); + return getComponentState(state, idComponent).query ?? ''; } diff --git a/packages/containers/src/FilterBar/FilterBar.test.js b/packages/containers/src/FilterBar/FilterBar.test.js index 56d44e56ad8..98259eb7892 100644 --- a/packages/containers/src/FilterBar/FilterBar.test.js +++ b/packages/containers/src/FilterBar/FilterBar.test.js @@ -1,5 +1,5 @@ -import { Map } from 'immutable'; -import { screen, render, fireEvent } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import Container, { DEFAULT_STATE, DISPLAY_NAME } from './FilterBar.container'; import Connected from './FilterBar.connect'; import { getComponentState, getQuery } from './FilterBar.selectors'; @@ -29,7 +29,7 @@ describe('Filter container', () => { const props = { onFilter: jest.fn(), setState: jest.fn(), - state: Map({ docked: false }), + state: { docked: false }, }; render(); const query = 'foo'; @@ -40,7 +40,7 @@ describe('Filter container', () => { const props = { onFilter: jest.fn(), setState: jest.fn(), - state: Map({ docked: false }), + state: { docked: false }, }; const query = 'foo'; render(); @@ -58,7 +58,7 @@ describe('Filter container', () => { const props = { onBlur: jest.fn(), setState: jest.fn(), - state: Map({ docked: false }), + state: { docked: false }, }; render(); fireEvent.blur(document.querySelector('input')); @@ -68,7 +68,7 @@ describe('Filter container', () => { const props = { onBlur: jest.fn(), setState: jest.fn(), - state: Map({ docked: false }), + state: { docked: false }, onFocus: jest.fn(), }; render(); @@ -76,13 +76,9 @@ describe('Filter container', () => { expect(props.onFocus).toHaveBeenCalled(); }); it('should call setState when onToggle event trigger', () => { - const state = Map({ docked: false }); - const prevState = { state }; - const setState = jest.fn(fn => { - prevState.state = fn(prevState); - }); + const state = { docked: false }; const props = { - setState, + setState: jest.fn(), state, dockable: true, onToggle: jest.fn(), @@ -90,21 +86,20 @@ describe('Filter container', () => { render(); fireEvent.blur(document.querySelector('input')); expect(props.setState).toHaveBeenCalled(); - expect(prevState.state).not.toBe(state); - expect(prevState.state.get('docked')).toBe(true); + expect(props.setState).toHaveBeenCalledWith({ docked: true }); expect(props.onToggle).toHaveBeenCalled(); }); }); describe('Filter Selectors', () => { it('should return the filter component state', () => { - const componentState = Map({ + const componentState = { query: 'Toto was here', docked: true, - }); + }; const state = { cmf: { - components: Map({ [DISPLAY_NAME]: Map({ myFilterComponent: componentState }) }), + components: { [DISPLAY_NAME]: { myFilterComponent: componentState } }, }, }; expect(getComponentState(state, 'myFilterComponent')).toEqual(componentState); @@ -112,19 +107,19 @@ describe('Filter Selectors', () => { it('should return the default filter component state', () => { const state = { cmf: { - components: Map(), + components: {}, }, }; expect(getComponentState(state, 'myFilterComponent')).toEqual(DEFAULT_STATE); }); it('should return the query', () => { - const componentState = Map({ + const componentState = { query: 'Hello world', docked: true, - }); + }; const state = { cmf: { - components: Map({ [DISPLAY_NAME]: Map({ myFilterComponent: componentState }) }), + components: { [DISPLAY_NAME]: { myFilterComponent: componentState } }, }, }; expect(getQuery(state, 'myFilterComponent')).toEqual('Hello world'); diff --git a/packages/containers/src/Form/Form.container.jsx b/packages/containers/src/Form/Form.container.jsx index a3d5747e35f..daa47b92f87 100644 --- a/packages/containers/src/Form/Form.container.jsx +++ b/packages/containers/src/Form/Form.container.jsx @@ -1,6 +1,5 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Immutable from 'immutable'; import { cmfConnect } from '@talend/react-cmf'; import BaseForm from '@talend/react-forms'; import classnames from 'classnames'; @@ -10,7 +9,7 @@ if (process.env.FORM_MOZ) { DefaultArrayFieldTemplate = BaseForm.deprecated.templates.ArrayFieldTemplate; } -export const DEFAULT_STATE = new Immutable.Map({}); +export const DEFAULT_STATE = {}; /** * Because we don't want to loose form input @@ -42,7 +41,7 @@ class Form extends Component { * @return {[type]} [description] */ static getFormData(state, formId) { - return state.cmf.components.getIn(['Container(Form)', formId, 'data'], new Immutable.Map()); + return state.cmf.components?.['Container(Form)']?.[formId]?.data ?? {}; } static getDerivedStateFromProps(nextProps, prevState) { @@ -62,7 +61,7 @@ class Form extends Component { constructor(props) { super(props); - this.state = DEFAULT_STATE.toJS(); + this.state = { ...DEFAULT_STATE }; this.formActions = this.formActions.bind(this); this.onChange = this.onChange.bind(this); this.onSubmit = this.onSubmit.bind(this); @@ -105,7 +104,7 @@ class Form extends Component { } jsonSchema() { - const state = (this.props.state || DEFAULT_STATE).toJS(); + const state = this.props.state || DEFAULT_STATE; if (typeof this.props.jsonSchema === 'function') { return this.props.jsonSchema(state.data); } @@ -113,7 +112,7 @@ class Form extends Component { } uiSchema() { - const state = (this.props.state || DEFAULT_STATE).toJS(); + const state = this.props.state || DEFAULT_STATE; if (typeof this.props.uiSchema === 'function') { return this.props.uiSchema(state.data); } @@ -121,7 +120,7 @@ class Form extends Component { } data() { - const state = (this.props.state || DEFAULT_STATE).toJS(); + const state = this.props.state || DEFAULT_STATE; if (typeof this.props.data === 'function') { return this.props.data(state.data); } @@ -129,7 +128,7 @@ class Form extends Component { } errors() { - const state = (this.props.state || DEFAULT_STATE).toJS(); + const state = this.props.state || DEFAULT_STATE; if (typeof this.props.errors === 'function') { return this.props.errors(state.errors); } @@ -138,14 +137,14 @@ class Form extends Component { formActions() { if (typeof this.props.actions === 'function') { - const state = (this.props.state || DEFAULT_STATE).toJS(); + const state = this.props.state || DEFAULT_STATE; return this.props.actions(state.data || this.props.data); } return this.props.actions; } render() { - const state = (this.props.state || DEFAULT_STATE).toJS(); + const state = this.props.state || DEFAULT_STATE; const props = { data: { jsonSchema: this.jsonSchema(), diff --git a/packages/containers/src/Form/Form.test.js b/packages/containers/src/Form/Form.test.js index 5ca8c98b793..ea576ed1a2f 100644 --- a/packages/containers/src/Form/Form.test.js +++ b/packages/containers/src/Form/Form.test.js @@ -1,5 +1,4 @@ import { render } from '@testing-library/react'; -import { fromJS } from 'immutable'; import Connected from './Form.connect'; import Container from './Form.container'; @@ -45,7 +44,7 @@ describe('Container(Form)', () => { const setState = jest.fn(); const event = { target: 'test' }; const form = new Container({ - state: fromJS({ data: { schema: true } }), + state: { data: { schema: true } }, onErrors, setState, }); @@ -59,7 +58,7 @@ describe('Container(Form)', () => { const dispatchActionCreator = jest.fn(); const setState = jest.fn(); const form = new Container({ - state: fromJS({ data: { schema: true } }), + state: { data: { schema: true } }, setState, onSubmitActionCreator: 'myaction', onSubmit, @@ -70,7 +69,7 @@ describe('Container(Form)', () => { expect(dispatchActionCreator.mock.calls[0][0]).toBe('myaction'); expect(dispatchActionCreator.mock.calls[0][1]).toBe(null); expect(dispatchActionCreator.mock.calls[0][2].formData).toEqual({ foo: 'bar' }); - expect(dispatchActionCreator.mock.calls[0][2].props.state.size).toBe(1); + expect(dispatchActionCreator.mock.calls[0][2].props.state).toEqual({ data: { schema: true } }); expect(setState.mock.calls.length).toBe(0); }); @@ -79,7 +78,7 @@ describe('Container(Form)', () => { const setState = jest.fn(); const event = { target: 'test' }; const form = new Container({ - state: fromJS({ data: { schema: true } }), + state: { data: { schema: true } }, onChange, setState, }); @@ -93,17 +92,17 @@ describe('Container(Form)', () => { const formId = 'my-form'; const state = { cmf: { - components: fromJS({ + components: { 'Container(Form)': { [formId]: { data: { foo: 'bar' }, }, }, - }), + }, }, }; const formData = Container.getFormData(state, formId); - expect(formData.get('foo')).toBe('bar'); + expect(formData.foo).toBe('bar'); }); it('should formActions return props.action', () => { diff --git a/packages/containers/src/GuidedTour/GuidedTour.connect.js b/packages/containers/src/GuidedTour/GuidedTour.connect.js index 8d56e8ce315..d253f8a72d2 100644 --- a/packages/containers/src/GuidedTour/GuidedTour.connect.js +++ b/packages/containers/src/GuidedTour/GuidedTour.connect.js @@ -1,9 +1,8 @@ -import { Map } from 'immutable'; import { cmfConnect } from '@talend/react-cmf'; import GuidedTourContainer from './GuidedTour.container'; -export const DEFAULT_STATE = Map({}); +export const DEFAULT_STATE = {}; export default cmfConnect({ componentId: ownProps => ownProps.componentId || ownProps.id, diff --git a/packages/containers/src/GuidedTour/GuidedTour.container.jsx b/packages/containers/src/GuidedTour/GuidedTour.container.jsx index 50dcdd876a1..cb06f4c664c 100644 --- a/packages/containers/src/GuidedTour/GuidedTour.container.jsx +++ b/packages/containers/src/GuidedTour/GuidedTour.container.jsx @@ -59,7 +59,7 @@ class GuidedTourContainer extends Component { const { controls } = this.state; return ( b.label ? 1 : -1; @@ -40,7 +39,7 @@ class HeaderBar extends RComponent { // products URL has changed or products have not been loaded yet const hasProductsUrlChanged = this.props.productsUrl !== prevProps.productsUrl; const hasProductsNotBeenLoaded = - this.props.state.get('productsFetchState') === Constants.PRODUCTS_NOT_LOADED; + this.props.state?.productsFetchState === Constants.PRODUCTS_NOT_LOADED; if (this.props.productsUrl && (hasProductsNotBeenLoaded || hasProductsUrlChanged)) { this.props.dispatch(fetchProducts(this.props.productsUrl)); @@ -56,7 +55,7 @@ class HeaderBar extends RComponent { } = this.props; const hasFetchedProducts = - this.props.state.get('productsFetchState') === Constants.FETCH_PRODUCTS_SUCCESS; + this.props.state?.productsFetchState === Constants.FETCH_PRODUCTS_SUCCESS; const productsProps = {}; diff --git a/packages/containers/src/HeaderBar/HeaderBar.test.js b/packages/containers/src/HeaderBar/HeaderBar.test.js index c15484d3479..c90d98fc26a 100644 --- a/packages/containers/src/HeaderBar/HeaderBar.test.js +++ b/packages/containers/src/HeaderBar/HeaderBar.test.js @@ -1,8 +1,8 @@ -import { Map, List } from 'immutable'; import { render, screen } from '@testing-library/react'; // eslint-disable-next-line @talend/import-depth import { prepareCMF } from '@talend/react-cmf/lib/mock/rtl'; + import Container, { DEFAULT_STATE } from './HeaderBar.container'; import Connected, { mapStateToProps } from './HeaderBar.connect'; import Constants from './HeaderBar.constant'; @@ -28,9 +28,9 @@ describe('Container HeaderBar', () => { url: 'http://foo.bar', }, ], - state: new Map({ + state: { productsFetchState: Constants.FETCH_PRODUCTS_SUCCESS, - }), + }, }; render(await prepareCMF()); @@ -65,9 +65,9 @@ describe('Container HeaderBar', () => { }, ], }, - state: new Map({ + state: { productsFetchState: Constants.FETCH_PRODUCTS_SUCCESS, - }), + }, }; render(await prepareCMF()); expect(screen.getAllByRole('menuitem')).toHaveLength(3); @@ -90,9 +90,9 @@ describe('Container HeaderBar', () => { prepareProducts: jest.fn(products => products.map(product => ({ ...product, label: `${product.label} and bar` })), ), - state: new Map({ + state: { productsFetchState: Constants.FETCH_PRODUCTS_SUCCESS, - }), + }, }; render(); @@ -103,9 +103,9 @@ describe('Container HeaderBar', () => { it('should render HeaderBar container while fetching items', async () => { const props = { ...containerProps, - state: new Map({ + state: { productsFetchState: Constants.FETCHING_PRODUCTS, - }), + }, }; render(await prepareCMF()); @@ -124,11 +124,7 @@ describe('Connected HeaderBar', () => { it('should mapStateToProps with an empty list of products', () => { const state = { cmf: { - collections: new Map({ - HeaderBar: { - [Constants.COLLECTION_ID]: new List(), - }, - }), + collections: {}, }, }; const ownProps = {}; @@ -146,9 +142,7 @@ describe('Connected HeaderBar', () => { const apps = [{ url: 'foobar' }]; const state = { cmf: { - collections: new Map({ - [Constants.COLLECTION_ID]: new List(apps), - }), + collections: { [Constants.COLLECTION_ID]: apps }, }, }; const ownProps = {}; diff --git a/packages/containers/src/HomeListView/HomeListView.connect.test.js b/packages/containers/src/HomeListView/HomeListView.connect.test.js index 8c0d68fe22e..fe6482c4436 100644 --- a/packages/containers/src/HomeListView/HomeListView.connect.test.js +++ b/packages/containers/src/HomeListView/HomeListView.connect.test.js @@ -1,6 +1,5 @@ // rewrite tests using react-testing-library import { screen, render } from '@testing-library/react'; -import { fromJS } from 'immutable'; // eslint-disable-next-line @talend/import-depth import { prepareCMF } from '@talend/react-cmf/lib/mock/rtl'; @@ -36,7 +35,7 @@ const toolbar = { }, }; -const items = fromJS([ +const items = [ { id: 1, label: 'Title with actions', @@ -47,7 +46,7 @@ const items = fromJS([ display: 'text', className: 'item-0-class', }, -]); +]; const listProps = { list, diff --git a/packages/containers/src/HomeListView/HomeListView.stories.jsx b/packages/containers/src/HomeListView/HomeListView.stories.jsx index 472c69b8f7e..63003aff52d 100644 --- a/packages/containers/src/HomeListView/HomeListView.stories.jsx +++ b/packages/containers/src/HomeListView/HomeListView.stories.jsx @@ -1,6 +1,5 @@ import { Drawer } from '@talend/react-components'; import { action } from 'storybook/actions'; -import Immutable from 'immutable'; import HomeListView from '.'; @@ -128,7 +127,7 @@ const toolbar = { }, }; -const items = Immutable.fromJS([ +const items = [ { id: 1, label: 'Title with actions', @@ -156,7 +155,7 @@ const items = Immutable.fromJS([ modified: '2016-09-22', author: 'Jean-Pierre DUPONT with super long name', }, -]); +]; const listProps = { list, diff --git a/packages/containers/src/List/List.connect.js b/packages/containers/src/List/List.connect.js index c05069134d7..f6a43dab964 100644 --- a/packages/containers/src/List/List.connect.js +++ b/packages/containers/src/List/List.connect.js @@ -1,5 +1,4 @@ import get from 'lodash/get'; -import { List } from 'immutable'; import { cmfConnect } from '@talend/react-cmf'; import Container, { DEFAULT_STATE } from './List.container'; import { @@ -48,7 +47,7 @@ export function mapStateToProps(state, ownProps, cmfProps) { props.items = getItems(state, config); - const totalResults = props.items.size; + const totalResults = props.items.length; if (get(ownProps, ['toolbar', 'pagination'])) { props.items = getPagedItems(state, config, props.items); @@ -56,12 +55,18 @@ export function mapStateToProps(state, ownProps, cmfProps) { const cmfState = get(cmfProps, 'state'); if (cmfState) { - props.state = cmfState.setIn(['totalResults'], totalResults); - if (props.state.has('toolbar')) { - props.state = props.state.mergeIn( - ['toolbar', 'pagination'], - configureGetPagination(state, config), - ); + props.state = { ...cmfState, totalResults }; + if (props.state.toolbar !== undefined) { + props.state = { + ...props.state, + toolbar: { + ...props.state.toolbar, + pagination: { + ...props.state.toolbar?.pagination, + ...configureGetPagination(state, config), + }, + }, + }; } } @@ -81,7 +86,7 @@ export default cmfConnect({ defaultProps: { saga: 'List#root', - listItems: new List(), + listItems: [], }, componentId, diff --git a/packages/containers/src/List/List.container.jsx b/packages/containers/src/List/List.container.jsx index f0d715322ae..cf2125b3297 100644 --- a/packages/containers/src/List/List.container.jsx +++ b/packages/containers/src/List/List.container.jsx @@ -1,6 +1,4 @@ import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { Map, List as ImmutableList } from 'immutable'; import Component from '@talend/react-components/lib/List'; import VirtualizedList from '@talend/react-components/lib/VirtualizedList'; import get from 'lodash/get'; @@ -44,9 +42,9 @@ export const connectedCellDictionary = { }, }; -export const DEFAULT_STATE = new Map({ +export const DEFAULT_STATE = { displayMode: 'table', - selectedItems: new ImmutableList(), + selectedItems: [], searchQuery: '', itemsPerPage: 10, startIndex: 1, @@ -54,7 +52,7 @@ export const DEFAULT_STATE = new Map({ sortOn: 'name', sortAsc: true, filterDocked: true, -}); +}; /** * merge props.items with actions @@ -63,7 +61,7 @@ export const DEFAULT_STATE = new Map({ * @return {Array} [description] */ export function getItems(context, props) { - return props.items.toJS().map(item => { + return props.items.map(item => { const actionsItems = get(props, 'actions.items', []); let actions = []; if ( @@ -89,7 +87,7 @@ export function getItems(context, props) { function List(props) { const context = useCMFContext(); - const state = props.state.toJS(); + const state = props.state; function onChangePage(startIndex, itemsPerPage) { props.setState({ startIndex, itemsPerPage }); @@ -100,7 +98,7 @@ function List(props) { } function getSelectedItems() { - return props.state.get('selectedItems', new ImmutableList()); + return props.state?.selectedItems ?? []; } function onToggleMultiSelection(event, data) { @@ -108,11 +106,11 @@ function List(props) { const dataIndex = selectedItems.indexOf(data[props.idKey]); if (dataIndex > -1) { props.setState({ - selectedItems: selectedItems.splice(dataIndex, 1), + selectedItems: selectedItems.filter((_, i) => i !== dataIndex), }); } else { props.setState({ - selectedItems: selectedItems.push(data[props.idKey]), + selectedItems: [...selectedItems, data[props.idKey]], }); } } @@ -120,13 +118,13 @@ function List(props) { function onToggleAllMultiSelection() { const selectedItems = getSelectedItems(); const items = props.items; - if (selectedItems.size !== items.size) { + if (selectedItems.length !== items.length) { props.setState({ - selectedItems: items.map(item => item.get(props.idKey)), + selectedItems: items.map(item => item[props.idKey]), }); } else { props.setState({ - selectedItems: new ImmutableList([]), + selectedItems: [], }); } } @@ -268,7 +266,7 @@ function List(props) { newProps.list.itemProps.onToggle = onToggleMultiSelection; newProps.list.itemProps.onToggleAll = onToggleAllMultiSelection; newProps.list.itemProps.isSelected = isSelected; - newProps.toolbar.actionBar.selected = getSelectedItems().size; + newProps.toolbar.actionBar.selected = getSelectedItems().length; } const actions = newProps.actions; @@ -353,7 +351,7 @@ List.propTypes = { }), cellDictionary: PropTypes.object, displayMode: PropTypes.string, - items: ImmutablePropTypes.list.isRequired, + items: PropTypes.array.isRequired, state: cmfConnect.propTypes.state, ...cmfConnect.propTypes, }; diff --git a/packages/containers/src/List/List.sagas.test.js b/packages/containers/src/List/List.sagas.test.js index 4aa9a5fb8cf..4e567bfc001 100644 --- a/packages/containers/src/List/List.sagas.test.js +++ b/packages/containers/src/List/List.sagas.test.js @@ -1,12 +1,11 @@ import { put } from 'redux-saga/effects'; -import { fromJS } from 'immutable'; import { mock } from '@talend/react-cmf'; import { onToggleFilter, onFilterChange, onChangeSortChange } from './List.sagas'; import Connected from './List.connect'; const localConfig = { collectionId: 'default', - items: fromJS([ + items: [ { id: 'id1', value: 'value1', @@ -17,7 +16,7 @@ const localConfig = { value: 'value2', text: 'text', }, - ]), + ], list: { columns: [ { key: 'id', name: 'ID' }, @@ -27,7 +26,7 @@ const localConfig = { }; const state = mock.store.state(); -state.cmf.collections = fromJS({ +state.cmf.collections = { default: { columns: [ { key: 'id', name: 'ID' }, @@ -35,7 +34,7 @@ state.cmf.collections = fromJS({ ], items: localConfig.items, }, -}); +}; const context = mock.store.context(); const event = { type: 'click' }; @@ -47,14 +46,14 @@ const data = { props: { config: localConfig }, }; -state.cmf.components = fromJS({ +state.cmf.components = { 'Container(List)': { default: { itemsPerPage: 1, startIndex: 1, }, }, -}); +}; describe('List sagas', () => { it('should check onToggleFilter action', () => { diff --git a/packages/containers/src/List/List.stories.jsx b/packages/containers/src/List/List.stories.jsx index bd3f759b262..55819b708c1 100644 --- a/packages/containers/src/List/List.stories.jsx +++ b/packages/containers/src/List/List.stories.jsx @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import api from '@talend/react-cmf'; -import Immutable from 'immutable'; import cloneDeep from 'lodash/cloneDeep'; import List from '.'; @@ -115,16 +114,16 @@ const customHeight = { table: 100, }; -const defaultListState = new Immutable.Map({ +const defaultListState = { displayMode: 'large', -}); +}; -const defaultSortedListState = new Immutable.Map({ +const defaultSortedListState = { sortOn: 'modified', sortAsc: false, -}); +}; -const items = Immutable.fromJS([ +const items = [ { id: 'id1', label: 'Title with actions', @@ -179,7 +178,7 @@ const items = Immutable.fromJS([ modified: '2016-09-22', author: 'Jean-Pierre DUPONT', }, -]); +]; const referenceDatetime = Date.now(); const minusThreeHours = referenceDatetime - 3600 * 3 * 1000; @@ -189,7 +188,7 @@ const minusThreeMin = referenceDatetime - 60 * 3 * 1000; const oneDay = 24 * 3600 * 1000; -const itemsWithTimestamp = Immutable.fromJS([ +const itemsWithTimestamp = [ { id: 'id0', label: 'Title with actions but first', @@ -227,7 +226,7 @@ const itemsWithTimestamp = Immutable.fromJS([ modified: minusOneHours, author: 'Jean-Pierre DUPONT with super long name', }, -]); +]; const sortUpdatedAsc = { field: 'modified', @@ -258,73 +257,72 @@ export const WithSeparatorActions = () => ( ); export const Pagination = () => { const propsPg = cloneDeep(props); - const itemsPg = items.concat( - Immutable.fromJS([ - { - id: 'id4', - label: 'Title with actions', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT', - }, - { - id: 'ID5', - label: 'Title in input mode', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT', - }, - { - id: 'iD6', - label: 'Super long title to trigger overflow on some rendering', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT with super long name', - }, - { - id: 'id7', - label: 'Title with actions', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT', - }, - { - id: 'ID8', - label: 'Title in input mode', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT', - }, - { - id: 'iD9', - label: 'Super long title to trigger overflow on some rendering', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT with super long name', - }, - { - id: 'id10', - label: 'Title with actions', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT', - }, - { - id: 'ID11', - label: 'Title in input mode', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT', - }, - { - id: 'iD12', - label: 'Super long title to trigger overflow on some rendering', - created: '2016-09-22', - modified: '2016-09-22', - author: 'Jean-Pierre DUPONT with super long name', - }, - ]), - ); + const itemsPg = [ + ...items, + { + id: 'id4', + label: 'Title with actions', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT', + }, + { + id: 'ID5', + label: 'Title in input mode', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT', + }, + { + id: 'iD6', + label: 'Super long title to trigger overflow on some rendering', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT with super long name', + }, + { + id: 'id7', + label: 'Title with actions', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT', + }, + { + id: 'ID8', + label: 'Title in input mode', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT', + }, + { + id: 'iD9', + label: 'Super long title to trigger overflow on some rendering', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT with super long name', + }, + { + id: 'id10', + label: 'Title with actions', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT', + }, + { + id: 'ID11', + label: 'Title in input mode', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT', + }, + { + id: 'iD12', + label: 'Super long title to trigger overflow on some rendering', + created: '2016-09-22', + modified: '2016-09-22', + author: 'Jean-Pierre DUPONT with super long name', + }, + ]; propsPg.toolbar.pagination = {}; return (
diff --git a/packages/containers/src/List/List.test.jsx b/packages/containers/src/List/List.test.jsx index aa29224a643..f324f61549d 100644 --- a/packages/containers/src/List/List.test.jsx +++ b/packages/containers/src/List/List.test.jsx @@ -2,7 +2,6 @@ /* eslint-disable react/display-name */ import { fireEvent, render, screen } from '@testing-library/react'; -import { fromJS, List as ImmutableList, Map } from 'immutable'; import cloneDeep from 'lodash/cloneDeep'; // eslint-disable-next-line @talend/import-depth @@ -70,7 +69,7 @@ const settings = { actions, }; -const items = fromJS([ +const items = [ { id: 1, name: 'Title with actions', @@ -98,7 +97,7 @@ const items = fromJS([ modified: '2016-09-22', author: 'Jean-Pierre DUPONT with super long name', }, -]); +]; vi.mock('@talend/react-components/lib/List', () => ({ default: ({ getProps, ...props }) => ( @@ -144,7 +143,7 @@ describe('Container List', () => { , { cmfModule }, @@ -172,7 +171,7 @@ describe('Container List', () => { , { cmfModule }, @@ -492,7 +491,7 @@ describe('Container List', () => { left: ['object:remove'], }; multiSelectionSetting.setState = jest.fn(); - const state = fromJS({ selectedItems: [] }); + const state = { selectedItems: [] }; multiSelectionSetting.state = state; render( await prepareCMF( @@ -505,8 +504,8 @@ describe('Container List', () => { const props = getProps.mock.calls[0][0]; props.list.itemProps.onToggle({}, { id: 1 }); // then - expect(multiSelectionSetting.setState.mock.calls[0][0]).toMatchObject({ - selectedItems: expect.any(ImmutableList), + expect(multiSelectionSetting.setState.mock.calls[0][0]).toEqual({ + selectedItems: [1], }); }); @@ -519,7 +518,7 @@ describe('Container List', () => { left: ['object:remove'], }; multiSelectionSetting.setState = jest.fn(); - const state = fromJS({ selectedItems: [1] }); + const state = { selectedItems: [1] }; multiSelectionSetting.state = state; render( await prepareCMF( @@ -533,7 +532,7 @@ describe('Container List', () => { props.list.itemProps.onToggle({}, { id: 1 }); // then expect(multiSelectionSetting.setState.mock.calls[0][0]).toEqual({ - selectedItems: new ImmutableList([]), + selectedItems: [], }); }); it('should select all items', async () => { @@ -546,7 +545,7 @@ describe('Container List', () => { left: ['object:remove'], }; multiSelectionSetting.setState = jest.fn(); - const state = fromJS({ selectedItems: [] }); + const state = { selectedItems: [] }; multiSelectionSetting.state = state; render( await prepareCMF( @@ -559,9 +558,8 @@ describe('Container List', () => { const props = getProps.mock.calls[0][0]; props.list.itemProps.onToggleAll(); // then - expect(multiSelectionSetting.setState.mock.calls[0][0]).toEqual({ - selectedItems: new ImmutableList([1, 2, 3]), + selectedItems: [1, 2, 3], }); }); @@ -574,7 +572,7 @@ describe('Container List', () => { left: ['object:remove'], }; multiSelectionSetting.setState = jest.fn(); - const state = fromJS({ selectedItems: [1, 2, 3] }); + const state = { selectedItems: [1, 2, 3] }; multiSelectionSetting.state = state; render( await prepareCMF( @@ -588,7 +586,7 @@ describe('Container List', () => { props.list.itemProps.onToggleAll(); // then expect(multiSelectionSetting.setState.mock.calls[0][0]).toEqual({ - selectedItems: new ImmutableList([]), + selectedItems: [], }); }); @@ -601,7 +599,7 @@ describe('Container List', () => { left: ['object:remove'], }; multiSelectionSetting.setState = jest.fn(); - const state = fromJS({ selectedItems: [1, 2, 3] }); + const state = { selectedItems: [1, 2, 3] }; multiSelectionSetting.state = state; // when @@ -629,14 +627,14 @@ describe('Connected List', () => { // given const state = { cmf: { - components: fromJS({ + components: { 'Container(List)': { - cid: DEFAULT_STATE.toJS(), + cid: DEFAULT_STATE, }, - }), - collections: fromJS({ + }, + collections: { cid: items, - }), + }, }, }; @@ -651,18 +649,18 @@ describe('Connected List', () => { // given const state = { cmf: { - components: fromJS({ + components: { 'Container(List)': { - default: DEFAULT_STATE.toJS(), + default: DEFAULT_STATE, }, - }), - collections: new Map(), + }, + collections: {}, }, }; // when : no collectionId defined const props = mapStateToProps(state, { - items: fromJS(items), + items: items, }); // then @@ -673,10 +671,10 @@ describe('Connected List', () => { // given const state = { cmf: { - components: fromJS({ + components: { 'Container(List)': { cid: { - ...DEFAULT_STATE.toJS(), + ...DEFAULT_STATE, toolbar: { pagination: { onChange: 'pagination:change', @@ -684,8 +682,8 @@ describe('Connected List', () => { }, }, }, - }), - collections: fromJS({ + }, + collections: { cid: { pagination: { totalResults: 36, @@ -694,7 +692,7 @@ describe('Connected List', () => { }, items, }, - }), + }, }, }; @@ -709,7 +707,7 @@ describe('Connected List', () => { // given const state = { cmf: { - components: fromJS({ + components: { 'Container(List)': { default: { displayMode: 'large', @@ -721,25 +719,25 @@ describe('Connected List', () => { filterDocked: true, }, }, - }), - collections: new Map(), + }, + collections: {}, }, }; const initalSettings = cloneDeep(settings); - initalSettings.items = fromJS(items); + initalSettings.items = items; initalSettings.toolbar.filter.defaultFiltering = false; // when : no collectionId defined const props = mapStateToProps(state, initalSettings); // then - expect(props.items.size).toBe(items.size); + expect(props.items.length).toBe(items.length); }); it('should disable sorting when defaultSorting is set to false', () => { // given const state = { cmf: { - components: fromJS({ + components: { 'Container(List)': { default: { displayMode: 'large', @@ -751,25 +749,25 @@ describe('Connected List', () => { filterDocked: true, }, }, - }), - collections: new Map(), + }, + collections: {}, }, }; const initalSettings = cloneDeep(settings); - initalSettings.items = fromJS(items); + initalSettings.items = items; initalSettings.toolbar.sort.defaultSorting = false; // when : no collectionId defined const props = mapStateToProps(state, initalSettings); // then - expect(props.items.toJS()[0].id).toBe(1); + expect(props.items[0].id).toBe(1); }); it('should disable paging when defaultPaging is set to false', () => { // given const state = { cmf: { - components: fromJS({ + components: { 'Container(List)': { default: { displayMode: 'large', @@ -781,17 +779,17 @@ describe('Connected List', () => { filterDocked: true, }, }, - }), - collections: new Map(), + }, + collections: {}, }, }; const initalSettings = cloneDeep(settings); - initalSettings.items = fromJS(items); + initalSettings.items = items; initalSettings.toolbar.pagination.defaultPaging = false; // when : no collectionId defined const props = mapStateToProps(state, initalSettings); // then - expect(props.items.size).toBe(items.size); + expect(props.items.length).toBe(items.length); }); }); diff --git a/packages/containers/src/List/__snapshots__/List.test.jsx.snap b/packages/containers/src/List/__snapshots__/List.test.jsx.snap index 05d987def17..bb561f1e429 100644 --- a/packages/containers/src/List/__snapshots__/List.test.jsx.snap +++ b/packages/containers/src/List/__snapshots__/List.test.jsx.snap @@ -2,33 +2,33 @@ exports[`Connected List > should map items to props from collection List 1`] = ` { - "items": Immutable.List [ - Immutable.Map { - "id": 3, - "name": "Super long title to trigger overflow on some rendering", - "created": "2016-09-22", - "modified": "2016-09-22", + "items": [ + { "author": "Jean-Pierre DUPONT with super long name", - }, - Immutable.Map { - "id": 2, - "name": "Title in input mode", "created": "2016-09-22", + "id": 3, "modified": "2016-09-22", + "name": "Super long title to trigger overflow on some rendering", + }, + { "author": "Jean-Pierre DUPONT", - "icon": "fa fa-file-pdf-o", - "display": "input", "className": "item-1-class", - }, - Immutable.Map { - "id": 1, - "name": "Title with actions", "created": "2016-09-22", + "display": "input", + "icon": "fa fa-file-pdf-o", + "id": 2, "modified": "2016-09-22", + "name": "Title in input mode", + }, + { "author": "Jean-Pierre DUPONT", - "icon": "fa fa-file-excel-o", - "display": "text", "className": "item-0-class", + "created": "2016-09-22", + "display": "text", + "icon": "fa fa-file-excel-o", + "id": 1, + "modified": "2016-09-22", + "name": "Title with actions", }, ], } @@ -36,33 +36,33 @@ exports[`Connected List > should map items to props from collection List 1`] = ` exports[`Connected List > should map items to props from collection Map 1`] = ` { - "items": Immutable.List [ - Immutable.Map { - "id": 3, - "name": "Super long title to trigger overflow on some rendering", - "created": "2016-09-22", - "modified": "2016-09-22", + "items": [ + { "author": "Jean-Pierre DUPONT with super long name", - }, - Immutable.Map { - "id": 2, - "name": "Title in input mode", "created": "2016-09-22", + "id": 3, "modified": "2016-09-22", + "name": "Super long title to trigger overflow on some rendering", + }, + { "author": "Jean-Pierre DUPONT", - "icon": "fa fa-file-pdf-o", - "display": "input", "className": "item-1-class", - }, - Immutable.Map { - "id": 1, - "name": "Title with actions", "created": "2016-09-22", + "display": "input", + "icon": "fa fa-file-pdf-o", + "id": 2, "modified": "2016-09-22", + "name": "Title in input mode", + }, + { "author": "Jean-Pierre DUPONT", - "icon": "fa fa-file-excel-o", - "display": "text", "className": "item-0-class", + "created": "2016-09-22", + "display": "text", + "icon": "fa fa-file-excel-o", + "id": 1, + "modified": "2016-09-22", + "name": "Title with actions", }, ], } @@ -70,33 +70,33 @@ exports[`Connected List > should map items to props from collection Map 1`] = ` exports[`Connected List > should map items to props from default collection List 1`] = ` { - "items": Immutable.List [ - Immutable.Map { - "id": 3, - "name": "Super long title to trigger overflow on some rendering", - "created": "2016-09-22", - "modified": "2016-09-22", + "items": [ + { "author": "Jean-Pierre DUPONT with super long name", - }, - Immutable.Map { - "id": 2, - "name": "Title in input mode", "created": "2016-09-22", + "id": 3, "modified": "2016-09-22", + "name": "Super long title to trigger overflow on some rendering", + }, + { "author": "Jean-Pierre DUPONT", - "icon": "fa fa-file-pdf-o", - "display": "input", "className": "item-1-class", - }, - Immutable.Map { - "id": 1, - "name": "Title with actions", "created": "2016-09-22", + "display": "input", + "icon": "fa fa-file-pdf-o", + "id": 2, "modified": "2016-09-22", + "name": "Title in input mode", + }, + { "author": "Jean-Pierre DUPONT", - "icon": "fa fa-file-excel-o", - "display": "text", "className": "item-0-class", + "created": "2016-09-22", + "display": "text", + "icon": "fa fa-file-excel-o", + "id": 1, + "modified": "2016-09-22", + "name": "Title with actions", }, ], } @@ -110,33 +110,33 @@ exports[`Container List > should put default props 1`] = ` "title": "object:open", }, "displayMode": "table", - "items": Immutable.List [ - Immutable.Map { - "id": 1, - "name": "Title with actions", - "created": "2016-09-22", - "modified": "2016-09-22", + "items": [ + { "author": "Jean-Pierre DUPONT", - "icon": "fa fa-file-excel-o", - "display": "text", "className": "item-0-class", - }, - Immutable.Map { - "id": 2, - "name": "Title in input mode", "created": "2016-09-22", + "display": "text", + "icon": "fa fa-file-excel-o", + "id": 1, "modified": "2016-09-22", + "name": "Title with actions", + }, + { "author": "Jean-Pierre DUPONT", - "icon": "fa fa-file-pdf-o", - "display": "input", "className": "item-1-class", - }, - Immutable.Map { - "id": 3, - "name": "Super long title to trigger overflow on some rendering", "created": "2016-09-22", + "display": "input", + "icon": "fa fa-file-pdf-o", + "id": 2, "modified": "2016-09-22", + "name": "Title in input mode", + }, + { "author": "Jean-Pierre DUPONT with super long name", + "created": "2016-09-22", + "id": 3, + "modified": "2016-09-22", + "name": "Super long title to trigger overflow on some rendering", }, ], "list": { diff --git a/packages/containers/src/List/selector.js b/packages/containers/src/List/selector.js index 578e2cbd89f..56996fcd179 100644 --- a/packages/containers/src/List/selector.js +++ b/packages/containers/src/List/selector.js @@ -2,29 +2,25 @@ import cmf from '@talend/react-cmf'; import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; import { createSelector } from 'reselect'; -import { Map, List } from 'immutable'; function contains(listItem, query, columns) { - let item = listItem; - if (Map.isMap(listItem)) { - item = listItem.toJS(); - } return columns.some( column => - typeof item[column.key] === 'string' && - item[column.key].toLowerCase().indexOf(query.toLowerCase()) !== -1, + typeof listItem[column.key] === 'string' && + listItem[column.key].toLowerCase().indexOf(query.toLowerCase()) !== -1, ); } function getCollection(state, collectionId) { - return state.cmf.collections.get(collectionId); + if (collectionId == null) return undefined; + return cmf.selectors.collections.get(state, collectionId); } export function getCollectionItems(state, collectionId) { const collection = getCollection(state, collectionId); - if (Map.isMap(collection)) { - return collection.get('items'); + if (collection != null && !Array.isArray(collection) && typeof collection === 'object') { + return collection.items !== undefined ? collection.items : collection; } return collection; } @@ -32,15 +28,18 @@ export function getCollectionItems(state, collectionId) { export function configureGetPagination(state, { collectionId }) { const collection = getCollection(state, collectionId); - if (Map.isMap(collection)) { - return collection.get('pagination'); + if (collection != null && !Array.isArray(collection) && typeof collection === 'object') { + return typeof collection.get === 'function' + ? collection.get('pagination') + : collection.pagination; } return null; } function getComponentState(collectionId) { - return state => state.cmf.components.getIn(['Container(List)', collectionId || 'default']); + return state => + cmf.selectors.components.getComponentState(state, 'Container(List)', collectionId || 'default'); } export function configureGetFilteredItems(configure) { @@ -51,7 +50,7 @@ export function configureGetFilteredItems(configure) { componentState => { let results = localConfig.items; if (componentState) { - const searchQuery = componentState.get('searchQuery'); + const searchQuery = componentState?.searchQuery; if (searchQuery && results) { results = results.filter(item => contains(item, searchQuery, localConfig.columns)); } @@ -60,13 +59,16 @@ export function configureGetFilteredItems(configure) { }, ); - return createSelector([getFilteredList, getComponentState], items => items); + return createSelector( + [getFilteredList, getComponentState(localConfig.collectionId)], + items => items, + ); } export function compare(sortBy) { return (a, b) => { - let aValue = a.get(sortBy); - let bValue = b.get(sortBy); + let aValue = typeof a.get === 'function' ? a.get(sortBy) : a[sortBy]; + let bValue = typeof b.get === 'function' ? b.get(sortBy) : b[sortBy]; if (typeof aValue === 'string' && typeof bValue === 'string') { aValue = aValue.toLowerCase(); @@ -93,22 +95,22 @@ export function compare(sortBy) { } export function getSortedResults(componentState, config, listItems) { - if (!List.isList(listItems)) { - return new List(); + if (listItems == null || typeof listItems.filter !== 'function') { + return []; } let results = listItems; if (!isEmpty(componentState)) { - const sortBy = componentState.get('sortOn'); - const sortAsc = componentState.get('sortAsc'); + const sortBy = componentState.sortOn; + const sortAsc = componentState.sortAsc; const sortedColumn = get(config, 'columns', []).find(column => column.key === sortBy); if (get(sortedColumn, 'sortFunction')) { // Immutable sort method returns sorted array - results = results.sort( + results = [...results].sort( cmf.registry.getFromRegistry(sortedColumn.sortFunction)(sortBy, sortAsc), ); } else { - results = results.sort(compare(sortBy)); + results = [...results].sort(compare(sortBy)); } if (!sortAsc) { @@ -123,25 +125,25 @@ export function configureGetSortedItems(config, listItems) { getSortedResults(componentState, config, listItems), ); - return createSelector([getSortedList, getComponentState], items => items); + return createSelector([getSortedList, getComponentState(config.collectionId)], items => items); } export function configureGetPagedItems(configure, listItems) { const getPagedList = createSelector(getComponentState(configure.collectionId), componentState => { let results = listItems; if (componentState) { - const startIndex = componentState.get('startIndex'); - const itemsPerPage = componentState.get('itemsPerPage'); + const startIndex = componentState.startIndex; + const itemsPerPage = componentState.itemsPerPage; if (itemsPerPage > 0 && startIndex > 0) { results = results.slice( startIndex - 1, - Math.min(startIndex + itemsPerPage - 1, results.size), + Math.min(startIndex + itemsPerPage - 1, results.size ?? results.length), ); } } return results; }); - return createSelector([getPagedList, getComponentState], items => items); + return createSelector([getPagedList, getComponentState(configure.collectionId)], items => items); } diff --git a/packages/containers/src/List/selector.test.js b/packages/containers/src/List/selector.test.js index b9525d7d579..8ba0eb4dfdb 100644 --- a/packages/containers/src/List/selector.test.js +++ b/packages/containers/src/List/selector.test.js @@ -1,5 +1,4 @@ import cmf, { mock } from '@talend/react-cmf'; -import { fromJS, List } from 'immutable'; import { mapStateToProps } from './List.connect'; import { compare, getSortedResults } from './selector'; @@ -26,7 +25,7 @@ const localConfig = { }; const state = mock.store.state(); -state.cmf.collections = fromJS({ +state.cmf.collections = { default: { columns: [ { key: 'id', name: 'ID' }, @@ -34,11 +33,11 @@ state.cmf.collections = fromJS({ ], items: localConfig.items, }, -}); +}; describe('List Selector tests', () => { it('should not filter the list when there is no search query', () => { - state.cmf.components = fromJS({ + state.cmf.components = { 'Container(List)': { default: { displayMode: 'large', @@ -50,14 +49,14 @@ describe('List Selector tests', () => { filterDocked: true, }, }, - }); + }; const props = mapStateToProps(state, localConfig); - expect(props.items.size).toBe(localConfig.items.length); + expect(props.items.length).toBe(localConfig.items.length); }); it('should filter the list when filter on visible column', () => { - state.cmf.components = fromJS({ + state.cmf.components = { 'Container(List)': { default: { displayMode: 'large', @@ -69,14 +68,14 @@ describe('List Selector tests', () => { filterDocked: true, }, }, - }); + }; const props = mapStateToProps(state, localConfig); - expect(props.items.size).toBe(1); + expect(props.items.length).toBe(1); }); it('should return no elements when search on non visible column', () => { - state.cmf.components = fromJS({ + state.cmf.components = { 'Container(List)': { default: { displayMode: 'large', @@ -88,85 +87,76 @@ describe('List Selector tests', () => { filterDocked: true, }, }, - }); + }; const props = mapStateToProps(state, localConfig); - expect(props.items.size).toBe(0); + expect(props.items.length).toBe(0); }); it('should return items in a page when pagination applied', () => { - state.cmf.components = fromJS({ + state.cmf.components = { 'Container(List)': { default: { itemsPerPage: 1, startIndex: 1, }, }, - }); + }; const props = mapStateToProps(state, { ...localConfig, toolbar: { pagination: {} } }); - expect(props.items.size).toBe(1); + expect(props.items.length).toBe(1); }); it('should sort a different column type correctly', () => { expect( - fromJS([{ stringID: '1' }, { stringID: '11' }, { stringID: '12' }, { stringID: '2' }]).sort( + [{ stringID: '1' }, { stringID: '11' }, { stringID: '12' }, { stringID: '2' }].sort( compare('stringID'), ), - ).toEqual( - fromJS([{ stringID: '1' }, { stringID: '11' }, { stringID: '12' }, { stringID: '2' }]), - ); + ).toEqual([{ stringID: '1' }, { stringID: '11' }, { stringID: '12' }, { stringID: '2' }]); expect( - fromJS([ + [ { stringName: 'Uzbekistan' }, { stringName: 'American Samoa' }, { stringName: 'Djibouti' }, { stringName: 'Luxembourg' }, - ]).sort(compare('stringName')), - ).toEqual( - fromJS([ - { stringName: 'American Samoa' }, - { stringName: 'Djibouti' }, - { stringName: 'Luxembourg' }, - { stringName: 'Uzbekistan' }, - ]), - ); + ].sort(compare('stringName')), + ).toEqual([ + { stringName: 'American Samoa' }, + { stringName: 'Djibouti' }, + { stringName: 'Luxembourg' }, + { stringName: 'Uzbekistan' }, + ]); expect( - fromJS([{ intID: 1 }, { intID: 11 }, { intID: 12 }, { intID: 2 }]).sort(compare('intID')), - ).toEqual(fromJS([{ intID: 1 }, { intID: 2 }, { intID: 11 }, { intID: 12 }])); + [{ intID: 1 }, { intID: 11 }, { intID: 12 }, { intID: 2 }].sort(compare('intID')), + ).toEqual([{ intID: 1 }, { intID: 2 }, { intID: 11 }, { intID: 12 }]); expect( - fromJS([{ mixedID: '1' }, { mixedID: '11' }, { mixedID: '-' }, { mixedID: '2' }]).sort( + [{ mixedID: '1' }, { mixedID: '11' }, { mixedID: '-' }, { mixedID: '2' }].sort( compare('mixedID'), ), - ).toEqual(fromJS([{ mixedID: '-' }, { mixedID: '1' }, { mixedID: '11' }, { mixedID: '2' }])); + ).toEqual([{ mixedID: '-' }, { mixedID: '1' }, { mixedID: '11' }, { mixedID: '2' }]); expect( - fromJS([ - { mixedString: 'a' }, - { mixedString: 'b' }, - { mixedString: 'C' }, - { mixedString: 'D' }, - ]).sort(compare('mixedString')), - ).toEqual( - fromJS([ - { mixedString: 'a' }, - { mixedString: 'b' }, - { mixedString: 'C' }, - { mixedString: 'D' }, - ]), - ); + [{ mixedString: 'a' }, { mixedString: 'b' }, { mixedString: 'C' }, { mixedString: 'D' }].sort( + compare('mixedString'), + ), + ).toEqual([ + { mixedString: 'a' }, + { mixedString: 'b' }, + { mixedString: 'C' }, + { mixedString: 'D' }, + ]); }); it('should test the getSortedResults method', () => { cmf.registry.addToRegistry('myCustomSortFn', (sortBy, sortAsc) => (a, b) => { if (sortAsc) { - return a.get(sortBy) > b.get(sortBy) ? -1 : 1; + return a[sortBy] > b[sortBy] ? -1 : 1; } return 0; }); - const componentState = fromJS({ + const componentState = { sortOn: 'data', sortAsc: true, - }); + }; const config = { columns: [ { @@ -178,41 +168,40 @@ describe('List Selector tests', () => { // Sorting the list expect( - getSortedResults( - componentState, - config, - fromJS([{ data: 0 }, { data: 4 }, { data: 2 }, { data: 11 }, { data: 1 }, { data: 23 }]), - ), - ).toEqual( - fromJS([{ data: 0 }, { data: 1 }, { data: 2 }, { data: 4 }, { data: 11 }, { data: 23 }]), - ); + getSortedResults(componentState, config, [ + { data: 0 }, + { data: 4 }, + { data: 2 }, + { data: 11 }, + { data: 1 }, + { data: 23 }, + ]), + ).toEqual([{ data: 0 }, { data: 1 }, { data: 2 }, { data: 4 }, { data: 11 }, { data: 23 }]); // Sorting by column and custom sort function expect( getSortedResults( - fromJS({ sortOn: 'a', sortAsc: true }), + { sortOn: 'a', sortAsc: true }, { columns: [{ key: 'a', sortFunction: 'myCustomSortFn' }] }, - fromJS([{ a: 1 }, { a: 3 }, { a: 2 }]), + [{ a: 1 }, { a: 3 }, { a: 2 }], ), - ).toEqual(fromJS([{ a: 3 }, { a: 2 }, { a: 1 }])); + ).toEqual([{ a: 3 }, { a: 2 }, { a: 1 }]); // Desc sort expect( - getSortedResults( - fromJS({ sortOn: 'key', sortAsc: false }), - config, - fromJS([{ key: 1 }, { key: 3 }, { key: 2 }]), - ), - ).toEqual(fromJS([{ key: 3 }, { key: 2 }, { key: 1 }])); + getSortedResults({ sortOn: 'key', sortAsc: false }, config, [ + { key: 1 }, + { key: 3 }, + { key: 2 }, + ]), + ).toEqual([{ key: 3 }, { key: 2 }, { key: 1 }]); // Edge cases [null, undefined, 1, true, false, [], {}].forEach(val => - expect(getSortedResults(val, val, fromJS([{ item: 'one' }]))).toEqual( - fromJS([{ item: 'one' }]), - ), + expect(getSortedResults(val, val, [{ item: 'one' }])).toEqual([{ item: 'one' }]), ); // With no items - expect(getSortedResults(componentState, config, null)).toEqual(new List()); + expect(getSortedResults(componentState, config, null)).toEqual([]); }); }); diff --git a/packages/containers/src/Notification/Notification.connect.js b/packages/containers/src/Notification/Notification.connect.js index 76e3105003b..a057cc47ad8 100644 --- a/packages/containers/src/Notification/Notification.connect.js +++ b/packages/containers/src/Notification/Notification.connect.js @@ -7,11 +7,11 @@ export function componentId(ownProps) { export function deleteNotification(indexNotification) { return function mutator(prevStateProps) { - const notifications = prevStateProps.state.get('notifications'); + const notifications = prevStateProps.state.notifications; const index = notifications.indexOf(indexNotification); if (index > -1) { - const newNotif = notifications.delete(index); - return prevStateProps.state.set('notifications', newNotif); + const newNotif = [...notifications.slice(0, index), ...notifications.slice(index + 1)]; + return { ...prevStateProps.state, notifications: newNotif }; } return prevStateProps.state; }; diff --git a/packages/containers/src/Notification/Notification.container.jsx b/packages/containers/src/Notification/Notification.container.jsx index cc5cdf5ad2e..42736f8472b 100644 --- a/packages/containers/src/Notification/Notification.container.jsx +++ b/packages/containers/src/Notification/Notification.container.jsx @@ -1,14 +1,13 @@ import PropTypes from 'prop-types'; -import { List, Map } from 'immutable'; import Component from '@talend/react-components/lib/Notification'; import { cmfConnect } from '@talend/react-cmf'; -export const DEFAULT_STATE = new Map({ - notifications: new List(), -}); +export const DEFAULT_STATE = { + notifications: [], +}; function Notification(props) { - const state = (props.state || DEFAULT_STATE).toJS(); + const state = props.state || DEFAULT_STATE; return ( props.deleteNotification(i)} diff --git a/packages/containers/src/Notification/Notification.sagas.js b/packages/containers/src/Notification/Notification.sagas.js index ee0b8e3ba1b..97208d6e2d1 100644 --- a/packages/containers/src/Notification/Notification.sagas.js +++ b/packages/containers/src/Notification/Notification.sagas.js @@ -12,12 +12,16 @@ const DEFAULT_COMPONENT_ID = 'Notification'; export function* onPushNotification(action) { const componentState = yield select(state => Notification.getState(state, DEFAULT_COMPONENT_ID)); - const newComponentState = componentState.updateIn(['notifications'], notifications => - notifications.push({ - id: randomUUID(), - ...action.notification, - }), - ); + const newComponentState = { + ...componentState, + notifications: [ + ...(componentState?.notifications ?? []), + { + id: randomUUID(), + ...action.notification, + }, + ], + }; const updateStateAction = Notification.setStateAction(newComponentState, DEFAULT_COMPONENT_ID); yield put(updateStateAction); diff --git a/packages/containers/src/Notification/Notification.sagas.test.js b/packages/containers/src/Notification/Notification.sagas.test.js index e26a0a4db47..745303fec8d 100644 --- a/packages/containers/src/Notification/Notification.sagas.test.js +++ b/packages/containers/src/Notification/Notification.sagas.test.js @@ -1,4 +1,3 @@ -import Immutable from 'immutable'; import { runSaga } from 'redux-saga'; import { onPushNotification } from './Notification.sagas'; @@ -18,13 +17,13 @@ describe('Notification sagas', () => { dispatch: a => dispatched.push(a), getState: () => ({ cmf: { - components: Immutable.fromJS({ + components: { 'Container(Notification)': { Notification: { notifications: [], }, }, - }), + }, }, }), }, @@ -32,8 +31,7 @@ describe('Notification sagas', () => { onPushNotificationAction, ).done; - // Convert first, the half immutable payload to a full one then back to a full js one - const actions = Immutable.fromJS(dispatched).toJS(); + const actions = dispatched; expect(actions[0]).toEqual({ type: 'Container(Notification).setState', diff --git a/packages/containers/src/Notification/Notification.stories.jsx b/packages/containers/src/Notification/Notification.stories.jsx index 85f74f523b4..3d9e2bc4fa1 100644 --- a/packages/containers/src/Notification/Notification.stories.jsx +++ b/packages/containers/src/Notification/Notification.stories.jsx @@ -1,8 +1,7 @@ -import { List, Map } from 'immutable'; import Notification from '.'; -const initialState = new Map({ - notifications: new List([ +const initialState = { + notifications: [ { id: 'story-1', message: 'This is a feedback of your operation1, This is a feedback of your operation1', @@ -20,8 +19,8 @@ const initialState = new Map({ type: 'warning', message: ['This is a feedback of your operation3', 'details'], }, - ]), -}); + ], +}; export default { title: 'Notification', diff --git a/packages/containers/src/Notification/Notification.test.jsx b/packages/containers/src/Notification/Notification.test.jsx index c38ad6f2e6b..61030d533c8 100644 --- a/packages/containers/src/Notification/Notification.test.jsx +++ b/packages/containers/src/Notification/Notification.test.jsx @@ -3,7 +3,6 @@ /* eslint-disable react/display-name */ import { render } from '@testing-library/react'; import { mock } from '@talend/react-cmf'; -import Immutable, { fromJS } from 'immutable'; // eslint-disable-next-line @talend/import-depth import { prepareCMF } from '@talend/react-cmf/lib/mock/rtl'; import Container from './Notification.container'; @@ -30,7 +29,7 @@ describe('Connected Notification', () => { it('mergeProps should merge the props', () => { const message = { message: 'hello world' }; const stateProps = { - state: fromJS({ notifications: [message] }), + state: { notifications: [message] }, }; const dispatchProps = { setState: jest.fn(), @@ -38,7 +37,7 @@ describe('Connected Notification', () => { const ownProps = { foo: 'bar' }; const props = mergeProps(stateProps, dispatchProps, ownProps); expect(props.foo).toBe('bar'); - expect(props.state.get('notifications').size).toBe(1); + expect(props.state.notifications.length).toBe(1); expect(typeof props.setState).toBe('function'); expect(typeof props.deleteNotification).toBe('function'); props.deleteNotification(message); @@ -46,20 +45,20 @@ describe('Connected Notification', () => { }); it('deleteNotification should delete notification', () => { - const message = fromJS({ message: 'hello world' }); + const message = { message: 'hello world' }; const stateProps = { - state: fromJS({ notifications: [message] }), + state: { notifications: [message] }, }; - expect(deleteNotification(message)(stateProps).toJS()).toEqual({ + expect(deleteNotification(message)(stateProps)).toEqual({ notifications: [], }); }); it('deleteNotification should do nothing if the notification does not exist', () => { - const ok = fromJS({ message: 'ahah' }); - const ko = fromJS({ message: 'hello world' }); + const ok = { message: 'ahah' }; + const ko = { message: 'hello world' }; const stateProps = { - state: fromJS({ notifications: [ok] }), + state: { notifications: [ok] }, }; expect(deleteNotification(ko)(stateProps)).toEqual(stateProps.state); }); @@ -68,74 +67,65 @@ describe('Connected Notification', () => { describe('Notification.pushNotification', () => { it('should add a Notification in the state', () => { const state = mock.store.state(); - state.cmf.components = fromJS({ + state.cmf.components = { 'Container(Notification)': { Notification: { notifications: [], }, }, - }); + }; const notification = { message: 'hello world' }; const newState = pushNotification(state, notification); expect(newState).not.toBe(state); - const notifications = newState.cmf.components.getIn([ - 'Container(Notification)', - 'Notification', - 'notifications', - ]); - expect(notifications.size).toBe(1); - expect(notifications.get(0).message).toBe('hello world'); + const notifications = + newState.cmf.components['Container(Notification)'].Notification.notifications; + expect(notifications.length).toBe(1); + expect(notifications[0].message).toBe('hello world'); }); it('should add a Notification in the state even if the state slot is not yet available', () => { const state = mock.store.state(); - state.cmf.components = new Immutable.Map(); + state.cmf.components = {}; const notification = { message: 'hello world' }; const newState = pushNotification(state, notification); - const notifications = newState.cmf.components.getIn([ - 'Container(Notification)', - 'Notification', - 'notifications', - ]); - expect(notifications.size).toBe(1); - expect(notifications.get(0).message).toBe('hello world'); + const notifications = + newState.cmf.components['Container(Notification)'].Notification.notifications; + expect(notifications.length).toBe(1); + expect(notifications[0].message).toBe('hello world'); }); it('should delete all Notification in the state', () => { const state = mock.store.state(); - state.cmf.components = fromJS({ + state.cmf.components = { 'Container(Notification)': { Notification: { notifications: [{ message: 'hello world' }, { message: 'hello world2' }], }, }, - }); + }; const newState = clearNotifications(state); expect(newState).not.toBe(state); - const notifications = newState.cmf.components.getIn([ - 'Container(Notification)', - 'Notification', - 'notifications', - ]); - expect(notifications.size).toBe(0); + const notifications = + newState.cmf.components['Container(Notification)'].Notification.notifications; + expect(notifications.length).toBe(0); }); it('should not change the state if no notification', () => { const state = mock.store.state(); - state.cmf.components = fromJS({ + state.cmf.components = { 'Container(Notification)': { Notification: { notifications: [], }, }, - }); + }; const newState = pushNotification(state); expect(newState).toBe(state); }); it('should not change the state if notification state is not yet availbale', () => { const state = mock.store.state(); - state.cmf.components = fromJS({}); + state.cmf.components = {}; const newState = pushNotification(state); expect(newState).toBe(state); }); diff --git a/packages/containers/src/Notification/clearNotifications.js b/packages/containers/src/Notification/clearNotifications.js index 4c6abd0b661..44d375f5c0a 100644 --- a/packages/containers/src/Notification/clearNotifications.js +++ b/packages/containers/src/Notification/clearNotifications.js @@ -1,13 +1,31 @@ +import cmf from '@talend/react-cmf'; + export default function clearNotifications(state) { - const path = ['Container(Notification)', 'Notification', 'notifications']; - let notifs = state.cmf.components.getIn(path); + const notifs = cmf.selectors.components.getComponentStateProperty( + state, + 'Container(Notification)', + 'Notification', + 'notifications', + ); if (!notifs) { return state; } - notifs = notifs.clear(); - const newState = { ...state }; - newState.cmf.components = state.cmf.components.setIn(path, notifs); - return newState; + return { + ...state, + cmf: { + ...state.cmf, + components: { + ...state.cmf.components, + 'Container(Notification)': { + ...state.cmf.components?.['Container(Notification)'], + Notification: { + ...state.cmf.components?.['Container(Notification)']?.Notification, + notifications: [], + }, + }, + }, + }, + }; } diff --git a/packages/containers/src/Notification/pushNotification.js b/packages/containers/src/Notification/pushNotification.js index 3e8e51ca99c..831983a4dd9 100644 --- a/packages/containers/src/Notification/pushNotification.js +++ b/packages/containers/src/Notification/pushNotification.js @@ -1,5 +1,5 @@ import get from 'lodash/get'; -import Immutable from 'immutable'; +import cmf from '@talend/react-cmf'; import { randomUUID } from '@talend/utils'; /** @@ -13,13 +13,29 @@ export default function pushNotification(state, notification) { if (!get(notification, 'message')) { return state; } - const path = ['Container(Notification)', 'Notification', 'notifications']; - let notifs = state.cmf.components.getIn(path, new Immutable.List()); - notifs = notifs.push({ - id: randomUUID(), - ...notification, - }); - const newState = { ...state }; - newState.cmf.components = state.cmf.components.setIn(path, notifs); + const notifs = + cmf.selectors.components.getComponentStateProperty( + state, + 'Container(Notification)', + 'Notification', + 'notifications', + ) || []; + const newNotifs = [...notifs, { id: randomUUID(), ...notification }]; + const newState = { + ...state, + cmf: { + ...state.cmf, + components: { + ...state.cmf.components, + 'Container(Notification)': { + ...state.cmf.components?.['Container(Notification)'], + Notification: { + ...state.cmf.components?.['Container(Notification)']?.Notification, + notifications: newNotifs, + }, + }, + }, + }, + }; return newState; } diff --git a/packages/containers/src/ObjectViewer/ObjectViewer.container.jsx b/packages/containers/src/ObjectViewer/ObjectViewer.container.jsx index 43c5ac9482e..b34899439f1 100644 --- a/packages/containers/src/ObjectViewer/ObjectViewer.container.jsx +++ b/packages/containers/src/ObjectViewer/ObjectViewer.container.jsx @@ -1,37 +1,35 @@ import PropTypes from 'prop-types'; import { Component as RComponent } from 'react'; -import { List, Map } from 'immutable'; import get from 'lodash/get'; import Component from '@talend/react-components/lib/ObjectViewer'; import { cmfConnect } from '@talend/react-cmf'; -export const DEFAULT_STATE = new Map({ - edited: new List(), // Array of JSONPath - opened: new List(), // Array of JSONPath +export const DEFAULT_STATE = { + edited: [], // Array of JSONPath + opened: [], // Array of JSONPath selectedJsonpath: '', // Selected JSONPath - modified: new Map(), // Store the onChange -}); + modified: {}, // Store the onChange +}; export function open(path, state) { - return state.set('opened', state.get('opened').push(path)); + return { opened: [...(state.opened ?? []), path] }; } export function select(path, state) { - return state.set('selectedJsonpath', path); + return { selectedJsonpath: path }; } export function close(path, state) { - const opened = state.get('opened'); - return state.set('opened', opened.delete(opened.indexOf(path))); + return { opened: (state.opened ?? []).filter(p => p !== path) }; } export function edit(path, state) { - return state.set('edited', state.get('edited').push(path)); + return { edited: [...(state.edited ?? []), path] }; } export function change(path, state, value) { - return state.set('modified', state.get('modified').set(path, value)); + return { modified: { ...(state.modified ?? {}), [path]: value } }; } export function toggleState(prevState, data) { @@ -42,21 +40,16 @@ export function toggleState(prevState, data) { return open(data.jsonpath, prevState.state); } - return prevState; + return {}; } export function openAllState(prevState, siblings) { - let openedIds = prevState.state.get('opened'); - - siblings + const openedIds = prevState.state?.opened ?? []; + const newIds = siblings .filter(({ data }) => typeof data === 'object') - .forEach(({ jsonpath }) => { - if (!openedIds.includes(jsonpath)) { - openedIds = openedIds.push(jsonpath); - } - }); - - return prevState.state.set('opened', openedIds); + .map(({ jsonpath }) => jsonpath) + .filter(id => !openedIds.includes(id)); + return { opened: [...openedIds, ...newIds] }; } export function selectWrapper(prevState, data) { @@ -68,7 +61,7 @@ export function editWrapper(prevState, data) { return edit(data.jsonpath, prevState.state); } - return prevState; + return {}; } class ObjectViewer extends RComponent { @@ -112,7 +105,7 @@ class ObjectViewer extends RComponent { } render() { - const state = (this.props.state || DEFAULT_STATE).toJS(); + const state = this.props.state || DEFAULT_STATE; return ( ); } diff --git a/packages/containers/src/ObjectViewer/ObjectViewer.test.jsx b/packages/containers/src/ObjectViewer/ObjectViewer.test.jsx index 12112325c00..63aa0cea999 100644 --- a/packages/containers/src/ObjectViewer/ObjectViewer.test.jsx +++ b/packages/containers/src/ObjectViewer/ObjectViewer.test.jsx @@ -118,14 +118,14 @@ describe('editValue', () => { }; prevState.state = editWrapper(prevState, someData); - expect(prevState.state.get('edited').size).toBe(1); + expect(prevState.state.edited.length).toBe(1); }); it('should change', () => { const prevState = { state: DEFAULT_STATE }; prevState.state = change(path, prevState.state, 'new label'); - expect(prevState.state.get('modified').size).toBe(1); + expect(Object.keys(prevState.state.modified).length).toBe(1); }); }); @@ -133,18 +133,18 @@ describe('toggleState', () => { const prevState = { state: DEFAULT_STATE }; it('should open', () => { const newState = toggleState(prevState, { isOpened: false, jsonpath: path }); - expect(newState.get('opened').size).toBe(1); - expect(newState.get('opened').first()).toEqual(path); + expect(newState.opened.length).toBe(1); + expect(newState.opened[0]).toEqual(path); }); it(' should close', () => { - prevState.state = prevState.state.set('opened', prevState.state.get('opened').push(path)); + prevState.state = { ...prevState.state, opened: [...(prevState.state.opened ?? []), path] }; - expect(prevState.state.get('opened').size).toBe(1); - expect(prevState.state.get('opened').first()).toEqual(path); + expect(prevState.state.opened.length).toBe(1); + expect(prevState.state.opened[0]).toEqual(path); const newState = toggleState(prevState, { isOpened: true, jsonpath: path }); - expect(newState.get('opened').size).toBe(0); + expect(newState.opened.length).toBe(0); }); }); @@ -153,6 +153,6 @@ describe('select', () => { const prevState = { state: DEFAULT_STATE }; prevState.state = selectWrapper(prevState, { jsonpath: path }); - expect(prevState.state.get('selectedJsonpath')).toEqual(path); + expect(prevState.state.selectedJsonpath).toEqual(path); }); }); diff --git a/packages/containers/src/PieChartButton/PieChartButton.connect.jsx b/packages/containers/src/PieChartButton/PieChartButton.connect.jsx index 2d908461d60..018edf6ecf4 100644 --- a/packages/containers/src/PieChartButton/PieChartButton.connect.jsx +++ b/packages/containers/src/PieChartButton/PieChartButton.connect.jsx @@ -1,10 +1,9 @@ import PropTypes from 'prop-types'; -import Immutable from 'immutable'; import omit from 'lodash/omit'; import { cmfConnect, Inject } from '@talend/react-cmf'; import PieChart from '@talend/react-components/lib/PieChart'; -export const DEFAULT_STATE = new Immutable.Map({}); +export const DEFAULT_STATE = {}; export function ContainerPieChartButton(props) { let overlayComponent = null; @@ -30,14 +29,14 @@ export function ContainerPieChartButton(props) { } const state = props.state || DEFAULT_STATE; - const model = state.has('model') ? state.get('model').toJS() : props.model; + const model = 'model' in state ? state.model : props.model; const newProps = { ...omit(props, cmfConnect.INJECTED_PROPS.concat(['getComponent', 'initialState'])), model, - inProgress: state.get('inProgress', props.inProgress), - loading: state.get('loading', props.loading), - available: state.get('available', props.available), + inProgress: state?.inProgress ?? props.inProgress, + loading: state?.loading ?? props.loading, + available: state?.available ?? props.available, overlayComponent, onClick, }; diff --git a/packages/containers/src/PieChartButton/PieChartButton.test.js b/packages/containers/src/PieChartButton/PieChartButton.test.js index f2e7d72dc0d..76e60a354a7 100644 --- a/packages/containers/src/PieChartButton/PieChartButton.test.js +++ b/packages/containers/src/PieChartButton/PieChartButton.test.js @@ -1,4 +1,3 @@ -import Immutable from 'immutable'; import { screen, render } from '@testing-library/react'; import Connected, { ContainerPieChartButton } from './PieChartButton.connect'; @@ -11,7 +10,7 @@ describe('PieChartButton connected', () => { describe('PieChartButton container', () => { it('should render', () => { - const initialState = Immutable.fromJS({ + const initialState = { model: [ { percentage: 10, color: 'rio-grande' }, { percentage: 15, color: 'chestnut-rose' }, @@ -19,13 +18,13 @@ describe('PieChartButton container', () => { { percentage: 20, color: 'dove-gray' }, { percentage: 15, color: 'silver-chalice' }, ], - }); + }; const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); it('should render not available pie chart button', () => { - const initialState = Immutable.fromJS({ + const initialState = { model: [ { percentage: 10, color: 'rio-grande' }, { percentage: 15, color: 'chestnut-rose' }, @@ -34,13 +33,13 @@ describe('PieChartButton container', () => { { percentage: 15, color: 'silver-chalice' }, ], available: false, - }); + }; const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it('should render loading pie chart button', () => { - const initialState = Immutable.fromJS({ + const initialState = { model: [ { percentage: 10, color: 'rio-grande' }, { percentage: 15, color: 'chestnut-rose' }, @@ -49,7 +48,7 @@ describe('PieChartButton container', () => { { percentage: 15, color: 'silver-chalice' }, ], loading: true, - }); + }; render(); expect(screen.getByLabelText('Loading chart')).toBeVisible(); expect(screen.getByLabelText('Loading chart')).toHaveAttribute('aria-busy', 'true'); diff --git a/packages/containers/src/SelectObject/SelectObject.component.jsx b/packages/containers/src/SelectObject/SelectObject.component.jsx index acd7c66d523..f6fc3177756 100644 --- a/packages/containers/src/SelectObject/SelectObject.component.jsx +++ b/packages/containers/src/SelectObject/SelectObject.component.jsx @@ -1,5 +1,3 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; - import classNames from 'classnames'; import PropTypes from 'prop-types'; @@ -55,12 +53,12 @@ function SelectObject({
{filteredData.map(data => ( results.onClick(event, data)} > -

{data.get(nameAttr)}

- {data.get('currentPosition')} +

{data[nameAttr]}

+ {data.currentPosition}
))}
@@ -80,12 +78,12 @@ SelectObject.propTypes = { list: PropTypes.object, filter: PropTypes.object, schema: PropTypes.object, - filteredData: ImmutablePropTypes.List, + filteredData: PropTypes.array, results: PropTypes.shape({ selectedId: PropTypes.string, onClick: PropTypes.func, }), - sourceData: ImmutablePropTypes.List, + sourceData: PropTypes.array, selected: PropTypes.object, }; diff --git a/packages/containers/src/SelectObject/SelectObject.component.test.js b/packages/containers/src/SelectObject/SelectObject.component.test.js index 3a7421577e8..7c8c6172dc2 100644 --- a/packages/containers/src/SelectObject/SelectObject.component.test.js +++ b/packages/containers/src/SelectObject/SelectObject.component.test.js @@ -1,13 +1,12 @@ import { render } from '@testing-library/react'; import { mock } from '@talend/react-cmf'; -import Immutable from 'immutable'; import Component from './SelectObject.component'; describe('Component SelectObject', () => { it('should render', () => { const context = mock.store.context(); - const item = new Immutable.Map({ id: '1', name: 'foo' }); + const item = { id: '1', name: 'foo', get: k => ({ id: '1', name: 'foo' })[k] }; const props = { id: 'my-tree', schema: { @@ -21,7 +20,7 @@ describe('Component SelectObject', () => { }, }, }, - sourceData: new Immutable.List([item]), + sourceData: Object.assign([item], { toJS: () => [{ id: '1', name: 'foo' }] }), filter: { className: 'my-custom-filter', }, diff --git a/packages/containers/src/SelectObject/SelectObject.connect.js b/packages/containers/src/SelectObject/SelectObject.connect.js index b4391ac49ff..57fdfe5a7ce 100644 --- a/packages/containers/src/SelectObject/SelectObject.connect.js +++ b/packages/containers/src/SelectObject/SelectObject.connect.js @@ -1,4 +1,4 @@ -import { cmfConnect } from '@talend/react-cmf'; +import cmf, { cmfConnect } from '@talend/react-cmf'; import Container, { DEFAULT_STATE } from './SelectObject.container'; import { DISPLAY_NAME as FILTER_NAME, QUERY_ATTR } from '../FilterBar/FilterBar.container'; @@ -7,7 +7,7 @@ import { DISPLAY_NAME as TREE_NAME } from '../TreeView/TreeView.container'; export function mapStateToProps(state, ownProps) { const props = {}; if (ownProps.source) { - props.sourceData = state.cmf.collections.getIn(ownProps.source.split('.')); + props.sourceData = cmf.selectors.collections.get(state, ownProps.source.split('.')); } if (ownProps.nameAttr && ownProps.tree) { props.tree = { @@ -15,15 +15,11 @@ export function mapStateToProps(state, ownProps) { ...ownProps.tree, }; } - const filterState = state.cmf.components.getIn([FILTER_NAME, ownProps.id]); - if (filterState) { - props.query = filterState.get(QUERY_ATTR, ''); - } else { - props.query = ''; - } - const treeState = state.cmf.components.getIn([TREE_NAME, ownProps.id]); + const filterState = state.cmf.components?.[FILTER_NAME]?.[ownProps.id]; + props.query = filterState?.[QUERY_ATTR] ?? ''; + const treeState = state.cmf.components?.[TREE_NAME]?.[ownProps.id]; if (treeState) { - props.selectedId = treeState.get('selectedId'); + props.selectedId = treeState.selectedId; } return props; } diff --git a/packages/containers/src/SelectObject/SelectObject.connect.test.js b/packages/containers/src/SelectObject/SelectObject.connect.test.js index 46cc330f9e3..763f4cf91a0 100644 --- a/packages/containers/src/SelectObject/SelectObject.connect.test.js +++ b/packages/containers/src/SelectObject/SelectObject.connect.test.js @@ -1,5 +1,4 @@ import { mock } from '@talend/react-cmf'; -import Immutable from 'immutable'; import Container from './SelectObject.container'; import Connected, { mapStateToProps } from './SelectObject.connect'; @@ -11,21 +10,12 @@ describe('Connected SelectObject', () => { }); it('should map state to props', () => { const state = mock.store.state(); - const data = new Immutable.List([ - new Immutable.Map({ label: 'foo' }), - new Immutable.Map({ label: 'bar' }), - ]); - state.cmf.collections = new Immutable.Map({ - width: new Immutable.Map({ data }), - }); - state.cmf.components = new Immutable.Map({ - 'Container(FilterBar)': new Immutable.Map({ - test: new Immutable.Map({ query: 'foo' }), - }), - 'Container(Tree)': new Immutable.Map({ - test: new Immutable.Map({ selectedId: '27' }), - }), - }); + const data = [{ label: 'foo' }, { label: 'bar' }]; + state.cmf.collections = { width: { data } }; + state.cmf.components = { + 'Container(FilterBar)': { test: { query: 'foo' } }, + 'Container(Tree)': { test: { selectedId: '27' } }, + }; const props = mapStateToProps(state, { id: 'test', nameAttr: 'label', diff --git a/packages/containers/src/SelectObject/SelectObject.container.jsx b/packages/containers/src/SelectObject/SelectObject.container.jsx index d5d7c00e3f7..5ec936a6de4 100644 --- a/packages/containers/src/SelectObject/SelectObject.container.jsx +++ b/packages/containers/src/SelectObject/SelectObject.container.jsx @@ -2,12 +2,11 @@ import { Component as RComponent } from 'react'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; import { cmfConnect } from '@talend/react-cmf'; -import Immutable, { List } from 'immutable'; import Component from './SelectObject.component'; export const DISPLAY_NAME = 'Container(SelectObject)'; -export const DEFAULT_STATE = new Immutable.Map({}); +export const DEFAULT_STATE = {}; function noop() {} @@ -18,11 +17,11 @@ function noop() {} */ export function getById(items, id, { idAttr = 'id' } = {}) { let found; - items.forEach(item => { - if (item.get(idAttr) === id) { - found = item.toJS(); - } else if (!found && item.get('children', new List()).size > 0) { - found = getById(item.get('children'), id, { idAttr }); + (items || []).forEach(item => { + if (item[idAttr] === id) { + found = item; + } else if (!found && (item.children ?? []).length > 0) { + found = getById(item.children, id, { idAttr }); } }); return found; @@ -35,7 +34,7 @@ export function getById(items, id, { idAttr = 'id' } = {}) { * @return {Boolean} */ function isLeafElement(item) { - return item.get('children', new List()).size === 0; + return (item.children ?? []).length === 0; } /** @@ -46,14 +45,14 @@ function isLeafElement(item) { * @param {String} query the query element used to match * @param {String} nameAttr the attribute of item on which should be matched * @param {callback} onMatch callback to call if match happen - * @param {List} accumulator + * @param {Array} accumulator */ function matchOnLeaf(item, currentPosition, query, nameAttr, onMatch, accumulator) { - const currentElementName = item.get(nameAttr, ''); + const currentElementName = item[nameAttr] ?? ''; if (currentElementName.toLowerCase().includes(query.toLowerCase())) { - const withElementPosition = item.set('currentPosition', currentPosition); + const withElementPosition = { ...item, currentPosition }; onMatch(item); - return accumulator.push(withElementPosition); + return [...accumulator, withElementPosition]; } return accumulator; } @@ -65,7 +64,7 @@ function matchOnLeaf(item, currentPosition, query, nameAttr, onMatch, accumulato * @param {Object} options {query, items, idAttr } */ export function filter( - items = new List(), + items = [], query = '', { nameAttr = 'name', onMatch = noop } = {}, currentPosition = 'root', @@ -75,15 +74,15 @@ export function filter( if (isLeafElement(item)) { return matchOnLeaf(item, currentPosition, query, nameAttr, onMatch, accumulator); } - const currentElementName = item.get(nameAttr, ''); + const currentElementName = item[nameAttr] ?? ''; const result = filter( - item.get('children'), + item.children, query, { nameAttr }, `${currentPosition} > ${currentElementName}`, ); - return accumulator.concat(result); - }, new List()); + return [...accumulator, ...result]; + }, []); } return items; } @@ -94,32 +93,31 @@ export function filter( * @param {Object} options {query, items, idAttr } */ export function filterAll( - items = new List(), + items = [], query = '', { nameAttr = 'name', onMatch = noop } = {}, currentPosition = 'root', ) { - const result = new List(); - if (query) { return items.reduce((acc, item) => { - const name = item.get(nameAttr, ''); - const children = item.get('children', null); + const name = item[nameAttr] ?? ''; + const children = item.children ?? null; let results = acc; if (name.toLowerCase().includes(query.toLowerCase())) { onMatch(item); - results = acc.push(item.set('currentPosition', currentPosition)); + results = [...acc, { ...item, currentPosition }]; } if (children) { - results = results.concat( - filterAll(children, query, { nameAttr }, `${currentPosition} > ${name}`), - ); + results = [ + ...results, + ...filterAll(children, query, { nameAttr }, `${currentPosition} > ${name}`), + ]; } return results; - }, result); + }, []); } - return result; + return []; } class SelectObject extends RComponent { @@ -142,7 +140,7 @@ class SelectObject extends RComponent { }; static defaultProps = { - sourceData: new Immutable.List(), + sourceData: [], idAttr: 'id', nameAttr: 'name', breadCrumbsRootLabel: 'root', @@ -163,7 +161,7 @@ class SelectObject extends RComponent { } onResultsClick(event, item) { - this.props.setState({ selectedId: item.get(this.props.idAttr) }); + this.props.setState({ selectedId: item[this.props.idAttr] }); } render() { @@ -172,7 +170,7 @@ class SelectObject extends RComponent { const filterMethod = this.props.filterMode === SelectObject.FILTER_MODE.ALL ? this.filterAll : this.filter; const matches = []; - let selectedId = state.get('selectedId') || props.selectedId; + let selectedId = state.selectedId || props.selectedId; function addMatch(item) { matches.push(item); } @@ -189,7 +187,7 @@ class SelectObject extends RComponent { ); delete props.tree; if (!selectedId && matches.length === 1) { - selectedId = matches[0].get('id'); + selectedId = matches[0].id; } props.results = { onClick: this.onResultsClick, diff --git a/packages/containers/src/SelectObject/SelectObject.container.test.jsx b/packages/containers/src/SelectObject/SelectObject.container.test.jsx index 8b032b4c6d6..b0ea82e3b6b 100644 --- a/packages/containers/src/SelectObject/SelectObject.container.test.jsx +++ b/packages/containers/src/SelectObject/SelectObject.container.test.jsx @@ -2,7 +2,6 @@ /* eslint-disable react/display-name */ import { screen, render, fireEvent } from '@testing-library/react'; import { mock } from '@talend/react-cmf'; -import Immutable from 'immutable'; // eslint-disable-next-line @talend/import-depth import { prepareCMF } from '@talend/react-cmf/lib/mock/rtl'; @@ -29,8 +28,8 @@ describe('Container SelectObject', () => { it('should default props with Tree map the selectedId', async () => { const tree = {}; const getProps = jest.fn(); - const item = new Immutable.Map({ id: '1', name: 'foo' }); - const sourceData = new Immutable.List([item]); + const item = { id: '1', name: 'foo' }; + const sourceData = [item]; render( await prepareCMF( , @@ -43,9 +42,9 @@ describe('Container SelectObject', () => { idAttr: 'id', nameAttr: 'name', preview: undefined, - selected: item.toJS(), + selected: item, selectedId: '1', - sourceData: new Immutable.List([item]), + sourceData: [item], tree: { onSelect: expect.anything(), selectedId: '1', @@ -55,9 +54,9 @@ describe('Container SelectObject', () => { it('should set selectedId props to the only matched item if nothing selected', async () => { const getProps = jest.fn(); const tree = {}; - const item1 = new Immutable.Map({ id: '1', name: 'foo' }); - const item2 = new Immutable.Map({ id: '2', name: 'bar' }); - const sourceData = new Immutable.List([item1, item2]); + const item1 = { id: '1', name: 'foo' }; + const item2 = { id: '2', name: 'bar' }; + const sourceData = [item1, item2]; render( await prepareCMF( @@ -71,9 +70,9 @@ describe('Container SelectObject', () => { idAttr: 'id', nameAttr: 'name', query: 'f', - selected: item1.toJS(), + selected: item1, sourceData, - filteredData: expect.any(Immutable.List), + filteredData: expect.any(Array), results: { idAttr: 'id', nameAttr: 'name', @@ -81,7 +80,7 @@ describe('Container SelectObject', () => { selectedId: '1', }, }); - expect(props.filteredData.toJS()).toEqual([item1.set('currentPosition', 'root').toJS()]); + expect(props.filteredData).toEqual([{ ...item1, currentPosition: 'root' }]); }); it('should call props.setState when onTreeClick is called', () => { const props = { idAttr: 'id', setState: jest.fn() }; @@ -99,7 +98,7 @@ describe('Container SelectObject', () => { }); it('should call filter and getById', () => { const props = { - sourceData: new Immutable.List(), + sourceData: [], query: 'query', selectedId: 1, }; @@ -113,18 +112,18 @@ describe('Container SelectObject', () => { describe('getById', () => { it('should return nothing if not found and POO if found', () => { - const subfirst = new Immutable.Map({ id: 11 }); - const first = new Immutable.Map({ id: 1, children: new Immutable.List([subfirst]) }); - const second = new Immutable.Map({ id: 2 }); - const items = new Immutable.List([first, second]); + const subfirst = { id: 11 }; + const first = { id: 1, children: [subfirst] }; + const second = { id: 2 }; + const items = [first, second]; expect(getById(items, 11)).toEqual({ id: 11 }); expect(getById(items, 3)).toBe(); }); it('should return be able to support some options', () => { - const subfirst = new Immutable.Map({ myid: 11 }); - const first = new Immutable.Map({ myid: 1, children: new Immutable.List([subfirst]) }); - const second = new Immutable.Map({ myid: 2 }); - const items = new Immutable.List([first, second]); + const subfirst = { myid: 11 }; + const first = { myid: 1, children: [subfirst] }; + const second = { myid: 2 }; + const items = [first, second]; expect(getById(items, 11, { idAttr: 'myid' })).toEqual({ myid: 11 }); expect(getById(items, 3)).toBe(); }); @@ -132,147 +131,147 @@ describe('Container SelectObject', () => { describe('filter', () => { it('does not match on non leaf element (non leaf element have children)', () => { // given - const subfirst = new Immutable.Map({ id: 11, name: 'sub' }); - const first = new Immutable.Map({ + const subfirst = { id: 11, name: 'sub' }; + const first = { id: 1, name: 'abc', - children: new Immutable.List([subfirst]), - }); - const second = new Immutable.Map({ id: 2, name: 'foo' }); - const items = new Immutable.List([first, second]); + children: [subfirst], + }; + const second = { id: 2, name: 'foo' }; + const items = [first, second]; // when const results = filter(items, 'ab'); // then - expect(results.size).toBe(0); + expect(results.length).toBe(0); }); it('does match only on leaf element', () => { // given - const subfirst = new Immutable.Map({ id: 11, name: 'sub' }); - const first = new Immutable.Map({ + const subfirst = { id: 11, name: 'sub' }; + const first = { id: 1, name: 'abc', - children: new Immutable.List([subfirst]), - }); - const second = new Immutable.Map({ id: 2, name: 'foo' }); - const items = new Immutable.List([first, second]); + children: [subfirst], + }; + const second = { id: 2, name: 'foo' }; + const items = [first, second]; // when const results = filter(items, 'sub'); // then - expect(results.size).toBe(1); - expect(results.get(0).get('name')).toBe('sub'); - expect(results.get(0).get('toggled')).toBeFalsy(); - expect(results.get(0).get('currentPosition')).toBe('root > abc'); - expect(results.get(0).get('children')).toBeFalsy(); + expect(results.length).toBe(1); + expect(results[0].name).toBe('sub'); + expect(results[0].toggled).toBeFalsy(); + expect(results[0].currentPosition).toBe('root > abc'); + expect(results[0].children).toBeFalsy(); }); it('does match on multiple leaf elements of different depth, result is list', () => { // given - const subfirst = new Immutable.Map({ id: 11, name: 'sub' }); - const first = new Immutable.Map({ + const subfirst = { id: 11, name: 'sub' }; + const first = { id: 1, name: 'abc', - children: new Immutable.List([subfirst]), - }); - const second = new Immutable.Map({ id: 2, name: 'sub' }); - const items = new Immutable.List([first, second]); + children: [subfirst], + }; + const second = { id: 2, name: 'sub' }; + const items = [first, second]; // when const results = filter(items, 'sub'); // then - expect(results.size).toBe(2); - expect(results.get(0).get('name')).toBe('sub'); - expect(results.get(0).get('currentPosition')).toBe('root > abc'); - expect(results.get(1).get('name')).toBe('sub'); - expect(results.get(1).get('currentPosition')).toBe('root'); - expect(results.get(0).get('toggled')).toBeFalsy(); - expect(results.get(0).get('children')).toBeFalsy(); - expect(results.get(1).get('children')).toBeFalsy(); + expect(results.length).toBe(2); + expect(results[0].name).toBe('sub'); + expect(results[0].currentPosition).toBe('root > abc'); + expect(results[1].name).toBe('sub'); + expect(results[1].currentPosition).toBe('root'); + expect(results[0].toggled).toBeFalsy(); + expect(results[0].children).toBeFalsy(); + expect(results[1].children).toBeFalsy(); }); it('does match on multiple leaf children of a node', () => { // given - const subfirst = new Immutable.Map({ id: 11, name: 'sub1' }); - const subsecond = new Immutable.Map({ + const subfirst = { id: 11, name: 'sub1' }; + const subsecond = { id: 12, name: 'sub2', - children: new Immutable.List([Immutable.Map()]), - }); - const subthird = new Immutable.Map({ id: 13, name: 'sub3' }); - const first = new Immutable.Map({ + children: [{}], + }; + const subthird = { id: 13, name: 'sub3' }; + const first = { id: 1, name: 'abc', - children: new Immutable.List([subfirst, subsecond, subthird]), - }); - const second = new Immutable.Map({ id: 2, name: 'sub' }); - const items = new Immutable.List([first, second]); + children: [subfirst, subsecond, subthird], + }; + const second = { id: 2, name: 'sub' }; + const items = [first, second]; // when const results = filter(items, 'sub'); // then - expect(results.size).toBe(3); - expect(results.get(0).get('name')).toBe('sub1'); - expect(results.get(0).get('currentPosition')).toBe('root > abc'); - expect(results.get(1).get('name')).toBe('sub3'); - expect(results.get(1).get('currentPosition')).toBe('root > abc'); - expect(results.get(2).get('name')).toBe('sub'); - expect(results.get(2).get('currentPosition')).toBe('root'); - expect(results.get(0).get('toggled')).toBeFalsy(); - expect(results.get(0).get('children')).toBeFalsy(); - expect(results.get(1).get('children')).toBeFalsy(); - expect(results.get(2).get('children')).toBeFalsy(); + expect(results.length).toBe(3); + expect(results[0].name).toBe('sub1'); + expect(results[0].currentPosition).toBe('root > abc'); + expect(results[1].name).toBe('sub3'); + expect(results[1].currentPosition).toBe('root > abc'); + expect(results[2].name).toBe('sub'); + expect(results[2].currentPosition).toBe('root'); + expect(results[0].toggled).toBeFalsy(); + expect(results[0].children).toBeFalsy(); + expect(results[1].children).toBeFalsy(); + expect(results[2].children).toBeFalsy(); }); it('does match on multiple leaf children of different node', () => { // given - const subfirst = new Immutable.Map({ id: 11, name: 'sub1' }); - const subsecond = new Immutable.Map({ id: 13, name: 'sub2' }); - const first = new Immutable.Map({ + const subfirst = { id: 11, name: 'sub1' }; + const subsecond = { id: 13, name: 'sub2' }; + const first = { id: 1, name: 'abc', - children: new Immutable.List([subfirst]), - }); - const second = new Immutable.Map({ + children: [subfirst], + }; + const second = { id: 2, name: 'sub', - children: new Immutable.List([subsecond]), - }); - const items = new Immutable.List([first, second]); + children: [subsecond], + }; + const items = [first, second]; // when const results = filter(items, 'sub'); // then - expect(results.size).toBe(2); - expect(results.get(0).get('name')).toBe('sub1'); - expect(results.get(0).get('currentPosition')).toBe('root > abc'); - expect(results.get(1).get('name')).toBe('sub2'); - expect(results.get(1).get('currentPosition')).toBe('root > sub'); - expect(results.get(0).get('toggled')).toBeFalsy(); - expect(results.get(0).get('children')).toBeFalsy(); - expect(results.get(1).get('children')).toBeFalsy(); + expect(results.length).toBe(2); + expect(results[0].name).toBe('sub1'); + expect(results[0].currentPosition).toBe('root > abc'); + expect(results[1].name).toBe('sub2'); + expect(results[1].currentPosition).toBe('root > sub'); + expect(results[0].toggled).toBeFalsy(); + expect(results[0].children).toBeFalsy(); + expect(results[1].children).toBeFalsy(); }); it('return the original struct if no query or empty query is provided', () => { // given - const subfirst = new Immutable.Map({ id: 11, name: 'sub1' }); - const subsecond = new Immutable.Map({ + const subfirst = { id: 11, name: 'sub1' }; + const subsecond = { id: 12, name: 'sub2', - children: new Immutable.List([Immutable.Map()]), - }); - const subthird = new Immutable.Map({ id: 13, name: 'sub3' }); - const first = new Immutable.Map({ + children: [{}], + }; + const subthird = { id: 13, name: 'sub3' }; + const first = { id: 1, name: 'abc', - children: new Immutable.List([subfirst, subsecond, subthird]), - }); - const second = new Immutable.Map({ id: 2, name: 'sub' }); - const items = new Immutable.List([first, second]); + children: [subfirst, subsecond, subthird], + }; + const second = { id: 2, name: 'sub' }; + const items = [first, second]; // when const results = filter(items, ''); @@ -309,43 +308,43 @@ describe('Container SelectObject', () => { }, ]; - const items = Immutable.fromJS(tree); + const items = tree; const results = filterAll(items, 'ab'); - expect(results.size).toBe(3); - expect(results.get(0).get('name')).toBe('abc'); - expect(results.get(0).get('currentPosition')).toBe('root'); + expect(results.length).toBe(3); + expect(results[0].name).toBe('abc'); + expect(results[0].currentPosition).toBe('root'); - expect(results.get(1).get('name')).toBe('sub abc'); - expect(results.get(1).get('currentPosition')).toBe('root > abc'); + expect(results[1].name).toBe('sub abc'); + expect(results[1].currentPosition).toBe('root > abc'); - expect(results.get(2).get('name')).toBe('sub sub abc'); - expect(results.get(2).get('currentPosition')).toBe('root > abc > sub abc'); + expect(results[2].name).toBe('sub sub abc'); + expect(results[2].currentPosition).toBe('root > abc > sub abc'); }); it('does match on multiple leaf elements of different depth, result is list', () => { // given - const subfirst = new Immutable.Map({ id: 11, name: 'sub' }); - const first = new Immutable.Map({ + const subfirst = { id: 11, name: 'sub' }; + const first = { id: 1, name: 'abc', - children: new Immutable.List([subfirst]), - }); - const second = new Immutable.Map({ id: 2, name: 'sub' }); - const items = new Immutable.List([first, second]); + children: [subfirst], + }; + const second = { id: 2, name: 'sub' }; + const items = [first, second]; // when const results = filter(items, 'sub'); // then - expect(results.size).toBe(2); - expect(results.get(0).get('name')).toBe('sub'); - expect(results.get(0).get('currentPosition')).toBe('root > abc'); - expect(results.get(1).get('name')).toBe('sub'); - expect(results.get(1).get('currentPosition')).toBe('root'); - expect(results.get(0).get('toggled')).toBeFalsy(); - expect(results.get(0).get('children')).toBeFalsy(); - expect(results.get(1).get('children')).toBeFalsy(); + expect(results.length).toBe(2); + expect(results[0].name).toBe('sub'); + expect(results[0].currentPosition).toBe('root > abc'); + expect(results[1].name).toBe('sub'); + expect(results[1].currentPosition).toBe('root'); + expect(results[0].toggled).toBeFalsy(); + expect(results[0].children).toBeFalsy(); + expect(results[1].children).toBeFalsy(); }); }); }); diff --git a/packages/containers/src/ShortcutManager/ShortcutManager.test.js b/packages/containers/src/ShortcutManager/ShortcutManager.test.js index 2a9758541b3..e4284c14593 100644 --- a/packages/containers/src/ShortcutManager/ShortcutManager.test.js +++ b/packages/containers/src/ShortcutManager/ShortcutManager.test.js @@ -1,6 +1,5 @@ import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Map } from 'immutable'; import { mock } from '@talend/react-cmf'; @@ -21,7 +20,7 @@ describe('handles routes', () => { esc: { '/test': '/test/next' }, }, }; - state.cmf.components = new Map(); + state.cmf.components = {}; state.routing = { locationBeforeTransitions: { pathname: '/test', diff --git a/packages/containers/src/SidePanel/SidePanel.container.jsx b/packages/containers/src/SidePanel/SidePanel.container.jsx index be28c2988df..25725d119a9 100644 --- a/packages/containers/src/SidePanel/SidePanel.container.jsx +++ b/packages/containers/src/SidePanel/SidePanel.container.jsx @@ -1,6 +1,5 @@ import { Component as RComponent } from 'react'; -import { Map } from 'immutable'; import omit from 'lodash/omit'; import { cmfConnect } from '@talend/react-cmf'; @@ -8,9 +7,9 @@ import Component from '@talend/react-components/lib/SidePanel'; import { ACTION_TYPE_LINK } from './constants'; -export const DEFAULT_STATE = new Map({ +export const DEFAULT_STATE = { docked: false, -}); +}; /** * Checkout the {@link http://talend.github.io/ui/main/containers/?selectedKind=SidePanelExample&selectedStory=Default|examples} @@ -30,13 +29,13 @@ class SidePanel extends RComponent { onToggleDock() { const state = this.props.state || DEFAULT_STATE; - this.props.setState({ docked: !state.get('docked') }); + this.props.setState({ docked: !state?.docked }); } render() { const { state = DEFAULT_STATE } = this.props; const props = { - docked: state.get('docked'), + docked: state?.docked, onToggleDock: this.onToggleDock, ...omit(this.props, cmfConnect.INJECTED_PROPS), }; diff --git a/packages/containers/src/Slider/Slider.container.jsx b/packages/containers/src/Slider/Slider.container.jsx index e2b6c7db968..baac2a62df6 100644 --- a/packages/containers/src/Slider/Slider.container.jsx +++ b/packages/containers/src/Slider/Slider.container.jsx @@ -2,13 +2,12 @@ import { Component as RComponent } from 'react'; import { cmfConnect } from '@talend/react-cmf'; import Component from '@talend/react-components/lib/Slider'; import omit from 'lodash/omit'; -import Immutable from 'immutable'; import PropTypes from 'prop-types'; export const VALUE_ATTR = 'value'; -export const DEFAULT_STATE = new Immutable.Map({ +export const DEFAULT_STATE = { [VALUE_ATTR]: undefined, -}); +}; export const DISPLAY_NAME = 'Container(Slider)'; @@ -38,7 +37,7 @@ class Slider extends RComponent { } onChange(value) { - this.props.setState(prevState => prevState.state.set(VALUE_ATTR, value)); + this.props.setState({ [VALUE_ATTR]: value }); if (this.props.onChange) { this.props.onChange(value); } @@ -48,7 +47,7 @@ class Slider extends RComponent { const state = this.props.state || DEFAULT_STATE; const props = { ...omit(this.props, cmfConnect.INJECTED_PROPS), - value: state.get(VALUE_ATTR, this.props.value), + value: state?.[VALUE_ATTR] ?? this.props.value, onChange: this.onChange, onAfterChange: this.onAfterChange, }; diff --git a/packages/containers/src/Slider/Slider.selectors.js b/packages/containers/src/Slider/Slider.selectors.js index 1fb22e15eb6..5866ca769c8 100644 --- a/packages/containers/src/Slider/Slider.selectors.js +++ b/packages/containers/src/Slider/Slider.selectors.js @@ -6,7 +6,7 @@ import { DEFAULT_STATE, DISPLAY_NAME, VALUE_ATTR } from './Slider.container'; * @param {string} idComponent */ export function getComponentState(state, idComponent) { - return state.cmf.components.getIn([DISPLAY_NAME, idComponent], DEFAULT_STATE); + return state.cmf.components?.[DISPLAY_NAME]?.[idComponent] ?? DEFAULT_STATE; } /** @@ -15,5 +15,5 @@ export function getComponentState(state, idComponent) { * @param {string} idComponent */ export function getValue(state, idComponent) { - return getComponentState(state, idComponent).get(VALUE_ATTR, ''); + return getComponentState(state, idComponent)?.[VALUE_ATTR] ?? ''; } diff --git a/packages/containers/src/Slider/Slider.stories.jsx b/packages/containers/src/Slider/Slider.stories.jsx index c6d47b338bb..2699c1dd807 100644 --- a/packages/containers/src/Slider/Slider.stories.jsx +++ b/packages/containers/src/Slider/Slider.stories.jsx @@ -1,5 +1,3 @@ -import { Map } from 'immutable'; - import Slider from '.'; const icons = [ @@ -65,10 +63,10 @@ const actions = [ const functionToFormat = value => `${value}`; -const nullState = new Map(); -const initialState = new Map({ +const nullState = {}; +const initialState = { value: 50, -}); +}; export default { title: 'Slider', diff --git a/packages/containers/src/Slider/Slider.test.js b/packages/containers/src/Slider/Slider.test.js index f0cd2ce791d..ca368865518 100644 --- a/packages/containers/src/Slider/Slider.test.js +++ b/packages/containers/src/Slider/Slider.test.js @@ -1,7 +1,7 @@ /* eslint-disable testing-library/no-container */ -import { Map } from 'immutable'; import { render } from '@testing-library/react'; import Container, { DISPLAY_NAME } from './Slider.container'; + import Connected from './Slider.connect'; import { getComponentState, getValue } from './Slider.selectors'; @@ -17,9 +17,7 @@ describe('Filter container', () => { const props = { id: 'filter', }; - const initialState = new Map({ - value: 15, - }); + const initialState = { value: 15 }; const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); @@ -58,24 +56,20 @@ describe('Filter container', () => { describe('Slider Selectors', () => { it('should return the slider component state', () => { - const componentState = Map({ - value: '12', - }); + const componentState = { value: '12' }; const state = { cmf: { - components: Map({ [DISPLAY_NAME]: Map({ mySliderComponent: componentState }) }), + components: { [DISPLAY_NAME]: { mySliderComponent: componentState } }, }, }; expect(getComponentState(state, 'mySliderComponent')).toEqual(componentState); }); it('should return the value', () => { - const componentState = Map({ - value: 12, - }); + const componentState = { value: 12 }; const state = { cmf: { - components: Map({ [DISPLAY_NAME]: Map({ mySliderComponent: componentState }) }), + components: { [DISPLAY_NAME]: { mySliderComponent: componentState } }, }, }; expect(getValue(state, 'mySliderComponent')).toEqual(12); diff --git a/packages/containers/src/Slider/__snapshots__/Slider.test.js.snap b/packages/containers/src/Slider/__snapshots__/Slider.test.js.snap index d39db64108a..3129624dfec 100644 --- a/packages/containers/src/Slider/__snapshots__/Slider.test.js.snap +++ b/packages/containers/src/Slider/__snapshots__/Slider.test.js.snap @@ -19,16 +19,26 @@ exports[`Filter container > should render 1`] = ` class="rc-slider-step" />
+ class="_tc-slider__handler_5330c6" + > +
+ 0 +
+
+
diff --git a/packages/containers/src/SubHeaderBar/SubHeaderBar.container.jsx b/packages/containers/src/SubHeaderBar/SubHeaderBar.container.jsx index 5b577857231..613eb5f908b 100644 --- a/packages/containers/src/SubHeaderBar/SubHeaderBar.container.jsx +++ b/packages/containers/src/SubHeaderBar/SubHeaderBar.container.jsx @@ -1,12 +1,11 @@ import { Component as RComponent } from 'react'; import PropTypes from 'prop-types'; import Component from '@talend/react-components/lib/SubHeaderBar'; -import Immutable from 'immutable'; import omit from 'lodash/omit'; import { cmfConnect } from '@talend/react-cmf'; export const DISPLAY_NAME = 'Container(SubHeaderBar)'; -export const DEFAULT_STATE = new Immutable.Map({}); +export const DEFAULT_STATE = {}; class SubHeaderBar extends RComponent { static displayName = DISPLAY_NAME; @@ -65,7 +64,7 @@ class SubHeaderBar extends RComponent { const props = { ...omit(this.props, cmfConnect.INJECTED_PROPS), ...eventHandlerProps, - ...state.toJS(), + ...state, }; return ; diff --git a/packages/containers/src/SubHeaderBar/SubHeaderBar.selectors.js b/packages/containers/src/SubHeaderBar/SubHeaderBar.selectors.js index b81aab77726..ad14393b39b 100644 --- a/packages/containers/src/SubHeaderBar/SubHeaderBar.selectors.js +++ b/packages/containers/src/SubHeaderBar/SubHeaderBar.selectors.js @@ -7,5 +7,5 @@ import { DEFAULT_STATE, DISPLAY_NAME } from './SubHeaderBar.container'; */ // eslint-disable-next-line import/prefer-default-export export function getComponentState(state, idComponent) { - return state.cmf.components.getIn([DISPLAY_NAME, idComponent], DEFAULT_STATE); + return state.cmf.components?.[DISPLAY_NAME]?.[idComponent] ?? DEFAULT_STATE; } diff --git a/packages/containers/src/SubHeaderBar/SubHeaderBar.test.js b/packages/containers/src/SubHeaderBar/SubHeaderBar.test.js index bf9d111fcc0..2dfe61ee62c 100644 --- a/packages/containers/src/SubHeaderBar/SubHeaderBar.test.js +++ b/packages/containers/src/SubHeaderBar/SubHeaderBar.test.js @@ -1,6 +1,6 @@ import { screen, render, fireEvent } from '@testing-library/react'; -import { Map } from 'immutable'; import Container, { DEFAULT_STATE, DISPLAY_NAME } from './SubHeaderBar.container'; + import Connect from './SubHeaderBar.connect'; import { getComponentState } from './SubHeaderBar.selectors'; @@ -50,11 +50,11 @@ describe('SubHeaderBar container', () => { describe('SubHeaderBar selectors', () => { let mockState; - const componentState = Map({}); + const componentState = {}; beforeEach(() => { mockState = { cmf: { - components: Map({ [DISPLAY_NAME]: Map({ mySubHeaderBar: componentState }) }), + components: { [DISPLAY_NAME]: { mySubHeaderBar: componentState } }, }, }; }); diff --git a/packages/containers/src/TabBar/TabBar.connect.js b/packages/containers/src/TabBar/TabBar.connect.js index 35cfb527c7a..35e93f8ef7c 100644 --- a/packages/containers/src/TabBar/TabBar.connect.js +++ b/packages/containers/src/TabBar/TabBar.connect.js @@ -1,8 +1,7 @@ import { cmfConnect } from '@talend/react-cmf'; import TabBar from '@talend/react-components/lib/TabBar'; -import Immutable from 'immutable'; -export const DEFAULT_STATE = new Immutable.Map({}); +export const DEFAULT_STATE = {}; export default cmfConnect({ componentId: ownProps => ownProps.componentId || ownProps.id, diff --git a/packages/containers/src/TabBar/TabBar.selectors.js b/packages/containers/src/TabBar/TabBar.selectors.js index 72994c9ce03..a8951ef3819 100644 --- a/packages/containers/src/TabBar/TabBar.selectors.js +++ b/packages/containers/src/TabBar/TabBar.selectors.js @@ -1,3 +1,4 @@ +import cmf from '@talend/react-cmf'; import TabBar from '@talend/react-components/lib/TabBar'; import { DEFAULT_STATE } from './TabBar.connect'; @@ -7,7 +8,10 @@ import { DEFAULT_STATE } from './TabBar.connect'; * @param {string} idComponent */ export function getComponentState(state, idComponent) { - return state.cmf.components.getIn([TabBar.displayName, idComponent], DEFAULT_STATE); + return ( + cmf.selectors.components.getComponentState(state, TabBar.displayName, idComponent) ?? + DEFAULT_STATE + ); } /** @@ -16,5 +20,10 @@ export function getComponentState(state, idComponent) { * @param {string} idComponent */ export function getSelectedKey(state, idComponent) { - return getComponentState(state, idComponent).get('selectedKey', undefined); + const compState = getComponentState(state, idComponent); + if (compState == null) return undefined; + if (typeof compState.get === 'function') { + return compState.get('selectedKey', undefined); + } + return compState.selectedKey; } diff --git a/packages/containers/src/TabBar/TabBar.test.js b/packages/containers/src/TabBar/TabBar.test.js index 1f76111fad6..922cd7d98e7 100644 --- a/packages/containers/src/TabBar/TabBar.test.js +++ b/packages/containers/src/TabBar/TabBar.test.js @@ -1,5 +1,5 @@ -import { Map } from 'immutable'; import Component from '@talend/react-components/lib/TabBar'; + import Connected, { DEFAULT_STATE } from './TabBar.connect'; import { getComponentState, getSelectedKey } from './TabBar.selectors'; @@ -12,10 +12,12 @@ describe('TabBar connected', () => { describe('TabBar selectors', () => { let mockState; - const componentState = Map({ selectedKey: 'hello' }); + const componentState = { selectedKey: 'hello' }; beforeEach(() => { mockState = { - cmf: { components: Map({ [Component.displayName]: Map({ thisTabBar: componentState }) }) }, + cmf: { + components: { [Component.displayName]: { thisTabBar: componentState } }, + }, }; }); diff --git a/packages/containers/src/TreeView/TreeView.container.jsx b/packages/containers/src/TreeView/TreeView.container.jsx index 50576e8990e..c3e492c93fd 100644 --- a/packages/containers/src/TreeView/TreeView.container.jsx +++ b/packages/containers/src/TreeView/TreeView.container.jsx @@ -2,9 +2,8 @@ import { Component as RComponent } from 'react'; import PropTypes from 'prop-types'; import { cmfConnect } from '@talend/react-cmf'; import Component from '@talend/react-components/lib/TreeView'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Immutable from 'immutable'; import omit from 'lodash/omit'; +import _get from 'lodash/get'; const OPENED_ATTR = 'opened'; const SELECTED_ATTR = 'selectedId'; @@ -14,10 +13,10 @@ export const DEFAULT_PROPS = { nameAttr: 'name', childrenAttr: 'children', }; -export const DEFAULT_STATE = new Immutable.Map({ - [OPENED_ATTR]: new Immutable.List(), +export const DEFAULT_STATE = { + [OPENED_ATTR]: [], [SELECTED_ATTR]: undefined, -}); +}; function itemHasChildId(data, idAttr, idToMatch) { if (!data.children || !data.children.length) { @@ -30,34 +29,31 @@ function itemHasChildId(data, idAttr, idToMatch) { function toggleState(prevProps, data, idAttr) { const id = data[idAttr]; - const opened = prevProps.state.get(OPENED_ATTR); + const opened = prevProps.state?.[OPENED_ATTR] ?? []; const index = opened.indexOf(id); if (index !== -1) { - let nextState = prevProps.state.set(OPENED_ATTR, opened.delete(index)); - const selectedId = nextState.get(SELECTED_ATTR); + const newOpened = opened.filter((_, i) => i !== index); + const selectedId = prevProps.state?.[SELECTED_ATTR]; if (selectedId !== undefined && itemHasChildId(data, idAttr, selectedId)) { - nextState = nextState.set(SELECTED_ATTR, undefined); + return { [OPENED_ATTR]: newOpened, [SELECTED_ATTR]: undefined }; } - - return nextState; + return { [OPENED_ATTR]: newOpened }; } - return prevProps.state.set(OPENED_ATTR, prevProps.state.get(OPENED_ATTR).push(id)); + return { [OPENED_ATTR]: [...opened, id] }; } function openAllState(prevProps, data, idAttr) { - const nextOpened = data - .reduce((accu, item) => accu.add(item[idAttr]), prevProps.state.get(OPENED_ATTR).toSet()) - .toList(); - - return prevProps.state.set(OPENED_ATTR, nextOpened); + const openedIds = prevProps.state?.[OPENED_ATTR] ?? []; + const newIds = data.map(item => item[idAttr]).filter(id => !openedIds.includes(id)); + return { [OPENED_ATTR]: [...openedIds, ...newIds] }; } function selectWrapper(prevProps, id) { - const selected = prevProps.state.get(SELECTED_ATTR); + const selected = prevProps.state?.[SELECTED_ATTR]; if (id === selected) { - return prevProps.state.set(SELECTED_ATTR, undefined); + return { [SELECTED_ATTR]: undefined }; } - return prevProps.state.set(SELECTED_ATTR, id); + return { [SELECTED_ATTR]: id }; } /** @@ -71,8 +67,8 @@ export function transform(items, props, parent) { return undefined; } const state = props.state || DEFAULT_STATE; - const selectedId = state.get(SELECTED_ATTR); - const opened = state.get(OPENED_ATTR); + const selectedId = state[SELECTED_ATTR]; + const opened = state[OPENED_ATTR] ?? []; return items.map(item => { const elem = { @@ -103,7 +99,7 @@ class TreeView extends RComponent { static propTypes = { childrenAttr: PropTypes.string, - data: ImmutablePropTypes.list, + data: PropTypes.array, idAttr: PropTypes.string, nameAttr: PropTypes.string, onClick: PropTypes.func, @@ -182,14 +178,14 @@ class TreeView extends RComponent { } const state = this.props.state || DEFAULT_STATE; - return state.get(SELECTED_ATTR); + return state?.[SELECTED_ATTR]; } render() { if (!this.props.data) { return null; } - const structure = transform(this.props.data.toJS(), this.props); + const structure = transform(this.props.data, this.props); const props = omit(this.props, cmfConnect.INJECTED_PROPS); return ( { beforeEach(() => { context = mock.store.context(); state = mock.store.state(); - data = new Immutable.List([ - new Immutable.Map({ id: 1, name: 'foo', children: [{ id: 11, name: 'fofo', childre: [] }] }), - new Immutable.Map({ id: 2, name: 'bar', children: [] }), - ]); + data = [ + { id: 1, name: 'foo', children: [{ id: 11, name: 'fofo', childre: [] }] }, + { id: 2, name: 'bar', children: [] }, + ]; context.store.getState = () => state; }); @@ -55,12 +54,12 @@ describe('TreeView', () => { // then expect(setState).toHaveBeenCalled(); expect(prevState.state).not.toBe(DEFAULT_STATE); - expect(prevState.state.get('selectedId')).toEqual(1); - expect(onSelect).toHaveBeenCalledWith(expect.anything(data.get(0).toJS())); + expect(prevState.state.selectedId).toEqual(1); + expect(onSelect).toHaveBeenCalledWith(expect.anything()); expect(dispatchActionCreator).toHaveBeenCalled(); expect(dispatchActionCreator.mock.calls[0][0]).toBe(onSelectActionCreator); expect(dispatchActionCreator.mock.calls[0][1].props).toMatchObject(props); - expect(dispatchActionCreator.mock.calls[0][2]).toEqual(expect.anything(data.get(0).toJS())); + expect(dispatchActionCreator.mock.calls[0][2]).toEqual(expect.anything()); }); it('should open/close on toggle', () => { @@ -84,14 +83,14 @@ describe('TreeView', () => { // then expect(setState).toHaveBeenCalled(); expect(prevState.state).not.toBe(DEFAULT_STATE); - expect(prevState.state.get('opened').toJS()).toEqual([1]); + expect(prevState.state.opened).toEqual([1]); // when fireEvent.click(document.querySelector('button')); // then expect(setState.mock.calls.length).toBe(2); - expect(prevState.state.get('opened').toJS()).toEqual([]); + expect(prevState.state.opened).toEqual([]); }); it('should setState onSelect', () => { @@ -116,12 +115,12 @@ describe('TreeView', () => { fireEvent.click(screen.getByText('foo')); expect(setState).toHaveBeenCalled(); expect(prevState.state).not.toBe(DEFAULT_STATE); - expect(prevState.state.get('selectedId')).toEqual(1); - expect(onSelect).toHaveBeenCalledWith(expect.anything(data.get(0).toJS())); + expect(prevState.state.selectedId).toEqual(1); + expect(onSelect).toHaveBeenCalledWith(expect.anything()); expect(dispatchActionCreator).toHaveBeenCalled(); expect(dispatchActionCreator.mock.calls[0][0]).toBe(onSelectActionCreator); expect(dispatchActionCreator.mock.calls[0][1].props).toMatchObject(props); - expect(dispatchActionCreator.mock.calls[0][2]).toEqual(expect.anything(data.get(0).toJS())); + expect(dispatchActionCreator.mock.calls[0][2]).toEqual(expect.anything()); }); it('should unselect onSelect twice', () => { @@ -147,28 +146,25 @@ describe('TreeView', () => { // then expect(setState).toHaveBeenCalled(); expect(prevState.state).not.toBe(DEFAULT_STATE); - expect(prevState.state.get('selectedId')).toEqual(1); - expect(onSelect).toHaveBeenCalledWith(expect.anything(data.get(0).toJS())); + expect(prevState.state.selectedId).toEqual(1); + expect(onSelect).toHaveBeenCalledWith(expect.anything()); // when fireEvent.click(screen.getByText('foo')); // then - expect(prevState.state.get('selectedId')).toBe(); + expect(prevState.state.selectedId).toBe(); }); }); describe('mapStateToProps', () => { it('should return props', () => { const state = mock.store.state(); - const data = new Immutable.Map({ - foo: new Immutable.Map({ - bar: new Immutable.List([new Immutable.Map({ foo: 'bar' })]), - }), - }); - state.cmf.collections = state.cmf.collections.set('data', data); + const barList = [{ foo: 'bar' }]; + const data = { foo: { bar: barList } }; + state.cmf.collections = { ...state.cmf.collections, data }; const props = mapStateToProps(state, { collection: 'data.foo.bar' }); - expect(props.data).toBe(data.getIn(['foo', 'bar'])); + expect(props.data).toBe(barList); }); }); @@ -180,10 +176,10 @@ describe('transform', () => { it('should add toggled booleans', () => { const props = { ...DEFAULT_PROPS, - state: Immutable.Map({ - opened: Immutable.List([1, 11, 111]), + state: { + opened: [1, 11, 111], selectedId: 11, - }), + }, }; const items = [ { @@ -212,10 +208,10 @@ describe('transform', () => { it("should unfold selected's parents", () => { const props = { ...DEFAULT_PROPS, - state: Immutable.Map({ - opened: Immutable.List([1, 11, 111]), + state: { + opened: [1, 11, 111], selectedId: 111, - }), + }, }; const items = [ { diff --git a/packages/containers/src/Typeahead/Typeahead.container.jsx b/packages/containers/src/Typeahead/Typeahead.container.jsx index a4428c53407..052d8e7628a 100644 --- a/packages/containers/src/Typeahead/Typeahead.container.jsx +++ b/packages/containers/src/Typeahead/Typeahead.container.jsx @@ -1,6 +1,5 @@ import { Component as RComponent } from 'react'; -import Immutable from 'immutable'; import omit from 'lodash/omit'; import PropTypes from 'prop-types'; @@ -8,13 +7,13 @@ import { cmfConnect, componentState } from '@talend/react-cmf'; import Component from '@talend/react-components/lib/Typeahead'; export const DISPLAY_NAME = 'Container(Typeahead)'; -export const DEFAULT_STATE = new Immutable.Map({ +export const DEFAULT_STATE = { docked: true, searching: false, focusedSectionIndex: null, focusedItemIndex: null, items: null, -}); +}; /** * The Typeahead React container @@ -37,12 +36,12 @@ export default class Typeahead extends RComponent { } onToggle() { - this.props.setState(() => ({ - docked: !this.props.state.get('docked', true), + this.props.setState({ + docked: !(this.props.state?.docked ?? true), focusedSectionIndex: null, focusedItemIndex: null, items: null, - })); + }); } onBlur(event) { @@ -108,12 +107,12 @@ export default class Typeahead extends RComponent { const { items } = this.props; const props = { ...omit(this.props, cmfConnect.INJECTED_PROPS), - ...this.props.state.toJS(), + ...this.props.state, onToggle: this.onToggle, onBlur: this.onBlur, onSelect: this.onSelect, onKeyDown: this.onKeyDown, - items: items && items.toJS ? items.toJS() : items, + items, }; return ; diff --git a/packages/containers/src/Typeahead/Typeahead.test.js b/packages/containers/src/Typeahead/Typeahead.test.js index 1a73269b8e5..8c71c32088d 100644 --- a/packages/containers/src/Typeahead/Typeahead.test.js +++ b/packages/containers/src/Typeahead/Typeahead.test.js @@ -1,5 +1,4 @@ import { fireEvent, render } from '@testing-library/react'; -import { Map } from 'immutable'; import Connect from './Typeahead.connect'; import Container, { DEFAULT_STATE } from './Typeahead.container'; @@ -30,15 +29,16 @@ describe('Typeahead container', () => { let state; const props = { ...defaultProps, - state: Map({ docked: true }), - setState: jest.fn(fn => { - state = fn(); + state: { docked: true }, + setState: jest.fn(newState => { + state = newState; }), onToggle: jest.fn(), }; render(); - fireEvent.click(document.querySelector('button')); + const button = document.querySelector('button'); + if (button) fireEvent.click(button); expect(props.setState).toHaveBeenCalled(); expect(state.docked).toEqual(false); }); @@ -63,7 +63,7 @@ describe('Typeahead container', () => { const event = { key: KEYS.DOWN, preventDefault: () => {} }; const props = { ...defaultProps, - state: Map({ docked: true }), + state: { docked: true }, setState: jest.fn(), onKeyDown: jest.fn(), onBlur: jest.fn(), @@ -78,7 +78,7 @@ describe('Typeahead container', () => { const event = { key: 'Esc', preventDefault: () => {} }; const props = { ...defaultProps, - state: Map({ docked: true }), + state: { docked: true }, setState: jest.fn(), onKeyDown: jest.fn(), onBlur: jest.fn(), @@ -93,7 +93,7 @@ describe('Typeahead container', () => { const event = { key: 'Enter', preventDefault: () => {} }; const props = { ...defaultProps, - state: Map({ docked: true }), + state: { docked: true }, setState: jest.fn(), onKeyDown: jest.fn(), onSelect: jest.fn(), diff --git a/packages/containers/src/test-setup.js b/packages/containers/src/test-setup.js index d0e25dbdd65..cfb462d31f0 100644 --- a/packages/containers/src/test-setup.js +++ b/packages/containers/src/test-setup.js @@ -11,3 +11,13 @@ vi.mock('@talend/utils', async () => { // Keep existing tests functional while migrating from Jest to Vitest. globalThis.jest = vi; globalThis.xit = it.skip; + +// Node v25+ declares `localStorage` on globalThis, which prevents vitest/jsdom from +// overriding it with jsdom's Storage. Use the JSDOM instance directly to fix this. +const jsdomLocalStorage = globalThis.jsdom?.window?.localStorage; +if (jsdomLocalStorage) { + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: jsdomLocalStorage, + }); +} diff --git a/packages/faceted-search/jest.setup.js b/packages/faceted-search/jest.setup.js deleted file mode 100644 index dcf681f6b5b..00000000000 --- a/packages/faceted-search/jest.setup.js +++ /dev/null @@ -1,13 +0,0 @@ -import '@testing-library/jest-dom/vitest'; - -// Keep existing test code working without touching every `jest.fn()` call. -globalThis.jest = vi; - -vi.mock('@talend/utils', async () => { - let i = 0; - const actual = await vi.importActual('@talend/utils'); - return { - ...actual, - randomUUID: () => `mocked-uuid-${i++}`, - }; -}); diff --git a/packages/faceted-search/test-setup.js b/packages/faceted-search/test-setup.js new file mode 100644 index 00000000000..8eed115050e --- /dev/null +++ b/packages/faceted-search/test-setup.js @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom/vitest'; + +// Keep existing test code working without touching every `jest.fn()` call. +globalThis.jest = vi; + +// Node v25+ declares `localStorage` on globalThis, which prevents vitest/jsdom from +// overriding it with jsdom's Storage. Use the JSDOM instance directly to fix this. +const jsdomLocalStorage = globalThis.jsdom?.window?.localStorage; +if (jsdomLocalStorage) { + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: jsdomLocalStorage, + }); +} + +vi.mock('@talend/utils', async () => { + let i = 0; + const actual = await vi.importActual('@talend/utils'); + return { + ...actual, + randomUUID: () => `mocked-uuid-${i++}`, + }; +}); diff --git a/packages/faceted-search/vitest.config.ts b/packages/faceted-search/vitest.config.ts index 7a2b1a42335..a75f582e99d 100644 --- a/packages/faceted-search/vitest.config.ts +++ b/packages/faceted-search/vitest.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ env: { TZ: 'UTC', }, - setupFiles: ['jest.setup.js'], + setupFiles: ['test-setup.js'], include: ['src/**/*.test.{js,jsx,ts,tsx}'], exclude: ['lib/**', 'lib-esm/**'], coverage: { diff --git a/packages/flow-designer/src/api/data/data.ts b/packages/flow-designer/src/api/data/data.ts index 3b9ef0965eb..96baf80951c 100644 --- a/packages/flow-designer/src/api/data/data.ts +++ b/packages/flow-designer/src/api/data/data.ts @@ -30,9 +30,7 @@ export function isMapElseThrow(map: Map) { export function isKeyElseThrow(key: string | number) { const test = isString(key); if (!test) { - throwInDev( - `key should be a string, was given ${key && key.toString()} of type ${typeof key}`, - ); + throwInDev(`key should be a string, was given ${key && key.toString()} of type ${typeof key}`); } return test; } diff --git a/packages/flow-designer/src/api/link/link.test.ts b/packages/flow-designer/src/api/link/link.test.ts index 0a4932cd800..3e240f9bb35 100644 --- a/packages/flow-designer/src/api/link/link.test.ts +++ b/packages/flow-designer/src/api/link/link.test.ts @@ -509,9 +509,7 @@ describe('Link', () => { // given // when // expect - expect(() => Link.deleteGraphicalAttribute(key, improperLink)).toThrow( - isNotLinkException, - ); + expect(() => Link.deleteGraphicalAttribute(key, improperLink)).toThrow(isNotLinkException); }); }); }); diff --git a/packages/flow-designer/src/api/node/node.test.ts b/packages/flow-designer/src/api/node/node.test.ts index 7422038bea8..8bb3a18ea66 100644 --- a/packages/flow-designer/src/api/node/node.test.ts +++ b/packages/flow-designer/src/api/node/node.test.ts @@ -115,9 +115,7 @@ describe('Node', () => { // given // when // expect - expect(() => Node.create(id, position, improperSize, nodeType)).toThrow( - improperSizeMessage, - ); + expect(() => Node.create(id, position, improperSize, nodeType)).toThrow(improperSizeMessage); }); it('throw if given an improper componentType', () => { // given @@ -545,9 +543,7 @@ you should use Position module functions to create and transform Position`); // given // when // expect - expect(() => Node.deleteGraphicalAttribute(key, improperNode)).toThrow( - isNotNodeException, - ); + expect(() => Node.deleteGraphicalAttribute(key, improperNode)).toThrow(isNotNodeException); }); }); }); diff --git a/packages/flow-designer/src/api/node/node.ts b/packages/flow-designer/src/api/node/node.ts index 8917e4d0015..a09dd05c656 100644 --- a/packages/flow-designer/src/api/node/node.ts +++ b/packages/flow-designer/src/api/node/node.ts @@ -102,12 +102,14 @@ export function getPosition(node: NodeRecordType | NestedNodeRecordType) { * @param {NodeRecord} node * @returns {NodeRecord} */ -export const setPosition = curry((position: PositionRecord, node: NodeRecordType | NestedNodeRecordType) => { - if (isPositionElseThrow(position) && isNodeElseThrow(node)) { - return node.setIn(positionSelector, position); - } - return node; -}); +export const setPosition = curry( + (position: PositionRecord, node: NodeRecordType | NestedNodeRecordType) => { + if (isPositionElseThrow(position) && isNodeElseThrow(node)) { + return node.setIn(positionSelector, position); + } + return node; + }, +); /** * @param {NodeRecord} node @@ -150,13 +152,15 @@ export function getComponentType(node: NodeRecordType | NestedNodeRecordType) { * @param {NodeRecord} node * @returns {NodeRecord} */ -export const setComponentType = curry((nodeType: string, node: NodeRecordType | NestedNodeRecordType) => { - if (isString(nodeType) && isNodeElseThrow(node)) { - return node.setIn(componentTypeSelector, nodeType); - } - throwInDev(`nodeType should be a string, was given ${nodeType && nodeType.toString()}`); - return node; -}); +export const setComponentType = curry( + (nodeType: string, node: NodeRecordType | NestedNodeRecordType) => { + if (isString(nodeType) && isNodeElseThrow(node)) { + return node.setIn(componentTypeSelector, nodeType); + } + throwInDev(`nodeType should be a string, was given ${nodeType && nodeType.toString()}`); + return node; + }, +); /** * @function @@ -164,15 +168,17 @@ export const setComponentType = curry((nodeType: string, node: NodeRecordType | * @param {NodeRecord} node * @returns {NodeRecord} */ -export const setComponents = curry((components: Map, node: NestedNodeRecordType) => { - if (Map.isMap(components) && isNodeElseThrow(node)) { - return node.setIn(componentsSelector, components); - } - throwInDev( - `components should be a Immutable.List, was given ${components && components.toString()}`, - ); - return node; -}); +export const setComponents = curry( + (components: Map, node: NestedNodeRecordType) => { + if (Map.isMap(components) && isNodeElseThrow(node)) { + return node.setIn(componentsSelector, components); + } + throwInDev( + `components should be a Immutable.List, was given ${components && components.toString()}`, + ); + return node; + }, +); /** * @param {NodeRecord} node @@ -192,12 +198,14 @@ export function getComponents(node: NestedNodeRecordType) { * @param {NodeRecord} node * @returns {NodeRecord} */ -export const setData = curry((key: string, value: any, node: NodeRecordType | NestedNodeRecordType) => { - if (isNodeElseThrow(node)) { - return node.set('data', Data.set(key, value, node.get('data'))); - } - return node; -}); +export const setData = curry( + (key: string, value: any, node: NodeRecordType | NestedNodeRecordType) => { + if (isNodeElseThrow(node)) { + return node.set('data', Data.set(key, value, node.get('data'))); + } + return node; + }, +); /** * @function @@ -262,15 +270,14 @@ export function isWhiteListAttribute(key: string) { * @param {NodeRecord} node * @returns {NodeRecord} */ -export const setGraphicalAttribute = curry((key: string, value: any, node: NodeRecordType | NestedNodeRecordType) => { - if (isNodeElseThrow(node) && isWhiteListAttribute(key)) { - return node.set( - 'graphicalAttributes', - Data.set(key, value, node.get('graphicalAttributes')), - ); - } - return node; -}); +export const setGraphicalAttribute = curry( + (key: string, value: any, node: NodeRecordType | NestedNodeRecordType) => { + if (isNodeElseThrow(node) && isWhiteListAttribute(key)) { + return node.set('graphicalAttributes', Data.set(key, value, node.get('graphicalAttributes'))); + } + return node; + }, +); /** * @function @@ -278,12 +285,14 @@ export const setGraphicalAttribute = curry((key: string, value: any, node: NodeR * @param {NodeRecord} node * @returns {any | null} */ -export const getGraphicalAttribute = curry((key: string, node: NodeRecordType | NestedNodeRecordType) => { - if (isNodeElseThrow(node) && isWhiteListAttribute(key)) { - return Data.get(key, node.get('graphicalAttributes')); - } - return null; -}); +export const getGraphicalAttribute = curry( + (key: string, node: NodeRecordType | NestedNodeRecordType) => { + if (isNodeElseThrow(node) && isWhiteListAttribute(key)) { + return Data.get(key, node.get('graphicalAttributes')); + } + return null; + }, +); /** * @function @@ -291,12 +300,14 @@ export const getGraphicalAttribute = curry((key: string, node: NodeRecordType | * @param {NodeRecord} node * @returns {bool} */ -export const hasGraphicalAttribute = curry((key: string, node: NodeRecordType | NestedNodeRecordType) => { - if (isNodeElseThrow(node) && isWhiteListAttribute(key)) { - return Data.has(key, node.get('graphicalAttributes')); - } - return false; -}); +export const hasGraphicalAttribute = curry( + (key: string, node: NodeRecordType | NestedNodeRecordType) => { + if (isNodeElseThrow(node) && isWhiteListAttribute(key)) { + return Data.has(key, node.get('graphicalAttributes')); + } + return false; + }, +); /** * @function @@ -304,15 +315,14 @@ export const hasGraphicalAttribute = curry((key: string, node: NodeRecordType | * @param {NodeRecord} node * @returns {NodeRecord} */ -export const deleteGraphicalAttribute = curry((key: string, node: NodeRecordType | NestedNodeRecordType) => { - if (isNodeElseThrow(node) && isWhiteListAttribute(key)) { - return node.set( - 'graphicalAttributes', - Data.deleteKey(key, node.get('graphicalAttributes')), - ); - } - return node; -}); +export const deleteGraphicalAttribute = curry( + (key: string, node: NodeRecordType | NestedNodeRecordType) => { + if (isNodeElseThrow(node) && isWhiteListAttribute(key)) { + return node.set('graphicalAttributes', Data.deleteKey(key, node.get('graphicalAttributes'))); + } + return node; + }, +); /** * Create a new Node @@ -342,11 +352,8 @@ export const create = curry( ])(new NestedNodeRecord()); } - return flow([ - setId(id), - setPosition(position), - setSize(size), - setComponentType(componentType), - ])(new NodeRecord()); + return flow([setId(id), setPosition(position), setSize(size), setComponentType(componentType)])( + new NodeRecord(), + ); }, ); diff --git a/packages/flow-designer/src/api/port/port.test.ts b/packages/flow-designer/src/api/port/port.test.ts index b40b23d3727..834c760237c 100644 --- a/packages/flow-designer/src/api/port/port.test.ts +++ b/packages/flow-designer/src/api/port/port.test.ts @@ -78,41 +78,31 @@ describe('port api', () => { // given // when // expect - expect(() => - Port.create(improperId as any, nodeId, index, topology, portType), - ).toThrow(); + expect(() => Port.create(improperId as any, nodeId, index, topology, portType)).toThrow(); }); it('throw if given an improper NodeId', () => { // given // when // expect - expect(() => - Port.create(id, improperNodeId as any, index, topology, portType), - ).toThrow(); + expect(() => Port.create(id, improperNodeId as any, index, topology, portType)).toThrow(); }); it('throw if given an improper index', () => { // given // when // expect - expect(() => - Port.create(id, nodeId, improperIndex as any, topology, portType), - ).toThrow(); + expect(() => Port.create(id, nodeId, improperIndex as any, topology, portType)).toThrow(); }); it('throw if given an improper topology', () => { // given // when // expect - expect(() => - Port.create(id, nodeId, index, impropertopology as any, portType), - ).toThrow(); + expect(() => Port.create(id, nodeId, index, impropertopology as any, portType)).toThrow(); }); it('throw if given an improper componentType', () => { // given // when // expect - expect(() => - Port.create(id, nodeId, index, topology, improperPortType as any), - ).toThrow(); + expect(() => Port.create(id, nodeId, index, topology, improperPortType as any)).toThrow(); }); }); diff --git a/packages/flow-designer/src/api/size/size.test.ts b/packages/flow-designer/src/api/size/size.test.ts index 2f914e84cc3..d12d354cfd8 100644 --- a/packages/flow-designer/src/api/size/size.test.ts +++ b/packages/flow-designer/src/api/size/size.test.ts @@ -155,9 +155,7 @@ describe('Size', () => { // given // when // expect - expect(() => Size.setHeight(height, improperTestSize)).toThrow( - isNotProperSizeException, - ); + expect(() => Size.setHeight(height, improperTestSize)).toThrow(isNotProperSizeException); }); }); }); diff --git a/packages/flow-designer/src/api/size/size.ts b/packages/flow-designer/src/api/size/size.ts index 9437370d6ad..382ba4a647b 100644 --- a/packages/flow-designer/src/api/size/size.ts +++ b/packages/flow-designer/src/api/size/size.ts @@ -93,9 +93,7 @@ export const setHeight = curry((height: number, size: SizeRecordType) => { if (isSizeElseThrow(size) && typeof height === 'number') { return size.set('height', height); } - throwInDev( - `height should be a number, was given ${height.toString()} of type ${typeof height}`, - ); + throwInDev(`height should be a number, was given ${height.toString()} of type ${typeof height}`); return size; }); diff --git a/packages/flow-designer/src/reducers/link.reducer.test.ts b/packages/flow-designer/src/reducers/link.reducer.test.ts index fad7dba49d0..4602215588f 100644 --- a/packages/flow-designer/src/reducers/link.reducer.test.ts +++ b/packages/flow-designer/src/reducers/link.reducer.test.ts @@ -87,10 +87,7 @@ describe('check linkreducer', () => { }), ), ) - .set( - 'parents', - Map().set('id1', Map()).set('id2', Map().set('id1', 'id1')).set('id3', Map()), - ) + .set('parents', Map().set('id1', Map()).set('id2', Map().set('id1', 'id1')).set('id3', Map())) .set( 'childrens', Map().set('id1', Map().set('id2', 'id2')).set('id2', Map()).set('id3', Map()), diff --git a/packages/flow-designer/src/reducers/link.reducer.ts b/packages/flow-designer/src/reducers/link.reducer.ts index f206990e8d4..12f579727e4 100644 --- a/packages/flow-designer/src/reducers/link.reducer.ts +++ b/packages/flow-designer/src/reducers/link.reducer.ts @@ -48,13 +48,9 @@ export default function linkReducer(state = defaultState, action: any) { 'properties', fromJS(action.data && action.data.properties) || Map(), ), - graphicalAttributes: new LinkGraphicalAttributes( - action.graphicalAttributes, - ).set( + graphicalAttributes: new LinkGraphicalAttributes(action.graphicalAttributes).set( 'properties', - fromJS( - action.graphicalAttributes && action.graphicalAttributes.properties, - ) || Map(), + fromJS(action.graphicalAttributes && action.graphicalAttributes.properties) || Map(), ), }), ) // parcourir l'ensemble des parents et set le composant cible en tant que sucessors ' @@ -75,21 +71,11 @@ export default function linkReducer(state = defaultState, action: any) { state.getIn(['ports', action.sourceId]).nodeId, ) .setIn( - [ - 'out', - state.getIn(['ports', action.sourceId]).nodeId, - action.sourceId, - action.linkId, - ], + ['out', state.getIn(['ports', action.sourceId]).nodeId, action.sourceId, action.linkId], action.linkId, ) .setIn( - [ - 'in', - state.getIn(['ports', action.targetId]).nodeId, - action.targetId, - action.linkId, - ], + ['in', state.getIn(['ports', action.targetId]).nodeId, action.targetId, action.linkId], action.linkId, ); case FLOWDESIGNER_LINK_SET_TARGET: @@ -116,12 +102,7 @@ export default function linkReducer(state = defaultState, action: any) { action.linkId, ]) .setIn( - [ - 'in', - state.getIn(['ports', action.targetId]).nodeId, - action.targetId, - action.linkId, - ], + ['in', state.getIn(['ports', action.targetId]).nodeId, action.targetId, action.linkId], action.linkId, ) .deleteIn([ @@ -132,8 +113,7 @@ export default function linkReducer(state = defaultState, action: any) { .setIn( [ 'childrens', - state.getIn(['ports', state.getIn(['links', action.linkId]).sourceId]) - .nodeId, + state.getIn(['ports', state.getIn(['links', action.linkId]).sourceId]).nodeId, state.getIn(['ports', action.targetId]).nodeId, ], state.getIn(['ports', action.targetId]).nodeId, @@ -162,12 +142,7 @@ export default function linkReducer(state = defaultState, action: any) { action.linkId, ]) .setIn( - [ - 'out', - state.getIn(['ports', action.sourceId]).nodeId, - action.sourceId, - action.linkId, - ], + ['out', state.getIn(['ports', action.sourceId]).nodeId, action.sourceId, action.linkId], action.linkId, ) .deleteIn([ @@ -178,8 +153,7 @@ export default function linkReducer(state = defaultState, action: any) { .setIn( [ 'parents', - state.getIn(['ports', state.getIn(['links', action.linkId]).targetId]) - .nodeId, + state.getIn(['ports', state.getIn(['links', action.linkId]).targetId]).nodeId, state.getIn(['ports', action.sourceId]).nodeId, ], state.getIn(['ports', action.sourceId]).nodeId, @@ -253,10 +227,7 @@ export default function linkReducer(state = defaultState, action: any) { return state.mergeIn(['links', action.linkId, 'data'], fromJS(action.data)); } catch (error) { console.error(error); - return state.mergeIn( - ['links', action.linkId, 'data', 'properties'], - fromJS(action.data), - ); + return state.mergeIn(['links', action.linkId, 'data', 'properties'], fromJS(action.data)); } case FLOWDESIGNER_LINK_REMOVE_DATA: diff --git a/packages/flow-designer/src/reducers/node.reducer.ts b/packages/flow-designer/src/reducers/node.reducer.ts index 3bf8c708ec2..fed6b0c0115 100644 --- a/packages/flow-designer/src/reducers/node.reducer.ts +++ b/packages/flow-designer/src/reducers/node.reducer.ts @@ -33,10 +33,7 @@ const nodeReducer = (state: State = defaultState, action: any) => { switch (action.type) { case FLOWDESIGNER_NODE_ADD: if (state.getIn(['nodes', action.nodeId])) { - invariant( - false, - `Can not create node ${action.nodeId} since it does already exist`, - ); + invariant(false, `Can not create node ${action.nodeId} since it does already exist`); } return state @@ -49,18 +46,10 @@ const nodeReducer = (state: State = defaultState, action: any) => { 'properties', fromJS(action.data && action.data.properties) || Map(), ), - graphicalAttributes: new NodeGraphicalAttributes( - fromJS(action.graphicalAttributes), - ) + graphicalAttributes: new NodeGraphicalAttributes(fromJS(action.graphicalAttributes)) .set('nodeSize', new SizeRecord(action.graphicalAttributes.nodeSize)) - .set( - 'position', - new PositionRecord(action.graphicalAttributes.position), - ) - .set( - 'properties', - fromJS(action.graphicalAttributes.properties) || Map(), - ), + .set('position', new PositionRecord(action.graphicalAttributes.position)) + .set('properties', fromJS(action.graphicalAttributes.properties) || Map()), }), ) .setIn(['out', action.nodeId], Map()) @@ -107,13 +96,7 @@ const nodeReducer = (state: State = defaultState, action: any) => { ['nodes', action.nodeId, 'graphicalAttributes', 'position'], new PositionRecord(action.nodePosition), ) - .deleteIn([ - 'nodes', - action.nodeId, - 'graphicalAttributes', - 'properties', - 'startPosition', - ]); + .deleteIn(['nodes', action.nodeId, 'graphicalAttributes', 'properties', 'startPosition']); case FLOWDESIGNER_NODE_APPLY_MOVEMENT: return state.update('nodes', (nodes: NodeRecordMap) => nodes.map(node => { @@ -142,19 +125,13 @@ const nodeReducer = (state: State = defaultState, action: any) => { ); case FLOWDESIGNER_NODE_SET_TYPE: if (!state.getIn(['nodes', action.nodeId])) { - invariant( - false, - `Can't set node.type on node ${action.nodeid} since it doesn't exist`, - ); + invariant(false, `Can't set node.type on node ${action.nodeid} since it doesn't exist`); } return state.setIn(['nodes', action.nodeId, 'type'], action.nodeType); case FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES: if (!state.getIn(['nodes', action.nodeId])) { - invariant( - false, - `Can't set a graphical attribute on non existing node ${action.nodeId}`, - ); + invariant(false, `Can't set a graphical attribute on non existing node ${action.nodeId}`); } try { @@ -194,10 +171,7 @@ const nodeReducer = (state: State = defaultState, action: any) => { return state.mergeIn(['nodes', action.nodeId, 'data'], fromJS(action.data)); } catch (error) { console.error(error); - return state.mergeIn( - ['nodes', action.nodeId, 'data', 'properties'], - fromJS(action.data), - ); + return state.mergeIn(['nodes', action.nodeId, 'data', 'properties'], fromJS(action.data)); } case FLOWDESIGNER_NODE_REMOVE_DATA: diff --git a/packages/flow-designer/src/reducers/port.reducer.ts b/packages/flow-designer/src/reducers/port.reducer.ts index 63a7da00fb6..51a0361e2d4 100644 --- a/packages/flow-designer/src/reducers/port.reducer.ts +++ b/packages/flow-designer/src/reducers/port.reducer.ts @@ -78,17 +78,13 @@ function indexPortMap(ports: PortRecordMap): PortRecordMap { */ function setPort(state: State, port: PortRecordType) { const index: number = - port.graphicalAttributes.properties.index || - calculateNewPortIndex(state.get('ports'), port); + port.graphicalAttributes.properties.index || calculateNewPortIndex(state.get('ports'), port); const newState = state.setIn( ['ports', port.id], new PortRecord({ id: port.id, nodeId: port.nodeId, - data: Map(port.data).set( - 'properties', - fromJS(port.data && port.data.properties) || Map(), - ), + data: Map(port.data).set('properties', fromJS(port.data && port.data.properties) || Map()), graphicalAttributes: Map(port.graphicalAttributes) .set('position', new PositionRecord(port.graphicalAttributes.position)) .set( @@ -121,10 +117,7 @@ export default function portReducer(state: State, action: PortAction): State { switch (action.type) { case FLOWDESIGNER_PORT_ADD: if (!state.getIn(['nodes', action.nodeId])) { - invariant( - false, - `Can't set a new port ${action.id} on non existing node ${action.nodeId}`, - ); + invariant(false, `Can't set a new port ${action.id} on non existing node ${action.nodeId}`); } return setPort(state, { @@ -151,10 +144,7 @@ export default function portReducer(state: State, action: PortAction): State { } case FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES: if (!state.getIn(['ports', action.portId])) { - invariant( - false, - `Can't set an graphical attribute on non existing port ${action.portId}`, - ); + invariant(false, `Can't set an graphical attribute on non existing port ${action.portId}`); } try { @@ -194,10 +184,7 @@ export default function portReducer(state: State, action: PortAction): State { return state.mergeIn(['ports', action.portId, 'data'], fromJS(action.data)); } catch (error) { console.error(error); - return state.mergeIn( - ['ports', action.portId, 'data', 'properties'], - fromJS(action.data), - ); + return state.mergeIn(['ports', action.portId, 'data', 'properties'], fromJS(action.data)); } case FLOWDESIGNER_PORT_REMOVE_DATA: @@ -223,16 +210,8 @@ export default function portReducer(state: State, action: PortAction): State { ), ) .deleteIn(['ports', action.portId]) - .deleteIn([ - 'out', - state.getIn(['ports', action.portId, 'nodeId']), - action.portId, - ]) - .deleteIn([ - 'in', - state.getIn(['ports', action.portId, 'nodeId']), - action.portId, - ]); + .deleteIn(['out', state.getIn(['ports', action.portId, 'nodeId']), action.portId]) + .deleteIn(['in', state.getIn(['ports', action.portId, 'nodeId']), action.portId]); return newState.mergeDeep({ ports: indexPortMap( filterPortsByDirection( diff --git a/packages/flow-designer/src/selectors/portSelectors.ts b/packages/flow-designer/src/selectors/portSelectors.ts index 55b16526ad6..ab3f253b13d 100644 --- a/packages/flow-designer/src/selectors/portSelectors.ts +++ b/packages/flow-designer/src/selectors/portSelectors.ts @@ -70,8 +70,9 @@ export const getSinkPorts = createSelector( */ export const getEmitterPortsForNode = createSelector( getEmitterPorts as any, - (ports: PortRecordMap): PortRecord => (nodeId: string) => - ports.filter((port: any) => Port.getNodeId(port) === nodeId), + (ports: PortRecordMap): PortRecord => + (nodeId: string) => + ports.filter((port: any) => Port.getNodeId(port) === nodeId), ); /** @@ -79,8 +80,9 @@ export const getEmitterPortsForNode = createSelector( */ export const getSinkPortsForNode = createSelector( getSinkPorts as any, - (ports: PortRecordMap): PortRecord => (nodeId: string) => - ports.filter((port: any) => Port.getNodeId(port) === nodeId), + (ports: PortRecordMap): PortRecord => + (nodeId: string) => + ports.filter((port: any) => Port.getNodeId(port) === nodeId), ); /** diff --git a/packages/playground-vite/.gitignore b/packages/playground-vite/.gitignore index d44b5d89dee..3f57154fda9 100644 --- a/packages/playground-vite/.gitignore +++ b/packages/playground-vite/.gitignore @@ -1,3 +1,4 @@ dist package-lock.json .cache-loader/ +mockVite/**/_*.bundled_*.mjs diff --git a/packages/sagas/package.json b/packages/sagas/package.json index 3de91f425c5..93610c2aa94 100644 --- a/packages/sagas/package.json +++ b/packages/sagas/package.json @@ -41,7 +41,6 @@ }, "dependencies": { "@talend/react-cmf": "^12.1.1", - "immutable": "^3.8.2", "redux-saga": "^1.4.2" }, "peerDependencies": { diff --git a/packages/sagas/src/pending/pending.js b/packages/sagas/src/pending/pending.js index 1530c6b40b6..e8587ff4036 100644 --- a/packages/sagas/src/pending/pending.js +++ b/packages/sagas/src/pending/pending.js @@ -1,6 +1,5 @@ import { delay, call, put, select, take } from 'redux-saga/effects'; import cmf from '@talend/react-cmf'; -import { Map } from 'immutable'; import { PENDING_DELAY_TO_SHOW, PENDING_COLLECTION_NAME, SHOW_PENDING } from '../constants'; @@ -12,11 +11,12 @@ const addOrReplace = cmf.actions.collections.addOrReplace; * @param {string} asyncActionId the unique contextualized actionId */ export function findPenderById(state, asyncActionId) { - return state.cmf.collections.getIn([PENDING_COLLECTION_NAME, asyncActionId]); + const collection = cmf.selectors.collections.getCollectionPlain(state, PENDING_COLLECTION_NAME); + return collection ? collection[asyncActionId] : undefined; } export function findPenders(state) { - return state.cmf.collections.get(PENDING_COLLECTION_NAME); + return cmf.selectors.collections.getCollectionPlain(state, PENDING_COLLECTION_NAME); } /** @@ -25,7 +25,7 @@ export function findPenders(state) { export function* ensurePendersCollectionExists() { const collection = yield select(findPenders); if (!collection) { - yield put(addOrReplace(PENDING_COLLECTION_NAME, new Map())); + yield put(addOrReplace(PENDING_COLLECTION_NAME, {})); } } @@ -42,9 +42,9 @@ export default function* pendingMaybeNeeded(asyncCallerId, actionId) { yield delay(PENDING_DELAY_TO_SHOW); pending = true; yield call(ensurePendersCollectionExists); - let pendersCollection = yield select(findPenders); - pendersCollection = pendersCollection.set(asyncActionId, SHOW_PENDING); - yield put(addOrReplace(PENDING_COLLECTION_NAME, pendersCollection)); + const pendersCollection = yield select(findPenders); + const newPendersCollection = { ...pendersCollection, [asyncActionId]: SHOW_PENDING }; + yield put(addOrReplace(PENDING_COLLECTION_NAME, newPendersCollection)); yield take('DO_NOT_QUIT'); } finally { if (pending) { @@ -52,9 +52,9 @@ export default function* pendingMaybeNeeded(asyncCallerId, actionId) { const penderStatus = yield select(findPenderById, asyncActionId); if (penderStatus) { - let pendersCollection = yield select(findPenders); - pendersCollection = pendersCollection.delete(asyncActionId); - yield put(addOrReplace(PENDING_COLLECTION_NAME, pendersCollection)); + const pendersCollection = yield select(findPenders); + const { [asyncActionId]: _, ...updatedPendersCollection } = pendersCollection; + yield put(addOrReplace(PENDING_COLLECTION_NAME, updatedPendersCollection)); } } } diff --git a/packages/sagas/src/pending/pending.test.js b/packages/sagas/src/pending/pending.test.js index 4cf9171587b..f37f94d7b83 100644 --- a/packages/sagas/src/pending/pending.test.js +++ b/packages/sagas/src/pending/pending.test.js @@ -1,6 +1,5 @@ import { delay, select, put, call, take } from 'redux-saga/effects'; import cmf from '@talend/react-cmf'; -import { Map } from 'immutable'; import pendingMaybeNeeded, { ensurePendersCollectionExists, findPenders, @@ -16,10 +15,8 @@ describe('test pending status', () => { const gen = ensurePendersCollectionExists(); expect(gen.next().value).toEqual(select(findPenders)); - // if penders collection has been create - expect(gen.next(undefined).value).toEqual( - put(addOrReplace(PENDING_COLLECTION_NAME, new Map())), - ); + // when no collection exists, saga puts a new empty plain object collection + expect(gen.next(undefined).value).toEqual(put(addOrReplace(PENDING_COLLECTION_NAME, {}))); // the saga is finished expect(gen.next()).toEqual({ done: true, value: undefined }); }); @@ -27,28 +24,29 @@ describe('test pending status', () => { const gen = ensurePendersCollectionExists(); expect(gen.next().value).toEqual(select(findPenders)); - expect(gen.next(new Map())).toEqual({ done: true, value: undefined }); + // existing truthy plain object → saga skips creation and finishes + expect(gen.next({})).toEqual({ done: true, value: undefined }); }); it('should pend and then clear pending', () => { const gen = pendingMaybeNeeded('', 'streams:create'); - let pendersCollection = new Map(); + const pendersCollection = {}; expect(gen.next().value).toEqual(delay(PENDING_DELAY_TO_SHOW)); expect(gen.next().value).toEqual(call(ensurePendersCollectionExists)); expect(gen.next().value).toEqual(select(findPenders)); - pendersCollection = pendersCollection.set('#streams:create', SHOW_PENDING); + // saga spreads collection + new entry, yields put with updated plain object expect(gen.next(pendersCollection).value).toEqual( - put(addOrReplace(PENDING_COLLECTION_NAME, pendersCollection)), + put(addOrReplace(PENDING_COLLECTION_NAME, { '#streams:create': SHOW_PENDING })), ); expect(gen.next().value).toEqual(take('DO_NOT_QUIT')); expect(gen.next().value).toEqual(call(ensurePendersCollectionExists)); expect(gen.next().value).toEqual(select(findPenderById, '#streams:create')); expect(gen.next(SHOW_PENDING).value).toEqual(select(findPenders)); - pendersCollection = pendersCollection.delete('#streams:create'); - expect(gen.next(pendersCollection).value).toEqual( - put(addOrReplace(PENDING_COLLECTION_NAME, pendersCollection)), + // saga destructures entry out, yields put with empty plain object + expect(gen.next({ '#streams:create': SHOW_PENDING }).value).toEqual( + put(addOrReplace(PENDING_COLLECTION_NAME, {})), ); // the saga is finished expect(gen.next()).toEqual({ done: true, value: undefined }); diff --git a/packages/stepper/package.json b/packages/stepper/package.json index 842ad47716f..5c974ef2952 100644 --- a/packages/stepper/package.json +++ b/packages/stepper/package.json @@ -68,7 +68,6 @@ "eslint": "^10.0.3", "stylelint": "^17.4.0", "i18next": "^23.16.8", - "immutable": "^3.8.2", "jsdom": "^26.1.0", "prettier": "^3.8.1", "prop-types": "^15.8.1",