From 448bb0f93c123dbd4c3b209e95933dcaeb26ddc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Sch=C3=B6nb=C3=A4chler?= Date: Tue, 6 Feb 2024 00:54:00 +0100 Subject: [PATCH] refactor(business): introduce ngrx signals --- .eslintrc.json | 5 + package-lock.json | 77 +++++ package.json | 3 + src/app/business/business.store.ts | 208 ++++++++++++ .../business-filter-form.component.html | 30 ++ .../business-filter-form.component.scss | 0 .../business-filter-form.component.ts | 63 ++++ .../business-detail/business-detail.page.html | 15 +- .../business-detail/business-detail.page.ts | 68 +--- .../business-list/business-list.page.html | 49 +-- .../business-list/business-list.page.ts | 317 ++++-------------- .../business-list/search-suggestions.ts | 18 + src/app/business/services/business.service.ts | 30 +- 13 files changed, 510 insertions(+), 373 deletions(-) create mode 100644 src/app/business/business.store.ts create mode 100644 src/app/business/components/business-filter-form/business-filter-form.component.html create mode 100644 src/app/business/components/business-filter-form/business-filter-form.component.scss create mode 100644 src/app/business/components/business-filter-form/business-filter-form.component.ts create mode 100644 src/app/business/containers/business-list/search-suggestions.ts diff --git a/.eslintrc.json b/.eslintrc.json index 71e9842..82e4b04 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,11 @@ { "root": true, "ignorePatterns": ["projects/**/*"], + "settings": { + "import/resolver": { + "typescript": {} + } + }, "overrides": [ { "files": ["*.ts"], diff --git a/package-lock.json b/package-lock.json index 09fda57..9d4c5a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,8 @@ "@ionic/angular": "^7.6.4", "@ionic/storage-angular": "^4.0.0", "@ngneat/until-destroy": "^10.0.0", + "@ngrx/operators": "^17.1.0", + "@ngrx/signals": "^17.1.0", "ionicons": "^7.2.2", "lodash": "^4.17.21", "rxjs": "~7.8.1", @@ -50,6 +52,7 @@ "@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/parser": "6.18.1", "eslint": "^8.56.0", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "2.29.1", "eslint-plugin-jsdoc": "48.0.2", "eslint-plugin-unused-imports": "^3.0.0", @@ -3960,6 +3963,34 @@ "rxjs": "^6.4.0 || ^7.0.0" } }, + "node_modules/@ngrx/operators": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-17.1.0.tgz", + "integrity": "sha512-Dc2vPdBcVpDPy1PAappTRQe6fMXR/hUXjQL5olIqI9jA/08Yj2Y3MMmXG4uPXMj9iVACy1B3kmxz424uTpfc1w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngrx/signals": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-17.1.0.tgz", + "integrity": "sha512-CF387SNYUoPeFCh13S/vmsyFRZHXm7cZjxuwXbI3AWDS/JXO9GC2061O9f0Fy7DIyQtDisY9uGV5eOiKTYmT0w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": "^17.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, "node_modules/@ngtools/webpack": { "version": "17.2.0", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.0.tgz", @@ -8174,6 +8205,31 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, "node_modules/eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -9295,6 +9351,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/git-raw-commits": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", @@ -14104,6 +14172,15 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-url-loader": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", diff --git a/package.json b/package.json index df2193d..473b24a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@ionic/angular": "^7.6.4", "@ionic/storage-angular": "^4.0.0", "@ngneat/until-destroy": "^10.0.0", + "@ngrx/operators": "^17.1.0", + "@ngrx/signals": "^17.1.0", "ionicons": "^7.2.2", "lodash": "^4.17.21", "rxjs": "~7.8.1", @@ -57,6 +59,7 @@ "@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/parser": "6.18.1", "eslint": "^8.56.0", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "2.29.1", "eslint-plugin-jsdoc": "48.0.2", "eslint-plugin-unused-imports": "^3.0.0", diff --git a/src/app/business/business.store.ts b/src/app/business/business.store.ts new file mode 100644 index 0000000..e4189f1 --- /dev/null +++ b/src/app/business/business.store.ts @@ -0,0 +1,208 @@ +import { + getState, + patchState, + signalStore, + withComputed, + withHooks, + withMethods, + withState +} from '@ngrx/signals'; +import { Business, BusinessStatus, BusinessType } from 'swissparl'; +import { computed, inject } from '@angular/core'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { BusinessFilter, BusinessService } from './services/business.service'; +import { filter, pipe, switchMap, tap } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { tapResponse } from '@ngrx/operators'; +import * as _ from 'lodash'; + +type BusinessState = { + businesses: Business[]; + businessTypes: BusinessType[]; + businessStatuses: BusinessStatus[]; + selectedBusinessId: number | null; + hasNoContent: boolean; + isLoading: boolean; + isLoadingMore: boolean; + isRefreshing: boolean; + hasFilterError: boolean; + hasError: boolean; + query: BusinessFilter; +}; + +const initialState: BusinessState = { + businesses: [], + businessTypes: [], + businessStatuses: [], + selectedBusinessId: null, + hasNoContent: false, + isLoading: false, + isLoadingMore: false, + isRefreshing: false, + hasError: false, + hasFilterError: false, + query: { + top: 20, + skip: 0, + searchTerm: '', + businessTypes: [], + businessStatuses: [] + } +}; + +export const BusinessStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withComputed(({ businesses, selectedBusinessId }) => ({ + selectedBusiness: computed(() => + businesses().find((b) => b.ID === selectedBusinessId()) + ) + })), + withMethods((store, businessService = inject(BusinessService)) => ({ + loadBusinesses: rxMethod( + pipe( + tap((query) => + patchState(store, (state) => ({ + isLoading: state.businesses.length === 0, + hasError: false, + hasNoContent: false + })) + ), + switchMap((query) => + businessService.getBusinesses(query).pipe( + tapResponse({ + next: (businesses) => + patchState(store, (state) => ({ + businesses: state.isRefreshing + ? businesses + : [...state.businesses, ...businesses], + hasNoContent: businesses.length === 0 + })), + error: () => patchState(store, { hasError: true }), + finalize: () => + patchState(store, { + isLoading: false, + isLoadingMore: false, + isRefreshing: false + }) + }) + ) + ) + ) + ), + + loadMore: () => { + patchState(store, (state) => ({ + isLoadingMore: true, + query: { + ...state.query, + skip: state.query.skip + state.query.top + } + })); + }, + + refresh: () => { + patchState(store, (state) => ({ + isRefreshing: true, + query: { + ...state.query, + skip: 0 + } + })); + }, + + updateQuery: (query: BusinessFilter) => { + patchState(store, (state) => ({ + businesses: [], + query: { ...state.query, ...query } + })); + }, + + resetQuery: () => { + patchState(store, () => ({ + query: initialState.query + })); + }, + + selectBusiness: rxMethod( + pipe( + filter((id) => { + const state = getState(store); + const isLoaded = state.businesses.find((b) => b.ID === id); + + if (isLoaded) { + patchState(store, { selectedBusinessId: id }); + } + + return !isLoaded; + }), + tap(() => + patchState(store, { + isLoading: true, + hasError: false + }) + ), + switchMap((id) => + businessService.getBusiness(id).pipe( + tapResponse({ + next: (business) => + patchState(store, { + businesses: [business], + selectedBusinessId: business.ID + }), + error: () => patchState(store, { hasError: true }), + finalize: () => patchState(store, { isLoading: false }) + }) + ) + ) + ) + ), + + loadBusinessStates: rxMethod( + pipe( + tap(() => + patchState(store, { + hasFilterError: false + }) + ), + switchMap(() => + businessService.getBusinessStatus().pipe( + map((businessStates) => + _.uniqBy(businessStates, 'BusinessStatusId') + ), // TODO move _.uniqBy to service + tapResponse({ + next: (businessStatuses) => + patchState(store, { businessStatuses }), + error: () => patchState(store, { hasFilterError: true }) + }) + ) + ) + ) + ), + + loadBusinessTypes: rxMethod( + pipe( + tap(() => + patchState(store, { + hasFilterError: false + }) + ), + switchMap(() => + businessService.getBusinessTypes().pipe( + tapResponse({ + next: (businessTypes) => patchState(store, { businessTypes }), + error: () => patchState(store, { hasFilterError: true }) + }) + ) + ) + ) + ) + })), + + withHooks({ + onInit: ({ loadBusinesses, loadBusinessStates, loadBusinessTypes }) => { + loadBusinessStates(null); + loadBusinessTypes(null); + } + }) +); diff --git a/src/app/business/components/business-filter-form/business-filter-form.component.html b/src/app/business/components/business-filter-form/business-filter-form.component.html new file mode 100644 index 0000000..1c4ffc2 --- /dev/null +++ b/src/app/business/components/business-filter-form/business-filter-form.component.html @@ -0,0 +1,30 @@ +
+

Geschäftstyp

+ + + {{ businessType.BusinessTypeName }} + + +

Geschäftsstatus

+ + + {{ businessStatus.BusinessStatusName }} + + + +
+ Filter anwenden +
+
+ + diff --git a/src/app/business/components/business-filter-form/business-filter-form.component.scss b/src/app/business/components/business-filter-form/business-filter-form.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/business/components/business-filter-form/business-filter-form.component.ts b/src/app/business/components/business-filter-form/business-filter-form.component.ts new file mode 100644 index 0000000..43e612f --- /dev/null +++ b/src/app/business/components/business-filter-form/business-filter-form.component.ts @@ -0,0 +1,63 @@ +import { + Component, + computed, + EventEmitter, + inject, + Output +} from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { NgForOf, NgIf } from '@angular/common'; +import { BusinessStore } from '../../business.store'; +import { ErrorScreenComponent } from '../../../shared/components/error-screen/error-screen.component'; + +@Component({ + selector: 'app-business-filter-form', + templateUrl: './business-filter-form.component.html', + styleUrls: ['./business-filter-form.component.scss'], + standalone: true, + imports: [ + FormsModule, + IonicModule, + NgForOf, + NgIf, + ReactiveFormsModule, + ErrorScreenComponent + ] +}) +export class BusinessFilterFormComponent { + readonly store = inject(BusinessStore); + + @Output() submitFilter = new EventEmitter(); + + businessTypeCheckboxes = computed(() => + this.store.businessTypes().map((type) => ({ + ...type, + checked: this.store.query().businessTypes.some((t) => t.ID === type.ID) + })) + ); + + businessStatusCheckboxes = computed(() => + this.store.businessStatuses().map((status) => ({ + ...status, + checked: this.store + .query() + .businessStatuses.some( + (s) => s.BusinessStatusId === status.BusinessStatusId + ) + })) + ); + + onSubmit() { + this.submitFilter.emit(); + this.store.updateQuery({ + ...this.store.query(), + businessTypes: this.businessTypeCheckboxes().filter( + (type) => type.checked + ), + businessStatuses: this.businessStatusCheckboxes().filter( + (status) => status.checked + ) + }); + } +} diff --git a/src/app/business/containers/business-detail/business-detail.page.html b/src/app/business/containers/business-detail/business-detail.page.html index 003abdb..38cade1 100644 --- a/src/app/business/containers/business-detail/business-detail.page.html +++ b/src/app/business/containers/business-detail/business-detail.page.html @@ -11,20 +11,21 @@ - - - + + + Zu den Abstimmungen @@ -35,5 +36,5 @@ - + diff --git a/src/app/business/containers/business-detail/business-detail.page.ts b/src/app/business/containers/business-detail/business-detail.page.ts index 10c66b9..4b3ca7d 100644 --- a/src/app/business/containers/business-detail/business-detail.page.ts +++ b/src/app/business/containers/business-detail/business-detail.page.ts @@ -1,18 +1,13 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Business } from 'swissparl'; -import { BusinessService } from '../../services/business.service'; -import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { catchError, first, switchMap, tap } from 'rxjs/operators'; -import { Subject, from, of } from 'rxjs'; +import { Component, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, RouterLink } from '@angular/router'; import { IonicModule } from '@ionic/angular'; import { BusinessCardComponent } from '../../components/business-card/business-card.component'; import { BusinessDetailTextComponent } from '../../components/business-detail-text/business-detail-text.component'; import { LoadingScreenComponent } from '../../../shared/components/loading-screen/loading-screen.component'; import { ErrorScreenComponent } from '../../../shared/components/error-screen/error-screen.component'; import { NgIf } from '@angular/common'; +import { BusinessStore } from '../../business.store'; -@UntilDestroy() @Component({ selector: 'app-business-detail', templateUrl: './business-detail.page.html', @@ -24,62 +19,19 @@ import { NgIf } from '@angular/common'; BusinessCardComponent, BusinessDetailTextComponent, LoadingScreenComponent, - ErrorScreenComponent + ErrorScreenComponent, + RouterLink ] }) export class BusinessDetailPage implements OnInit { - business: Business = null; - loading = true; - error = false; - - trigger$ = new Subject(); - - constructor( - private router: Router, - private route: ActivatedRoute, - private businessService: BusinessService - ) {} + readonly store = inject(BusinessStore); + readonly route = inject(ActivatedRoute); ngOnInit() { - from(this.trigger$) - .pipe( - untilDestroyed(this), - tap(() => (this.loading = true)), - switchMap(() => - this.businessService - .getBusiness(parseInt(this.route.snapshot.params.id)) - .pipe( - first(), - catchError(() => { - this.error = true; - this.loading = false; - return of(null); - }) - ) - ) - ) - .subscribe((business) => { - if (business === null) { - return; - } - this.business = business; - this.loading = false; - }); - } - - ionViewDidEnter() { - if (this.business === null) { - this.trigger$.next(); - } - } - - goToVote() { - this.router.navigate(['layout', 'votes'], { - queryParams: { BusinessShortNumber: this.business.BusinessShortNumber } - }); + this.store.selectBusiness(parseInt(this.route.snapshot.params.id)); } - retrySearch() { - this.trigger$.next(); + retry() { + this.store.selectBusiness(parseInt(this.route.snapshot.params.id)); } } diff --git a/src/app/business/containers/business-list/business-list.page.html b/src/app/business/containers/business-list/business-list.page.html index 6ecaab7..15e8d1e 100644 --- a/src/app/business/containers/business-list/business-list.page.html +++ b/src/app/business/containers/business-list/business-list.page.html @@ -20,13 +20,13 @@
- + {{ businessType.BusinessTypeName }} - - {{ businesStatus.BusinessStatusName }} + + {{ businessStatus.BusinessStatusName }}
@@ -40,14 +40,14 @@ - - - + + + @@ -77,37 +77,8 @@ - -
-

Geschäftstyp

- - - {{ businessType.BusinessTypeName }} - - -

Geschäftsstatus

- - - {{ businessStatus.BusinessStatusName }} - - -
- -
- Filter anwenden -
+ +
diff --git a/src/app/business/containers/business-list/business-list.page.ts b/src/app/business/containers/business-list/business-list.page.ts index 93f1fee..6976a81 100644 --- a/src/app/business/containers/business-list/business-list.page.ts +++ b/src/app/business/containers/business-list/business-list.page.ts @@ -1,27 +1,31 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + OnInit, + ViewChild +} from '@angular/core'; import { Router } from '@angular/router'; -import { BehaviorSubject, Subject, combineLatest, from, of } from 'rxjs'; -import { map, first, tap, catchError, switchMap } from 'rxjs/operators'; -import { Business, BusinessStatus, BusinessType } from 'swissparl'; -import { BusinessService } from '../../services/business.service'; +import { ReactiveFormsModule } from '@angular/forms'; import { - FormArray, - FormBuilder, - FormControl, - FormGroup, - ReactiveFormsModule -} from '@angular/forms'; -import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import * as _ from 'lodash'; -import { IonicModule, IonSearchbar, Platform } from '@ionic/angular'; + InfiniteScrollCustomEvent, + IonicModule, + IonSearchbar, + Platform, + RefresherCustomEvent +} from '@ionic/angular'; import { Keyboard } from '@capacitor/keyboard'; import { NgFor, NgIf } from '@angular/common'; import { BusinessCardComponent } from '../../components/business-card/business-card.component'; import { LoadingScreenComponent } from '../../../shared/components/loading-screen/loading-screen.component'; import { ErrorScreenComponent } from '../../../shared/components/error-screen/error-screen.component'; import { NoContentScreenComponent } from '../../../shared/components/no-content-screen/no-content-screen.component'; +import { BusinessStore } from '../../business.store'; +import { SearchSuggestions } from './search-suggestions'; +import { HideKeyboardOnEnterDirective } from '../../../shared/directives/hide-keyboard-on-enter.directive'; +import { BusinessFilterFormComponent } from '../../components/business-filter-form/business-filter-form.component'; -@UntilDestroy() @Component({ selector: 'app-business-list', templateUrl: './business-list.page.html', @@ -35,196 +39,46 @@ import { NoContentScreenComponent } from '../../../shared/components/no-content- BusinessCardComponent, LoadingScreenComponent, ErrorScreenComponent, - NoContentScreenComponent - ] + NoContentScreenComponent, + HideKeyboardOnEnterDirective, + BusinessFilterFormComponent + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class BusinessListPage implements OnInit { - top = 20; - skip = 0; - businesses: Business[] = []; - loading = true; - error = false; - noContent = false; - isModalOpen = false; - showFilterButton = false; - presentingElement = null; - showSuggestedSearches: boolean = false; - suggestedSearchTerms: string[] = [ - 'Internationale Politik', - 'Verkehr', - 'Finanzwesen', - 'Soziale Fragen', - 'Umwelt', - 'Bildung', - 'Wirtschaft', - 'Staatspolitik', - 'Migration', - 'Gesundheit', - 'Parlament', - 'Strafrecht', - 'Steuer', - 'Europapolitik', - 'Sicherheitspolitik', - 'Raumplanung' - ]; - - filterForm: FormGroup; - filterError = false; - searchTerm$ = new BehaviorSubject(''); - - businessStatuses: BusinessStatus[] = []; - businessTypes: BusinessType[] = []; - selectedBusinessTypes: BusinessType[] = []; - selectedBusinessStatuses: BusinessStatus[] = []; - @ViewChild('searchBar', { static: false }) searchBar: IonSearchbar; - trigger$ = new Subject(); - filter$ = new BehaviorSubject<{ - businessTypes: number[]; - businessStatuses: number[]; - }>({ - businessTypes: [], - businessStatuses: [] - }); - - constructor( - private businessService: BusinessService, - private router: Router, - private platform: Platform, - private fb: FormBuilder - ) {} + readonly store = inject(BusinessStore); + readonly router = inject(Router); + readonly platform = inject(Platform); - ngOnInit() { - this.presentingElement = document.querySelector( - 'app-business-list .ion-page' - ); - - this.filterForm = this.fb.group({ - businessStatus: new FormArray([]), - businessType: new FormArray([]) - }); - - from(this.trigger$) - .pipe( - untilDestroyed(this), - tap(() => (this.filterError = false)), - switchMap(() => - this.businessService.getBusinessStatus().pipe( - catchError(() => { - this.filterError = true; - return of(null); - }) - ) - ), - map((businessStatus) => _.uniqBy(businessStatus, 'BusinessStatusId')) - ) - .subscribe((businessStatus) => { - if (businessStatus === null) { - return; - } - businessStatus.forEach(() => { - const control = new FormControl(false); - (this.filterForm.controls.businessStatus as FormArray).push(control); - }); - this.businessStatuses = businessStatus; - }); - - from(this.trigger$) - .pipe( - untilDestroyed(this), - tap(() => (this.filterError = false)), - switchMap(() => - this.businessService.getBusinessTypes().pipe( - catchError(() => { - this.filterError = true; - return of(null); - }) - ) - ) - ) - .subscribe((businessTypes) => { - if (businessTypes === null) { - return; - } - businessTypes.forEach(() => { - const control = new FormControl(false); - (this.filterForm.controls.businessType as FormArray).push(control); - }); - this.businessTypes = businessTypes; - }); + isModalOpen = false; + presentingElement = null; - combineLatest([this.searchTerm$, this.filter$, this.trigger$]) - .pipe( - untilDestroyed(this), - tap(() => { - this.skip = 0; - this.error = false; - this.loading = true; - }), - switchMap(([searchTerm, { businessStatuses, businessTypes }]) => - this.businessService - .getBusinesses({ - top: this.top, - skip: this.skip, - searchTerm, - businessStatuses, - businessTypes - }) - .pipe( - catchError(() => { - this.error = true; - this.loading = false; - return of(null); - }) - ) - ) - ) - .subscribe((businesses) => { - if (businesses === null) { - return; - } - this.businesses = businesses; - this.noContent = businesses.length === 0; - this.loading = false; - }); + showSuggestedSearches: boolean = false; + suggestedSearchTerms = SearchSuggestions; - this.filterForm.valueChanges - .pipe(untilDestroyed(this)) - .subscribe((formValues) => { - this.selectedBusinessTypes = []; - formValues.businessType.forEach((value, index) => { - if (value) { - this.selectedBusinessTypes.push(this.businessTypes[index]); - } - }); + refreshOrLoadMoreEvent: InfiniteScrollCustomEvent | RefresherCustomEvent; - this.selectedBusinessStatuses = []; - formValues.businessStatus.forEach((value, index) => { - if (value) { - this.selectedBusinessStatuses.push(this.businessStatuses[index]); - } + constructor() { + effect(() => { + if (!this.store.isLoadingMore() && !this.store.isRefreshing()) { + this.refreshOrLoadMoreEvent?.target?.complete().catch(() => { + console.error('Error completing refresh or load more event'); }); + } + }); + } - if ( - this.selectedBusinessTypes.length > 0 || - this.selectedBusinessStatuses.length > 0 - ) { - this.showFilterButton = true; - } - }); - + ngOnInit() { this.addKeyBoardListener(); - } - ionViewDidEnter() { - // trigger search again if we are coming back from another page and no items have been loaded yet - if (this.businesses.length === 0) { - this.trigger$.next(); - } + this.store.loadBusinesses(this.store.query); + + this.presentingElement = document.querySelector('ion-router-outlet'); } - addKeyBoardListener() { + private addKeyBoardListener() { if (!this.platform.is('capacitor')) return; Keyboard.addListener('keyboardWillHide', () => { this.showSuggestedSearches = false; @@ -240,90 +94,41 @@ export class BusinessListPage implements OnInit { onSearch(event: any) { this.showSuggestedSearches = false; - this.searchTerm$.next(event.target.value); + this.store.updateQuery({ + ...this.store.query(), + searchTerm: event.target.value + }); } onSuggestedSearchTopic(searchTerm: string) { this.showSuggestedSearches = false; this.searchBar.value = searchTerm; - this.searchTerm$.next(searchTerm); + this.store.updateQuery({ + ...this.store.query(), + searchTerm + }); } retrySearch() { - this.trigger$.next(); + this.store.loadBusinesses(this.store.query()); } resetFilter() { - this.filterForm.reset(); - this.searchTerm$.next(''); this.searchBar.value = ''; - this.filter$.next({ - businessTypes: [], - businessStatuses: [] - }); + this.store.resetQuery(); } - distanceReached(event: any) { - this.skip += this.top; - this.fetchBusinesses().subscribe((newBusinesses) => { - if (newBusinesses === null) { - return; - } - this.businesses = [...this.businesses, ...newBusinesses]; - event.target.complete(); - }); + distanceReached(event: InfiniteScrollCustomEvent) { + this.refreshOrLoadMoreEvent = event; + this.store.loadMore(); } - handleRefresh(event) { - this.skip = 0; - this.fetchBusinesses().subscribe((businesses) => { - if (businesses === null) { - return; - } - this.businesses = businesses; - event.target.complete(); - }); - } - - fetchBusinesses() { - return this.businessService - .getBusinesses({ - top: this.top, - skip: this.skip, - searchTerm: this.searchTerm$.getValue(), - businessStatuses: this.filter$.getValue().businessStatuses, - businessTypes: this.filter$.getValue().businessTypes - }) - .pipe( - first(), - catchError(() => { - this.error = true; - return of(null); - }) - ); + handleRefresh(event: RefresherCustomEvent) { + this.refreshOrLoadMoreEvent = event; + this.store.refresh(); } onClickBusiness(id: number) { this.router.navigate(['business', 'detail', id]); } - - applyFilter() { - this.toggleFilterModal(); - this.filter$.next({ - businessTypes: this.selectedBusinessTypes.map((bt) => bt.ID), - businessStatuses: this.selectedBusinessStatuses.map( - (bs) => bs.BusinessStatusId - ) - }); - } - - get businessTypesArray(): FormControl[] { - return (this.filterForm.controls.businessType as FormArray) - .controls as FormControl[]; - } - - get businessStatusesArray(): FormControl[] { - return (this.filterForm.controls.businessStatus as FormArray) - .controls as FormControl[]; - } } diff --git a/src/app/business/containers/business-list/search-suggestions.ts b/src/app/business/containers/business-list/search-suggestions.ts new file mode 100644 index 0000000..6501317 --- /dev/null +++ b/src/app/business/containers/business-list/search-suggestions.ts @@ -0,0 +1,18 @@ +export const SearchSuggestions: string[] = [ + 'Internationale Politik', + 'Verkehr', + 'Finanzwesen', + 'Soziale Fragen', + 'Umwelt', + 'Bildung', + 'Wirtschaft', + 'Staatspolitik', + 'Migration', + 'Gesundheit', + 'Parlament', + 'Strafrecht', + 'Steuer', + 'Europapolitik', + 'Sicherheitspolitik', + 'Raumplanung' +]; diff --git a/src/app/business/services/business.service.ts b/src/app/business/services/business.service.ts index 7c91286..03d8330 100644 --- a/src/app/business/services/business.service.ts +++ b/src/app/business/services/business.service.ts @@ -4,6 +4,14 @@ import { map } from 'rxjs/operators'; import { SwissParlService } from '../../shared/services/swissparl.service'; import { Business, BusinessStatus, BusinessType } from 'swissparl'; +export type BusinessFilter = { + top: number; + skip?: number; + searchTerm?: string; + businessTypes?: BusinessType[]; + businessStatuses?: BusinessStatus[]; +}; + @Injectable({ providedIn: 'root' }) @@ -16,23 +24,19 @@ export class BusinessService { searchTerm, businessTypes, businessStatuses - }: { - top: number; - skip?: number; - searchTerm?: string; - businessTypes?: number[]; - businessStatuses?: number[]; - }): Observable { - var businessTypeFilterArray = []; - if (businessTypes && businessTypes.length > 0) { - businessTypes.forEach((id) => { + }: BusinessFilter): Observable { + const businessTypeIds = businessTypes.map((bt) => bt.ID); + const businessTypeFilterArray = []; + if (businessTypeIds && businessTypes.length > 0) { + businessTypeIds.forEach((id) => { businessTypeFilterArray.push({ BusinessType: id }); }); } - var businessStatusFilterArray = []; - if (businessStatuses && businessStatuses.length > 0) { - businessStatuses.forEach((id) => { + const businessStatusIds = businessStatuses.map((bs) => bs.BusinessStatusId); + const businessStatusFilterArray = []; + if (businessStatusIds && businessStatuses.length > 0) { + businessStatusIds.forEach((id) => { businessStatusFilterArray.push({ BusinessStatus: id }); }); }