Skip to content

Commit b5a357c

Browse files
committed
feat: add file picker
1 parent 71eea9f commit b5a357c

File tree

4 files changed

+156
-0
lines changed

4 files changed

+156
-0
lines changed

packages/elements/src/components/app/app.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export class MutationTestReportAppComponent extends RealTimeElement {
343343
public render() {
344344
if (this.context.result ?? this.errorMessage) {
345345
return html`
346+
<mte-file-picker .rootModel="${this.rootModel}"></mte-file-picker>
346347
<div class="container bg-white pb-4 font-sans text-gray-800 motion-safe:transition-max-width">
347348
<div class="space-y-4 transition-colors">
348349
${this.renderErrorMessage()}

packages/elements/src/components/file-icon/file-icon.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { customElement, property } from 'lit/decorators.js';
33
import { determineLanguage, ProgrammingLanguage } from '../../lib/code-helpers.js';
44
import style from './file-icon.css?inline';
55
import { classMap } from 'lit/directives/class-map.js';
6+
67
@customElement('mte-file-icon')
78
export class MutationTestReportFileIconComponent extends LitElement {
89
@property({ attribute: 'file-name' })
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { html, LitElement, nothing, } from "lit";
2+
import { map } from 'lit/directives/map.js';
3+
import { customElement, property, state } from "lit/decorators.js";
4+
import { tailwind } from "../../style/index.js";
5+
import { FileUnderTestModel, Metrics, MetricsResult, MutationTestMetricsResult, TestFileModel, TestMetrics } from "mutation-testing-metrics";
6+
7+
@customElement('mte-file-picker')
8+
export class MutationTestReportFilePickerComponent extends LitElement {
9+
static styles = [tailwind];
10+
11+
#currentPressedKeys = new Set<string>();
12+
#searchMap = new Map<string, string>();
13+
14+
@property({ type: Object })
15+
public declare rootModel: MutationTestMetricsResult;
16+
17+
@state()
18+
public declare openPicker: boolean;
19+
20+
@state()
21+
public declare filteredFiles: string[];
22+
23+
constructor() {
24+
super();
25+
26+
this.openPicker = false;
27+
}
28+
29+
connectedCallback(): void {
30+
super.connectedCallback();
31+
32+
this.#prepareMap();
33+
this.#filter('');
34+
35+
document.addEventListener('keydown', (e) => this.#handleKeyDown(e));
36+
document.addEventListener('keyup', (e) => this.#handleKeyUp(e));
37+
}
38+
39+
render() {
40+
if (!this.openPicker) {
41+
return nothing;
42+
}
43+
44+
return html`
45+
<div @click="${this.#closePicker}" class="fixed flex justify-center items-center top-0 left-0 h-full w-full bg-black/50 backdrop-blur-lg z-50">
46+
<div @click="${(e: MouseEvent) => e.stopPropagation()}" role="dialog" id="picker" class="flex flex-col bg-gray-200/60 backdrop-blur-lg h-full md:h-1/2 w-full md:w-1/2 max-w-[40rem] rounded-lg p-4 m-4">
47+
<div class="flex items-center mb-2 p-2 rounded bg-gray-200/60 backdrop-blur-lg">
48+
<div class="h-full flex items-center mx-2 text-gray-800">
49+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
50+
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
51+
</svg>
52+
</div>
53+
<input
54+
@keyup="${this.#handleSearch}"
55+
type="text"
56+
style="box-shadow: none"
57+
class="mr-2 w-full text-gray-800 bg-transparent border-transparent border-0 focus:shadow-none rounded" placeholder="Search for a file" />
58+
</div>
59+
<div class="height-full overflow-auto">
60+
${this.#renderFoundFiles()}
61+
</div>
62+
</div>
63+
</div>
64+
`;
65+
}
66+
67+
#renderFoundFiles() {
68+
return html`
69+
<ul class="flex flex-col">
70+
${map(this.filteredFiles, (file) => {
71+
return html`
72+
<li>
73+
<a class="flex border-2 border-black bg-black rounded p-1 my-1 text-gray-800 focus-visible:border-primary-500 outline-none" @click="${this.#closePicker}" href="#mutant/${file}">${file}</a>
74+
</li>`
75+
})}
76+
</ul>
77+
`;
78+
}
79+
80+
#prepareMap() {
81+
const prepareFiles = (result: MetricsResult<FileUnderTestModel, Metrics>, parentPath: string | null = null) => {
82+
if (result.file != null) {
83+
this.#searchMap.set(parentPath == null ? result.name : `${parentPath}/${result.name}`, '');
84+
}
85+
86+
result.childResults.forEach((child) => {
87+
if (parentPath != null && parentPath !== 'All files') {
88+
prepareFiles(child, `${parentPath}/${result.name}`);
89+
}
90+
else {
91+
prepareFiles(child, result.name);
92+
}
93+
});
94+
}
95+
96+
prepareFiles(this.rootModel.systemUnderTestMetrics);
97+
}
98+
99+
#handleKeyDown(event: KeyboardEvent) {
100+
this.#currentPressedKeys.add(event.key);
101+
102+
if ((this.#currentPressedKeys.has('Control') && this.#currentPressedKeys.has('k'))) {
103+
this.#togglePicker(event);
104+
}
105+
106+
if (!this.openPicker && this.#currentPressedKeys.has('/')) {
107+
this.#togglePicker(event);
108+
}
109+
110+
if (this.#currentPressedKeys.has('Escape')) {
111+
this.openPicker = false;
112+
}
113+
}
114+
115+
#togglePicker(event: KeyboardEvent) {
116+
event.preventDefault();
117+
event.stopPropagation();
118+
119+
this.openPicker = !this.openPicker;
120+
121+
if (!this.openPicker) {
122+
document.body.style.overflow = 'auto';
123+
} else {
124+
document.body.style.overflow = 'hidden';
125+
}
126+
127+
setTimeout(() => {
128+
this.shadowRoot?.querySelector('input')?.focus();
129+
}, 10);
130+
}
131+
132+
#closePicker() {
133+
document.body.style.overflow = 'auto';
134+
this.openPicker = false;
135+
this.#filter('');
136+
}
137+
138+
#handleKeyUp(event: KeyboardEvent) {
139+
this.#currentPressedKeys.delete(event.key);
140+
}
141+
142+
#handleSearch(event: KeyboardEvent) {
143+
if (!this.openPicker) {
144+
return;
145+
}
146+
147+
this.#filter((event.target as HTMLInputElement).value);
148+
}
149+
150+
#filter(filterKey: string) {
151+
this.filteredFiles = Array.from(this.#searchMap.keys()).filter((file) => file.includes(filterKey));
152+
}
153+
}

packages/elements/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './components/app/app.component.js';
22
import './components/file/file.component.js';
3+
import './components/file-picker/file-picker.component.js';
34
import './components/breadcrumb.js';
45
import './components/state-filter/state-filter.component.js';
56
import './components/theme-switch/theme-switch.component.js';

0 commit comments

Comments
 (0)