Merge pull request #4188 from MaximilianKrambach/hierarchicAgenda
agenda hierarchies
This commit is contained in:
commit
c52b82e77d
@ -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 }
|
||||
];
|
||||
|
@ -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 {}
|
||||
|
@ -11,7 +11,6 @@
|
||||
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
|
||||
<span>{{ selectedRows.length }} </span><span translate>selected</span>
|
||||
</div>
|
||||
|
||||
</os-head-bar>
|
||||
<mat-drawer-container class="on-transition-fade">
|
||||
<os-sort-filter-bar [filterService]="filterService" (searchFieldChange)="searchFilter($event)"></os-sort-filter-bar>
|
||||
@ -30,8 +29,10 @@
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell>
|
||||
<!-- <mat-cell (click)="onTitleColumn(item)" *matCellDef="let item"> -->
|
||||
<mat-cell (click)="selectItem(item, $event)" *matCellDef="let item">
|
||||
<div [ngStyle]="{'margin-left': item.agendaListLevel * 25 + 'px' }">
|
||||
<span *ngIf="item.closed"> <mat-icon class="done-check">check</mat-icon> </span>
|
||||
<span> {{ item.getListTitle() }} </span>
|
||||
<span class="table-view-list-title">{{ item.getListTitle() }}</span>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
@ -61,7 +62,10 @@
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell>
|
||||
<mat-cell *matCellDef="let item">
|
||||
<button mat-icon-button (click)="onSpeakerIcon(item)">
|
||||
<mat-icon [matBadge]="item.waitingSpeakerAmount > 0 ? item.waitingSpeakerAmount : null" matBadgeColor="accent">
|
||||
<mat-icon
|
||||
[matBadge]="item.waitingSpeakerAmount > 0 ? item.waitingSpeakerAmount : null"
|
||||
matBadgeColor="accent"
|
||||
>
|
||||
mic
|
||||
</mat-icon>
|
||||
</button>
|
||||
@ -102,8 +106,12 @@
|
||||
<mat-icon>format_list_numbered</mat-icon>
|
||||
<span translate>Numbering</span>
|
||||
</button>
|
||||
<button mat-menu-item routerLink="sort-agenda">
|
||||
<mat-icon>sort</mat-icon>
|
||||
<span translate>Sort</span>
|
||||
</button>
|
||||
</div>
|
||||
<button mat-menu-item (click)="csvExportItemList();">
|
||||
<button mat-menu-item (click)="csvExportItemList()">
|
||||
<mat-icon>archive</mat-icon>
|
||||
<span translate>Export as CSV</span>
|
||||
</button>
|
||||
@ -179,7 +187,10 @@
|
||||
|
||||
<!-- List of speakers for mobile -->
|
||||
<button mat-menu-item (click)="onSpeakerIcon(item)" *ngIf="vp.isMobile">
|
||||
<mat-icon [matBadge]="item.waitingSpeakerAmount > 0 ? item.waitingSpeakerAmount : null" matBadgeColor="accent">
|
||||
<mat-icon
|
||||
[matBadge]="item.waitingSpeakerAmount > 0 ? item.waitingSpeakerAmount : null"
|
||||
matBadgeColor="accent"
|
||||
>
|
||||
mic
|
||||
</mat-icon>
|
||||
<span translate>List of speakers</span>
|
||||
|
@ -81,8 +81,9 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Agenda');
|
||||
this.initTable();
|
||||
this.filterService.filter().subscribe(newAgendaItem => {
|
||||
this.dataSource.data = newAgendaItem;
|
||||
this.filterService.filter().subscribe(newAgendaItems => {
|
||||
newAgendaItems.sort((a, b) => a.agendaListWeight - b.agendaListWeight);
|
||||
this.dataSource.data = newAgendaItems;
|
||||
this.checkSelection();
|
||||
});
|
||||
this.config
|
||||
|
@ -0,0 +1,23 @@
|
||||
<os-head-bar [nav]="false">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Sort agenda</h2></div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-card>
|
||||
<div>
|
||||
<span translate>
|
||||
Drag and drop items to change the order of the agenda. Your modification will be saved immediately.
|
||||
</span>
|
||||
</div>
|
||||
<button mat-button (click)="expandCollapseAll(true)">{{ 'Expand all' | translate }}</button>
|
||||
<button mat-button (click)="expandCollapseAll(false)">{{ 'Collapse all' | translate }}</button>
|
||||
<os-sorting-tree
|
||||
#sorter
|
||||
(sort)="sort($event)"
|
||||
parentIdKey="parent_id"
|
||||
weightKey="weight"
|
||||
[modelsObservable]="itemsObservable"
|
||||
[expandCollapseAll]="expandCollapse"
|
||||
>
|
||||
</os-sorting-tree>
|
||||
</mat-card>
|
@ -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<AgendaSortComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@ -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<ViewItem[]>;
|
||||
|
||||
/**
|
||||
* Emits true for expand and false for collapse. Informs the sorter component about this actions.
|
||||
*/
|
||||
public readonly expandCollapse: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
|
||||
/**
|
||||
* 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<ViewItem>): 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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<ViewItem, Item> {
|
||||
* @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<ViewItem, Item> {
|
||||
public getDefaultAgendaVisibility(): Observable<number> {
|
||||
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<ViewItem>): Promise<void> {
|
||||
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<ViewItem[]> {
|
||||
return super.getViewModelListObservable().pipe(
|
||||
tap(items => {
|
||||
const iterator = this.treeService.traverseItems(items, 'weight', 'parent_id');
|
||||
let m: IteratorResult<ViewItem>;
|
||||
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;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -560,3 +560,8 @@ button.mat-menu-item.selected {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.table-view-list-title {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user