diff --git a/src/main/services/patcher/patcher-saga.ts b/src/main/services/patcher/patcher-saga.ts new file mode 100644 index 00000000..8df0aa16 --- /dev/null +++ b/src/main/services/patcher/patcher-saga.ts @@ -0,0 +1,47 @@ +import { call, debounce, delay, put, select, take } from 'redux-saga/effects'; + import { SagaIterator } from 'redux-saga'; + + import { run } from '../../utils/actions/sagas'; + import { PatcherActionTypes } from './patcher-types'; + import { ModelState } from '../../components/store/model-state'; + import { UMLContainerRepository } from '../uml-container/uml-container-repository'; + import { UMLElement } from '../uml-element/uml-element'; + import { UMLRelationship } from '../uml-relationship/uml-relationship'; + import { recalc } from '../uml-relationship/uml-relationship-saga'; + import { render } from '../layouter/layouter'; + + /** + * Fixes the layout of the diagram after importing a patch. + */ + export function* PatchLayouter(): SagaIterator { + yield run([patchLayout]); +} + +export function* patchLayout(): SagaIterator { + yield debounce(100, PatcherActionTypes.PATCH, recalculateLayouts); +} + +function* recalculateLayouts(): SagaIterator { + const { elements }: ModelState = yield select(); + + const ids = Object.values(elements) + .filter((x) => !x.owner) + .map((x) => x.id); + + if (!ids.length) { + return; + } + + yield put(UMLContainerRepository.append(ids)); + + for (const id of Object.keys(elements)) { + yield delay(0); + if (UMLElement.isUMLElement(elements[id])) { + yield call(render, id); + } + + if (UMLRelationship.isUMLRelationship(elements[id]) && !elements[id].isManuallyLayouted) { + yield call(recalc, id); + } + } +} diff --git a/src/main/services/saga.ts b/src/main/services/saga.ts index 4037e4eb..5bbbb63e 100644 --- a/src/main/services/saga.ts +++ b/src/main/services/saga.ts @@ -6,11 +6,12 @@ import { UMLContainerSaga } from './uml-container/uml-container-saga'; import { UMLDiagramSaga } from './uml-diagram/uml-diagram-saga'; import { UMLElementSaga } from './uml-element/uml-element-saga'; import { UMLRelationshipSaga } from './uml-relationship/uml-relationship-saga'; +import { PatchLayouter } from './patcher/patcher-saga'; export type SagaContext = { layer: ILayer | null; }; export function* saga(): SagaIterator { - yield composeSaga([Layouter, UMLElementSaga, UMLContainerSaga, UMLRelationshipSaga, UMLDiagramSaga]); + yield composeSaga([Layouter, UMLElementSaga, UMLContainerSaga, UMLRelationshipSaga, UMLDiagramSaga, PatchLayouter]); } diff --git a/src/tests/unit/services/patcher/patcher-saga-test.ts b/src/tests/unit/services/patcher/patcher-saga-test.ts new file mode 100644 index 00000000..383a9a7a --- /dev/null +++ b/src/tests/unit/services/patcher/patcher-saga-test.ts @@ -0,0 +1,70 @@ +import { call, debounce, delay, select, take } from 'redux-saga/effects'; + + import { patchLayout } from '../../../../main/services/patcher/patcher-saga'; + import { PatcherActionTypes, PatcherRepository } from '../../../../main/services/patcher'; + import { UMLElementState } from '../../../../main/services/uml-element/uml-element-types'; + import { IUMLRelationship } from '../../../../main/services/uml-relationship/uml-relationship'; + import { render } from '../../../../main/services/layouter/layouter'; + import { recalc } from '../../../../main/services/uml-relationship/uml-relationship-saga'; + + describe('test patcher saga.', () => { + test('it invokes re-renders and re-calcs after a patch.', () => { + const run = patchLayout(); + const debounced = run.next().value; + expect(debounced).toEqual(debounce(100, PatcherActionTypes.PATCH, expect.any(Function))); + + const fork = debounced['payload']['args'][2](); + expect(fork.next(PatcherRepository.patch([{ op: 'add', path: '/x', value: 42 }])).value).toEqual(select()); + + const elements: UMLElementState = { + x: { + type: 'Package', + id: 'x', + name: 'package', + owner: null, + bounds: { x: 0, y: 0, width: 100, height: 100 }, + }, + y: { + type: 'Class', + id: 'y', + name: 'class', + owner: 'x', + bounds: { x: 0, y: 0, width: 100, height: 100 }, + }, + z: { + type: 'Class', + id: 'z', + name: 'class', + owner: null, + bounds: { x: 0, y: 0, width: 100, height: 100 }, + }, + w: { + type: 'ClassInheritance', + id: 'w', + name: '...', + owner: null, + source: { element: 'y', direction: 'Up' }, + target: { element: 'z', direction: 'Down' }, + path: [ + { x: 0, y: 0 }, + { x: 200, y: 100 }, + ], + bounds: { x: 0, y: 0, width: 200, height: 100 }, + } as IUMLRelationship, + }; + + fork.next({ elements }); + + expect(fork.next().value).toEqual(delay(0)); + expect(fork.next().value).toEqual(call(render, 'x')); + + expect(fork.next().value).toEqual(delay(0)); + expect(fork.next().value).toEqual(call(render, 'y')); + + expect(fork.next().value).toEqual(delay(0)); + expect(fork.next().value).toEqual(call(render, 'z')); + + expect(fork.next().value).toEqual(delay(0)); + expect(fork.next().value).toEqual(call(recalc, 'w')); + }); + }); \ No newline at end of file