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:
- npm install
script:
- ng build --prod
- npm run ng-high-memory build --prod

View File

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

View File

@ -35,5 +35,7 @@ os-icon-container {
.content-node {
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="filter-count" *ngIf="filterService && showFilterSort">
<!-- Amount of filters -->
<div class="filter-count" *ngIf="filterService">
<span>{{ displayedCount }}&nbsp;</span><span translate>of</span>
<span>&nbsp;{{ totalCount }}</span>
<span *ngIf="extraItemInfo">&nbsp;·&nbsp;{{ extraItemInfo }}</span>
</div>
<div class="filter-count" *ngIf="!showFilterSort && itemsVerboseName">
<span>{{ totalCount }}&nbsp;</span><span>{{ itemsVerboseName | translate }}</span>
</div>
<!-- Current filters -->
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount">
<div><span translate>Active filters</span>:&nbsp;</div>
<div>
@ -22,12 +22,11 @@
</button>
</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">
{{ filterService.activeFilterCount }}&nbsp;
@ -35,12 +34,16 @@
<span *ngIf="filterService.activeFilterCount > 1" class="upper" translate>Filters</span>
</span>
</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>
</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>
</button>
<!-- Search bar -->
<mat-form-field *ngIf="isSearchBar">
<input
osAutofocus

View File

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

View File

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

View File

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

View File

@ -59,6 +59,10 @@ import { PermsDirective } from './directives/perms.directive';
import { DomChangeDirective } from './directives/dom-change.directive';
import { AutofocusDirective } from './directives/autofocus.directive';
// PblNgrid. Cleanup Required.
import { PblNgridModule } from '@pebula/ngrid';
import { PblNgridMaterialModule } from '@pebula/ngrid-material';
// components
import { HeadBarComponent } from './components/head-bar/head-bar.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 { BlockTileComponent } from './components/block-tile/block-tile.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.
@ -148,7 +153,9 @@ import { IconContainerComponent } from './components/icon-container/icon-contain
FileDropModule,
EditorModule,
CdkTreeModule,
ScrollingModule
ScrollingModule,
PblNgridModule,
PblNgridMaterialModule
],
exports: [
FormsModule,
@ -219,7 +226,11 @@ import { IconContainerComponent } from './components/icon-container/icon-contain
TileComponent,
BlockTileComponent,
ScrollingModule,
IconContainerComponent
IconContainerComponent,
SpeakerButtonComponent,
PblNgridModule,
PblNgridMaterialModule,
ListViewTableComponent
],
declarations: [
PermsDirective,
@ -252,7 +263,8 @@ import { IconContainerComponent } from './components/icon-container/icon-contain
GridLayoutComponent,
TileComponent,
BlockTileComponent,
IconContainerComponent
IconContainerComponent,
ListViewTableComponent
],
providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },

View File

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

View File

@ -1,44 +1,20 @@
@import '~assets/styles/tables.scss';
.os-listview-table {
/** Title */
.mat-column-title {
padding-left: 26px;
flex: 2 0 0;
.info-col-items {
display: inline-block;
white-space: nowrap;
.done-check {
margin-right: 10px;
}
}
/** Duration */
.mat-column-info {
flex: 2 0 0;
.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;
font-size: 14px;
.mat-icon {
display: inline-flex;
vertical-align: middle;
$icon-size: 18px;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
}
}
.done-check {
margin-right: 10px;
}

View File

@ -2,27 +2,28 @@ import { Component, OnInit } from '@angular/core';
import { MatSnackBar, MatDialog } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { AgendaCsvExportService } from '../../services/agenda-csv-export.service';
import { AgendaFilterListService } from '../../services/agenda-filter-list.service';
import { AgendaPdfService } from '../../services/agenda-pdf.service';
import { ConfigService } from 'app/core/ui-services/config.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 { 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 { 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 { 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 { 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 { _ } from 'app/core/translate/translation-marker';
/**
* List view for the agenda.
@ -32,18 +33,10 @@ import { ViewListOfSpeakers } from '../../models/view-list-of-speakers';
templateUrl: './agenda-list.component.html',
styleUrls: ['./agenda-list.component.scss']
})
export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, ItemRepositoryService>
implements OnInit {
export class AgendaListComponent extends ListViewBaseComponent<ViewItem> 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;
/**
@ -71,6 +64,28 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
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
* @param titleService Setting the browser tab title
@ -98,7 +113,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
private operator: OperatorService,
protected route: ActivatedRoute,
private router: Router,
private repo: ItemRepositoryService,
public repo: ItemRepositoryService,
private promptService: PromptService,
private dialog: MatDialog,
private config: ConfigService,
@ -110,9 +125,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
private pdfService: PdfDocumentService,
private listOfSpeakersRepo: ListOfSpeakersRepositoryService
) {
super(titleService, translate, matSnackBar, repo, route, storage, filterService);
// activate multiSelect mode for this listview
super(titleService, translate, matSnackBar, storage);
this.canMultiSelect = true;
}
@ -122,11 +135,9 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
*/
public ngOnInit(): void {
super.setTitle('Agenda');
this.initTable();
this.config
.get<boolean>('agenda_enable_numbering')
.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
*/
public singleSelectAction(item: ViewItem): void {
if (item.contentObject) {
this.router.navigate([item.contentObject.getDetailStateURL()]);
public getDetailUrl(item: ViewItem): string {
if (item.contentObject && !this.isMultiSelect) {
return item.contentObject.getDetailStateURL();
}
}
@ -156,11 +167,10 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, I
*
* @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) {
return;
}
event.stopPropagation();
const dialogRef = this.dialog.open(ItemInfoDialogComponent, {
width: '400px',
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
*/
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 {
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
* in the item number and title
*
* TODO: Filter predicates will be missed :(
*/
private setFulltextFilter(): void {
this.dataSource.filterPredicate = (data, filter) => {
if (!data) {
return false;
}
filter = filter ? filter.toLowerCase() : '';
return (
data.itemNumber.toLowerCase().includes(filter) ||
data
.getListTitle()
.toLowerCase()
.includes(filter)
);
};
}
// private setFulltextFilter(): void {
// this.dataSource.filterPredicate = (data, filter) => {
// if (!data) {
// return false;
// }
// filter = filter ? filter.toLowerCase() : '';
// return (
// data.itemNumber.toLowerCase().includes(filter) ||
// data
// .getListTitle()
// .toLowerCase()
// .includes(filter)
// );
// };
// }
}

View File

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

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 { 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 { AssignmentSortListService } from '../../services/assignment-sort-list.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',
styleUrls: ['./assignment-list.component.scss']
})
export class AssignmentListComponent
extends ListViewBaseComponent<ViewAssignment, Assignment, AssignmentRepositoryService>
implements OnInit {
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment> implements OnInit {
/**
* The different phases of an assignment. Info is fetched from server
*/
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.
*
@ -61,8 +78,7 @@ export class AssignmentListComponent
private router: Router,
public operator: OperatorService
) {
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
// activate multiSelect mode for this list view
super(titleService, translate, matSnackBar, storage);
this.canMultiSelect = true;
}
@ -72,7 +88,6 @@ export class AssignmentListComponent
*/
public ngOnInit(): void {
super.setTitle('Elections');
this.initTable();
}
/**
@ -83,16 +98,6 @@ export class AssignmentListComponent
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
*
@ -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 { ViewChild, Type, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { OnDestroy } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { PblDataSource, PblColumnDefinition } from '@pebula/ngrid';
import { BaseViewComponent } from './base-view';
import { BaseViewModel, TitleInformation } 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 { BaseViewModel } from './base-view-model';
import { StorageService } from 'app/core/core-services/storage.service';
import { BaseRepository } from 'app/core/repositories/base-repository';
export abstract class ListViewBaseComponent<
V extends BaseViewModel,
M extends BaseModel,
R extends BaseRepository<V, M, TitleInformation>
> extends BaseViewComponent implements OnDestroy {
export abstract class ListViewBaseComponent<V extends BaseViewModel> 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)
@ -37,235 +28,52 @@ export abstract class ListViewBaseComponent<
/**
* An array of currently selected items, upon which multi select actions can be performed
* see {@link selectItem}.
* Filled using double binding from list-view-tables
*/
public selectedRows: V[];
/**
* Holds the key for the storage.
* This is by default the component's name.
* Force children to have a tableColumnDefinition
*/
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
*/
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 titleService the title service
* @param translate the translate service
* @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 modelFilterListService filter do NOT rename to "filterListService"
* @param modelSortService sorting do NOT rename to "sortService"
*/
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
protected viewModelRepo: R,
protected route?: ActivatedRoute,
protected storage?: StorageService,
protected modelFilterListService?: BaseFilterListService<V>,
protected modelSortService?: BaseSortListService<V>
protected storage?: StorageService
) {
super(titleService, translate, matSnackBar);
this.selectedRows = [];
try {
this.paginationStorageKey = (<Type<any>>route.component).name;
} catch (e) {
this.paginationStorageKey = '';
}
}
/**
* Children need to call this in their init-function.
* 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'
* Detect changes to data source
*
* @param event the string to search for
* @param newDataSource
*/
public searchFilter(event: string): void {
this.dataSource.filter = event;
public onDataSourceChange(newDataSource: PblDataSource<V>): void {
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
*/
public toggleMultiSelect(): void {
if (!this.canMultiSelect || this.isMultiSelect) {
this._multiSelectMode = false;
this.clearSelection();
this.deselectAll();
} else {
this._multiSelectMode = true;
}
@ -275,11 +83,16 @@ export abstract class ListViewBaseComponent<
* Select all files in the current data source
*/
public selectAll(): void {
this.selectedRows = this.dataSource.filteredData;
this.dataSource.selection.select(...this.dataSource.source);
}
/**
* Handler to quickly unselect all items.
*/
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.
* @param item The row's entry
* Saves the scroll index in the storage
*
* @param key
* @param index
*/
public isSelected(item: V): boolean {
if (!this._multiSelectMode) {
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;
public saveScrollIndex(key: string, index: number): void {
this.storage.set(`scroll_${key}`, index);
}
}

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 {
font-style: italic;
color: slategray; // TODO: Colors per theme

View File

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

View File

@ -1,33 +1,5 @@
@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
::ng-deep .mat-tooltip {
white-space: pre-line !important;

View File

@ -5,13 +5,13 @@ import { Title } from '@angular/platform-browser';
import { MatSnackBar, MatDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ListViewBaseComponent } from '../../../base/list-view-base';
import { ViewMediafile } from '../../models/view-mediafile';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
import { MediaManageService } from 'app/core/ui-services/media-manage.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 { MediafilesSortListService } from '../../services/mediafiles-sort-list.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',
styleUrls: ['./mediafile-list.component.scss']
})
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile, Mediafile, MediafileRepositoryService>
implements OnInit {
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile> implements OnInit {
/**
* Holds the actions for logos. Updated via an observable
*/
@ -38,16 +37,6 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
*/
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
*/
@ -84,6 +73,28 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
@ViewChild('fileEditDialog')
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
*
@ -104,10 +115,10 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
titleService: Title,
protected translate: TranslateService,
matSnackBar: MatSnackBar,
route: ActivatedRoute,
private route: ActivatedRoute,
storage: StorageService,
private router: Router,
private repo: MediafileRepositoryService,
public repo: MediafileRepositoryService,
private mediaManage: MediaManageService,
private promptService: PromptService,
public vp: ViewportService,
@ -117,9 +128,7 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
private dialog: MatDialog,
private fb: FormBuilder
) {
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
// enables multiSelection for this listView
super(titleService, translate, matSnackBar, storage);
this.canMultiSelect = true;
}
@ -129,7 +138,6 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
*/
public ngOnInit(): void {
super.setTitle('Files');
this.initTable();
// Observe the logo actions
this.mediaManage.getLogoActions().subscribe(action => {
@ -140,7 +148,6 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
this.mediaManage.getFontActions().subscribe(action => {
this.fontActions = action;
});
this.setFulltextFilter();
}
/**
@ -284,34 +291,6 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
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.
*
@ -326,14 +305,16 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
/**
* Overwrites the dataSource's string filter with a case-insensitive search
* in the file name property
*
* TODO: Filter predicates will be missed :(
*/
private setFulltextFilter(): void {
this.dataSource.filterPredicate = (data, filter) => {
if (!data || !data.title) {
return false;
}
filter = filter ? filter.toLowerCase() : '';
return data.title.toLowerCase().indexOf(filter) >= 0;
};
}
// private setFulltextFilter(): void {
// this.dataSource.filterPredicate = (data, filter) => {
// if (!data || !data.title) {
// return false;
// }
// filter = filter ? filter.toLowerCase() : '';
// return data.title.toLowerCase().indexOf(filter) >= 0;
// };
// }
}

View File

@ -1,17 +1,14 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { MatSnackBar, MatTableDataSource } from '@angular/material';
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 { 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 { ViewCategory } from 'app/site/motions/models/view-category';
import { BaseViewComponent } from 'app/site/base/base-view';
/**
* Table for categories
@ -21,13 +18,17 @@ import { ViewCategory } from 'app/site/motions/models/view-category';
templateUrl: './category-list.component.html',
styleUrls: ['./category-list.component.scss']
})
export class CategoryListComponent extends ListViewBaseComponent<ViewCategory, Category, CategoryRepositoryService>
implements OnInit {
export class CategoryListComponent extends BaseViewComponent implements OnInit {
/**
* Holds the create form
*/
public createForm: FormGroup;
/**
* Table data Source
*/
public dataSource: MatTableDataSource<ViewCategory>;
/**
* Flag, if the creation panel is open
*/
@ -51,20 +52,17 @@ export class CategoryListComponent extends ListViewBaseComponent<ViewCategory, C
* @param storage
* @param repo
* @param formBuilder
* @param promptService
* @param operator
*/
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService,
private repo: CategoryRepositoryService,
private formBuilder: FormBuilder,
private operator: OperatorService
) {
super(titleService, translate, matSnackBar, repo, route, storage);
super(titleService, translate, matSnackBar);
this.createForm = this.formBuilder.group({
prefix: [''],
@ -78,7 +76,13 @@ export class CategoryListComponent extends ListViewBaseComponent<ViewCategory, C
*/
public ngOnInit(): void {
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>
</button>
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource">
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef> <span translate>Motion</span> </mat-header-cell>
<mat-cell *matCellDef="let motion">
{{ motion.getTitle() }}
</mat-cell>
</ng-container>
<pbl-ngrid
class="block-detail-table"
cellTooltip
showHeader="true"
vScrollFixed="80"
[dataSource]="dataSource"
[columns]="columnSet"
>
<!-- 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 -->
<ng-container matColumnDef="state">
<mat-header-cell *matHeaderCellDef> <span translate>State</span> </mat-header-cell>
<mat-cell class="chip-container" *matCellDef="let motion">
<!-- State column -->
<div *pblNgridCellDef="'state'; row as motion" class="cell-slot fill">
<div class="chip-container">
<mat-basic-chip disableRipple [ngClass]="motion.stateCssColor">
{{ getStateLabel(motion) }}
</mat-basic-chip>
</mat-cell>
</ng-container>
</div>
</div>
<!-- Recommendation column -->
<ng-container matColumnDef="recommendation">
<mat-header-cell *matHeaderCellDef> <span translate>Recommendation</span> </mat-header-cell>
<mat-cell class="chip-container" *matCellDef="let motion">
<mat-basic-chip *ngIf="motion.recommendation" disableRipple class="bluegrey">
{{ getRecommendationLabel(motion) }}
</mat-basic-chip>
</mat-cell>
</ng-container>
<div *pblNgridCellDef="'recommendation'; row as motion" class="cell-slot fill">
<mat-basic-chip *ngIf="!!motion.recommendation" disableRipple class="bluegrey">
{{ getRecommendationLabel(motion) }}
</mat-basic-chip>
</div>
<!-- Remove motion column -->
<ng-container matColumnDef="remove">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let motion">
<button
type="button"
mat-icon-button
color="warn"
matTooltip="{{ 'Remove from motion block' | translate }}"
(click)="onRemoveMotionButton(motion)"
>
<mat-icon>close</mat-icon>
</button>
</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>
<!-- Remove from block column -->
<div *pblNgridCellDef="'remove'; row as motion" class="cell-slot fill">
<button
type="button"
mat-icon-button
color="warn"
matTooltip="{{ 'Remove from motion block' | translate }}"
(click)="onRemoveMotionButton(motion)"
>
<mat-icon>close</mat-icon>
</button>
</div>
</pbl-ngrid>
</mat-card>
<!-- The menu content -->
<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>
@ -112,23 +102,17 @@
<div class="os-form-card-mobile" mat-dialog-content>
<form class="edit-form" [formGroup]="blockEditForm" (ngSubmit)="saveBlock()" (keydown)="onKeyDown($event)">
<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-checkbox formControlName="internal">Internal</mat-checkbox>
</form>
</div>
<div mat-dialog-actions>
<button
type="submit"
mat-button
[disabled]="!blockEditForm.valid"
color="primary"
(click)="saveBlock()"
>
<span translate>Save</span>
</button>
<button type="button" mat-button [mat-dialog-close]="null">
<span translate>Cancel</span>
</button>
</div>
<button type="submit" mat-button [disabled]="!blockEditForm.valid" color="primary" (click)="saveBlock()">
<span translate>Save</span>
</button>
<button type="button" mat-button [mat-dialog-close]="null">
<span translate>Cancel</span>
</button>
</div>
</ng-template>

View File

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

View File

@ -5,17 +5,15 @@ import { MatSnackBar, MatDialog } from '@angular/material';
import { Title } from '@angular/platform-browser';
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 { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-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 { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { StorageService } from 'app/core/core-services/storage.service';
import { Motion } from 'app/shared/models/motions/motion';
import { BaseViewComponent } from 'app/site/base/base-view';
/**
* 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',
styleUrls: ['./motion-block-detail.component.scss']
})
export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion, Motion, MotionRepositoryService>
implements OnInit {
export class MotionBlockDetailComponent extends BaseViewComponent implements OnInit {
/**
* Determines the block id from the given URL
*/
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
*/
@ -61,9 +99,7 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
titleService: Title,
protected translate: TranslateService,
matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService,
private operator: OperatorService,
private route: ActivatedRoute,
private router: Router,
protected repo: MotionBlockRepositoryService,
protected motionRepo: MotionRepositoryService,
@ -71,7 +107,7 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
private fb: FormBuilder,
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 {
super.setTitle('Motion block');
this.initTable();
const blockId = parseInt(this.route.snapshot.params.id, 10);
// pseudo filter
@ -88,33 +124,17 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
this.repo.getViewModelObservable(blockId).subscribe(newBlock => {
if (newBlock) {
this.block = newBlock;
this.subscriptions.push(
this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(viewMotions => {
if (viewMotions && viewMotions.length) {
this.dataSource.data = viewMotions;
} else {
this.dataSource.data = [];
}
this.dataSource = createDS<ViewMotion>()
.onTrigger(() => {
return this.repo.getViewMotionsByBlock(this.block.motionBlock);
})
);
.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
*/
@ -169,8 +189,8 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
* Following a recommendation implies, that a valid recommendation exists.
*/
public isFollowingProhibited(): boolean {
if (this.dataSource.data) {
return this.dataSource.data.every(motion => motion.isInFinalState() || !motion.recommendation_id);
if (this.dataSource && this.dataSource.source) {
return this.dataSource.source.every(motion => motion.isInFinalState() || !motion.recommendation_id);
} else {
return false;
}

View File

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

View File

@ -1,22 +1,5 @@
@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 {
width: 50%;
}

View File

@ -1,11 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { BehaviorSubject } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
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',
styleUrls: ['./motion-block-list.component.scss']
})
export class MotionBlockListComponent
extends ListViewBaseComponent<ViewMotionBlock, MotionBlock, MotionBlockRepositoryService>
implements OnInit {
export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBlock> implements OnInit {
/**
* Holds the create form
*/
@ -63,6 +61,21 @@ export class MotionBlockListComponent
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
*
@ -83,16 +96,15 @@ export class MotionBlockListComponent
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService,
private repo: MotionBlockRepositoryService,
public repo: MotionBlockRepositoryService,
private agendaRepo: ItemRepositoryService,
private formBuilder: FormBuilder,
private itemRepo: ItemRepositoryService,
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({
title: ['', Validators.required],
@ -107,24 +119,10 @@ export class MotionBlockListComponent
*/
public ngOnInit(): void {
super.setTitle('Motion blocks');
this.initTable();
this.items = this.itemRepo.getViewModelListBehaviorSubject();
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
*

View File

@ -14,99 +14,88 @@
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</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>
<mat-drawer-container class="on-transition-fade">
<os-sort-filter-bar
[filterCount]="filteredCount"
<ng-container *ngIf="selectedView === 'tiles'; then tiles; else list"></ng-container>
<ng-template #list>
<os-list-view-table
[repo]="motionRepo"
[filterService]="filterService"
[sortService]="sortService"
[showFilterSort]="selectedView === 'list'"
[itemsVerboseName]="motionsVerboseName"
(searchFieldChange)="searchFilter($event)"
[columns]="tableColumnDefinition"
[multiSelect]="isMultiSelect"
[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">
<mat-button-toggle value="tiles" matTooltip="{{ 'Tile view' | translate }}"><mat-icon>view_module</mat-icon></mat-button-toggle>
<mat-button-toggle value="list" matTooltip="{{ 'List view' | translate }}"><mat-icon>view_headline</mat-icon></mat-button-toggle>
</mat-button-toggle-group>
</os-sort-filter-bar>
<!-- Identifier -->
<div *pblNgridCellDef="'identifier'; row as motion" class="cell-slot fill">
<a class="detail-link" [routerLink]="motion.id" *ngIf="!isMultiSelect"></a>
<div class="column-identifier innerTable">
{{ motion.identifier }}
</div>
</div>
<div [ngSwitch]="selectedView">
<span *ngSwitchCase="'list'">
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<!-- Selector column -->
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell>
<mat-cell *matCellDef="let motion">
<mat-icon>{{ isSelected(motion) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- Title -->
<div *pblNgridCellDef="'title'; row as motion; rowContext as rowContext" class="cell-slot fill">
<a
class="detail-link"
(click)="saveScrollIndex('motion', rowContext.identity)"
[routerLink]="motion.id"
*ngIf="!isMultiSelect"
></a>
<div class="column-title innerTable">
<div class="title-line ellipsis-overflow">
<!-- Is Favorite -->
<span *ngIf="motion.star" class="favorite-star">
<mat-icon inline>star</mat-icon>
</span>
<!-- Projector column -->
<ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
<mat-cell *matCellDef="let motion">
<os-projector-button [object]="motion"></os-projector-button>
</mat-cell>
</ng-container>
<!-- Has File -->
<span class="attached-files" *ngIf="motion.hasAttachments()">
<mat-icon>attach_file</mat-icon>
</span>
<!-- identifier column -->
<ng-container matColumnDef="identifier">
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div class="innerTable">
{{ motion.identifier }}
</div>
</mat-cell>
</ng-container>
<!-- The title -->
<span class="motion-list-title">
{{ motion.title }}
</span>
</div>
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div class="innerTable max-width">
<!-- title line -->
<div class="title-line ellipsis-overflow">
<!-- favorite icon -->
<span *ngIf="motion.star" class="favorite-star">
<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>
<!-- Submitters -->
<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 column -->
<ng-container matColumnDef="state">
@ -118,7 +107,9 @@
<os-icon-container icon="device_hub">{{ motion.category }}</os-icon-container>
</div>
<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 class="ellipsis-overflow spacer-top-5" *ngIf="motion.tags && motion.tags.length">
<os-icon-container icon="local_offer">
@ -133,64 +124,104 @@
</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.id" *ngIf="!isMultiSelect"></a>
</mat-cell>
</ng-container>
<!-- Workflow state -->
<div class="ellipsis-overflow white">
<mat-basic-chip *ngIf="motion.state" [ngClass]="motion.stateCssColor" [disabled]="true">
{{ getStateLabel(motion) }}
</mat-basic-chip>
</div>
<!-- Speakers column -->
<ng-container matColumnDef="speakers">
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell>
<mat-cell *matCellDef="let motion">
<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"
<!-- Recommendation -->
<div
*ngIf="motion.recommendation && motion.state.next_states_id.length > 0"
class="ellipsis-overflow white spacer-top-3"
>
</mat-row>
</mat-table>
</span>
<span *ngSwitchCase="'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>
</span>
</div>
<mat-basic-chip class="bluegrey" [disabled]="true">
{{ getRecommendationLabel(motion) }}
</mat-basic-chip>
</div>
</div>
</div>
<mat-paginator [style.display]="selectedView === 'list' ? 'block' : 'none'" class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
</mat-drawer-container>
<!-- Category, blocks and tags -->
<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">
<div *ngIf="!isMultiSelect">

View File

@ -1,9 +1,11 @@
@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 */
.innerTable {
display: inline-block;
vertical-align: top;
line-height: 150%;
}
@ -18,70 +20,49 @@
}
}
.os-listview-table {
/** identifier */
.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
}
.projector-button {
margin: auto;
}
/** Title */
.mat-column-title {
width: 100%;
flex: 1 0 200px;
display: block;
padding-left: 10px;
.column-identifier {
margin-top: $text-margin-top;
}
.title-line {
font-weight: 500;
font-size: 16px;
.column-title {
margin-top: $text-margin-top;
.attached-files {
.mat-icon {
display: inline-flex;
vertical-align: middle;
$icon-size: 16px;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
}
}
.title-line {
font-weight: 500;
font-size: 16px;
.favorite-star {
padding-right: 3px;
.attached-files {
.mat-icon {
display: inline-flex;
vertical-align: middle;
$icon-size: 16px;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
}
}
.submitters-line {
font-size: 90%;
.favorite-star {
padding-right: 3px;
}
}
/** State */
.mat-column-state {
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;
.submitters-line {
font-size: 90%;
}
}
.max-width {
width: 100%;
.column-state {
margin-top: $text-margin-top;
width: inherit;
.mat-icon {
font-size: 150%;
}
}
os-grid-layout {

View File

@ -4,6 +4,7 @@ import { Title } from '@angular/platform-browser';
import { MatSnackBar, MatDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.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 { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
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 { 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 { ViewWorkflow } from 'app/site/motions/models/view-workflow';
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 { StorageService } from 'app/core/core-services/storage.service';
import { PdfError } from 'app/core/ui-services/pdf-document.service';
import { tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { ColumnRestriction } from 'app/shared/components/list-view-table/list-view-table.component';
interface TileCategoryInformation {
filter: string;
@ -76,8 +72,7 @@ interface InfoDialog {
templateUrl: './motion-list.component.html',
styleUrls: ['./motion-list.component.scss']
})
export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motion, MotionRepositoryService>
implements OnInit {
export class MotionListComponent extends ListViewBaseComponent<ViewMotion> implements OnInit {
/**
* 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
* Define the columns to show
*/
public displayedColumnsDesktop: string[] = ['identifier', 'title', 'state', 'anchor'];
/**
* Columns to display in table when mobile view is available
*/
public displayedColumnsMobile = ['identifier', 'title', 'anchor'];
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'identifier'
},
{
prop: 'title',
width: 'auto'
},
{
prop: 'state',
width: '20%',
minWidth: 160
},
{
prop: 'speaker'
}
];
/**
* 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 motionBlocks: ViewMotionBlock[] = [];
public restrictedColumns: ColumnRestriction[] = [
{
columnName: 'speaker',
permission: 'agenda.can_see'
}
];
/**
* List of `TileCategoryInformation`.
* 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;
/**
* 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.
*
@ -165,7 +174,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
titleService: Title,
protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar,
route: ActivatedRoute,
private route: ActivatedRoute,
storage: StorageService,
public filterService: MotionFilterListService,
public sortService: MotionSortListService,
@ -175,19 +184,15 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
private motionBlockRepo: MotionBlockRepositoryService,
private categoryRepo: CategoryRepositoryService,
private workflowRepo: WorkflowRepositoryService,
protected motionRepo: MotionRepositoryService,
public motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService,
private operator: OperatorService,
private pdfExport: MotionPdfExportService,
private dialog: MatDialog,
private vp: ViewportService,
public multiselectService: MotionMultiselectService,
public perms: LocalPermissionsService,
private motionXlsxExport: MotionXlsxExportService
) {
super(titleService, translate, matSnackBar, motionRepo, route, storage, filterService, sortService);
// enable multiSelect for this listView
super(titleService, translate, matSnackBar, storage);
this.canMultiSelect = true;
}
@ -199,77 +204,107 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
*/
public async ngOnInit(): Promise<void> {
super.setTitle('Motions');
this.initTable();
this.storedView = await this.storage.get<string>('motionListView');
this.subscriptions.push(
this.configService
.get<boolean>('motions_statutes_enabled')
.subscribe(enabled => (this.statutesEnabled = enabled)),
this.configService.get<string>('motions_recommendations_by').subscribe(recommender => {
this.recommendationEnabled = !!recommender;
}),
this.motionBlockRepo.getViewModelListObservable().subscribe(mBs => {
this.motionBlocks = mBs;
this.updateStateColumnVisibility();
}),
this.categoryRepo.getViewModelListObservable().subscribe(cats => {
this.categories = cats;
if (cats.length > 0) {
this.selectedView = this.storedView || 'tiles';
} else {
this.selectedView = 'list';
}
this.updateStateColumnVisibility();
}),
this.tagRepo.getViewModelListObservable().subscribe(tags => {
this.tags = tags;
this.updateStateColumnVisibility();
}),
this.workflowRepo.getViewModelListObservable().subscribe(wfs => (this.workflows = wfs))
);
this.setFulltextFilter();
const storedView = await this.storage.get<string>('motionListView');
this.configService
.get<boolean>('motions_statutes_enabled')
.subscribe(enabled => (this.statutesEnabled = enabled));
this.configService.get<string>('motions_recommendations_by').subscribe(recommender => {
this.recommendationEnabled = !!recommender;
});
this.motionBlockRepo.getViewModelListObservable().subscribe(mBs => {
this.motionBlocks = mBs;
});
this.categoryRepo.getViewModelListObservable().subscribe(cats => {
this.categories = cats;
if (cats.length > 0) {
this.selectedView = storedView || 'tiles';
} else {
this.selectedView = 'list';
}
});
this.tagRepo.getViewModelListObservable().subscribe(tags => {
this.tags = tags;
});
this.workflowRepo.getViewModelListObservable().subscribe(wfs => (this.workflows = wfs));
this.motionRepo.getViewModelListObservable().subscribe(motions => {
if (motions && motions.length) {
this.createTiles(motions);
}
});
}
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
* @param motion The row the user clicked at
* Handler for the plus button
*/
public singleSelectAction(motion: ViewMotion): void {
this.router.navigate(['./' + motion.id], { relativeTo: this.route });
public onPlusButton(): void {
this.router.navigate(['./new'], { relativeTo: this.route });
}
/**
* Overwriting method of base-class.
* Every time this method is called, all motions are counted in their related categories.
*
* @returns {Observable<ViewMotion[]>} An observable containing the list of motions.
* Opens the export dialog.
* The export will be limited to the selected data if multiselect modus is
* active and there are rows selected
*/
protected getModelListObservable(): Observable<ViewMotion[]> {
return super.getModelListObservable().pipe(
tap(motions => {
this.informationOfMotionsInTileCategories = {};
for (const motion of motions) {
if (motion.star) {
this.countMotions(-1, true, 'star', 'Favorites');
}
public openExportDialog(): void {
const exportDialogRef = this.dialog.open(MotionExportDialogComponent, {
width: '1100px',
maxWidth: '90vw',
maxHeight: '90vh',
data: this.dataSource
});
if (motion.category_id) {
this.countMotions(
motion.category_id,
motion.category_id,
'category',
motion.category.name,
motion.category.prefix
exportDialogRef.afterClosed().subscribe((result: any) => {
if (result && result.format) {
const data = this.isMultiSelect ? this.selectedRows : this.dataSource.source;
if (result.format === 'pdf') {
try {
this.pdfExport.exportMotionCatalog(
data,
result.lnMode,
result.crMode,
result.content,
result.metaInfo,
result.comments
);
} else {
this.countMotions(-2, null, 'category', 'No category');
} 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);
}
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;
}
/**
* 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.
*
@ -441,7 +376,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
*/
public directPdfExport(): void {
this.pdfExport.exportMotionCatalog(
this.dataSource.data,
this.dataSource.source,
this.configService.instant<string>('motions_default_line_numbering') as LineNumberingMode,
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
* 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 {
this.dataSource.filterPredicate = (data, filter) => {
if (!data) {
return false;
}
filter = filter ? filter.toLowerCase() : '';
if (data.submitters.length && data.submitters.find(user => user.full_name.toLowerCase().includes(filter))) {
return true;
}
if (data.motion_block && data.motion_block.title.toLowerCase().includes(filter)) {
return true;
}
if (data.title.toLowerCase().includes(filter)) {
return true;
}
if (data.identifier && data.identifier.toLowerCase().includes(filter)) {
return true;
}
// private setFulltextFilter(): void {
// this.dataSource.filterPredicate = (data, filter) => {
// if (!data) {
// return false;
// }
// filter = filter ? filter.toLowerCase() : '';
// if (data.submitters.length && data.submitters.find(user => user.full_name.toLowerCase().includes(filter))) {
// return true;
// }
// if (data.motion_block && data.motion_block.title.toLowerCase().includes(filter)) {
// return true;
// }
// if (data.title.toLowerCase().includes(filter)) {
// return true;
// }
// if (data.identifier && data.identifier.toLowerCase().includes(filter)) {
// return true;
// }
if (
this.getStateLabel(data) &&
this.getStateLabel(data)
.toLocaleLowerCase()
.includes(filter)
) {
return true;
}
// if (
// this.getStateLabel(data) &&
// this.getStateLabel(data)
// .toLocaleLowerCase()
// .includes(filter)
// ) {
// return true;
// }
if (
this.getRecommendationLabel(data) &&
this.getRecommendationLabel(data)
.toLocaleLowerCase()
.includes(filter)
) {
return true;
}
// if (
// this.getRecommendationLabel(data) &&
// this.getRecommendationLabel(data)
// .toLocaleLowerCase()
// .includes(filter)
// ) {
// return true;
// }
const dataid = '' + data.id;
if (dataid.includes(filter)) {
return true;
}
// const dataid = '' + data.id;
// if (dataid.includes(filter)) {
// return true;
// }
return false;
};
}
// return false;
// };
// }
/**
* This function saves the selected view by changes.
@ -504,11 +441,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
*/
public onChangeView(value: string): void {
this.selectedView = value;
this.storedView = 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.
*/
public changeToViewWithTileCategory(tileCategory: TileCategoryInformation): void {
this.onChangeView('list');
this.filterService.clearAllFilters();
this.filterService.toggleFilterOption(tileCategory.filter, {
label: tileCategory.name,
condition: tileCategory.condition,
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 ev a MouseEvent.
*/
public async openEditInfo(motion: ViewMotion, ev: MouseEvent): Promise<void> {
ev.stopPropagation();
public async openEditInfo(motion: ViewMotion): Promise<void> {
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.
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 copyDialog = { ...this.infoDialog };
// Copies the interface to check, if changes were made.
const copyDialog = { ...this.infoDialog };
const dialogRef = this.dialog.open(this.motionInfoDialog, {
width: '400px',
maxWidth: '90vw',
maxHeight: '90vh',
disableClose: true
});
const dialogRef = this.dialog.open(this.motionInfoDialog, {
width: '400px',
maxWidth: '90vw',
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);
dialogRef.keydownEvents().subscribe((event: KeyboardEvent) => {
if (event.key === 'Enter' && event.shiftKey) {
dialogRef.close(this.infoDialog);
}
}
});
}
});
/**
* Checks if there are at least categories, motion-blocks or tags the user can select.
*/
public updateStateColumnVisibility(): void {
const metaInfoAvailable = this.isCategoryAvailable() || this.isMotionBlockAvailable() || this.isTagAvailable();
if (!metaInfoAvailable && this.displayedColumnsDesktop.includes('state')) {
this.displayedColumnsDesktop.splice(this.displayedColumnsDesktop.indexOf('state'), 1);
} else if (metaInfoAvailable && !this.displayedColumnsDesktop.includes('state')) {
this.displayedColumnsDesktop = this.displayedColumnsDesktop.concat('state');
// 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);
}
}
});
}
}
/**
* Checks motion-blocks are available.
* Checks if categories are available.
*
* @returns A boolean if they are available.
*/
public isMotionBlockAvailable(): boolean {
return !!this.motionBlocks && this.motionBlocks.length > 0;
public isCategoryAvailable(): boolean {
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.
*/
public isCategoryAvailable(): boolean {
return !!this.categories && this.categories.length > 0;
public isMotionBlockAvailable(): boolean {
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)">
<!-- Title -->
<div class="title-slot"><h2 translate>Workflows</h2></div>
</os-head-bar>
<mat-table class="os-headed-listview-table on-transition-fade" [dataSource]="dataSource">
<!-- Name Column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>
<span translate>Name</span>
</mat-header-cell>
<mat-cell *matCellDef="let workflow" (click)="onClickWorkflow(workflow)">
{{ workflow.name | translate }}
</mat-cell>
</ng-container>
<os-list-view-table
[repo]="workflowRepo"
[columns]="tableColumnDefinition"
scrollKey="workflow"
(dataSourceChange)="onDataSourceChange($event)"
>
<!-- Name column -->
<div *pblNgridCellDef="'name'; value as name; row as workflow; rowContext as rowContext" class="cell-slot fill">
<a
class="detail-link"
(click)="saveScrollIndex('workflow', rowContext.identity)"
[routerLink]="workflow.id"
*ngIf="!isMultiSelect"
></a>
<div translate>{{ name }}</div>
</div>
<!-- Delete Column -->
<ng-container matColumnDef="delete">
<mat-header-cell *matHeaderCellDef> </mat-header-cell>
<mat-cell *matCellDef="let workflow">
<button type="button" mat-icon-button (click)="onDeleteWorkflow(workflow)">
<mat-icon color="warn">delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row *matRowDef="let row; columns: getColumnDefinition()"></mat-row>
</mat-table>
<!-- Delete column -->
<div *pblNgridCellDef="'delete'; row as workflow" class="cell-slot fill">
<button type="button" mat-icon-button (click)="onDeleteWorkflow(workflow)">
<mat-icon color="warn">delete</mat-icon>
</button>
</div>
</os-list-view-table>
<!-- New workflow dialog -->
<ng-template #newWorkflowDialog>

View File

@ -2,11 +2,11 @@ import { Component, OnInit, TemplateRef } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { MatSnackBar, MatDialog } from '@angular/material';
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 { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
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',
styleUrls: ['./workflow-list.component.scss']
})
export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, Workflow, WorkflowRepositoryService>
implements OnInit {
export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow> implements OnInit {
/**
* Holds the new workflow title
*/
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
@ -39,8 +46,6 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
* @param matSnackBar Showing errors
* @param translate handle trandlations
* @param dialog Dialog options
* @param router navigating back and forth
* @param route Information about the current router
* @param workflowRepo Repository for Workflows
* @param promptService Before delete, ask
*/
@ -48,14 +53,12 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
titleService: Title,
protected translate: TranslateService,
matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService,
private dialog: MatDialog,
private router: Router,
protected workflowRepo: WorkflowRepositoryService,
public workflowRepo: WorkflowRepositoryService,
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 {
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);
}
}
/**
* 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>
</os-head-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let tag">{{ tag.getTitle() }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="['name']"></mat-header-row>
<mat-row (click)="selectItem(row, $event)" *matRowDef="let row; columns: ['name']"></mat-row>
</mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
<os-list-view-table
[repo]="repo"
[columns]="tableColumnDefinition"
[allowProjector]="false"
[(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)"
>
<!-- Name column -->
<div *pblNgridCellDef="'name'; value as name; row as tag" class="cell-slot fill clickable" (click)="selectTag(tag)">
<div>
{{ name }}
</div>
</div>
</os-list-view-table>

View File

@ -1,16 +1,15 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
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 { 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';
/**
@ -23,9 +22,9 @@ import { ViewTag } from '../../models/view-tag';
@Component({
selector: 'os-tag-list',
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 newTag = false;
public selectedTag: ViewTag;
@ -33,6 +32,16 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRep
@ViewChild('tagForm')
public tagForm: FormGroup;
/**
* Define the columns to show
*/
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'name',
width: 'auto'
}
];
/**
* Constructor.
* @param titleService
@ -43,14 +52,12 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRep
*/
public constructor(
titleService: Title,
protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService,
private repo: TagRepositoryService,
public repo: TagRepositoryService,
protected translate: TranslateService, // protected required for ng-translate-extract
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 {
super.setTitle('Tags');
this.initTable();
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 {
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
* @param viewTag
*/
public singleSelectAction(viewTag: ViewTag): void {
public selectTag(viewTag: ViewTag): void {
this.selectedTag = viewTag;
this.setEditMode(true, false);
this.tagForm.setValue({ name: this.selectedTag.name });

View File

@ -14,114 +14,76 @@
</div>
</os-head-bar>
<mat-drawer-container class="on-transition-fade">
<os-sort-filter-bar
[filterCount]="filteredCount"
[sortService]="sortService"
[filterService]="filterService"
(searchFieldChange)="searchFilter($event)"
<os-list-view-table
[repo]="repo"
[filterService]="filterService"
[sortService]="sortService"
[columns]="tableColumnDefinition"
[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>
<!-- Selector column -->
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell>
<mat-cell *matCellDef="let user">
<mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- Info column -->
<div *pblNgridCellDef="'infos'; row as user" class="cell-slot fill">
<div class="infoCell">
<mat-icon
inline
*ngIf="user.is_last_email_send"
matTooltip="{{ 'Email sent' | translate }} ({{ getEmailSentTime(user) }})"
>
mail
</mat-icon>
</div>
</div>
<!-- Projector column -->
<ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
<mat-cell *matCellDef="let user">
<os-projector-button [object]="user"></os-projector-button>
</mat-cell>
</ng-container>
<!-- name column -->
<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>
<!-- Presence column -->
<div *pblNgridCellDef="'presence'; row as user" class="cell-slot fill">
<div *ngIf="user.is_active">
<mat-checkbox (change)="setPresent(user)" [checked]="user.is_present" [disabled]="isMultiSelect">
<span translate>Present</span>
</mat-checkbox>
</div>
</div>
</os-list-view-table>
<mat-menu #userMenu="matMenu">
<div *ngIf="!isMultiSelect">

View File

@ -1,51 +1,11 @@
@import '~assets/styles/tables.scss';
.os-listview-table {
.mat-column-projector {
padding-right: 15px;
}
.groupsCell {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.mat-column-name {
flex: 1 0 200px;
}
.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;
}
mat-icon {
font-size: 80%;
}
}
.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 { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { ChoiceService } from 'app/core/ui-services/choice.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 { UserPdfExportService } from '../../services/user-pdf-export.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 { ViewUser } from '../../models/view-user';
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 { StorageService } from 'app/core/core-services/storage.service';
@ -61,7 +62,7 @@ interface InfoDialog {
templateUrl: './user-list.component.html',
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.
*/
@ -83,16 +84,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
*/
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
*/
@ -114,6 +105,27 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
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
* @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 route the local route
* @param operator
* @param vp
* @param csvExport CSV export Service,
* @param promptService
* @param groupRepo
@ -137,14 +148,13 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
titleService: Title,
protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar,
route: ActivatedRoute,
private route: ActivatedRoute,
storage: StorageService,
private repo: UserRepositoryService,
public repo: UserRepositoryService,
private groupRepo: GroupRepositoryService,
private choiceService: ChoiceService,
private router: Router,
private operator: OperatorService,
private vp: ViewportService,
protected csvExport: CsvExportService,
private promptService: PromptService,
public filterService: UserFilterListService,
@ -153,7 +163,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
private userPdf: UserPdfExportService,
private dialog: MatDialog
) {
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
super(titleService, translate, matSnackBar, storage);
// enable multiSelect for this listView
this.canMultiSelect = true;
@ -168,8 +178,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
*/
public ngOnInit(): void {
super.setTitle('Participants');
this.initTable();
this.setFulltextFilter();
// Initialize the groups
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)));
}
/**
* 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
*/
@ -239,7 +239,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
*/
public csvExportUserList(): void {
this.csvExport.export(
this.dataSource.filteredData,
this.dataSource.source,
[
{ property: 'title' },
{ 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)
*/
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
*/
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
*
@ -436,14 +417,16 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User, Use
/**
* Overwrites the dataSource's string filter with a case-insensitive search
* in the full_name property
*
* TODO: Filter predicates will be missed :(
*/
private setFulltextFilter(): void {
this.dataSource.filterPredicate = (data, filter) => {
if (!data || !data.full_name) {
return false;
}
filter = filter ? filter.toLowerCase() : '';
return data.full_name.toLowerCase().indexOf(filter) >= 0;
};
}
// private setFulltextFilter(): void {
// this.dataSource.filterPredicate = (data, filter) => {
// if (!data || !data.full_name) {
// return false;
// }
// filter = filter ? filter.toLowerCase() : '';
// return data.full_name.toLowerCase().indexOf(filter) >= 0;
// };
// }
}

View File

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

View File

@ -25,6 +25,9 @@
@import './assets/styles/fonts.scss';
@import '~material-icon-font/dist/Material-Icons.css';
/** NGrid */
@import '~@pebula/ngrid/theming';
/** Mix the component-related style-rules */
@mixin openslides-components-theme($theme) {
@include os-site-theme($theme);
@ -34,7 +37,6 @@
@include os-sorting-tree-style($theme);
@include os-global-spinner-theme($theme);
@include os-tile-style($theme);
/** More components are added here */
}
/** date-time-picker */
@ -52,6 +54,13 @@
@include angular-material-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 {
img.dark {
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 */
* {
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
@ -580,6 +629,17 @@ button.mat-menu-item.selected {
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 */
/* medium to small */