diff --git a/front/src/reducers/editor/__tests__/editorReducer.spec.ts b/front/src/reducers/editor/__tests__/editorReducer.spec.ts new file mode 100644 index 00000000000..72eb9b2f2ca --- /dev/null +++ b/front/src/reducers/editor/__tests__/editorReducer.spec.ts @@ -0,0 +1,65 @@ +import { EditorState } from 'applications/editor/tools/types'; +import { createStoreWithoutMiddleware } from 'Store'; +import editorTestDataBuilder from './editorTestDataBuilder'; +import { + editorInitialState, + selectLayers, + editorSlice, + loadDataModelAction, + updateTotalsIssueAction, + updateFiltersIssueAction, +} from '..'; + +const createStore = (initialStateExtra?: EditorState) => + createStoreWithoutMiddleware({ + [editorSlice.name]: initialStateExtra, + }); + +describe('editorReducer', () => { + const testDataBuilder = editorTestDataBuilder(); + it('should return initial state', () => { + const store = createStore(); + const editorState = store.getState()[editorSlice.name]; + expect(editorState).toEqual(editorInitialState); + }); + + it('should handle selectLayers', () => { + const store = createStore(); + const editorLayers = testDataBuilder.buildEditorLayers(['catenaries', 'routes']); + store.dispatch(selectLayers(editorLayers)); + const editorState = store.getState()[editorSlice.name]; + expect(editorState.editorLayers).toEqual(new Set(['catenaries', 'routes'])); + }); + + describe('should handle loadDataModelAction', () => { + it('should have empty array initially', () => { + const store = createStore(); + const editorState = store.getState()[editorSlice.name]; + expect(editorState.editorSchema).toEqual([]); + }); + + it('should update editorSchema', () => { + const store = createStore(); + const editorSchema = testDataBuilder.buildEditorSchema(); + store.dispatch(loadDataModelAction(editorSchema)); + const editorState = store.getState()[editorSlice.name]; + expect(editorState.editorSchema).toEqual(editorSchema); + }); + }); + + it('should handle updateTotalIssueAction', () => { + const store = createStore(); + const newIssues = testDataBuilder.buildTotalIssue(5, 10); + store.dispatch(updateTotalsIssueAction(newIssues)); + const editorState = store.getState()[editorSlice.name]; + expect(editorState.issues).toEqual({ ...editorInitialState.issues, ...newIssues }); + }); + + it('should handle updateFiltersIssueAction', () => { + const store = createStore(); + const newIssues = testDataBuilder.buildFilterIssue('warnings', 'empty_object'); + store.dispatch(updateFiltersIssueAction(newIssues)); + const editorState = store.getState()[editorSlice.name]; + expect(editorState.issues).toEqual({ ...editorInitialState.issues, ...newIssues }); + }); +}); diff --git a/front/src/reducers/editor/__tests__/editorTestDataBuilder.ts b/front/src/reducers/editor/__tests__/editorTestDataBuilder.ts new file mode 100644 index 00000000000..fcc8c012906 --- /dev/null +++ b/front/src/reducers/editor/__tests__/editorTestDataBuilder.ts @@ -0,0 +1,26 @@ +import { LayerType, EditorState } from 'applications/editor/tools/types'; + +export default function editorTestDataBuilder() { + return { + buildEditorLayers: (layers: Array): EditorState['editorLayers'] => new Set(layers), + buildEditorSchema: (): EditorState['editorSchema'] => [ + { layer: 'layerA', objType: 'BufferStop', schema: {} }, + { layer: 'layerA', objType: 'Route', schema: {} }, + { layer: 'layerA', objType: 'SwitchType', schema: {} }, + ], + buildTotalIssue: ( + total: EditorState['issues']['total'], + filterTotal: EditorState['issues']['filterTotal'] + ): Pick => ({ + total, + filterTotal, + }), + buildFilterIssue: ( + filterLevel: EditorState['issues']['filterLevel'], + filterType: EditorState['issues']['filterType'] + ): Omit => ({ + filterLevel, + filterType, + }), + }; +} diff --git a/front/src/reducers/editor/index.ts b/front/src/reducers/editor/index.ts index a9ffefd78a1..f4ea0140827 100644 --- a/front/src/reducers/editor/index.ts +++ b/front/src/reducers/editor/index.ts @@ -1,8 +1,7 @@ -import produce from 'immer'; import { Feature } from 'geojson'; import { omit, clone, isNil, isUndefined } from 'lodash'; import { JSONSchema7, JSONSchema7Definition } from 'json-schema'; -import { Action, AnyAction, Dispatch, Reducer } from '@reduxjs/toolkit'; +import { AnyAction, createSlice, Dispatch, PayloadAction } from '@reduxjs/toolkit'; import { setLoading, setSuccess, setFailure, setSuccessWithoutMessage } from 'reducers/main'; import { updateIssuesSettings } from 'reducers/map'; @@ -12,7 +11,7 @@ import { allInfraErrorTypes, infraErrorTypeList, } from 'applications/editor/components/InfraErrors/types'; -import { EditorState, LayerType } from 'applications/editor/tools/types'; +import { EditorState } from 'applications/editor/tools/types'; import { entityToCreateOperation, entityToUpdateOperation, @@ -20,37 +19,61 @@ import { } from 'applications/editor/data/utils'; import infra_schema from '../osrdconf/infra_schema.json'; -// -// Actions -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +export const editorInitialState: EditorState = { + // Definition of entities (json schema) + editorSchema: [], + // ID of selected layers on which we are working + editorLayers: new Set(['track_sections', 'errors']), + // Editor issue management + issues: { + total: 0, + filterTotal: 0, + filterLevel: 'all', + filterType: null, + }, +}; -const SELECT_LAYERS = 'editor/SELECT_LAYERS'; -export interface ActionSelectLayers extends AnyAction { - type: typeof SELECT_LAYERS; - layers: Set; -} -export function selectLayers( - layers: ActionSelectLayers['layers'] -): ThunkAction { - return (dispatch) => { - dispatch({ - type: SELECT_LAYERS, - layers, - }); - }; -} +export const editorSlice = createSlice({ + name: 'editor', + initialState: editorInitialState, + reducers: { + selectLayers(state, action: PayloadAction) { + state.editorLayers = action.payload; + }, + loadDataModelAction(state, action: PayloadAction) { + state.editorSchema = action.payload; + }, + updateTotalsIssueAction( + state, + action: PayloadAction> + ) { + state.issues = { + ...state.issues, + ...action.payload, + }; + }, + updateFiltersIssueAction( + state, + action: PayloadAction> + ) { + state.issues = { + ...state.issues, + ...action.payload, + }; + }, + }, +}); -// -// Verify if the data model definition is already loaded. -// If not we do it and store it in the state -// -export const LOAD_DATA_MODEL = 'editor/LOAD_DATA_MODEL'; -export interface ActionLoadDataModel extends AnyAction { - type: typeof LOAD_DATA_MODEL; - schema: EditorSchema; -} +export const { + selectLayers, + loadDataModelAction, + updateTotalsIssueAction, + updateFiltersIssueAction, +} = editorSlice.actions; + +export type editorSliceActionsType = typeof editorSlice.actions; -export function loadDataModel(): ThunkAction { +export function loadDataModel(): ThunkAction { return async (dispatch: Dispatch, getState) => { // check if we need to load the model if (!Object.keys(getState().editor.editorSchema).length) { @@ -87,10 +110,7 @@ export function loadDataModel(): ThunkAction { } as EditorSchema[0]; }); dispatch(setSuccessWithoutMessage()); - dispatch({ - type: LOAD_DATA_MODEL, - schema, - }); + dispatch(loadDataModelAction(schema)); } catch (e) { console.error(e); dispatch(setFailure(e as Error)); @@ -99,14 +119,9 @@ export function loadDataModel(): ThunkAction { }; } -const UPDATE_TOTALS_ISSUE = 'editor/UPDATE_TOTALS_ISSUE'; -export interface ActionUpdateTotalsIssue extends AnyAction { - type: typeof UPDATE_TOTALS_ISSUE; - issues: Pick; -} export function updateTotalsIssue( infraID: number | undefined -): ThunkAction { +): ThunkAction { return async (dispatch: Dispatch, getState) => { const { editor } = getState(); dispatch(setLoading()); @@ -142,10 +157,7 @@ export function updateTotalsIssue( const filterResult = await filterResp; filterTotal = filterResult.data?.count || 0; } - dispatch({ - type: UPDATE_TOTALS_ISSUE, - issues: { total, filterTotal }, - }); + dispatch(updateTotalsIssueAction({ total, filterTotal })); } catch (e) { dispatch(setFailure(e as Error)); throw e; @@ -155,15 +167,10 @@ export function updateTotalsIssue( }; } -const UPDATE_FILTERS_ISSUE = 'editor/UPDATE_FILTERS_ISSUE'; -export interface ActionUpdateFiltersIssue extends AnyAction { - type: typeof UPDATE_FILTERS_ISSUE; - issues: Omit; -} export function updateFiltersIssue( infraID: number | undefined, filters: Partial> -): ThunkAction { +): ThunkAction { return async (dispatch: Dispatch, getState) => { const { editor } = getState() as { editor: EditorState }; let level = isUndefined(filters.filterLevel) ? editor.issues.filterLevel : filters.filterLevel; @@ -187,10 +194,7 @@ export function updateFiltersIssue( } } - dispatch({ - type: UPDATE_FILTERS_ISSUE, - issues: { filterLevel: level, filterType: type }, - }); + dispatch(updateFiltersIssueAction({ filterLevel: level, filterType: type })); dispatch(updateTotalsIssue(infraID)); // dispatch the list of types matched by the filter to the map @@ -261,57 +265,10 @@ export function save( } export type EditorActions = - | ActionLoadDataModel - | ActionSave - | ActionSelectLayers - | ActionUpdateTotalsIssue - | ActionUpdateFiltersIssue; - -// -// State definition -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -export const initialState: EditorState = { - // Definition of entities (json schema) - editorSchema: [], - // ID of selected layers on which we are working - editorLayers: new Set(['track_sections', 'errors']), - // Editor issue management - issues: { - total: 0, - filterTotal: 0, - filterLevel: 'all', - filterType: null, - }, -}; - -// -// State reducer -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -const reducer = (inputState: EditorState | undefined, action: EditorActions) => { - const state = inputState || initialState; - - return produce(state, (draft) => { - switch (action.type) { - case SELECT_LAYERS: - draft.editorLayers = action.layers; - break; - case LOAD_DATA_MODEL: - draft.editorSchema = action.schema; - break; - case UPDATE_FILTERS_ISSUE: - case UPDATE_TOTALS_ISSUE: - draft.issues = { - ...state.issues, - ...action.issues, - }; - break; - default: - // Nothing to do here - break; - } - }); -}; + | editorSliceActionsType['loadDataModelAction'] + | editorSliceActionsType['selectLayers'] + | editorSliceActionsType['updateFiltersIssueAction'] + | editorSliceActionsType['updateTotalsIssueAction'] + | ActionSave; -// TODO: to avoid error "Type 'Action' is not assignable to type 'EditorActions'" -// We need to migrate the editor store with slice -export default reducer as Reducer; +export default editorSlice.reducer; diff --git a/front/src/reducers/editor/selectors.ts b/front/src/reducers/editor/selectors.ts index f19f8b0a104..24386c7fb17 100644 --- a/front/src/reducers/editor/selectors.ts +++ b/front/src/reducers/editor/selectors.ts @@ -1,4 +1,8 @@ +import { EditorState } from 'applications/editor/tools/types'; import { RootState } from 'reducers'; +import { makeSubSelector } from 'utils/selectors'; export const getEditorState = (state: RootState) => state.editor; -export const getEditorIssue = (state: RootState) => state.editor.issues; +const makeEditorSelector = makeSubSelector(getEditorState); + +export const getEditorIssue = makeEditorSelector('issues'); diff --git a/front/src/reducers/index.ts b/front/src/reducers/index.ts index 08e5b5e42af..52bca45473a 100644 --- a/front/src/reducers/index.ts +++ b/front/src/reducers/index.ts @@ -14,8 +14,8 @@ import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; import userReducer, { UserState, userInitialState } from 'reducers/user'; import mainReducer, { MainState, mainInitialState } from 'reducers/main'; -import mapReducer, { MapState, mapInitialState } from './map'; -import editorReducer, { EditorActions, initialState as editorInitialState } from './editor'; +import mapReducer, { MapState, mapInitialState } from 'reducers/map'; +import editorReducer, { EditorActions, editorInitialState, editorSlice } from 'reducers/editor'; import osrdconfReducer, { initialState as osrdconfInitialState } from './osrdconf'; // Dependency cycle will be removed during the refactoring of store // eslint-disable-next-line import/no-cycle @@ -83,7 +83,7 @@ type AllActions = EditorActions | Action; export interface RootState { user: UserState; map: MapState; - editor: EditorState; + [editorSlice.name]: EditorState; main: MainState; [stdcmConfSlice.name]: OsrdStdcmConfState; [simulationConfSlice.name]: OsrdConfState; @@ -96,7 +96,7 @@ export interface RootState { export const rootInitialState: RootState = { user: userInitialState, map: mapInitialState, - editor: editorInitialState, + [editorSlice.name]: editorInitialState, main: mainInitialState, [stdcmConfSlice.name]: stdcmConfInitialState, [simulationConfSlice.name]: simulationConfInitialState, @@ -118,7 +118,7 @@ export type AnyReducerState = export const rootReducer: ReducersMapObject = { user: userReducer, map: mapReducer, - editor: editorReducer, + [editorSlice.name]: editorReducer, main: mainReducer, [stdcmConfSlice.name]: stdcmConfReducer, [simulationConfSlice.name]: simulationConfReducer,