Skip to content

Commit 7eb28ff

Browse files
committed
refactor: generalize multi-select component
Generalizes the component for multi-select dropdown so we can reuse it.
1 parent 6dba326 commit 7eb28ff

File tree

9 files changed

+135
-123
lines changed

9 files changed

+135
-123
lines changed

report-app/angular.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"browser": "src/main.ts",
1717
"tsConfig": "tsconfig.app.json",
1818
"externalDependencies": [
19+
"firebase-admin",
1920
"@firebase/app",
2021
"@firebase/firestore",
2122
"tiktoken",

report-app/src/app/pages/report-viewer/failed-checks-filter.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

report-app/src/app/pages/report-viewer/report-viewer.html

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -240,16 +240,11 @@ <h4>Logs</h4>
240240

241241
<h2>Generated applications</h2>
242242
@if (allFailedChecks().length > 0) {
243-
<details class="filter-dropdown" #dropdown>
244-
<summary>Filter by failed checks ({{ selectedChecks().size }} selected)</summary>
245-
<div class="dropdown-content">
246-
<failed-checks-filter
247-
[allFailedChecks]="allFailedChecks()"
248-
[selectedChecks]="selectedChecks()"
249-
(toggleCheck)="toggleCheckFilter($event)"
250-
/>
251-
</div>
252-
</details>
243+
<multi-select
244+
[label]="`Filter by failed checks (${selectedChecks().length} selected)`"
245+
[options]="allFailedChecks()"
246+
[(selected)]="selectedChecks"
247+
class="check-filter"/>
253248
}
254249
<div class="apps-list">
255250
@for (result of filteredResults(); track result) {

report-app/src/app/pages/report-viewer/report-viewer.scss

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -147,51 +147,8 @@ lighthouse-category + lighthouse-category {
147147
margin-bottom: 2em;
148148
}
149149

150-
.filter-dropdown {
151-
position: relative;
152-
display: inline-block;
153-
154-
margin-bottom: 1.5rem;
155-
gap: 0.5rem;
156-
background-color: var(--card-bg-color);
157-
padding: 0;
158-
border-radius: var(--control-border-radius);
159-
border: 1px solid var(--border-color);
160-
transition: background-color var(--transition-speed) ease;
161-
162-
&:hover {
163-
background-color: var(--button-active-bg-color);
164-
}
165-
166-
& summary {
167-
cursor: pointer;
168-
padding: 0.5rem 1.25rem;
169-
border-radius: 4px;
170-
background-color: transparent;
171-
list-style: none;
172-
font-weight: 500;
173-
color: var(--text-secondary);
174-
transition: color var(--transition-speed) ease;
175-
}
176-
177-
&[open] .dropdown-content {
178-
display: block;
179-
color: var(--text-primary);
180-
}
181-
}
182-
183-
.dropdown-content {
184-
display: none;
185-
position: absolute;
186-
background-color: var(--card-bg-color);
187-
min-width: 380px;
188-
box-shadow: var(--shadow);
189-
z-index: 1;
190-
border: 1px solid var(--border-color);
191-
border-radius: var(--border-radius);
192-
padding: 10px;
193-
max-height: 300px;
194-
overflow: auto;
150+
.check-filter {
151+
margin-bottom: 1rem;
195152
}
196153

197154
.no-failed-checks {

report-app/src/app/pages/report-viewer/report-viewer.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import {
3232
StackedBarChartData,
3333
} from '../../shared/visualization/stacked-bar-chart/stacked-bar-chart';
3434
import {formatFile} from './formatter';
35-
import {FailedChecksFilter} from './failed-checks-filter';
3635
import {MessageSpinner} from '../../shared/message-spinner';
3736
import {createPromptDebuggingZip} from '../../shared/debugging-zip';
3837
import {Score} from '../../shared/score/score';
@@ -42,6 +41,7 @@ import {ExpansionPanelHeader} from '../../shared/expansion-panel/expansion-panel
4241
import {ProviderLabel} from '../../shared/provider-label';
4342
import {AiAssistant} from '../../shared/ai-assistant/ai-assistant';
4443
import {LighthouseCategory} from './lighthouse-category';
44+
import {MultiSelect} from '../../shared/multi-select/multi-select';
4545

4646
const localReportRegex = /-l\d+$/;
4747

@@ -51,7 +51,6 @@ const localReportRegex = /-l\d+$/;
5151
CodeViewer,
5252
DatePipe,
5353
DecimalPipe,
54-
FailedChecksFilter,
5554
MessageSpinner,
5655
Score,
5756
ExpansionPanel,
@@ -60,12 +59,10 @@ const localReportRegex = /-l\d+$/;
6059
NgxJsonViewerModule,
6160
AiAssistant,
6261
LighthouseCategory,
62+
MultiSelect,
6363
],
6464
templateUrl: './report-viewer.html',
6565
styleUrls: ['./report-viewer.scss'],
66-
host: {
67-
'(document:click)': 'closeDropdownIfOpen($event)',
68-
},
6966
})
7067
export class ReportViewer {
7168
private clipboard = inject(Clipboard);
@@ -107,7 +104,7 @@ export class ReportViewer {
107104
return this.reportsFetcher.reportGroups().find(group => group.id === id);
108105
});
109106

110-
protected selectedChecks = signal<Set<string>>(new Set());
107+
protected selectedChecks = signal<string[]>([]);
111108

112109
protected allFailedChecks = computed(() => {
113110
if (!this.selectedReport.hasValue()) {
@@ -136,11 +133,11 @@ export class ReportViewer {
136133
}
137134

138135
const failedChecksArray = Array.from(failedChecksMap.entries()).map(([name, count]) => ({
139-
name,
140-
count,
136+
label: `${name} (${count})`,
137+
value: name,
141138
}));
142139

143-
return failedChecksArray.sort((a, b) => a.name.localeCompare(b.name));
140+
return failedChecksArray.sort((a, b) => a.label.localeCompare(b.label));
144141
});
145142

146143
protected filteredResults = computed(() => {
@@ -151,7 +148,7 @@ export class ReportViewer {
151148
return [];
152149
}
153150

154-
if (checks.size === 0) {
151+
if (checks.length === 0) {
155152
return report.results;
156153
}
157154

@@ -164,7 +161,7 @@ export class ReportViewer {
164161
if (this.isSkippedAssessment(assessment)) {
165162
continue;
166163
}
167-
if (assessment.successPercentage < 1 && checks.has(assessment.name)) {
164+
if (assessment.successPercentage < 1 && checks.includes(assessment.name)) {
168165
return true;
169166
}
170167
}
@@ -353,27 +350,6 @@ export class ReportViewer {
353350
return value.state === IndividualAssessmentState.SKIPPED;
354351
}
355352

356-
protected dropdownRef = viewChild<ElementRef>('dropdown');
357-
358-
protected closeDropdownIfOpen(event: MouseEvent): void {
359-
const detailsElement = this.dropdownRef()?.nativeElement;
360-
if (detailsElement?.hasAttribute('open') && !detailsElement.contains(event.target)) {
361-
detailsElement.removeAttribute('open');
362-
}
363-
}
364-
365-
protected toggleCheckFilter(check: string): void {
366-
this.selectedChecks.update(currentChecks => {
367-
const checks = new Set(currentChecks);
368-
if (checks.has(check)) {
369-
checks.delete(check);
370-
} else {
371-
checks.add(check);
372-
}
373-
return checks;
374-
});
375-
}
376-
377353
protected async format(file: LlmResponseFile): Promise<void> {
378354
const result = await formatFile(file, this.selectedReport.value()!.details.summary.framework);
379355
if (typeof result === 'string') {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<button
2+
[class.open]="isOpen()"
3+
(click)="isOpen.set(!isOpen())"
4+
(keydown.escape)="isOpen.set(false)">
5+
{{label()}}
6+
7+
<div class="dropdown">
8+
@for (option of options(); track option.value) {
9+
<label>
10+
<input
11+
type="checkbox"
12+
[checked]="selected().includes(option.value)"
13+
(change)="optionClicked(option)"
14+
/>
15+
{{ option.label }}
16+
</label>
17+
}
18+
</div>
19+
</button>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
:host {
2+
display: inline-flex;
3+
}
4+
5+
button {
6+
border-radius: var(--control-border-radius);
7+
border: 1px solid var(--border-color);
8+
background-color: var(--card-bg-color);
9+
color: var(--text-secondary);
10+
font-size: 0.9rem;
11+
font-weight: 500;
12+
padding: 0 36px 0 16px;
13+
height: var(--control-height);
14+
display: inline-block;
15+
transition: background-color var(--transition-speed) ease;
16+
position: relative;
17+
cursor: pointer;
18+
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292.4 292.4"><path fill="%23007bff" d="M287 69.4a17.6 17.6 0 0 0-13-5.4H18.4c-5 0-9.3 1.8-12.9 5.4A17.6 17.6 0 0 0 0 82.2c0 5 1.8 9.3 5.4 12.9l128 127.9c3.6 3.6 7.8 5.4 12.8 5.4s9.2-1.8 12.8-5.4L287 95.1c3.6-3.6 5.4-7.8 5.4-12.8 0-5-1.8-9.2-5.4-12.9z"/></svg>');
19+
background-repeat: no-repeat;
20+
background-position: right 1rem center;
21+
background-size: 0.65rem auto;
22+
23+
&:not(.open):focus, &:not(.open):hover {
24+
border-color: var(--accent-blue);
25+
}
26+
}
27+
28+
.dropdown {
29+
display: none;
30+
position: absolute;
31+
top: 100%;
32+
left: 0;
33+
background-color: var(--card-bg-color);
34+
min-width: 380px;
35+
box-shadow: var(--shadow);
36+
z-index: 1;
37+
border: 1px solid var(--border-color);
38+
border-radius: var(--border-radius);
39+
padding: 10px;
40+
max-height: 300px;
41+
overflow: auto;
42+
text-align: left;
43+
44+
.open & {
45+
display: block;
46+
color: var(--text-primary);
47+
}
48+
}
49+
50+
label {
51+
display: flex;
52+
padding: 8px 12px;
53+
cursor: pointer;
54+
align-items: center;
55+
gap: 0.5rem;
56+
}
57+
58+
label:hover {
59+
background-color: #eff6ff;
60+
border-radius: var(--border-radius);
61+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {Component, ElementRef, inject, input, model, signal} from '@angular/core';
2+
3+
interface MultiSelectOption {
4+
label: string;
5+
value: unknown;
6+
}
7+
8+
@Component({
9+
selector: 'multi-select',
10+
templateUrl: 'multi-select.html',
11+
styleUrl: 'multi-select.scss',
12+
host: {
13+
'(document:click)': 'outsideClick($event)',
14+
},
15+
})
16+
export class MultiSelect {
17+
private elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
18+
19+
options = input.required<MultiSelectOption[]>();
20+
label = input.required<string>();
21+
selected = model<unknown[]>([]);
22+
23+
protected isOpen = signal(false);
24+
25+
protected optionClicked(option: MultiSelectOption) {
26+
this.selected.update(selected => {
27+
if (selected.includes(option.value)) {
28+
return selected.filter(current => current !== option.value);
29+
}
30+
return [...selected, option.value];
31+
});
32+
}
33+
34+
protected outsideClick(event: MouseEvent) {
35+
if (this.isOpen() && !this.elementRef.nativeElement.contains(event.target as HTMLElement)) {
36+
this.isOpen.set(false);
37+
}
38+
}
39+
}

report-app/src/app/shared/scoring.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ export function getHardcodedColor(
5555
CACHED_COLORS[colorMode][varName] = value;
5656
}
5757

58-
console.log(CACHED_COLORS);
59-
6058
return CACHED_COLORS[colorMode][varName];
6159
}
6260

0 commit comments

Comments
 (0)