From 80782ccbec9d460ceadeacf6dcb5eb41c7b28ee1 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Thu, 22 Nov 2018 15:14:01 +0100 Subject: [PATCH] =?UTF-8?q?OpenSlides=20=E2=99=A5=20Trees?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/core/services/tree.service.spec.ts | 17 +++ client/src/app/core/services/tree.service.ts | 142 ++++++++++++++++++ .../sorting-tree/sorting-tree.component.ts | 116 ++------------ client/src/app/site/base/base-repository.ts | 16 +- .../call-list/call-list.component.ts | 5 +- .../motion-list/motion-list.component.ts | 4 +- .../app/site/motions/models/view-motion.ts | 10 +- .../services/motion-repository.service.ts | 22 ++- 8 files changed, 220 insertions(+), 112 deletions(-) create mode 100644 client/src/app/core/services/tree.service.spec.ts create mode 100644 client/src/app/core/services/tree.service.ts diff --git a/client/src/app/core/services/tree.service.spec.ts b/client/src/app/core/services/tree.service.spec.ts new file mode 100644 index 000000000..46753f533 --- /dev/null +++ b/client/src/app/core/services/tree.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { TreeService } from './tree.service'; +import { E2EImportsModule } from '../../../e2e-imports.module'; + +describe('TreeService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [TreeService] + }); + }); + + it('should be created', inject([TreeService], (service: TreeService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/client/src/app/core/services/tree.service.ts b/client/src/app/core/services/tree.service.ts new file mode 100644 index 000000000..ace025b55 --- /dev/null +++ b/client/src/app/core/services/tree.service.ts @@ -0,0 +1,142 @@ +import { Injectable } from '@angular/core'; + +import { OpenSlidesComponent } from 'app/openslides.component'; +import { Displayable } from 'app/shared/models/base/displayable'; +import { Identifiable } from 'app/shared/models/base/identifiable'; + +/** + * A representation of nodes in our tree. Saves the displayed name, the id, the element and children to build a full tree. + */ +export interface OSTreeNode { + name: string; + id: number; + item: T; + children?: OSTreeNode[]; +} + +/** + * This services handles all operations belonging to trees. It can build trees of plain lists (giving the weight + * and parentId property) and traverse the trees in pre-order. + */ +@Injectable({ + providedIn: 'root' +}) +export class TreeService extends OpenSlidesComponent { + /** + * Yes, a constructor. + */ + public constructor() { + super(); + } + + /** + * Returns the weight casted to a number from a given model. + * + * @param item The model to get the weight from. + * @param key + * @returns the weight of the model + */ + private getAttributeAsNumber(item: T, key: keyof T): number { + return (item[key]) as number; + } + + /** + * Build our representation of a tree node given the model and optional children + * to append to this node. + * + * @param item The model to create a node of. + * @param children Optional children to append to this node. + * @returns The created node. + */ + private buildTreeNode(item: T, children?: OSTreeNode[]): OSTreeNode { + return { + name: item.getTitle(), + id: item.id, + item: item, + children: children + }; + } + + /** + * Builds a tree from the given items on the relations between items with weight and parentId + * + * @param items All items to traverse + * @param weightKey The key giving access to the weight property + * @param parentIdKey The key giving access to the parentId property + * @returns An iterator for all items in the right order. + */ + public makeTree( + items: T[], + weightKey: keyof T, + parentIdKey: keyof T + ): OSTreeNode[] { + // Sort items after their weight + items.sort((a, b) => this.getAttributeAsNumber(a, weightKey) - this.getAttributeAsNumber(b, weightKey)); + // Build a dict with all children (dict-value) to a specific + // item id (dict-key). + const children: { [parendId: number]: T[] } = {}; + items.forEach(model => { + if (model[parentIdKey]) { + const parentId = this.getAttributeAsNumber(model, parentIdKey); + if (children[parentId]) { + children[parentId].push(model); + } else { + children[parentId] = [model]; + } + } + }); + // Recursive function that generates a nested list with all + // items with there children + const getChildren: (_models?: T[]) => OSTreeNode[] = _models => { + if (!_models) { + return; + } + const nodes: OSTreeNode[] = []; + _models.forEach(_model => { + nodes.push(this.buildTreeNode(_model, getChildren(children[_model.id]))); + }); + return nodes; + }; + // Generates the list of root items (with no parents) + const parentItems = items.filter(model => !this.getAttributeAsNumber(model, parentIdKey)); + return getChildren(parentItems); + } + + /** + * Traverses the given tree in pre order. + * + * @param tree The tree to traverse + * @returns An iterator for all items in the right order. + */ + public *traverseTree(tree: OSTreeNode[]): Iterator { + const nodesToVisit = tree.reverse(); + while (nodesToVisit.length > 0) { + const node = nodesToVisit.pop(); + if (node.children) { + node.children.reverse().forEach(n => { + nodesToVisit.push(n); + }); + } + yield node.item; + } + } + + /** + * Traverses items in pre-order givem (implicit) by the weight and parentId. + * + * Just builds the tree with `makeTree` and get the iterator from `traverseTree`. + * + * @param items All items to traverse + * @param weightKey The key giving access to the weight property + * @param parentIdKey The key giving access to the parentId property + * @returns An iterator for all items in the right order. + */ + public traverseItems( + items: T[], + weightKey: keyof T, + parentIdKey: keyof T + ): Iterator { + const tree = this.makeTree(items, weightKey, parentIdKey); + return this.traverseTree(tree); + } +} diff --git a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts index 5adf12d4d..3af6db442 100644 --- a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts +++ b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts @@ -7,24 +7,16 @@ import { Subscription, Observable } from 'rxjs'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { Displayable } from 'app/shared/models/base/displayable'; - -/** - * An representation of our nodes. Saves the displayed name, the id and children to build a full tree. - */ -interface OSTreeNode { - name: string; - id: number; - children?: OSTreeNode[]; -} +import { OSTreeNode, TreeService } from 'app/core/services/tree.service'; /** * The data representation for the sort event. */ -export interface OSTreeSortEvent { +export interface OSTreeSortEvent { /** * Gives all nodes to be inserted below the parent_id. */ - nodes: OSTreeNode[]; + nodes: OSTreeNode[]; /** * Provides the parent id for the nodes array. Do not provide it, if it's the @@ -62,8 +54,8 @@ export class SortingTreeComponent implemen if (this.modelSubscription) { this.modelSubscription.unsubscribe(); } - this.modelSubscription = models.pipe(auditTime(100)).subscribe(m => { - this.nodes = this.makeTree(m); + this.modelSubscription = models.pipe(auditTime(10)).subscribe(items => { + this.nodes = this.treeService.makeTree(items, this.weightKey, this.parentIdKey); setTimeout(() => this.tree.treeModel.expandAll()); }); } @@ -93,7 +85,7 @@ export class SortingTreeComponent implemen * sorted part of the tree. */ @Output() - public readonly sort = new EventEmitter(); + public readonly sort = new EventEmitter>(); /** * Options for the tree. As a default drag and drop is allowed. @@ -112,12 +104,12 @@ export class SortingTreeComponent implemen /** * This is our actual tree represented by our own nodes. */ - public nodes: OSTreeNode[] = []; + public nodes: OSTreeNode[] = []; /** * Constructor. Adds the eventhandler for the drop event to the tree. */ - public constructor() { + public constructor(private treeService: TreeService) { this.treeOptions.actionMapping = { mouse: { drop: this.drop.bind(this) @@ -149,9 +141,13 @@ export class SortingTreeComponent implemen * @param param3 The previous and new position os the node */ private drop(tree: TreeModel, node: TreeNode, $event: any, { from, to }: { from: any; to: any }): void { - // check if dropped itself - if (from.id === to.parent.id) { - return; + // check if dropped itself by going the tree upwards and check, if one of them is the "from"-node. + let parent = to.parent; + while (parent !== null) { + if (from.id === parent.id) { + return; + } + parent = parent.parent; } let parentId; @@ -165,86 +161,4 @@ export class SortingTreeComponent implemen transferArrayItem(fromArray, to.parent.data.children, from.index, to.index); this.sort.emit({ nodes: to.parent.data.children, parent_id: parentId }); } - - /** - * Returns the weight casted to a number from a given model. - * - * @param model The model to get the weight from. - * @returns the weight of the model - */ - private getWeight(model: T): number { - return (model[this.weightKey]) as number; - } - - /** - * Returns the parent id casted to a number from a given model. - * - * @param model The model to get the parent id from. - * @returns the parent id of the model - */ - private getParentId(model: T): number { - return (model[this.parentIdKey]) as number; - } - - /** - * Build our representation of a tree node given the model and optional children - * to append to this node. - * - * @param model The model to create a node of. - * @param children Optional children to append to this node. - * @returns The created node. - */ - private buildTreeNode(model: T, children?: OSTreeNode[]): OSTreeNode { - return { - name: model.getTitle(), - id: model.id, - children: children - }; - } - - /** - * Creates a tree from the given models with their parent and weight properties. - * - * @param models All models to build the tree of - * @returns The first layer of the tree given as an array of nodes, because this tree may not have a single root. - */ - private makeTree(models: T[]): OSTreeNode[] { - // copy references to avoid side effects: - models = models.map(x => x); - - // Sort items after there weight - models.sort((a, b) => this.getWeight(a) - this.getWeight(b)); - - // Build a dict with all children (dict-value) to a specific - // item id (dict-key). - const children: { [parendId: number]: T[] } = {}; - - models.forEach(model => { - if (model[this.parentIdKey]) { - const parentId = this.getParentId(model); - if (children[parentId]) { - children[parentId].push(model); - } else { - children[parentId] = [model]; - } - } - }); - - // Recursive function that generates a nested list with all - // items with there children - const getChildren: (_models?: T[]) => OSTreeNode[] = _models => { - if (!_models) { - return; - } - const nodes: OSTreeNode[] = []; - _models.forEach(_model => { - nodes.push(this.buildTreeNode(_model, getChildren(children[_model.id]))); - }); - return nodes; - }; - - // Generates the list of root items (with no parents) - const parentItems = models.filter(model => !this.getParentId(model)); - return getChildren(parentItems); - } } diff --git a/client/src/app/site/base/base-repository.ts b/client/src/app/site/base/base-repository.ts index 6f330b757..ba7482be8 100644 --- a/client/src/app/site/base/base-repository.ts +++ b/client/src/app/site/base/base-repository.ts @@ -5,6 +5,7 @@ import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model import { CollectionStringModelMapperService } from '../../core/services/collectionStringModelMapper.service'; import { DataStoreService } from '../../core/services/data-store.service'; import { Identifiable } from '../../shared/models/base/identifiable'; +import { auditTime } from 'rxjs/operators'; export abstract class BaseRepository extends OpenSlidesComponent { /** @@ -20,7 +21,7 @@ export abstract class BaseRepository = new BehaviorSubject(null); + protected readonly viewModelListSubject: BehaviorSubject = new BehaviorSubject([]); /** * @@ -42,9 +43,12 @@ export abstract class BaseRepository { this.viewModelStore[model.id] = this.createViewModel(model); + }); + // Update the list and then all models on their own + this.updateViewModelListObservable(); + this.DS.getAll(this.baseModelCtor).forEach((model: M) => { this.updateViewModelObservable(model.id); }); - this.updateViewModelListObservable(); // Could be raise in error if the root injector is not known this.DS.changeObservable.subscribe(model => { @@ -131,10 +135,14 @@ export abstract class BaseRepository { - return this.viewModelListSubject.asObservable(); + return this.viewModelListSubject.asObservable().pipe(auditTime(1)); } /** diff --git a/client/src/app/site/motions/components/call-list/call-list.component.ts b/client/src/app/site/motions/components/call-list/call-list.component.ts index 500a55262..f02842f74 100644 --- a/client/src/app/site/motions/components/call-list/call-list.component.ts +++ b/client/src/app/site/motions/components/call-list/call-list.component.ts @@ -56,8 +56,11 @@ export class CallListComponent extends BaseViewComponent { /** * Handler for the sort event. The data to change is given to * the repo, sending it to the server. + * + * @param data The event data. The representation fits the servers requirements, so it can directly + * be send to the server via the repository. */ - public sort(data: OSTreeSortEvent): void { + public sort(data: OSTreeSortEvent): void { this.motionRepo.sortMotions(data).then(null, this.raiseError); } diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/components/motion-list/motion-list.component.ts index 9def50613..bce0701c8 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.ts @@ -75,8 +75,8 @@ export class MotionListComponent extends ListViewBaseComponent imple this.repo.getViewModelListObservable().subscribe(newMotions => { // TODO: This is for testing purposes. Can be removed with #3963 this.dataSource.data = newMotions.sort((a, b) => { - if (a.weight !== b.weight) { - return a.weight - b.weight; + if (a.callListWeight !== b.callListWeight) { + return a.callListWeight - b.callListWeight; } else { return a.id - b.id; } diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index e9c2f64ec..270588615 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -62,6 +62,12 @@ export class ViewMotion extends BaseViewModel { */ public highlightedLine: number; + /** + * Is set by the repository; this is the order of the flat call list given by + * the properties weight and sort_parent_id + */ + public callListWeight: number; + public get motion(): Motion { return this._motion; } @@ -179,7 +185,7 @@ export class ViewMotion extends BaseViewModel { } public get agendaSpeakerAmount(): number { - return this.item ? this.item.speakerAmount : null + return this.item ? this.item.speakerAmount : null; } public constructor( @@ -189,7 +195,7 @@ export class ViewMotion extends BaseViewModel { supporters?: User[], workflow?: Workflow, state?: WorkflowState, - item?: Item, + item?: Item ) { super(); diff --git a/client/src/app/site/motions/services/motion-repository.service.ts b/client/src/app/site/motions/services/motion-repository.service.ts index b6f7c77d0..097f5ae1c 100644 --- a/client/src/app/site/motions/services/motion-repository.service.ts +++ b/client/src/app/site/motions/services/motion-repository.service.ts @@ -1,5 +1,8 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + import { DataSendService } from '../../../core/services/data-send.service'; import { Motion } from '../../../shared/models/motions/motion'; import { User } from '../../../shared/models/users/user'; @@ -20,6 +23,7 @@ import { CollectionStringModelMapperService } from '../../../core/services/colle import { HttpService } from 'app/core/services/http.service'; import { Item } from 'app/shared/models/agenda/item'; import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component'; +import { TreeService } from 'app/core/services/tree.service'; /** * Repository Services for motions (and potentially categories) @@ -51,7 +55,8 @@ export class MotionRepositoryService extends BaseRepository private dataSend: DataSendService, private httpService: HttpService, private readonly lineNumbering: LinenumberingService, - private readonly diff: DiffService + private readonly diff: DiffService, + private treeService: TreeService ) { super(DS, mapperService, Motion, [Category, User, Workflow, Item]); } @@ -77,6 +82,19 @@ export class MotionRepositoryService extends BaseRepository return new ViewMotion(motion, category, submitters, supporters, workflow, state, item); } + public getViewModelListObservable(): Observable { + return super.getViewModelListObservable().pipe( + tap(motions => { + const iterator = this.treeService.traverseItems(motions, 'weight', 'sort_parent_id'); + let m: IteratorResult; + let virtualWeightCounter = 0; + while (!(m = iterator.next()).done) { + m.value.callListWeight = virtualWeightCounter++; + } + }) + ); + } + /** * Creates a motion * Creates a (real) motion with patched data and delegate it @@ -146,7 +164,7 @@ export class MotionRepositoryService extends BaseRepository * * @param data The reordered data from the sorting */ - public async sortMotions(data: OSTreeSortEvent): Promise { + public async sortMotions(data: OSTreeSortEvent): Promise { const url = '/rest/motions/motion/sort/'; await this.httpService.post(url, data); }