Merge pull request #4737 from tsiegleauq/virtual-scroll-components

Add virtual scrolling to tables
This commit is contained in:
Emanuel Schütze 2019-06-14 11:31:09 +02:00 committed by GitHub
commit 184bb17596
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1710 additions and 1819 deletions

View File

@ -145,4 +145,4 @@ matrix:
install: install:
- npm install - npm install
script: script:
- ng build --prod - npm run ng-high-memory build --prod

View File

@ -27,6 +27,7 @@
"dependencies": { "dependencies": {
"@angular/animations": "^7.2.14", "@angular/animations": "^7.2.14",
"@angular/cdk": "^7.3.7", "@angular/cdk": "^7.3.7",
"@angular/cdk-experimental": "^7.3.7",
"@angular/common": "^7.2.14", "@angular/common": "^7.2.14",
"@angular/compiler": "^7.2.14", "@angular/compiler": "^7.2.14",
"@angular/core": "^7.2.14", "@angular/core": "^7.2.14",
@ -41,6 +42,9 @@
"@ngx-pwa/local-storage": "^7.4.1", "@ngx-pwa/local-storage": "^7.4.1",
"@ngx-translate/core": "^11.0.1", "@ngx-translate/core": "^11.0.1",
"@ngx-translate/http-loader": "^4.0.0", "@ngx-translate/http-loader": "^4.0.0",
"@pebula/ngrid": "1.0.0-alpha.20",
"@pebula/ngrid-material": "1.0.0-alpha.20",
"@pebula/utils": "1.0.0-alpha.3",
"@tinymce/tinymce-angular": "^3.0.0", "@tinymce/tinymce-angular": "^3.0.0",
"core-js": "^3.0.1", "core-js": "^3.0.1",
"css-element-queries": "^1.1.1", "css-element-queries": "^1.1.1",
@ -59,7 +63,7 @@
"rxjs": "^6.5.1", "rxjs": "^6.5.1",
"tinymce": "^4.9.2", "tinymce": "^4.9.2",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"zone.js": "^0.9.0" "zone.js": "~0.8.26"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.13.8", "@angular-devkit/build-angular": "^0.13.8",
@ -86,6 +90,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^1.18.0", "prettier": "^1.18.0",
"protractor": "^5.4.2", "protractor": "^5.4.2",
"resize-observer-polyfill": "^1.5.1",
"source-map-explorer": "^1.7.0", "source-map-explorer": "^1.7.0",
"ts-node": "~8.1.0", "ts-node": "~8.1.0",
"tslint": "~5.16.0", "tslint": "~5.16.0",

View File

@ -35,5 +35,7 @@ os-icon-container {
.content-node { .content-node {
margin: auto 5px; margin: auto 5px;
text-overflow: ellipsis;
overflow: hidden;
} }
} }

View File

@ -0,0 +1,36 @@
<mat-drawer-container class="on-transition-fade" *ngIf="columns && columnSet">
<os-sort-filter-bar
*ngIf="showFilterBar"
[filterCount]="countFilter"
[filterService]="filterService"
[sortService]="sortService"
(searchFieldChange)="searchFilter($event)"
>
</os-sort-filter-bar>
<!-- vScrollFixed="110" -->
<!-- vScrollAuto () -->
<pbl-ngrid
class="pbl-ngrid-cell-ellipsis"
[ngClass]="showFilterBar ? 'virtual-scroll-with-head-bar' : 'virtual-scroll-full-page'"
cellTooltip
showHeader="!showFilterBar"
matCheckboxSelection="selection"
vScrollFixed="110"
[dataSource]="dataSource"
[columns]="columnSet"
[hideColumns]="hiddenColumns"
>
<!-- "row" has the view model -->
<!-- "value" has the property, that was defined in the columnDefinition -->
<!-- "col" has a column reference -->
<!-- Projector column -->
<div *pblNgridCellDef="'projector'; row as viewModel" class="fill">
<os-projector-button class="projector-button" [object]="viewModel"></os-projector-button>
</div>
<!-- Slot transclusion for the individual cells -->
<ng-content select=".cell-slot"></ng-content>
</pbl-ngrid>
</mat-drawer-container>

View File

@ -0,0 +1,5 @@
@import '~assets/styles/tables.scss';
.projector-button {
margin: auto;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ListViewTableComponent } from './list-view-table.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('ListViewTableComponent', () => {
let component: ListViewTableComponent<any, any>;
let fixture: ComponentFixture<ListViewTableComponent<any, any>>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ListViewTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,379 @@
import {
Component,
OnInit,
Input,
ViewChild,
Output,
EventEmitter,
ChangeDetectionStrategy,
ViewEncapsulation,
ChangeDetectorRef
} from '@angular/core';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
import { PblDataSource, columnFactory, PblNgridComponent, createDS } from '@pebula/ngrid';
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
import { Observable } from 'rxjs';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { BaseModel } from 'app/shared/models/base/base-model';
import { PblColumnDefinition, PblNgridColumnSet } from '@pebula/ngrid/lib/table';
import { Permission, OperatorService } from 'app/core/core-services/operator.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewportService } from 'app/core/ui-services/viewport.service';
/**
* To hide columns via restriction
*/
export interface ColumnRestriction {
columnName: string;
permission: Permission;
}
/**
* Powerful list view table component.
*
* Creates a sort-filter-bar and table with virtual scrolling, where projector and multi select is already
* embedded
*
* Takes a repository-service, a sort-service and a filter-service as an input to display data
* Requires multi-select information
* Double binds selected rows
*
* required both columns definition and a transclusion slot using the ".columns" slot as selector
*
* Can inform about changes in the DataSource
*
* !! Due to bugs in Firefox, ALL inputs to os-list-view-table need to be simple objects.
* NO getter, NO return of a function
* If otherwise more logic is required, use `changeDetectionStrategy.OnPush`
* in your component
*
* @example
* ```html
* <os-list-view-table
* [repo]="motionRepo"
* [filterService]="filterService"
* [sortService]="sortService"
* [columns]="motionColumnDefinition"
* [restricted]="restrictedColumns"
* [hiddenInMobile]="['state']"
* [allowProjector]="false"
* [multiSelect]="isMultiSelect"
* scrollKey="motion"
* [(selectedRows)]="selectedRows"
* (dataSourceChange)="onDataSourceChange($event)"
* >
* <div *pblNgridCellDef="'identifier'; row as motion" class="cell-slot">
* {{ motion.identifier }}
* </div>
* </os-list-view-table>
* ```
*/
@Component({
selector: 'os-list-view-table',
templateUrl: './list-view-table.component.html',
styleUrls: ['./list-view-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel> implements OnInit {
/**
* Declare the table
*/
@ViewChild(PblNgridComponent)
private ngrid: PblNgridComponent;
/**
* The required repository
*/
@Input()
public repo: BaseRepository<V, M, any>;
/**
* The currently active sorting service for the list view
*/
@Input()
public sortService: BaseSortListService<V>;
/**
* The currently active filter service for the list view. It is supposed to
* be a FilterListService extendingFilterListService.
*/
@Input()
public filterService: BaseFilterListService<V>;
/**
* Current state of the multi select mode.
*/
@Input()
private multiSelect = false;
/**
* If a Projector column should be shown (at all)
*/
@Input()
private allowProjector = true;
/**
* columns to hide in mobile mode
*/
@Input()
public hiddenInMobile: string[];
/**
* To hide columns for users with insufficient permissions
*/
@Input()
public restricted: ColumnRestriction[];
/**
* An array of currently selected items, upon which multi select actions can be performed
* see {@link selectItem}.
*/
@Input()
private selectedRows: V[];
/**
* Double binding the selected rows
*/
@Output()
private selectedRowsChange = new EventEmitter<V[]>();
/**
* The specific column definition to display in the table
*/
@Input()
public columns: PblColumnDefinition[] = [];
/**
* Key to restore scroll position after navigating
*/
@Input()
public scrollKey: string;
/**
* Wether or not to show the filter bar
*/
@Input()
public showFilterBar = true;
/**
* Inform about changes in the dataSource
*/
@Output()
public dataSourceChange = new EventEmitter<PblDataSource<V>>();
/**
* test data source
*/
public dataSource: PblDataSource<V>;
/**
* Minimal column width
*/
private columnMinWidth = '60px';
/**
* The column set to display in the table
*/
public columnSet: PblNgridColumnSet;
/**
* Check if mobile and required semaphore for change detection
*/
private isMobile: boolean;
/**
* Most, of not all list views require these
*/
private get defaultColumns(): PblColumnDefinition[] {
const columns = [
{
prop: 'selection',
label: '',
width: this.columnMinWidth
}
];
if (this.allowProjector && this.operator.hasPerms('projector.can_manage_projector')) {
columns.push({
prop: 'projector',
label: '',
width: this.columnMinWidth
});
}
return columns;
}
/**
* Gets the amount of filtered data
*/
public get countFilter(): number {
return this.dataSource.source.length;
}
/**
* @returns the repositories `viewModelListObservable`
*/
private get viewModelListObservable(): Observable<V[]> {
return this.repo.getViewModelListObservable();
}
/**
* Define which columns to hide. Uses the input-property
* "hide" to hide individual columns
*/
public get hiddenColumns(): string[] {
let hidden: string[] = [];
if (this.multiSelect) {
hidden.push('projector');
} else {
hidden.push('selection');
}
if (this.isMobile && this.hiddenInMobile && this.hiddenInMobile.length) {
hidden = hidden.concat(this.hiddenInMobile);
}
if (this.restricted && this.restricted.length) {
const restrictedColumns = this.restricted
.filter(restriction => !this.operator.hasPerms(restriction.permission))
.map(restriction => restriction.columnName);
hidden = hidden.concat(restrictedColumns);
}
return hidden;
}
/**
* Yep it's a constructor.
*
* @param store: Access the scroll storage key
*/
public constructor(
private operator: OperatorService,
vp: ViewportService,
private store: StorageService,
private ref: ChangeDetectorRef
) {
vp.isMobileSubject.subscribe(mobile => {
if (mobile !== this.isMobile) {
this.ref.markForCheck();
}
this.isMobile = mobile;
});
}
public ngOnInit(): void {
// Create ans observe dataSource
this.dataSource = createDS<V>()
.onTrigger(() => {
let listObservable: Observable<V[]>;
if (this.filterService && this.sortService) {
// filtering and sorting
this.filterService.initFilters(this.viewModelListObservable);
this.sortService.initSorting(this.filterService.outputObservable);
listObservable = this.sortService.outputObservable;
} else if (this.filterService) {
// only filter service
this.filterService.initFilters(this.viewModelListObservable);
listObservable = this.filterService.outputObservable;
} else if (this.sortService) {
// only sorting
this.sortService.initSorting(this.viewModelListObservable);
listObservable = this.sortService.outputObservable;
} else {
// none of both
listObservable = this.viewModelListObservable;
}
return listObservable;
})
.create();
// inform listening components about changes in the data source
this.dataSource.onSourceChanged.subscribe(() => {
this.dataSourceChange.next(this.dataSource);
this.checkSelection();
});
// data selection
this.dataSource.selection.changed.subscribe(selection => {
this.selectedRows = selection.source.selected;
this.selectedRowsChange.emit(this.selectedRows);
});
// Define the columns. Has to be in the OnInit cause "columns" is slower than
// the constructor of this class
this.columnSet = columnFactory()
.default({ width: this.columnMinWidth, css: 'ngrid-lg' })
.table(...this.defaultColumns, ...this.columns)
.build();
// restore scroll position
if (this.scrollKey) {
this.scrollToPreviousPosition(this.scrollKey);
}
}
/**
* Central search/filter function. Can be extended and overwritten by a filterPredicate.
* Functions for that are usually called 'setFulltextFilter'
*
* @param event the string to search for
*/
public searchFilter(event: string): void {
this.dataSource.setFilter(event, this.ngrid.columnApi.columns);
}
/**
* Loads the scroll-index from the storage
*
* @param key the key of the scroll index
* @returns the scroll index or 0 if not found
*/
public async getScrollIndex(key: string): Promise<number> {
const scrollIndex = await this.store.get<number>(`scroll_${key}`);
return scrollIndex ? scrollIndex : 0;
}
/**
* Automatically scrolls to a stored scroll position
*
* TODO: Only the position will be stored, not the item.
* Changing the filtering and sorting will confuse the order
*
* TODO: getScrollIndex is not supported by virtual scrolling with the `vScrollAuto` directive.
* Furthermore, dynamic assigning the amount of pixels in vScrollFixed
* does not work, tying the tables to the same hight.
*/
public scrollToPreviousPosition(key: string): void {
this.getScrollIndex(key).then(index => {
this.ngrid.viewport.scrollToIndex(index);
});
}
/**
* Checks the array of selected items against the datastore data. This is
* meant to reselect items by their id even if some of their data changed,
* and to remove selected data that don't exist anymore.
* To be called after an update of data. Checks if updated selected items
* are still present in the dataSource, and (re-)selects them. This should
* be called as the observed datasource updates.
*/
protected checkSelection(): void {
if (this.multiSelect) {
const previouslySelectedRows = [];
this.selectedRows.forEach(selectedRow => {
const newRow = this.dataSource.source.find(item => item.id === selectedRow.id);
if (newRow) {
previouslySelectedRows.push(newRow);
}
});
this.dataSource.selection.clear();
this.dataSource.selection.select(...previouslySelectedRows);
}
}
}

View File

@ -1,12 +1,12 @@
<div class="custom-table-header flex-spaced on-transition-fade"> <div class="custom-table-header flex-spaced on-transition-fade">
<div class="filter-count" *ngIf="filterService && showFilterSort"> <!-- Amount of filters -->
<div class="filter-count" *ngIf="filterService">
<span>{{ displayedCount }}&nbsp;</span><span translate>of</span> <span>{{ displayedCount }}&nbsp;</span><span translate>of</span>
<span>&nbsp;{{ totalCount }}</span> <span>&nbsp;{{ totalCount }}</span>
<span *ngIf="extraItemInfo">&nbsp;·&nbsp;{{ extraItemInfo }}</span> <span *ngIf="extraItemInfo">&nbsp;·&nbsp;{{ extraItemInfo }}</span>
</div> </div>
<div class="filter-count" *ngIf="!showFilterSort && itemsVerboseName">
<span>{{ totalCount }}&nbsp;</span><span>{{ itemsVerboseName | translate }}</span> <!-- Current filters -->
</div>
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount"> <div class="current-filters" *ngIf="filterService && filterService.activeFilterCount">
<div><span translate>Active filters</span>:&nbsp;</div> <div><span translate>Active filters</span>:&nbsp;</div>
<div> <div>
@ -22,12 +22,11 @@
</button> </button>
</div> </div>
</div> </div>
<div>
<span class="extra-controls-wrapper">
<ng-content select=".extra-controls-slot"></ng-content>
</span>
<button mat-button *ngIf="hasFilters && showFilterSort" (click)="filterMenu.opened ? filterMenu.close() : filterMenu.open()"> <!-- Actions -->
<div class="action-buttons">
<!-- Filter button -->
<button mat-button *ngIf="hasFilters" (click)="filterMenu.opened ? filterMenu.close() : filterMenu.open()">
<span *ngIf="!filterService.activeFilterCount" class="upper" translate> Filter </span> <span *ngIf="!filterService.activeFilterCount" class="upper" translate> Filter </span>
<span *ngIf="filterService.activeFilterCount"> <span *ngIf="filterService.activeFilterCount">
{{ filterService.activeFilterCount }}&nbsp; {{ filterService.activeFilterCount }}&nbsp;
@ -35,12 +34,16 @@
<span *ngIf="filterService.activeFilterCount > 1" class="upper" translate>Filters</span> <span *ngIf="filterService.activeFilterCount > 1" class="upper" translate>Filters</span>
</span> </span>
</button> </button>
<button mat-button *ngIf="vp.isMobile && hasSorting && showFilterSort" (click)="openSortDropDown()">
<!-- Sort Button -->
<button mat-button *ngIf="vp.isMobile && hasSorting" (click)="openSortDropDown()">
<span class="upper" translate>Sort</span> <span class="upper" translate>Sort</span>
</button> </button>
<button mat-button *ngIf="!vp.isMobile && hasSorting && showFilterSort" [matMenuTriggerFor]="menu"> <button mat-button *ngIf="!vp.isMobile && hasSorting" [matMenuTriggerFor]="menu">
<span class="upper" translate>Sort</span> <span class="upper" translate>Sort</span>
</button> </button>
<!-- Search bar -->
<mat-form-field *ngIf="isSearchBar"> <mat-form-field *ngIf="isSearchBar">
<input <input
osAutofocus osAutofocus

View File

@ -5,6 +5,11 @@
cursor: pointer; cursor: pointer;
} }
} }
.action-buttons {
margin-left: auto;
}
span.right-with-margin { span.right-with-margin {
margin-right: 25px; margin-right: 25px;
} }

View File

@ -1,13 +1,16 @@
<ng-container *osPerms="'agenda.can_see_list_of_speakers'"> <ng-container *osPerms="'agenda.can_see_list_of_speakers'">
<ng-container *ngIf="listOfSpeakers"> <ng-container *ngIf="listOfSpeakers">
<button type="button" *ngIf="!menuItem" mat-icon-button [routerLink]="listOfSpeakers.listOfSpeakersUrl" [disabled]="disabled"> <a class="anchor-button" *ngIf="!menuItem" [routerLink]="listOfSpeakersUrl">
<mat-icon <button type="button" mat-icon-button [disabled]="disabled">
[matBadge]="listOfSpeakers.waitingSpeakerAmount > 0 ? listOfSpeakers.waitingSpeakerAmount : null" <mat-icon
matBadgeColor="accent" [matBadge]="listOfSpeakers.waitingSpeakerAmount > 0 ? listOfSpeakers.waitingSpeakerAmount : null"
> matBadgeColor="accent"
mic >
</mat-icon> mic
</button> </mat-icon>
</button>
</a>
<button type="button" *ngIf="menuItem" mat-menu-item [routerLink]="listOfSpeakers.listOfSpeakersUrl"> <button type="button" *ngIf="menuItem" mat-menu-item [routerLink]="listOfSpeakers.listOfSpeakersUrl">
<mat-icon>mic</mat-icon> <mat-icon>mic</mat-icon>
<span translate>List of speakers</span> <span translate>List of speakers</span>

View File

@ -38,6 +38,12 @@ export class SpeakerButtonComponent {
@Input() @Input()
public menuItem = false; public menuItem = false;
public get listOfSpeakersUrl(): string {
if (!this.disabled) {
return this.listOfSpeakers.listOfSpeakersUrl;
}
}
/** /**
* The constructor * The constructor
*/ */

View File

@ -59,6 +59,10 @@ import { PermsDirective } from './directives/perms.directive';
import { DomChangeDirective } from './directives/dom-change.directive'; import { DomChangeDirective } from './directives/dom-change.directive';
import { AutofocusDirective } from './directives/autofocus.directive'; import { AutofocusDirective } from './directives/autofocus.directive';
// PblNgrid. Cleanup Required.
import { PblNgridModule } from '@pebula/ngrid';
import { PblNgridMaterialModule } from '@pebula/ngrid-material';
// components // components
import { HeadBarComponent } from './components/head-bar/head-bar.component'; import { HeadBarComponent } from './components/head-bar/head-bar.component';
import { LegalNoticeContentComponent } from './components/legal-notice-content/legal-notice-content.component'; import { LegalNoticeContentComponent } from './components/legal-notice-content/legal-notice-content.component';
@ -89,6 +93,7 @@ import { GridLayoutComponent } from './components/grid-layout/grid-layout.compon
import { TileComponent } from './components/tile/tile.component'; import { TileComponent } from './components/tile/tile.component';
import { BlockTileComponent } from './components/block-tile/block-tile.component'; import { BlockTileComponent } from './components/block-tile/block-tile.component';
import { IconContainerComponent } from './components/icon-container/icon-container.component'; import { IconContainerComponent } from './components/icon-container/icon-container.component';
import { ListViewTableComponent } from './components/list-view-table/list-view-table.component';
/** /**
* Share Module for all "dumb" components and pipes. * Share Module for all "dumb" components and pipes.
@ -148,7 +153,9 @@ import { IconContainerComponent } from './components/icon-container/icon-contain
FileDropModule, FileDropModule,
EditorModule, EditorModule,
CdkTreeModule, CdkTreeModule,
ScrollingModule ScrollingModule,
PblNgridModule,
PblNgridMaterialModule
], ],
exports: [ exports: [
FormsModule, FormsModule,
@ -219,7 +226,11 @@ import { IconContainerComponent } from './components/icon-container/icon-contain
TileComponent, TileComponent,
BlockTileComponent, BlockTileComponent,
ScrollingModule, ScrollingModule,
IconContainerComponent IconContainerComponent,
SpeakerButtonComponent,
PblNgridModule,
PblNgridMaterialModule,
ListViewTableComponent
], ],
declarations: [ declarations: [
PermsDirective, PermsDirective,
@ -252,7 +263,8 @@ import { IconContainerComponent } from './components/icon-container/icon-contain
GridLayoutComponent, GridLayoutComponent,
TileComponent, TileComponent,
BlockTileComponent, BlockTileComponent,
IconContainerComponent IconContainerComponent,
ListViewTableComponent
], ],
providers: [ providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter }, { provide: DateAdapter, useClass: OpenSlidesDateAdapter },

View File

@ -13,97 +13,74 @@
</div> </div>
</os-head-bar> </os-head-bar>
<mat-drawer-container class="on-transition-fade"> <os-list-view-table
<os-sort-filter-bar [repo]="repo"
[filterCount]="filteredCount" [filterService]="filterService"
[extraItemInfo]="getDurationEndString()" [columns]="tableColumnDefinition"
[filterService]="filterService" [multiSelect]="isMultiSelect"
(searchFieldChange)="searchFilter($event)" [hiddenInMobile]="['info']"
></os-sort-filter-bar> scrollKey="agenda"
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> [(selectedRows)]="selectedRows"
<!-- selector column --> (dataSourceChange)="onDataSourceChange($event)"
<ng-container matColumnDef="selector"> >
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell> <!-- Title column -->
<mat-cell *matCellDef="let item" class="checkbox-cell"> <div *pblNgridCellDef="'title'; row as item; rowContext as rowContext" class="cell-slot fill">
<mat-icon>{{ isSelected(item) ? 'check_circle' : '' }}</mat-icon> <a
</mat-cell> class="detail-link"
</ng-container> (click)="saveScrollIndex('agenda', rowContext.identity)"
[routerLink]="getDetailUrl(item)"
*ngIf="!isMultiSelect"
></a>
<div [ngStyle]="{ 'margin-left': item.level * 25 + 'px' }">
<os-icon-container [icon]="item.closed ? 'check' : null" size="large">
{{ item.getListTitle() }}
</os-icon-container>
</div>
</div>
<!-- Projector column --> <!-- Info Column -->
<ng-container matColumnDef="projector"> <div *pblNgridCellDef="'info'; row as item" class="cell-slot fill clickable" (click)="openEditInfo(item)">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell> <div class="info-col-items">
<mat-cell *matCellDef="let item"> <div *osPerms="'agenda.can_manage'; and: item.verboseType">
<div *ngIf="item.contentObject && !isMultiSelect"> <os-icon-container icon="visibility">{{ item.verboseType | translate }}</os-icon-container>
<os-projector-button [object]="item.contentObject"></os-projector-button> </div>
</div> <div *ngIf="item.duration" class="spacer-top-5">
</mat-cell> <os-icon-container icon="access_time">
</ng-container> {{ durationService.durationToString(item.duration, 'h') }}
</os-icon-container>
</div>
<!-- title column --> <div *ngIf="item.comment" class="spacer-top-5">
<ng-container matColumnDef="title"> <os-icon-container icon="comment">{{ item.comment }}</os-icon-container>
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell> </div>
<mat-cell *matCellDef="let item"> </div>
<div [ngStyle]="{ 'margin-left': item.level * 25 + 'px' }"> </div>
<os-icon-container [icon]="item.closed ? 'check' : null" size="large">{{ item.getListTitle() }}</os-icon-container>
</div>
</mat-cell>
</ng-container>
<!-- Info column --> <!-- Speaker -->
<ng-container matColumnDef="info"> <div *pblNgridCellDef="'speaker'; row as item; rowContext as rowContext" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef mat-sort-header>Info</mat-header-cell> <os-speaker-button [disabled]="isMultiSelect"></os-speaker-button>
<mat-cell (click)="openEditInfo(item, $event)" *matCellDef="let item">
<div class="fill">
<div class="info-col-items">
<div *osPerms="'agenda.can_manage'; and: item.verboseType">
<os-icon-container icon="visibility">{{ item.verboseType | translate }}</os-icon-container>
</div>
<div *ngIf="item.duration" class="spacer-top-5">
<os-icon-container icon="access_time">{{ durationService.durationToString(item.duration, 'h') }}</os-icon-container>
</div>
<div *ngIf="item.comment" class="spacer-top-5">
<os-icon-container icon="comment">{{ item.comment }}</os-icon-container>
</div>
</div>
</div>
</mat-cell>
</ng-container>
<!-- Speakers column --> <os-speaker-button
<ng-container matColumnDef="speakers"> [object]="item.contentObjectData"
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell> [disabled]="isMultiSelect"
<mat-cell *matCellDef="let item"> (click)="saveScrollIndex('agenda', rowContext.identity)"
<os-speaker-button [object]="item.contentObjectData" [disabled]="isMultiSelect"></os-speaker-button> ></os-speaker-button>
</mat-cell> </div>
</ng-container>
<!-- menu --> <!-- Menu -->
<ng-container matColumnDef="menu"> <div *pblNgridCellDef="'menu'; row as item" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef mat-sort-header>Menu</mat-header-cell> <button
<mat-cell *matCellDef="let item"> mat-icon-button
<button [disabled]="isMultiSelect"
mat-icon-button *osPerms="'agenda.can_manage'"
[disabled]="isMultiSelect" [matMenuTriggerFor]="singleItemMenu"
*osPerms="'agenda.can_manage'" (click)="$event.stopPropagation()"
[matMenuTriggerFor]="singleItemMenu" [matMenuTriggerData]="{ item: item }"
(click)="$event.stopPropagation()" >
[matMenuTriggerData]="{ item: item }" <mat-icon>more_vert</mat-icon>
> </button>
<mat-icon>more_vert</mat-icon> </div>
</button> </os-list-view-table>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row
class="lg"
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
(click)="selectItem(row, $event)"
*matRowDef="let row; columns: getColumnDefinition()"
></mat-row>
</mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
</mat-drawer-container>
<mat-menu #agendaMenu="matMenu"> <mat-menu #agendaMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">

View File

@ -1,44 +1,20 @@
@import '~assets/styles/tables.scss'; @import '~assets/styles/tables.scss';
.os-listview-table { .info-col-items {
/** Title */ display: inline-block;
.mat-column-title { white-space: nowrap;
padding-left: 26px;
flex: 2 0 0;
.done-check { font-size: 14px;
margin-right: 10px; .mat-icon {
} display: inline-flex;
} vertical-align: middle;
$icon-size: 18px;
/** Duration */ font-size: $icon-size;
.mat-column-info { height: $icon-size;
flex: 2 0 0; width: $icon-size;
.info-col-items {
display: inline-block;
white-space: nowrap;
font-size: 14px;
.mat-icon {
display: inline-flex;
vertical-align: middle;
$icon-size: 18px;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
}
}
}
/** Speakers indicator */
.mat-column-speakers {
flex: 0 0 50px;
}
/** menu indicator */
.mat-column-menu {
flex: 0 0 50px;
justify-content: flex-end !important;
} }
} }
.done-check {
margin-right: 10px;
}

View File

@ -2,27 +2,28 @@ import { Component, OnInit } from '@angular/core';
import { MatSnackBar, MatDialog } from '@angular/material'; import { MatSnackBar, MatDialog } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { AgendaCsvExportService } from '../../services/agenda-csv-export.service'; import { AgendaCsvExportService } from '../../services/agenda-csv-export.service';
import { AgendaFilterListService } from '../../services/agenda-filter-list.service'; import { AgendaFilterListService } from '../../services/agenda-filter-list.service';
import { AgendaPdfService } from '../../services/agenda-pdf.service'; import { AgendaPdfService } from '../../services/agenda-pdf.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { DurationService } from 'app/core/ui-services/duration.service'; import { DurationService } from 'app/core/ui-services/duration.service';
import { Item } from 'app/shared/models/agenda/item';
import { ItemInfoDialogComponent } from '../item-info-dialog/item-info-dialog.component'; import { ItemInfoDialogComponent } from '../item-info-dialog/item-info-dialog.component';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { ListOfSpeakersRepositoryService } from 'app/core/repositories/agenda/list-of-speakers-repository.service';
import { ListViewBaseComponent } from 'app/site/base/list-view-base'; import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service'; import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewportService } from 'app/core/ui-services/viewport.service';
import { ViewItem } from '../../models/view-item'; import { ViewItem } from '../../models/view-item';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { _ } from 'app/core/translate/translation-marker';
import { StorageService } from 'app/core/core-services/storage.service';
import { ListOfSpeakersRepositoryService } from 'app/core/repositories/agenda/list-of-speakers-repository.service';
import { ViewListOfSpeakers } from '../../models/view-list-of-speakers'; import { ViewListOfSpeakers } from '../../models/view-list-of-speakers';
import { _ } from 'app/core/translate/translation-marker';
/** /**
* List view for the agenda. * List view for the agenda.
@ -32,18 +33,10 @@ import { ViewListOfSpeakers } from '../../models/view-list-of-speakers';
templateUrl: './agenda-list.component.html', templateUrl: './agenda-list.component.html',
styleUrls: ['./agenda-list.component.scss'] styleUrls: ['./agenda-list.component.scss']
}) })
export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, ItemRepositoryService> export class AgendaListComponent extends ListViewBaseComponent<ViewItem> implements OnInit {
implements OnInit {
/** /**
* Determine the display columns in desktop view * Show or hide the numbering button
*/ */
public displayedColumnsDesktop: string[] = ['title', 'info'];
/**
* Determine the display columns in mobile view
*/
public displayedColumnsMobile: string[] = ['title'];
public isNumberingAllowed: boolean; public isNumberingAllowed: boolean;
/** /**
@ -71,6 +64,28 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
getDialogTitle: () => this.translate.instant('Agenda') getDialogTitle: () => this.translate.instant('Agenda')
}; };
/**
* Define the columns to show
*/
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'title',
width: 'auto'
},
{
prop: 'info',
width: '15%'
},
{
prop: 'speaker',
width: this.singleButtonWidth
},
{
prop: 'menu',
width: this.singleButtonWidth
}
];
/** /**
* The usual constructor for components * The usual constructor for components
* @param titleService Setting the browser tab title * @param titleService Setting the browser tab title
@ -98,7 +113,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
private operator: OperatorService, private operator: OperatorService,
protected route: ActivatedRoute, protected route: ActivatedRoute,
private router: Router, private router: Router,
private repo: ItemRepositoryService, public repo: ItemRepositoryService,
private promptService: PromptService, private promptService: PromptService,
private dialog: MatDialog, private dialog: MatDialog,
private config: ConfigService, private config: ConfigService,
@ -110,9 +125,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
private pdfService: PdfDocumentService, private pdfService: PdfDocumentService,
private listOfSpeakersRepo: ListOfSpeakersRepositoryService private listOfSpeakersRepo: ListOfSpeakersRepositoryService
) { ) {
super(titleService, translate, matSnackBar, repo, route, storage, filterService); super(titleService, translate, matSnackBar, storage);
// activate multiSelect mode for this listview
this.canMultiSelect = true; this.canMultiSelect = true;
} }
@ -122,11 +135,9 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Agenda'); super.setTitle('Agenda');
this.initTable();
this.config this.config
.get<boolean>('agenda_enable_numbering') .get<boolean>('agenda_enable_numbering')
.subscribe(autoNumbering => (this.isNumberingAllowed = autoNumbering)); .subscribe(autoNumbering => (this.isNumberingAllowed = autoNumbering));
this.setFulltextFilter();
} }
/** /**
@ -144,9 +155,9 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
* *
* @param item the item that was selected from the list view * @param item the item that was selected from the list view
*/ */
public singleSelectAction(item: ViewItem): void { public getDetailUrl(item: ViewItem): string {
if (item.contentObject) { if (item.contentObject && !this.isMultiSelect) {
this.router.navigate([item.contentObject.getDetailStateURL()]); return item.contentObject.getDetailStateURL();
} }
} }
@ -156,11 +167,10 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
* *
* @param item The view item that was clicked * @param item The view item that was clicked
*/ */
public openEditInfo(item: ViewItem, event: MouseEvent): void { public openEditInfo(item: ViewItem): void {
if (this.isMultiSelect || !this.canManage) { if (this.isMultiSelect || !this.canManage) {
return; return;
} }
event.stopPropagation();
const dialogRef = this.dialog.open(ItemInfoDialogComponent, { const dialogRef = this.dialog.open(ItemInfoDialogComponent, {
width: '400px', width: '400px',
data: item, data: item,
@ -254,33 +264,11 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
} }
} }
/**
* Determine what columns to show
*
* @returns an array of strings with the dialogs to show
*/
public getColumnDefinition(): string[] {
let columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
if (this.operator.hasPerms('agenda.can_see_list_of_speakers')) {
columns = columns.concat(['speakers']);
}
if (this.operator.hasPerms('agenda.can_manage')) {
columns = columns.concat(['menu']);
}
if (this.operator.hasPerms('core.can_manage_projector') && !this.isMultiSelect) {
columns = ['projector'].concat(columns);
}
if (this.isMultiSelect) {
columns = ['selector'].concat(columns);
}
return columns;
}
/** /**
* Export all items as CSV * Export all items as CSV
*/ */
public csvExportItemList(): void { public csvExportItemList(): void {
this.csvExport.exportItemList(this.dataSource.filteredData); this.csvExport.exportItemList(this.dataSource.source);
} }
/** /**
@ -289,7 +277,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
*/ */
public onDownloadPdf(): void { public onDownloadPdf(): void {
const filename = this.translate.instant('Agenda'); const filename = this.translate.instant('Agenda');
this.pdfService.download(this.agendaPdfService.agendaListToDocDef(this.dataSource.filteredData), filename); this.pdfService.download(this.agendaPdfService.agendaListToDocDef(this.dataSource.source), filename);
} }
/** /**
@ -319,20 +307,22 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
/** /**
* Overwrites the dataSource's string filter with a case-insensitive search * Overwrites the dataSource's string filter with a case-insensitive search
* in the item number and title * in the item number and title
*
* TODO: Filter predicates will be missed :(
*/ */
private setFulltextFilter(): void { // private setFulltextFilter(): void {
this.dataSource.filterPredicate = (data, filter) => { // this.dataSource.filterPredicate = (data, filter) => {
if (!data) { // if (!data) {
return false; // return false;
} // }
filter = filter ? filter.toLowerCase() : ''; // filter = filter ? filter.toLowerCase() : '';
return ( // return (
data.itemNumber.toLowerCase().includes(filter) || // data.itemNumber.toLowerCase().includes(filter) ||
data // data
.getListTitle() // .getListTitle()
.toLowerCase() // .toLowerCase()
.includes(filter) // .includes(filter)
); // );
}; // };
} // }
} }

View File

@ -19,117 +19,87 @@
</div> </div>
</os-head-bar> </os-head-bar>
<mat-drawer-container class="on-transition-fade"> <os-list-view-table
<os-sort-filter-bar [repo]="repo"
[filterCount]="filteredCount" [filterService]="filterService"
[filterService]="filterService" [sortService]="sortService"
[sortService]="sortService" [columns]="tableColumnDefinition"
(searchFieldChange)="searchFilter($event)" [multiSelect]="isMultiSelect"
> scrollKey="assignments"
</os-sort-filter-bar> [(selectedRows)]="selectedRows"
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> (dataSourceChange)="onDataSourceChange($event)"
<!-- selector column --> >
<ng-container matColumnDef="selector"> <!-- Title -->
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell"></mat-header-cell> <div *pblNgridCellDef="'title'; row as assignment; rowContext as rowContext" class="cell-slot fill">
<mat-cell *matCellDef="let assignment" class="icon-cell"> <a
<mat-icon>{{ isSelected(assignment) ? 'check_circle' : '' }}</mat-icon> class="detail-link"
</mat-cell> (click)="saveScrollIndex('assignments', rowContext.identity)"
</ng-container> [routerLink]="assignment.id"
*ngIf="!isMultiSelect"
></a>
<div>
{{ assignment.getListTitle() }}
</div>
</div>
<!-- projector column --> <!-- Phase -->
<ng-container matColumnDef="projector"> <div *pblNgridCellDef="'phase'; row as assignment" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell> <mat-chip-list>
<mat-cell *matCellDef="let assignment"> <mat-chip color="primary" selected>{{ assignment.phaseString | translate }}</mat-chip>
<os-projector-button [object]="assignment"></os-projector-button> </mat-chip-list>
</mat-cell> </div>
</ng-container>
<!-- name column --> <!-- Candidates -->
<ng-container matColumnDef="title"> <div *pblNgridCellDef="'candidates'; row as assignment" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell> <mat-chip-list>
<mat-cell *matCellDef="let assignment">{{ assignment.getListTitle() }}</mat-cell> <mat-chip color="accent" selected matTooltip="{{ 'Number of candidates' | translate }}">
</ng-container> {{ assignment.candidateAmount }}
</mat-chip>
</mat-chip-list>
</div>
</os-list-view-table>
<!-- phase column--> <mat-menu #assignmentMenu="matMenu">
<ng-container matColumnDef="phase"> <div *ngIf="!isMultiSelect">
<mat-header-cell *matHeaderCellDef mat-sort-header>Phase</mat-header-cell> <button mat-menu-item *osPerms="'assignment.can_manage'" (click)="toggleMultiSelect()">
<mat-cell *matCellDef="let assignment"> <mat-icon>library_add</mat-icon>
<mat-chip-list> <span translate>Multiselect</span>
<mat-chip color="primary" selected>{{ assignment.phaseString | translate }}</mat-chip> </button>
</mat-chip-list> <button mat-menu-item (click)="downloadAssignmentButton()">
</mat-cell> <mat-icon>archive</mat-icon>
<button mat-menu-item (click)="selectAll()"> <span translate>Export ...</span>
<mat-icon>done_all</mat-icon> </button>
<span translate>Select all</span> </div>
</button>
<button mat-menu-item (click)="deselectAll()">
<mat-icon>clear</mat-icon>
<span translate>Deselect all</span>
</button>
</ng-container>
<!-- candidates column --> <div *ngIf="isMultiSelect">
<ng-container matColumnDef="candidates"> <button mat-menu-item (click)="selectAll()">
<mat-header-cell *matHeaderCellDef mat-sort-header>Candidates</mat-header-cell> <mat-icon>done_all</mat-icon>
<mat-cell *matCellDef="let assignment"> <span translate>Select all</span>
<mat-chip-list> </button>
<mat-chip color="accent" selected matTooltip="{{ 'Number of candidates' | translate }}">{{ assignment.candidateAmount }}</mat-chip> <button mat-menu-item [disabled]="!selectedRows.length" (click)="deselectAll()">
</mat-chip-list> <mat-icon>clear</mat-icon>
</mat-cell> <span translate>Deselect all</span>
</ng-container> </button>
<mat-divider></mat-divider>
<mat-header-row *matHeaderRowDef="getColumnDefintion()"></mat-header-row> <button
<mat-row *osPerms="'assignment.can_manage'"
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''" mat-menu-item
(click)="selectItem(row, $event)" [disabled]="!selectedRows.length"
*matRowDef="let row; columns: getColumnDefintion()" (click)="downloadAssignmentButton(selectedRows)"
> >
</mat-row> <mat-icon>archive</mat-icon>
</mat-table> <span>{{ 'Export selected elections' | translate }}</span>
</button>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator> <mat-divider></mat-divider>
<button
<mat-menu #assignmentMenu="matMenu"> mat-menu-item
<div *ngIf="!isMultiSelect"> class="red-warning-text"
<button mat-menu-item *osPerms="'assignment.can_manage'" (click)="toggleMultiSelect()"> *osPerms="'assignment.can_manage'"
<mat-icon>library_add</mat-icon> [disabled]="!selectedRows.length"
<span translate>Multiselect</span> (click)="deleteSelected()"
</button> >
<button mat-menu-item (click)="downloadAssignmentButton()"> <mat-icon>delete</mat-icon>
<mat-icon>archive</mat-icon> <span translate>Delete</span>
<span translate>Export ...</span> </button>
</button> </div>
</div> </mat-menu>
<div *ngIf="isMultiSelect">
<button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon>
<span translate>Select all</span>
</button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="deselectAll()">
<mat-icon>clear</mat-icon>
<span translate>Deselect all</span>
</button>
<mat-divider></mat-divider>
<button
*osPerms="'assignment.can_manage'"
mat-menu-item
[disabled]="!selectedRows.length"
(click)="downloadAssignmentButton(selectedRows)">
<mat-icon>archive</mat-icon>
<span>{{ 'Export selected elections' | translate }}</span>
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="red-warning-text"
*osPerms="'assignment.can_manage'"
[disabled]="!selectedRows.length"
(click)="deleteSelected()"
>
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
</div>
</mat-menu>
</mat-drawer-container>

View File

@ -1,4 +0,0 @@
/** Title */
.mat-column-title {
padding-left: 10px;
}

View File

@ -4,8 +4,8 @@ import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { Assignment } from 'app/shared/models/assignments/assignment';
import { AssignmentFilterListService } from '../../services/assignment-filter.service'; import { AssignmentFilterListService } from '../../services/assignment-filter.service';
import { AssignmentSortListService } from '../../services/assignment-sort-list.service'; import { AssignmentSortListService } from '../../services/assignment-sort-list.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service'; import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
@ -24,14 +24,31 @@ import { AssignmentPdfExportService } from '../../services/assignment-pdf-export
templateUrl: './assignment-list.component.html', templateUrl: './assignment-list.component.html',
styleUrls: ['./assignment-list.component.scss'] styleUrls: ['./assignment-list.component.scss']
}) })
export class AssignmentListComponent export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment> implements OnInit {
extends ListViewBaseComponent<ViewAssignment, Assignment, AssignmentRepositoryService>
implements OnInit {
/** /**
* The different phases of an assignment. Info is fetched from server * The different phases of an assignment. Info is fetched from server
*/ */
public phaseOptions = AssignmentPhases; public phaseOptions = AssignmentPhases;
/**
* Define the columns to show
*/
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'title',
width: 'auto'
},
{
prop: 'phase',
width: '20%',
minWidth: 180
},
{
prop: 'candidates',
width: this.singleButtonWidth
}
];
/** /**
* Constructor. * Constructor.
* *
@ -61,8 +78,7 @@ export class AssignmentListComponent
private router: Router, private router: Router,
public operator: OperatorService public operator: OperatorService
) { ) {
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService); super(titleService, translate, matSnackBar, storage);
// activate multiSelect mode for this list view
this.canMultiSelect = true; this.canMultiSelect = true;
} }
@ -72,7 +88,6 @@ export class AssignmentListComponent
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Elections'); super.setTitle('Elections');
this.initTable();
} }
/** /**
@ -83,16 +98,6 @@ export class AssignmentListComponent
this.router.navigate(['./new'], { relativeTo: this.route }); this.router.navigate(['./new'], { relativeTo: this.route });
} }
/**
* Action to be performed after a click on a row in the table, if in single select mode.
* Navigates to the corresponding assignment
*
* @param assignment The entry of row clicked
*/
public singleSelectAction(assignment: ViewAssignment): void {
this.router.navigate([assignment.getDetailStateURL()], { relativeTo: this.route });
}
/** /**
* Function to download the assignment list * Function to download the assignment list
* *
@ -115,20 +120,4 @@ export class AssignmentListComponent
} }
} }
} }
/**
* Fetch the column definitions for the data table
*
* @returns a list of string matching the columns
*/
public getColumnDefintion(): string[] {
let list = ['title', 'phase', 'candidates'];
if (this.operator.hasPerms('core.can_manage_projector')) {
list = ['projector'].concat(list);
}
if (this.isMultiSelect) {
list = ['selector'].concat(list);
}
return list;
}
} }

View File

@ -1,28 +1,19 @@
import { MatTableDataSource, MatTable, MatSort, MatPaginator, MatSnackBar, PageEvent } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { ViewChild, Type, OnDestroy } from '@angular/core'; import { OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { PblDataSource, PblColumnDefinition } from '@pebula/ngrid';
import { BaseViewComponent } from './base-view'; import { BaseViewComponent } from './base-view';
import { BaseViewModel, TitleInformation } from './base-view-model'; import { BaseViewModel } from './base-view-model';
import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
import { BaseModel } from 'app/shared/models/base/base-model';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { BaseRepository } from 'app/core/repositories/base-repository';
export abstract class ListViewBaseComponent< export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseViewComponent implements OnDestroy {
V extends BaseViewModel,
M extends BaseModel,
R extends BaseRepository<V, M, TitleInformation>
> extends BaseViewComponent implements OnDestroy {
/** /**
* The data source for a table. Requires to be initialized with a BaseViewModel * The source of the table data, will be filled by an event emitter
*/ */
public dataSource: MatTableDataSource<V>; public dataSource: PblDataSource<V>;
/** /**
* Toggle for enabling the multiSelect mode. Defaults to false (inactive) * Toggle for enabling the multiSelect mode. Defaults to false (inactive)
@ -37,235 +28,52 @@ export abstract class ListViewBaseComponent<
/** /**
* An array of currently selected items, upon which multi select actions can be performed * An array of currently selected items, upon which multi select actions can be performed
* see {@link selectItem}. * see {@link selectItem}.
* Filled using double binding from list-view-tables
*/ */
public selectedRows: V[]; public selectedRows: V[];
/** /**
* Holds the key for the storage. * Force children to have a tableColumnDefinition
* This is by default the component's name.
*/ */
private paginationStorageKey: string; public abstract tableColumnDefinition: PblColumnDefinition[];
/** /**
* Holds the value from local storage with the 'Paginator' key. * NGrid column width for single buttons
*/ */
private paginationStorageObject: { [key: string]: number }; public singleButtonWidth = '40px';
/** /**
* Determine the default page size of paginated list views * @param titleService the title service
*/
public pageSize = [50, 100, 150, 200, 250];
/**
* The table itself
*/
@ViewChild(MatTable)
protected table: MatTable<V>;
/**
* Table paginator
*/
@ViewChild(MatPaginator)
protected paginator: MatPaginator;
/**
* Sorter for a table
*/
@ViewChild(MatSort)
protected sort: MatSort;
/**
* @returns the amount of currently dispalyed items (only showing items that pass all filters)
*/
public get filteredCount(): number {
return this.dataSource.filteredData.length;
}
/**
* @param titleService the title serivce
* @param translate the translate service * @param translate the translate service
* @param matSnackBar showing errors * @param matSnackBar showing errors
* @param viewModelRepo Repository for the view Model. Do NOT rename to "repo"
* @param route Access the current route
* @param storage Access the store * @param storage Access the store
* @param modelFilterListService filter do NOT rename to "filterListService"
* @param modelSortService sorting do NOT rename to "sortService"
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
protected viewModelRepo: R, protected storage?: StorageService
protected route?: ActivatedRoute,
protected storage?: StorageService,
protected modelFilterListService?: BaseFilterListService<V>,
protected modelSortService?: BaseSortListService<V>
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
this.selectedRows = []; this.selectedRows = [];
try {
this.paginationStorageKey = (<Type<any>>route.component).name;
} catch (e) {
this.paginationStorageKey = '';
}
} }
/** /**
* Children need to call this in their init-function. * Detect changes to data source
* Calling these three functions in the constructor of this class
* would be too early, resulting in non-paginated tables
*/
public initTable(): void {
this.dataSource = new MatTableDataSource();
this.dataSource.paginator = this.paginator;
// Set the initial page settings.
if (this.dataSource.paginator) {
this.initializePagination();
this.dataSource.paginator._intl.itemsPerPageLabel = this.translate.instant('items per page');
}
if (this.modelFilterListService && this.modelSortService) {
// filtering and sorting
this.modelFilterListService.initFilters(this.getModelListObservable());
this.modelSortService.initSorting(this.modelFilterListService.outputObservable);
this.subscriptions.push(this.modelSortService.outputObservable.subscribe(data => this.setDataSource(data)));
} else if (this.modelFilterListService) {
// only filter service
this.modelFilterListService.initFilters(this.getModelListObservable());
this.subscriptions.push(
this.modelFilterListService.outputObservable.subscribe(data => this.setDataSource(data))
);
} else if (this.modelSortService) {
// only sorting
this.modelSortService.initSorting(this.getModelListObservable());
this.subscriptions.push(this.modelSortService.outputObservable.subscribe(data => this.setDataSource(data)));
} else {
// none of both
this.subscriptions.push(this.getModelListObservable().subscribe(data => this.setDataSource(data)));
}
}
/**
* Standard filtering function. Sufficient for most list views but can be overwritten
*/
protected getModelListObservable(): Observable<V[]> {
return this.viewModelRepo.getViewModelListObservable();
}
private setDataSource(data: V[]): void {
// the dataArray needs to be cleared (since angular 7)
// changes are not detected properly anymore
this.dataSource.data = [];
this.dataSource.data = data;
this.checkSelection();
}
public onSortButton(itemProperty: string): void {
let newOrder: 'asc' | 'desc' = 'asc';
if (itemProperty === this.sort.active) {
newOrder = this.sort.direction === 'asc' ? 'desc' : 'asc';
}
const newSort = {
disableClear: true,
id: itemProperty,
start: newOrder
};
this.sort.sort(newSort);
}
public onFilterData(filteredDataSource: MatTableDataSource<V>): void {
this.dataSource = filteredDataSource;
this.dataSource.paginator = this.paginator;
}
/**
* Central search/filter function. Can be extended and overwritten by a filterPredicate.
* Functions for that are usually called 'setFulltextFilter'
* *
* @param event the string to search for * @param newDataSource
*/ */
public searchFilter(event: string): void { public onDataSourceChange(newDataSource: PblDataSource<V>): void {
this.dataSource.filter = event; this.dataSource = newDataSource;
} }
/**
* Initialize the settings for the paginator in every list view.
*/
private async initializePagination(): Promise<void> {
// If the storage is not available - like in history mode - do nothing.
if (this.storage) {
this.paginationStorageObject = (await this.storage.get('Pagination')) || {};
// Set the number of items per page -- by default to 25.
this.paginator.pageSize = this.paginationStorageObject[this.paginationStorageKey] || 25;
// Subscription to page change events, like size, index.
this.subscriptions.push(
this.paginator.page.subscribe((event: PageEvent) => {
this.setPageSettings(event.pageSize);
})
);
}
}
/**
* Function to set the new selected page size in the browser's local storage.
*
* @param size is the new page size.
*/
public async setPageSettings(size: number): Promise<void> {
if (this.paginationStorageObject) {
this.paginationStorageObject[this.paginationStorageKey] = size;
await this.storage.set('Pagination', this.paginationStorageObject);
}
}
/**
* Default click action on selecting an item. In multiselect modus,
* this just adds/removes from a selection, else it performs a {@link singleSelectAction}
* @param row The clicked row's {@link ViewModel}
* @param event The Mouse event
*/
public selectItem(row: V, event: MouseEvent): void {
if (this.isMultiSelect) {
event.stopPropagation();
const idx = this.selectedRows.indexOf(row);
if (idx < 0) {
this.selectedRows.push(row);
} else {
this.selectedRows.splice(idx, 1);
}
} else {
event.stopPropagation();
this.singleSelectAction(row);
}
}
/**
* row clicks that should be ignored.
* Required for buttons or check boxes in tables
*
* @param event click event
*/
public ignoreClick(event: MouseEvent): void {
if (!this.isMultiSelect) {
event.stopPropagation();
}
}
/**
* Method to perform an action on click on a row, if not in MultiSelect Modus.
* Should be overridden by implementations. Currently there is no default action.
* @param row a ViewModel
*/
public singleSelectAction(row: V): void {}
/** /**
* enables/disables the multiSelect Mode * enables/disables the multiSelect Mode
*/ */
public toggleMultiSelect(): void { public toggleMultiSelect(): void {
if (!this.canMultiSelect || this.isMultiSelect) { if (!this.canMultiSelect || this.isMultiSelect) {
this._multiSelectMode = false; this._multiSelectMode = false;
this.clearSelection(); this.deselectAll();
} else { } else {
this._multiSelectMode = true; this._multiSelectMode = true;
} }
@ -275,11 +83,16 @@ export abstract class ListViewBaseComponent<
* Select all files in the current data source * Select all files in the current data source
*/ */
public selectAll(): void { public selectAll(): void {
this.selectedRows = this.dataSource.filteredData; this.dataSource.selection.select(...this.dataSource.source);
} }
/**
* Handler to quickly unselect all items.
*/
public deselectAll(): void { public deselectAll(): void {
this.selectedRows = []; if (this.dataSource) {
this.dataSource.selection.clear();
}
} }
/** /**
@ -290,39 +103,12 @@ export abstract class ListViewBaseComponent<
} }
/** /**
* checks if a row is currently selected in the multiSelect modus. * Saves the scroll index in the storage
* @param item The row's entry *
* @param key
* @param index
*/ */
public isSelected(item: V): boolean { public saveScrollIndex(key: string, index: number): void {
if (!this._multiSelectMode) { this.storage.set(`scroll_${key}`, index);
return false;
}
return this.selectedRows.indexOf(item) >= 0;
}
/**
* Handler to quickly unselect all items.
*/
public clearSelection(): void {
this.selectedRows = [];
}
/**
* Checks the array of selected items against the datastore data. This is
* meant to reselect items by their id even if some of their data changed,
* and to remove selected data that don't exist anymore.
* To be called after an update of data. Checks if updated selected items
* are still present in the dataSource, and (re-)selects them. This should
* be called as the observed datasource updates.
*/
protected checkSelection(): void {
const newSelection = [];
this.selectedRows.forEach(selectedrow => {
const newrow = this.dataSource.filteredData.find(item => item.id === selectedrow.id);
if (newrow) {
newSelection.push(newrow);
}
});
this.selectedRows = newSelection;
} }
} }

View File

@ -1,25 +1,3 @@
.mat-table {
/** Time */
.mat-column-time {
flex: 1 0 50px;
}
/** Element */
.mat-column-element {
flex: 3 0 50px;
}
/** Info */
.mat-column-info {
flex: 1 0 50px;
}
/** User */
.mat-column-user {
flex: 1 0 50px;
}
}
.no-info { .no-info {
font-style: italic; font-style: italic;
color: slategray; // TODO: Colors per theme color: slategray; // TODO: Colors per theme

View File

@ -1,8 +1,4 @@
<os-head-bar <os-head-bar [mainButton]="canUploadFiles" [multiSelectMode]="isMultiSelect" (mainEvent)="onMainEvent()">
[mainButton]="canUploadFiles"
[multiSelectMode]="isMultiSelect"
(mainEvent)="onMainEvent()"
>
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2 translate>Files</h2> <h2 translate>Files</h2>
@ -14,6 +10,7 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</div> </div>
<!-- Multiselect info --> <!-- Multiselect info -->
<div *ngIf="this.isMultiSelect" class="central-info-slot"> <div *ngIf="this.isMultiSelect" class="central-info-slot">
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button> <button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
@ -21,99 +18,61 @@
</div> </div>
</os-head-bar> </os-head-bar>
<mat-drawer-container class="on-transition-fade"> <os-list-view-table
<os-sort-filter-bar [repo]="repo"
[filterCount]="filteredCount" [filterService]="filterService"
[sortService]="sortService" [sortService]="sortService"
[filterService]="filterService" [columns]="tableColumnDefinition"
(searchFieldChange)="searchFilter($event)" [multiSelect]="isMultiSelect"
> scrollKey="user"
</os-sort-filter-bar> [(selectedRows)]="selectedRows"
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> (dataSourceChange)="onDataSourceChange($event)"
<!-- Selector Column --> >
<ng-container matColumnDef="selector"> <!-- File title column -->
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell"></mat-header-cell> <div *pblNgridCellDef="'title'; row as file" class="cell-slot fill">
<mat-cell *matCellDef="let item" class="icon-cell" (click)="selectItem(item, $event)"> <a class="detail-link" [routerLink]="file.downloadUrl" target="_blank" *ngIf="!isMultiSelect"></a>
<mat-icon>{{ isSelected(item) ? 'check_circle' : '' }}</mat-icon> <span *ngIf="file.is_hidden">
</mat-cell> <mat-icon matTooltip="{{ 'is hidden' | translate }}">lock</mat-icon>
</ng-container> &nbsp;
</span>
<div>
{{ file.title }}
</div>
</div>
<!-- Projector column --> <!-- Info column -->
<ng-container matColumnDef="projector"> <div *pblNgridCellDef="'info'; row as file" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell> <div class="file-info-cell">
<mat-cell *matCellDef="let file"> <os-icon-container icon="insert_drive_file">{{ file.type }}</os-icon-container>
<os-projector-button *ngIf="file.isProjectable()" [object]="file"></os-projector-button> <os-icon-container icon="data_usage">{{ file.size }}</os-icon-container>
</mat-cell> </div>
</ng-container> </div>
<!-- Filename --> <!-- Indicator column -->
<ng-container matColumnDef="title"> <div *pblNgridCellDef="'indicator'; row as file" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> <div
<mat-cell *matCellDef="let file"> *ngIf="getFileSettings(file).length > 0"
<span *ngIf="file.is_hidden"> [matMenuTriggerFor]="singleFileMenu"
<mat-icon matTooltip="{{ 'is hidden' | translate }}">lock</mat-icon> [matMenuTriggerData]="{ file: file }"
&nbsp; [matTooltip]="formatIndicatorTooltip(file)"
</span> >
{{ file.title }}</mat-cell <mat-icon *ngIf="file.isFont()">text_fields</mat-icon>
> <mat-icon *ngIf="file.isImage()">insert_photo</mat-icon>
</ng-container> </div>
</div>
<!-- Info --> <!-- Menu column -->
<ng-container matColumnDef="info"> <div *pblNgridCellDef="'menu'; row as file" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell> <button
<mat-cell *matCellDef="let file"> mat-icon-button
<div class="file-info-cell"> [matMenuTriggerFor]="singleFileMenu"
<os-icon-container icon="insert_drive_file">{{ file.type }}</os-icon-container> [matMenuTriggerData]="{ file: file }"
<os-icon-container icon="data_usage">{{ file.size }}</os-icon-container> [disabled]="isMultiSelect"
</div> >
</mat-cell> <mat-icon>more_vert</mat-icon>
</ng-container> </button>
</div>
<!-- indicator --> </os-list-view-table>
<ng-container matColumnDef="indicator">
<mat-header-cell *matHeaderCellDef mat-sort-header>Indicator</mat-header-cell>
<mat-cell *matCellDef="let file">
<!-- check if the file is managed -->
<div
*ngIf="getFileSettings(file).length > 0"
[matMenuTriggerFor]="singleFileMenu"
(click)="$event.stopPropagation()"
[matMenuTriggerData]="{ file: file }"
[matTooltip]="formatIndicatorTooltip(file)"
>
<mat-icon *ngIf="file.isFont()">text_fields</mat-icon>
<mat-icon *ngIf="file.isImage()">insert_photo</mat-icon>
</div>
</mat-cell>
</ng-container>
<!-- menu -->
<ng-container matColumnDef="menu">
<mat-header-cell *matHeaderCellDef mat-sort-header>Menu</mat-header-cell>
<mat-cell *matCellDef="let file">
<button
mat-icon-button
[matMenuTriggerFor]="singleFileMenu"
(click)="$event.stopPropagation()"
[matMenuTriggerData]="{ file: file }"
[disabled]="isMultiSelect"
>
<mat-icon>more_vert</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row
*matRowDef="let row; columns: getColumnDefinition()"
(click)="selectItem(row, $event)"
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
></mat-row>
</mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
</mat-drawer-container>
<!-- Template for the managing buttons --> <!-- Template for the managing buttons -->
<ng-template #manageButton let-file="file" let-action="action"> <ng-template #manageButton let-file="file" let-action="action">
@ -189,11 +148,7 @@
<ng-template #fileEditDialog> <ng-template #fileEditDialog>
<h1 mat-dialog-title>{{ 'Edit details for' | translate }}</h1> <h1 mat-dialog-title>{{ 'Edit details for' | translate }}</h1>
<div class="os-form-card-mobile" mat-dialog-content> <div class="os-form-card-mobile" mat-dialog-content>
<form <form class="edit-file-form" [formGroup]="fileEditForm" (keydown)="keyDownFunction($event)">
class="edit-file-form"
[formGroup]="fileEditForm"
(keydown)="keyDownFunction($event)"
>
<mat-form-field> <mat-form-field>
<input <input
type="text" type="text"

View File

@ -1,33 +1,5 @@
@import '~assets/styles/tables.scss'; @import '~assets/styles/tables.scss';
.os-listview-table {
/** Projector button **/
.mat-column-projector {
padding-right: 15px;
}
/** Title */
.mat-column-title {
flex: 2 0 50px;
}
/** Info */
.mat-column-info {
width: 100%;
flex: 1 0 40px;
}
/** Indicator */
.mat-column-indicator {
flex: 1 0 30px;
}
/** Menu */
.mat-column-menu {
flex: 0 0 30px;
}
}
// multi line tooltip // multi line tooltip
::ng-deep .mat-tooltip { ::ng-deep .mat-tooltip {
white-space: pre-line !important; white-space: pre-line !important;

View File

@ -5,13 +5,13 @@ import { Title } from '@angular/platform-browser';
import { MatSnackBar, MatDialog } from '@angular/material'; import { MatSnackBar, MatDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ListViewBaseComponent } from '../../../base/list-view-base'; import { ListViewBaseComponent } from '../../../base/list-view-base';
import { ViewMediafile } from '../../models/view-mediafile'; import { ViewMediafile } from '../../models/view-mediafile';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service'; import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
import { MediaManageService } from 'app/core/ui-services/media-manage.service'; import { MediaManageService } from 'app/core/ui-services/media-manage.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { MediafileFilterListService } from '../../services/mediafile-filter.service'; import { MediafileFilterListService } from '../../services/mediafile-filter.service';
import { MediafilesSortListService } from '../../services/mediafiles-sort-list.service'; import { MediafilesSortListService } from '../../services/mediafiles-sort-list.service';
import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewportService } from 'app/core/ui-services/viewport.service';
@ -26,8 +26,7 @@ import { StorageService } from 'app/core/core-services/storage.service';
templateUrl: './mediafile-list.component.html', templateUrl: './mediafile-list.component.html',
styleUrls: ['./mediafile-list.component.scss'] styleUrls: ['./mediafile-list.component.scss']
}) })
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile, Mediafile, MediafileRepositoryService> export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile> implements OnInit {
implements OnInit {
/** /**
* Holds the actions for logos. Updated via an observable * Holds the actions for logos. Updated via an observable
*/ */
@ -38,16 +37,6 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
*/ */
public fontActions: string[]; public fontActions: string[];
/**
* Columns to display in Mediafile table when desktop view is available
*/
public displayedColumnsDesktop: string[] = ['title', 'info', 'indicator'];
/**
* Columns to display in Mediafile table when mobile view is available
*/
public displayedColumnsMobile: string[] = ['title'];
/** /**
* Show or hide the edit mode * Show or hide the edit mode
*/ */
@ -84,6 +73,28 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
@ViewChild('fileEditDialog') @ViewChild('fileEditDialog')
public fileEditDialog: TemplateRef<string>; public fileEditDialog: TemplateRef<string>;
/**
* Define the columns to show
*/
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'title',
width: 'auto'
},
{
prop: 'info',
width: '20%'
},
{
prop: 'indicator',
width: this.singleButtonWidth
},
{
prop: 'menu',
width: this.singleButtonWidth
}
];
/** /**
* Constructs the component * Constructs the component
* *
@ -104,10 +115,10 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
titleService: Title, titleService: Title,
protected translate: TranslateService, protected translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
route: ActivatedRoute, private route: ActivatedRoute,
storage: StorageService, storage: StorageService,
private router: Router, private router: Router,
private repo: MediafileRepositoryService, public repo: MediafileRepositoryService,
private mediaManage: MediaManageService, private mediaManage: MediaManageService,
private promptService: PromptService, private promptService: PromptService,
public vp: ViewportService, public vp: ViewportService,
@ -117,9 +128,7 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
private dialog: MatDialog, private dialog: MatDialog,
private fb: FormBuilder private fb: FormBuilder
) { ) {
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService); super(titleService, translate, matSnackBar, storage);
// enables multiSelection for this listView
this.canMultiSelect = true; this.canMultiSelect = true;
} }
@ -129,7 +138,6 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Files'); super.setTitle('Files');
this.initTable();
// Observe the logo actions // Observe the logo actions
this.mediaManage.getLogoActions().subscribe(action => { this.mediaManage.getLogoActions().subscribe(action => {
@ -140,7 +148,6 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
this.mediaManage.getFontActions().subscribe(action => { this.mediaManage.getFontActions().subscribe(action => {
this.fontActions = action; this.fontActions = action;
}); });
this.setFulltextFilter();
} }
/** /**
@ -284,34 +291,6 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
this.mediaManage.setAs(file, action); this.mediaManage.setAs(file, action);
} }
/**
* Uses the ViewportService to determine which column definition to use
*
* @returns the column definition for the screen size
*/
public getColumnDefinition(): string[] {
let columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
if (this.operator.hasPerms('core.can_manage_projector') && !this.isMultiSelect) {
columns = ['projector'].concat(columns);
}
if (this.isMultiSelect) {
columns = ['selector'].concat(columns);
}
if (this.canEdit) {
columns = columns.concat(['menu']);
}
return columns;
}
/**
* Directly downloads a mediafile
*
* @param file the select file to download
*/
public singleSelectAction(file: ViewMediafile): void {
window.open(file.downloadUrl);
}
/** /**
* Clicking escape while in editFileForm should deactivate edit mode. * Clicking escape while in editFileForm should deactivate edit mode.
* *
@ -326,14 +305,16 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
/** /**
* Overwrites the dataSource's string filter with a case-insensitive search * Overwrites the dataSource's string filter with a case-insensitive search
* in the file name property * in the file name property
*
* TODO: Filter predicates will be missed :(
*/ */
private setFulltextFilter(): void { // private setFulltextFilter(): void {
this.dataSource.filterPredicate = (data, filter) => { // this.dataSource.filterPredicate = (data, filter) => {
if (!data || !data.title) { // if (!data || !data.title) {
return false; // return false;
} // }
filter = filter ? filter.toLowerCase() : ''; // filter = filter ? filter.toLowerCase() : '';
return data.title.toLowerCase().indexOf(filter) >= 0; // return data.title.toLowerCase().indexOf(filter) >= 0;
}; // };
} // }
} }

View File

@ -1,17 +1,14 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar, MatTableDataSource } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { Category } from 'app/shared/models/motions/category';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service'; import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { ViewCategory } from 'app/site/motions/models/view-category'; import { ViewCategory } from 'app/site/motions/models/view-category';
import { BaseViewComponent } from 'app/site/base/base-view';
/** /**
* Table for categories * Table for categories
@ -21,13 +18,17 @@ import { ViewCategory } from 'app/site/motions/models/view-category';
templateUrl: './category-list.component.html', templateUrl: './category-list.component.html',
styleUrls: ['./category-list.component.scss'] styleUrls: ['./category-list.component.scss']
}) })
export class CategoryListComponent extends ListViewBaseComponent<ViewCategory, Category, CategoryRepositoryService> export class CategoryListComponent extends BaseViewComponent implements OnInit {
implements OnInit {
/** /**
* Holds the create form * Holds the create form
*/ */
public createForm: FormGroup; public createForm: FormGroup;
/**
* Table data Source
*/
public dataSource: MatTableDataSource<ViewCategory>;
/** /**
* Flag, if the creation panel is open * Flag, if the creation panel is open
*/ */
@ -51,20 +52,17 @@ export class CategoryListComponent extends ListViewBaseComponent<ViewCategory, C
* @param storage * @param storage
* @param repo * @param repo
* @param formBuilder * @param formBuilder
* @param promptService
* @param operator * @param operator
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService,
private repo: CategoryRepositoryService, private repo: CategoryRepositoryService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private operator: OperatorService private operator: OperatorService
) { ) {
super(titleService, translate, matSnackBar, repo, route, storage); super(titleService, translate, matSnackBar);
this.createForm = this.formBuilder.group({ this.createForm = this.formBuilder.group({
prefix: [''], prefix: [''],
@ -78,7 +76,13 @@ export class CategoryListComponent extends ListViewBaseComponent<ViewCategory, C
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Categories'); super.setTitle('Categories');
this.initTable();
this.dataSource = new MatTableDataSource();
this.repo.getViewModelListObservable().subscribe(viewCategories => {
if (viewCategories && viewCategories.length && this.dataSource) {
this.dataSource.data = viewCategories;
}
});
} }
/** /**

View File

@ -27,67 +27,57 @@
<span translate>Follow recommendations for all motions</span> <span translate>Follow recommendations for all motions</span>
</button> </button>
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource"> <pbl-ngrid
<!-- title column --> class="block-detail-table"
<ng-container matColumnDef="title"> cellTooltip
<mat-header-cell *matHeaderCellDef> <span translate>Motion</span> </mat-header-cell> showHeader="true"
<mat-cell *matCellDef="let motion"> vScrollFixed="80"
{{ motion.getTitle() }} [dataSource]="dataSource"
</mat-cell> [columns]="columnSet"
</ng-container> >
<!-- Title column -->
<div
*pblNgridCellDef="'title'; row as motion; rowContext as rowContext"
class="cell-slot fill motion-block-title"
>
<a class="detail-link" [routerLink]="motion.getDetailStateURL()" *ngIf="!isMultiSelect"></a>
<span>{{ motion.getTitle() }}</span>
</div>
<!-- state column --> <!-- State column -->
<ng-container matColumnDef="state"> <div *pblNgridCellDef="'state'; row as motion" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef> <span translate>State</span> </mat-header-cell> <div class="chip-container">
<mat-cell class="chip-container" *matCellDef="let motion">
<mat-basic-chip disableRipple [ngClass]="motion.stateCssColor"> <mat-basic-chip disableRipple [ngClass]="motion.stateCssColor">
{{ getStateLabel(motion) }} {{ getStateLabel(motion) }}
</mat-basic-chip> </mat-basic-chip>
</mat-cell> </div>
</ng-container> </div>
<!-- Recommendation column --> <!-- Recommendation column -->
<ng-container matColumnDef="recommendation"> <div *pblNgridCellDef="'recommendation'; row as motion" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef> <span translate>Recommendation</span> </mat-header-cell> <mat-basic-chip *ngIf="!!motion.recommendation" disableRipple class="bluegrey">
<mat-cell class="chip-container" *matCellDef="let motion"> {{ getRecommendationLabel(motion) }}
<mat-basic-chip *ngIf="motion.recommendation" disableRipple class="bluegrey"> </mat-basic-chip>
{{ getRecommendationLabel(motion) }} </div>
</mat-basic-chip>
</mat-cell>
</ng-container>
<!-- Remove motion column --> <!-- Remove from block column -->
<ng-container matColumnDef="remove"> <div *pblNgridCellDef="'remove'; row as motion" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef></mat-header-cell> <button
<mat-cell *matCellDef="let motion"> type="button"
<button mat-icon-button
type="button" color="warn"
mat-icon-button matTooltip="{{ 'Remove from motion block' | translate }}"
color="warn" (click)="onRemoveMotionButton(motion)"
matTooltip="{{ 'Remove from motion block' | translate }}" >
(click)="onRemoveMotionButton(motion)" <mat-icon>close</mat-icon>
> </button>
<mat-icon>close</mat-icon> </div>
</button> </pbl-ngrid>
</mat-cell>
</ng-container>
<!-- Anchor column to open the separate tab -->
<ng-container matColumnDef="anchor">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let motion">
<a [routerLink]="motion.getDetailStateURL()"></a>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row *matRowDef="let row; columns: getColumnDefinition()"> </mat-row>
</table>
</mat-card> </mat-card>
<!-- The menu content --> <!-- The menu content -->
<mat-menu #motionBlockMenu="matMenu"> <mat-menu #motionBlockMenu="matMenu">
<os-speaker-button [menuItem]=true [object]="block"></os-speaker-button> <os-speaker-button [menuItem]="true" [object]="block"></os-speaker-button>
<os-projector-button *ngIf="block" [object]="block" [menuItem]="true"></os-projector-button> <os-projector-button *ngIf="block" [object]="block" [menuItem]="true"></os-projector-button>
@ -112,23 +102,17 @@
<div class="os-form-card-mobile" mat-dialog-content> <div class="os-form-card-mobile" mat-dialog-content>
<form class="edit-form" [formGroup]="blockEditForm" (ngSubmit)="saveBlock()" (keydown)="onKeyDown($event)"> <form class="edit-form" [formGroup]="blockEditForm" (ngSubmit)="saveBlock()" (keydown)="onKeyDown($event)">
<mat-form-field> <mat-form-field>
<input matInput osAutofocus placeholder="{{ 'Title' | translate }}" formControlName="title" required/> <input matInput osAutofocus placeholder="{{ 'Title' | translate }}" formControlName="title" required />
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="internal">Internal</mat-checkbox> <mat-checkbox formControlName="internal">Internal</mat-checkbox>
</form> </form>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>
<button <button type="submit" mat-button [disabled]="!blockEditForm.valid" color="primary" (click)="saveBlock()">
type="submit" <span translate>Save</span>
mat-button </button>
[disabled]="!blockEditForm.valid" <button type="button" mat-button [mat-dialog-close]="null">
color="primary" <span translate>Cancel</span>
(click)="saveBlock()" </button>
> </div>
<span translate>Save</span>
</button>
<button type="button" mat-button [mat-dialog-close]="null">
<span translate>Cancel</span>
</button>
</div>
</ng-template> </ng-template>

View File

@ -1,54 +1,27 @@
@import '~assets/styles/tables.scss'; @import '~assets/styles/tables.scss';
.block-title { .block-detail-table {
padding: 40px; margin-top: 10px;
padding-left: 25px; height: calc(100vh - 250px);
line-height: 180%;
font-size: 120%;
color: #317796; // TODO: put in theme as $primary
h2 { ::ng-deep .pbl-ngrid-row {
margin: 0; height: 80px !important;
font-weight: normal; }
.pbl-ngrid-column-title {
height: 100%;
} }
} }
.block-card { @media only screen and (max-width: 960px) {
margin: 0 20px 0 20px; .block-detail-table {
padding: 25px; height: calc(100vh - 186px);
button {
.mat-icon {
margin-right: 5px;
}
} }
} }
.chip-container { .motion-block-title {
display: block; &.pbl-ngrid-cell {
min-height: 0; // default is inherit, will appear on the top edge of the cell height: 100%;
}
.os-headed-listview-table {
// Title
.mat-column-title {
flex: 4 0 0;
}
// State
.mat-column-state {
flex: 2 0 0;
}
// Recommendation
.mat-column-recommendation {
flex: 2 0 0;
}
// Remove
.mat-column-remove {
flex: 1 0 0;
justify-content: flex-end !important;
} }
} }

View File

@ -5,17 +5,15 @@ import { MatSnackBar, MatDialog } from '@angular/material';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition, PblDataSource, createDS, columnFactory } from '@pebula/ngrid';
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service'; import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { OperatorService } from 'app/core/core-services/operator.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewMotion } from 'app/site/motions/models/view-motion'; import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { StorageService } from 'app/core/core-services/storage.service'; import { BaseViewComponent } from 'app/site/base/base-view';
import { Motion } from 'app/shared/models/motions/motion';
/** /**
* Detail component to display one motion block * Detail component to display one motion block
@ -25,13 +23,53 @@ import { Motion } from 'app/shared/models/motions/motion';
templateUrl: './motion-block-detail.component.html', templateUrl: './motion-block-detail.component.html',
styleUrls: ['./motion-block-detail.component.scss'] styleUrls: ['./motion-block-detail.component.scss']
}) })
export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion, Motion, MotionRepositoryService> export class MotionBlockDetailComponent extends BaseViewComponent implements OnInit {
implements OnInit {
/** /**
* Determines the block id from the given URL * Determines the block id from the given URL
*/ */
public block: ViewMotionBlock; public block: ViewMotionBlock;
/**
* Data source for the motions in the block
*/
public dataSource: PblDataSource<ViewMotion>;
/**
* Define the columns to show
*/
public tableColumnDefinition: PblColumnDefinition[] = [];
/**
* Define the columns to show
* TODO: The translation will not update when the
*/
public columnSet = columnFactory()
.table(
{
prop: 'title',
label: this.translate.instant('Title'),
width: 'auto'
},
{
prop: 'state',
label: this.translate.instant('State'),
width: '30%',
minWidth: 60
},
{
prop: 'recommendation',
label: this.translate.instant('Recommendation'),
width: '30%',
minWidth: 60
},
{
prop: 'remove',
label: '',
width: '40px'
}
)
.build();
/** /**
* The form to edit blocks * The form to edit blocks
*/ */
@ -61,9 +99,7 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
titleService: Title, titleService: Title,
protected translate: TranslateService, protected translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
route: ActivatedRoute, private route: ActivatedRoute,
storage: StorageService,
private operator: OperatorService,
private router: Router, private router: Router,
protected repo: MotionBlockRepositoryService, protected repo: MotionBlockRepositoryService,
protected motionRepo: MotionRepositoryService, protected motionRepo: MotionRepositoryService,
@ -71,7 +107,7 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
private fb: FormBuilder, private fb: FormBuilder,
private dialog: MatDialog private dialog: MatDialog
) { ) {
super(titleService, translate, matSnackBar, motionRepo, route, storage); super(titleService, translate, matSnackBar);
} }
/** /**
@ -80,7 +116,7 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Motion block'); super.setTitle('Motion block');
this.initTable();
const blockId = parseInt(this.route.snapshot.params.id, 10); const blockId = parseInt(this.route.snapshot.params.id, 10);
// pseudo filter // pseudo filter
@ -88,33 +124,17 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
this.repo.getViewModelObservable(blockId).subscribe(newBlock => { this.repo.getViewModelObservable(blockId).subscribe(newBlock => {
if (newBlock) { if (newBlock) {
this.block = newBlock; this.block = newBlock;
this.subscriptions.push(
this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(viewMotions => { this.dataSource = createDS<ViewMotion>()
if (viewMotions && viewMotions.length) { .onTrigger(() => {
this.dataSource.data = viewMotions; return this.repo.getViewMotionsByBlock(this.block.motionBlock);
} else {
this.dataSource.data = [];
}
}) })
); .create();
} }
}) })
); );
} }
/**
* Returns the columns that should be shown in the table
*
* @returns an array of strings building the column definition
*/
public getColumnDefinition(): string[] {
let columns = ['title', 'state', 'recommendation', 'anchor'];
if (this.operator.hasPerms('motions.can_manage_manage')) {
columns = columns.concat('remove');
}
return columns;
}
/** /**
* Click handler for recommendation button * Click handler for recommendation button
*/ */
@ -169,8 +189,8 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
* Following a recommendation implies, that a valid recommendation exists. * Following a recommendation implies, that a valid recommendation exists.
*/ */
public isFollowingProhibited(): boolean { public isFollowingProhibited(): boolean {
if (this.dataSource.data) { if (this.dataSource && this.dataSource.source) {
return this.dataSource.data.every(motion => motion.isInFinalState() || !motion.recommendation_id); return this.dataSource.source.every(motion => motion.isInFinalState() || !motion.recommendation_id);
} else { } else {
return false; return false;
} }

View File

@ -49,52 +49,40 @@
<!-- Save and Cancel buttons --> <!-- Save and Cancel buttons -->
<mat-card-actions> <mat-card-actions>
<button mat-button [disabled]="!createBlockForm.valid" (click)="onSaveNewButton()"><span translate>Save</span></button> <button mat-button [disabled]="!createBlockForm.valid" (click)="onSaveNewButton()">
<span translate>Save</span>
</button>
<button mat-button (click)="onCancel()"><span translate>Cancel</span></button> <button mat-button (click)="onCancel()"><span translate>Cancel</span></button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
<!-- Table -->
<mat-card class="os-card"> <mat-card class="os-card">
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource"> <os-list-view-table
<!-- Projector column --> [repo]="repo"
<ng-container matColumnDef="projector"> [showFilterBar]="false"
<mat-header-cell *matHeaderCellDef></mat-header-cell> [columns]="tableColumnDefinition"
<mat-cell *matCellDef="let block"> [multiSelect]="isMultiSelect"
<os-projector-button [object]="block"></os-projector-button> scrollKey="motionBlock"
</mat-cell> [(selectedRows)]="selectedRows"
</ng-container> (dataSourceChange)="onDataSourceChange($event)"
>
<!-- title column --> <!-- Title column -->
<ng-container matColumnDef="title"> <div *pblNgridCellDef="'title'; value as title; row as block; rowContext as rowContext" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef> <a
<span translate>Title</span> class="detail-link"
</mat-header-cell> (click)="saveScrollIndex('motionBlock', rowContext.identity)"
<mat-cell *matCellDef="let block"> [routerLink]="block.id"
*ngIf="!isMultiSelect"
></a>
<div>
<mat-icon matTooltip="Internal" *ngIf="block.internal">lock</mat-icon> <mat-icon matTooltip="Internal" *ngIf="block.internal">lock</mat-icon>
{{ block.title }} {{ title }}
</mat-cell> </div>
</ng-container> </div>
<!-- amount column --> <!-- Amount -->
<ng-container matColumnDef="amount"> <div *pblNgridCellDef="'amount'; row as block" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef> <span class="os-amount-chip">{{ getMotionAmount(block.motionBlock) }}</span>
<span translate>Motions</span> </div>
</mat-header-cell> </os-list-view-table>
<mat-cell *matCellDef="let block">
<span class="os-amount-chip">{{ getMotionAmount(block.motionBlock) }}</span>
</mat-cell>
</ng-container>
<!-- Anchor column to open the separate tab -->
<ng-container matColumnDef="anchor">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let block">
<a [routerLink]="block.id"></a>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row *matRowDef="let row; columns: getColumnDefinition()"> </mat-row>
</table>
</mat-card> </mat-card>

View File

@ -1,22 +1,5 @@
@import '~assets/styles/tables.scss'; @import '~assets/styles/tables.scss';
.os-headed-listview-table {
// Title
.mat-column-title {
flex: 9 0 0;
}
// Amount
.mat-column-amount {
flex: 1 0 60px;
}
// Menu
.mat-column-menu {
flex: 0 0 40px;
}
}
::ng-deep .mat-form-field { ::ng-deep .mat-form-field {
width: 50%; width: 50%;
} }

View File

@ -1,11 +1,11 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { itemVisibilityChoices } from 'app/shared/models/agenda/item'; import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
@ -26,9 +26,7 @@ import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
templateUrl: './motion-block-list.component.html', templateUrl: './motion-block-list.component.html',
styleUrls: ['./motion-block-list.component.scss'] styleUrls: ['./motion-block-list.component.scss']
}) })
export class MotionBlockListComponent export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBlock> implements OnInit {
extends ListViewBaseComponent<ViewMotionBlock, MotionBlock, MotionBlockRepositoryService>
implements OnInit {
/** /**
* Holds the create form * Holds the create form
*/ */
@ -63,6 +61,21 @@ export class MotionBlockListComponent
return this.operator.hasPerms('motions.can_manage', 'motions.can_manage_metadata'); return this.operator.hasPerms('motions.can_manage', 'motions.can_manage_metadata');
} }
/**
* Define the columns to show
*/
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'title',
label: this.translate.instant('Title'),
width: 'auto'
},
{
prop: 'amount',
label: this.translate.instant('Motions')
}
];
/** /**
* Constructor for the motion block list view * Constructor for the motion block list view
* *
@ -83,16 +96,15 @@ export class MotionBlockListComponent
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService, storage: StorageService,
private repo: MotionBlockRepositoryService, public repo: MotionBlockRepositoryService,
private agendaRepo: ItemRepositoryService, private agendaRepo: ItemRepositoryService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private itemRepo: ItemRepositoryService, private itemRepo: ItemRepositoryService,
private operator: OperatorService, private operator: OperatorService,
sortService: MotionBlockSortService public sortService: MotionBlockSortService
) { ) {
super(titleService, translate, matSnackBar, repo, route, storage, null, sortService); super(titleService, translate, matSnackBar, storage);
this.createBlockForm = this.formBuilder.group({ this.createBlockForm = this.formBuilder.group({
title: ['', Validators.required], title: ['', Validators.required],
@ -107,24 +119,10 @@ export class MotionBlockListComponent
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Motion blocks'); super.setTitle('Motion blocks');
this.initTable();
this.items = this.itemRepo.getViewModelListBehaviorSubject(); this.items = this.itemRepo.getViewModelListBehaviorSubject();
this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility)); this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility));
} }
/**
* Returns the columns that should be shown in the table
*
* @returns an array of strings building the column definition
*/
public getColumnDefinition(): string[] {
let columns = ['title', 'amount', 'anchor'];
if (this.operator.hasPerms('core.can_manage_projector')) {
columns = ['projector'].concat(columns);
}
return columns;
}
/** /**
* return the amount of motions in a motion block * return the amount of motions in a motion block
* *

View File

@ -14,99 +14,88 @@
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button> <button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span> <span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div> </div>
<div class="extra-controls-slot">
<div *ngIf="isCategoryAvailable()">
<button
mat-button
*ngIf="selectedView !== 'tiles'"
(click)="onChangeView('tiles')"
matTooltip="{{ 'Tile view' | translate }}"
>
<mat-icon>view_module</mat-icon>
</button>
<button
mat-button
*ngIf="selectedView !== 'list'"
(click)="onChangeView('list')"
matTooltip="{{ 'List view' | translate }}"
>
<mat-icon>view_headline</mat-icon>
</button>
</div>
</div>
</os-head-bar> </os-head-bar>
<mat-drawer-container class="on-transition-fade"> <ng-container *ngIf="selectedView === 'tiles'; then tiles; else list"></ng-container>
<os-sort-filter-bar
[filterCount]="filteredCount" <ng-template #list>
<os-list-view-table
[repo]="motionRepo"
[filterService]="filterService" [filterService]="filterService"
[sortService]="sortService" [sortService]="sortService"
[showFilterSort]="selectedView === 'list'" [columns]="tableColumnDefinition"
[itemsVerboseName]="motionsVerboseName" [multiSelect]="isMultiSelect"
(searchFieldChange)="searchFilter($event)" [restricted]="restrictedColumns"
[hiddenInMobile]="['state']"
scrollKey="motion"
[(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)"
> >
<mat-button-toggle-group *ngIf="isCategoryAvailable()" #group="matButtonToggleGroup" [value]="selectedView" (change)="onChangeView(group.value)" appearance="legacy" aria-label="Select view" class="extra-controls-slot select-view-wrapper"> <!-- Identifier -->
<mat-button-toggle value="tiles" matTooltip="{{ 'Tile view' | translate }}"><mat-icon>view_module</mat-icon></mat-button-toggle> <div *pblNgridCellDef="'identifier'; row as motion" class="cell-slot fill">
<mat-button-toggle value="list" matTooltip="{{ 'List view' | translate }}"><mat-icon>view_headline</mat-icon></mat-button-toggle> <a class="detail-link" [routerLink]="motion.id" *ngIf="!isMultiSelect"></a>
</mat-button-toggle-group> <div class="column-identifier innerTable">
</os-sort-filter-bar> {{ motion.identifier }}
</div>
</div>
<div [ngSwitch]="selectedView"> <!-- Title -->
<span *ngSwitchCase="'list'"> <div *pblNgridCellDef="'title'; row as motion; rowContext as rowContext" class="cell-slot fill">
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <a
<!-- Selector column --> class="detail-link"
<ng-container matColumnDef="selector"> (click)="saveScrollIndex('motion', rowContext.identity)"
<mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell> [routerLink]="motion.id"
<mat-cell *matCellDef="let motion"> *ngIf="!isMultiSelect"
<mat-icon>{{ isSelected(motion) ? 'check_circle' : '' }}</mat-icon> ></a>
</mat-cell> <div class="column-title innerTable">
</ng-container> <div class="title-line ellipsis-overflow">
<!-- Is Favorite -->
<span *ngIf="motion.star" class="favorite-star">
<mat-icon inline>star</mat-icon>
</span>
<!-- Projector column --> <!-- Has File -->
<ng-container matColumnDef="projector"> <span class="attached-files" *ngIf="motion.hasAttachments()">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell> <mat-icon>attach_file</mat-icon>
<mat-cell *matCellDef="let motion"> </span>
<os-projector-button [object]="motion"></os-projector-button>
</mat-cell>
</ng-container>
<!-- identifier column --> <!-- The title -->
<ng-container matColumnDef="identifier"> <span class="motion-list-title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell> {{ motion.title }}
<mat-cell *matCellDef="let motion"> </span>
<div class="innerTable"> </div>
{{ motion.identifier }}
</div>
</mat-cell>
</ng-container>
<!-- title column --> <!-- Submitters -->
<ng-container matColumnDef="title"> <div class="submitters-line ellipsis-overflow" *ngIf="motion.submitters.length">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell> <span translate>by</span> {{ motion.submitters }}
<mat-cell *matCellDef="let motion"> <span *osPerms="'motions.can_manage'">
<div class="innerTable max-width"> &middot;
<!-- title line --> <span translate>Sequential number</span>
<div class="title-line ellipsis-overflow"> {{ motion.id }}
<!-- favorite icon --> </span>
<span *ngIf="motion.star" class="favorite-star"> </div>
<mat-icon inline>star</mat-icon>
</span>
<!-- attachment icon -->
<span class="attached-files" *ngIf="motion.hasAttachments()">
<mat-icon>attach_file</mat-icon>
</span>
<!-- title -->
<span class="motion-list-title">
{{ motion.title }}
</span>
</div>
<!-- submitters line -->
<div class="submitters-line ellipsis-overflow" *ngIf="motion.submitters.length">
<span translate>by</span> {{ motion.submitters }}
<span *osPerms="'motions.can_manage'">
&middot;
<span translate>Sequential number</span>
{{ motion.id }}
</span>
</div>
<!-- state line-->
<div class="ellipsis-overflow white">
<mat-basic-chip *ngIf="motion.state" [ngClass]="motion.stateCssColor" [disabled]="true">
{{ getStateLabel(motion) }}
</mat-basic-chip>
</div>
<!-- recommendation line -->
<div
*ngIf="motion.recommendation && motion.state.next_states_id.length > 0"
class="ellipsis-overflow white spacer-top-3"
>
<mat-basic-chip class="bluegrey" [disabled]="true">
{{ getRecommendationLabel(motion) }}
</mat-basic-chip>
</div>
</div>
</mat-cell>
</ng-container>
<!-- state column --> <!-- state column -->
<ng-container matColumnDef="state"> <ng-container matColumnDef="state">
@ -118,7 +107,9 @@
<os-icon-container icon="device_hub">{{ motion.category }}</os-icon-container> <os-icon-container icon="device_hub">{{ motion.category }}</os-icon-container>
</div> </div>
<div class="ellipsis-overflow spacer-top-5" *ngIf="motion.motion_block"> <div class="ellipsis-overflow spacer-top-5" *ngIf="motion.motion_block">
<os-icon-container icon="widgets">{{ motion.motion_block.title }}</os-icon-container> <os-icon-container icon="widgets">{{
motion.motion_block.title
}}</os-icon-container>
</div> </div>
<div class="ellipsis-overflow spacer-top-5" *ngIf="motion.tags && motion.tags.length"> <div class="ellipsis-overflow spacer-top-5" *ngIf="motion.tags && motion.tags.length">
<os-icon-container icon="local_offer"> <os-icon-container icon="local_offer">
@ -133,64 +124,104 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<!-- Anchor column to open the separate tab --> <!-- Workflow state -->
<ng-container matColumnDef="anchor"> <div class="ellipsis-overflow white">
<mat-header-cell *matHeaderCellDef></mat-header-cell> <mat-basic-chip *ngIf="motion.state" [ngClass]="motion.stateCssColor" [disabled]="true">
<mat-cell *matCellDef="let motion"> {{ getStateLabel(motion) }}
<a [routerLink]="motion.id" *ngIf="!isMultiSelect"></a> </mat-basic-chip>
</mat-cell> </div>
</ng-container>
<!-- Speakers column --> <!-- Recommendation -->
<ng-container matColumnDef="speakers"> <div
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell> *ngIf="motion.recommendation && motion.state.next_states_id.length > 0"
<mat-cell *matCellDef="let motion"> class="ellipsis-overflow white spacer-top-3"
<os-speaker-button [object]="motion" [disabled]="isMultiSelect"></os-speaker-button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
(click)="selectItem(row, $event)"
*matRowDef="let row; columns: getColumnDefinition()"
class="lg"
> >
</mat-row> <mat-basic-chip class="bluegrey" [disabled]="true">
</mat-table> {{ getRecommendationLabel(motion) }}
</span> </mat-basic-chip>
<span *ngSwitchCase="'tiles'"> </div>
<os-grid-layout> </div>
<os-block-tile </div>
*ngFor="let tileCategory of tileCategories"
(clicked)="changeToViewWithTileCategory(tileCategory)"
[orientation]="'horizontal'"
[only]="'title'"
[blockType]="'node'"
[data]="tileCategory"
title="{{ tileCategory.name | translate }}">
<ng-container class="block-node">
<table matTooltip="{{ tileCategory.amountOfMotions }} {{ 'Motions' | translate }} {{ tileCategory.name | translate }}">
<tbody>
<tr>
<td>
<span class="tile-block-title" [matBadge]="tileCategory.amountOfMotions" [matBadgeColor]="'accent'" [ngSwitch]="tileCategory.name">
<span *ngSwitchCase="'Favorites'"><mat-icon>star</mat-icon></span>
<span *ngSwitchCase="'No category'"><mat-icon>block</mat-icon></span>
<span *ngSwitchDefault>{{ tileCategory.prefix }}</span>
</span>
</td>
</tr>
</tbody>
</table>
</ng-container>
</os-block-tile>
</os-grid-layout>
</span>
</div>
<mat-paginator [style.display]="selectedView === 'list' ? 'block' : 'none'" class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator> <!-- Category, blocks and tags -->
</mat-drawer-container> <div
*pblNgridCellDef="'state'; row as motion"
class="cell-slot fill"
[ngClass]="isMultiSelect ? '' : 'clickable'"
(click)="openEditInfo(motion)"
>
<div class="column-state innerTable">
<!-- Category -->
<div class="ellipsis-overflow" *ngIf="motion.category">
<os-icon-container icon="device_hub">{{ motion.category }}</os-icon-container>
</div>
<!-- Motion Block -->
<div class="ellipsis-overflow spacer-top-5" *ngIf="motion.motion_block">
<os-icon-container icon="widgets">{{ motion.motion_block.title }}</os-icon-container>
</div>
<!-- Tags -->
<div class="ellipsis-overflow spacer-top-5" *ngIf="motion.tags && motion.tags.length">
<os-icon-container icon="local_offer">
<span *ngFor="let tag of motion.tags; let last = last">
{{ tag.getTitle() }}
<span *ngIf="!last">,&nbsp;</span>
</span>
</os-icon-container>
</div>
</div>
</div>
<!-- Speaker column-->
<div *pblNgridCellDef="'speaker'; row as motion; rowContext as rowContext" class="fill">
<os-speaker-button
[object]="motion"
[disabled]="isMultiSelect"
(click)="saveScrollIndex('motion', rowContext.identity)"
></os-speaker-button>
</div>
</os-list-view-table>
</ng-template>
<ng-template #tiles>
<os-grid-layout>
<os-block-tile
*ngFor="let tileCategory of tileCategories"
(clicked)="changeToViewWithTileCategory(tileCategory)"
[orientation]="'horizontal'"
[only]="'title'"
[blockType]="'node'"
[data]="tileCategory"
title="{{ tileCategory.name | translate }}"
>
<ng-container class="block-node">
<table
matTooltip="{{ tileCategory.amountOfMotions }} {{ 'Motions' | translate }} {{
tileCategory.name | translate
}}"
>
<tbody>
<tr>
<td>
<span
class="tile-block-title"
[matBadge]="tileCategory.amountOfMotions"
[matBadgeColor]="'accent'"
[ngSwitch]="tileCategory.name"
>
<span *ngSwitchCase="'Favorites'"><mat-icon>star</mat-icon></span>
<span *ngSwitchCase="'No category'"><mat-icon>block</mat-icon></span>
<span *ngSwitchDefault>{{ tileCategory.prefix }}</span>
</span>
</td>
</tr>
</tbody>
</table>
</ng-container>
</os-block-tile>
</os-grid-layout>
</ng-template>
<mat-menu #motionListMenu="matMenu"> <mat-menu #motionListMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">

View File

@ -1,9 +1,11 @@
@import '~assets/styles/tables.scss'; @import '~assets/styles/tables.scss';
// Determine the distance between the top edge to the start of the table content
$text-margin-top: 10px;
/** css hacks https://codepen.io/edge0703/pen/iHJuA */ /** css hacks https://codepen.io/edge0703/pen/iHJuA */
.innerTable { .innerTable {
display: inline-block; display: inline-block;
vertical-align: top;
line-height: 150%; line-height: 150%;
} }
@ -18,70 +20,49 @@
} }
} }
.os-listview-table { .projector-button {
/** identifier */ margin: auto;
.mat-column-identifier { }
padding-left: 10px;
flex: 0 0 50px;
line-height: 60px; // set the text in the vertical middle, since vertical-align will not work
display: initial; // reset display
text-align: center; // center text
}
/** Title */ .column-identifier {
.mat-column-title { margin-top: $text-margin-top;
width: 100%; }
flex: 1 0 200px;
display: block;
padding-left: 10px;
.title-line { .column-title {
font-weight: 500; margin-top: $text-margin-top;
font-size: 16px;
.attached-files { .title-line {
.mat-icon { font-weight: 500;
display: inline-flex; font-size: 16px;
vertical-align: middle;
$icon-size: 16px;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
}
}
.favorite-star { .attached-files {
padding-right: 3px; .mat-icon {
display: inline-flex;
vertical-align: middle;
$icon-size: 16px;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
} }
} }
.submitters-line { .favorite-star {
font-size: 90%; padding-right: 3px;
} }
} }
/** State */ .submitters-line {
.mat-column-state { font-size: 90%;
flex: 0 0 160px;
.state-column {
width: 160px;
}
mat-icon {
font-size: 150%;
}
}
/** Speakers indicator */
.mat-column-speakers {
flex: 0 0 100px;
justify-content: flex-end !important;
} }
} }
.max-width { .column-state {
width: 100%; margin-top: $text-margin-top;
width: inherit;
.mat-icon {
font-size: 150%;
}
} }
os-grid-layout { os-grid-layout {

View File

@ -4,6 +4,7 @@ import { Title } from '@angular/platform-browser';
import { MatSnackBar, MatDialog } from '@angular/material'; import { MatSnackBar, MatDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service'; import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
@ -12,12 +13,8 @@ import { MotionBlockRepositoryService } from 'app/core/repositories/motions/moti
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service'; import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service'; import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { MotionExportDialogComponent } from '../motion-export-dialog/motion-export-dialog.component'; import { MotionExportDialogComponent } from '../motion-export-dialog/motion-export-dialog.component';
import { OperatorService } from 'app/core/core-services/operator.service';
import { ViewportService } from 'app/core/ui-services/viewport.service';
import { Motion } from 'app/shared/models/motions/motion';
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from 'app/site/motions/models/view-motion'; import { ViewMotion, LineNumberingMode, ChangeRecoMode } from 'app/site/motions/models/view-motion';
import { ViewWorkflow } from 'app/site/motions/models/view-workflow'; import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
import { ViewCategory } from 'app/site/motions/models/view-category'; import { ViewCategory } from 'app/site/motions/models/view-category';
@ -31,8 +28,7 @@ import { MotionXlsxExportService } from 'app/site/motions/services/motion-xlsx-e
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service'; import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { PdfError } from 'app/core/ui-services/pdf-document.service'; import { PdfError } from 'app/core/ui-services/pdf-document.service';
import { tap } from 'rxjs/operators'; import { ColumnRestriction } from 'app/shared/components/list-view-table/list-view-table.component';
import { Observable } from 'rxjs';
interface TileCategoryInformation { interface TileCategoryInformation {
filter: string; filter: string;
@ -76,8 +72,7 @@ interface InfoDialog {
templateUrl: './motion-list.component.html', templateUrl: './motion-list.component.html',
styleUrls: ['./motion-list.component.scss'] styleUrls: ['./motion-list.component.scss']
}) })
export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motion, MotionRepositoryService> export class MotionListComponent extends ListViewBaseComponent<ViewMotion> implements OnInit {
implements OnInit {
/** /**
* Reference to the dialog for quick editing meta information. * Reference to the dialog for quick editing meta information.
*/ */
@ -96,13 +91,25 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
/** /**
* Columns to display in table when desktop view is available * Columns to display in table when desktop view is available
* Define the columns to show
*/ */
public displayedColumnsDesktop: string[] = ['identifier', 'title', 'state', 'anchor']; public tableColumnDefinition: PblColumnDefinition[] = [
{
/** prop: 'identifier'
* Columns to display in table when mobile view is available },
*/ {
public displayedColumnsMobile = ['identifier', 'title', 'anchor']; prop: 'title',
width: 'auto'
},
{
prop: 'state',
width: '20%',
minWidth: 160
},
{
prop: 'speaker'
}
];
/** /**
* Value of the configuration variable `motions_statutes_enabled` - are statutes enabled? * Value of the configuration variable `motions_statutes_enabled` - are statutes enabled?
@ -116,6 +123,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
public categories: ViewCategory[] = []; public categories: ViewCategory[] = [];
public motionBlocks: ViewMotionBlock[] = []; public motionBlocks: ViewMotionBlock[] = [];
public restrictedColumns: ColumnRestriction[] = [
{
columnName: 'speaker',
permission: 'agenda.can_see'
}
];
/** /**
* List of `TileCategoryInformation`. * List of `TileCategoryInformation`.
* Necessary to not iterate over the values of the map below. * Necessary to not iterate over the values of the map below.
@ -132,11 +146,6 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
*/ */
public motionsVerboseName: string; public motionsVerboseName: string;
/**
* Store the view as member - if the user changes the view, this member is as well changed.
*/
private storedView: string;
/** /**
* Constructor implements title and translation Module. * Constructor implements title and translation Module.
* *
@ -165,7 +174,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
titleService: Title, titleService: Title,
protected translate: TranslateService, // protected required for ng-translate-extract protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
route: ActivatedRoute, private route: ActivatedRoute,
storage: StorageService, storage: StorageService,
public filterService: MotionFilterListService, public filterService: MotionFilterListService,
public sortService: MotionSortListService, public sortService: MotionSortListService,
@ -175,19 +184,15 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
private motionBlockRepo: MotionBlockRepositoryService, private motionBlockRepo: MotionBlockRepositoryService,
private categoryRepo: CategoryRepositoryService, private categoryRepo: CategoryRepositoryService,
private workflowRepo: WorkflowRepositoryService, private workflowRepo: WorkflowRepositoryService,
protected motionRepo: MotionRepositoryService, public motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService, private motionCsvExport: MotionCsvExportService,
private operator: OperatorService,
private pdfExport: MotionPdfExportService, private pdfExport: MotionPdfExportService,
private dialog: MatDialog, private dialog: MatDialog,
private vp: ViewportService,
public multiselectService: MotionMultiselectService, public multiselectService: MotionMultiselectService,
public perms: LocalPermissionsService, public perms: LocalPermissionsService,
private motionXlsxExport: MotionXlsxExportService private motionXlsxExport: MotionXlsxExportService
) { ) {
super(titleService, translate, matSnackBar, motionRepo, route, storage, filterService, sortService); super(titleService, translate, matSnackBar, storage);
// enable multiSelect for this listView
this.canMultiSelect = true; this.canMultiSelect = true;
} }
@ -199,77 +204,107 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
*/ */
public async ngOnInit(): Promise<void> { public async ngOnInit(): Promise<void> {
super.setTitle('Motions'); super.setTitle('Motions');
this.initTable(); const storedView = await this.storage.get<string>('motionListView');
this.storedView = await this.storage.get<string>('motionListView');
this.subscriptions.push( this.configService
this.configService .get<boolean>('motions_statutes_enabled')
.get<boolean>('motions_statutes_enabled') .subscribe(enabled => (this.statutesEnabled = enabled));
.subscribe(enabled => (this.statutesEnabled = enabled)), this.configService.get<string>('motions_recommendations_by').subscribe(recommender => {
this.configService.get<string>('motions_recommendations_by').subscribe(recommender => { this.recommendationEnabled = !!recommender;
this.recommendationEnabled = !!recommender; });
}), this.motionBlockRepo.getViewModelListObservable().subscribe(mBs => {
this.motionBlockRepo.getViewModelListObservable().subscribe(mBs => { this.motionBlocks = mBs;
this.motionBlocks = mBs; });
this.updateStateColumnVisibility(); this.categoryRepo.getViewModelListObservable().subscribe(cats => {
}), this.categories = cats;
this.categoryRepo.getViewModelListObservable().subscribe(cats => { if (cats.length > 0) {
this.categories = cats; this.selectedView = storedView || 'tiles';
if (cats.length > 0) { } else {
this.selectedView = this.storedView || 'tiles'; this.selectedView = 'list';
} else { }
this.selectedView = 'list'; });
} this.tagRepo.getViewModelListObservable().subscribe(tags => {
this.updateStateColumnVisibility(); this.tags = tags;
}), });
this.tagRepo.getViewModelListObservable().subscribe(tags => { this.workflowRepo.getViewModelListObservable().subscribe(wfs => (this.workflows = wfs));
this.tags = tags;
this.updateStateColumnVisibility(); this.motionRepo.getViewModelListObservable().subscribe(motions => {
}), if (motions && motions.length) {
this.workflowRepo.getViewModelListObservable().subscribe(wfs => (this.workflows = wfs)) this.createTiles(motions);
); }
this.setFulltextFilter(); });
}
private createTiles(motions: ViewMotion[]): void {
this.informationOfMotionsInTileCategories = {};
for (const motion of motions) {
if (motion.star) {
this.countMotions(-1, true, 'star', 'Favorites');
}
if (motion.category_id) {
this.countMotions(
motion.category_id,
motion.category_id,
'category',
motion.category.name,
motion.category.prefix
);
} else {
this.countMotions(-2, null, 'category', 'No category');
}
}
this.tileCategories = Object.values(this.informationOfMotionsInTileCategories);
} }
/** /**
* The action performed on a click in single select modus * Handler for the plus button
* @param motion The row the user clicked at
*/ */
public singleSelectAction(motion: ViewMotion): void { public onPlusButton(): void {
this.router.navigate(['./' + motion.id], { relativeTo: this.route }); this.router.navigate(['./new'], { relativeTo: this.route });
} }
/** /**
* Overwriting method of base-class. * Opens the export dialog.
* Every time this method is called, all motions are counted in their related categories. * The export will be limited to the selected data if multiselect modus is
* * active and there are rows selected
* @returns {Observable<ViewMotion[]>} An observable containing the list of motions.
*/ */
protected getModelListObservable(): Observable<ViewMotion[]> { public openExportDialog(): void {
return super.getModelListObservable().pipe( const exportDialogRef = this.dialog.open(MotionExportDialogComponent, {
tap(motions => { width: '1100px',
this.informationOfMotionsInTileCategories = {}; maxWidth: '90vw',
for (const motion of motions) { maxHeight: '90vh',
if (motion.star) { data: this.dataSource
this.countMotions(-1, true, 'star', 'Favorites'); });
}
if (motion.category_id) { exportDialogRef.afterClosed().subscribe((result: any) => {
this.countMotions( if (result && result.format) {
motion.category_id, const data = this.isMultiSelect ? this.selectedRows : this.dataSource.source;
motion.category_id, if (result.format === 'pdf') {
'category', try {
motion.category.name, this.pdfExport.exportMotionCatalog(
motion.category.prefix data,
result.lnMode,
result.crMode,
result.content,
result.metaInfo,
result.comments
); );
} else { } catch (err) {
this.countMotions(-2, null, 'category', 'No category'); if (err instanceof PdfError) {
this.raiseError(err.message);
} else {
throw err;
}
} }
} else if (result.format === 'csv') {
this.motionCsvExport.exportMotionList(data, [...result.content, ...result.metaInfo], result.crMode);
} else if (result.format === 'xlsx') {
this.motionXlsxExport.exportMotionList(data, result.metaInfo);
} }
}
this.tileCategories = Object.values(this.informationOfMotionsInTileCategories); });
this.motionsVerboseName = this.motionRepo.getVerboseName(motions.length > 1);
})
);
} }
/** /**
@ -303,106 +338,6 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
this.informationOfMotionsInTileCategories[id] = info; this.informationOfMotionsInTileCategories[id] = info;
} }
/**
* Get the icon to the corresponding Motion Status
* TODO Needs to be more accessible (Motion workflow needs adjustment on the server)
*
* @param state the name of the state
* @returns the icon string
*/
public getStateIcon(state: WorkflowState): string {
const stateName = state.name;
if (stateName === 'accepted') {
return 'thumb_up';
} else if (stateName === 'rejected') {
return 'thumb_down';
} else if (stateName === 'not decided') {
return 'help';
} else {
return '';
}
}
/**
* Determines if an icon should be shown in the list view
*
* @param state the workflowstate
* @returns a boolean if the icon should be shown
*/
public isDisplayIcon(state: WorkflowState): boolean {
if (state) {
return state.name === 'accepted' || state.name === 'rejected' || state.name === 'not decided';
} else {
return false;
}
}
/**
* Handler for the plus button
*/
public onPlusButton(): void {
this.router.navigate(['./new'], { relativeTo: this.route });
}
/**
* Opens the export dialog.
* The export will be limited to the selected data if multiselect modus is
* active and there are rows selected
*/
public openExportDialog(): void {
const exportDialogRef = this.dialog.open(MotionExportDialogComponent, {
width: '1100px',
maxWidth: '90vw',
maxHeight: '90vh',
data: this.dataSource
});
exportDialogRef.afterClosed().subscribe((result: any) => {
if (result && result.format) {
const data = this.isMultiSelect ? this.selectedRows : this.dataSource.filteredData;
if (result.format === 'pdf') {
try {
this.pdfExport.exportMotionCatalog(
data,
result.lnMode,
result.crMode,
result.content,
result.metaInfo,
result.comments
);
} catch (err) {
if (err instanceof PdfError) {
this.raiseError(err.message);
} else {
throw err;
}
}
} else if (result.format === 'csv') {
this.motionCsvExport.exportMotionList(data, [...result.content, ...result.metaInfo], result.crMode);
} else if (result.format === 'xlsx') {
this.motionXlsxExport.exportMotionList(data, result.metaInfo);
}
}
});
}
/**
* Returns current definitions for the listView table
*/
public getColumnDefinition(): string[] {
let columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
if (this.operator.hasPerms('core.can_manage_projector') && !this.isMultiSelect) {
columns = ['projector'].concat(columns);
}
if (this.isMultiSelect) {
columns = ['selector'].concat(columns);
}
if (this.operator.hasPerms('agenda.can_see')) {
columns = columns.concat(['speakers']);
}
return columns;
}
/** /**
* Wraps multiselect actions to close the multiselect mode or throw an error if one happens. * Wraps multiselect actions to close the multiselect mode or throw an error if one happens.
* *
@ -441,7 +376,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
*/ */
public directPdfExport(): void { public directPdfExport(): void {
this.pdfExport.exportMotionCatalog( this.pdfExport.exportMotionCatalog(
this.dataSource.data, this.dataSource.source,
this.configService.instant<string>('motions_default_line_numbering') as LineNumberingMode, this.configService.instant<string>('motions_default_line_numbering') as LineNumberingMode,
this.configService.instant<string>('motions_recommendation_text_mode') as ChangeRecoMode this.configService.instant<string>('motions_recommendation_text_mode') as ChangeRecoMode
); );
@ -450,52 +385,54 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
/** /**
* Overwrites the dataSource's string filter with a case-insensitive search * Overwrites the dataSource's string filter with a case-insensitive search
* in the identifier, title, state, recommendations, submitters, motion blocks and id * in the identifier, title, state, recommendations, submitters, motion blocks and id
*
* TODO: Does currently not work with virtual scrolling tables. Filter predicates will be missed :(
*/ */
private setFulltextFilter(): void { // private setFulltextFilter(): void {
this.dataSource.filterPredicate = (data, filter) => { // this.dataSource.filterPredicate = (data, filter) => {
if (!data) { // if (!data) {
return false; // return false;
} // }
filter = filter ? filter.toLowerCase() : ''; // filter = filter ? filter.toLowerCase() : '';
if (data.submitters.length && data.submitters.find(user => user.full_name.toLowerCase().includes(filter))) { // if (data.submitters.length && data.submitters.find(user => user.full_name.toLowerCase().includes(filter))) {
return true; // return true;
} // }
if (data.motion_block && data.motion_block.title.toLowerCase().includes(filter)) { // if (data.motion_block && data.motion_block.title.toLowerCase().includes(filter)) {
return true; // return true;
} // }
if (data.title.toLowerCase().includes(filter)) { // if (data.title.toLowerCase().includes(filter)) {
return true; // return true;
} // }
if (data.identifier && data.identifier.toLowerCase().includes(filter)) { // if (data.identifier && data.identifier.toLowerCase().includes(filter)) {
return true; // return true;
} // }
if ( // if (
this.getStateLabel(data) && // this.getStateLabel(data) &&
this.getStateLabel(data) // this.getStateLabel(data)
.toLocaleLowerCase() // .toLocaleLowerCase()
.includes(filter) // .includes(filter)
) { // ) {
return true; // return true;
} // }
if ( // if (
this.getRecommendationLabel(data) && // this.getRecommendationLabel(data) &&
this.getRecommendationLabel(data) // this.getRecommendationLabel(data)
.toLocaleLowerCase() // .toLocaleLowerCase()
.includes(filter) // .includes(filter)
) { // ) {
return true; // return true;
} // }
const dataid = '' + data.id; // const dataid = '' + data.id;
if (dataid.includes(filter)) { // if (dataid.includes(filter)) {
return true; // return true;
} // }
return false; // return false;
}; // };
} // }
/** /**
* This function saves the selected view by changes. * This function saves the selected view by changes.
@ -504,11 +441,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
*/ */
public onChangeView(value: string): void { public onChangeView(value: string): void {
this.selectedView = value; this.selectedView = value;
this.storedView = value;
this.storage.set('motionListView', value); this.storage.set('motionListView', value);
if (value === 'list') {
this.initTable();
}
} }
/** /**
@ -517,13 +450,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
* @param tileCategory information about filter and condition. * @param tileCategory information about filter and condition.
*/ */
public changeToViewWithTileCategory(tileCategory: TileCategoryInformation): void { public changeToViewWithTileCategory(tileCategory: TileCategoryInformation): void {
this.onChangeView('list');
this.filterService.clearAllFilters(); this.filterService.clearAllFilters();
this.filterService.toggleFilterOption(tileCategory.filter, { this.filterService.toggleFilterOption(tileCategory.filter, {
label: tileCategory.name, label: tileCategory.name,
condition: tileCategory.condition, condition: tileCategory.condition,
isActive: false isActive: false
}); });
this.onChangeView('list');
} }
/** /**
@ -532,69 +465,58 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
* @param motion the ViewMotion whose content is edited. * @param motion the ViewMotion whose content is edited.
* @param ev a MouseEvent. * @param ev a MouseEvent.
*/ */
public async openEditInfo(motion: ViewMotion, ev: MouseEvent): Promise<void> { public async openEditInfo(motion: ViewMotion): Promise<void> {
ev.stopPropagation(); if (!this.isMultiSelect) {
// The interface holding the current information from motion.
this.infoDialog = {
title: motion.title,
motionBlock: motion.motion_block_id,
category: motion.category_id,
tags: motion.tags_id
};
// The interface holding the current information from motion. // Copies the interface to check, if changes were made.
this.infoDialog = { const copyDialog = { ...this.infoDialog };
title: motion.title,
motionBlock: motion.motion_block_id,
category: motion.category_id,
tags: motion.tags_id
};
// Copies the interface to check, if changes were made. const dialogRef = this.dialog.open(this.motionInfoDialog, {
const copyDialog = { ...this.infoDialog }; width: '400px',
maxWidth: '90vw',
maxHeight: '90vh',
disableClose: true
});
const dialogRef = this.dialog.open(this.motionInfoDialog, { dialogRef.keydownEvents().subscribe((event: KeyboardEvent) => {
width: '400px', if (event.key === 'Enter' && event.shiftKey) {
maxWidth: '90vw', dialogRef.close(this.infoDialog);
maxHeight: '90vh',
disableClose: true
});
dialogRef.keydownEvents().subscribe((event: KeyboardEvent) => {
if (event.key === 'Enter' && event.shiftKey) {
dialogRef.close(this.infoDialog);
}
});
// After closing the dialog: Goes through the fields and check if they are changed
// TODO: Logic like this should be handled in a service
dialogRef.afterClosed().subscribe(async (result: InfoDialog) => {
if (result) {
const partialUpdate = {
category_id: result.category !== copyDialog.category ? result.category : undefined,
motion_block_id: result.motionBlock !== copyDialog.motionBlock ? result.motionBlock : undefined,
tags_id: JSON.stringify(result.tags) !== JSON.stringify(copyDialog.tags) ? result.tags : undefined
};
// TODO: "only update if different" was another repo-todo
if (!Object.keys(partialUpdate).every(key => partialUpdate[key] === undefined)) {
await this.motionRepo.update(partialUpdate, motion).then(null, this.raiseError);
} }
} });
});
}
/** // After closing the dialog: Goes through the fields and check if they are changed
* Checks if there are at least categories, motion-blocks or tags the user can select. // TODO: Logic like this should be handled in a service
*/ dialogRef.afterClosed().subscribe(async (result: InfoDialog) => {
public updateStateColumnVisibility(): void { if (result) {
const metaInfoAvailable = this.isCategoryAvailable() || this.isMotionBlockAvailable() || this.isTagAvailable(); const partialUpdate = {
if (!metaInfoAvailable && this.displayedColumnsDesktop.includes('state')) { category_id: result.category !== copyDialog.category ? result.category : undefined,
this.displayedColumnsDesktop.splice(this.displayedColumnsDesktop.indexOf('state'), 1); motion_block_id: result.motionBlock !== copyDialog.motionBlock ? result.motionBlock : undefined,
} else if (metaInfoAvailable && !this.displayedColumnsDesktop.includes('state')) { tags_id:
this.displayedColumnsDesktop = this.displayedColumnsDesktop.concat('state'); JSON.stringify(result.tags) !== JSON.stringify(copyDialog.tags) ? result.tags : undefined
};
// TODO: "only update if different" was another repo-todo
if (!Object.keys(partialUpdate).every(key => partialUpdate[key] === undefined)) {
await this.motionRepo.update(partialUpdate, motion).then(null, this.raiseError);
}
}
});
} }
} }
/** /**
* Checks motion-blocks are available. * Checks if categories are available.
* *
* @returns A boolean if they are available. * @returns A boolean if they are available.
*/ */
public isMotionBlockAvailable(): boolean { public isCategoryAvailable(): boolean {
return !!this.motionBlocks && this.motionBlocks.length > 0; return !!this.categories && this.categories.length > 0;
} }
/** /**
@ -607,11 +529,11 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
} }
/** /**
* Checks if categories are available. * Checks motion-blocks are available.
* *
* @returns A boolean if they are available. * @returns A boolean if they are available.
*/ */
public isCategoryAvailable(): boolean { public isMotionBlockAvailable(): boolean {
return !!this.categories && this.categories.length > 0; return !!this.motionBlocks && this.motionBlocks.length > 0;
} }
} }

View File

@ -1,33 +1,32 @@
<!-- <os-head-bar [mainButton]=true (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect"> -->
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="true" (mainEvent)="onNewButton(newWorkflowDialog)"> <os-head-bar prevUrl="../.." [nav]="false" [mainButton]="true" (mainEvent)="onNewButton(newWorkflowDialog)">
<!-- Title --> <!-- Title -->
<div class="title-slot"><h2 translate>Workflows</h2></div> <div class="title-slot"><h2 translate>Workflows</h2></div>
</os-head-bar> </os-head-bar>
<mat-table class="os-headed-listview-table on-transition-fade" [dataSource]="dataSource"> <os-list-view-table
<!-- Name Column --> [repo]="workflowRepo"
<ng-container matColumnDef="name"> [columns]="tableColumnDefinition"
<mat-header-cell *matHeaderCellDef> scrollKey="workflow"
<span translate>Name</span> (dataSourceChange)="onDataSourceChange($event)"
</mat-header-cell> >
<mat-cell *matCellDef="let workflow" (click)="onClickWorkflow(workflow)"> <!-- Name column -->
{{ workflow.name | translate }} <div *pblNgridCellDef="'name'; value as name; row as workflow; rowContext as rowContext" class="cell-slot fill">
</mat-cell> <a
</ng-container> class="detail-link"
(click)="saveScrollIndex('workflow', rowContext.identity)"
[routerLink]="workflow.id"
*ngIf="!isMultiSelect"
></a>
<div translate>{{ name }}</div>
</div>
<!-- Delete Column --> <!-- Delete column -->
<ng-container matColumnDef="delete"> <div *pblNgridCellDef="'delete'; row as workflow" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef> </mat-header-cell> <button type="button" mat-icon-button (click)="onDeleteWorkflow(workflow)">
<mat-cell *matCellDef="let workflow"> <mat-icon color="warn">delete</mat-icon>
<button type="button" mat-icon-button (click)="onDeleteWorkflow(workflow)"> </button>
<mat-icon color="warn">delete</mat-icon> </div>
</button> </os-list-view-table>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row *matRowDef="let row; columns: getColumnDefinition()"></mat-row>
</mat-table>
<!-- New workflow dialog --> <!-- New workflow dialog -->
<ng-template #newWorkflowDialog> <ng-template #newWorkflowDialog>

View File

@ -2,11 +2,11 @@ import { Component, OnInit, TemplateRef } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ListViewBaseComponent } from 'app/site/base/list-view-base'; import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { MatSnackBar, MatDialog } from '@angular/material'; import { MatSnackBar, MatDialog } from '@angular/material';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { Router, ActivatedRoute } from '@angular/router';
import { ViewWorkflow } from 'app/site/motions/models/view-workflow'; import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service'; import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { Workflow } from 'app/shared/models/motions/workflow'; import { Workflow } from 'app/shared/models/motions/workflow';
@ -20,17 +20,24 @@ import { StorageService } from 'app/core/core-services/storage.service';
templateUrl: './workflow-list.component.html', templateUrl: './workflow-list.component.html',
styleUrls: ['./workflow-list.component.scss'] styleUrls: ['./workflow-list.component.scss']
}) })
export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, Workflow, WorkflowRepositoryService> export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow> implements OnInit {
implements OnInit {
/** /**
* Holds the new workflow title * Holds the new workflow title
*/ */
public newWorkflowTitle: string; public newWorkflowTitle: string;
/** /**
* Determine the coloms in the table * Define the columns to show
*/ */
private columns: string[] = ['name', 'delete']; public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'name',
width: 'auto'
},
{
prop: 'delete'
}
];
/** /**
* Constructor * Constructor
@ -39,8 +46,6 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
* @param matSnackBar Showing errors * @param matSnackBar Showing errors
* @param translate handle trandlations * @param translate handle trandlations
* @param dialog Dialog options * @param dialog Dialog options
* @param router navigating back and forth
* @param route Information about the current router
* @param workflowRepo Repository for Workflows * @param workflowRepo Repository for Workflows
* @param promptService Before delete, ask * @param promptService Before delete, ask
*/ */
@ -48,14 +53,12 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
titleService: Title, titleService: Title,
protected translate: TranslateService, protected translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService, storage: StorageService,
private dialog: MatDialog, private dialog: MatDialog,
private router: Router, public workflowRepo: WorkflowRepositoryService,
protected workflowRepo: WorkflowRepositoryService,
private promptService: PromptService private promptService: PromptService
) { ) {
super(titleService, translate, matSnackBar, workflowRepo, route, storage); super(titleService, translate, matSnackBar, storage);
} }
/** /**
@ -63,17 +66,6 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Workflows'); super.setTitle('Workflows');
this.initTable();
this.workflowRepo.getViewModelListObservable().subscribe(newWorkflows => (this.dataSource.data = newWorkflows));
}
/**
* Click a workflow in the table
*
* @param selected the selected workflow
*/
public onClickWorkflow(selected: ViewWorkflow): void {
this.router.navigate([`${selected.id}`], { relativeTo: this.route });
} }
/** /**
@ -106,13 +98,4 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
this.workflowRepo.delete(selected).then(() => {}, this.raiseError); this.workflowRepo.delete(selected).then(() => {}, this.raiseError);
} }
} }
/**
* Get the column definition for the current workflow table
*
* @returns The column definition for the table
*/
public getColumnDefinition(): string[] {
return this.columns;
}
} }

View File

@ -33,13 +33,17 @@
</div> </div>
</os-head-bar> </os-head-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <os-list-view-table
<ng-container matColumnDef="name"> [repo]="repo"
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> [columns]="tableColumnDefinition"
<mat-cell *matCellDef="let tag">{{ tag.getTitle() }}</mat-cell> [allowProjector]="false"
</ng-container> [(selectedRows)]="selectedRows"
<mat-header-row *matHeaderRowDef="['name']"></mat-header-row> (dataSourceChange)="onDataSourceChange($event)"
<mat-row (click)="selectItem(row, $event)" *matRowDef="let row; columns: ['name']"></mat-row> >
</mat-table> <!-- Name column -->
<div *pblNgridCellDef="'name'; value as name; row as tag" class="cell-slot fill clickable" (click)="selectTag(tag)">
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator> <div>
{{ name }}
</div>
</div>
</os-list-view-table>

View File

@ -1,16 +1,15 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { FormGroup, FormControl, Validators } from '@angular/forms'; import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ListViewBaseComponent } from '../../../base/list-view-base';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { Tag } from 'app/shared/models/core/tag'; import { Tag } from 'app/shared/models/core/tag';
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service'; import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ListViewBaseComponent } from '../../../base/list-view-base';
import { ViewTag } from '../../models/view-tag'; import { ViewTag } from '../../models/view-tag';
/** /**
@ -23,9 +22,9 @@ import { ViewTag } from '../../models/view-tag';
@Component({ @Component({
selector: 'os-tag-list', selector: 'os-tag-list',
templateUrl: './tag-list.component.html', templateUrl: './tag-list.component.html',
styleUrls: ['./tag-list.component.css'] styleUrls: ['./tag-list.component.scss']
}) })
export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRepositoryService> implements OnInit { export class TagListComponent extends ListViewBaseComponent<ViewTag> implements OnInit {
public editTag = false; public editTag = false;
public newTag = false; public newTag = false;
public selectedTag: ViewTag; public selectedTag: ViewTag;
@ -33,6 +32,16 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRep
@ViewChild('tagForm') @ViewChild('tagForm')
public tagForm: FormGroup; public tagForm: FormGroup;
/**
* Define the columns to show
*/
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'name',
width: 'auto'
}
];
/** /**
* Constructor. * Constructor.
* @param titleService * @param titleService
@ -43,14 +52,12 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRep
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
route: ActivatedRoute, public repo: TagRepositoryService,
storage: StorageService, protected translate: TranslateService, // protected required for ng-translate-extract
private repo: TagRepositoryService,
private promptService: PromptService private promptService: PromptService
) { ) {
super(titleService, translate, matSnackBar, repo, route, storage); super(titleService, translate, matSnackBar);
} }
/** /**
@ -59,13 +66,7 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRep
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Tags'); super.setTitle('Tags');
this.initTable();
this.tagForm = new FormGroup({ name: new FormControl('', Validators.required) }); this.tagForm = new FormGroup({ name: new FormControl('', Validators.required) });
// TODO Tag has not yet sort or filtering functions
this.repo.getViewModelListObservable().subscribe(newTags => {
this.dataSource.data = [];
this.dataSource.data = newTags;
});
} }
/** /**
@ -116,7 +117,7 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRep
} }
/** /**
* Canceles the editing * Cancels the editing
*/ */
public cancelEditing(): void { public cancelEditing(): void {
this.newTag = false; this.newTag = false;
@ -128,7 +129,7 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRep
* Handler for a click on a row in the table * Handler for a click on a row in the table
* @param viewTag * @param viewTag
*/ */
public singleSelectAction(viewTag: ViewTag): void { public selectTag(viewTag: ViewTag): void {
this.selectedTag = viewTag; this.selectedTag = viewTag;
this.setEditMode(true, false); this.setEditMode(true, false);
this.tagForm.setValue({ name: this.selectedTag.name }); this.tagForm.setValue({ name: this.selectedTag.name });

View File

@ -14,114 +14,76 @@
</div> </div>
</os-head-bar> </os-head-bar>
<mat-drawer-container class="on-transition-fade"> <os-list-view-table
<os-sort-filter-bar [repo]="repo"
[filterCount]="filteredCount" [filterService]="filterService"
[sortService]="sortService" [sortService]="sortService"
[filterService]="filterService" [columns]="tableColumnDefinition"
(searchFieldChange)="searchFilter($event)" [multiSelect]="isMultiSelect"
[hiddenInMobile]="['group']"
scrollKey="user"
[(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)"
>
<!-- Name column -->
<div *pblNgridCellDef="'short_name'; value as name; row as user; rowContext as rowContext" class="cell-slot fill">
<a
class="detail-link"
(click)="saveScrollIndex('user', rowContext.identity)"
[routerLink]="user.id"
*ngIf="!isMultiSelect"
></a>
<div>
{{ name }}
</div>
</div>
<!-- group column -->
<div
*pblNgridCellDef="'group'; row as user"
class="cell-slot fill"
[ngClass]="isMultiSelect ? '' : 'clickable'"
(click)="openEditInfo(user, $event)"
> >
</os-sort-filter-bar> <div class="groupsCell">
<div *ngIf="user.groups && user.groups.length">
<os-icon-container icon="people">
<span *ngFor="let group of user.groups; let last = last">
{{ group.getTitle() | translate }} <span *ngIf="!last">,&nbsp;</span>
</span>
</os-icon-container>
</div>
<div *ngIf="user.structure_level" class="spacer-top-5">
<os-icon-container icon="flag">{{ user.structure_level }}</os-icon-container>
</div>
<div *ngIf="user.number" class="spacer-top-5">
<os-icon-container icon="perm_identity">{{ user.number }}</os-icon-container>
</div>
</div>
</div>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <!-- Info column -->
<!-- Selector column --> <div *pblNgridCellDef="'infos'; row as user" class="cell-slot fill">
<ng-container matColumnDef="selector"> <div class="infoCell">
<mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell> <mat-icon
<mat-cell *matCellDef="let user"> inline
<mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon> *ngIf="user.is_last_email_send"
</mat-cell> matTooltip="{{ 'Email sent' | translate }} ({{ getEmailSentTime(user) }})"
</ng-container> >
mail
</mat-icon>
</div>
</div>
<!-- Projector column --> <!-- Presence column -->
<ng-container matColumnDef="projector"> <div *pblNgridCellDef="'presence'; row as user" class="cell-slot fill">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell> <div *ngIf="user.is_active">
<mat-cell *matCellDef="let user"> <mat-checkbox (change)="setPresent(user)" [checked]="user.is_present" [disabled]="isMultiSelect">
<os-projector-button [object]="user"></os-projector-button> <span translate>Present</span>
</mat-cell> </mat-checkbox>
</ng-container> </div>
</div>
<!-- name column --> </os-list-view-table>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let user">
{{ user.short_name }}
</mat-cell>
</ng-container>
<!-- group column -->
<ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
<mat-cell (click)="openEditInfo(user, $event)" *matCellDef="let user">
<div class="fill">
<div class="groupsCell">
<div *ngIf="user.groups && user.groups.length">
<os-icon-container icon="people">
<span *ngFor="let group of user.groups; let last = last">
{{ group.getTitle() | translate }} <span *ngIf="!last">,&nbsp;</span>
</span>
</os-icon-container>
</div>
<div *ngIf="user.structure_level" class="spacer-top-5">
<os-icon-container icon="flag">{{ user.structure_level }}</os-icon-container>
</div>
<div *ngIf="user.number" class="spacer-top-5">
<os-icon-container icon="perm_identity">{{ user.number }}</os-icon-container>
</div>
</div>
</div>
</mat-cell>
</ng-container>
<!-- Anchor column to open separate tab -->
<ng-container matColumnDef="anchor">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let user">
<a [routerLink]="user.id" *ngIf="!isMultiSelect"></a>
</mat-cell>
</ng-container>
<!-- Infos column -->
<ng-container matColumnDef="infos">
<mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell>
<mat-cell *matCellDef="let user" class="infoCell">
<div>
<mat-icon inline *ngIf="user.is_last_email_send"
matTooltip="{{ 'Email sent' | translate }} ({{ getEmailSentTime(user) }})">
mail
</mat-icon>
</div>
</mat-cell>
</ng-container>
<!-- Presence column -->
<ng-container matColumnDef="presence">
<mat-header-cell *matHeaderCellDef mat-sort-header>Presence</mat-header-cell>
<mat-cell (click)="ignoreClick($event)" *matCellDef="let user" class="presentCell">
<div class="fill" *ngIf="user.is_active">
<mat-checkbox
class="checkboxPresent"
(change)="setPresent(user)"
[checked]="user.is_present"
[disabled]="isMultiSelect"
>
<span translate>Present</span>
</mat-checkbox>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
*matRowDef="let row; columns: getColumnDefinition()"
(click)="selectItem(row, $event)"
class="lg"
>
</mat-row>
</mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
</mat-drawer-container>
<mat-menu #userMenu="matMenu"> <mat-menu #userMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">

View File

@ -1,51 +1,11 @@
@import '~assets/styles/tables.scss'; @import '~assets/styles/tables.scss';
.os-listview-table { .groupsCell {
.mat-column-projector { text-overflow: ellipsis;
padding-right: 15px; overflow: hidden;
} white-space: nowrap;
.mat-column-name { mat-icon {
flex: 1 0 200px; font-size: 80%;
}
.mat-column-group {
flex: 2 0 60px;
.groupsCell {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
mat-icon {
font-size: 80%;
}
}
}
.mat-column-presence {
flex: 0 0 60px;
mat-icon {
font-size: 100%;
margin-right: 5px;
}
div {
display: inherit;
}
} }
} }
.infoCell {
max-width: 25px;
}
.presentCell {
align-content: left;
padding-right: 50px;
}
.checkboxPresent {
margin-left: 15px;
}

View File

@ -2,7 +2,9 @@ import { Component, OnInit, ViewChild, TemplateRef } from '@angular/core';
import { MatSnackBar, MatDialog } from '@angular/material'; import { MatSnackBar, MatDialog } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ChoiceService } from 'app/core/ui-services/choice.service'; import { ChoiceService } from 'app/core/ui-services/choice.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
@ -14,11 +16,10 @@ import { UserFilterListService } from '../../services/user-filter-list.service';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { UserPdfExportService } from '../../services/user-pdf-export.service'; import { UserPdfExportService } from '../../services/user-pdf-export.service';
import { UserSortListService } from '../../services/user-sort-list.service'; import { UserSortListService } from '../../services/user-sort-list.service';
import { ViewportService } from 'app/core/ui-services/viewport.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { ViewUser } from '../../models/view-user'; import { ViewUser } from '../../models/view-user';
import { ViewGroup } from '../../models/view-group'; import { ViewGroup } from '../../models/view-group';
import { genders, User } from 'app/shared/models/users/user'; import { genders } from 'app/shared/models/users/user';
import { _ } from 'app/core/translate/translation-marker'; import { _ } from 'app/core/translate/translation-marker';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
@ -61,7 +62,7 @@ interface InfoDialog {
templateUrl: './user-list.component.html', templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss'] styleUrls: ['./user-list.component.scss']
}) })
export class UserListComponent extends ListViewBaseComponent<ViewUser, User, UserRepositoryService> implements OnInit { export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
/** /**
* The reference to the template. * The reference to the template.
*/ */
@ -83,16 +84,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
*/ */
public genderList = genders; public genderList = genders;
/**
* Columns to display in table when desktop view is available
*/
public displayedColumnsDesktop: string[] = ['name', 'group', 'anchor'];
/**
* Columns to display in table when mobile view is available
*/
public displayedColumnsMobile = ['name', 'anchor'];
/** /**
* Stores the observed configuration if the presence view is available to administrators * Stores the observed configuration if the presence view is available to administrators
*/ */
@ -114,6 +105,27 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
return this.operator.hasPerms('users.can_manage'); return this.operator.hasPerms('users.can_manage');
} }
/**
* Define the columns to show
*/
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'short_name',
width: 'auto'
},
{
prop: 'group',
width: '15%'
},
{
prop: 'infos'
},
{
prop: 'presence',
width: '100px'
}
];
/** /**
* The usual constructor for components * The usual constructor for components
* @param titleService Serivce for setting the title * @param titleService Serivce for setting the title
@ -124,7 +136,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
* @param router the router service * @param router the router service
* @param route the local route * @param route the local route
* @param operator * @param operator
* @param vp
* @param csvExport CSV export Service, * @param csvExport CSV export Service,
* @param promptService * @param promptService
* @param groupRepo * @param groupRepo
@ -137,14 +148,13 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
titleService: Title, titleService: Title,
protected translate: TranslateService, // protected required for ng-translate-extract protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
route: ActivatedRoute, private route: ActivatedRoute,
storage: StorageService, storage: StorageService,
private repo: UserRepositoryService, public repo: UserRepositoryService,
private groupRepo: GroupRepositoryService, private groupRepo: GroupRepositoryService,
private choiceService: ChoiceService, private choiceService: ChoiceService,
private router: Router, private router: Router,
private operator: OperatorService, private operator: OperatorService,
private vp: ViewportService,
protected csvExport: CsvExportService, protected csvExport: CsvExportService,
private promptService: PromptService, private promptService: PromptService,
public filterService: UserFilterListService, public filterService: UserFilterListService,
@ -153,7 +163,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
private userPdf: UserPdfExportService, private userPdf: UserPdfExportService,
private dialog: MatDialog private dialog: MatDialog
) { ) {
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService); super(titleService, translate, matSnackBar, storage);
// enable multiSelect for this listView // enable multiSelect for this listView
this.canMultiSelect = true; this.canMultiSelect = true;
@ -168,8 +178,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Participants'); super.setTitle('Participants');
this.initTable();
this.setFulltextFilter();
// Initialize the groups // Initialize the groups
this.groups = this.groupRepo.getSortedViewModelList().filter(group => group.id !== 1); this.groups = this.groupRepo.getSortedViewModelList().filter(group => group.id !== 1);
@ -178,14 +186,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1))); .subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)));
} }
/**
* Handles the click on a user row if not in multiSelect modus
* @param row selected row
*/
public singleSelectAction(row: ViewUser): void {
this.router.navigate([`./${row.id}`], { relativeTo: this.route });
}
/** /**
* Handles the click on the plus button * Handles the click on the plus button
*/ */
@ -239,7 +239,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
*/ */
public csvExportUserList(): void { public csvExportUserList(): void {
this.csvExport.export( this.csvExport.export(
this.dataSource.filteredData, this.dataSource.source,
[ [
{ property: 'title' }, { property: 'title' },
{ property: 'first_name', label: 'Given name' }, { property: 'first_name', label: 'Given name' },
@ -264,7 +264,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
* (access information, including personal information such as initial passwords) * (access information, including personal information such as initial passwords)
*/ */
public onDownloadAccessPdf(): void { public onDownloadAccessPdf(): void {
this.userPdf.exportMultipleUserAccessPDF(this.dataSource.data); this.userPdf.exportMultipleUserAccessPDF(this.dataSource.source);
} }
/** /**
@ -272,7 +272,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
* with all users currently matching the filter * with all users currently matching the filter
*/ */
public pdfExportUserList(): void { public pdfExportUserList(): void {
this.userPdf.exportUserList(this.dataSource.data); this.userPdf.exportUserList(this.dataSource.source);
} }
/** /**
@ -403,25 +403,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
} }
} }
/**
* returns the column definition
*
* @returns column definition
*/
public getColumnDefinition(): string[] {
let columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
if (this.operator.hasPerms('core.can_manage_projector') && !this.isMultiSelect) {
columns = ['projector'].concat(columns);
}
if (this.operator.hasPerms('users.can_manage')) {
columns = columns.concat(['infos', 'presence']);
}
if (this.isMultiSelect) {
columns = ['selector'].concat(columns);
}
return columns;
}
/** /**
* Sets the user present * Sets the user present
* *
@ -436,14 +417,16 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
/** /**
* Overwrites the dataSource's string filter with a case-insensitive search * Overwrites the dataSource's string filter with a case-insensitive search
* in the full_name property * in the full_name property
*
* TODO: Filter predicates will be missed :(
*/ */
private setFulltextFilter(): void { // private setFulltextFilter(): void {
this.dataSource.filterPredicate = (data, filter) => { // this.dataSource.filterPredicate = (data, filter) => {
if (!data || !data.full_name) { // if (!data || !data.full_name) {
return false; // return false;
} // }
filter = filter ? filter.toLowerCase() : ''; // filter = filter ? filter.toLowerCase() : '';
return data.full_name.toLowerCase().indexOf(filter) >= 0; // return data.full_name.toLowerCase().indexOf(filter) >= 0;
}; // };
} // }
} }

View File

@ -20,6 +20,10 @@
color: mat-color($primary); color: mat-color($primary);
} }
.anchor-button {
color: mat-color($foreground, text) !important;
}
.accent, .accent,
.accent-text { .accent-text {
color: mat-color($accent); color: mat-color($accent);

View File

@ -25,6 +25,9 @@
@import './assets/styles/fonts.scss'; @import './assets/styles/fonts.scss';
@import '~material-icon-font/dist/Material-Icons.css'; @import '~material-icon-font/dist/Material-Icons.css';
/** NGrid */
@import '~@pebula/ngrid/theming';
/** Mix the component-related style-rules */ /** Mix the component-related style-rules */
@mixin openslides-components-theme($theme) { @mixin openslides-components-theme($theme) {
@include os-site-theme($theme); @include os-site-theme($theme);
@ -34,7 +37,6 @@
@include os-sorting-tree-style($theme); @include os-sorting-tree-style($theme);
@include os-global-spinner-theme($theme); @include os-global-spinner-theme($theme);
@include os-tile-style($theme); @include os-tile-style($theme);
/** More components are added here */
} }
/** date-time-picker */ /** date-time-picker */
@ -52,6 +54,13 @@
@include angular-material-theme($openslides-theme); @include angular-material-theme($openslides-theme);
@include openslides-components-theme($openslides-theme); @include openslides-components-theme($openslides-theme);
/** NGrid OS Theme */
$ngrid-material-theme: pbl-light-theme($openslides-theme);
$narrow-spacing: (
spacing: $pbl-spacing-theme-narrow
);
@include pbl-ngrid-theme(map-merge($ngrid-material-theme, $narrow-spacing));
.logo-container { .logo-container {
img.dark { img.dark {
display: none; display: none;
@ -139,6 +148,46 @@
} }
} }
/**
* Patches to NGrid Classes
*/
.pbl-ngrid-row:hover {
background-color: rgba(0, 0, 0, 0.025);
}
.pbl-ngrid-cell {
position: relative;
.fill {
display: inherit;
height: 100%;
width: 100%;
// try to put all children in the in the vertical middle
* {
margin-top: auto;
margin-bottom: auto;
}
}
.clickable {
cursor: pointer;
}
.detail-link {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
.ngrid-lg {
height: 110px;
min-height: 90px;
}
/** Define the general style-rules */ /** Define the general style-rules */
* { * {
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif; font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
@ -580,6 +629,17 @@ button.mat-menu-item.selected {
height: calc(100vh - 128px); height: calc(100vh - 128px);
} }
.virtual-scroll-with-head-bar {
height: calc(100vh - 189px);
// For some reason, hiding the table header adds an empty meta bar.
.pbl-ngrid-container {
> div {
display: none;
}
}
}
/** media queries */ /** media queries */
/* medium to small */ /* medium to small */