Skip to content

Commit e47f550

Browse files
committed
SPIKE Adv table in-column filtering
1 parent eb32013 commit e47f550

File tree

12 files changed

+539
-120
lines changed

12 files changed

+539
-120
lines changed

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"./components/hds/advanced-table/th-button-sort.js": "./dist/_app_/components/hds/advanced-table/th-button-sort.js",
155155
"./components/hds/advanced-table/th-button-tooltip.js": "./dist/_app_/components/hds/advanced-table/th-button-tooltip.js",
156156
"./components/hds/advanced-table/th-context-menu.js": "./dist/_app_/components/hds/advanced-table/th-context-menu.js",
157+
"./components/hds/advanced-table/th-filter-menu.js": "./dist/_app_/components/hds/advanced-table/th-filter-menu.js",
157158
"./components/hds/advanced-table/th-reorder-drop-target.js": "./dist/_app_/components/hds/advanced-table/th-reorder-drop-target.js",
158159
"./components/hds/advanced-table/th-reorder-handle.js": "./dist/_app_/components/hds/advanced-table/th-reorder-handle.js",
159160
"./components/hds/advanced-table/th-resize-handle.js": "./dist/_app_/components/hds/advanced-table/th-resize-handle.js",

packages/components/src/components/hds/advanced-table/index.hbs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
SPDX-License-Identifier: MPL-2.0
44
}}
55

6+
{{#if this.hasActiveFilters}}
7+
<Hds::Button
8+
@text="Clear all filters"
9+
@color="tertiary"
10+
@icon="x"
11+
@size="small"
12+
class="hds-advanced-table__clear-filters-button"
13+
{{on "click" this.clearFilters}}
14+
/>
15+
{{/if}}
616
<div
717
class="hds-advanced-table__container
818
{{(if this.isStickyHeaderPinned 'hds-advanced-table__container--header-is-pinned')}}"
@@ -63,11 +73,14 @@
6373
@isStickyColumn={{this._isStickyColumn column}}
6474
@isStickyColumnPinned={{this.isStickyColumnPinned}}
6575
@tableHeight={{this._tableHeight}}
76+
@filters={{this.filters}}
77+
@isLiveFilter={{@isLiveFilter}}
6678
@onColumnResize={{@onColumnResize}}
6779
@onPinFirstColumn={{this._onPinFirstColumn}}
6880
@onReorderDragEnd={{fn (mut this._tableModel.reorderDraggedColumn) null}}
6981
@onReorderDragStart={{fn (mut this._tableModel.reorderDraggedColumn)}}
7082
@onReorderDrop={{this._tableModel.moveColumnToDropTarget}}
83+
@onFilter={{this.onFilter}}
7184
{{this._registerThElement column}}
7285
>
7386
{{column.label}}

packages/components/src/components/hds/advanced-table/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import type {
2929
HdsAdvancedTableModel,
3030
HdsAdvancedTableExpandState,
3131
HdsAdvancedTableColumnReorderCallback,
32+
HdsAdvancedTableFilter,
33+
HdsAdvancedTableFilters,
3234
} from './types.ts';
3335
import type HdsAdvancedTableColumnType from './models/column.ts';
3436
import type { HdsFormCheckboxBaseSignature } from '../form/checkbox/base.ts';
@@ -149,12 +151,15 @@ export interface HdsAdvancedTableSignature {
149151
hasStickyFirstColumn?: boolean;
150152
childrenKey?: string;
151153
maxHeight?: string;
154+
filters?: HdsAdvancedTableFilters;
155+
isLiveFilter?: boolean;
152156
onColumnReorder?: HdsAdvancedTableColumnReorderCallback;
153157
onColumnResize?: (columnKey: string, newWidth?: string) => void;
154158
onSelectionChange?: (
155159
selection: HdsAdvancedTableOnSelectionChangeSignature
156160
) => void;
157161
onSort?: (sortBy: string, sortOrder: HdsAdvancedTableThSortOrder) => void;
162+
onFilter?: (filters: HdsAdvancedTableFilters) => void;
158163
};
159164
Blocks: {
160165
body?: [
@@ -222,6 +227,8 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
222227
@tracked showScrollIndicatorTop = false;
223228
@tracked showScrollIndicatorBottom = false;
224229
@tracked stickyColumnOffset = '0px';
230+
@tracked filters: HdsAdvancedTableFilters = this.args.filters ?? {};
231+
@tracked hasActiveFilters: boolean = Object.keys(this.filters).length > 0;
225232

226233
constructor(owner: Owner, args: HdsAdvancedTableSignature['Args']) {
227234
super(owner, args);
@@ -698,6 +705,30 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
698705
}
699706
}
700707

708+
@action
709+
onFilter(
710+
key: string,
711+
keyFilter?: HdsAdvancedTableFilter[] | HdsAdvancedTableFilter
712+
): void {
713+
this._updateFilter(key, keyFilter);
714+
715+
const { onFilter } = this.args;
716+
if (onFilter && typeof onFilter === 'function') {
717+
onFilter(this.filters);
718+
}
719+
}
720+
721+
@action
722+
clearFilters(): void {
723+
this.filters = {};
724+
this.hasActiveFilters = false;
725+
726+
const { onFilter } = this.args;
727+
if (onFilter && typeof onFilter === 'function') {
728+
onFilter(this.filters);
729+
}
730+
}
731+
701732
private _updateScrollIndicators(element: HTMLElement): void {
702733
// 6px as a buffer so the shadow doesn't appear over the border radius on the edge of the table
703734
const SCROLL_BUFFER = 6;
@@ -764,4 +795,21 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
764795
}
765796
return undefined;
766797
};
798+
799+
private _updateFilter(
800+
key: string,
801+
keyFilter?: HdsAdvancedTableFilter[] | HdsAdvancedTableFilter
802+
): void {
803+
const newFilters = { ...this.filters };
804+
if (
805+
!keyFilter ||
806+
(keyFilter && Array.isArray(keyFilter) && keyFilter.length === 0)
807+
) {
808+
delete newFilters[key];
809+
} else {
810+
newFilters[key] = keyFilter;
811+
}
812+
this.filters = newFilters;
813+
this.hasActiveFilters = Object.keys(this.filters).length > 0;
814+
}
767815
}

packages/components/src/components/hds/advanced-table/models/column.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import type { HdsDropdownToggleButtonSignature } from '../../dropdown/toggle/but
1313
import type {
1414
HdsAdvancedTableCell,
1515
HdsAdvancedTableHorizontalAlignment,
16+
HdsAdvancedTableFilterOption,
1617
HdsAdvancedTableColumn as HdsAdvancedTableColumnType,
18+
HdsAdvancedTableFilterType,
1719
} from '../types';
1820

1921
export const DEFAULT_WIDTH = '1fr'; // default to '1fr' to allow flexible width
@@ -51,6 +53,8 @@ export default class HdsAdvancedTableColumn {
5153
@tracked
5254
thContextMenuToggleElement?: HdsDropdownToggleButtonSignature['Element'] =
5355
undefined;
56+
@tracked filterOptions?: HdsAdvancedTableFilterOption[] = undefined;
57+
@tracked filterType?: HdsAdvancedTableFilterType = undefined;
5458

5559
// width properties
5660
@tracked transientWidth?: `${number}px` = undefined; // used for transient width changes
@@ -165,6 +169,8 @@ export default class HdsAdvancedTableColumn {
165169
this.tooltip = column.tooltip;
166170
this._setWidthValues(column);
167171
this.sortingFunction = column.sortingFunction;
172+
this.filterOptions = column.filterOptions;
173+
this.filterType = column.filterType;
168174
}
169175

170176
// main collection function
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{{!
2+
Copyright (c) HashiCorp, Inc.
3+
SPDX-License-Identifier: MPL-2.0
4+
}}
5+
<Hds::Dropdown
6+
class="hds-advanced-table__th-filter-menu {{if this.hasActiveFilters 'hds-advanced-table__th-filter-menu--active'}}"
7+
@enableCollisionDetection={{true}}
8+
@listPosition="bottom-right"
9+
@height="210px"
10+
{{this._updateInternalFilters}}
11+
...attributes
12+
as |D|
13+
>
14+
<D.ToggleIcon @icon="filter" @text="Filter for {{@column.label}}" @hasChevron={{false}} @size="small" />
15+
{{#each @column.filterOptions as |option|}}
16+
{{#if (eq @column.filterType "radio")}}
17+
<D.Radio
18+
@value={{option.value}}
19+
checked={{(this._isChecked option.value)}}
20+
data-test-filter-option-key={{option.value}}
21+
{{on "change" this.onFilter}}
22+
>
23+
{{option.label}}
24+
</D.Radio>
25+
{{else}}
26+
<D.Checkbox
27+
@value={{option.value}}
28+
checked={{(this._isChecked option.value)}}
29+
data-test-filter-option-key={{option.value}}
30+
{{on "change" this.onFilter}}
31+
>
32+
{{option.label}}
33+
</D.Checkbox>
34+
{{/if}}
35+
{{/each}}
36+
{{#unless @isLiveFilter}}
37+
<D.Footer @hasDivider={{true}}>
38+
<Hds::ButtonSet>
39+
<Hds::Button @text="Apply filters" @isFullWidth={{true}} @size="small" {{on "click" this.onApply}} />
40+
<Hds::Button @text="Clear" @color="secondary" @size="small" {{on "click" this.onClear}} />
41+
</Hds::ButtonSet>
42+
</D.Footer>
43+
{{/unless}}
44+
</Hds::Dropdown>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import Component from '@glimmer/component';
7+
import { action } from '@ember/object';
8+
import { tracked } from '@glimmer/tracking';
9+
import { modifier } from 'ember-modifier';
10+
11+
import type HdsAdvancedTableColumn from './models/column.ts';
12+
import type { HdsDropdownSignature } from '../dropdown/index.ts';
13+
import type {
14+
HdsAdvancedTableFilter,
15+
HdsAdvancedTableFilters,
16+
} from './types.ts';
17+
18+
export interface HdsAdvancedTableThFilterMenuSignature {
19+
Args: {
20+
column: HdsAdvancedTableColumn;
21+
filters?: HdsAdvancedTableFilters;
22+
isLiveFilter?: boolean;
23+
onFilter?: (
24+
key: string,
25+
keyFilter?: HdsAdvancedTableFilter | HdsAdvancedTableFilter[]
26+
) => void;
27+
};
28+
Element: HdsDropdownSignature['Element'];
29+
}
30+
31+
export default class HdsAdvancedTableThFilterMenu extends Component<HdsAdvancedTableThFilterMenuSignature> {
32+
@tracked internalFilters:
33+
| HdsAdvancedTableFilter[]
34+
| HdsAdvancedTableFilter
35+
| undefined = [];
36+
@tracked hasActiveFilters: boolean = this.keyFilter !== undefined;
37+
38+
private _updateInternalFilters = modifier(() => {
39+
this.internalFilters = this.keyFilter;
40+
});
41+
42+
get keyFilter():
43+
| HdsAdvancedTableFilter[]
44+
| HdsAdvancedTableFilter
45+
| undefined {
46+
const { filters, column } = this.args;
47+
48+
if (!filters || !column) {
49+
return undefined;
50+
}
51+
return filters[column.key];
52+
}
53+
54+
@action
55+
onFilter(event: Event): void {
56+
const addFilter = (value: unknown): HdsAdvancedTableFilter[] => {
57+
const newFilter = {
58+
text: value as string,
59+
value: value,
60+
};
61+
if (
62+
Array.isArray(this.internalFilters) &&
63+
input.classList.contains('hds-form-checkbox')
64+
) {
65+
this.internalFilters.push(newFilter);
66+
return this.internalFilters;
67+
} else {
68+
return [newFilter];
69+
}
70+
};
71+
72+
const removeFilter = (value: string): HdsAdvancedTableFilter[] => {
73+
const newFilter = [] as HdsAdvancedTableFilter[];
74+
if (Array.isArray(this.internalFilters)) {
75+
this.internalFilters.forEach((filter) => {
76+
if (filter.value != value) {
77+
newFilter.push(filter);
78+
}
79+
});
80+
}
81+
return newFilter;
82+
};
83+
84+
const input = event.target as HTMLInputElement;
85+
86+
let newFilter = [] as HdsAdvancedTableFilter[];
87+
88+
if (input.checked) {
89+
newFilter = addFilter(input.value);
90+
} else {
91+
newFilter = removeFilter(input.value);
92+
}
93+
94+
this.internalFilters = newFilter;
95+
96+
if (this.args.isLiveFilter) {
97+
const { onFilter, column } = this.args;
98+
if (onFilter && typeof onFilter === 'function') {
99+
if (newFilter.length === 0) {
100+
onFilter(column?.key, undefined);
101+
} else {
102+
onFilter(column?.key, newFilter);
103+
}
104+
this.hasActiveFilters = newFilter != undefined && newFilter.length > 0;
105+
}
106+
}
107+
}
108+
109+
@action
110+
onApply(): void {
111+
const { onFilter, column } = this.args;
112+
if (onFilter && typeof onFilter === 'function') {
113+
onFilter(column?.key, this.internalFilters);
114+
}
115+
}
116+
117+
@action
118+
onClear(): void {
119+
this.internalFilters = [];
120+
121+
const { onFilter, column } = this.args;
122+
if (onFilter && typeof onFilter === 'function') {
123+
onFilter(column?.key, this.internalFilters);
124+
}
125+
}
126+
127+
private _isChecked = (value: string): boolean => {
128+
if (Array.isArray(this.internalFilters)) {
129+
return this.internalFilters.some((filter) => filter.value === value);
130+
} else if (this.internalFilters && value) {
131+
return this.internalFilters.value === value;
132+
}
133+
return false;
134+
};
135+
}

packages/components/src/components/hds/advanced-table/th-sort.hbs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
{{#if @tooltip}}
3535
<Hds::AdvancedTable::ThButtonTooltip @tooltip={{@tooltip}} @labelId={{this._labelId}} />
3636
{{/if}}
37+
{{#if (gt this.numFilters 0)}}
38+
<Hds::BadgeCount @text={{this.numFilters}} @type="outlined" @size="small" />
39+
{{/if}}
3740
</div>
3841

3942
<Hds::Layout::Flex class="hds-advanced-table__th-actions" @align="center" @gap="8">
@@ -44,6 +47,15 @@
4447
/>
4548

4649
{{#if @column}}
50+
{{#if @column.filterOptions}}
51+
<Hds::AdvancedTable::ThFilterMenu
52+
@column={{@column}}
53+
@filters={{@filters}}
54+
@isLiveFilter={{@isLiveFilter}}
55+
@onFilter={{@onFilter}}
56+
/>
57+
{{/if}}
58+
4759
<Hds::AdvancedTable::ThContextMenu
4860
@column={{@column}}
4961
@hasReorderableColumns={{@hasReorderableColumns}}

0 commit comments

Comments
 (0)