From 15e9ea898b4cbb6712516592e5994210f7dea6a7 Mon Sep 17 00:00:00 2001 From: Sean Engelhardt Date: Fri, 14 Jun 2019 11:18:54 +0200 Subject: [PATCH] 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. --- .travis.yml | 2 +- client/package.json | 7 +- .../icon-container.component.scss | 2 + .../list-view-table.component.html | 36 ++ .../list-view-table.component.scss | 5 + .../list-view-table.component.spec.ts | 25 + .../list-view-table.component.ts | 379 ++++++++++++++ .../sort-filter-bar.component.html | 25 +- .../sort-filter-bar.component.scss | 5 + .../speaker-button.component.html | 19 +- .../speaker-button.component.ts | 6 + client/src/app/shared/shared.module.ts | 18 +- .../agenda-list/agenda-list.component.html | 149 +++--- .../agenda-list/agenda-list.component.scss | 54 +- .../agenda-list/agenda-list.component.ts | 120 ++--- .../assignment-list.component.html | 188 +++---- .../assignment-list.component.scss | 4 - .../assignment-list.component.ts | 55 +- client/src/app/site/base/list-view-base.ts | 278 ++-------- .../history-list/history-list.component.scss | 22 - .../mediafile-list.component.html | 155 ++---- .../mediafile-list.component.scss | 28 - .../mediafile-list.component.ts | 95 ++-- .../category-list/category-list.component.ts | 28 +- .../motion-block-detail.component.html | 112 ++-- .../motion-block-detail.component.scss | 57 +- .../motion-block-detail.component.ts | 88 ++-- .../motion-block-list.component.html | 70 +-- .../motion-block-list.component.scss | 17 - .../motion-block-list.component.ts | 42 +- .../motion-list/motion-list.component.html | 311 ++++++----- .../motion-list/motion-list.component.scss | 85 ++- .../motion-list/motion-list.component.ts | 494 ++++++++---------- .../workflow-list.component.html | 47 +- .../workflow-list/workflow-list.component.ts | 45 +- .../tag-list/tag-list.component.html | 24 +- ....component.css => tag-list.component.scss} | 0 .../components/tag-list/tag-list.component.ts | 41 +- .../user-list/user-list.component.html | 172 +++--- .../user-list/user-list.component.scss | 52 +- .../user-list/user-list.component.ts | 101 ++-- .../styles/global-components-style.scss | 4 + client/src/styles.scss | 62 ++- 43 files changed, 1710 insertions(+), 1819 deletions(-) create mode 100644 client/src/app/shared/components/list-view-table/list-view-table.component.html create mode 100644 client/src/app/shared/components/list-view-table/list-view-table.component.scss create mode 100644 client/src/app/shared/components/list-view-table/list-view-table.component.spec.ts create mode 100644 client/src/app/shared/components/list-view-table/list-view-table.component.ts rename client/src/app/site/tags/components/tag-list/{tag-list.component.css => tag-list.component.scss} (100%) diff --git a/.travis.yml b/.travis.yml index 07eda9bc0..3e4290e24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -145,4 +145,4 @@ matrix: install: - npm install script: - - ng build --prod + - npm run ng-high-memory build --prod diff --git a/client/package.json b/client/package.json index ea7dd0196..e5d4eeccd 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/app/shared/components/icon-container/icon-container.component.scss b/client/src/app/shared/components/icon-container/icon-container.component.scss index eefe94630..d52dfe256 100644 --- a/client/src/app/shared/components/icon-container/icon-container.component.scss +++ b/client/src/app/shared/components/icon-container/icon-container.component.scss @@ -35,5 +35,7 @@ os-icon-container { .content-node { margin: auto 5px; + text-overflow: ellipsis; + overflow: hidden; } } diff --git a/client/src/app/shared/components/list-view-table/list-view-table.component.html b/client/src/app/shared/components/list-view-table/list-view-table.component.html new file mode 100644 index 000000000..a4c7dfa1b --- /dev/null +++ b/client/src/app/shared/components/list-view-table/list-view-table.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + +
+ +
+ + + +
+
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 @@
-
+ +
{{ displayedCount }} of  {{ totalCount }}  ยท {{ extraItemInfo }}
-
- {{ totalCount }} {{ itemsVerboseName | translate }} -
+ +
Active filters
@@ -22,12 +22,11 @@
-
- - - - - - + + - + + + +
- - - - - - - - {{ isSelected(item) ? 'check_circle' : '' }} - - + + +
+ +
+ + {{ item.getListTitle() }} + +
+
- - - Projector - -
- -
-
-
+ +
+
+
+ {{ item.verboseType | translate }} +
+
+ + {{ durationService.durationToString(item.duration, 'h') }} + +
- - - Topic - -
- {{ item.getListTitle() }} -
-
-
+
+ {{ item.comment }} +
+
+
- - - Info - -
-
-
- {{ item.verboseType | translate }} -
-
- {{ durationService.durationToString(item.duration, 'h') }} -
-
- {{ item.comment }} -
-
-
-
-
+ +
+ - - - Speakers - - - - + +
- - - Menu - - - - - - - -
- -
+ +
+ +
+
diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.scss b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.scss index b0228309d..97bf09076 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.scss +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.scss @@ -1,44 +1,20 @@ @import '~assets/styles/tables.scss'; -.os-listview-table { - /** Title */ - .mat-column-title { - padding-left: 26px; - flex: 2 0 0; +.info-col-items { + display: inline-block; + white-space: nowrap; - .done-check { - margin-right: 10px; - } - } - - /** Duration */ - .mat-column-info { - flex: 2 0 0; - - .info-col-items { - display: inline-block; - white-space: nowrap; - - font-size: 14px; - .mat-icon { - display: inline-flex; - vertical-align: middle; - $icon-size: 18px; - font-size: $icon-size; - height: $icon-size; - width: $icon-size; - } - } - } - - /** Speakers indicator */ - .mat-column-speakers { - flex: 0 0 50px; - } - - /** menu indicator */ - .mat-column-menu { - flex: 0 0 50px; - justify-content: flex-end !important; + font-size: 14px; + .mat-icon { + display: inline-flex; + vertical-align: middle; + $icon-size: 18px; + font-size: $icon-size; + height: $icon-size; + width: $icon-size; } } + +.done-check { + margin-right: 10px; +} diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts index 43dac913b..fb058a898 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts @@ -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 - implements OnInit { +export class AgendaListComponent extends ListViewBaseComponent 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 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('agenda_enable_numbering') .subscribe(autoNumbering => (this.isNumberingAllowed = autoNumbering)); - this.setFulltextFilter(); } /** @@ -144,9 +155,9 @@ export class AgendaListComponent extends ListViewBaseComponent { - 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) + // ); + // }; + // } } diff --git a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.html b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.html index 906a67527..7abc3945b 100644 --- a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.html +++ b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.html @@ -19,117 +19,87 @@
- - - - - - - - - {{ isSelected(assignment) ? 'check_circle' : '' }} - - + + +
+ +
+ {{ assignment.getListTitle() }} +
+
- - - Projector - - - - + +
+ + {{ assignment.phaseString | translate }} + +
- - - Title - {{ assignment.getListTitle() }} - + +
+ + + {{ assignment.candidateAmount }} + + +
+
- - - Phase - - - {{ assignment.phaseString | translate }} - - - - - + +
+ + +
- - - Candidates - - - {{ assignment.candidateAmount }} - - - - - - + + + + - -
- -
- - - - - - -
- - + archive + {{ 'Export selected elections' | translate }} + + + +
+ diff --git a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.scss b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.scss index 5bd6af3bf..e69de29bb 100644 --- a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.scss +++ b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.scss @@ -1,4 +0,0 @@ -/** Title */ -.mat-column-title { - padding-left: 10px; -} diff --git a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts index 1676733e2..609dbab85 100644 --- a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts +++ b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts @@ -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 - implements OnInit { +export class AssignmentListComponent extends ListViewBaseComponent 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; - } } diff --git a/client/src/app/site/base/list-view-base.ts b/client/src/app/site/base/list-view-base.ts index 90285d086..d9daec998 100644 --- a/client/src/app/site/base/list-view-base.ts +++ b/client/src/app/site/base/list-view-base.ts @@ -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 -> extends BaseViewComponent implements OnDestroy { +export abstract class ListViewBaseComponent 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; + public dataSource: PblDataSource; /** * 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; - - /** - * 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, - protected modelSortService?: BaseSortListService + protected storage?: StorageService ) { super(titleService, translate, matSnackBar); this.selectedRows = []; - try { - this.paginationStorageKey = (>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 { - 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): 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): void { + this.dataSource = newDataSource; } - /** - * Initialize the settings for the paginator in every list view. - */ - private async initializePagination(): Promise { - // 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 { - 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); } } diff --git a/client/src/app/site/history/components/history-list/history-list.component.scss b/client/src/app/site/history/components/history-list/history-list.component.scss index c8585210c..8fe9cce38 100644 --- a/client/src/app/site/history/components/history-list/history-list.component.scss +++ b/client/src/app/site/history/components/history-list/history-list.component.scss @@ -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 diff --git a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html index c4b369e24..abde9a5a9 100644 --- a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html +++ b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html @@ -1,8 +1,4 @@ - +

Files

@@ -14,6 +10,7 @@ more_vert
+
@@ -21,99 +18,61 @@
- - - - - - - - - {{ isSelected(item) ? 'check_circle' : '' }} - - + + +
+ + + lock +   + +
+ {{ file.title }} +
+
- - - Projector - - - - + +
+
+ {{ file.type }} + {{ file.size }} +
+
- - - Name - - - lock -   - - {{ file.title }} - + +
+
+ text_fields + insert_photo +
+
- - - Group - -
- {{ file.type }} - {{ file.size }} -
-
-
- - - - Indicator - - - -
- text_fields - insert_photo -
-
-
- - - - Menu - - - - - - - -
- - -
+ +
+ +
+ @@ -189,11 +148,7 @@

{{ 'Edit details for' | translate }}

-
+ - implements OnInit { +export class MediafileListComponent extends ListViewBaseComponent implements OnInit { /** * Holds the actions for logos. Updated via an observable */ @@ -38,16 +37,6 @@ export class MediafileListComponent extends ListViewBaseComponent; + /** + * 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 { @@ -140,7 +148,6 @@ export class MediafileListComponent extends ListViewBaseComponent { this.fontActions = action; }); - this.setFulltextFilter(); } /** @@ -284,34 +291,6 @@ export class MediafileListComponent extends ListViewBaseComponent { - 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; + // }; + // } } diff --git a/client/src/app/site/motions/modules/category/components/category-list/category-list.component.ts b/client/src/app/site/motions/modules/category/components/category-list/category-list.component.ts index f876cc39d..a04ae7db5 100644 --- a/client/src/app/site/motions/modules/category/components/category-list/category-list.component.ts +++ b/client/src/app/site/motions/modules/category/components/category-list/category-list.component.ts @@ -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 - implements OnInit { +export class CategoryListComponent extends BaseViewComponent implements OnInit { /** * Holds the create form */ public createForm: FormGroup; + /** + * Table data Source + */ + public dataSource: MatTableDataSource; + /** * Flag, if the creation panel is open */ @@ -51,20 +52,17 @@ export class CategoryListComponent extends ListViewBaseComponent { + if (viewCategories && viewCategories.length && this.dataSource) { + this.dataSource.data = viewCategories; + } + }); } /** diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html index 0039ad6d0..6a24cc041 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html @@ -27,67 +27,57 @@ Follow recommendations for all motions - - - - Motion - - {{ motion.getTitle() }} - - + + +
+ + {{ motion.getTitle() }} +
- - - State - + +
+
{{ getStateLabel(motion) }} - - +
+
- - Recommendation - - - {{ getRecommendationLabel(motion) }} - - - +
+ + {{ getRecommendationLabel(motion) }} + +
- - - - - - - - - - - - - - - - - - -
+ +
+ +
+ - + @@ -112,23 +102,17 @@
- + Internal
- - -
+ + +
diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.scss b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.scss index 2f5db7e35..5360da704 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.scss +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.scss @@ -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%; } } diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts index f5cb8328d..e1402330c 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts @@ -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 - 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; + + /** + * 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 { if (newBlock) { this.block = newBlock; - this.subscriptions.push( - this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(viewMotions => { - if (viewMotions && viewMotions.length) { - this.dataSource.data = viewMotions; - } else { - this.dataSource.data = []; - } + + this.dataSource = createDS() + .onTrigger(() => { + return this.repo.getViewMotionsByBlock(this.block.motionBlock); }) - ); + .create(); } }) ); } - /** - * Returns the columns that should be shown in the table - * - * @returns an array of strings building the column definition - */ - public getColumnDefinition(): string[] { - let columns = ['title', 'state', 'recommendation', 'anchor']; - if (this.operator.hasPerms('motions.can_manage_manage')) { - columns = columns.concat('remove'); - } - return columns; - } - /** * Click handler for recommendation button */ @@ -169,8 +189,8 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent 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; } diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html index c27aaa8c0..1dfa93491 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html @@ -49,52 +49,40 @@ - + - - - - - - - - - - - - - - Title - - + + +
+ +
lock - {{ block.title }} - - + {{ title }} +
+
- - - - Motions - - - {{ getMotionAmount(block.motionBlock) }} - - - - - - - - - - - - - -
+ +
+ {{ getMotionAmount(block.motionBlock) }} +
+
diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.scss b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.scss index 651a1b806..65be598eb 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.scss +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.scss @@ -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%; } diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.ts b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.ts index f27e56cac..949e2ee35 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.ts +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.ts @@ -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 - implements OnInit { +export class MotionBlockListComponent extends ListViewBaseComponent 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 * diff --git a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.html b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.html index 738f744f8..0a5df7595 100644 --- a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.html @@ -14,99 +14,88 @@ {{ selectedRows.length }} selected + +
+
+ + + +
+
- - + + + - - view_module - view_headline - - + +
+ +
+ {{ motion.identifier }} +
+
-
- - - - - - - {{ isSelected(motion) ? 'check_circle' : '' }} - - + +
+ +
+
+ + + star + - - - Projector - - - - + + + attach_file + - - - Identifier - -
- {{ motion.identifier }} -
-
-
+ + + {{ motion.title }} + +
- - - Title - -
- -
- - - star - - - - attach_file - - - - {{ motion.title }} - -
- -
- by {{ motion.submitters }} - - · - Sequential number - {{ motion.id }} - -
- -
- - {{ getStateLabel(motion) }} - -
- -
- - {{ getRecommendationLabel(motion) }} - -
-
-
-
+ +
+ by {{ motion.submitters }} + + · + Sequential number + {{ motion.id }} + +
@@ -118,7 +107,9 @@ {{ motion.category }}
- {{ motion.motion_block.title }} + {{ + motion.motion_block.title + }}
@@ -133,64 +124,104 @@ - - - - - - - + +
+ + {{ getStateLabel(motion) }} + +
- - - Speakers - - - - - - - +
- - - - - - - - - - - - - -
- - star - block - {{ tileCategory.prefix }} - -
-
-
-
-
-
+ + {{ getRecommendationLabel(motion) }} + +
+
+
- -
+ +
+
+ +
+ {{ motion.category }} +
+ + +
+ {{ motion.motion_block.title }} +
+ + +
+ + + {{ tag.getTitle() }} + + + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + +
+ + star + block + {{ tileCategory.prefix }} + +
+
+
+
+
diff --git a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.scss b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.scss index f5ceb0779..5cfeae55d 100644 --- a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.scss +++ b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.scss @@ -1,9 +1,11 @@ @import '~assets/styles/tables.scss'; +// Determine the distance between the top edge to the start of the table content +$text-margin-top: 10px; + /** css hacks https://codepen.io/edge0703/pen/iHJuA */ .innerTable { display: inline-block; - vertical-align: top; line-height: 150%; } @@ -18,70 +20,49 @@ } } -.os-listview-table { - /** identifier */ - .mat-column-identifier { - padding-left: 10px; - flex: 0 0 50px; - line-height: 60px; // set the text in the vertical middle, since vertical-align will not work - display: initial; // reset display - text-align: center; // center text - } +.projector-button { + margin: auto; +} - /** Title */ - .mat-column-title { - width: 100%; - flex: 1 0 200px; - display: block; - padding-left: 10px; +.column-identifier { + margin-top: $text-margin-top; +} - .title-line { - font-weight: 500; - font-size: 16px; +.column-title { + margin-top: $text-margin-top; - .attached-files { - .mat-icon { - display: inline-flex; - vertical-align: middle; - $icon-size: 16px; - font-size: $icon-size; - height: $icon-size; - width: $icon-size; - } - } + .title-line { + font-weight: 500; + font-size: 16px; - .favorite-star { - padding-right: 3px; + .attached-files { + .mat-icon { + display: inline-flex; + vertical-align: middle; + $icon-size: 16px; + font-size: $icon-size; + height: $icon-size; + width: $icon-size; } } - .submitters-line { - font-size: 90%; + .favorite-star { + padding-right: 3px; } } - /** State */ - .mat-column-state { - flex: 0 0 160px; - - .state-column { - width: 160px; - } - - mat-icon { - font-size: 150%; - } - } - - /** Speakers indicator */ - .mat-column-speakers { - flex: 0 0 100px; - justify-content: flex-end !important; + .submitters-line { + font-size: 90%; } } -.max-width { - width: 100%; +.column-state { + margin-top: $text-margin-top; + width: inherit; + + .mat-icon { + font-size: 150%; + } } os-grid-layout { diff --git a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts index d16cb29c9..17fa70f67 100644 --- a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts @@ -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 - implements OnInit { +export class MotionListComponent extends ListViewBaseComponent implements OnInit { /** * Reference to the dialog for quick editing meta information. */ @@ -96,13 +91,25 @@ export class MotionListComponent extends ListViewBaseComponent { super.setTitle('Motions'); - this.initTable(); - this.storedView = await this.storage.get('motionListView'); - this.subscriptions.push( - this.configService - .get('motions_statutes_enabled') - .subscribe(enabled => (this.statutesEnabled = enabled)), - this.configService.get('motions_recommendations_by').subscribe(recommender => { - this.recommendationEnabled = !!recommender; - }), - this.motionBlockRepo.getViewModelListObservable().subscribe(mBs => { - this.motionBlocks = mBs; - this.updateStateColumnVisibility(); - }), - this.categoryRepo.getViewModelListObservable().subscribe(cats => { - this.categories = cats; - if (cats.length > 0) { - this.selectedView = this.storedView || 'tiles'; - } else { - this.selectedView = 'list'; - } - this.updateStateColumnVisibility(); - }), - this.tagRepo.getViewModelListObservable().subscribe(tags => { - this.tags = tags; - this.updateStateColumnVisibility(); - }), - this.workflowRepo.getViewModelListObservable().subscribe(wfs => (this.workflows = wfs)) - ); - this.setFulltextFilter(); + const storedView = await this.storage.get('motionListView'); + + this.configService + .get('motions_statutes_enabled') + .subscribe(enabled => (this.statutesEnabled = enabled)); + this.configService.get('motions_recommendations_by').subscribe(recommender => { + this.recommendationEnabled = !!recommender; + }); + this.motionBlockRepo.getViewModelListObservable().subscribe(mBs => { + this.motionBlocks = mBs; + }); + this.categoryRepo.getViewModelListObservable().subscribe(cats => { + this.categories = cats; + if (cats.length > 0) { + this.selectedView = storedView || 'tiles'; + } else { + this.selectedView = 'list'; + } + }); + this.tagRepo.getViewModelListObservable().subscribe(tags => { + this.tags = tags; + }); + this.workflowRepo.getViewModelListObservable().subscribe(wfs => (this.workflows = wfs)); + + this.motionRepo.getViewModelListObservable().subscribe(motions => { + if (motions && motions.length) { + this.createTiles(motions); + } + }); + } + + private createTiles(motions: ViewMotion[]): void { + this.informationOfMotionsInTileCategories = {}; + for (const motion of motions) { + if (motion.star) { + this.countMotions(-1, true, 'star', 'Favorites'); + } + + if (motion.category_id) { + this.countMotions( + motion.category_id, + motion.category_id, + 'category', + motion.category.name, + motion.category.prefix + ); + } else { + this.countMotions(-2, null, 'category', 'No category'); + } + } + + this.tileCategories = Object.values(this.informationOfMotionsInTileCategories); } /** - * The action performed on a click in single select modus - * @param motion The row the user clicked at + * Handler for the plus button */ - public singleSelectAction(motion: ViewMotion): void { - this.router.navigate(['./' + motion.id], { relativeTo: this.route }); + public onPlusButton(): void { + this.router.navigate(['./new'], { relativeTo: this.route }); } /** - * Overwriting method of base-class. - * Every time this method is called, all motions are counted in their related categories. - * - * @returns {Observable} An observable containing the list of motions. + * Opens the export dialog. + * The export will be limited to the selected data if multiselect modus is + * active and there are rows selected */ - protected getModelListObservable(): Observable { - return super.getModelListObservable().pipe( - tap(motions => { - this.informationOfMotionsInTileCategories = {}; - for (const motion of motions) { - if (motion.star) { - this.countMotions(-1, true, 'star', 'Favorites'); - } + public openExportDialog(): void { + const exportDialogRef = this.dialog.open(MotionExportDialogComponent, { + width: '1100px', + maxWidth: '90vw', + maxHeight: '90vh', + data: this.dataSource + }); - if (motion.category_id) { - this.countMotions( - motion.category_id, - motion.category_id, - 'category', - motion.category.name, - motion.category.prefix + exportDialogRef.afterClosed().subscribe((result: any) => { + if (result && result.format) { + const data = this.isMultiSelect ? this.selectedRows : this.dataSource.source; + if (result.format === 'pdf') { + try { + this.pdfExport.exportMotionCatalog( + data, + result.lnMode, + result.crMode, + result.content, + result.metaInfo, + result.comments ); - } else { - this.countMotions(-2, null, 'category', 'No category'); + } catch (err) { + if (err instanceof PdfError) { + this.raiseError(err.message); + } else { + throw err; + } } + } else if (result.format === 'csv') { + this.motionCsvExport.exportMotionList(data, [...result.content, ...result.metaInfo], result.crMode); + } else if (result.format === 'xlsx') { + this.motionXlsxExport.exportMotionList(data, result.metaInfo); } - - this.tileCategories = Object.values(this.informationOfMotionsInTileCategories); - this.motionsVerboseName = this.motionRepo.getVerboseName(motions.length > 1); - }) - ); + } + }); } /** @@ -303,106 +338,6 @@ export class MotionListComponent extends ListViewBaseComponent { - 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('motions_default_line_numbering') as LineNumberingMode, this.configService.instant('motions_recommendation_text_mode') as ChangeRecoMode ); @@ -450,52 +385,54 @@ export class MotionListComponent extends ListViewBaseComponent { - 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 { - ev.stopPropagation(); + public async openEditInfo(motion: ViewMotion): Promise { + if (!this.isMultiSelect) { + // The interface holding the current information from motion. + this.infoDialog = { + title: motion.title, + motionBlock: motion.motion_block_id, + category: motion.category_id, + tags: motion.tags_id + }; - // The interface holding the current information from motion. - this.infoDialog = { - title: motion.title, - motionBlock: motion.motion_block_id, - category: motion.category_id, - tags: motion.tags_id - }; + // Copies the interface to check, if changes were made. + const copyDialog = { ...this.infoDialog }; - // Copies the interface to check, if changes were made. - const copyDialog = { ...this.infoDialog }; + const dialogRef = this.dialog.open(this.motionInfoDialog, { + width: '400px', + maxWidth: '90vw', + maxHeight: '90vh', + disableClose: true + }); - const dialogRef = this.dialog.open(this.motionInfoDialog, { - width: '400px', - maxWidth: '90vw', - maxHeight: '90vh', - disableClose: true - }); - - dialogRef.keydownEvents().subscribe((event: KeyboardEvent) => { - if (event.key === 'Enter' && event.shiftKey) { - dialogRef.close(this.infoDialog); - } - }); - - // After closing the dialog: Goes through the fields and check if they are changed - // TODO: Logic like this should be handled in a service - dialogRef.afterClosed().subscribe(async (result: InfoDialog) => { - if (result) { - const partialUpdate = { - category_id: result.category !== copyDialog.category ? result.category : undefined, - motion_block_id: result.motionBlock !== copyDialog.motionBlock ? result.motionBlock : undefined, - tags_id: JSON.stringify(result.tags) !== JSON.stringify(copyDialog.tags) ? result.tags : undefined - }; - // TODO: "only update if different" was another repo-todo - if (!Object.keys(partialUpdate).every(key => partialUpdate[key] === undefined)) { - await this.motionRepo.update(partialUpdate, motion).then(null, this.raiseError); + dialogRef.keydownEvents().subscribe((event: KeyboardEvent) => { + if (event.key === 'Enter' && event.shiftKey) { + dialogRef.close(this.infoDialog); } - } - }); - } + }); - /** - * Checks if there are at least categories, motion-blocks or tags the user can select. - */ - public updateStateColumnVisibility(): void { - const metaInfoAvailable = this.isCategoryAvailable() || this.isMotionBlockAvailable() || this.isTagAvailable(); - if (!metaInfoAvailable && this.displayedColumnsDesktop.includes('state')) { - this.displayedColumnsDesktop.splice(this.displayedColumnsDesktop.indexOf('state'), 1); - } else if (metaInfoAvailable && !this.displayedColumnsDesktop.includes('state')) { - this.displayedColumnsDesktop = this.displayedColumnsDesktop.concat('state'); + // After closing the dialog: Goes through the fields and check if they are changed + // TODO: Logic like this should be handled in a service + dialogRef.afterClosed().subscribe(async (result: InfoDialog) => { + if (result) { + const partialUpdate = { + category_id: result.category !== copyDialog.category ? result.category : undefined, + motion_block_id: result.motionBlock !== copyDialog.motionBlock ? result.motionBlock : undefined, + tags_id: + JSON.stringify(result.tags) !== JSON.stringify(copyDialog.tags) ? result.tags : undefined + }; + // TODO: "only update if different" was another repo-todo + if (!Object.keys(partialUpdate).every(key => partialUpdate[key] === undefined)) { + await this.motionRepo.update(partialUpdate, motion).then(null, this.raiseError); + } + } + }); } } /** - * Checks motion-blocks are available. + * Checks if categories are available. * * @returns A boolean if they are available. */ - public isMotionBlockAvailable(): boolean { - return !!this.motionBlocks && this.motionBlocks.length > 0; + public isCategoryAvailable(): boolean { + return !!this.categories && this.categories.length > 0; } /** @@ -607,11 +529,11 @@ export class MotionListComponent extends ListViewBaseComponent 0; + public isMotionBlockAvailable(): boolean { + return !!this.motionBlocks && this.motionBlocks.length > 0; } } diff --git a/client/src/app/site/motions/modules/motion-workflow/components/workflow-list/workflow-list.component.html b/client/src/app/site/motions/modules/motion-workflow/components/workflow-list/workflow-list.component.html index 21a79c80f..fc80d45d8 100644 --- a/client/src/app/site/motions/modules/motion-workflow/components/workflow-list/workflow-list.component.html +++ b/client/src/app/site/motions/modules/motion-workflow/components/workflow-list/workflow-list.component.html @@ -1,33 +1,32 @@ -

Workflows

- - - - - Name - - - {{ workflow.name | translate }} - - + + +
+ +
{{ name }}
+
- - - - - - - - - - -
+ +
+ +
+ diff --git a/client/src/app/site/motions/modules/motion-workflow/components/workflow-list/workflow-list.component.ts b/client/src/app/site/motions/modules/motion-workflow/components/workflow-list/workflow-list.component.ts index a7cc5354c..006427dcf 100644 --- a/client/src/app/site/motions/modules/motion-workflow/components/workflow-list/workflow-list.component.ts +++ b/client/src/app/site/motions/modules/motion-workflow/components/workflow-list/workflow-list.component.ts @@ -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 - implements OnInit { +export class WorkflowListComponent extends ListViewBaseComponent 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 (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 {}, this.raiseError); } } - - /** - * Get the column definition for the current workflow table - * - * @returns The column definition for the table - */ - public getColumnDefinition(): string[] { - return this.columns; - } } diff --git a/client/src/app/site/tags/components/tag-list/tag-list.component.html b/client/src/app/site/tags/components/tag-list/tag-list.component.html index be67cad26..404dd35fa 100644 --- a/client/src/app/site/tags/components/tag-list/tag-list.component.html +++ b/client/src/app/site/tags/components/tag-list/tag-list.component.html @@ -33,13 +33,17 @@
- - - Name - {{ tag.getTitle() }} - - - - - - + + +
+
+ {{ name }} +
+
+
diff --git a/client/src/app/site/tags/components/tag-list/tag-list.component.css b/client/src/app/site/tags/components/tag-list/tag-list.component.scss similarity index 100% rename from client/src/app/site/tags/components/tag-list/tag-list.component.css rename to client/src/app/site/tags/components/tag-list/tag-list.component.scss diff --git a/client/src/app/site/tags/components/tag-list/tag-list.component.ts b/client/src/app/site/tags/components/tag-list/tag-list.component.ts index 89bf531d4..6f74def4f 100644 --- a/client/src/app/site/tags/components/tag-list/tag-list.component.ts +++ b/client/src/app/site/tags/components/tag-list/tag-list.component.ts @@ -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 implements OnInit { +export class TagListComponent extends ListViewBaseComponent implements OnInit { public editTag = false; public newTag = false; public selectedTag: ViewTag; @@ -33,6 +32,16 @@ export class TagListComponent extends ListViewBaseComponent { - this.dataSource.data = []; - this.dataSource.data = newTags; - }); } /** @@ -116,7 +117,7 @@ export class TagListComponent extends ListViewBaseComponent - - + +
+ +
+ {{ name }} +
+
+ + +
- +
+
+ + + {{ group.getTitle() | translate }} + + +
+
+ {{ user.structure_level }} +
+
+ {{ user.number }} +
+
+
- - - - - - {{ isSelected(user) ? 'check_circle' : '' }} - - + +
+
+ + mail + +
+
- - - Projector - - - - - - - - Name - - {{ user.short_name }} - - - - - - Group - -
-
-
- - - {{ group.getTitle() | translate }} - - -
-
- {{ user.structure_level }} -
-
- {{ user.number }} -
-
-
-
-
- - - - - - - - - - - - - -
- - mail - -
-
-
- - - - Presence - -
- - Present - -
-
-
- - - - -
- - -
+ +
+
+ + Present + +
+
+
diff --git a/client/src/app/site/users/components/user-list/user-list.component.scss b/client/src/app/site/users/components/user-list/user-list.component.scss index c073436ba..d9f27e3d8 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.scss +++ b/client/src/app/site/users/components/user-list/user-list.component.scss @@ -1,51 +1,11 @@ @import '~assets/styles/tables.scss'; -.os-listview-table { - .mat-column-projector { - padding-right: 15px; - } +.groupsCell { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; - .mat-column-name { - flex: 1 0 200px; - } - - .mat-column-group { - flex: 2 0 60px; - - .groupsCell { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - - mat-icon { - font-size: 80%; - } - } - } - - .mat-column-presence { - flex: 0 0 60px; - - mat-icon { - font-size: 100%; - margin-right: 5px; - } - - div { - display: inherit; - } + mat-icon { + font-size: 80%; } } - -.infoCell { - max-width: 25px; -} - -.presentCell { - align-content: left; - padding-right: 50px; -} - -.checkboxPresent { - margin-left: 15px; -} diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index 04b4416b5..532522be5 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -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 implements OnInit { +export class UserListComponent extends ListViewBaseComponent implements OnInit { /** * The reference to the template. */ @@ -83,16 +84,6 @@ export class UserListComponent extends ListViewBaseComponent group.id !== 1); @@ -178,14 +186,6 @@ export class UserListComponent extends ListViewBaseComponent (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 { - 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; + // }; + // } } diff --git a/client/src/assets/styles/global-components-style.scss b/client/src/assets/styles/global-components-style.scss index 9a053ab27..bc9a21db4 100644 --- a/client/src/assets/styles/global-components-style.scss +++ b/client/src/assets/styles/global-components-style.scss @@ -20,6 +20,10 @@ color: mat-color($primary); } + .anchor-button { + color: mat-color($foreground, text) !important; + } + .accent, .accent-text { color: mat-color($accent); diff --git a/client/src/styles.scss b/client/src/styles.scss index be3550e22..6d2d8bfa6 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -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 */