Skip to content

Commit 0c7eaac

Browse files
authored
feat(ui): add project filter/sort support (#8689)
1 parent f865d2b commit 0c7eaac

File tree

8 files changed

+236
-20
lines changed

8 files changed

+236
-20
lines changed

packages/ui/client/components/FilterStatus.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
<script setup lang="ts">
2-
defineProps<{ label: string }>()
2+
const { disabled = false } = defineProps<{
3+
label: string
4+
disabled?: boolean
5+
}>()
36
const modelValue = defineModel<boolean | null>()
7+
8+
function toggle() {
9+
if (disabled) {
10+
return
11+
}
12+
13+
modelValue.value = !modelValue.value
14+
}
415
</script>
516

617
<template>
718
<label
8-
class="font-light text-sm checkbox flex items-center cursor-pointer py-1 text-sm w-full gap-y-1 mb-1px"
19+
class="font-light text-sm checkbox flex items-center py-1 text-sm w-full gap-y-1 mb-1px"
20+
:class="disabled ? 'cursor-not-allowed op50' : 'cursor-pointer'"
921
v-bind="$attrs"
10-
@click.prevent="modelValue = !modelValue"
22+
@click.prevent="toggle"
1123
>
1224
<span
1325
:class="[
@@ -19,6 +31,7 @@ const modelValue = defineModel<boolean | null>()
1931
<input
2032
v-model="modelValue"
2133
type="checkbox"
34+
:disabled="disabled"
2235
sr-only
2336
>
2437
<span flex-1 ms-2 select-none>{{ label }}</span>

packages/ui/client/components/explorer/Explorer.vue

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
<script setup lang="ts">
22
import type { File, Task } from '@vitest/runner'
33
import { useResizeObserver } from '@vueuse/core'
4-
54
import { hideAllPoppers } from 'floating-vue'
6-
75
import { computed, ref } from 'vue'
6+
87
// @ts-expect-error missing types
98
import { RecycleScroller } from 'vue-virtual-scroller'
10-
11-
import { config } from '~/composables/client'
12-
9+
import { availableProjects, config } from '~/composables/client'
1310
import { useSearch } from '~/composables/explorer/search'
11+
import { ALL_PROJECTS, projectSort } from '~/composables/explorer/state'
1412
import { activeFileId } from '~/composables/params'
1513
import DetailsPanel from '../DetailsPanel.vue'
1614
import FilterStatus from '../FilterStatus.vue'
@@ -32,6 +30,8 @@ const emit = defineEmits<{
3230
const includeTaskLocation = computed(() => config.value.includeTaskLocation)
3331
3432
const searchBox = ref<HTMLInputElement | undefined>()
33+
const selectProjectRef = ref<HTMLSelectElement | undefined>()
34+
const sortProjectRef = ref<HTMLSelectElement | undefined>()
3535
3636
const {
3737
initialized,
@@ -47,7 +47,15 @@ const {
4747
filteredFiles,
4848
testsTotal,
4949
uiEntries,
50-
} = useSearch(searchBox)
50+
enableProjects,
51+
disableClearProjects,
52+
currentProject,
53+
currentProjectName,
54+
clearProject,
55+
disableProjectSort,
56+
clearProjectSort,
57+
disableClearProjectSort,
58+
} = useSearch(searchBox, selectProjectRef, sortProjectRef)
5159
5260
const filterClass = ref<string>('grid-cols-2')
5361
const filterHeaderClass = ref<string>('grid-col-span-2')
@@ -71,6 +79,104 @@ useResizeObserver(() => testExplorerRef.value, ([{ contentRect }]) => {
7179
<div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base">
7280
<slot name="header" :filtered-files="isFiltered || isFilteredByStatus ? filteredFiles : undefined" />
7381
</div>
82+
<div
83+
v-if="enableProjects"
84+
p="l3 y2 r2"
85+
bg-header
86+
border="b-2 base"
87+
grid="~ cols-[auto_auto_minmax(0,1fr)_auto] gap-x-2 gap-y-1"
88+
items-center
89+
>
90+
<!-- Row 1 -->
91+
<div class="i-carbon:workspace" flex-shrink-0 />
92+
<label for="project-select" text-sm>
93+
Projects
94+
</label>
95+
<div class="relative flex-1">
96+
<select
97+
id="project-select"
98+
ref="selectProjectRef"
99+
v-model="currentProject"
100+
w-full
101+
appearance-none
102+
bg-base
103+
text-base
104+
border="~ base rounded"
105+
pl-2
106+
pr-8
107+
py-1
108+
text-sm
109+
cursor-pointer
110+
hover:bg-active
111+
class="outline-none"
112+
>
113+
<option :value="ALL_PROJECTS" class="text-base bg-base">
114+
All Projects
115+
</option>
116+
<option
117+
v-for="project in availableProjects"
118+
:key="project"
119+
:value="project"
120+
class="text-base bg-base"
121+
>
122+
{{ project }}
123+
</option>
124+
</select>
125+
<div class="i-carbon:chevron-down absolute right-2 top-1/2 op50 -translate-y-1/2 pointer-events-none" />
126+
</div>
127+
128+
<IconButton
129+
v-tooltip.bottom="'Clear project filter'"
130+
:disabled="disableClearProjects"
131+
title="Clear project filter"
132+
icon="i-carbon:filter-remove"
133+
@click.passive="clearProject(true)"
134+
/>
135+
136+
<!-- Row 2 -->
137+
<div class="i-carbon:arrows-vertical" flex-shrink-0 />
138+
<label for="project-sort" text-sm>
139+
Sort by
140+
</label>
141+
<div class="relative flex-1" :class="{ 'op-50 cursor-not-allowed': disableProjectSort }">
142+
<select
143+
id="project-sort"
144+
ref="sortProjectRef"
145+
v-model="projectSort"
146+
w-full
147+
appearance-none
148+
bg-base
149+
text-base
150+
border="~ base rounded"
151+
pl-2
152+
pr-8
153+
py-1
154+
text-sm
155+
cursor-pointer
156+
hover:bg-active
157+
class="outline-none"
158+
:disabled="disableProjectSort"
159+
>
160+
<option value="default" class="text-base bg-base">
161+
Default
162+
</option>
163+
<option value="asc" class="text-base bg-base">
164+
Project A-Z
165+
</option>
166+
<option value="desc" class="text-base bg-base">
167+
Project Z-A
168+
</option>
169+
</select>
170+
<div class="i-carbon:chevron-down absolute right-2 top-1/2 op50 -translate-y-1/2 pointer-events-none" />
171+
</div>
172+
<IconButton
173+
v-tooltip.bottom="'Reset sort'"
174+
:disabled="disableClearProjectSort"
175+
title="Reset sort"
176+
icon="i-carbon:filter-reset"
177+
@click.passive="clearProjectSort(true)"
178+
/>
179+
</div>
74180
<div
75181
p="l3 y2 r2"
76182
flex="~ gap-2"
@@ -149,7 +255,7 @@ useResizeObserver(() => testExplorerRef.value, ([{ contentRect }]) => {
149255
</div>
150256
</template>
151257
<!-- empty-state -->
152-
<template v-if="(isFiltered || isFilteredByStatus) && uiEntries.length === 0">
258+
<template v-if="(isFiltered || isFilteredByStatus || !!currentProjectName) && uiEntries.length === 0">
153259
<div v-if="initialized" flex="~ col" items-center p="x4 y4" font-light>
154260
<div op30>
155261
No matched test

packages/ui/client/composables/client/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const client = (function createVitestClient() {
6969

7070
export const config = shallowRef<SerializedConfig>({} as any)
7171
export const status = ref<WebSocketStatus>('CONNECTING')
72+
export const availableProjects = shallowRef<string[]>([])
7273

7374
export const current = computed(() => {
7475
const currentFileId = activeFileId.value
@@ -184,6 +185,7 @@ watch(
184185
return file
185186
})
186187
}
188+
availableProjects.value = projects.map(p => p.name)
187189
explorerTree.loadFiles(files, projects)
188190
client.state.collectFiles(files)
189191
explorerTree.startRun()

packages/ui/client/composables/explorer/filter.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import type { Task } from '@vitest/runner'
22
import type { FileTreeNode, Filter, FilterResult, ParentTreeNode, UITaskTreeNode } from '~/composables/explorer/types'
33
import { client, findById } from '~/composables/client'
44
import { explorerTree } from '~/composables/explorer/index'
5-
import { filteredFiles, uiEntries } from '~/composables/explorer/state'
5+
import { currentProjectName, filteredFiles, projectSort, uiEntries } from '~/composables/explorer/state'
66
import {
7+
getSortedRootTasks,
78
isFileNode,
89
isParentNode,
910
isTestNode,
10-
sortedRootTasks,
1111
} from '~/composables/explorer/utils'
1212
import { caseInsensitiveMatch } from '~/utils/task'
1313

@@ -36,7 +36,13 @@ export function* filterAll(
3636
search: string,
3737
filter: Filter,
3838
) {
39-
for (const node of sortedRootTasks()) {
39+
const project = currentProjectName.value
40+
const tasks = getSortedRootTasks(projectSort.value)
41+
42+
for (const node of tasks) {
43+
if (project && node.projectName !== project) {
44+
continue
45+
}
4046
yield* filterNode(node, search, filter)
4147
}
4248
}

packages/ui/client/composables/explorer/search.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
11
import type { Ref } from 'vue'
2+
import type { ProjectSortUIType } from '~/composables/explorer/types'
23
import { debouncedWatch } from '@vueuse/core'
34
import { computed, ref, watch } from 'vue'
45
import { explorerTree } from '~/composables/explorer'
56
import {
7+
ALL_PROJECTS,
8+
currentProject,
9+
currentProjectName,
10+
disableClearProjects,
11+
enableProjects,
612
filter,
713
filteredFiles,
814
initialized,
915
isFiltered,
1016
isFilteredByStatus,
1117
openedTreeItems,
18+
projectSort,
1219
search,
1320
testsTotal,
1421
treeFilter,
1522
uiEntries,
1623
} from './state'
1724

18-
export function useSearch(searchBox: Ref<HTMLDivElement | undefined>) {
25+
export function useSearch(
26+
searchBox: Ref<HTMLInputElement | undefined>,
27+
selectProject: Ref<HTMLSelectElement | undefined>,
28+
sortProject: Ref<HTMLSelectElement | undefined>,
29+
) {
1930
const disableFilter = computed(() => {
2031
if (isFilteredByStatus.value) {
2132
return false
2233
}
2334

2435
return !filter.onlyTests
2536
})
37+
2638
const disableClearSearch = computed(() => search.value === '')
2739
const debouncedSearch = ref(search.value)
40+
const disableProjectSort = computed(() => currentProject.value !== ALL_PROJECTS)
41+
const disableClearProjectSort = computed(() => disableProjectSort.value || projectSort.value === 'default')
2842

2943
debouncedWatch(() => search.value, (value) => {
3044
debouncedSearch.value = value?.trim() ?? ''
@@ -47,9 +61,25 @@ export function useSearch(searchBox: Ref<HTMLDivElement | undefined>) {
4761
}
4862
}
4963

64+
function clearProject(focus: boolean) {
65+
currentProject.value = ALL_PROJECTS
66+
if (focus) {
67+
selectProject.value?.focus()
68+
}
69+
}
70+
71+
function clearProjectSort(focus: boolean) {
72+
projectSort.value = 'default'
73+
if (focus) {
74+
sortProject.value?.focus()
75+
}
76+
}
77+
5078
function clearAll() {
5179
clearFilter(false)
5280
clearSearch(true)
81+
clearProject(false)
82+
clearProjectSort(false)
5383
}
5484

5585
function updateFilterStorage(
@@ -58,6 +88,8 @@ export function useSearch(searchBox: Ref<HTMLDivElement | undefined>) {
5888
successValue: boolean,
5989
skippedValue: boolean,
6090
onlyTestsValue: boolean,
91+
projectValue: string,
92+
projectSortValue: ProjectSortUIType,
6193
) {
6294
if (!initialized.value) {
6395
return
@@ -68,6 +100,8 @@ export function useSearch(searchBox: Ref<HTMLDivElement | undefined>) {
68100
treeFilter.value.success = successValue
69101
treeFilter.value.skipped = skippedValue
70102
treeFilter.value.onlyTests = onlyTestsValue
103+
treeFilter.value.project = projectValue
104+
treeFilter.value.projectSort = projectSortValue === 'default' ? undefined : projectSortValue
71105
}
72106

73107
watch(
@@ -77,9 +111,19 @@ export function useSearch(searchBox: Ref<HTMLDivElement | undefined>) {
77111
filter.success,
78112
filter.skipped,
79113
filter.onlyTests,
114+
currentProject.value,
115+
projectSort.value,
80116
] as const,
81-
([search, failed, success, skipped, onlyTests]) => {
82-
updateFilterStorage(search, failed, success, skipped, onlyTests)
117+
([search, failed, success, skipped, onlyTests, project, projectSort]) => {
118+
updateFilterStorage(
119+
search,
120+
failed,
121+
success,
122+
skipped,
123+
onlyTests,
124+
project,
125+
projectSort,
126+
)
83127
explorerTree.filterNodes()
84128
},
85129
{ flush: 'post' },
@@ -105,5 +149,14 @@ export function useSearch(searchBox: Ref<HTMLDivElement | undefined>) {
105149
filteredFiles,
106150
testsTotal,
107151
uiEntries,
152+
enableProjects,
153+
disableClearProjects,
154+
currentProject,
155+
currentProjectName,
156+
clearProject,
157+
projectSort,
158+
disableProjectSort,
159+
clearProjectSort,
160+
disableClearProjectSort,
108161
}
109162
}

0 commit comments

Comments
 (0)