Skip to content

Commit

Permalink
make patching-induced rerenders stable (#335)
Browse files Browse the repository at this point in the history
Co-authored-by: Matthias Lehner <[email protected]>
  • Loading branch information
loreanvictor and matthiaslehnertum authored Dec 22, 2023
1 parent 5aea464 commit 62a3a8f
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 4 deletions.
44 changes: 44 additions & 0 deletions src/main/components/store/merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ModelState } from '../../components/store/model-state';
import { AssessmentState } from '../../services/assessment/assessment-types';
import { UMLElementState } from '../../services/uml-element/uml-element-types';

/**
* Merges the old state with the new state. In particular, it maintains
* all potential prototypes, and gracefully updates owned elements list in the
* diagram. The boundaries of the diagram are NOT updated, which is to be done, if
* necessary, by some subsequent side-effect.
* @param oldState
* @param newState
* @returns The merged state.
*/
export function merge(oldState: ModelState, newState: ModelState): ModelState {
return {
...oldState,
diagram: {
...oldState.diagram,
ownedElements: oldState.diagram.ownedElements.filter(
(id) => !!newState.elements[id] && !newState.elements[id].owner,
),
ownedRelationships: oldState.diagram.ownedRelationships.filter((id) => !!newState.elements[id]),
},
elements: Object.keys(newState.elements).reduce((acc, id) => {
return {
...acc,
[id]: {
...oldState.elements[id],
...newState.elements[id],
},
};
}, {} as UMLElementState),
interactive: newState.interactive,
assessments: Object.keys(newState.assessments).reduce((acc, id) => {
return {
...acc,
[id]: {
...oldState.assessments[id],
...newState.assessments[id],
},
};
}, {} as AssessmentState),
};
}
2 changes: 2 additions & 0 deletions src/main/components/store/model-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
Patcher,
} from '../../services/patcher';
import { UMLModel } from '../../typings';
import { merge } from './merge';

type OwnProps = PropsWithChildren<{
initialState?: PreloadedState<PartialModelState>;
Expand All @@ -48,6 +49,7 @@ export const createReduxStore = (
patcher &&
createPatcherReducer<UMLModel, ModelState>(patcher, {
transform: (model) => ModelState.fromModel(model, false) as ModelState,
merge,
});

const reducer: Reducer<ModelState, Actions> = (state, action) => {
Expand Down
18 changes: 14 additions & 4 deletions src/main/services/patcher/patcher-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Reducer } from 'redux';
import { Patcher } from './patcher';
import { PatcherActionTypes } from './patcher-types';
import { deepClone } from 'fast-json-patch';

export type PatcherReducerOptions<T, U = T> = {
/**
Expand All @@ -10,10 +11,21 @@ export type PatcherReducerOptions<T, U = T> = {
* @returns The state in the internal schema.
*/
transform?: (state: T) => U;

/**
* Merges the old state with the new state. This is useful when naive strategies
* like `Object.assign` would trigger unwanted side-effects and more context-aware merging
* of state is required.
* @param oldState
* @param newState
* @returns The merged state.
*/
merge?: (oldState: U, newState: U) => U;
};

const _DefaultOptions = {
transform: (state: any) => state,
merge: (oldState: any, newState: any) => ({ ...oldState, ...newState }),
};

/**
Expand All @@ -27,16 +39,14 @@ export function createPatcherReducer<T, U = T>(
options: PatcherReducerOptions<T, U> = _DefaultOptions,
): Reducer<U> {
const transform = options.transform || _DefaultOptions.transform;
const merge = options.merge || _DefaultOptions.merge;

return (state = {} as U, action) => {
const { type, payload } = action;
if (type === PatcherActionTypes.PATCH) {
const res = transform(patcher.patch(payload));

return {
...state,
...res,
};
return merge(state, res);
}

return state;
Expand Down
22 changes: 22 additions & 0 deletions src/tests/unit/components/store/merge-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { merge } from '../../../../main/components/store/merge';
import { ModelState } from '../../../../main/components/store/model-state';
import diagram from '../../test-resources/class-diagram.json';
import diagram3 from '../../test-resources/class-diagram-3.json';

describe('merge', () => {
const pkgId = 'c10b995a-036c-4e9e-aa67-0570ada5cb6a';
const class1Id = '04d3509e-0dce-458b-bf62-f3555497a5a4';
const class2Id = '9eadc4f6-caa0-4835-a24c-71c0c1ccbc39';

test('merges two model states.', () => {
const oldState = ModelState.fromModel(diagram as any) as ModelState;
const newState = ModelState.fromModel(diagram3 as any) as ModelState;

const merged = merge(oldState, newState);

expect(merged.elements[pkgId]).not.toBeDefined();
expect(merged.elements[class1Id]).toBeDefined();
expect(merged.elements[class2Id]).toBeDefined();
expect(merged.elements[class1Id].owner).toBeNull();
});
});
12 changes: 12 additions & 0 deletions src/tests/unit/services/patcher/patcher-reducer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ describe('test patcher reducer.', () => {
expect(nextState).toEqual({ y: 42 });
});

test('calls given merge function for applying the patch.', () => {
const cb = jest.fn((x, y) => ({ ...x, ...y }));

const patcher = new Patcher();
patcher.initialize({});

const reducer = createPatcherReducer(patcher, { merge: cb });
const nextState = reducer({ y: 41 }, PatcherRepository.patch([{ op: 'add', path: '/x', value: 42 }]));

expect(nextState).toEqual({ x: 42, y: 41 });
});

test('handles weird options.', () => {
expect(() => createPatcherReducer(new Patcher(), {})).not.toThrow();
});
Expand Down
90 changes: 90 additions & 0 deletions src/tests/unit/test-resources/class-diagram-3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"version": "3.0.0",
"type": "ClassDiagram",
"size": { "width": 860, "height": 260 },
"interactive": { "elements": {}, "relationships": {} },
"elements": {
"04d3509e-0dce-458b-bf62-f3555497a5a4": {
"id": "04d3509e-0dce-458b-bf62-f3555497a5a4",
"name": "Class",
"type": "Class",
"owner": null,
"bounds": { "x": 200, "y": 70, "width": 200, "height": 140 },
"attributes": ["90f94404-1fc6-4121-97ed-6b2c6d57d525"],
"methods": ["12d8bb82-e4e5-4505-a0eb-48ddab0fc0a0"]
},
"90f94404-1fc6-4121-97ed-6b2c6d57d525": {
"id": "90f94404-1fc6-4121-97ed-6b2c6d57d525",
"name": "+ attr: Type",
"type": "ClassAttribute",
"owner": "04d3509e-0dce-458b-bf62-f3555497a5a4",
"bounds": { "x": 80, "y": 110, "width": 200, "height": 30 }
},
"12d8bb82-e4e5-4505-a0eb-48ddab0fc0a0": {
"id": "12d8bb82-e4e5-4505-a0eb-48ddab0fc0a0",
"name": "+ method()",
"type": "ClassMethod",
"owner": "04d3509e-0dce-458b-bf62-f3555497a5a4",
"bounds": { "x": 80, "y": 140, "width": 200, "height": 30 }
},
"9eadc4f6-caa0-4835-a24c-71c0c1ccbc39": {
"id": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39",
"name": "Class",
"type": "Class",
"owner": null,
"bounds": { "x": 620, "y": 90, "width": 200, "height": 100 },
"attributes": ["dbd4193a-4483-43df-8934-77192be006c2"],
"methods": ["e7ef41ee-290e-4df2-a535-199f1c5521fd"]
},
"dbd4193a-4483-43df-8934-77192be006c2": {
"id": "dbd4193a-4483-43df-8934-77192be006c2",
"name": "+ attribute: Type",
"type": "ClassAttribute",
"owner": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39",
"bounds": { "x": 620, "y": 130, "width": 200, "height": 30 }
},
"e7ef41ee-290e-4df2-a535-199f1c5521fd": {
"id": "e7ef41ee-290e-4df2-a535-199f1c5521fd",
"name": "+ method()",
"type": "ClassMethod",
"owner": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39",
"bounds": { "x": 620, "y": 160, "width": 200, "height": 30 }
}
},
"relationships": {
"f5c4e20d-8347-4136-bc02-b7a016e017f5": {
"id": "f5c4e20d-8347-4136-bc02-b7a016e017f5",
"name": "",
"type": "ClassBidirectional",
"owner": null,
"bounds": { "x": 280, "y": 130, "width": 340, "height": 1 },
"path": [
{ "x": 340, "y": 0 },
{ "x": 0, "y": 0 }
],
"source": {
"direction": "Left",
"element": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39",
"multiplicity": "",
"role": ""
},
"target": {
"direction": "Right",
"element": "04d3509e-0dce-458b-bf62-f3555497a5a4",
"multiplicity": "",
"role": ""
},
"isManuallyLayouted": false
}
},
"assessments": {
"04d3509e-0dce-458b-bf62-f3555497a5a4": {
"modelElementId": "04d3509e-0dce-458b-bf62-f3555497a5a4",
"elementType": "Class",
"score": 10,
"feedback": "good",
"label": "Class",
"labelColor": "blue"
}
}
}

0 comments on commit 62a3a8f

Please sign in to comment.