diff --git a/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.html b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.html new file mode 100644 index 000000000..82114b0ec --- /dev/null +++ b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.html @@ -0,0 +1,39 @@ + +
+ {{ headerText }} + {{ headerText }} + + +
+ + + + + diff --git a/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.scss b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.scss new file mode 100644 index 000000000..a7e6f51b2 --- /dev/null +++ b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.scss @@ -0,0 +1,47 @@ +@use 'node_modules/@angular/material/index' as mat; +@use 'src/theming/m3-theme' as *; +@use 'colors'; + +:host { + display: contents; +} + +th { + height: var(--mat-table-header-container-height); + vertical-align: middle; +} + +.header-filter-container { + display: flex; + align-items: center; +} + +.filter-button { + display: flex; + width: 32px; + height: 32px; + justify-content: center; + align-items: center; + flex-shrink: 0; + margin: 0 2px 0 38px; + padding: 0; + border: none; + background: colors.$white; + cursor: pointer; + border-radius: 50%; + &:hover { + background-color: rgba(0, 0, 0, 0.04); + } + &:active, + &.active { + background-color: colors.$secondary-container; + color: colors.$on-secondary-container; + &:hover { + filter: brightness(90%); + } + } +} + +.filter-button.active .mat-icon { + color: mat.get-theme-color($light-theme, primary, 35); +} diff --git a/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.spec.ts b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.spec.ts new file mode 100644 index 000000000..088037d95 --- /dev/null +++ b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.spec.ts @@ -0,0 +1,91 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FilterHeaderComponent } from './filter-header.component'; +import { MatIconModule } from '@angular/material/icon'; +import { MatSortModule } from '@angular/material/sort'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTableModule } from '@angular/material/table'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +@Component({ + selector: 'app-dummy-table', + template: ` + + + + + + + + +
{{ element }}
+ `, + standalone: false, +}) +export class DummyTableComponent { + data = ['Row 1', 'Row 2', 'Row 3']; + displayedColumns = ['testColumn']; +} + +describe('FilterHeaderComponent within mat-table', () => { + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DummyTableComponent], + imports: [ + BrowserAnimationsModule, + FilterHeaderComponent, + MatIconModule, + MatSortModule, + MatButtonModule, + MatTableModule, + CommonModule, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DummyTableComponent); + fixture.detectChanges(); + compiled = fixture.nativeElement as HTMLElement; + }); + + it('should have the filter header component', () => { + const filterHeader = compiled.querySelector( + 'app-filter-header' + ) as HTMLElement; + expect(filterHeader).toBeTruthy(); + const headerText = filterHeader?.querySelector( + '.header-filter-container span' + ) as HTMLSpanElement; + expect(headerText?.textContent?.trim()).toBe('Test Header'); + }); + + it('should emit an event when filter button is clicked in filter header', () => { + const filterHeader = fixture.debugElement.query( + By.css('app-filter-header') + ); + const filterHeaderComponent = + filterHeader.componentInstance as FilterHeaderComponent; + + spyOn(filterHeaderComponent.emitOpenFilter, 'emit'); + + const button = filterHeader.query(By.css('.filter-button')) + .nativeElement as HTMLButtonElement; + button.click(); + + expect(filterHeaderComponent.emitOpenFilter.emit).toHaveBeenCalledWith({ + event: new PointerEvent('event'), + filter: 'testFilter', + filterOpened: false, + }); + }); +}); diff --git a/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.ts b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.ts new file mode 100644 index 000000000..32ea42a1d --- /dev/null +++ b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.ts @@ -0,0 +1,44 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule, NgIf } from '@angular/common'; +import { MatTableModule } from '@angular/material/table'; +import { MatSortModule } from '@angular/material/sort'; + +export interface OpenFilterEvent { + event: Event; + filter: string; + filterOpened: boolean; +} + +@Component({ + selector: 'app-filter-header', + standalone: true, + imports: [ + MatIconModule, + MatButtonModule, + CommonModule, + MatTableModule, + MatSortModule, + NgIf, + ], + templateUrl: './filter-header.component.html', + styleUrl: './filter-header.component.scss', +}) +export class FilterHeaderComponent { + @Output() emitOpenFilter = new EventEmitter(); + @Input({ required: true }) filterName!: string; + @Input({ required: true }) filterOpened!: boolean; + @Input() hasSorting: boolean = true; + @Input({ required: true }) activeFilter!: string; + @Input() sortActionDescription: string = ''; + @Input({ required: true }) headerText!: string; + + openFilter(event: Event, filter: string, filterOpened: boolean): void { + this.emitOpenFilter.emit({ + event, + filter, + filterOpened, + }); + } +} diff --git a/modules/ui/src/app/pages/reports/reports.component.html b/modules/ui/src/app/pages/reports/reports.component.html index e51907b28..4ca8f68d0 100644 --- a/modules/ui/src/app/pages/reports/reports.component.html +++ b/modules/ui/src/app/pages/reports/reports.component.html @@ -32,23 +32,15 @@

Reports

tabindex="0"> - - Started - - - + sortActionDescription="Sort by started date" + headerText="Started" + [filterName]="FilterName.Started" + [filterOpened]="vm.filterOpened" + [activeFilter]="vm.activeFilter" + (emitOpenFilter)="openFilter($event)"> + {{ getFormattedDateString(data.started) }} @@ -70,45 +62,29 @@

Reports

- - Device - - - + sortActionDescription="Sort by device" + headerText="Device" + [filterName]="FilterName.DeviceInfo" + [filterOpened]="vm.filterOpened" + [activeFilter]="vm.activeFilter" + (emitOpenFilter)="openFilter($event)"> + {{ data.deviceInfo }} - - Firmware - - - + sortActionDescription="Sort by firmware" + headerText="Firmware" + [filterName]="FilterName.DeviceFirmware" + [filterOpened]="vm.filterOpened" + [activeFilter]="vm.activeFilter" + (emitOpenFilter)="openFilter($event)"> + {{ data.deviceFirmware }} @@ -128,23 +104,15 @@

Reports

- - Result - - - + sortActionDescription="Sort by result" + headerText="Result" + [filterName]="FilterName.Results" + [filterOpened]="vm.filterOpened" + [activeFilter]="vm.activeFilter" + (emitOpenFilter)="openFilter($event)"> + Reports - - - -
diff --git a/modules/ui/src/app/pages/reports/reports.component.scss b/modules/ui/src/app/pages/reports/reports.component.scss index b502c7768..ce158fcef 100644 --- a/modules/ui/src/app/pages/reports/reports.component.scss +++ b/modules/ui/src/app/pages/reports/reports.component.scss @@ -76,7 +76,7 @@ justify-content: center; align-items: center; flex-shrink: 0; - margin: 0 2px 0 8px; + margin: 0 2px 0 12px; padding: 0; border: none; background: colors.$white; diff --git a/modules/ui/src/app/pages/reports/reports.component.spec.ts b/modules/ui/src/app/pages/reports/reports.component.spec.ts index 4bcf2ebf3..b829e1cb3 100644 --- a/modules/ui/src/app/pages/reports/reports.component.spec.ts +++ b/modules/ui/src/app/pages/reports/reports.component.spec.ts @@ -181,7 +181,11 @@ describe('ReportsComponent', () => { } as MatDialogRef); fixture.detectChanges(); - component.openFilter(event, '', false); + component.openFilter({ + event, + filter: '', + filterOpened: false, + }); expect(openSpy).toHaveBeenCalled(); expect(openSpy).toHaveBeenCalledWith(FilterDialogComponent, { @@ -225,10 +229,26 @@ describe('ReportsComponent', () => { } as MatDialogRef); fixture.detectChanges(); - component.openFilter(event, FilterName.Started, false); - component.openFilter(event, FilterName.Results, false); - component.openFilter(event, FilterName.DeviceFirmware, false); - component.openFilter(event, FilterName.DeviceInfo, false); + component.openFilter({ + event, + filter: FilterName.Started, + filterOpened: false, + }); + component.openFilter({ + event, + filter: FilterName.Results, + filterOpened: false, + }); + component.openFilter({ + event, + filter: FilterName.DeviceFirmware, + filterOpened: false, + }); + component.openFilter({ + event, + filter: FilterName.DeviceInfo, + filterOpened: false, + }); expect(mockReportsStore.setFilteredValuesResults).toHaveBeenCalledWith( mockFilterResults ); diff --git a/modules/ui/src/app/pages/reports/reports.component.ts b/modules/ui/src/app/pages/reports/reports.component.ts index eb5d90441..a40c2f8f8 100644 --- a/modules/ui/src/app/pages/reports/reports.component.ts +++ b/modules/ui/src/app/pages/reports/reports.component.ts @@ -35,6 +35,7 @@ import { MatDialog } from '@angular/material/dialog'; import { tap } from 'rxjs/internal/operators/tap'; import { FilterName, Filters } from '../../model/filters'; import { ReportsStore } from './reports.store'; +import { OpenFilterEvent } from './components/filter-header/filter-header.component'; @Component({ selector: 'app-history', @@ -77,7 +78,7 @@ export class ReportsComponent implements OnInit, OnDestroy { return this.testRunService.getResultClass(status); } - openFilter(event: Event, filter: string, filterOpened: boolean) { + openFilter({ event, filter, filterOpened }: OpenFilterEvent) { event.stopPropagation(); const target = new ElementRef(event.currentTarget); diff --git a/modules/ui/src/app/pages/reports/reports.module.ts b/modules/ui/src/app/pages/reports/reports.module.ts index 18db8a27f..e8ccc0844 100644 --- a/modules/ui/src/app/pages/reports/reports.module.ts +++ b/modules/ui/src/app/pages/reports/reports.module.ts @@ -27,6 +27,7 @@ import { FilterChipsComponent } from './components/filter-chips/filter-chips.com import { DeleteReportComponent } from './components/delete-report/delete-report.component'; import { DownloadReportZipComponent } from '../../components/download-report-zip/download-report-zip.component'; import { DownloadReportPdfComponent } from '../../components/download-report-pdf/download-report-pdf.component'; +import { FilterHeaderComponent } from './components/filter-header/filter-header.component'; @NgModule({ declarations: [ReportsComponent], @@ -43,6 +44,7 @@ import { DownloadReportPdfComponent } from '../../components/download-report-pdf DeleteReportComponent, DownloadReportZipComponent, DownloadReportPdfComponent, + FilterHeaderComponent, ], providers: [DatePipe], }) diff --git a/modules/ui/src/styles.scss b/modules/ui/src/styles.scss index f63c34fcb..0893b13e1 100644 --- a/modules/ui/src/styles.scss +++ b/modules/ui/src/styles.scss @@ -423,13 +423,19 @@ button:not(.mat-mdc-button-disabled) { .mat-sort-header-indicator { &::before { content: 'arrow_upward'; - top: -0em; - line-height: 12px; + line-height: 32px; font-family: #{variables.$font-symbols}; position: absolute; color: var(--mat-table-header-headline-color); font-size: 19px; font-weight: 400; + background-color: rgba(68, 71, 70, 0.08); + border-radius: 50%; + height: 32px; + width: 32px; + top: -10px; + text-align: center; + margin-left: 2px; } } } @@ -439,9 +445,25 @@ button:not(.mat-mdc-button-disabled) { .mat-sort-header-indicator { &::before { content: 'arrow_downward'; - top: -14px; - line-height: 20px; + top: -20px; } } } } + +/* + +.mat-mdc-icon-button:hover .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-icon-button-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +