Skip to content

Commit b493c68

Browse files
committed
Merge branch 'main' of github.com:hypertrace/hypertrace-ui into top-level-api-nav
2 parents c22eb70 + 16e2732 commit b493c68

File tree

7 files changed

+194
-32
lines changed

7 files changed

+194
-32
lines changed

projects/common/src/color/color.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const enum Color {
77
BlueGray4 = '#4b5f77',
88
Gray1 = '#f4f5f5',
99
Gray2 = '#e1e4e5',
10+
Gray4 = '#889499',
1011
Gray5 = '#657277',
1112
Gray7 = '#272c2e',
1213
Gray9 = '#080909',

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
.multi-select-content {
5555
@include dropdown();
5656
min-width: 120px;
57-
max-height: 204px;
5857

5958
.multi-select-option {
6059
display: flex;
@@ -65,15 +64,15 @@
6564
align-items: center;
6665

6766
.checkbox {
68-
margin-right: 8px;
67+
margin: 0px;
6968
}
7069

7170
.icon {
72-
margin-right: 8px;
71+
margin-left: 8px;
7372
}
7473

7574
.label {
76-
margin-right: 8px;
75+
margin-left: 8px;
7776
}
7877

7978
&:hover {

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

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { fakeAsync, flush } from '@angular/core/testing';
22
import { IconType } from '@hypertrace/assets-library';
33
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
44
import { MockComponent } from 'ng-mocks';
5+
import { DividerComponent } from '../divider/divider.component';
56
import { LabelComponent } from '../label/label.component';
67
import { LetAsyncModule } from '../let-async/let-async.module';
78
import { SelectJustify } from '../select/select-justify';
@@ -13,7 +14,7 @@ describe('Multi Select Component', () => {
1314
component: MultiSelectComponent,
1415
imports: [LetAsyncModule],
1516
entryComponents: [SelectOptionComponent],
16-
declarations: [MockComponent(LabelComponent)],
17+
declarations: [MockComponent(LabelComponent), MockComponent(DividerComponent)],
1718
shallow: true
1819
});
1920

@@ -75,7 +76,7 @@ describe('Multi Select Component', () => {
7576
spectator.tick();
7677

7778
spectator.click('.trigger-content');
78-
const optionElements = spectator.queryAll('.multi-select-option', { root: true });
79+
const optionElements = spectator.queryAll('.multi-select-option:not(.all-options)', { root: true });
7980
expect(spectator.query('.multi-select-content', { root: true })).toExist();
8081
expect(optionElements.length).toBe(3);
8182

@@ -109,15 +110,79 @@ describe('Multi Select Component', () => {
109110
spectator.tick();
110111
spectator.click('.trigger-content');
111112

112-
const optionElements = spectator.queryAll('.multi-select-option', { root: true });
113+
const optionElements = spectator.queryAll('.multi-select-option:not(.all-options)', { root: true });
113114
spectator.click(optionElements[2]);
115+
spectator.tick();
114116

115117
expect(onChange).toHaveBeenCalledTimes(1);
116118
expect(onChange).toHaveBeenCalledWith([selectionOptions[1].value, selectionOptions[2].value]);
117119
expect(spectator.query(LabelComponent)?.label).toEqual('second and 1 more');
118120
flush();
119121
}));
120122

123+
test('should show all checkbox only when enabled by input property', fakeAsync(() => {
124+
spectator = hostFactory(
125+
`
126+
<ht-multi-select>
127+
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
128+
</ht-select-option>
129+
</ht-multi-select>`,
130+
{
131+
hostProps: {
132+
options: selectionOptions
133+
}
134+
}
135+
);
136+
137+
spectator.tick();
138+
spectator.click('.trigger-content');
139+
140+
const allOptionElement = spectator.query('.all-options', { root: true });
141+
expect(allOptionElement).not.toExist();
142+
143+
flush();
144+
}));
145+
146+
test('should notify and update selection when all checkbox is selected', fakeAsync(() => {
147+
const onChange = jest.fn();
148+
149+
spectator = hostFactory(
150+
`
151+
<ht-multi-select [selected]="selected" (selectedChange)="onChange($event)" [placeholder]="placeholder" [showAllOptionControl]="showAllOptionControl">
152+
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
153+
</ht-select-option>
154+
</ht-multi-select>`,
155+
{
156+
hostProps: {
157+
options: selectionOptions,
158+
selected: [selectionOptions[1].value],
159+
placeholder: 'Select options',
160+
showAllOptionControl: true,
161+
onChange: onChange
162+
}
163+
}
164+
);
165+
166+
spectator.tick();
167+
spectator.click('.trigger-content');
168+
169+
const allOptionElement = spectator.query('.all-options', { root: true });
170+
expect(allOptionElement).toExist();
171+
spectator.click(allOptionElement!);
172+
173+
expect(onChange).toHaveBeenCalledTimes(1);
174+
expect(onChange).toHaveBeenCalledWith(selectionOptions.map(option => option.value));
175+
expect(spectator.query(LabelComponent)?.label).toEqual('first and 2 more');
176+
177+
// De select all
178+
spectator.click(allOptionElement!);
179+
expect(onChange).toHaveBeenCalledTimes(2);
180+
expect(onChange).toHaveBeenLastCalledWith([]);
181+
expect(spectator.query(LabelComponent)?.label).toEqual('Select options');
182+
183+
flush();
184+
}));
185+
121186
test('should notify but not change trigger label if triggerLabelDisplayMode is placeholder', fakeAsync(() => {
122187
const onChange = jest.fn();
123188

@@ -141,7 +206,7 @@ describe('Multi Select Component', () => {
141206
expect(spectator.query(LabelComponent)?.label).toEqual('Placeholder');
142207
spectator.click('.trigger-content');
143208

144-
const optionElements = spectator.queryAll('.multi-select-option', { root: true });
209+
const optionElements = spectator.queryAll('.multi-select-option:not(.all-options)', { root: true });
145210
spectator.click(optionElements[2]);
146211

147212
expect(onChange).toHaveBeenCalledTimes(1);

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

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ import { SelectSize } from '../select/select-size';
4040
</ht-popover-trigger>
4141
<ht-popover-content>
4242
<div class="multi-select-content">
43+
<ng-container *ngIf="this.showAllOptionControl">
44+
<div class="multi-select-option all-options" (click)="this.onAllSelectionChange()">
45+
<input class="checkbox" type="checkbox" [checked]="this.areAllOptionsSelected()" />
46+
<span class="label">All</span>
47+
</div>
48+
49+
<ht-divider></ht-divider>
50+
</ng-container>
51+
4352
<div *ngFor="let item of items" (click)="this.onSelectionChange(item)" class="multi-select-option">
4453
<input class="checkbox" type="checkbox" [checked]="this.isSelectedItem(item)" />
4554
<ht-icon
@@ -79,6 +88,9 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
7988
@Input()
8089
public justify?: SelectJustify;
8190

91+
@Input()
92+
public showAllOptionControl?: boolean = false;
93+
8294
@Input()
8395
public triggerLabelDisplayMode: TriggerLabelDisplayMode = TriggerLabelDisplayMode.Selection;
8496

@@ -113,6 +125,33 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
113125
this.setTriggerLabel();
114126
}
115127

128+
public onAllSelectionChange(): void {
129+
this.selected = this.areAllOptionsSelected() ? [] : this.items!.map(item => item.value); // Select All or none
130+
this.setSelection();
131+
}
132+
133+
public areAllOptionsSelected(): boolean {
134+
return this.selected !== undefined && this.items !== undefined && this.selected.length === this.items.length;
135+
}
136+
137+
public onSelectionChange(item: SelectOptionComponent<V>): void {
138+
this.selected = this.isSelectedItem(item)
139+
? this.selected?.filter(value => value !== item.value)
140+
: (this.selected ?? []).concat(item.value);
141+
142+
this.setSelection();
143+
}
144+
145+
public isSelectedItem(item: SelectOptionComponent<V>): boolean {
146+
return this.selected !== undefined && this.selected.filter(value => value === item.value).length > 0;
147+
}
148+
149+
private setSelection(): void {
150+
this.setTriggerLabel();
151+
this.selected$ = this.buildObservableOfSelected();
152+
this.selectedChange.emit(this.selected);
153+
}
154+
116155
private setTriggerLabel(): void {
117156
if (this.triggerLabelDisplayMode === TriggerLabelDisplayMode.Placeholder) {
118157
this.triggerLabel = this.placeholder;
@@ -130,10 +169,6 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
130169
}
131170
}
132171

133-
public isSelectedItem(item: SelectOptionComponent<V>): boolean {
134-
return this.selected !== undefined && this.selected.filter(value => value === item.value).length > 0;
135-
}
136-
137172
private buildObservableOfSelected(): Observable<SelectOption<V>[] | undefined> {
138173
if (!this.items) {
139174
return EMPTY;
@@ -145,16 +180,6 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
145180
);
146181
}
147182

148-
public onSelectionChange(item: SelectOptionComponent<V>): void {
149-
this.selected = this.isSelectedItem(item)
150-
? this.selected?.filter(value => value !== item.value)
151-
: (this.selected ?? []).concat(item.value);
152-
153-
this.setTriggerLabel();
154-
this.selected$ = this.buildObservableOfSelected();
155-
this.selectedChange.emit(this.selected);
156-
}
157-
158183
// Find the select option object for a value
159184
private findItems(value: V[] | undefined): SelectOption<V>[] | undefined {
160185
if (this.items === undefined) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { CommonModule } from '@angular/common';
22
import { NgModule } from '@angular/core';
33
import { FormsModule } from '@angular/forms';
4+
import { DividerModule } from '../divider/divider.module';
45
import { IconModule } from '../icon/icon.module';
56
import { LabelModule } from '../label/label.module';
67
import { LetAsyncModule } from '../let-async/let-async.module';
78
import { PopoverModule } from '../popover/popover.module';
89
import { MultiSelectComponent } from './multi-select.component';
910

1011
@NgModule({
11-
imports: [FormsModule, CommonModule, IconModule, LabelModule, LetAsyncModule, PopoverModule],
12+
imports: [FormsModule, CommonModule, IconModule, LabelModule, LetAsyncModule, PopoverModule, DividerModule],
1213
declarations: [MultiSelectComponent],
1314
exports: [MultiSelectComponent]
1415
})
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { fakeAsync } from '@angular/core/testing';
2+
import { By } from '@angular/platform-browser';
3+
import { runFakeRxjs } from '@hypertrace/test-utils';
4+
import { createHostFactory, Spectator } from '@ngneat/spectator/jest';
5+
import { SearchBoxComponent } from './search-box.component';
6+
7+
describe('Search box Component', () => {
8+
let spectator: Spectator<SearchBoxComponent>;
9+
10+
const createHost = createHostFactory({
11+
component: SearchBoxComponent,
12+
shallow: true
13+
});
14+
15+
test('should work with default values', fakeAsync(() => {
16+
spectator = createHost(
17+
`<ht-search-box [placeholder]="placeholder" (valueChange)="onValueChange($event)"></ht-search-box>`,
18+
{
19+
hostProps: {
20+
placeholder: 'Test Placeholder'
21+
}
22+
}
23+
);
24+
25+
const inputDebugElement = spectator.debugElement.query(By.css('input'));
26+
expect((inputDebugElement.nativeElement as HTMLInputElement)?.placeholder).toEqual('Test Placeholder');
27+
spectator.component.value = 'Test';
28+
29+
runFakeRxjs(({ expectObservable }) => {
30+
expectObservable(spectator.component.valueChange).toBe('x', {
31+
x: 'Test'
32+
});
33+
34+
spectator.triggerEventHandler(inputDebugElement, 'input', spectator.component.value);
35+
spectator.tick();
36+
});
37+
}));
38+
39+
test('should work with arbitrary debounce time', fakeAsync(() => {
40+
spectator = createHost(
41+
`<ht-search-box [placeholder]="placeholder" [debounceTime]="debounceTime" (valueChange)="onValueChange($event)"></ht-search-box>`,
42+
{
43+
hostProps: {
44+
placeholder: 'Test Placeholder',
45+
debounceTime: 200
46+
}
47+
}
48+
);
49+
50+
const inputDebugElement = spectator.debugElement.query(By.css('input'));
51+
expect((inputDebugElement.nativeElement as HTMLInputElement)?.placeholder).toEqual('Test Placeholder');
52+
spectator.component.value = 'Test2';
53+
54+
runFakeRxjs(({ expectObservable }) => {
55+
expectObservable(spectator.component.valueChange).toBe('200ms x', {
56+
x: 'Test2'
57+
});
58+
59+
spectator.triggerEventHandler(inputDebugElement, 'input', spectator.component.value);
60+
spectator.tick();
61+
});
62+
}));
63+
});

projects/components/src/search-box/search-box.component.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
22
import { IconType } from '@hypertrace/assets-library';
33
import { SubscriptionLifecycle, TypedSimpleChanges } from '@hypertrace/common';
44
import { Subject } from 'rxjs';
@@ -33,15 +33,15 @@ import { IconSize } from '../icon/icon-size';
3333
</div>
3434
`
3535
})
36-
export class SearchBoxComponent implements OnChanges {
36+
export class SearchBoxComponent implements OnInit, OnChanges {
3737
@Input()
3838
public placeholder: string = 'Search';
3939

4040
@Input()
4141
public value: string = '';
4242

4343
@Input()
44-
public debounceTime: number = 0;
44+
public debounceTime?: number;
4545

4646
@Output()
4747
public readonly valueChange: EventEmitter<string> = new EventEmitter();
@@ -55,14 +55,13 @@ export class SearchBoxComponent implements OnChanges {
5555
public isFocused: boolean = false;
5656
private readonly debouncedValueSubject: Subject<string> = new Subject();
5757

58+
public ngOnInit(): void {
59+
this.setDebouncedSubscription();
60+
}
61+
5862
public ngOnChanges(changes: TypedSimpleChanges<this>): void {
5963
if (changes.debounceTime) {
60-
this.subscriptionLifecycle.unsubscribe();
61-
this.subscriptionLifecycle.add(
62-
this.debouncedValueSubject
63-
.pipe(debounceTime(this.debounceTime))
64-
.subscribe(value => this.valueChange.emit(value))
65-
);
64+
this.setDebouncedSubscription();
6665
}
6766
}
6867

@@ -82,4 +81,13 @@ export class SearchBoxComponent implements OnChanges {
8281
this.value = '';
8382
this.onValueChange();
8483
}
84+
85+
private setDebouncedSubscription(): void {
86+
this.subscriptionLifecycle.unsubscribe();
87+
this.subscriptionLifecycle.add(
88+
this.debouncedValueSubject
89+
.pipe(debounceTime(this.debounceTime ?? 0))
90+
.subscribe(value => this.valueChange.emit(value))
91+
);
92+
}
8593
}

0 commit comments

Comments
 (0)