+
+
+
+
+
diff --git a/client/src/app/shared/components/list-view-table/list-view-table.component.scss b/client/src/app/shared/components/list-view-table/list-view-table.component.scss
new file mode 100644
index 000000000..a98acac70
--- /dev/null
+++ b/client/src/app/shared/components/list-view-table/list-view-table.component.scss
@@ -0,0 +1,5 @@
+@import '~assets/styles/tables.scss';
+
+.projector-button {
+ margin: auto;
+}
diff --git a/client/src/app/shared/components/list-view-table/list-view-table.component.spec.ts b/client/src/app/shared/components/list-view-table/list-view-table.component.spec.ts
new file mode 100644
index 000000000..03aa5198d
--- /dev/null
+++ b/client/src/app/shared/components/list-view-table/list-view-table.component.spec.ts
@@ -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;
+ let fixture: ComponentFixture>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [E2EImportsModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ListViewTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/shared/components/list-view-table/list-view-table.component.ts b/client/src/app/shared/components/list-view-table/list-view-table.component.ts
new file mode 100644
index 000000000..d8957635a
--- /dev/null
+++ b/client/src/app/shared/components/list-view-table/list-view-table.component.ts
@@ -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
+ *
+ *
+ * {{ motion.identifier }}
+ *
+ *
+ * ```
+ */
+@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 implements OnInit {
+ /**
+ * Declare the table
+ */
+ @ViewChild(PblNgridComponent)
+ private ngrid: PblNgridComponent;
+
+ /**
+ * The required repository
+ */
+ @Input()
+ public repo: BaseRepository;
+
+ /**
+ * The currently active sorting service for the list view
+ */
+ @Input()
+ public sortService: BaseSortListService;
+
+ /**
+ * The currently active filter service for the list view. It is supposed to
+ * be a FilterListService extendingFilterListService.
+ */
+ @Input()
+ public filterService: BaseFilterListService;
+
+ /**
+ * 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();
+
+ /**
+ * 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>();
+
+ /**
+ * test data source
+ */
+ public dataSource: PblDataSource;
+
+ /**
+ * 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 {
+ 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()
+ .onTrigger(() => {
+ let listObservable: Observable;
+ 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 {
+ const scrollIndex = await this.store.get(`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);
+ }
+ }
+}
diff --git a/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.html b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.html
index 4da28f02c..3f581ed34 100644
--- a/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.html
+++ b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.html
@@ -1,12 +1,12 @@