Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions modules/ui/src/app/mocks/reports.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export const FILTERS = {
deviceFirmware: 'test',
results: ['test'],
dateRange: 'test',
quickSearch: 'test',
quickSearch: ['test'],
location: 'test',
linuxEnv: 'test',
pythonVersion: 'test',
Expand All @@ -226,7 +226,7 @@ export const EMPTY_FILTERS = {
deviceFirmware: '',
results: [''],
dateRange: '',
quickSearch: '',
quickSearch: [],
location: '',
linuxEnv: '',
pythonVersion: '',
Expand Down
2 changes: 1 addition & 1 deletion modules/ui/src/app/model/filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('Filters Models', () => {
expect(filters.deviceFirmware).toBe('');
expect(filters.results).toEqual([]);
expect(filters.dateRange).toBe('');
expect(filters.quickSearch).toBe('');
expect(filters.quickSearch).toEqual([]);
expect(filters.location).toBe('');
expect(filters.linuxEnv).toBe('');
expect(filters.pythonVersion).toBe('');
Expand Down
2 changes: 1 addition & 1 deletion modules/ui/src/app/model/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class Filters {
deviceFirmware = '';
results: string[] = [];
dateRange: DateRange | string = '';
quickSearch = '';
quickSearch: string[] = [];
location = '';
linuxEnv = '';
pythonVersion = '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
(menuClosed)="isMenuOpened = false"></div>

<div class="search-content">
@for (item of getActiveFilters(); track item.key) {
@for (item of getActiveFilters(); track getChipTrackId(item)) {
<div
class="filter-chip"
tabindex="0"
Expand All @@ -55,9 +55,21 @@
type="button"
class="filter-chip-remove"
[attr.aria-label]="getRemoveFilterAriaLabel(item.key, item.value)"
(click)="removeFilter(item.key, $event)"
(keydown.enter)="removeFilter(item.key, $event)"
(keydown.space)="removeFilter(item.key, $event)">
(click)="
item.key === FilterName.QuickSearch
? removeFilter(item.key, $event, item.value)
: removeFilter(item.key, $event)
"
(keydown.enter)="
item.key === FilterName.QuickSearch
? removeFilter(item.key, $event, item.value)
: removeFilter(item.key, $event)
"
(keydown.space)="
item.key === FilterName.QuickSearch
? removeFilter(item.key, $event, item.value)
: removeFilter(item.key, $event)
">
<mat-icon class="chip-close-icon" aria-hidden="true">close</mat-icon>
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('SearchComponent', () => {
deviceFirmware: '1.0',
results: ['Compliant'],
dateRange: { start: '10/01/2024', end: '10/05/2024' },
quickSearch: 'test',
quickSearch: ['test'],
location: 'Data Center',
linuxEnv: 'Ubuntu 24.04',
pythonVersion: '3.11',
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('SearchComponent', () => {
deviceFirmware: '1.0',
results: ['Compliant'],
dateRange: { start: '10/01/2024', end: '10/05/2024' },
quickSearch: 'test',
quickSearch: ['test'],
location: 'DC-1',
linuxEnv: 'Ubuntu',
pythonVersion: '3.11',
Expand Down Expand Up @@ -241,7 +241,19 @@ describe('SearchComponent', () => {
spyOn(component.filterCleared, 'emit');
component.removeFilter(FilterName.QuickSearch);

expect(component.filters.quickSearch).toBe('');
expect(component.filters.quickSearch).toEqual([]);
expect(component.filterCleared.emit).toHaveBeenCalledWith(
component.filters
);
});

it('should remove specific quickSearch chip when value is provided', () => {
component.filters.quickSearch = ['first', 'second', 'third'];
spyOn(component.filterCleared, 'emit');

component.removeFilter(FilterName.QuickSearch, undefined, 'second');

expect(component.filters.quickSearch).toEqual(['first', 'third']);
expect(component.filterCleared.emit).toHaveBeenCalledWith(
component.filters
);
Expand Down Expand Up @@ -305,7 +317,7 @@ describe('SearchComponent', () => {
expect(component.filters.deviceFirmware).toBe('');
expect(component.filters.results).toEqual([]);
expect(component.filters.dateRange).toBe('');
expect(component.filters.quickSearch).toBe('');
expect(component.filters.quickSearch).toEqual([]);
expect(component.filters.location).toBe('');
expect(component.filters.linuxEnv).toBe('');
expect(component.filters.pythonVersion).toBe('');
Expand Down Expand Up @@ -338,7 +350,7 @@ describe('SearchComponent', () => {
component.onEnter(event);

expect(event.preventDefault).toHaveBeenCalled();
expect(component.filters.quickSearch).toBe('Raspberry Pi');
expect(component.filters.quickSearch).toEqual(['Raspberry Pi']);
expect(component.searchQueryChanged.emit).toHaveBeenCalledWith(
'Raspberry Pi'
);
Expand All @@ -348,6 +360,39 @@ describe('SearchComponent', () => {
expect(component.inputValue).toBe('');
});

it('should add multiple search chips on subsequent Enters without replacing previous chip', () => {
spyOn(component.filterCleared, 'emit');
const event = new KeyboardEvent('keydown', { key: 'Enter' });

component.inputValue = 'first';
component.onEnter(event);

expect(component.filters.quickSearch).toEqual(['first']);

component.inputValue = 'second';
component.onEnter(event);

expect(component.filters.quickSearch).toEqual(['first', 'second']);

const active = component.getActiveFilters();
const searchChips = active.filter(
item => item.key === FilterName.QuickSearch
);
expect(searchChips.length).toBe(2);
expect(searchChips[0].value).toBe('first');
expect(searchChips[1].value).toBe('second');
});

it('should not add duplicate search chip if identical query is entered', () => {
component.filters.quickSearch = ['first'];
component.inputValue = 'first';
const event = new KeyboardEvent('keydown', { key: 'Enter' });
component.onEnter(event);

expect(component.filters.quickSearch).toEqual(['first']);
expect(component.inputValue).toBe('');
});

it('should not add search chip on Enter if input is empty or whitespace', () => {
spyOn(component.filterCleared, 'emit');
spyOn(component.searchQueryChanged, 'emit');
Expand All @@ -366,7 +411,7 @@ describe('SearchComponent', () => {
deviceFirmware: '1.0',
results: [],
dateRange: '',
quickSearch: '',
quickSearch: [],
location: '',
linuxEnv: '',
pythonVersion: '',
Expand All @@ -380,6 +425,19 @@ describe('SearchComponent', () => {
expect(component.removeFilter).toHaveBeenCalledWith('deviceFirmware');
});

it('should remove last quickSearch chip on Backspace when multiple exist', () => {
component.filters = {
...new Filters(),
quickSearch: ['first', 'second'],
};
component.inputValue = '';
spyOn(component, 'removeFilter').and.callThrough();

component.onBackspace();

expect(component.filters.quickSearch).toEqual(['first']);
});

it('should not remove last filter on Backspace when no active filters', () => {
component.filters = new Filters();
component.inputValue = '';
Expand All @@ -396,7 +454,7 @@ describe('SearchComponent', () => {
deviceFirmware: '',
results: [],
dateRange: '',
quickSearch: '',
quickSearch: [],
location: '',
linuxEnv: '',
pythonVersion: '',
Expand Down Expand Up @@ -481,7 +539,7 @@ describe('SearchComponent', () => {
deviceFirmware: '',
results: ['Compliant'],
dateRange: '',
quickSearch: 'test',
quickSearch: ['test'],
location: '',
linuxEnv: '',
pythonVersion: '',
Expand All @@ -493,6 +551,37 @@ describe('SearchComponent', () => {
expect(chips.length).toBe(3);
});

it('should render multiple search chips and remove only clicked chip from DOM', () => {
component.filters = {
...new Filters(),
quickSearch: ['apple', 'banana'],
};
fixture.detectChanges();

let chips = compiled.querySelectorAll('.filter-chip');
expect(chips.length).toBe(2);
expect(chips[0].textContent).toContain('search: "apple"');
expect(chips[1].textContent).toContain('search: "banana"');

spyOn(component, 'removeFilter').and.callThrough();
const firstRemoveBtn = chips[0].querySelector(
'.filter-chip-remove'
) as HTMLButtonElement;
firstRemoveBtn.click();

expect(component.removeFilter).toHaveBeenCalledWith(
FilterName.QuickSearch,
jasmine.any(MouseEvent),
'apple'
);
expect(component.filters.quickSearch).toEqual(['banana']);

fixture.detectChanges();
chips = compiled.querySelectorAll('.filter-chip');
expect(chips.length).toBe(1);
expect(chips[0].textContent).toContain('search: "banana"');
});

it('should render clear button when filters or input value are present', () => {
component.filters = new Filters();
component.inputValue = '';
Expand All @@ -512,7 +601,7 @@ describe('SearchComponent', () => {
deviceFirmware: '',
results: [],
dateRange: '',
quickSearch: '',
quickSearch: [],
location: '',
linuxEnv: '',
pythonVersion: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,34 @@ export class SearchComponent {
this.inputValue = target.value;
}

private normalizeQuickSearch(value: FilterValue): string[] {
if (Array.isArray(value)) {
return value.map(v => String(v).trim()).filter(Boolean);
}
if (typeof value === 'string' && value.trim()) {
return [value.trim()];
}
return [];
}

getChipTrackId(item: ActiveFilterItem): string {
return `${item.key}_${this.getFilterChipLabel(item.key, item.value)}`;
}

onEnter(event: Event): void {
event.preventDefault();
const query = this.inputValue.trim();
if (query) {
this.filters.quickSearch = query;
if (!this.filters) {
this.filters = new Filters();
}
const currentQueries = this.normalizeQuickSearch(
this.filters.quickSearch
);
if (!currentQueries.includes(query)) {
currentQueries.push(query);
}
this.filters.quickSearch = currentQueries;
this.searchQueryChanged.emit(query);
this.filterCleared.emit(this.filters);
this.inputValue = '';
Expand All @@ -146,7 +169,11 @@ export class SearchComponent {
const active = this.getActiveFilters();
if (active.length > 0) {
const lastFilter = active[active.length - 1];
this.removeFilter(lastFilter.key);
if (lastFilter.key === FilterName.QuickSearch) {
this.removeFilter(lastFilter.key, undefined, lastFilter.value);
} else {
this.removeFilter(lastFilter.key);
}
}
}
}
Expand All @@ -171,7 +198,14 @@ export class SearchComponent {
for (const key of keys) {
const value = this.filters[key];
if (!this.isValueEmpty(value)) {
items.push({ key, value });
if (key === FilterName.QuickSearch) {
const queries = this.normalizeQuickSearch(value);
for (const query of queries) {
items.push({ key, value: query });
}
} else {
items.push({ key, value });
}
}
}

Expand Down Expand Up @@ -248,7 +282,24 @@ export class SearchComponent {
return `Clear filter: ${this.getFilterChipLabel(key, value)}`;
}

removeFilter(key: string, event?: Event): void {
removeFilter(
key: string,
eventOrValue?: Event | FilterValue,
valueOrEvent?: FilterValue | Event
): void {
let event: Event | undefined;
let value: FilterValue | undefined;

if (eventOrValue instanceof Event) {
event = eventOrValue;
value = valueOrEvent as FilterValue;
} else if (valueOrEvent instanceof Event) {
event = valueOrEvent;
value = eventOrValue as FilterValue;
} else {
value = (eventOrValue as FilterValue) ?? (valueOrEvent as FilterValue);
}

if (event) {
event.preventDefault();
event.stopPropagation();
Expand All @@ -268,8 +319,16 @@ export class SearchComponent {
this.filters.dateRange = '';
break;
case FilterName.QuickSearch:
this.filters.quickSearch = '';
//this.searchQueryChanged.emit('');
if (value !== undefined) {
const currentQueries = this.normalizeQuickSearch(
this.filters.quickSearch
);
this.filters.quickSearch = currentQueries.filter(
q => q !== String(value)
);
} else {
this.filters.quickSearch = [];
}
break;
case FilterName.Location:
this.filters.location = '';
Expand Down Expand Up @@ -300,7 +359,7 @@ export class SearchComponent {
this.filters.deviceFirmware = '';
this.filters.results = [];
this.filters.dateRange = '';
this.filters.quickSearch = '';
this.filters.quickSearch = [];
this.filters.location = '';
this.filters.linuxEnv = '';
this.filters.pythonVersion = '';
Expand Down
3 changes: 1 addition & 2 deletions modules/ui/src/app/pages/reports/reports.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ <h2 id="reports-heading" class="title" tabindex="-1">Reports</h2>
[filterOpened]="vm.filterOpened"
[activeFilter]="vm.activeFilter"
(emitOpenFilter)="openFilter($event)"
(filterCleared)="filterCleared($event)"
(searchQueryChanged)="onSearchQueryChanged($event)">
(filterCleared)="filterCleared($event)">
</app-search>
<div
class="history-content"
Expand Down
Loading
Loading