diff --git a/client/src/app/site/agenda/agenda-routing.module.ts b/client/src/app/site/agenda/agenda-routing.module.ts index f4d341122..8ac7a8b4f 100644 --- a/client/src/app/site/agenda/agenda-routing.module.ts +++ b/client/src/app/site/agenda/agenda-routing.module.ts @@ -3,13 +3,15 @@ import { Routes, RouterModule } from '@angular/router'; import { AgendaImportListComponent } from './components/agenda-import-list/agenda-import-list.component'; import { AgendaListComponent } from './components/agenda-list/agenda-list.component'; -import { TopicDetailComponent } from './components/topic-detail/topic-detail.component'; +import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component'; import { SpeakerListComponent } from './components/speaker-list/speaker-list.component'; +import { TopicDetailComponent } from './components/topic-detail/topic-detail.component'; const routes: Routes = [ { path: '', component: AgendaListComponent }, { path: 'import', component: AgendaImportListComponent }, { path: 'topics/new', component: TopicDetailComponent }, + { path: 'sort-agenda', component: AgendaSortComponent }, { path: 'topics/:id', component: TopicDetailComponent }, { path: ':id/speakers', component: SpeakerListComponent } ]; diff --git a/client/src/app/site/agenda/agenda.module.ts b/client/src/app/site/agenda/agenda.module.ts index 2b68d67a8..bc1ac11ee 100644 --- a/client/src/app/site/agenda/agenda.module.ts +++ b/client/src/app/site/agenda/agenda.module.ts @@ -7,6 +7,7 @@ import { ItemInfoDialogComponent } from './components/item-info-dialog/item-info import { AgendaRoutingModule } from './agenda-routing.module'; import { SharedModule } from '../../shared/shared.module'; import { TopicDetailComponent } from './components/topic-detail/topic-detail.component'; +import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component'; /** * AppModule for the agenda and it's children. @@ -14,6 +15,12 @@ import { TopicDetailComponent } from './components/topic-detail/topic-detail.com @NgModule({ imports: [CommonModule, AgendaRoutingModule, SharedModule], entryComponents: [ItemInfoDialogComponent], - declarations: [AgendaListComponent, TopicDetailComponent, ItemInfoDialogComponent, AgendaImportListComponent] + declarations: [ + AgendaListComponent, + TopicDetailComponent, + ItemInfoDialogComponent, + AgendaImportListComponent, + AgendaSortComponent + ] }) export class AgendaModule {} diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html index 75af7c72c..786de9f0c 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html @@ -11,10 +11,9 @@ {{ selectedRows.length }} selected - - + @@ -30,8 +29,10 @@ Topic - check - {{ item.getListTitle() }} +
+ check + {{ item.getListTitle() }} +
@@ -61,7 +62,10 @@ Speakers @@ -102,14 +106,18 @@ format_list_numbered Numbering + - @@ -179,7 +187,10 @@ + + + + diff --git a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.spec.ts b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.spec.ts new file mode 100644 index 000000000..5ff961314 --- /dev/null +++ b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AgendaSortComponent } from './agenda-sort.component'; +import { E2EImportsModule } from 'e2e-imports.module'; + +describe('AgendaSortComponent', () => { + let component: AgendaSortComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [AgendaSortComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AgendaSortComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts new file mode 100644 index 000000000..0daa51c41 --- /dev/null +++ b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { MatSnackBar } from '@angular/material'; + +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; + +import { AgendaRepositoryService } from '../../services/agenda-repository.service'; +import { BaseViewComponent } from '../../../base/base-view'; +import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component'; +import { ViewItem } from '../../models/view-item'; + +/** + * Sort view for the agenda. + */ +@Component({ + selector: 'os-agenda-sort', + templateUrl: './agenda-sort.component.html' +}) +export class AgendaSortComponent extends BaseViewComponent { + /** + * All agendaItems sorted by their virtual weight {@link ViewItem.agendaListWeight} + */ + public itemsObservable: Observable; + + /** + * Emits true for expand and false for collapse. Informs the sorter component about this actions. + */ + public readonly expandCollapse: EventEmitter = new EventEmitter(); + + /** + * Updates the incoming/changing agenda items. + * @param title + * @param translate + * @param matSnackBar + * @param agendaRepo + */ + public constructor( + title: Title, + translate: TranslateService, + matSnackBar: MatSnackBar, + private agendaRepo: AgendaRepositoryService + ) { + super(title, translate, matSnackBar); + this.itemsObservable = this.agendaRepo.getViewModelListObservable(); + } + + /** + * 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 { + this.agendaRepo.sortItems(data).then(null, this.raiseError); + } + + /** + * Fires the expandCollapse event emitter. + * + * @param expand True, if the tree should be expanded. Otherwise collapsed + */ + public expandCollapseAll(expand: boolean): void { + this.expandCollapse.emit(expand); + } +} diff --git a/client/src/app/site/agenda/models/view-item.ts b/client/src/app/site/agenda/models/view-item.ts index 06d0d1342..332a6aa19 100644 --- a/client/src/app/site/agenda/models/view-item.ts +++ b/client/src/app/site/agenda/models/view-item.ts @@ -7,6 +7,19 @@ export class ViewItem extends BaseViewModel { private _item: Item; private _contentObject: AgendaBaseModel; + /** + * virtual weight defined by the order in the agenda tree, representing a shortcut to sorting by + * weight, parent_id and the parents' weight(s) + * TODO will be accurate if the viewMotion is observed via {@link getViewModelListObservable}, else, it will be undefined + */ + public agendaListWeight: number; + + /** + * The amount of parents in the agenda list tree. + * TODO will be accurate if the viewMotion is observed via {@link getViewModelListObservable}, else, it will be undefined + */ + public agendaListLevel: number; + public get item(): Item { return this._item; } @@ -67,6 +80,21 @@ export class ViewItem extends BaseViewModel { return this.item ? this.item.speakers : []; } + /** + * @returns the weight the server assigns to that item. Mostly useful for sorting within + * it's own hierarchy level (items sharing a parent) + */ + public get weight(): number { + return this.item ? this.item.weight : null; + } + + /** + * @returns the parent's id of that item (0 if no parent is set). + */ + public get parent_id(): number { + return this.item ? this.item.parent_id : null; + } + public constructor(item: Item, contentObject: AgendaBaseModel) { super(); this._item = item; diff --git a/client/src/app/site/agenda/services/agenda-repository.service.ts b/client/src/app/site/agenda/services/agenda-repository.service.ts index cd402bb63..40455ec57 100644 --- a/client/src/app/site/agenda/services/agenda-repository.service.ts +++ b/client/src/app/site/agenda/services/agenda-repository.service.ts @@ -1,21 +1,23 @@ import { Injectable } from '@angular/core'; -import { map } from 'rxjs/operators'; +import { tap, map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { BaseRepository } from '../../base/base-repository'; -import { DataStoreService } from '../../../core/services/data-store.service'; -import { Item } from '../../../shared/models/agenda/item'; -import { ViewItem } from '../models/view-item'; import { AgendaBaseModel } from '../../../shared/models/base/agenda-base-model'; import { BaseModel } from '../../../shared/models/base/base-model'; -import { Identifiable } from '../../../shared/models/base/identifiable'; import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service'; -import { ViewSpeaker } from '../models/view-speaker'; -import { Speaker } from 'app/shared/models/agenda/speaker'; -import { User } from 'app/shared/models/users/user'; -import { HttpService } from 'app/core/services/http.service'; import { ConfigService } from 'app/core/services/config.service'; import { DataSendService } from 'app/core/services/data-send.service'; +import { DataStoreService } from '../../../core/services/data-store.service'; +import { HttpService } from 'app/core/services/http.service'; +import { Identifiable } from '../../../shared/models/base/identifiable'; +import { Item } from '../../../shared/models/agenda/item'; +import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component'; +import { Speaker } from 'app/shared/models/agenda/speaker'; +import { User } from 'app/shared/models/users/user'; +import { ViewItem } from '../models/view-item'; +import { ViewSpeaker } from '../models/view-speaker'; +import { TreeService } from 'app/core/services/tree.service'; /** * Repository service for users @@ -34,13 +36,15 @@ export class AgendaRepositoryService extends BaseRepository { * @param mapperService OpenSlides mapping service for collection strings * @param config Read config variables * @param dataSend send models to the server + * @param treeService sort the data according to weight and parents */ public constructor( protected DS: DataStoreService, private httpService: HttpService, mapperService: CollectionStringModelMapperService, private config: ConfigService, - private dataSend: DataSendService + private dataSend: DataSendService, + private treeService: TreeService ) { super(DS, mapperService, Item); } @@ -229,4 +233,37 @@ export class AgendaRepositoryService extends BaseRepository { public getDefaultAgendaVisibility(): Observable { return this.config.get('agenda_new_items_default_visibility').pipe(map(key => +key)); } + + /** + * Sends the changed nodes to the server. + * + * @param data The reordered data from the sorting + */ + public async sortItems(data: OSTreeSortEvent): Promise { + const url = '/rest/agenda/item/sort/'; + await this.httpService.post(url, data); + } + + /** + * Add custom hook into the observables. The ViewItems get a virtual agendaListWeight (a sequential number) + * for the agenda topic order, and a virtual level for the hierarchy in the agenda list tree. Both values can be used + * for sorting and ordering instead of dealing with the sort parent id and weight. + * + * @override + */ + public getViewModelListObservable(): Observable { + return super.getViewModelListObservable().pipe( + tap(items => { + const iterator = this.treeService.traverseItems(items, 'weight', 'parent_id'); + let m: IteratorResult; + let virtualWeightCounter = 0; + while (!(m = iterator.next()).done) { + m.value.agendaListWeight = virtualWeightCounter++; + m.value.agendaListLevel = m.value.parent_id + ? this.getViewModel(m.value.parent_id).agendaListLevel + 1 + : 0; + } + }) + ); + } } diff --git a/client/src/styles.scss b/client/src/styles.scss index 5105ff3b2..9b2da8059 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -560,3 +560,8 @@ button.mat-menu-item.selected { max-width: 50%; } } + +.table-view-list-title { + font-weight: 500; + font-size: 16px; +}