Skip to content

Commit 4fd4692

Browse files
authored
feat: module graph selector (#138)
1 parent 965f2e1 commit 4fd4692

File tree

6 files changed

+360
-15
lines changed

6 files changed

+360
-15
lines changed

packages/vite/src/app/components/data/SearchPanel.vue

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,18 @@ function unselectToggle() {
7272

7373
<template>
7474
<div flex="col gap-2" max-w-90vw min-w-30vw border="~ base rounded-xl" bg-glass>
75-
<div v-if="modelValue.search !== false">
76-
<input
77-
v-model="model.search"
78-
p2 px4
79-
w-full
80-
style="outline: none"
81-
placeholder="Search"
82-
>
83-
</div>
75+
<slot name="search">
76+
<div v-if="modelValue.search !== false" class="flex items-center">
77+
<input
78+
v-model="model.search"
79+
p2 px4
80+
w-full
81+
style="outline: none"
82+
placeholder="Search"
83+
>
84+
<slot name="search-end" />
85+
</div>
86+
</slot>
8487
<div v-if="rules.length" :class="selectedContainerClass" flex="~ gap-2 wrap" p2 border="t base">
8588
<label
8689
v-for="rule of rules"

packages/vite/src/app/components/modules/FlatList.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
<script setup lang="ts">
22
import type { ModuleListItem, SessionContext } from '~~/shared/types'
33
4-
defineProps<{
4+
withDefaults(defineProps<{
55
session: SessionContext
66
modules: ModuleListItem[]
7+
disableTooltip?: boolean
8+
link?: boolean
9+
}>(), {
10+
disableTooltip: false,
11+
link: true,
12+
})
13+
14+
const emit = defineEmits<{
15+
(e: 'select', module: ModuleListItem): void
716
}>()
817
</script>
918

@@ -14,13 +23,14 @@ defineProps<{
1423
key-prop="id"
1524
>
1625
<template #default="{ item }">
17-
<div flex pb2>
26+
<div flex pb2 @click="emit('select', item)">
1827
<DisplayModuleId
1928
:id="item.id"
2029
:session
2130
hover="bg-active" block px2 p1 w-full
2231
border="~ base rounded"
23-
:link="true"
32+
:link="link"
33+
:disable-tooltip="disableTooltip"
2434
/>
2535
</div>
2636
</template>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<script setup lang="ts">
2+
import type { ModuleListItem, SessionContext } from '~~/shared/types'
3+
import { computed, watch } from 'vue'
4+
import { useModulePathSelector } from '~/composables/moduleGraph'
5+
6+
const props = defineProps<{
7+
session: SessionContext
8+
modules: ModuleListItem[]
9+
}>()
10+
11+
const emit = defineEmits<{
12+
(e: 'close'): void
13+
(e: 'select', nodes: { start: string, end: string }): void
14+
}>()
15+
16+
const modulesMap = computed(() => {
17+
const map = new Map<string, ModuleListItem>()
18+
props.modules.forEach((m) => {
19+
map.set(m.id, m)
20+
})
21+
return map
22+
})
23+
24+
const startSelector = useModulePathSelector({
25+
getModules: () => {
26+
if (!startSelector.state.value.search) {
27+
return props.modules
28+
}
29+
else {
30+
return startSelector.fuse.value!.value?.search(startSelector.state.value.search).map(r => r.item) ?? []
31+
}
32+
},
33+
})
34+
35+
startSelector.initSelector(computed(() => props.modules))
36+
37+
function getAllImports(moduleId: string, visited = new Set<string>()): ModuleListItem[] {
38+
if (visited.has(moduleId))
39+
return []
40+
visited.add(moduleId)
41+
42+
const module = modulesMap.value.get(moduleId)
43+
if (!module?.imports?.length)
44+
return []
45+
46+
const res: ModuleListItem[] = []
47+
48+
for (const importItem of module.imports) {
49+
const importedModule = modulesMap.value.get(importItem.module_id)
50+
if (!importedModule)
51+
continue
52+
53+
if (!visited.has(importedModule.id)) {
54+
res.push(importedModule)
55+
res.push(...getAllImports(importedModule.id, visited))
56+
}
57+
}
58+
59+
return res
60+
}
61+
62+
const endSelector = useModulePathSelector({
63+
getModules: () => {
64+
return startSelector.state.value.selected ? getAllImports(startSelector.state.value.selected) : []
65+
},
66+
})
67+
68+
endSelector.initSelector(endSelector.modules)
69+
70+
const filteredEndModules = computed(() => {
71+
if (!endSelector.state.value.search) {
72+
return endSelector.modules.value
73+
}
74+
else {
75+
return endSelector.fuse.value!.value?.search(endSelector.state.value.search).map(r => r.item) ?? []
76+
}
77+
})
78+
79+
watch([() => startSelector.state.value.selected, () => endSelector.state.value.selected], () => {
80+
emit('select', {
81+
start: startSelector.state.value.selected ?? '',
82+
end: endSelector.state.value.selected ?? '',
83+
})
84+
})
85+
</script>
86+
87+
<template>
88+
<div h12 px4 p2 relative flex="~ gap2 items-center">
89+
<div flex="~ items-center gap2" class="flex-1" min-w-0>
90+
<ModulesPathSelectorItem
91+
v-model:search="startSelector.state.value.search"
92+
placeholder="Start"
93+
:selector="startSelector"
94+
:session="session"
95+
:modules="startSelector.modules.value"
96+
@clear="() => { startSelector.clear(); endSelector.clear() }"
97+
/>
98+
<div class="i-carbon-arrow-right op50" flex-shrink-0 />
99+
100+
<ModulesPathSelectorItem
101+
v-model:search="endSelector.state.value.search"
102+
placeholder="End"
103+
:selector="endSelector"
104+
:session="session"
105+
:modules="filteredEndModules"
106+
@clear="endSelector.clear"
107+
>
108+
<template #empty>
109+
<div flex="~ items-center justify-center" w-full h-20>
110+
<span italic op50>
111+
{{ startSelector.state.value.selected ? 'No modules' : 'Select a start module to get end modules' }}
112+
</span>
113+
</div>
114+
</template>
115+
</ModulesPathSelectorItem>
116+
</div>
117+
118+
<DisplayCloseButton class="mr--2" @click="emit('close')" />
119+
</div>
120+
</template>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<script setup lang="ts">
2+
import type { ModuleListItem, SessionContext } from '~~/shared/types'
3+
import type { ModulePathSelector } from '~/composables/moduleGraph'
4+
import { hideAllPoppers, Menu as VMenu } from 'floating-vue'
5+
6+
withDefaults(
7+
defineProps<{
8+
selector: ModulePathSelector
9+
placeholder: string
10+
session: SessionContext
11+
modules?: ModuleListItem[]
12+
emptyStateText?: string
13+
onClear?: () => void
14+
}>(),
15+
{
16+
modules: undefined,
17+
emptyStateText: undefined,
18+
onClear: undefined,
19+
},
20+
)
21+
22+
const emit = defineEmits<{
23+
(e: 'clear'): void
24+
}>()
25+
26+
const search = defineModel<string>('search', { required: true })
27+
</script>
28+
29+
<template>
30+
<div flex-1 w-0>
31+
<div v-if="selector.state.value.selected" w-full overflow-hidden flex="~ items-center" border="~ base rounded" p1 relative>
32+
<div overflow-hidden text-ellipsis pr6 py0.5 w-0 flex-1>
33+
<DisplayModuleId
34+
:id="selector.state.value.selected"
35+
:session="session"
36+
block text-nowrap
37+
:link="false"
38+
:disable-tooltip="true"
39+
/>
40+
</div>
41+
<button i-carbon-clean text-4 hover="op100" op50 title="Clear" absolute right-2 @click="emit('clear')" />
42+
</div>
43+
<VMenu v-else :distance="15" :triggers="['click']" :auto-hide="false" :delay="{ show: 300, hide: 150 }">
44+
<input
45+
v-model="search"
46+
p1
47+
px4 w-full border="~ base rounded-1" style="outline: none" :placeholder="placeholder"
48+
@blur="hideAllPoppers"
49+
>
50+
<template #popper>
51+
<div class="p2 w100" flex="~ col gap2">
52+
<ModulesFlatList
53+
v-if="modules?.length"
54+
:session="session"
55+
:modules="modules"
56+
disable-tooltip
57+
:link="false"
58+
@select="selector.select"
59+
/>
60+
<slot v-else name="empty" />
61+
</div>
62+
</template>
63+
</VMenu>
64+
</div>
65+
</template>

packages/vite/src/app/composables/moduleGraph.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import type { ComputedRefWithControl } from '@vueuse/core'
12
import type { HierarchyLink, HierarchyNode } from 'd3-hierarchy'
23
import type { ComputedRef, InjectionKey, MaybeRef, Ref, ShallowReactive, ShallowRef } from 'vue'
3-
import { onKeyPressed, useEventListener, useMagicKeys } from '@vueuse/core'
4+
import type { ModuleListItem } from '~~/shared/types'
5+
import { computedWithControl, onKeyPressed, useEventListener, useMagicKeys } from '@vueuse/core'
46
import { hierarchy, tree } from 'd3-hierarchy'
57
import { linkHorizontal, linkVertical } from 'd3-shape'
8+
import Fuse from 'fuse.js'
69
import { computed, inject, nextTick, provide, ref, shallowReactive, shallowRef, unref } from 'vue'
710
import { useZoomElement } from './zoomElement'
811

@@ -352,3 +355,57 @@ export function useGraphDraggingScroll() {
352355
isGrabbing,
353356
}
354357
}
358+
359+
export interface ModulePathSelector {
360+
state: { value: { search: string, selected: string | null } }
361+
modules: ComputedRef<ModuleListItem[]>
362+
fuse: Ref<ComputedRefWithControl<Fuse<ModuleListItem>> | undefined>
363+
initSelector: (modules: ComputedRef<ModuleListItem[]>) => void
364+
select: (module: ModuleListItem) => void
365+
clear: () => void
366+
}
367+
368+
export function useModulePathSelector(options: {
369+
getModules: () => ModuleListItem[]
370+
}): ModulePathSelector {
371+
const state = ref<{
372+
search: string
373+
selected: string | null
374+
}>({
375+
search: '',
376+
selected: null,
377+
})
378+
const fuse = ref<ComputedRefWithControl<Fuse<ModuleListItem>>>()
379+
380+
const modules = computed(options.getModules)
381+
382+
function initSelector(modules: ComputedRef<ModuleListItem[]>) {
383+
fuse.value = computedWithControl(
384+
modules,
385+
() => new Fuse(modules.value, {
386+
includeScore: true,
387+
keys: ['id'],
388+
ignoreLocation: true,
389+
threshold: 0.4,
390+
}),
391+
)
392+
}
393+
394+
function select(node: ModuleListItem) {
395+
state.value.selected = node.id
396+
state.value.search = ''
397+
}
398+
399+
function clear() {
400+
state.value.selected = null
401+
}
402+
403+
return {
404+
state,
405+
modules,
406+
fuse,
407+
initSelector,
408+
select,
409+
clear,
410+
}
411+
}

0 commit comments

Comments
 (0)