Skip to content

Commit 4478015

Browse files
authored
feat(modules): dynamic sorting for module installation list (#470)
1 parent 87ec2ff commit 4478015

4 files changed

Lines changed: 84 additions & 13 deletions

File tree

packages/devtools-kit/src/_types/integrations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export interface ModuleStaticInfo {
122122
learn_more: string
123123
category: string
124124
type: ModuleType
125+
stats: ModuleStats
125126
maintainers: MaintainerInfo[]
126127
contributors: GitHubContributor[]
127128
compatibility: ModuleCompatibility
@@ -132,6 +133,13 @@ export interface ModuleCompatibility {
132133
requires: { bridge?: boolean | 'optional' }
133134
}
134135

136+
export interface ModuleStats {
137+
downloads: number
138+
stars: number
139+
publishedAt: number
140+
createdAt: number
141+
}
142+
135143
export type CompatibilityStatus = 'working' | 'wip' | 'unknown' | 'not-working'
136144
export type ModuleType = 'community' | 'official' | '3rd-party'
137145

packages/devtools/client/components/ModuleInstallList.vue

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,30 @@
22
// @ts-expect-error missing types
33
import { RecycleScroller } from 'vue-virtual-scroller'
44
import Fuse from 'fuse.js'
5+
import type { ModuleStaticInfo } from '../../src/types'
6+
7+
type SortingFunction<T> = (a: T, b: T) => number
58
69
const emit = defineEmits(['close'])
710
811
const collection = useModulesList()
912
13+
const sortingOptions = ['downloads', 'stars', 'updated', 'created'] as const
14+
const ascendingOrder = ref(false)
15+
const selectedSortingOption = ref<typeof sortingOptions[number]>(sortingOptions[0])
16+
17+
const sortingFactors: Record<typeof sortingOptions[number], SortingFunction<ModuleStaticInfo>> = {
18+
downloads: (a, b) => a.stats.downloads - b.stats.downloads,
19+
stars: (a, b) => a.stats.stars - b.stats.stars,
20+
created: (a, b) => a.stats.createdAt - b.stats.createdAt,
21+
updated: (a, b) => a.stats.publishedAt - b.stats.publishedAt,
22+
}
23+
24+
const sortedItems = computed(() => collection.value?.slice()
25+
.sort((a, b) => sortingFactors[selectedSortingOption.value](a, b) * (ascendingOrder.value ? 1 : -1)))
26+
1027
const search = ref('')
11-
const fuse = computed(() => new Fuse(collection.value || [], {
28+
const fuse = computed(() => new Fuse(sortedItems.value || [], {
1229
keys: [
1330
'name',
1431
'description',
@@ -19,7 +36,7 @@ const fuse = computed(() => new Fuse(collection.value || [], {
1936
2037
const items = computed(() => {
2138
if (!search.value)
22-
return collection.value
39+
return sortedItems.value
2340
return fuse.value.search(search.value).map(r => r.item)
2441
})
2542
</script>
@@ -33,20 +50,51 @@ const items = computed(() => {
3350
text="Install Module"
3451
/>
3552

36-
<NTextInput
37-
v-model="search"
38-
:autofocus="true"
39-
placeholder="Search..."
40-
icon="carbon-search" n="primary"
41-
mx6 px-5 py-2
42-
/>
53+
<NNavbar v-model:search="search" no-padding px-6 pb-5 pt-2>
54+
<template #actions>
55+
<NDropdown direction="end" n="sm primary">
56+
<template #trigger="{ click }">
57+
<div flex="~ items-center">
58+
<NButton
59+
:icon="ascendingOrder ? 'tabler:sort-ascending' : 'tabler:sort-descending'"
60+
h-full rounded-r-none
61+
@click="ascendingOrder = !ascendingOrder"
62+
/>
63+
<NButton
64+
flex="~ justify-between"
65+
min-w-30 border-l-0 rounded-l-none px-2 capitalize
66+
hover="border-l-1"
67+
@click="click()"
68+
>
69+
{{ selectedSortingOption }}
70+
<NIcon icon="carbon:chevron-down" />
71+
</NButton>
72+
</div>
73+
</template>
74+
<div flex="~ col" w-30 of-auto>
75+
<NButton
76+
v-for="item of sortingOptions"
77+
:key="item"
78+
:border="false" p2
79+
hover="n-checkbox-hover text-green"
80+
@click="selectedSortingOption = item"
81+
>
82+
<span flex="~ justify-between" w-full text-xs capitalize op75>
83+
{{ item }}
84+
<NIcon v-if="selectedSortingOption === item" icon="carbon:checkmark" />
85+
</span>
86+
</NButton>
87+
</div>
88+
</NDropdown>
89+
</template>
90+
</NNavbar>
4391

4492
<div flex-auto of-auto flex="~ col gap-2" pl6 pr4>
4593
<RecycleScroller
4694
v-slot="{ item }"
4795
class="scroller"
4896
:items="items"
49-
:item-size="160"
97+
:item-size="200"
5098
key-field="name"
5199
>
52100
<ModuleItemInstall :item="item" @start="emit('close')" />

packages/devtools/client/components/ModuleItemBase.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ const openInEditor = useOpenInEditor()
8989
</slot>
9090

9191
<slot name="items" />
92+
93+
<div v-if="data.stats" flex="~ gap-4 items-center">
94+
<div flex="~ gap-1 items-center" op50>
95+
<NIcon icon="carbon-star" text-lg />
96+
<span>
97+
{{ data.stats.stars }}
98+
</span>
99+
</div>
100+
<div flex="~ gap-1 items-center" op50>
101+
<NIcon icon="carbon-download" text-lg />
102+
<span>
103+
{{ data.stats.downloads }}
104+
</span>
105+
</div>
106+
</div>
92107
</div>
93108
<div flex="~ col" items-end>
94109
<div

packages/devtools/client/composables/state-modules.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ const ignoredModules = [
1212

1313
export function useModulesList() {
1414
return useAsyncState('getModulesList', async () => {
15-
const modules = await $fetch<ModuleStaticInfo[]>('https://cdn.jsdelivr.net/npm/@nuxt/modules@latest/modules.json')
16-
return modules
17-
.filter((m: ModuleStaticInfo) => !ignoredModules.includes(m.npm) && m.compatibility.nuxt.includes('^3'))
15+
const m = await $fetch<{ modules: ModuleStaticInfo[] }>('https://api.nuxt.com/modules?version=3')
16+
return m.modules
17+
.filter((item: ModuleStaticInfo) => !ignoredModules.includes(item.npm) && item.compatibility.nuxt.includes('^3'))
1818
})
1919
}
2020

0 commit comments

Comments
 (0)