Skip to content

Commit 1f07067

Browse files
authored
refactor: table controls more generic and common API (#576)
* refactor: table controls more generic and common API * fix: address pr comments
1 parent 7db1ade commit 1f07067

31 files changed

+684
-533
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const enum MultiSelectJustify {
2+
// These are used in css
3+
Left = 'flex-start',
4+
Center = 'center',
5+
Right = 'flex-end'
6+
}

projects/components/src/multi-select/multi-select.component.scss

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
.multi-select {
44
display: flex;
55
align-items: center;
6-
border: 1px solid $color-border;
7-
border-radius: 6px;
86
height: 100%;
97
width: 100%;
108

9+
&.border {
10+
border: 1px solid $color-border;
11+
border-radius: 6px;
12+
}
13+
1114
.multi-select-container {
1215
width: 100%;
1316
}
@@ -16,37 +19,46 @@
1619
opacity: 0.4;
1720
}
1821

22+
&.open .trigger-content.icon-mode {
23+
color: $blue-5;
24+
background: $blue-1;
25+
border-color: $blue-3;
26+
}
27+
1928
.trigger-content {
2029
display: flex;
2130
align-items: center;
2231
padding: 0 8px;
2332
cursor: pointer;
2433
height: 34px;
2534

26-
&.justify-left {
27-
justify-content: flex-start;
28-
}
35+
&.icon-mode {
36+
border: 1px solid transparent;
37+
border-radius: 64px;
38+
padding: 0 4px;
2939

30-
&.justify-right {
31-
justify-content: flex-end;
40+
&:hover {
41+
color: $blue-5;
42+
background: $blue-1;
43+
border-color: $blue-3;
44+
}
3245
}
3346

34-
&.justify-center {
35-
justify-content: center;
36-
}
37-
38-
.trigger-prefix-icon {
39-
padding-right: 8px;
40-
}
41-
42-
.trigger-label {
43-
@include body-2-medium();
47+
.trigger-label-container {
48+
display: flex;
49+
width: 100%;
50+
align-items: center;
4451
padding-left: 8px;
45-
}
4652

47-
.trigger-icon {
48-
padding: 0 8px;
49-
margin-left: auto;
53+
.trigger-label {
54+
@include body-2-medium();
55+
padding-left: 8px;
56+
}
57+
58+
.trigger-icon {
59+
padding: 0 8px;
60+
margin-left: auto;
61+
}
5062
}
5163
}
5264
}

projects/components/src/multi-select/multi-select.component.test.ts

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { MockComponent } from 'ng-mocks';
55
import { DividerComponent } from '../divider/divider.component';
66
import { LabelComponent } from '../label/label.component';
77
import { LetAsyncModule } from '../let-async/let-async.module';
8-
import { SelectJustify } from '../select/select-justify';
98
import { SelectOptionComponent } from '../select/select-option.component';
9+
import { MultiSelectJustify } from './multi-select-justify';
1010
import { MultiSelectComponent, TriggerLabelDisplayMode } from './multi-select.component';
1111

1212
describe('Multi Select Component', () => {
@@ -215,52 +215,39 @@ describe('Multi Select Component', () => {
215215
flush();
216216
}));
217217

218-
test('should set correct label alignment', fakeAsync(() => {
218+
test('should set correct justification', fakeAsync(() => {
219219
spectator = hostFactory(
220220
`
221-
<ht-multi-select [selected]="selected" [showBorder]="showBorder">
221+
<ht-multi-select [selected]="selected" [showBorder]="showBorder" [justify]="justify">
222222
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
223223
</ht-select-option>
224224
</ht-multi-select>`,
225225
{
226226
hostProps: {
227227
options: selectionOptions,
228228
selected: [selectionOptions[1].value],
229-
showBorder: true
229+
showBorder: true,
230+
justify: MultiSelectJustify.Left
230231
}
231232
}
232233
);
233234
spectator.tick();
234235

235236
expect(spectator.element).toHaveText(selectionOptions[1].label);
236-
expect(spectator.query('.trigger-content')).toBe(spectator.query('.justify-left'));
237+
expect(spectator.query('.trigger-content')!.getAttribute('style')).toBe('justify-content: flex-start;');
237238

238239
spectator.setInput({
239-
showBorder: false
240+
justify: MultiSelectJustify.Center
240241
});
241242

242243
expect(spectator.element).toHaveText(selectionOptions[1].label);
243-
expect(spectator.query('.trigger-content')).toBe(spectator.query('.justify-right'));
244+
expect(spectator.query('.trigger-content')!.getAttribute('style')).toBe('justify-content: center;');
244245

245246
spectator.setInput({
246-
justify: SelectJustify.Left
247+
justify: MultiSelectJustify.Right
247248
});
248249

249250
expect(spectator.element).toHaveText(selectionOptions[1].label);
250-
expect(spectator.query('.trigger-content')).toBe(spectator.query('.justify-left'));
251-
252-
spectator.setInput({
253-
justify: SelectJustify.Right
254-
});
255-
256-
expect(spectator.element).toHaveText(selectionOptions[1].label);
257-
expect(spectator.query('.trigger-content')).toBe(spectator.query('.justify-right'));
258-
259-
spectator.setInput({
260-
justify: SelectJustify.Center
261-
});
262-
263-
expect(spectator.element).toHaveText(selectionOptions[1].label);
264-
expect(spectator.query('.trigger-content')).toBe(spectator.query('.justify-center'));
251+
expect(spectator.query('.trigger-content')!.getAttribute('style')).toBe('justify-content: flex-end;');
265252
}));
266253
});

projects/components/src/multi-select/multi-select.component.ts

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import { LoggerService, queryListAndChanges$, TypedSimpleChanges } from '@hypert
1414
import { EMPTY, merge, Observable, of } from 'rxjs';
1515
import { map, switchMap } from 'rxjs/operators';
1616
import { IconSize } from '../icon/icon-size';
17-
import { SelectJustify } from '../select/select-justify';
1817
import { SelectOption } from '../select/select-option';
1918
import { SelectOptionComponent } from '../select/select-option.component';
2019
import { SelectSize } from '../select/select-size';
20+
import { MultiSelectJustify } from './multi-select-justify';
2121

2222
@Component({
2323
selector: 'ht-multi-select',
@@ -26,16 +26,32 @@ import { SelectSize } from '../select/select-size';
2626
template: `
2727
<div
2828
class="multi-select"
29-
[ngClass]="[this.size, this.showBorder ? 'border' : '', this.disabled ? 'disabled' : '']"
29+
[ngClass]="[
30+
this.size,
31+
this.showBorder ? 'border' : '',
32+
this.disabled ? 'disabled' : '',
33+
this.popoverOpen ? 'open' : ''
34+
]"
3035
*htLetAsync="this.selected$ as selected"
3136
>
32-
<ht-popover [disabled]="this.disabled" class="multi-select-container">
37+
<ht-popover
38+
[disabled]="this.disabled"
39+
class="multi-select-container"
40+
(popoverOpen)="this.popoverOpen = true"
41+
(popoverClose)="this.popoverOpen = false"
42+
>
3343
<ht-popover-trigger>
34-
<div class="trigger-content" [ngClass]="this.justifyClass" #triggerContainer>
35-
<ht-icon *ngIf="this.icon" class="trigger-prefix-icon" [icon]="this.icon" size="${IconSize.Small}">
36-
</ht-icon>
37-
<ht-label class="trigger-label" [label]="this.triggerLabel"></ht-label>
38-
<ht-icon class="trigger-icon" icon="${IconType.ChevronDown}" size="${IconSize.Small}"></ht-icon>
44+
<div
45+
class="trigger-content"
46+
[style.justify-content]="this.justify"
47+
[ngClass]="this.triggerLabelDisplayMode"
48+
#triggerContainer
49+
>
50+
<ht-icon *ngIf="this.icon" [icon]="this.icon" [size]="this.iconSize"> </ht-icon>
51+
<div *ngIf="!this.isIconOnlyMode()" class="trigger-label-container">
52+
<ht-label class="trigger-label" [label]="this.triggerLabel"></ht-label>
53+
<ht-icon class="trigger-icon" icon="${IconType.ChevronDown}" size="${IconSize.Small}"></ht-icon>
54+
</div>
3955
</div>
4056
</ht-popover-trigger>
4157
<ht-popover-content>
@@ -76,6 +92,9 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
7692
@Input()
7793
public icon?: string;
7894

95+
@Input()
96+
public iconSize: IconSize = IconSize.Small;
97+
7998
@Input()
8099
public placeholder?: string;
81100

@@ -86,7 +105,7 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
86105
public showBorder: boolean = false;
87106

88107
@Input()
89-
public justify?: SelectJustify;
108+
public justify: MultiSelectJustify = MultiSelectJustify.Left;
90109

91110
@Input()
92111
public showAllOptionControl?: boolean = false;
@@ -100,17 +119,10 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
100119
@ContentChildren(SelectOptionComponent)
101120
public items?: QueryList<SelectOptionComponent<V>>;
102121

103-
public selected$?: Observable<SelectOption<V>[] | undefined>;
122+
public popoverOpen: boolean = false;
123+
public selected$?: Observable<SelectOption<V>[]>;
104124
public triggerLabel?: string;
105125

106-
public get justifyClass(): string {
107-
if (this.justify !== undefined) {
108-
return this.justify;
109-
}
110-
111-
return this.showBorder ? SelectJustify.Left : SelectJustify.Right;
112-
}
113-
114126
public constructor(private readonly loggerService: LoggerService) {}
115127

116128
public ngAfterContentInit(): void {
@@ -130,6 +142,10 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
130142
this.setSelection();
131143
}
132144

145+
public isIconOnlyMode(): boolean {
146+
return this.triggerLabelDisplayMode === TriggerLabelDisplayMode.Icon;
147+
}
148+
133149
public areAllOptionsSelected(): boolean {
134150
return this.selected !== undefined && this.items !== undefined && this.selected.length === this.items.length;
135151
}
@@ -169,14 +185,14 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
169185
}
170186
}
171187

172-
private buildObservableOfSelected(): Observable<SelectOption<V>[] | undefined> {
188+
private buildObservableOfSelected(): Observable<SelectOption<V>[]> {
173189
if (!this.items) {
174190
return EMPTY;
175191
}
176192

177193
return queryListAndChanges$(this.items).pipe(
178194
switchMap(items => merge(of(undefined), ...items.map(option => option.optionChange$))),
179-
map(() => this.findItems(this.selected))
195+
map(() => this.findItems(this.selected) ?? [])
180196
);
181197
}
182198

@@ -193,6 +209,8 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
193209
}
194210

195211
export const enum TriggerLabelDisplayMode {
196-
Placeholder = 'placeholder',
197-
Selection = 'selection'
212+
// These may be used as css classes
213+
Placeholder = 'placeholder-mode',
214+
Selection = 'selection-mode',
215+
Icon = 'icon-mode'
198216
}
Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,55 @@
1-
import { KeyValue } from '@angular/common';
1+
import { Dictionary } from '@hypertrace/common';
22
import { SelectOption } from '../../select/select-option';
3+
import { TableFilter } from '../table-api';
34

4-
export interface SelectFilter {
5+
export interface SelectControl {
56
placeholder?: string;
6-
options: SelectOption<KeyValue<string, unknown>>[];
7+
default?: SelectOption<TableControlOption>;
8+
options: SelectOption<TableControlOption>[];
79
}
810

911
export interface SelectChange {
10-
select: SelectFilter;
11-
value: KeyValue<string, unknown>;
12+
select: SelectControl;
13+
value: TableControlOption;
1214
}
15+
16+
export interface CheckboxControl {
17+
label: string;
18+
value: boolean;
19+
options: TableCheckboxOptions;
20+
}
21+
22+
export interface CheckboxChange {
23+
checkbox: CheckboxControl;
24+
option: TableControlOption<unknown, boolean>;
25+
}
26+
27+
export const enum TableControlOptionType {
28+
Filter = 'filter',
29+
Property = 'property',
30+
UnsetFilter = 'unset-filter'
31+
}
32+
33+
export interface TableControlOption<TMetaValue = unknown, TValue = unknown> {
34+
type: TableControlOptionType;
35+
label: string;
36+
metaValue: TMetaValue; // Used in a query - type based on TableWidgetControlOptionType
37+
value?: TValue; // If a control needs to carry a value, use this (example: checkbox boolean)
38+
}
39+
40+
export interface TableUnsetControlOption extends TableControlOption<string, undefined> {
41+
type: TableControlOptionType.UnsetFilter;
42+
metaValue: string;
43+
}
44+
45+
export interface TableFilterControlOption extends TableControlOption<TableFilter> {
46+
type: TableControlOptionType.Filter;
47+
metaValue: TableFilter;
48+
}
49+
50+
export interface TablePropertyControlOption extends TableControlOption<Dictionary<unknown>> {
51+
type: TableControlOptionType.Property;
52+
metaValue: Dictionary<unknown>;
53+
}
54+
55+
export type TableCheckboxOptions = [TableControlOption<unknown, true>, TableControlOption<unknown, false>];

projects/components/src/table/controls/table-controls.component.scss

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import 'mixins';
2+
13
.table-controls {
24
display: flex;
35
padding-bottom: 16px;
@@ -9,16 +11,19 @@
911
}
1012
}
1113

12-
.filter-controls {
14+
.table-controls-left {
1315
display: flex;
1416

1517
.search-box {
1618
width: 256px;
1719
}
1820
}
1921

20-
.filter-checkbox {
22+
.table-controls-right {
2123
display: flex;
22-
align-items: center;
24+
25+
.filter-multi-select {
26+
color: $gray-7;
27+
}
2328
}
2429
}

0 commit comments

Comments
 (0)