Add virtual scrolling to tables
Replaces most mat-tables with tables using the NGrid library, supporting extremly performant virtual scrolling. The ListViewBaseComponent have been extremly simplified. All list-view code is now mich shorter and way less repitative The group list and the workflow list have not been altered. **Works:** - Fast virtual Scrolling without pagination - Click Filter - Search Filter - Sorting - Export filtered values (using click filters) - Export sorted values in correct order - Right-Click-new-tab - Hiding/showing columns by permission and screen size - Multi select - Auto Updates in MultiSelectMode keep the correct items selected - OsHeadBar shows the correct amount of data - Restore scroll position after navigation - Shared-Table Component - Clean-Up base-list-view - Motion List - Motion Block List - Motion Block Detail - User List - Agnnda List - Assignment List - MediaFile List - Tag List **TODO:** - Formulate filter predicates - LOS Badge autoupdate (change detection) - Better ellipses in lists - Horrizontal Scrolling, if the screen get's too small. - Issues in the change detection - Some Layouting **BUG:** - Using the seach filter prevents the sorting from working. - NGrid currently has no way to get the filtered list using search filter. Thus, search-filtered list cannot be exported.
This commit is contained in:
parent
3aa6b8048b
commit
15e9ea898b
@ -145,4 +145,4 @@ matrix:
|
||||
install:
|
||||
- npm install
|
||||
script:
|
||||
- ng build --prod
|
||||
- npm run ng-high-memory build --prod
|
||||
|
@ -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",
|
||||
|
@ -35,5 +35,7 @@ os-icon-container {
|
||||
|
||||
.content-node {
|
||||
margin: auto 5px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -0,0 +1,5 @@
|
||||
@import '~assets/styles/tables.scss';
|
||||
|
||||
.projector-button {
|
||||
margin: auto;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }} </span><span translate>of</span>
|
||||
<span> {{ totalCount }}</span>
|
||||
<span *ngIf="extraItemInfo"> · {{ extraItemInfo }}</span>
|
||||
</div>
|
||||
<div class="filter-count" *ngIf="!showFilterSort && itemsVerboseName">
|
||||
<span>{{ totalCount }} </span><span>{{ itemsVerboseName | translate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Current filters -->
|
||||
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount">
|
||||
<div><span translate>Active filters</span>: </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 }}
|
||||
@ -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
|
||||
|
@ -5,6 +5,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
span.right-with-margin {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<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">
|
||||
<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"
|
||||
@ -8,6 +9,8 @@
|
||||
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>
|
||||
|
@ -38,6 +38,12 @@ export class SpeakerButtonComponent {
|
||||
@Input()
|
||||
public menuItem = false;
|
||||
|
||||
public get listOfSpeakersUrl(): string {
|
||||
if (!this.disabled) {
|
||||
return this.listOfSpeakers.listOfSpeakersUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor
|
||||
*/
|
||||
|
@ -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 },
|
||||
|
@ -13,74 +13,62 @@
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-drawer-container class="on-transition-fade">
|
||||
<os-sort-filter-bar
|
||||
[filterCount]="filteredCount"
|
||||
[extraItemInfo]="getDurationEndString()"
|
||||
<os-list-view-table
|
||||
[repo]="repo"
|
||||
[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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- title column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell>
|
||||
<mat-cell *matCellDef="let item">
|
||||
[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>
|
||||
<os-icon-container [icon]="item.closed ? 'check' : null" size="large">
|
||||
{{ item.getListTitle() }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- 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">
|
||||
<!-- 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>
|
||||
<os-icon-container icon="access_time">
|
||||
{{ durationService.durationToString(item.duration, 'h') }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="item.comment" class="spacer-top-5">
|
||||
<os-icon-container icon="comment">{{ item.comment }}</os-icon-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Speakers column -->
|
||||
<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>
|
||||
<!-- Speaker -->
|
||||
<div *pblNgridCellDef="'speaker'; row as item; rowContext as rowContext" class="cell-slot fill">
|
||||
<os-speaker-button [disabled]="isMultiSelect"></os-speaker-button>
|
||||
|
||||
<!-- menu -->
|
||||
<ng-container matColumnDef="menu">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Menu</mat-header-cell>
|
||||
<mat-cell *matCellDef="let item">
|
||||
<os-speaker-button
|
||||
[object]="item.contentObjectData"
|
||||
[disabled]="isMultiSelect"
|
||||
(click)="saveScrollIndex('agenda', rowContext.identity)"
|
||||
></os-speaker-button>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div *pblNgridCellDef="'menu'; row as item" class="cell-slot fill">
|
||||
<button
|
||||
mat-icon-button
|
||||
[disabled]="isMultiSelect"
|
||||
@ -91,19 +79,8 @@
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
|
||||
<mat-menu #agendaMenu="matMenu">
|
||||
<div *ngIf="!isMultiSelect">
|
||||
|
@ -1,20 +1,5 @@
|
||||
@import '~assets/styles/tables.scss';
|
||||
|
||||
.os-listview-table {
|
||||
/** Title */
|
||||
.mat-column-title {
|
||||
padding-left: 26px;
|
||||
flex: 2 0 0;
|
||||
|
||||
.done-check {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/** Duration */
|
||||
.mat-column-info {
|
||||
flex: 2 0 0;
|
||||
|
||||
.info-col-items {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
@ -29,16 +14,7 @@
|
||||
width: $icon-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Speakers indicator */
|
||||
.mat-column-speakers {
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
/** menu indicator */
|
||||
.mat-column-menu {
|
||||
flex: 0 0 50px;
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
.done-check {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
@ -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)
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
}
|
||||
|
@ -19,75 +19,45 @@
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-drawer-container class="on-transition-fade">
|
||||
<os-sort-filter-bar
|
||||
[filterCount]="filteredCount"
|
||||
<os-list-view-table
|
||||
[repo]="repo"
|
||||
[filterService]="filterService"
|
||||
[sortService]="sortService"
|
||||
(searchFieldChange)="searchFilter($event)"
|
||||
[columns]="tableColumnDefinition"
|
||||
[multiSelect]="isMultiSelect"
|
||||
scrollKey="assignments"
|
||||
[(selectedRows)]="selectedRows"
|
||||
(dataSourceChange)="onDataSourceChange($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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- phase column-->
|
||||
<ng-container matColumnDef="phase">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Phase</mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment">
|
||||
<!-- 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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- candidates column -->
|
||||
<ng-container matColumnDef="candidates">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Candidates</mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment">
|
||||
<!-- 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 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()"
|
||||
>
|
||||
</mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
|
||||
<mat-menu #assignmentMenu="matMenu">
|
||||
<div *ngIf="!isMultiSelect">
|
||||
@ -115,7 +85,8 @@
|
||||
*osPerms="'assignment.can_manage'"
|
||||
mat-menu-item
|
||||
[disabled]="!selectedRows.length"
|
||||
(click)="downloadAssignmentButton(selectedRows)">
|
||||
(click)="downloadAssignmentButton(selectedRows)"
|
||||
>
|
||||
<mat-icon>archive</mat-icon>
|
||||
<span>{{ 'Export selected elections' | translate }}</span>
|
||||
</button>
|
||||
@ -132,4 +103,3 @@
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</mat-drawer-container>
|
||||
|
@ -1,4 +0,0 @@
|
||||
/** Title */
|
||||
.mat-column-title {
|
||||
padding-left: 10px;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
<os-list-view-table
|
||||
[repo]="repo"
|
||||
[filterService]="filterService"
|
||||
(searchFieldChange)="searchFilter($event)"
|
||||
[sortService]="sortService"
|
||||
[columns]="tableColumnDefinition"
|
||||
[multiSelect]="isMultiSelect"
|
||||
scrollKey="user"
|
||||
[(selectedRows)]="selectedRows"
|
||||
(dataSourceChange)="onDataSourceChange($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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Filename -->
|
||||
<ng-container matColumnDef="title">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let file">
|
||||
<!-- 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>
|
||||
|
||||
</span>
|
||||
{{ file.title }}</mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
<div>
|
||||
{{ file.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<ng-container matColumnDef="info">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
||||
<mat-cell *matCellDef="let file">
|
||||
<!-- 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>
|
||||
</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>
|
||||
|
||||
<!-- Indicator column -->
|
||||
<div *pblNgridCellDef="'indicator'; row as file" class="cell-slot fill">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- menu -->
|
||||
<ng-container matColumnDef="menu">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Menu</mat-header-cell>
|
||||
<mat-cell *matCellDef="let file">
|
||||
<!-- Menu column -->
|
||||
<div *pblNgridCellDef="'menu'; row as file" class="cell-slot fill">
|
||||
<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>
|
||||
</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"
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
// };
|
||||
// }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,39 +27,41 @@
|
||||
<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">
|
||||
<div *pblNgridCellDef="'recommendation'; row as motion" class="cell-slot fill">
|
||||
<mat-basic-chip *ngIf="!!motion.recommendation" disableRipple class="bluegrey">
|
||||
{{ getRecommendationLabel(motion) }}
|
||||
</mat-basic-chip>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Remove motion column -->
|
||||
<ng-container matColumnDef="remove">
|
||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||
<mat-cell *matCellDef="let motion">
|
||||
<!-- Remove from block column -->
|
||||
<div *pblNgridCellDef="'remove'; row as motion" class="cell-slot fill">
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
@ -69,25 +71,13 @@
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@ -118,13 +108,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button
|
||||
type="submit"
|
||||
mat-button
|
||||
[disabled]="!blockEditForm.valid"
|
||||
color="primary"
|
||||
(click)="saveBlock()"
|
||||
>
|
||||
<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">
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,31 +124,15 @@ 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 = [];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
this.dataSource = createDS<ViewMotion>()
|
||||
.onTrigger(() => {
|
||||
return this.repo.getViewMotionsByBlock(this.block.motionBlock);
|
||||
})
|
||||
.create();
|
||||
}
|
||||
return columns;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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">
|
||||
<!-- Amount -->
|
||||
<div *pblNgridCellDef="'amount'; row as block" class="cell-slot fill">
|
||||
<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>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
</mat-card>
|
||||
|
@ -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%;
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -14,73 +14,80 @@
|
||||
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
|
||||
<span>{{ selectedRows.length }} </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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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">
|
||||
<!-- 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>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
</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 -->
|
||||
<!-- 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">
|
||||
<!-- favorite icon -->
|
||||
<!-- Is Favorite -->
|
||||
<span *ngIf="motion.star" class="favorite-star">
|
||||
<mat-icon inline>star</mat-icon>
|
||||
</span>
|
||||
<!-- attachment icon -->
|
||||
|
||||
<!-- Has File -->
|
||||
<span class="attached-files" *ngIf="motion.hasAttachments()">
|
||||
<mat-icon>attach_file</mat-icon>
|
||||
</span>
|
||||
<!-- title -->
|
||||
|
||||
<!-- The title -->
|
||||
<span class="motion-list-title">
|
||||
{{ motion.title }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- submitters line -->
|
||||
|
||||
<!-- Submitters -->
|
||||
<div class="submitters-line ellipsis-overflow" *ngIf="motion.submitters.length">
|
||||
<span translate>by</span> {{ motion.submitters }}
|
||||
<span *osPerms="'motions.can_manage'">
|
||||
@ -89,24 +96,6 @@
|
||||
{{ motion.id }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- state line-->
|
||||
<div class="ellipsis-overflow white">
|
||||
<mat-basic-chip *ngIf="motion.state" [ngClass]="motion.stateCssColor" [disabled]="true">
|
||||
{{ getStateLabel(motion) }}
|
||||
</mat-basic-chip>
|
||||
</div>
|
||||
<!-- recommendation line -->
|
||||
<div
|
||||
*ngIf="motion.recommendation && motion.state.next_states_id.length > 0"
|
||||
class="ellipsis-overflow white spacer-top-3"
|
||||
>
|
||||
<mat-basic-chip class="bluegrey" [disabled]="true">
|
||||
{{ getRecommendationLabel(motion) }}
|
||||
</mat-basic-chip>
|
||||
</div>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- state column -->
|
||||
<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,33 +124,67 @@
|
||||
</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>
|
||||
<mat-basic-chip class="bluegrey" [disabled]="true">
|
||||
{{ getRecommendationLabel(motion) }}
|
||||
</mat-basic-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">, </span>
|
||||
</span>
|
||||
<span *ngSwitchCase="'tiles'">
|
||||
</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"
|
||||
@ -168,13 +193,23 @@
|
||||
[only]="'title'"
|
||||
[blockType]="'node'"
|
||||
[data]="tileCategory"
|
||||
title="{{ tileCategory.name | translate }}">
|
||||
title="{{ tileCategory.name | translate }}"
|
||||
>
|
||||
<ng-container class="block-node">
|
||||
<table matTooltip="{{ tileCategory.amountOfMotions }} {{ 'Motions' | translate }} – {{ tileCategory.name | translate }}">
|
||||
<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
|
||||
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>
|
||||
@ -186,11 +221,7 @@
|
||||
</ng-container>
|
||||
</os-block-tile>
|
||||
</os-grid-layout>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<mat-paginator [style.display]="selectedView === 'list' ? 'block' : 'none'" class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
|
||||
</mat-drawer-container>
|
||||
</ng-template>
|
||||
|
||||
<mat-menu #motionListMenu="matMenu">
|
||||
<div *ngIf="!isMultiSelect">
|
||||
|
@ -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,22 +20,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.column-title {
|
||||
margin-top: $text-margin-top;
|
||||
|
||||
.title-line {
|
||||
font-weight: 500;
|
||||
@ -60,30 +56,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** State */
|
||||
.mat-column-state {
|
||||
flex: 0 0 160px;
|
||||
.column-state {
|
||||
margin-top: $text-margin-top;
|
||||
width: inherit;
|
||||
|
||||
.state-column {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
.mat-icon {
|
||||
font-size: 150%;
|
||||
}
|
||||
}
|
||||
|
||||
/** Speakers indicator */
|
||||
.mat-column-speakers {
|
||||
flex: 0 0 100px;
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
}
|
||||
|
||||
.max-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
os-grid-layout {
|
||||
.color-select {
|
||||
width: 100%;
|
||||
|
@ -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,54 +204,38 @@ 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(
|
||||
const storedView = await this.storage.get<string>('motionListView');
|
||||
|
||||
this.configService
|
||||
.get<boolean>('motions_statutes_enabled')
|
||||
.subscribe(enabled => (this.statutesEnabled = enabled)),
|
||||
.subscribe(enabled => (this.statutesEnabled = enabled));
|
||||
this.configService.get<string>('motions_recommendations_by').subscribe(recommender => {
|
||||
this.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';
|
||||
this.selectedView = 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();
|
||||
});
|
||||
this.workflowRepo.getViewModelListObservable().subscribe(wfs => (this.workflows = wfs));
|
||||
|
||||
this.motionRepo.getViewModelListObservable().subscribe(motions => {
|
||||
if (motions && motions.length) {
|
||||
this.createTiles(motions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The action performed on a click in single select modus
|
||||
* @param motion The row the user clicked at
|
||||
*/
|
||||
public singleSelectAction(motion: ViewMotion): void {
|
||||
this.router.navigate(['./' + motion.id], { 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.
|
||||
*/
|
||||
protected getModelListObservable(): Observable<ViewMotion[]> {
|
||||
return super.getModelListObservable().pipe(
|
||||
tap(motions => {
|
||||
private createTiles(motions: ViewMotion[]): void {
|
||||
this.informationOfMotionsInTileCategories = {};
|
||||
for (const motion of motions) {
|
||||
if (motion.star) {
|
||||
@ -267,9 +256,55 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
}
|
||||
|
||||
this.tileCategories = Object.values(this.informationOfMotionsInTileCategories);
|
||||
this.motionsVerboseName = this.motionRepo.getVerboseName(motions.length > 1);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.source;
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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,9 +465,8 @@ 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,
|
||||
@ -566,7 +498,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
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
|
||||
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)) {
|
||||
@ -575,26 +508,15 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
<!-- 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>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: getColumnDefinition()"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
|
||||
<!-- New workflow dialog -->
|
||||
<ng-template #newWorkflowDialog>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 });
|
||||
|
@ -14,45 +14,37 @@
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-drawer-container class="on-transition-fade">
|
||||
<os-sort-filter-bar
|
||||
[filterCount]="filteredCount"
|
||||
[sortService]="sortService"
|
||||
<os-list-view-table
|
||||
[repo]="repo"
|
||||
[filterService]="filterService"
|
||||
(searchFieldChange)="searchFilter($event)"
|
||||
[sortService]="sortService"
|
||||
[columns]="tableColumnDefinition"
|
||||
[multiSelect]="isMultiSelect"
|
||||
[hiddenInMobile]="['group']"
|
||||
scrollKey="user"
|
||||
[(selectedRows)]="selectedRows"
|
||||
(dataSourceChange)="onDataSourceChange($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></mat-header-cell>
|
||||
<mat-cell *matCellDef="let user">
|
||||
<mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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 -->
|
||||
<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
|
||||
*pblNgridCellDef="'group'; row as user"
|
||||
class="cell-slot fill"
|
||||
[ngClass]="isMultiSelect ? '' : 'clickable'"
|
||||
(click)="openEditInfo(user, $event)"
|
||||
>
|
||||
<div class="groupsCell">
|
||||
<div *ngIf="user.groups && user.groups.length">
|
||||
<os-icon-container icon="people">
|
||||
@ -69,59 +61,29 @@
|
||||
</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) }})">
|
||||
<!-- 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>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<!-- 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"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
|
||||
<mat-menu #userMenu="matMenu">
|
||||
<div *ngIf="!isMultiSelect">
|
||||
|
@ -1,17 +1,5 @@
|
||||
@import '~assets/styles/tables.scss';
|
||||
|
||||
.os-listview-table {
|
||||
.mat-column-projector {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.mat-column-name {
|
||||
flex: 1 0 200px;
|
||||
}
|
||||
|
||||
.mat-column-group {
|
||||
flex: 2 0 60px;
|
||||
|
||||
.groupsCell {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@ -21,31 +9,3 @@
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-presence {
|
||||
flex: 0 0 60px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 100%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.infoCell {
|
||||
max-width: 25px;
|
||||
}
|
||||
|
||||
.presentCell {
|
||||
align-content: left;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.checkboxPresent {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
@ -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;
|
||||
// };
|
||||
// }
|
||||
}
|
||||
|
@ -20,6 +20,10 @@
|
||||
color: mat-color($primary);
|
||||
}
|
||||
|
||||
.anchor-button {
|
||||
color: mat-color($foreground, text) !important;
|
||||
}
|
||||
|
||||
.accent,
|
||||
.accent-text {
|
||||
color: mat-color($accent);
|
||||
|
@ -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;
|
||||
@ -582,6 +631,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 */
|
||||
|
Loading…
Reference in New Issue
Block a user