|
1 | 1 | <script setup lang="ts"> |
2 | 2 | import type { ModuleListItem, SessionContext } from '~~/shared/types' |
| 3 | +import { computedWithControl } from '@vueuse/core' |
3 | 4 | import { hideAllPoppers, Menu as VMenu } from 'floating-vue' |
| 5 | +import Fuse from 'fuse.js' |
| 6 | +import { computed, ref, watch } from 'vue' |
4 | 7 |
|
5 | | -defineProps<{ |
| 8 | +const props = defineProps<{ |
6 | 9 | session: SessionContext |
7 | 10 | modules: ModuleListItem[] |
8 | 11 | }>() |
9 | 12 |
|
10 | 13 | const emit = defineEmits<{ |
11 | 14 | (e: 'close'): void |
| 15 | + (e: 'select', nodes: { start: string, end: string }): void |
12 | 16 | }>() |
13 | 17 |
|
14 | | -function selectNode(node: ModuleListItem) { |
15 | | - return node |
| 18 | +const searchStart = ref<{ |
| 19 | + search: string |
| 20 | + selected: string | null |
| 21 | +}>({ |
| 22 | + search: '', |
| 23 | + selected: null, |
| 24 | +}) |
| 25 | +
|
| 26 | +const modulesMap = computed(() => { |
| 27 | + const map = new Map<string, ModuleListItem>() |
| 28 | + props.modules.forEach((m) => { |
| 29 | + map.set(m.id, m) |
| 30 | + }) |
| 31 | + return map |
| 32 | +}) |
| 33 | +
|
| 34 | +const startFuse = computedWithControl( |
| 35 | + () => props.modules, |
| 36 | + () => new Fuse(props.modules, { |
| 37 | + includeScore: true, |
| 38 | + keys: ['id'], |
| 39 | + ignoreLocation: true, |
| 40 | + threshold: 0.4, |
| 41 | + }), |
| 42 | +) |
| 43 | +
|
| 44 | +const startModules = computed(() => { |
| 45 | + if (!searchStart.value.search) { |
| 46 | + return props.modules |
| 47 | + } |
| 48 | + else { |
| 49 | + return startFuse.value |
| 50 | + .search(searchStart.value.search) |
| 51 | + .map(r => r.item) |
| 52 | + } |
| 53 | +}) |
| 54 | +
|
| 55 | +const endModules = computed(() => { |
| 56 | + function getAllImports(moduleId: string, visited = new Set<string>()): ModuleListItem[] { |
| 57 | + if (visited.has(moduleId)) |
| 58 | + return [] |
| 59 | + visited.add(moduleId) |
| 60 | +
|
| 61 | + const module = modulesMap.value.get(moduleId) |
| 62 | + if (!module?.imports?.length) |
| 63 | + return [] |
| 64 | +
|
| 65 | + const res: ModuleListItem[] = [] |
| 66 | +
|
| 67 | + for (const importItem of module.imports) { |
| 68 | + const importedModule = modulesMap.value.get(importItem.module_id) |
| 69 | + if (!importedModule) |
| 70 | + continue |
| 71 | +
|
| 72 | + if (!visited.has(importedModule.id)) { |
| 73 | + res.push(importedModule) |
| 74 | + res.push(...getAllImports(importedModule.id, visited)) |
| 75 | + } |
| 76 | + } |
| 77 | +
|
| 78 | + return res |
| 79 | + } |
| 80 | +
|
| 81 | + return searchStart.value.selected ? getAllImports(searchStart.value.selected) : [] |
| 82 | +}) |
| 83 | +
|
| 84 | +const endFuse = computedWithControl( |
| 85 | + () => endModules.value, |
| 86 | + () => new Fuse(endModules.value, { |
| 87 | + includeScore: true, |
| 88 | + keys: ['id'], |
| 89 | + ignoreLocation: true, |
| 90 | + threshold: 0.4, |
| 91 | + }), |
| 92 | +) |
| 93 | +
|
| 94 | +const searchEnd = ref<{ |
| 95 | + search: string |
| 96 | + selected: string | null |
| 97 | +}>({ |
| 98 | + search: '', |
| 99 | + selected: null, |
| 100 | +}) |
| 101 | +
|
| 102 | +const filteredEndModules = computed(() => { |
| 103 | + if (!searchEnd.value.search) { |
| 104 | + return endModules.value |
| 105 | + } |
| 106 | + else { |
| 107 | + return endFuse.value |
| 108 | + .search(searchEnd.value.search) |
| 109 | + .map(r => r.item) |
| 110 | + } |
| 111 | +}) |
| 112 | +
|
| 113 | +function selectStartNode(node: ModuleListItem) { |
| 114 | + searchStart.value.selected = node.id |
| 115 | + searchStart.value.search = '' |
| 116 | +} |
| 117 | +
|
| 118 | +function selectEndNode(node: ModuleListItem) { |
| 119 | + searchEnd.value.selected = node.id |
| 120 | + searchEnd.value.search = '' |
| 121 | +} |
| 122 | +
|
| 123 | +function clearSelected(type: 'start' | 'end') { |
| 124 | + if (type === 'start') { |
| 125 | + searchStart.value.selected = null |
| 126 | + } |
| 127 | + searchEnd.value.selected = null |
16 | 128 | } |
| 129 | +
|
| 130 | +watch([() => searchStart.value.selected, () => searchEnd.value.selected], () => { |
| 131 | + emit('select', { |
| 132 | + start: searchStart.value.selected ?? '', |
| 133 | + end: searchEnd.value.selected ?? '', |
| 134 | + }) |
| 135 | +}) |
17 | 136 | </script> |
18 | 137 |
|
19 | 138 | <template> |
20 | 139 | <div h12 px4 p2 relative flex="~ gap2 items-center"> |
21 | | - <VMenu inline :distance="15" :triggers="['click']" :auto-hide="false" :delay="{ show: 200, hide: 0 }"> |
22 | | - <input |
23 | | - p1 px4 border="~ base rounded-1" style="outline: none" |
24 | | - placeholder="Start" |
25 | | - @blur="hideAllPoppers" |
26 | | - > |
27 | | - <template #popper> |
28 | | - <div class="p2 w100" flex="~ col gap2"> |
29 | | - <ModulesFlatList |
30 | | - :session="session" |
31 | | - :modules="modules" |
32 | | - disable-tooltip |
33 | | - :link="false" |
34 | | - @select="selectNode" |
35 | | - /> |
| 140 | + <div flex="~ items-center gap2" class="flex-1" min-w-0> |
| 141 | + <div flex-1 w-0> |
| 142 | + <div v-if="searchStart.selected" w-full overflow-hidden flex="~ items-center" border="~ base rounded" p1 relative> |
| 143 | + <div overflow-hidden text-ellipsis pr6 py0.5 w-0 flex-1> |
| 144 | + <DisplayModuleId |
| 145 | + :id="searchStart.selected" |
| 146 | + :session="session" |
| 147 | + block text-nowrap |
| 148 | + :link="false" |
| 149 | + :disable-tooltip="true" |
| 150 | + /> |
| 151 | + </div> |
| 152 | + <button i-carbon-clean text-4 hover="op100" op50 title="Clear" absolute right-2 @click="clearSelected('start')" /> |
36 | 153 | </div> |
37 | | - </template> |
38 | | - </VMenu> |
39 | | - <div class="i-carbon-arrow-right op50" /> |
40 | | - <VMenu inline :distance="15" :triggers="['click']" :auto-hide="false" :delay="{ show: 200, hide: 0 }"> |
41 | | - <input |
42 | | - p1 px4 border="~ base rounded-1" style="outline: none" |
43 | | - placeholder="End" |
44 | | - @blur="hideAllPoppers" |
45 | | - > |
46 | | - <template #popper> |
47 | | - <div class="p2 w100" flex="~ col gap2"> |
48 | | - <ModulesFlatList |
49 | | - :session="session" |
50 | | - :modules="modules" |
51 | | - disable-tooltip |
52 | | - :link="false" |
53 | | - @select="selectNode" |
54 | | - /> |
| 154 | + <VMenu v-else :distance="15" :triggers="['click']" :auto-hide="false" :delay="{ show: 300, hide: 120 }"> |
| 155 | + <input |
| 156 | + v-model="searchStart.search" |
| 157 | + p1 px4 w-full border="~ base rounded-1" style="outline: none" |
| 158 | + placeholder="Start" |
| 159 | + @blur="hideAllPoppers" |
| 160 | + > |
| 161 | + <template #popper> |
| 162 | + <div class="p2 w100" flex="~ col gap2"> |
| 163 | + <ModulesFlatList |
| 164 | + :session="session" |
| 165 | + :modules="startModules" |
| 166 | + disable-tooltip |
| 167 | + :link="false" |
| 168 | + @select="selectStartNode" |
| 169 | + /> |
| 170 | + </div> |
| 171 | + </template> |
| 172 | + </VMenu> |
| 173 | + </div> |
| 174 | + <div class="i-carbon-arrow-right op50" flex-shrink-0 /> |
| 175 | + |
| 176 | + <div flex-1 w-0> |
| 177 | + <div v-if="searchEnd.selected" w-full overflow-hidden flex="~ items-center" border="~ base rounded" p1 relative> |
| 178 | + <div overflow-hidden text-ellipsis pr6 py0.5 w-0 flex-1> |
| 179 | + <DisplayModuleId |
| 180 | + :id="searchEnd.selected" |
| 181 | + :session="session" |
| 182 | + block text-nowrap |
| 183 | + :link="false" |
| 184 | + :disable-tooltip="true" |
| 185 | + /> |
| 186 | + </div> |
| 187 | + <button i-carbon-clean text-4 hover="op100" op50 title="Clear" absolute right-2 @click="clearSelected('end')" /> |
55 | 188 | </div> |
56 | | - </template> |
57 | | - </VMenu> |
| 189 | + <VMenu v-else :distance="15" :triggers="['click']" :auto-hide="false" :delay="{ show: 300, hide: 120 }"> |
| 190 | + <input |
| 191 | + v-model="searchEnd.search" |
| 192 | + p1 px4 w-full border="~ base rounded-1" style="outline: none" |
| 193 | + placeholder="End" |
| 194 | + @blur="hideAllPoppers" |
| 195 | + > |
| 196 | + <template #popper> |
| 197 | + <div class="p2 w100" flex="~ col gap2"> |
| 198 | + <ModulesFlatList |
| 199 | + v-if="filteredEndModules.length" |
| 200 | + :session="session" |
| 201 | + :modules="filteredEndModules" |
| 202 | + disable-tooltip |
| 203 | + :link="false" |
| 204 | + @select="selectEndNode" |
| 205 | + /> |
| 206 | + <div v-else flex="~ items-center justify-center" w-full h-20> |
| 207 | + <span italic op50> |
| 208 | + No modules |
| 209 | + </span> |
| 210 | + </div> |
| 211 | + </div> |
| 212 | + </template> |
| 213 | + </VMenu> |
| 214 | + </div> |
| 215 | + </div> |
58 | 216 |
|
59 | | - <DisplayCloseButton class="absolute right-2" @click="emit('close')" /> |
| 217 | + <DisplayCloseButton class="mr--2" @click="emit('close')" /> |
60 | 218 | </div> |
61 | 219 | </template> |
0 commit comments