Skip to content

Commit

Permalink
front: add editor slice
Browse files Browse the repository at this point in the history
  • Loading branch information
kmer2016 committed Nov 14, 2023
1 parent 598b2cb commit 9f5c3d2
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 114 deletions.
65 changes: 65 additions & 0 deletions front/src/reducers/editor/__tests__/editorReducer.spec.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
});
26 changes: 26 additions & 0 deletions front/src/reducers/editor/__tests__/editorTestDataBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LayerType, EditorState } from 'applications/editor/tools/types';

export default function editorTestDataBuilder() {
return {
buildEditorLayers: (layers: Array<LayerType>): 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<EditorState['issues'], 'total' | 'filterTotal'> => ({
total,
filterTotal,
}),
buildFilterIssue: (
filterLevel: EditorState['issues']['filterLevel'],
filterType: EditorState['issues']['filterType']
): Omit<EditorState['issues'], 'total' | 'filterTotal'> => ({
filterLevel,
filterType,
}),
};
}
173 changes: 65 additions & 108 deletions front/src/reducers/editor/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,45 +11,69 @@ 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,
entityToDeleteOperation,
} 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<LayerType>;
}
export function selectLayers(
layers: ActionSelectLayers['layers']
): ThunkAction<ActionSelectLayers> {
return (dispatch) => {
dispatch({
type: SELECT_LAYERS,
layers,
});
};
}
export const editorSlice = createSlice({
name: 'editor',
initialState: editorInitialState,
reducers: {
selectLayers(state, action: PayloadAction<EditorState['editorLayers']>) {
state.editorLayers = action.payload;
},
loadDataModelAction(state, action: PayloadAction<EditorState['editorSchema']>) {
state.editorSchema = action.payload;
},
updateTotalsIssueAction(
state,
action: PayloadAction<Pick<EditorState['issues'], 'total' | 'filterTotal'>>
) {
state.issues = {
...state.issues,
...action.payload,
};
},
updateFiltersIssueAction(
state,
action: PayloadAction<Omit<EditorState['issues'], 'total' | 'filterTotal'>>
) {
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<ActionLoadDataModel> {
export function loadDataModel(): ThunkAction<editorSliceActionsType['loadDataModelAction']> {
return async (dispatch: Dispatch, getState) => {
// check if we need to load the model
if (!Object.keys(getState().editor.editorSchema).length) {
Expand Down Expand Up @@ -87,10 +110,7 @@ export function loadDataModel(): ThunkAction<ActionLoadDataModel> {
} as EditorSchema[0];
});
dispatch(setSuccessWithoutMessage());
dispatch({
type: LOAD_DATA_MODEL,
schema,
});
dispatch(loadDataModelAction(schema));
} catch (e) {
console.error(e);
dispatch(setFailure(e as Error));
Expand All @@ -99,14 +119,9 @@ export function loadDataModel(): ThunkAction<ActionLoadDataModel> {
};
}

const UPDATE_TOTALS_ISSUE = 'editor/UPDATE_TOTALS_ISSUE';
export interface ActionUpdateTotalsIssue extends AnyAction {
type: typeof UPDATE_TOTALS_ISSUE;
issues: Pick<EditorState['issues'], 'total' | 'filterTotal'>;
}
export function updateTotalsIssue(
infraID: number | undefined
): ThunkAction<ActionUpdateTotalsIssue> {
): ThunkAction<editorSliceActionsType['updateTotalsIssueAction']> {
return async (dispatch: Dispatch, getState) => {
const { editor } = getState();
dispatch(setLoading());
Expand Down Expand Up @@ -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;
Expand All @@ -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<EditorState['issues'], 'total' | 'filterTotal'>;
}
export function updateFiltersIssue(
infraID: number | undefined,
filters: Partial<Pick<EditorState['issues'], 'filterLevel' | 'filterType'>>
): ThunkAction<ActionUpdateTotalsIssue> {
): ThunkAction<editorSliceActionsType['updateFiltersIssueAction']> {
return async (dispatch: Dispatch, getState) => {
const { editor } = getState() as { editor: EditorState };
let level = isUndefined(filters.filterLevel) ? editor.issues.filterLevel : filters.filterLevel;
Expand All @@ -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
Expand Down Expand Up @@ -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<any>' is not assignable to type 'EditorActions'"
// We need to migrate the editor store with slice
export default reducer as Reducer<EditorState, Action>;
export default editorSlice.reducer;
6 changes: 5 additions & 1 deletion front/src/reducers/editor/selectors.ts
Original file line number Diff line number Diff line change
@@ -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<EditorState>(getEditorState);

export const getEditorIssue = makeEditorSelector('issues');
10 changes: 5 additions & 5 deletions front/src/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -118,7 +118,7 @@ export type AnyReducerState =
export const rootReducer: ReducersMapObject<RootState> = {
user: userReducer,
map: mapReducer,
editor: editorReducer,
[editorSlice.name]: editorReducer,
main: mainReducer,
[stdcmConfSlice.name]: stdcmConfReducer,
[simulationConfSlice.name]: simulationConfReducer,
Expand Down

0 comments on commit 9f5c3d2

Please sign in to comment.