filterTable(name: string, option: string) {
this.waitDataTableToLoad();
- cy.get('select#filter_name').select(name);
- cy.get('select#filter_option').select(option);
+ cy.get('[data-testid=filter-button]').click();
+ cy.get('cds-popover-content')
+ .should('be.visible')
+ .within(() => {
+ cy.get(`[data-testid="filter-select-${name}"]`).find('select').select(option);
+ cy.get('[data-testid="apply-filters"]').click();
+ });
}
setPageSize(size: string) {
InputModule,
GridModule,
LayoutModule,
- InlineLoadingModule
+ InlineLoadingModule,
+ PopoverModule,
+ TooltipModule
} from 'carbon-components-angular';
import AddIcon from '@carbon/icons/es/add/16';
import FilterIcon from '@carbon/icons/es/filter/16';
InputModule,
GridModule,
LayoutModule,
- InlineLoadingModule
+ InlineLoadingModule,
+ PopoverModule,
+ TooltipModule
],
declarations: [
TableComponent,
</cds-table-toolbar-search>
<!-- end search -->
<!-- column filters -->
- <ng-container *ngIf="columnFilters.length !== 0">
- <div class="filter-wrapper">
- <svg [cdsIcon]="icons.filter"
- [size]="icons.size16"
- class="cds--toolbar-action__icon"></svg>
- <cds-select (valueChange)="onSelectFilter($event)"
- display="inline"
- id="filter_name">
- <ng-container *ngFor="let filter of columnFilters">
- <option [value]="filter.column.name"
- [selected]="filter.column.name === selectedFilter.column.name">{{ filter.column.name }}</option>
- </ng-container>
- </cds-select>
- <cds-select (valueChange)="onChangeFilter($event)"
- display="inline"
- id="filter_option">
- <option *ngIf="!selectedFilter.value"
- i18n>Any</option>
- <ng-container *ngFor="let option of selectedFilter.options">
- <option [value]="option.raw"
- [selected]="option.raw === selectedFilter?.value?.raw">{{ option.formatted }}</option>
- </ng-container>
- </cds-select>
+ @if (columnFilters.length !== 0) {
+ <div cdsPopover
+ [isOpen]="openFilterPopover"
+ [autoAlign]="true"
+ [caret]="false">
+ <cds-icon-button
+ kind="ghost"
+ class="toolbar-action"
+ (click)="toggleFilterPopover()"
+ data-testid="filter-button">
+ <svg [cdsIcon]="icons.filter"
+ [size]="icons.size16"
+ class="cds--toolbar-action__icon"></svg>
+ </cds-icon-button>
+ <cds-popover-content [cdsLayer]="0">
+ <div
+ cdsGrid
+ [fullWidth]="true"
+ [condensed]="true"
+ [narrow]="true">
+ @for (filter of columnFilters; track filter.column.name; let i = $index) {
+ <div
+ cdsRow
+ [cdsLayer]="2"
+ class="cds-mt-4">
+
+ <!-- filter names column -->
+ <div
+ cdsCol
+ [columnNumbers]="{sm: 4, md: 4}">
+ <cds-text-label>Filter
+ <input cdsText
+ readonly
+ size="sm"
+ [id]="'filter_key_' + i"
+ [name]="'filter_key_' + i"
+ [value]="filter.column.name">
+ </cds-text-label>
+ </div>
+
+ <!-- filter values column -->
+ <div
+ cdsCol
+ [columnNumbers]="{sm: 4, md: 4}">
+ <cds-select (valueChange)="onChangeFilter($event, filter)"
+ [id]="'filter_value_' + i"
+ label="Value"
+ size="sm"
+ [attr.data-testid]="'filter-select-' + filter.column.name"
+ i18n-label>
+ <option i18n>Any</option>
+ @for (option of filter.options; track option.raw) {
+ <option [value]="option.raw"
+ [selected]="option.raw === selectedFilters[filter.column.name]">
+ {{ option.formatted }}
+ </option>
+ }
+
+ </cds-select>
+ </div>
+ </div>
+ }
+ <div cdsRow
+ class="cds-mt-4 cds-mb-4">
+
+ <button cdsButton="primary"
+ size="sm"
+ data-testid="apply-filters"
+ (click)="onSubmitFilter()">
+ <ng-container i18n>Apply filters</ng-container>
+ </button>
+ </div>
+ </div>
+ </cds-popover-content>
</div>
- </ng-container>
+ }
<!-- end column filters -->
<!-- refresh button -->
<cds-icon-button
</cds-table-toolbar-content>
</cds-table-toolbar>
<!-- filter chips for column filters -->
- <div class="d-flex justify-content-end align-items-center filter-tags"
- *ngIf="toolHeader && columnFiltered">
- <div class="d-flex gap-2">
- <ng-container *ngFor="let filter of columnFilters">
- <cds-tag *ngIf="filter.value"
- type="outline"
- class="align-self-center">
- <span class="me-2">{{ filter.column.name }}: {{ filter.value.formatted }}</span>
- <button class="cds--tag__close-icon"
- (click)="onChangeFilter(filter)">
- <svg [cdsIcon]="icons.destroy"
- [size]="icons.size16"></svg>
+ @if (toolHeader && columnFiltered) {
+ <div class="filter-tags">
+ <div
+ class="align-items-center"
+ cdsStack="horizontal"
+ [gap]="2">
+ @for (filter of activeFilters | slice:0:3; track filter.column.name) {
+ @if (filter.value) {
+ <cds-tag-filter
+ closeButtonLabel="Clear"
+ i18n-closeButtonLabel
+ (close)="onRemoveFilter(filter)">
+ {{ filter.column.name }}={{ filter.value.formatted }}
+ </cds-tag-filter>
+ }
+ }
+ @if (activeFilters.length > 3) {
+ <cds-tooltip [description]="remainingFiltersTpl">
+ <cds-tag>
+ +{{ activeFilters.length - 3 }}
+ </cds-tag>
+
+ <ng-template #remainingFiltersTpl>
+ <div
+ cdsStack="vertical"
+ [gap]="2">
+ @for (filter of activeFilters | slice:3; track filter.column.name) {
+ <cds-tag-filter
+ closeButtonLabel="Clear"
+ i18n-closeButtonLabel
+ (close)="onRemoveFilter(filter)">
+ {{ filter.column.name }}={{ filter.value.formatted }}
+ </cds-tag-filter>
+ }
+ </div>
+ </ng-template>
+ </cds-tooltip>
+ }
+ <button cdsButton="ghost"
+ (click)="onClearFilters($event)">
+ <ng-container i18n>Clear filters</ng-container>
</button>
- </cds-tag>
- </ng-container>
- <button cdsButton="ghost"
- (click)="onClearFilters($event)">
- <ng-container i18n>Clear filters</ng-container>
- </button>
- </div>
- </div>
+ </div>
+ </div>
+ }
<!-- end filter chips for column filters -->
<table cdsTable
[sortable]="sortable"
}
.filter-tags {
- background-color: var(--cds-layer-accent);
- border-bottom: 1px solid var(--cds-layer-active);
+ background-color: var(--cds-layer-01);
+ border-top: var(--cds-spacing-01) solid var(--cds-layer-02);
color: var(--cds-text-primary);
}
) => {
component.search = search;
_.forEach(changes, (change) => {
- component.onSelectFilter(change.filter.column.name);
- component.onChangeFilter(change.value || undefined);
+ component.onChangeFilter(change.value || undefined, change.filter);
+ component.onSubmitFilter();
});
expect(component.rows).toEqual(results);
component.onClearSearch();
import { Icons, IconSize, EMPTY_STATE_IMAGE } from '~/app/shared/enum/icons.enum';
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
-import { CdTableColumnFilter } from '~/app/shared/models/cd-table-column-filter';
+import {
+ CdTableColumnFilter,
+ CdTableColumnFilterOption,
+ CdTableColumnSelectedFilter,
+ CdTableColumnStagedFilter
+} from '~/app/shared/models/cd-table-column-filter';
import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
editStates: EditState = {};
formGroup: CdFormGroup = new CdFormGroup({});
+ openFilterPopover = false;
+ stagedFilters: CdTableColumnStagedFilter = {};
+ selectedFilters: CdTableColumnSelectedFilter = {};
+
+ get activeFilters() {
+ return this.columnFilters.filter((filter) => filter.value);
+ }
+
constructor(
// private ngZone: NgZone,
private cdRef: ChangeDetectorRef,
this.tableColumns = this.localColumns;
}
+ toggleFilterPopover() {
+ this.openFilterPopover = !this.openFilterPopover;
+ }
+
initColumnFilters() {
let filterableColumns = _.filter(this.localColumns, { filterable: true });
filterableColumns = [...filterableColumns, ...this.extraFilterableColumns];
};
});
this.selectedFilter = _.first(this.columnFilters);
+ this.initSelectedColumnFilters();
+ }
+
+ private initSelectedColumnFilters() {
+ this.columnFilters.forEach((filter) => {
+ this.selectedFilters[filter.column.name] = filter.value?.raw;
+ });
}
private createColumnFilterOption(
});
}
- onSelectFilter(filter: string) {
- const value = this.columnFilters.find((x) => x.column.name === filter);
- this.selectedFilter = value;
+ // saving the filters to a staged variable so they are not applied immediately
+ onChangeFilter(selectedValue: string, filter: CdTableColumnFilter) {
+ const filterName = filter.column.name;
+ const selectedFilter = this.selectedFilters[filterName];
+ const newSelectedFilter = selectedFilter === selectedValue ? undefined : selectedValue;
+
+ this.selectedFilters[filterName] = newSelectedFilter;
+
+ const option = filter.options.find(
+ (x: CdTableColumnFilterOption) => x.raw === newSelectedFilter
+ );
+ this.stagedFilters[filterName] = option;
+ }
+
+ onSubmitFilter() {
+ this.columnFilters.forEach((filter) => {
+ const filterName = filter.column.name;
+
+ if (this.stagedFilters.hasOwnProperty(filterName)) {
+ filter.value = this.stagedFilters[filterName];
+ this.selectedFilter = filter;
+ }
+ });
+
+ this.stagedFilters = {};
+ this.updateFilter();
+ this.openFilterPopover = false;
}
- onChangeFilter(filter: string) {
- const option = this.selectedFilter.options.find((x) => x.raw === filter);
- this.selectedFilter.value = _.isEqual(this.selectedFilter.value, option) ? undefined : option;
+ onRemoveFilter(filter: CdTableColumnFilter) {
+ const filterName = filter.column.name;
+ filter.value = undefined;
+ this.selectedFilters[filterName] = undefined;
+ delete this.stagedFilters[filterName];
+ if (this.selectedFilter?.column.name === filterName) {
+ this.selectedFilter = undefined;
+ }
this.updateFilter();
}
});
this.selectedFilter = _.first(this.columnFilters);
this.updateFilter();
+ this.initSelectedColumnFilters();
}
updateFilter() {
export interface CdTableColumnFilter {
column: CdTableColumn;
- options: { raw: string; formatted: string }[]; // possible options of a filter
- value?: { raw: string; formatted: string }; // selected option
+ options: CdTableColumnFilterOption[]; // possible options of a filter
+ value?: CdTableColumnFilterOption; // selected option
+}
+
+export interface CdTableColumnStagedFilter {
+ [filterName: string]: CdTableColumnFilterOption;
+}
+
+export interface CdTableColumnSelectedFilter {
+ [filterName: string]: string | undefined;
+}
+
+export interface CdTableColumnFilterOption {
+ raw: string;
+ formatted: string;
}