Skip to content
This repository has been archived by the owner on Jan 24, 2023. It is now read-only.

Commit

Permalink
Merge pull request #3270 from cloudfoundry-incubator/hide-first-empty…
Browse files Browse the repository at this point in the history
…-filter

List Multifilter Improvements
  • Loading branch information
nwmac committed Jan 7, 2019
2 parents 795a2bc + 8aab1b2 commit d1619fa
Show file tree
Hide file tree
Showing 15 changed files with 303 additions and 122 deletions.
22 changes: 11 additions & 11 deletions src/frontend/app/shared/components/list/list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@
</div>
<!-- Multi filters, such as filter app wall by cf/org/space -->
<div class="list-component__header__left--multi-filters" [hidden]="(!(hasRows$ | async) && !filter) || (isAddingOrSelecting$ | async)">
<mat-form-field *ngFor="let multiFilterConfig of multiFilterConfigs" [floatLabel]="'never'">
<mat-select id="{{multiFilterConfig.key}}" matInput [(value)]="multiFilters[multiFilterConfig.key]" [disabled]="(dataSource.isLoadingPage$ | async) || (multiFilterConfig.loading$ | async) || !(multiFilterConfig.list$ | async) || !(multiFilterConfig.list$ | async).length" (selectionChange)="multiFilterConfig.select.next($event.value)">
<mat-option>{{ multiFilterConfig.allLabel || 'All' }}</mat-option>
<mat-option *ngFor="let selectItem of multiFilterConfig.list$ | async" [value]="selectItem.value">
{{selectItem.label}}
</mat-option>
</mat-select>
<mat-placeholder>{{multiFilterConfig.label}}</mat-placeholder>
</mat-form-field>
<app-stateful-icon *ngIf="(multiFilterConfigsLoading$ | async)" [state]="'busy'"></app-stateful-icon>
<ng-container *ngFor="let multiFilterManager of multiFilterManagers; first as isFirst">
<mat-form-field *ngIf="!isFirst || !(multiFilterManager.hasOneItem$ | async)" [floatLabel]="'never'">
<mat-select id="{{multiFilterManager.filterKey}}" matInput [(value)]="multiFilterManager.initialValue" [disabled]="!(multiFilterManager.filterIsReady$ | async)" (selectionChange)="multiFilterManager.selectItem($event.value)">
<mat-option>{{ multiFilterManager.allLabel }}</mat-option>
<mat-option *ngFor="let selectItem of multiFilterManager.filterItems$ | async" [value]="selectItem.value">
{{selectItem.label}}
</mat-option>
</mat-select>
<mat-placeholder>{{multiFilterManager.multiFilterConfig.label}}</mat-placeholder>
</mat-form-field>
</ng-container>
</div>
</div>
<div class="list-component__header__right">
Expand All @@ -61,7 +62,6 @@
<mat-icon style="transform: rotate(180deg);">sort</mat-icon>
</button>
</div>

<!-- Global actions (those not applied to specific rows) -->
<div *ngIf="!(isAddingOrSelecting$ | async) && globalActions?.length > 0">
<ng-container *ngFor="let action of globalActions">
Expand Down
49 changes: 49 additions & 0 deletions src/frontend/app/shared/components/list/list.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ describe('ListComponent', () => {
label: 'filterItemLabel',
item: 'filterItemItem',
value: 'filterItemValue'
},
{
label: 'filterItemLabel2',
item: 'filterItemItem2',
value: 'filterItemValue2'
}
]),
loading$: observableOf(false),
Expand Down Expand Up @@ -221,6 +226,50 @@ describe('ListComponent', () => {
// const sortSection: HTMLElement = headerRightSection.querySelector('.sort');
// expect(sortSection.hidden).toBeFalsy();
}));

it('First filter hidden if only one option', async(() => {
component.config.getMultiFiltersConfigs = () => {
return [
{
key: 'filterTestKey',
label: 'filterTestLabel',
list$: observableOf([
{
label: 'filterItemLabel',
item: 'filterItemItem',
value: 'filterItemValue'
},
]),
loading$: observableOf(false),
select: new BehaviorSubject(false)
}
];
};
component.config.enableTextFilter = true;
component.config.viewType = ListViewTypes.CARD_ONLY;
component.config.defaultView = 'card' as ListView;
component.config.cardComponent = EndpointCardComponent;
component.config.getColumns = () => [
{
columnId: 'filterTestKey',
headerCell: () => 'a',
cellDefinition: {
getValue: (row) => `${row}`
},
sort: true,
}
];

fixture.detectChanges();

const hostElement = fixture.nativeElement;

// multi filters
const multiFilterSection: HTMLElement = hostElement.querySelector('.list-component__header__left--multi-filters');
expect(multiFilterSection.hidden).toBeFalsy();
expect(multiFilterSection.childElementCount).toBe(0);

}));
});


Expand Down
47 changes: 32 additions & 15 deletions src/frontend/app/shared/components/list/list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
Optional,
OnChanges,
SimpleChanges,
Injector,
} from '@angular/core';
import { NgForm, NgModel } from '@angular/forms';
import { MatPaginator, PageEvent, SortDirection } from '@angular/material';
Expand Down Expand Up @@ -60,6 +59,7 @@ import {
IOptionalAction,
ListConfig,
ListViewTypes,
MultiFilterManager,
} from './list.component.types';


Expand Down Expand Up @@ -164,11 +164,10 @@ export class ListComponent<T> implements OnInit, OnChanges, OnDestroy, AfterView
singleActions: IListAction<T>[];
columns: ITableColumn<T>[];
dataSource: IListDataSource<T>;
multiFilterConfigs: IListMultiFilterConfig[];
multiFilterConfigsLoading$: Observable<boolean>;
multiFilterManagers: MultiFilterManager<T>[];

paginationController: IListPaginationController<T>;
multiFilterWidgetObservables = new Array<Subscription>();
private multiFilterWidgetObservables = new Array<Subscription>();

view$: Observable<ListView>;

Expand Down Expand Up @@ -240,6 +239,15 @@ export class ListComponent<T> implements OnInit, OnChanges, OnDestroy, AfterView
}
}

private getMultiFilterManagers() {
const configs = this.config.getMultiFiltersConfigs();
if (!configs) {
return null;
}
return configs.map(config => new MultiFilterManager<T>(config, this.dataSource));
}

// TODO: This needs tidying up - NJ
private initialise() {
this.globalActions = this.setupActionsDefaultObservables(
this.config.getGlobalActions()
Expand All @@ -256,7 +264,7 @@ export class ListComponent<T> implements OnInit, OnChanges, OnDestroy, AfterView
const schema = entityFactory(this.dataSource.entityKey);
this.dataSource.getRowState = this.getRowStateGeneratorFromEntityMonitor(schema, this.dataSource);
}
this.multiFilterConfigs = this.config.getMultiFiltersConfigs();
this.multiFilterManagers = this.getMultiFilterManagers();

// Create convenience observables that make the html clearer
this.isAddingOrSelecting$ = observableCombineLatest(
Expand Down Expand Up @@ -296,7 +304,7 @@ export class ListComponent<T> implements OnInit, OnChanges, OnDestroy, AfterView
this.globalActions && this.globalActions.length ||
this.multiActions && this.multiActions.length ||
viewType === 'cards' && this.sortColumns && this.sortColumns.length ||
this.multiFilterConfigs && this.multiFilterConfigs.length ||
this.multiFilterManagers && this.multiFilterManagers.length ||
this.config.enableTextFilter
);
}));
Expand Down Expand Up @@ -362,18 +370,27 @@ export class ListComponent<T> implements OnInit, OnChanges, OnDestroy, AfterView
filterStoreToWidget.pipe(
first(),
tap(() => {
const multiFiltersLoading = [];
Object.values(this.multiFilterConfigs).forEach((filterConfig: IListMultiFilterConfig) => {
filterConfig.select.next(this.multiFilters[filterConfig.key]);
const sub = filterConfig.select.asObservable().pipe(tap((filterItem: string) => {
this.paginationController.multiFilter(filterConfig, filterItem);
Object.values(this.multiFilterManagers).forEach((filterConfig: MultiFilterManager<T>, index: number) => {
// Pipe initial store value to the widget
filterConfig.applyInitialValue(this.multiFilters);
// The first filter will be hidden if there's only one filter option.
// To ensure subsequent filters behave correctly automatically select it
if (index === 0 && this.multiFilterManagers.length > 1) {
filterConfig.filterItems$.pipe(
first()
).subscribe(list => {
if (list && list.length === 1) {
filterConfig.selectItem(list[0].value);
}
});
}

// Pipe changes in the widgets to the store
const sub = filterConfig.multiFilterConfig.select.asObservable().pipe(tap((filterItem: string) => {
this.paginationController.multiFilter(filterConfig.multiFilterConfig, filterItem);
}));
this.multiFilterWidgetObservables.push(sub.subscribe());
multiFiltersLoading.push(filterConfig.loading$);
});
this.multiFilterConfigsLoading$ = observableCombineLatest(multiFiltersLoading).pipe(
map((isLoading: boolean[]) => !!isLoading.find(bool => bool))
);
})
).subscribe();

Expand Down
59 changes: 58 additions & 1 deletion src/frontend/app/shared/components/list/list.component.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { BehaviorSubject, Observable, of as observableOf, combineLatest } from 'rxjs';

import { ListView } from '../../../store/actions/list.actions';
import { defaultClientPaginationPageSize } from '../../../store/reducers/pagination-reducer/pagination.reducer';
import { ListDataSource } from './data-sources-controllers/list-data-source';
import { IListDataSource } from './data-sources-controllers/list-data-source-types';
import { ITableColumn, ITableText } from './list-table/table.types';
import { map, startWith } from 'rxjs/operators';


export enum ListViewTypes {
Expand Down Expand Up @@ -154,3 +155,59 @@ export interface IMultiListAction<T> extends IOptionalAction<T> {
export interface IGlobalListAction<T> extends IOptionalAction<T> {
action: () => void;
}

export class MultiFilterManager<T> {
public filterIsReady$: Observable<boolean>;
public filterItems$: Observable<IListMultiFilterConfigItem[]>;
public hasItems$: Observable<boolean>;
public hasOneItem$: Observable<boolean>;
public initialValue: string;

public filterKey: string;
public allLabel: string;

constructor(
public multiFilterConfig: IListMultiFilterConfig,
dataSource: IListDataSource<T>,
) {
this.filterKey = this.multiFilterConfig.key;
this.allLabel = multiFilterConfig.allLabel || 'All';
this.filterItems$ = this.getItemObservable(multiFilterConfig);
this.hasOneItem$ = this.filterItems$.pipe(map(items => items.length === 1));
this.hasItems$ = this.filterItems$.pipe(map(items => !!items.length));
this.filterIsReady$ = this.getReadyObservable(multiFilterConfig, dataSource, this.hasItems$);
}

private getReadyObservable(
multiFilterConfig: IListMultiFilterConfig,
dataSource: IListDataSource<T>,
hasItems$: Observable<boolean>
) {
return combineLatest(
dataSource.isLoadingPage$,
multiFilterConfig.loading$,
hasItems$,
).pipe(
map(([fetchingListPage, fetchingFilter, hasItems]) => (!fetchingListPage && !fetchingFilter) && hasItems),
startWith(false)
);
}

private getItemObservable(multiFilterConfig: IListMultiFilterConfig) {
return multiFilterConfig.list$.pipe(
map(list => list ? list : [])
);
}

public applyInitialValue(multiFilters: {}) {
const initialValue = multiFilters[this.multiFilterConfig.key];
if (initialValue) {
this.initialValue = initialValue;
this.selectItem(initialValue);
}
}

public selectItem(itemValue: string) {
this.multiFilterConfig.select.next(itemValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,23 +246,6 @@ export class CfOrgSpaceDataService implements OnDestroy {
}

private setupAutoSelectors() {
// Automatically select the cf on first load given the select mode setting
this.cf.list$.pipe(
first(),
tap(cfs => {
// if (this.cf.select.getValue()) {
// return;
// }

if (!!cfs.length &&
((this.selectMode === CfOrgSpaceSelectMode.FIRST_ONLY && cfs.length === 1) ||
(this.selectMode === CfOrgSpaceSelectMode.ANY))
) {
this.selectSet(this.cf.select, cfs[0].guid);
}
})
).subscribe();

const orgResetSub = this.cf.select.asObservable().pipe(
startWith(undefined),
distinctUntilChanged(),
Expand Down
Loading

0 comments on commit d1619fa

Please sign in to comment.