diff --git a/Phonebook.Frontend/.dockerignore b/Phonebook.Frontend/.dockerignore index 045d816fd..6e9f1c0f7 100644 --- a/Phonebook.Frontend/.dockerignore +++ b/Phonebook.Frontend/.dockerignore @@ -8,6 +8,9 @@ !protractor.conf.js !proxy.conf.json !tsconfig.json +!tsconfig.app.json +!tsconfig.spec.json +!tsconfig.worker.json !tslint.json !nginx !substitute_variables.sh diff --git a/Phonebook.Frontend/.gitignore b/Phonebook.Frontend/.gitignore index 438765172..169882c5a 100644 --- a/Phonebook.Frontend/.gitignore +++ b/Phonebook.Frontend/.gitignore @@ -1,13 +1,18 @@ # See http://help.github.com/ignore-files/ for more about ignoring files. # compiled output -dist/ +/dist /tmp /out-tsc -/src/changelog.md +# Only exists if Bazel was run +/bazel-out # dependencies -node_modules/ +/node_modules + +# profiling files +chrome-profiler-events.json +speed-measure-plugin.json # IDEs and editors /.idea @@ -25,6 +30,7 @@ node_modules/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +.history/* # misc /.sass-cache @@ -32,16 +38,20 @@ node_modules/ /coverage /libpeerconnection.log npm-debug.log +yarn-error.log testem.log /typings -yarn-error.log key.pem cert.pem -# e2e -/e2e/*.js -/e2e/*.map - # System Files .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + + +# Custom Settings + +/src/changelog.md + +## e2e +/e2e/*.map diff --git a/Phonebook.Frontend/Dockerfile b/Phonebook.Frontend/Dockerfile index 5ee3703a2..00ab67a83 100644 --- a/Phonebook.Frontend/Dockerfile +++ b/Phonebook.Frontend/Dockerfile @@ -12,7 +12,7 @@ RUN npm ci # Because: https://stackoverflow.com/questions/37715224/copy-multiple-directories-with-one-command COPY ./src/ ./src/ -COPY ["angular.json", "tsconfig.json", "tslint.json", "./"] +COPY ["angular.json", "tsconfig.json", "tsconfig.app.json", "tsconfig.spec.json", "tsconfig.worker.json", "tslint.json", "./"] RUN npm run build:de RUN npm run build:en diff --git a/Phonebook.Frontend/angular.json b/Phonebook.Frontend/angular.json index ca8b01674..8cff6c37e 100644 --- a/Phonebook.Frontend/angular.json +++ b/Phonebook.Frontend/angular.json @@ -14,7 +14,7 @@ "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", - "tsConfig": "src/tsconfig.app.json", + "tsConfig": "tsconfig.app.json", "polyfills": "src/polyfills.ts", "assets": [ "src/assets", @@ -79,7 +79,7 @@ "index": "src/index.html", "main": "src/main.ts", "showCircularDependencies": true, - "tsConfig": "src/tsconfig.app.json", + "tsConfig": "tsconfig.app.json", "polyfills": "src/polyfills.ts", "assets": [ "src/assets", @@ -112,7 +112,8 @@ "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } - ] + ], + "webWorkerTsConfig": "tsconfig.worker.json" }, "configurations": { "en": { @@ -172,7 +173,7 @@ "main": "src/test.ts", "karmaConfig": "./karma.conf.js", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", + "tsConfig": "tsconfig.spec.json", "scripts": [], "sourceMap": false, "styles": [ @@ -196,38 +197,22 @@ "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": [ - "src/tsconfig.app.json", - "src/tsconfig.spec.json" + "tsconfig.app.json", + "tsconfig.spec.json", + "e2e/tsconfig.json", + "tsconfig.worker.json" ], "exclude": [ "**/node_modules/**" ] } - } - } - }, - "phonebook-e2e": { - "root": "", - "sourceRoot": "", - "projectType": "application", - "architect": { + }, "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { - "protractorConfig": "./protractor.conf.js", + "protractorConfig": "e2e/protractor.conf.js", "devServerTarget": "phonebook:serve" } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": [ - "e2e/tsconfig.e2e.json" - ], - "exclude": [ - "**/node_modules/**" - ] - } } } } diff --git a/Phonebook.Frontend/protractor.conf.js b/Phonebook.Frontend/e2e/protractor.conf.js similarity index 87% rename from Phonebook.Frontend/protractor.conf.js rename to Phonebook.Frontend/e2e/protractor.conf.js index 72f9b3d18..4102ac543 100644 --- a/Phonebook.Frontend/protractor.conf.js +++ b/Phonebook.Frontend/e2e/protractor.conf.js @@ -6,9 +6,12 @@ process.env.CHROME_BIN = require('puppeteer').executablePath(); const { SpecReporter } = require('jasmine-spec-reporter'); +/** + * @type { import("protractor").Config } + */ exports.config = { allScriptsTimeout: 11000, - specs: ['./e2e/**/*.e2e-spec.ts'], + specs: ['./src/**/*.e2e-spec.ts'], capabilities: { browserName: 'chrome', binary: process.env.CHROME_BIN, @@ -26,7 +29,7 @@ exports.config = { }, onPrepare() { require('ts-node').register({ - project: 'e2e/tsconfig.e2e.json' + project: require('path').join(__dirname, './tsconfig.json') }); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); }, diff --git a/Phonebook.Frontend/e2e/app.e2e-spec.ts b/Phonebook.Frontend/e2e/src/app.e2e-spec.ts similarity index 100% rename from Phonebook.Frontend/e2e/app.e2e-spec.ts rename to Phonebook.Frontend/e2e/src/app.e2e-spec.ts diff --git a/Phonebook.Frontend/e2e/app.po.ts b/Phonebook.Frontend/e2e/src/app.po.ts similarity index 100% rename from Phonebook.Frontend/e2e/app.po.ts rename to Phonebook.Frontend/e2e/src/app.po.ts diff --git a/Phonebook.Frontend/e2e/tsconfig.e2e.json b/Phonebook.Frontend/e2e/tsconfig.json similarity index 60% rename from Phonebook.Frontend/e2e/tsconfig.e2e.json rename to Phonebook.Frontend/e2e/tsconfig.json index 5a4688fe3..677f30ff8 100644 --- a/Phonebook.Frontend/e2e/tsconfig.e2e.json +++ b/Phonebook.Frontend/e2e/tsconfig.json @@ -2,13 +2,8 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/e2e", - "baseUrl": "./", "module": "commonjs", "target": "es5", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] + "types": ["jasmine", "jasminewd2", "node"] } -} \ No newline at end of file +} diff --git a/Phonebook.Frontend/src/app/modules/table/PersonsDataSource.ts b/Phonebook.Frontend/src/app/modules/table/PersonsDataSource.ts index cb1bbd3e1..70fb9e346 100644 --- a/Phonebook.Frontend/src/app/modules/table/PersonsDataSource.ts +++ b/Phonebook.Frontend/src/app/modules/table/PersonsDataSource.ts @@ -1,9 +1,10 @@ +import { HttpClient } from '@angular/common/http'; import { MatTableDataSource } from '@angular/material/table'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { TableLogic } from 'src/app/modules/table/table-logic'; -import { Column, SearchFilter, TableSort } from 'src/app/shared/models'; +import { BehaviorSubject, Observable, Subscriber } from 'rxjs'; +import { performSearch, SearchParams } from 'src/app/modules/table/SearchParams'; +import { SearchFilter, TableSort } from 'src/app/shared/models'; import { Person } from 'src/app/shared/models/classes'; -import { PhonebookSortDirection } from 'src/app/shared/models/enumerables/PhonebookSortDirection'; +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; export class PersonsDataSource extends MatTableDataSource { private PAGE_SIZE: number = 30; @@ -32,9 +33,9 @@ export class PersonsDataSource extends MatTableDataSource { public dataChanged: BehaviorSubject = new BehaviorSubject(this.data); - private lastFilterKeyword: string = ''; + private worker: Worker | null = null; - constructor(private dataSource: Person[]) { + constructor(private dataSource: Person[], private httpClient: HttpClient) { super(); } @@ -50,41 +51,63 @@ export class PersonsDataSource extends MatTableDataSource { public refresh( filterKeyword: string, searchFilters: SearchFilter[], - searchableColumns: Column[], - sort: TableSort + searchableColumns: ColumnId[], + sort: TableSort | null ): Observable { this.loadingSubject.next(true); return new Observable(observer => { - let preResult = this.dataSource; + const searchParams: SearchParams = { + filterKeyword: filterKeyword, + searchFilters: searchFilters, + searchableColumns: searchableColumns, + sort: sort, + data: this.dataSource + }; - // Filtering - searchFilters.forEach(searchFilter => { - preResult = TableLogic.filter(preResult, searchFilter.filterValue, [searchFilter.filterColumn]); - }); - - // Searching - let searchResult: Person[] = TableLogic.filter(preResult, filterKeyword, searchableColumns); - - // Sorting - switch (sort.direction) { - case PhonebookSortDirection.none: { - searchResult = TableLogic.rankedSort(searchResult, filterKeyword, searchableColumns); - break; - } - default: { - searchResult = TableLogic.sort(searchResult, sort); - break; + if (typeof Worker !== 'undefined') { + // Reuse the Worker if it already exists. + if (this.worker == null) { + // Loading external Workers does not error if it could not be found... + this.httpClient + .get('table.worker', { + responseType: 'text' + }) + .subscribe( + req => { + const blob = new Blob([req]); + this.worker = new Worker(window.URL.createObjectURL(blob), { type: 'module' }); + this.worker.onmessage = ({ data }) => { + this.resolveObserver(data, observer); + }; + this.worker.onerror = (error: ErrorEvent) => { + const searchResult = performSearch(searchParams); + this.resolveObserver(searchResult, observer); + throw new Error('Service Worker crashed.'); + }; + this.worker.postMessage(searchParams); + }, + () => { + const searchResult = performSearch(searchParams); + this.resolveObserver(searchResult, observer); + throw new Error('Service Worker could not be loaded.'); + } + ); } + } else { + const searchResult = performSearch(searchParams); + this.resolveObserver(searchResult, observer); } - this.allData = searchResult; - this.lastFilterKeyword = filterKeyword; - - this.loadingSubject.next(false); - observer.next(searchResult); - observer.complete(); }); } + private resolveObserver(searchResult: Person[], observer: Subscriber): void { + this.allData = searchResult; + + this.loadingSubject.next(false); + observer.next(searchResult); + observer.complete(); + } + private updateVisibleData() { this.dataChanged.next(this.allData.slice(0, this.pageSize)); } diff --git a/Phonebook.Frontend/src/app/modules/table/SearchParams.ts b/Phonebook.Frontend/src/app/modules/table/SearchParams.ts new file mode 100644 index 000000000..0a3cb66b9 --- /dev/null +++ b/Phonebook.Frontend/src/app/modules/table/SearchParams.ts @@ -0,0 +1,31 @@ +import { TableLogic } from 'src/app/modules/table/table-logic'; +import { Person, PhonebookSortDirection, SearchFilter, TableSort } from 'src/app/shared/models'; +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; + +export class SearchParams { + public filterKeyword: string; + public searchFilters: SearchFilter[]; + public searchableColumns: ColumnId[]; + public sort: TableSort | null; + public data: Person[]; +} + +export function performSearch(searchParams: SearchParams): Person[] { + let preResult = searchParams.data; + + // Filtering + searchParams.searchFilters.forEach(searchFilter => { + preResult = TableLogic.filter(preResult, searchFilter.filterValue, [searchFilter.filterColumn]); + }); + + // Searching + let searchResult: Person[] = TableLogic.filter(preResult, searchParams.filterKeyword, searchParams.searchableColumns); + + // Sorting + if (searchParams.sort == null || searchParams.sort.direction === PhonebookSortDirection.none) { + searchResult = TableLogic.rankedSort(searchResult, searchParams.filterKeyword, searchParams.searchableColumns); + } else { + searchResult = TableLogic.sort(searchResult, searchParams.sort); + } + return searchResult; +} diff --git a/Phonebook.Frontend/src/app/modules/table/components/table/table.component.ts b/Phonebook.Frontend/src/app/modules/table/components/table/table.component.ts index 1573fec9e..a9264eea9 100644 --- a/Phonebook.Frontend/src/app/modules/table/components/table/table.component.ts +++ b/Phonebook.Frontend/src/app/modules/table/components/table/table.component.ts @@ -1,3 +1,4 @@ +import { HttpClient } from '@angular/common/http'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSort, SortDirection } from '@angular/material/sort'; @@ -21,10 +22,10 @@ import { SearchState, SetTableResultCount, TableState, UpdateUrl } from 'src/app }) export class TableComponent implements OnInit, OnDestroy { public get displayedColumns(): string[] { - return this.store.selectSnapshot(TableState.visibleColumns).map(col => col.id); + return this.store.selectSnapshot(TableState.visibleColumns); } - public dataSource: PersonsDataSource = new PersonsDataSource([]); + public dataSource: PersonsDataSource = new PersonsDataSource([], this.httpClient); public onTop: boolean = true; public columns: typeof ColumnDefinitions = ColumnDefinitions; public previewPerson: Person | null = null; @@ -36,11 +37,12 @@ export class TableComponent implements OnInit, OnDestroy { @ViewChild(MatSort, { static: true }) public sort: MatSort; public table: Element; - public get tableSort(): TableSort { - const col = ColumnDefinitions.getAll().find(column => { - return this.sort.active === column.id; - }); - return { column: col || null, direction: this.sort.direction as PhonebookSortDirection }; + public get tableSort(): TableSort | null { + const col = ColumnDefinitions.getColumnById(this.sort.active); + if (col == null) { + return null; + } + return { column: col.id, direction: this.sort.direction as PhonebookSortDirection }; } public sortDirection: SortDirection = ''; public sortActive: string = ''; @@ -50,12 +52,13 @@ export class TableComponent implements OnInit, OnDestroy { private personService: PersonService, public dialog: MatDialog, public store: Store, - public columnTranslate: ColumnTranslate + public columnTranslate: ColumnTranslate, + private httpClient: HttpClient ) {} public ngOnInit() { this.personService.getAll().subscribe(persons => { - this.dataSource = new PersonsDataSource(persons); + this.dataSource = new PersonsDataSource(persons, this.httpClient); // Defer until Data is loaded. this.refreshTableSubscription = merge( @@ -87,7 +90,7 @@ export class TableComponent implements OnInit, OnDestroy { this.sort.sortChange.subscribe((ev: { active: string; direction: string }) => { this.store.dispatch( new UpdateUrl({ - tableSort: this.tableSort + tableSort: this.tableSort || undefined }) ); this.refreshTable(); diff --git a/Phonebook.Frontend/src/app/modules/table/dialogs/table-settings-dialog/table-settings.dialog.html b/Phonebook.Frontend/src/app/modules/table/dialogs/table-settings-dialog/table-settings.dialog.html index 26711c067..618ba91e5 100644 --- a/Phonebook.Frontend/src/app/modules/table/dialogs/table-settings-dialog/table-settings.dialog.html +++ b/Phonebook.Frontend/src/app/modules/table/dialogs/table-settings-dialog/table-settings.dialog.html @@ -30,7 +30,7 @@

- {{ columnTranslate.getTranslation(column.id) }} + {{ columnTranslate.getTranslation(column) }} drag_handle @@ -53,7 +53,7 @@

- {{ columnTranslate.getTranslation(column.id) }} + {{ columnTranslate.getTranslation(column) }} drag_handle diff --git a/Phonebook.Frontend/src/app/modules/table/dialogs/table-settings-dialog/table-settings.dialog.ts b/Phonebook.Frontend/src/app/modules/table/dialogs/table-settings-dialog/table-settings.dialog.ts index 667f99054..b0e8d4486 100644 --- a/Phonebook.Frontend/src/app/modules/table/dialogs/table-settings-dialog/table-settings.dialog.ts +++ b/Phonebook.Frontend/src/app/modules/table/dialogs/table-settings-dialog/table-settings.dialog.ts @@ -3,7 +3,7 @@ import { Component, OnInit } from '@angular/core'; import { Store } from '@ngxs/store'; import { ColumnDefinitions } from 'src/app/shared/config/columnDefinitions'; import { ColumnTranslate } from 'src/app/shared/config/columnTranslate'; -import { Column } from 'src/app/shared/models'; +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; import { ResetTableSettings, SetVisibleTableColumns, TableState } from 'src/app/shared/states'; @Component({ @@ -12,8 +12,8 @@ import { ResetTableSettings, SetVisibleTableColumns, TableState } from 'src/app/ styleUrls: ['./table-settings.dialog.scss'] }) export class TableSettingsDialog implements OnInit { - public notDisplayedColumns: Column[] = []; - public displayedColumns: Column[] = this.store.selectSnapshot(TableState.visibleColumns); + public notDisplayedColumns: ColumnId[] = []; + public displayedColumns: ColumnId[] = this.store.selectSnapshot(TableState.visibleColumns); constructor(public store: Store, public columnTranslate: ColumnTranslate) {} @@ -22,11 +22,13 @@ export class TableSettingsDialog implements OnInit { } private updateNotDisplayedColumns() { - this.notDisplayedColumns = ColumnDefinitions.getAll().filter(col => { - return !this.displayedColumns.some(column => { - return col.id === column.id; - }); - }); + this.notDisplayedColumns = ColumnDefinitions.getAll() + .filter(col => { + return !this.displayedColumns.some(columnId => { + return col.id === columnId; + }); + }) + .map(col => col.id); } public resetTableSettings() { diff --git a/Phonebook.Frontend/src/app/modules/table/table-logic.spec.ts b/Phonebook.Frontend/src/app/modules/table/table-logic.spec.ts index 1c759cf75..bb5a11dc2 100644 --- a/Phonebook.Frontend/src/app/modules/table/table-logic.spec.ts +++ b/Phonebook.Frontend/src/app/modules/table/table-logic.spec.ts @@ -1,5 +1,15 @@ import { ColumnDefinitions } from 'src/app/shared/config/columnDefinitions'; -import { Business, City, Contacts, Location, Messenger, Person, PersonType, PhonebookSortDirection } from 'src/app/shared/models'; +import { + Business, + City, + Contacts, + Location, + Messenger, + Person, + PersonType, + PhonebookSortDirection +} from 'src/app/shared/models'; +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; import { TableLogic } from './table-logic'; describe('Table Logic - Sort', () => { @@ -32,7 +42,7 @@ describe('Table Logic - Sort', () => { ]; expect( TableLogic.sort(unsortedPersonsArray, { - column: ColumnDefinitions.fullname, + column: ColumnId.fullname, direction: PhonebookSortDirection.asc }) ).toEqual([ @@ -92,7 +102,7 @@ describe('Table Logic - Sort', () => { expect( TableLogic.sort(unsortedPersonsArray, { - column: ColumnDefinitions.fullname, + column: ColumnId.fullname, direction: PhonebookSortDirection.desc }) ).toEqual([ @@ -140,7 +150,7 @@ describe('Table Logic - Filter', () => { new Business([], [], [], [], [], [], '') ) ]; - expect(TableLogic.filter(unsortedPersonsArray, 'Mustermann', [ColumnDefinitions.fullname])).toEqual([ + expect(TableLogic.filter(unsortedPersonsArray, 'Mustermann', [ColumnDefinitions.fullname.id])).toEqual([ new Person( PersonType.Interner_Mitarbeiter, '', @@ -171,7 +181,7 @@ describe('Table Logic - Filter', () => { new Business([], [], [], [], [], [], '') ) ]; - expect(TableLogic.filter(unsortedPersonsArray, 'Otherman', [ColumnDefinitions.fullname])).toEqual([]); + expect(TableLogic.filter(unsortedPersonsArray, 'Otherman', [ColumnDefinitions.fullname.id])).toEqual([]); }); it('Find Person with Diarectics', () => { @@ -191,7 +201,7 @@ describe('Table Logic - Filter', () => { ]; expect( TableLogic.filter(unsortedPersonsArray, 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖòóôõöÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž', [ - ColumnDefinitions.fullname + ColumnDefinitions.fullname.id ]) ).toEqual([ new Person( diff --git a/Phonebook.Frontend/src/app/modules/table/table-logic.ts b/Phonebook.Frontend/src/app/modules/table/table-logic.ts index 2edbbd1c3..b43e3da14 100644 --- a/Phonebook.Frontend/src/app/modules/table/table-logic.ts +++ b/Phonebook.Frontend/src/app/modules/table/table-logic.ts @@ -1,7 +1,7 @@ import { Helpers } from 'src/app/modules/table/helpers'; import { ColumnDefinitions } from 'src/app/shared/config/columnDefinitions'; -import { Column } from 'src/app/shared/models'; import { Person } from 'src/app/shared/models/classes/Person'; +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; import { PhonebookSortDirection } from 'src/app/shared/models/enumerables/PhonebookSortDirection'; import { TableSort } from 'src/app/shared/models/interfaces/TableSort'; @@ -15,7 +15,7 @@ export class TableLogic { * @param filterString * @param columns */ - public static filter(persons: Person[], filterString: string, searchColumns: Column[]): Person[] { + public static filter(persons: Person[], filterString: string, searchColumns: ColumnId[]): Person[] { if (filterString === '') { return persons; } @@ -23,7 +23,7 @@ export class TableLogic { return persons.filter(person => { for (let i = 0; i < searchColumns.length; i++) { - if (searchColumns[i].filterFunction(searchString, person)) { + if (ColumnDefinitions[searchColumns[i]].filterFunction(searchString, person)) { return true; } } @@ -33,7 +33,7 @@ export class TableLogic { public static sort(list: Person[], sort: TableSort): Person[] { const sortedArray = list.slice(); - const col = sort.column; + const col = ColumnDefinitions[sort.column]; if (col != null) { return sortedArray.sort((a, b) => { return col.sortFunction(a, b, sort.direction); @@ -47,7 +47,7 @@ export class TableLogic { * @param filterString The keyword you are ranking after. * @param columns Column enum with set Flags for each Column you want to search in. */ - public static rankedSort(list: Person[], rankString: string, columns: Column[]): Person[] { + public static rankedSort(list: Person[], rankString: string, columns: ColumnId[]): Person[] { const rankedList: RankedListItem[] = list.map(person => { return new RankedListItem(person); }); @@ -60,8 +60,8 @@ export class TableLogic { // Calculate the rank for all items. columns.forEach(col => { - if (col.filterFunction(searchString, x.item)) { - x.rank += col.rank; + if (ColumnDefinitions[col].filterFunction(searchString, x.item)) { + x.rank += ColumnDefinitions[col].rank; } }); } diff --git a/Phonebook.Frontend/src/app/modules/table/table.worker.ts b/Phonebook.Frontend/src/app/modules/table/table.worker.ts new file mode 100644 index 000000000..50d87b9f8 --- /dev/null +++ b/Phonebook.Frontend/src/app/modules/table/table.worker.ts @@ -0,0 +1,7 @@ +import { performSearch } from 'src/app/modules/table/SearchParams'; + +/// + +addEventListener('message', ({ data }) => { + postMessage(performSearch(data)); +}); diff --git a/Phonebook.Frontend/src/app/services/api/person.service.ts b/Phonebook.Frontend/src/app/services/api/person.service.ts index c5990849f..1c77b356c 100644 --- a/Phonebook.Frontend/src/app/services/api/person.service.ts +++ b/Phonebook.Frontend/src/app/services/api/person.service.ts @@ -3,8 +3,8 @@ import { Injectable } from '@angular/core'; import { ConnectableObservable, Observable } from 'rxjs'; import { map, publishReplay } from 'rxjs/operators'; import { TableLogic } from 'src/app/modules/table/table-logic'; -import { ColumnDefinitions } from 'src/app/shared/config/columnDefinitions'; import { Business, Contacts, Location, Messenger, Person, PhonebookSortDirection, Room } from 'src/app/shared/models'; +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; @Injectable() export class PersonService { @@ -77,7 +77,7 @@ export class PersonService { const observable = this.http.get('/api/persons').pipe( map(personArray => { return TableLogic.sort(this.generateRealPersonArray(personArray), { - column: ColumnDefinitions.fullname, + column: ColumnId.fullname, direction: PhonebookSortDirection.asc }); }), diff --git a/Phonebook.Frontend/src/app/shared/components/add-filter/add-filter.component.ts b/Phonebook.Frontend/src/app/shared/components/add-filter/add-filter.component.ts index d0b6f3b51..5baf62507 100644 --- a/Phonebook.Frontend/src/app/shared/components/add-filter/add-filter.component.ts +++ b/Phonebook.Frontend/src/app/shared/components/add-filter/add-filter.component.ts @@ -38,6 +38,6 @@ export class AddFilterComponent { if (this.resetSearchTerm) { this.store.dispatch(new UpdateUrl({ searchTerm: '' })); } - this.store.dispatch(new AddSearchFilter({ filterColumn: this.filterColumn, filterValue: this.filterValue })); + this.store.dispatch(new AddSearchFilter({ filterColumn: this.filterColumn.id, filterValue: this.filterValue })); } } diff --git a/Phonebook.Frontend/src/app/shared/components/search/search.component.html b/Phonebook.Frontend/src/app/shared/components/search/search.component.html index cdd15230c..875f6ecfc 100644 --- a/Phonebook.Frontend/src/app/shared/components/search/search.component.html +++ b/Phonebook.Frontend/src/app/shared/components/search/search.component.html @@ -4,7 +4,7 @@ - {{ columnTranslate.getTranslation(filter.filterColumn.id) }}: {{ filter.filterValue }} + {{ columnTranslate.getTranslation(filter.filterColumn) }}: {{ filter.filterValue }} cancel diff --git a/Phonebook.Frontend/src/app/shared/components/search/search.component.ts b/Phonebook.Frontend/src/app/shared/components/search/search.component.ts index d4904136f..f64d2839d 100644 --- a/Phonebook.Frontend/src/app/shared/components/search/search.component.ts +++ b/Phonebook.Frontend/src/app/shared/components/search/search.component.ts @@ -6,7 +6,14 @@ import { Observable } from 'rxjs'; import { ColumnDefinitions } from 'src/app/shared/config/columnDefinitions'; import { ColumnTranslate } from 'src/app/shared/config/columnTranslate'; import { SearchFilter } from 'src/app/shared/models'; -import { AddSearchFilter, RemoveLastSearchFilter, RemoveSearchFilter, ResetSearch, SearchState, UpdateUrl } from 'src/app/shared/states'; +import { + AddSearchFilter, + RemoveLastSearchFilter, + RemoveSearchFilter, + ResetSearch, + SearchState, + UpdateUrl +} from 'src/app/shared/states'; @Component({ selector: 'app-search', @@ -80,7 +87,7 @@ export class SearchComponent implements OnInit, OnDestroy { return keyvalue[0].toLowerCase() === this.columnTranslate.getTranslation(col.id).toLowerCase(); }); if (col != null) { - this.store.dispatch(new AddSearchFilter({ filterColumn: col, filterValue: keyvalue[1] })); + this.store.dispatch(new AddSearchFilter({ filterColumn: col.id, filterValue: keyvalue[1] })); (event.target as HTMLInputElement).value = ''; } else { this.snackBar diff --git a/Phonebook.Frontend/src/app/shared/config/columnDefinitions.ts b/Phonebook.Frontend/src/app/shared/config/columnDefinitions.ts index 1eb3d466d..9f658f380 100644 --- a/Phonebook.Frontend/src/app/shared/config/columnDefinitions.ts +++ b/Phonebook.Frontend/src/app/shared/config/columnDefinitions.ts @@ -17,6 +17,9 @@ export const ColumnDefinitions: { room: Readonly; building: Readonly; costcenter: Readonly; + /** + * Use as a last resort, there is function for finding a Column by Id + */ getAll(): Readonly[]; getDefault(): Readonly[]; getAllFilterableColumns(): Readonly[]; diff --git a/Phonebook.Frontend/src/app/shared/models/classes/Service.ts b/Phonebook.Frontend/src/app/shared/models/classes/Service.ts index 6e5b3d9ea..6ac36740f 100644 --- a/Phonebook.Frontend/src/app/shared/models/classes/Service.ts +++ b/Phonebook.Frontend/src/app/shared/models/classes/Service.ts @@ -1,3 +1,5 @@ +import { Location } from 'src/app/shared/models/classes/Location'; + /* tslint:disable:variable-name */ export class Service { public Location: Location; diff --git a/Phonebook.Frontend/src/app/shared/models/interfaces/SearchFilter.ts b/Phonebook.Frontend/src/app/shared/models/interfaces/SearchFilter.ts index 60b803331..abdf6faa1 100644 --- a/Phonebook.Frontend/src/app/shared/models/interfaces/SearchFilter.ts +++ b/Phonebook.Frontend/src/app/shared/models/interfaces/SearchFilter.ts @@ -1,6 +1,6 @@ -import { Column } from './Column'; +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; export interface SearchFilter { - filterColumn: Column; + filterColumn: ColumnId; filterValue: string; } diff --git a/Phonebook.Frontend/src/app/shared/models/interfaces/TableSort.ts b/Phonebook.Frontend/src/app/shared/models/interfaces/TableSort.ts index 60aa436c2..b38724b7d 100644 --- a/Phonebook.Frontend/src/app/shared/models/interfaces/TableSort.ts +++ b/Phonebook.Frontend/src/app/shared/models/interfaces/TableSort.ts @@ -1,7 +1,7 @@ +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; import { PhonebookSortDirection } from 'src/app/shared/models/enumerables/PhonebookSortDirection'; -import { Column } from './Column'; export interface TableSort { - column: Column | null; + column: ColumnId; direction: PhonebookSortDirection; } diff --git a/Phonebook.Frontend/src/app/shared/states/Search.state.spec.ts b/Phonebook.Frontend/src/app/shared/states/Search.state.spec.ts index ba2aa920f..c7afff611 100644 --- a/Phonebook.Frontend/src/app/shared/states/Search.state.spec.ts +++ b/Phonebook.Frontend/src/app/shared/states/Search.state.spec.ts @@ -3,7 +3,15 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgxsRouterPluginModule } from '@ngxs/router-plugin'; import { NgxsModule, Store } from '@ngxs/store'; import { ColumnDefinitions } from 'src/app/shared/config/columnDefinitions'; -import { AddSearchFilter, RemoveLastSearchFilter, RemoveSearchFilter, ResetSearch, SearchState, SetSearchFiltersAndSearchTerm } from 'src/app/shared/states/Search.state'; +import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; +import { + AddSearchFilter, + RemoveLastSearchFilter, + RemoveSearchFilter, + ResetSearch, + SearchState, + SetSearchFiltersAndSearchTerm +} from 'src/app/shared/states/Search.state'; const ROUTER_STATE = { state: { @@ -89,19 +97,19 @@ describe('[States] Search', () => { SearchState.searchFilters({ searchTerm: '', searchFilters: [ - { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }, - { filterColumn: ColumnDefinitions.email, filterValue: 'testValueEmail' } + { filterColumn: ColumnId.city, filterValue: 'testValueCity' }, + { filterColumn: ColumnId.email, filterValue: 'testValueEmail' } ] }) ).toEqual([ - { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }, - { filterColumn: ColumnDefinitions.email, filterValue: 'testValueEmail' } + { filterColumn: ColumnId.city, filterValue: 'testValueCity' }, + { filterColumn: ColumnId.email, filterValue: 'testValueEmail' } ]); }); it('it adds Search Filter', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; - const filter2 = { filterColumn: ColumnDefinitions.email, filterValue: 'testValueEmail' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; + const filter2 = { filterColumn: ColumnId.email, filterValue: 'testValueEmail' }; expect(store.selectSnapshot(storeSnapshot => storeSnapshot.searchstate.searchFilters)).toEqual([]); store.dispatch(new AddSearchFilter(filter1)); expect(store.selectSnapshot(storeSnapshot => storeSnapshot.searchstate.searchFilters)).toEqual([filter1]); @@ -112,8 +120,8 @@ describe('[States] Search', () => { }); it('it adds Search Filter - update if different', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; - const filter1derivate = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCityDerivate' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; + const filter1derivate = { filterColumn: ColumnId.city, filterValue: 'testValueCityDerivate' }; store.dispatch(new AddSearchFilter(filter1)); expect(store.selectSnapshot(storeSnapshot => storeSnapshot.searchstate.searchFilters)).toEqual([filter1]); store.dispatch(new AddSearchFilter(filter1derivate)); @@ -121,7 +129,7 @@ describe('[States] Search', () => { }); it('it removes Search Filter', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; store.reset({ searchstate: { searchTerm: '', @@ -134,8 +142,8 @@ describe('[States] Search', () => { }); it('it removes Search Filter - independently', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; - const filter2 = { filterColumn: ColumnDefinitions.email, filterValue: 'testValueEmail' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; + const filter2 = { filterColumn: ColumnId.email, filterValue: 'testValueEmail' }; const filter3 = { filterColumn: ColumnDefinitions.costcenter, filterValue: 'testValueCostcenter' }; store.reset({ searchstate: { @@ -149,7 +157,7 @@ describe('[States] Search', () => { }); it('it removes Search Filter - no Filter existing', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; store.reset({ searchstate: { searchTerm: '', @@ -162,8 +170,8 @@ describe('[States] Search', () => { }); it('it removes Search Filter - Filter not existing', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; - const filter2 = { filterColumn: ColumnDefinitions.email, filterValue: 'testValueEmail' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; + const filter2 = { filterColumn: ColumnId.email, filterValue: 'testValueEmail' }; const filter3 = { filterColumn: ColumnDefinitions.costcenter, filterValue: 'testValueCostcenter' }; store.reset({ searchstate: { @@ -177,8 +185,8 @@ describe('[States] Search', () => { }); it('it remove last Search Filter', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; - const filter2 = { filterColumn: ColumnDefinitions.email, filterValue: 'testValueEmail' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; + const filter2 = { filterColumn: ColumnId.email, filterValue: 'testValueEmail' }; const filter3 = { filterColumn: ColumnDefinitions.costcenter, filterValue: 'testValueCostcenter' }; store.reset({ searchstate: { @@ -192,7 +200,7 @@ describe('[States] Search', () => { }); it('it resets Search', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; store.reset({ searchstate: { searchTerm: 'helloTest', @@ -208,7 +216,7 @@ describe('[States] Search', () => { }); it('it sets SearchFilters and SearchTerm', () => { - const filter1 = { filterColumn: ColumnDefinitions.city, filterValue: 'testValueCity' }; + const filter1 = { filterColumn: ColumnId.city, filterValue: 'testValueCity' }; store.dispatch(new SetSearchFiltersAndSearchTerm([filter1], 'helloTest')); expect(store.selectSnapshot(storeSnapshot => storeSnapshot.searchstate)).toEqual({ searchTerm: 'helloTest', diff --git a/Phonebook.Frontend/src/app/shared/states/Search.state.ts b/Phonebook.Frontend/src/app/shared/states/Search.state.ts index cfc2e8e58..2b663ccfe 100644 --- a/Phonebook.Frontend/src/app/shared/states/Search.state.ts +++ b/Phonebook.Frontend/src/app/shared/states/Search.state.ts @@ -80,7 +80,7 @@ export class SearchState { const col = ColumnDefinitions.getColumnById(key); if (col != null) { searchFilter.push({ - filterColumn: col, + filterColumn: col.id, filterValue: pathSegment.queryParamMap.get(key) || '' }); } @@ -107,13 +107,11 @@ export class SearchState { @Selector() public static searchFilters(state: SearchStateModel): SearchFilter[] { return state.searchFilters.map(filter => { - const tmp = ColumnDefinitions.getAll().find(c => { - return c.id === filter.filterColumn.id; - }); + const tmp = ColumnDefinitions.getColumnById(filter.filterColumn); if (tmp == null) { throw Error('Filter Column not found.'); } - filter.filterColumn = tmp; + filter.filterColumn = tmp.id; return filter; }); } @@ -121,7 +119,7 @@ export class SearchState { @Action(AddSearchFilter) public addSearchFilter(ctx: StateContext, action: AddSearchFilter) { const state = ctx.getState(); - const index = state.searchFilters.findIndex(f => f.filterColumn.id === action.searchFilter.filterColumn.id); + const index = state.searchFilters.findIndex(f => f.filterColumn === action.searchFilter.filterColumn); if (index >= 0) { state.searchFilters[index] = action.searchFilter; } else { @@ -185,7 +183,7 @@ export class SearchState { const routeSnapshot: ActivatedRouteSnapshot = routeState.root; if (update.searchFilter != null) { update.searchFilter.forEach(filter => { - params[filter.filterColumn.id] = filter.filterValue; + params[filter.filterColumn] = filter.filterValue; }); // Remove Query Params that are not used anymore by setting them 'null' explicitly if (routeSnapshot.firstChild != null && routeSnapshot.firstChild.firstChild != null) { @@ -201,7 +199,7 @@ export class SearchState { } if (update.tableSort != null) { if (update.tableSort.direction !== PhonebookSortDirection.none) { - params.sortColumn = update.tableSort.column ? update.tableSort.column.id : null; + params.sortColumn = update.tableSort.column ? update.tableSort.column : null; params.sortDirection = update.tableSort.direction; } else { params.sortColumn = null; diff --git a/Phonebook.Frontend/src/app/shared/states/Table.state.spec.ts b/Phonebook.Frontend/src/app/shared/states/Table.state.spec.ts index 00cafafe2..a1ed0408f 100644 --- a/Phonebook.Frontend/src/app/shared/states/Table.state.spec.ts +++ b/Phonebook.Frontend/src/app/shared/states/Table.state.spec.ts @@ -1,6 +1,5 @@ import { async, TestBed } from '@angular/core/testing'; import { NgxsModule, Store } from '@ngxs/store'; -import { ColumnDefinitions } from 'src/app/shared/config/columnDefinitions'; import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; import { SetVisibleTableColumns, TableState } from 'src/app/shared/states'; import { SetTableResultCount } from 'src/app/shared/states/Table.state'; @@ -28,7 +27,7 @@ describe('[States] Table', () => { visibleColumns: [ColumnId.building], resultCount: 0 }) - ).toEqual([ColumnDefinitions.building]); + ).toEqual([ColumnId.building]); }); it('it should return search Table Result Count', () => { @@ -60,9 +59,9 @@ describe('[States] Table', () => { ColumnId.city, ColumnId.role ]); - store.dispatch(new SetVisibleTableColumns([ColumnDefinitions.building])); + store.dispatch(new SetVisibleTableColumns([ColumnId.building])); expect(store.selectSnapshot(storeSnapshot => storeSnapshot.tablestate.visibleColumns)).toEqual([ColumnId.building]); - store.dispatch(new SetVisibleTableColumns([ColumnDefinitions.role, ColumnDefinitions.picture])); + store.dispatch(new SetVisibleTableColumns([ColumnId.role, ColumnId.picture])); expect(store.selectSnapshot(storeSnapshot => storeSnapshot.tablestate.visibleColumns)).toEqual([ ColumnId.role, ColumnId.picture diff --git a/Phonebook.Frontend/src/app/shared/states/Table.state.ts b/Phonebook.Frontend/src/app/shared/states/Table.state.ts index 157c01ff4..c08e4c0c8 100644 --- a/Phonebook.Frontend/src/app/shared/states/Table.state.ts +++ b/Phonebook.Frontend/src/app/shared/states/Table.state.ts @@ -1,11 +1,10 @@ import { Action, Selector, State, StateContext } from '@ngxs/store'; import { ColumnDefinitions, getColumnsAsStringArray } from 'src/app/shared/config/columnDefinitions'; -import { Column } from 'src/app/shared/models'; import { ColumnId } from 'src/app/shared/models/enumerables/ColumnId'; export class SetVisibleTableColumns { public static readonly type: string = '[Table State] Set visible Table Columns'; - constructor(public columns: Column[]) {} + constructor(public columns: ColumnId[]) {} } export class ResetTableSettings { @@ -31,16 +30,8 @@ export interface TableStateModel { }) export class TableState { @Selector() - public static visibleColumns(state: TableStateModel): Column[] { - return state.visibleColumns.map(col => { - const tmp = ColumnDefinitions.getAll().find(c => { - return c.id === col; - }); - if (tmp == null) { - throw Error('TableState: Column with ID:' + col + ') not found.'); - } - return tmp; - }); + public static visibleColumns(state: TableStateModel): ColumnId[] { + return state.visibleColumns; } @Selector() @@ -51,7 +42,7 @@ export class TableState { @Action(SetVisibleTableColumns) public setVisibleTableColumns(ctx: StateContext, action: SetVisibleTableColumns) { const state = ctx.getState(); - ctx.setState({ ...state, visibleColumns: action.columns.map(col => col.id) }); + ctx.setState({ ...state, visibleColumns: action.columns }); } @Action(ResetTableSettings) diff --git a/Phonebook.Frontend/src/tsconfig.spec.json b/Phonebook.Frontend/src/tsconfig.spec.json deleted file mode 100644 index db18d250d..000000000 --- a/Phonebook.Frontend/src/tsconfig.spec.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/spec", - "types": [ - "node", - "jasmine" - ] - }, - "files": [ - "test.ts", - "polyfills.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] -} \ No newline at end of file diff --git a/Phonebook.Frontend/src/tsconfig.app.json b/Phonebook.Frontend/tsconfig.app.json similarity index 61% rename from Phonebook.Frontend/src/tsconfig.app.json rename to Phonebook.Frontend/tsconfig.app.json index 5d60cc770..c89ddca04 100644 --- a/Phonebook.Frontend/src/tsconfig.app.json +++ b/Phonebook.Frontend/tsconfig.app.json @@ -1,22 +1,18 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/app", - "types": [ - "node" - ], - "strict": true, - "strictNullChecks": true, - "noImplicitAny": false - }, - "angularCompilerOptions": { - "fullTemplateTypeCheck": true, - "strictInjectionParameters": true, - "preserveWhitespaces": false, - "strictPropertyInitialization": false - }, - "exclude": [ - "test.ts", - "**/*.spec.ts" - ] -} \ No newline at end of file +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [], + "strict": true, + "strictNullChecks": true, + "noImplicitAny": false + }, + "angularCompilerOptions": { + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "preserveWhitespaces": false, + "strictPropertyInitialization": false + }, + "include": ["src/**/*.ts"], + "exclude": ["src/test.ts", "src/**/*.spec.ts", "src/**/*.worker.ts"] +} diff --git a/Phonebook.Frontend/tsconfig.json b/Phonebook.Frontend/tsconfig.json index e2450102f..a2f0b264e 100644 --- a/Phonebook.Frontend/tsconfig.json +++ b/Phonebook.Frontend/tsconfig.json @@ -2,22 +2,17 @@ "compileOnSave": false, "compilerOptions": { "baseUrl": "./", - "downlevelIteration": true, - "module": "esnext", "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, + "module": "esnext", "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, + "importHelpers": true, "target": "es2015", - "typeRoots": [ - "node_modules/@types" - ], - "lib": [ - "es2017", - "dom" - ], - "paths": {} + "typeRoots": ["node_modules/@types"], + "lib": ["es2018", "dom"], + "downlevelIteration": true } -} \ No newline at end of file +} diff --git a/Phonebook.Frontend/tsconfig.spec.json b/Phonebook.Frontend/tsconfig.spec.json new file mode 100644 index 000000000..430cf757c --- /dev/null +++ b/Phonebook.Frontend/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine", "node"] + }, + "files": ["src/test.ts", "src/polyfills.ts"], + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/Phonebook.Frontend/tsconfig.worker.json b/Phonebook.Frontend/tsconfig.worker.json new file mode 100644 index 000000000..6cc331e24 --- /dev/null +++ b/Phonebook.Frontend/tsconfig.worker.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/worker", + "lib": ["es2018", "webworker"], + "types": [] + }, + "include": ["src/**/*.worker.ts"] +} diff --git a/phonebook.code-workspace b/phonebook.code-workspace index 0911cd79a..484c981fc 100644 --- a/phonebook.code-workspace +++ b/phonebook.code-workspace @@ -39,6 +39,10 @@ ], "settings": { "cSpell.words": ["Kubernetes", "Phonebook"], + "editor.codeActionsOnSave": { + "source.organizeImports": true, + "source.fixAll": true + }, "javascript.preferences.quoteStyle": "single", "typescript.preferences.quoteStyle": "single", "javascript.referencesCodeLens.enabled": true,