Skip to content

Commit

Permalink
Merged PR 11794: GB3-333: DataCatalogue filtering
Browse files Browse the repository at this point in the history
This PR adds the DataCatalogueFilter shenanigans:

* Adds the filter component as a popup
* Adds a basic filter button (without the final design) AND all the chips (yummy)
* Refactored some existing components (i.e. the accordion) to be reusable
* Adds tests (note: I had to use a `get`ter for the filterConfig, because it was not testable using `public readonly`, since I have to play with the configuration to test all paths - it's not optimal, I know)
  • Loading branch information
Tugark committed Aug 14, 2023
2 parents 120ccdf + 116fe17 commit a30b78f
Show file tree
Hide file tree
Showing 33 changed files with 984 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<h1 mat-dialog-title>Filter</h1>
<div mat-dialog-content class="filter-dialog-content">
<cdk-accordion>
<accordion-item *ngFor="let filter of dataCatalogueFilters; trackBy: trackByFilterLabel" [header]="filter.label" variant="dark">
<div class="filter-dialog-content__wrapper">
<mat-checkbox
class="filter-dialog-content__wrapper__checkbox"
(change)="toggleFilter(filter.key, filterValue.value)"
*ngFor="let filterValue of filter.filterValues"
[checked]="filterValue.isActive"
>
{{ filterValue.value }}
</mat-checkbox>
</div>
</accordion-item>
</cdk-accordion>
</div>
<div mat-dialog-actions>
<button (click)="resetFilters()" mat-button>Zurücksetzen</button>
<button (click)="close()" mat-button>Schliessen</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@use 'functions/helper.function' as functions;
@use 'mixins/material.mixin' as mat-mixins;
@use 'variables/ktzh-design-variables' as ktzh-variables;

:host ::ng-deep .filter-dialog-content__wrapper__checkbox {
@include mat-mixins.mat-checkbox-override-accent-color(
functions.get-color-from-palette(ktzh-variables.$zh-secondary-accent),
functions.get-contrast-color-from-palette(ktzh-variables.$zh-secondary-accent)
);
}

.filter-dialog-content {
padding: 20px 24px 20px 24px;

.filter-dialog-content__wrapper {
display: flex;
flex-flow: column;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {MatDialogRef} from '@angular/material/dialog';
import {DataCatalogueFilter} from '../../../shared/interfaces/data-catalogue-filter.interface';
import {Observable, Subscription, tap} from 'rxjs';
import {selectFilters} from '../../../state/data-catalogue/reducers/data-catalogue.reducer';
import {Store} from '@ngrx/store';
import {DataCatalogueActions} from '../../../state/data-catalogue/actions/data-catalogue.actions';
import {DataCatalogueFilterKey} from '../../../shared/types/data-catalogue-filter.type';

@Component({
selector: 'data-catalogue-filter-dialog',
templateUrl: './data-catalogue-filter-dialog.component.html',
styleUrls: ['./data-catalogue-filter-dialog.component.scss'],
})
export class DataCatalogueFilterDialogComponent implements OnInit, OnDestroy {
public dataCatalogueFilters: DataCatalogueFilter[] = [];
private readonly dataCatalogueFilters$: Observable<DataCatalogueFilter[]> = this.store.select(selectFilters);
private readonly subscriptions: Subscription = new Subscription();

constructor(
private readonly dialogRef: MatDialogRef<DataCatalogueFilterDialogComponent>,
private readonly store: Store,
) {}

public ngOnDestroy() {
this.subscriptions.unsubscribe();
}

public ngOnInit() {
this.subscriptions.add(
this.dataCatalogueFilters$.pipe(tap((dataCatalogueFilters) => (this.dataCatalogueFilters = dataCatalogueFilters))).subscribe(),
);
}

public trackByFilterLabel(index: number, item: DataCatalogueFilter) {
return item.label;
}

public toggleFilter(key: DataCatalogueFilterKey, filterValue: string) {
this.store.dispatch(DataCatalogueActions.toggleFilter({key, value: filterValue}));
}

public resetFilters() {
this.store.dispatch(DataCatalogueActions.resetFilters());
}

public close() {
this.dialogRef.close();
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
<start-page-section [background]="'primary'">
<hero-header
heroTitle="Datenkatalog"
heroText="Der Datenkatalog listet Geodaten auf und lässt sich nach verschiedenen Kriterien sortieren. Die Geodaten bestehen aus Geodatensätzen, Geodiensten und GIS-Browser Karten sowie den eigentlichen Geometadaten."
heroTitle="Datenkatalog"
></hero-header>
</start-page-section>
<div class="data-catalogue-overview">
<div class="data-catalogue-overview__content">
<loading-and-process-bar [loadingState]="loadingState"></loading-and-process-bar>

<ng-container *ngIf="loadingState === 'loaded'">
<mat-paginator [pageSize]="20" hidePageSize showFirstLastButtons></mat-paginator>
<div class="data-catalogue-overview__content__active-filters">
<span class="data-catalogue-overview__content__active-filters__result-size">
{{ dataCatalogueItems.data.length }} {{ dataCatalogueItems.data.length === 1 ? 'Resultat' : 'Resultate' }}
</span>
<div>
<mat-chip-row *ngFor="let activeFilter of activeFilters" (removed)="toggleFilter(activeFilter)">
{{ activeFilter.value }}
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
</div>
</div>
<!-- TODO: this button will be styled and located properly within the search bar when that's done -->
<div style="display: flex; justify-content: end">
<button (click)="openFilterWindow()" color="accent" mat-flat-button>Filter</button>
</div>
<cdk-table [dataSource]="dataCatalogueItems">
<ng-container cdkColumnDef="data">
<cdk-cell *cdkCellDef="let row">
Expand All @@ -20,6 +35,7 @@
<!-- Header and Row Declarations -->
<cdk-row *cdkRowDef="let row; columns: ['data']"></cdk-row>
</cdk-table>
<mat-paginator [pageSize]="10" hidePageSize showFirstLastButtons></mat-paginator>
</ng-container>
<b *ngIf="loadingState === 'error'">Fehler beim Laden der Metadatenübersicht.</b>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
max-width: ktzh-variables.$zh-layout-max-content-width;
width: 100%;

.data-catalogue-overview__content__active-filters {
display: flex;
align-items: center;
gap: 8px;

.data-catalogue-overview__content__active-filters__result-size {
display: flex;
flex-shrink: 0;
}
}

.cdk-table {
display: flex;
flex-flow: column;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import {AfterViewInit, Component, Injectable, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {filter, Observable, Subject, Subscription, take, tap} from 'rxjs';
import {filter, Observable, Subject, Subscription, tap} from 'rxjs';
import {Store} from '@ngrx/store';
import {DataCatalogueActions} from '../../../state/data-catalogue/actions/data-catalogue.actions';
import {DataCatalogueState} from '../../../state/data-catalogue/states/data-catalogue.state';
import {selectDataCatalogueState} from '../../../state/data-catalogue/reducers/data-catalogue.reducer';
import {selectLoadingState} from '../../../state/data-catalogue/reducers/data-catalogue.reducer';
import {OverviewMetadataItem} from '../../../shared/models/overview-metadata-item.model';
import {LoadingState} from '../../../shared/types/loading-state.type';
import {MatPaginator, MatPaginatorIntl} from '@angular/material/paginator';
import {MatTableDataSource} from '@angular/material/table';
import {selectDataCatalogueItems} from '../../../state/data-catalogue/selectors/data-catalogue-items.selector';
import {PanelClass} from '../../../shared/enums/panel-class.enum';
import {MatDialog} from '@angular/material/dialog';
import {DataCatalogueFilterDialogComponent} from '../data-catalogue-filter-dialog/data-catalogue-filter-dialog.component';
import {ActiveDataCatalogueFilter} from '../../../shared/interfaces/data-catalogue-filter.interface';
import {selectActiveFilterValues} from '../../../state/data-catalogue/selectors/active-filter-values.selector';

const FILTER_DIALOG_WIDTH_IN_PX = 956;

@Injectable()
class DataCataloguePaginatorIntl implements MatPaginatorIntl {
Expand All @@ -20,14 +27,14 @@ class DataCataloguePaginatorIntl implements MatPaginatorIntl {

public getRangeLabel(page: number, pageSize: number, length: number): string {
if (length === 0) {
return this.getRangeLabelText(1, 1, 0);
return this.getRangeLabelText(1, 1);
}
const amountPages = Math.ceil(length / pageSize);
return this.getRangeLabelText(page + 1, amountPages, length);
return this.getRangeLabelText(page + 1, amountPages);
}

private getRangeLabelText(currentPage: number, amountPages: number, length: number): string {
return `Seite ${currentPage} von ${amountPages} | ${length} Elemente`;
private getRangeLabelText(currentPage: number, amountPages: number): string {
return `Seite ${currentPage} von ${amountPages}`;
}
}

Expand All @@ -39,24 +46,27 @@ class DataCataloguePaginatorIntl implements MatPaginatorIntl {
})
export class DataCatalogueOverviewComponent implements OnInit, OnDestroy, AfterViewInit {
public loadingState: LoadingState = 'undefined';
// We use the MatTableDataSource here because it already has pagination handling embedded - depending on our needs, we might to
// implement a custom DataSource and handle pagination manually.
public dataCatalogueItems: MatTableDataSource<OverviewMetadataItem> = new MatTableDataSource<OverviewMetadataItem>([]);
private readonly dataCatalogue$: Observable<DataCatalogueState> = this.store.select(selectDataCatalogueState);
public activeFilters: ActiveDataCatalogueFilter[] = [];
private readonly activeFilters$: Observable<ActiveDataCatalogueFilter[]> = this.store.select(selectActiveFilterValues);
private readonly dataCatalogueItems$: Observable<OverviewMetadataItem[]> = this.store.select(selectDataCatalogueItems);
private readonly dataCatalogueLoadingState$: Observable<LoadingState> = this.store.select(selectLoadingState);
private readonly subscriptions: Subscription = new Subscription();
@ViewChild(MatPaginator) private paginator!: MatPaginator;

constructor(private readonly store: Store) {
constructor(
private readonly store: Store,
private readonly dialogService: MatDialog,
) {
this.store.dispatch(DataCatalogueActions.loadCatalogue());
}

public ngAfterViewInit() {
// In order for the paginator to correctly work, we need to wait for its rendered state in the DOM.
this.subscriptions.add(
this.dataCatalogue$
this.dataCatalogueLoadingState$
.pipe(
filter(({loadingState}) => loadingState === 'loaded'),
take(1),
filter((loadingState) => loadingState === 'loaded'),
tap(() => {
// This is necessary to force it to be rendered in the next tick, otherwise, changedetection won't pick it up
setTimeout(() => (this.dataCatalogueItems.paginator = this.paginator), 0);
Expand All @@ -67,19 +77,36 @@ export class DataCatalogueOverviewComponent implements OnInit, OnDestroy, AfterV
}

public ngOnInit() {
this.initSubscriptions();
}

public ngOnDestroy() {
this.subscriptions.unsubscribe();
}

public openFilterWindow() {
this.dialogService.open<DataCatalogueFilterDialogComponent>(DataCatalogueFilterDialogComponent, {
panelClass: PanelClass.ApiWrapperDialog,
restoreFocus: false,
width: `${FILTER_DIALOG_WIDTH_IN_PX}px`,
});
}

public toggleFilter({key, value}: ActiveDataCatalogueFilter) {
this.store.dispatch(DataCatalogueActions.toggleFilter({key, value}));
}

private initSubscriptions() {
this.subscriptions.add(this.dataCatalogueLoadingState$.pipe(tap((loadingState) => (this.loadingState = loadingState))).subscribe());
this.subscriptions.add(
this.dataCatalogue$
this.dataCatalogueItems$
.pipe(
tap(({items, loadingState}) => {
tap((items) => {
this.dataCatalogueItems.data = items;
this.loadingState = loadingState;
}),
)
.subscribe(),
);
}

public ngOnDestroy() {
this.subscriptions.unsubscribe();
this.subscriptions.add(this.activeFilters$.pipe(tap((activeFilters) => (this.activeFilters = activeFilters))).subscribe());
}
}
2 changes: 2 additions & 0 deletions src/app/data-catalogue/data-catalogue.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {DataDisplaySectionComponent} from './components/data-display-section/dat
import {DataCatalogueDetailPageComponent} from './components/data-catalogue-detail-page/data-catalogue-detail-page.component';
import {DataCatalogueDetailPageSectionComponent} from './components/data-catalogue-detail-page-section/data-catalogue-detail-page-section.component';
import {DataCatalogueOverviewItemComponent} from './components/data-catalogue-overview-item/data-catalogue-overview-item.component';
import {DataCatalogueFilterDialogComponent} from './components/data-catalogue-filter-dialog/data-catalogue-filter-dialog.component';

@NgModule({
declarations: [
Expand All @@ -30,6 +31,7 @@ import {DataCatalogueOverviewItemComponent} from './components/data-catalogue-ov
DataCatalogueDetailPageComponent,
DataCatalogueDetailPageSectionComponent,
DataCatalogueOverviewItemComponent,
DataCatalogueFilterDialogComponent,
],
imports: [CommonModule, DataCatalogueRoutingModule, MaterialModule, SharedModule],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<cdk-accordion-item
#accordionItem="cdkAccordionItem"
[attr.aria-controls]="'media-contact-body-' + ariaIdentifier"
[attr.aria-expanded]="accordionItem.expanded"
[attr.id]="'media-contact-header-' + ariaIdentifier"
class="accordion-item"
role="button"
tabindex="0"
>
<div class="accordion-item__content" [ngClass]="{'accordion-item__content--dark': variant === 'dark'}">
<div (click)="accordionItem.toggle()" class="accordion-item__content__header mat-h2">
<span>{{ header }}</span>
<span class="accordion-item__content__header__icon-wrapper">
<mat-icon [svgIcon]="accordionItem.expanded ? 'ktzh_remove' : 'ktzh_add'"></mat-icon>
</span>
</div>
<div
[attr.aria-labelledby]="'media-contact-header-' + ariaIdentifier"
[attr.id]="'media-contact-body-' + ariaIdentifier"
[style.display]="accordionItem.expanded ? '' : 'none'"
class="accordion-item__content__body"
role="region"
>
<ng-content></ng-content>
</div>
</div>
</cdk-accordion-item>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@use 'variables/ktzh-design-variables' as ktzh-variables;

.accordion-item {
.accordion-item__content {
border-width: 1px 0;
border-color: ktzh-variables.$zh-white;
border-style: solid;
/* Use negative margin to overlap the borders. Hack? */
/* todo: The issue here is that for this to work, you'd also have to
* set border-bottom to none for all .accordion_item__content:not(:last-child)
* set the negative margin on .accordion_item + .accordion_item
-> That way, it would be properly faked. But it does not work because we cannot access the host container of the host, which would be the accordion?
*/
margin-top: -1px;

&--dark {
border-color: ktzh-variables.$zh-black20;
}

.accordion-item__content__header {
margin: 18px 0;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;

.accordion-item__content__header__icon-wrapper {
display: flex;
}
}

.accordion-item__content__body {
margin-bottom: 24px;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {Component, Input, OnInit} from '@angular/core';

/**
* Implements the KTZH accordion style; to be used with a cdk-accordion element.
*/
@Component({
selector: 'accordion-item',
templateUrl: './accordion-item.component.html',
styleUrls: ['./accordion-item.component.scss'],
})
export class AccordionItemComponent implements OnInit {
/**
* Defines the color of the borders and the text:
* * Light = white borders, white font
* * Dark = dark borders, black font
*/
@Input() public variant: 'light' | 'dark' = 'light';
@Input() public header!: string;
public ariaIdentifier!: string;

public ngOnInit() {
// generate identifier without custom characters and stuff for aria identification
this.ariaIdentifier = btoa(this.header);
}
}
Loading

0 comments on commit a30b78f

Please sign in to comment.