Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search bar in custom views #3529

Merged
merged 8 commits into from
Apr 2, 2024
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
1 change: 1 addition & 0 deletions visualization/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/)

### Added 🚀

- Add search bar for custom configs [#3529](https://github.com/MaibornWolff/codecharta/pull/3529)
- Automatically reverse the metric direction for those where higher values indicate better codequality, such as `branch_coverage` [#3518](https://github.com/MaibornWolff/codecharta/pull/3518)
- Display summary metrics for root node as default [#3525](https://github.com/MaibornWolff/codecharta/pull/3525)
- Remove whitespace on screenshots [#3527](https://github.com/MaibornWolff/codecharta/pull/3527)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { FilterCustomConfigDataBySearchTermPipe } from "./filterCustomConfigDataBySearchTerm.pipe"
import { CustomConfigItem } from "../../../customConfigs.component"
import { CUSTOM_CONFIG_ITEM_GROUPS } from "../../../../../util/dataMocks"

describe("FilterCustomConfigDataBySearchTermPipe", () => {
let pipe: FilterCustomConfigDataBySearchTermPipe
let allCustomConfigItems: CustomConfigItem[]

beforeEach(() => {
pipe = new FilterCustomConfigDataBySearchTermPipe()
allCustomConfigItems = [...CUSTOM_CONFIG_ITEM_GROUPS.values()].flatMap(group => group.customConfigItems)
})

it("should filter custom configurations by name", () => {
const searchTerm = "samplemap view #1"
const results = pipe.transform(allCustomConfigItems, searchTerm)
expect(results.length).toBe(2)
expect(results.every(item => item.name.toLocaleLowerCase().includes(searchTerm))).toBe(true)
})

it("should filter custom configurations by mapSelectionMode", () => {
const searchTerm = "standard"
const results = pipe.transform(allCustomConfigItems, searchTerm)
expect(results.length).toBe(4)
for (const item of results) {
expect(item.mapSelectionMode.toLowerCase()).toContain(searchTerm)
}
})

it("should filter custom configurations by metric names", () => {
const searchTerm = "rloc"
const results = pipe.transform(allCustomConfigItems, searchTerm)
expect(results.length).toBe(5)
for (const item of results) {
const metricValues = Object.values(item.metrics).map(metric => metric?.toLowerCase())
expect(metricValues.some(metric => metric?.includes(searchTerm))).toBe(true)
}
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from "@angular/core"
import { CustomConfigItem } from "../../../customConfigs.component"

@Pipe({
name: "filterCustomConfigDataBySearchTerm"
})
export class FilterCustomConfigDataBySearchTermPipe implements PipeTransform {
transform(customConfigItems: CustomConfigItem[], searchTerm: string): CustomConfigItem[] {
const lowerCasedSearchTerm = searchTerm.toLocaleLowerCase().trimEnd()
return customConfigItems.filter(item => this.isItemMatchingSearchTerm(item, lowerCasedSearchTerm))
}

private isItemMatchingSearchTerm(customConfigItem: CustomConfigItem, searchTerm: string): boolean {
const isSearchTermIncludedInName = customConfigItem.name.toLocaleLowerCase().includes(searchTerm)
const isSearchTermIncludedInMode = customConfigItem.mapSelectionMode.toLocaleLowerCase().includes(searchTerm)
const isSearchTermIncludedInMetrics = Object.values(customConfigItem.metrics).some(metric =>
metric?.toLocaleLowerCase().includes(searchTerm)
)

return isSearchTermIncludedInName || isSearchTermIncludedInMode || isSearchTermIncludedInMetrics
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,52 @@
<mat-expansion-panel
[expanded]="isExpanded"
(opened)="isExpanded = true"
(closed)="isExpanded = false"
#matExpansionPanel
*ngFor="let customConfigItemGroup of customConfigItemGroups | keyvalue"
[expanded]="isGroupExpanded(customConfigItemGroup.key) || searchTerm.length > 0"
>
<mat-expansion-panel-header>
<mat-expansion-panel-header (click)="toggleGroupExpansion(customConfigItemGroup.key)">
<mat-panel-title class="custom-config-item-group-title">
Custom View(s) in
<strong> {{ customConfigItemGroup.value.mapSelectionMode | titlecase }} </strong>
mode for
{{ customConfigItemGroup.value.mapNames }}
</mat-panel-title>
</mat-expansion-panel-header>
<mat-list *ngFor="let customConfig of customConfigItemGroup.value.customConfigItems">
<mat-list-item title="{{ customConfig | customConfig2ApplicableMessage }}">
<div class="metrics-box">
<p class="config-item-name" title="{{ customConfig.name }}">
<strong>
<ng-container
*ngIf="customConfigItemGroup.value.customConfigItems | filterCustomConfigDataBySearchTerm : searchTerm as filteredCustomConfigs"
>
<mat-list *ngIf="filteredCustomConfigs.length > 0">
<mat-list-item title="{{ customConfig | customConfig2ApplicableMessage }}" *ngFor="let customConfig of filteredCustomConfigs">
<div class="metrics-box">
<p class="config-item-name" title="{{ customConfig.name }}">
<strong>
<span (click)="applyCustomConfig(customConfig.id)" mat-dialog-close>
{{ customConfig.name | truncateText : 75 }}
</span>
</strong>
</p>
</div>
<div class="custom-config-note">
<p class="custom-config-note-content">
<span (click)="applyCustomConfig(customConfig.id)" mat-dialog-close>
{{ customConfig.name | truncateText : 75 }}
{{ customConfig.note ? (customConfig.note | truncateText : 95) : "Add Note" }}
</span>
</strong>
</p>
</div>
<div class="custom-config-note">
<p class="custom-config-note-content">
<span (click)="applyCustomConfig(customConfig.id)" mat-dialog-close>
{{ customConfig.note ? (customConfig.note | truncateText : 95) : "Add Note" }}
</span>
</p>
<cc-custom-config-note-dialog-button [customConfigItem]="customConfig"></cc-custom-config-note-dialog-button>
</div>
<div class="custom-config-action-buttons">
<cc-apply-custom-config-button [customConfigItem]="customConfig"></cc-apply-custom-config-button>
<button class="remove-button" title="Remove Custom View" (click)="removeCustomConfig(customConfig.id)">
<i class="fa fa-trash"></i>
</button>
</div>
</mat-list-item>
</mat-list>
</p>
<cc-custom-config-note-dialog-button [customConfigItem]="customConfig"></cc-custom-config-note-dialog-button>
</div>
<div class="custom-config-action-buttons">
<cc-apply-custom-config-button [customConfigItem]="customConfig"></cc-apply-custom-config-button>
<button
class="remove-button"
title="Remove Custom View"
(click)="removeCustomConfig(customConfig.id, customConfigItemGroup.key)"
>
<i class="fa fa-trash"></i>
</button>
</div>
</mat-list-item>
</mat-list>
<div class="no-configs-found-message" *ngIf="filteredCustomConfigs.length === 0">
<p>No configurations found.</p>
</div>
</ng-container>
</mat-expansion-panel>
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { TestBed } from "@angular/core/testing"
import { CustomConfigsModule } from "../../customConfigs.module"
import { render, screen } from "@testing-library/angular"
import { CustomConfigItemGroupComponent } from "./customConfigItemGroup.component"
import { CUSTOM_CONFIG_ITEM_GROUPS } from "../../../../util/dataMocks"
import { MatDialog, MatDialogRef } from "@angular/material/dialog"
import { expect } from "@jest/globals"
import { State } from "@ngrx/store"
import { MockStore, provideMockStore } from "@ngrx/store/testing"
import { queryByRole, queryByText, render, screen, waitFor } from "@testing-library/angular"
import userEvent from "@testing-library/user-event"
import { CustomConfigMapSelectionMode } from "../../../../model/customConfig/customConfig.api.model"
import { defaultState } from "../../../../state/store/state.manager"
import { CustomConfigHelper } from "../../../../util/customConfigHelper"
import { CUSTOM_CONFIG_ITEM_GROUPS } from "../../../../util/dataMocks"
import { ThreeCameraService } from "../../../codeMap/threeViewer/threeCamera.service"
import { ThreeOrbitControlsService } from "../../../codeMap/threeViewer/threeOrbitControls.service"
import userEvent from "@testing-library/user-event"
import { expect } from "@jest/globals"
import { CustomConfigMapSelectionMode } from "../../../../model/customConfig/customConfig.api.model"
import { CustomConfigsModule } from "../../customConfigs.module"
import { visibleFilesBySelectionModeSelector } from "../../visibleFilesBySelectionMode.selector"
import { MatDialog, MatDialogRef } from "@angular/material/dialog"
import { MockStore, provideMockStore } from "@ngrx/store/testing"
import { State } from "@ngrx/store"
import { defaultState } from "../../../../state/store/state.manager"
import { CustomConfigItemGroupComponent } from "./customConfigItemGroup.component"

describe("customConfigItemGroupComponent", () => {
let mockedDialog = { open: jest.fn() }
Expand Down Expand Up @@ -151,4 +151,106 @@ describe("customConfigItemGroupComponent", () => {
expect(editNoteArea.disabled).toBe(false)
expect(getComputedStyle(applyCustomConfigButton).color).toBe("rgb(204, 204, 204)")
})

it("should expand custom config item group on toggle expansion", async () => {
const customConfigItemGroups = new Map([["File_B_File_C_STANDARD", CUSTOM_CONFIG_ITEM_GROUPS.get("File_B_File_C_STANDARD")]])
const { fixture, container } = await render(CustomConfigItemGroupComponent, {
excludeComponentDeclaration: true,
componentProperties: {
expandedStates: {}
},
componentInputs: {
customConfigItemGroups,
searchTerm: ""
}
})

expect(fixture.componentInstance.isGroupExpanded("Custom View(s) in Standard mode for fileB fileC")).toBeFalsy()

const header = queryByRole(container as HTMLElement, "button")
expect(header).not.toBeNull()

const toggleGroupExpansionSpy = jest.spyOn(fixture.componentInstance, "toggleGroupExpansion")

await userEvent.click(header)

expect(toggleGroupExpansionSpy).toBeCalledTimes(1)
waitFor(() => {
expect(fixture.componentInstance.isGroupExpanded("Custom View(s) in Standard mode for fileB fileC")).toBeTruthy()
})
})

it("should reset expanded states on new searchterm or configitems", async () => {
const customConfigItemGroups = new Map([["File_B_File_C_STANDARD", CUSTOM_CONFIG_ITEM_GROUPS.get("File_B_File_C_STANDARD")]])
const { rerender, fixture } = await render(CustomConfigItemGroupComponent, {
excludeComponentDeclaration: true,
componentProperties: {
expandedStates: {}
},
componentInputs: {
customConfigItemGroups,
searchTerm: ""
}
})

waitFor(() => {
expect(fixture.componentInstance.isGroupExpanded("Custom View(s) in Standard mode for fileB fileC")).toBeFalsy()
})

await rerender({
componentProperties: {
expandedStates: {}
},
componentInputs: {
customConfigItemGroups,
searchTerm: "rloc"
}
})

waitFor(() => {
expect(fixture.componentInstance.isGroupExpanded("Custom View(s) in Standard mode for fileB fileC")).toBeTruthy()
})

await rerender({
componentProperties: {
expandedStates: {}
},
componentInputs: {
customConfigItemGroups,
searchTerm: ""
}
})

waitFor(() => {
expect(fixture.componentInstance.isGroupExpanded("Custom View(s) in Standard mode for fileB fileC")).toBeFalsy()
})
})

it("should display no configs found message when searchterm doesnt match any configs", async () => {
const customConfigItemGroups = new Map([["File_B_File_C_STANDARD", CUSTOM_CONFIG_ITEM_GROUPS.get("File_B_File_C_STANDARD")]])
const { rerender, container } = await render(CustomConfigItemGroupComponent, {
excludeComponentDeclaration: true,
componentProperties: {
expandedStates: {}
},
componentInputs: {
customConfigItemGroups,
searchTerm: ""
}
})

expect(queryByText(container as HTMLElement, "No configurations found.")).toBeNull()

await rerender({
componentProperties: {
expandedStates: {}
},
componentInputs: {
customConfigItemGroups,
searchTerm: "non matching searchterm"
}
})

expect(queryByText(container as HTMLElement, "No configurations found.")).not.toBeNull()
})
})
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
import { Component, Input, ViewEncapsulation } from "@angular/core"
import { Component, Input, OnChanges, SimpleChanges, ViewChild, ViewEncapsulation } from "@angular/core"
import { CustomConfigHelper } from "../../../../util/customConfigHelper"
import { CustomConfigItemGroup } from "../../customConfigs.component"
import { ThreeCameraService } from "../../../codeMap/threeViewer/threeCamera.service"
import { ThreeOrbitControlsService } from "../../../codeMap/threeViewer/threeOrbitControls.service"
import { Store } from "@ngrx/store"
import { MatExpansionPanel } from "@angular/material/expansion"

@Component({
selector: "cc-custom-config-item-group",
templateUrl: "./customConfigItemGroup.component.html",
styleUrls: ["./customConfigItemGroup.component.scss"],
encapsulation: ViewEncapsulation.None
})
export class CustomConfigItemGroupComponent {
export class CustomConfigItemGroupComponent implements OnChanges {
@Input() customConfigItemGroups: Map<string, CustomConfigItemGroup>
isExpanded = false
@ViewChild("matExpansionPanel") matExpansionPanel: MatExpansionPanel
@Input() searchTerm = ""
expandedStates: { [key: string]: boolean } = {}
manuallyToggled: Set<string> = new Set()

constructor(
private store: Store,
private threeCameraService: ThreeCameraService,
private threeOrbitControlsService: ThreeOrbitControlsService
) {}

removeCustomConfig(configId: string) {
ngOnChanges(changes: SimpleChanges): void {
if (changes.searchTerm) {
if (changes.searchTerm.currentValue.length > 0) {
for (const groupKey of Object.keys(this.expandedStates)) {
this.expandedStates[groupKey] = true
}
} else {
for (const groupKey of Object.keys(this.expandedStates)) {
if (!this.manuallyToggled.has(groupKey)) {
this.expandedStates[groupKey] = false
}
}
}
}
}

isGroupExpanded(groupKey: string): boolean {
return this.searchTerm.length > 0
? !this.manuallyToggled.has(groupKey) || this.expandedStates[groupKey]
: this.expandedStates[groupKey] || false
}

toggleGroupExpansion(groupKey: string): void {
this.expandedStates[groupKey] = !this.isGroupExpanded(groupKey)
this.manuallyToggled.add(groupKey)
}
removeCustomConfig(configId: string, groupKey: string) {
CustomConfigHelper.deleteCustomConfig(configId)
this.expandedStates[groupKey] = true
}

applyCustomConfig(configId: string) {
Expand Down
Loading
Loading