mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-16 16:07:45 +08:00
feat(providers): pin associated models to the top of the associate dialog
Show already checked / already associated models first in the 关联模型 list so they are easy to find and uncheck. Keep that order inside the current search results.
This commit is contained in:
@@ -98,6 +98,7 @@
|
||||
v-for="model in filteredGlobalModels"
|
||||
:key="model.id"
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
|
||||
:data-testid="`batch-assign-model-${model.id}`"
|
||||
@click="toggleGlobalModelSelection(model.id)"
|
||||
>
|
||||
<div
|
||||
@@ -255,15 +256,29 @@ const existingGlobalModelIds = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// 过滤后的全局模型
|
||||
function globalModelMatchesQuery(model: GlobalModelResponse, query: string): boolean {
|
||||
if (!query) return true
|
||||
return model.name.toLowerCase().includes(query) || model.display_name.toLowerCase().includes(query)
|
||||
}
|
||||
|
||||
function compareGlobalModelsByName(left: GlobalModelResponse, right: GlobalModelResponse): number {
|
||||
const nameA = (left.display_name || left.name || '').toLowerCase()
|
||||
const nameB = (right.display_name || right.name || '').toLowerCase()
|
||||
return nameA.localeCompare(nameB)
|
||||
}
|
||||
|
||||
// 过滤后的全局模型:当前已勾选/已关联的排在可见结果顶部,便于取消关联
|
||||
const filteredGlobalModels = computed(() => {
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
return allGlobalModels.value.filter(m => {
|
||||
if (query && !m.name.toLowerCase().includes(query) && !m.display_name.toLowerCase().includes(query)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
const selectedIds = selectedGlobalModelIds.value
|
||||
const matched = allGlobalModels.value.filter(model => globalModelMatchesQuery(model, query))
|
||||
const pinned = matched
|
||||
.filter(model => selectedIds.has(model.id))
|
||||
.sort(compareGlobalModelsByName)
|
||||
const rest = matched
|
||||
.filter(model => !selectedIds.has(model.id))
|
||||
.sort(compareGlobalModelsByName)
|
||||
return [...pinned, ...rest]
|
||||
})
|
||||
|
||||
// 全局模型是否全选
|
||||
|
||||
+95
@@ -84,6 +84,36 @@ afterEach(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function createGlobalModel(id: string, name: string, displayName = name) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
display_name: displayName,
|
||||
is_active: true,
|
||||
default_tiered_pricing: { tiers: [] },
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
}
|
||||
}
|
||||
|
||||
function createProviderModel(id: string, globalModelId: string) {
|
||||
return {
|
||||
id,
|
||||
provider_id: 'provider-1',
|
||||
global_model_id: globalModelId,
|
||||
provider_model_name: globalModelId,
|
||||
is_active: true,
|
||||
is_available: true,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
}
|
||||
}
|
||||
|
||||
function visibleModelIds(root: HTMLElement): string[] {
|
||||
return Array.from(root.querySelectorAll('[data-testid^="batch-assign-model-"]'))
|
||||
.map(node => node.getAttribute('data-testid')?.replace('batch-assign-model-', '') ?? '')
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
describe('BatchAssignModelsDialog loading', () => {
|
||||
it('loads model choices when lazily mounted in the open state', async () => {
|
||||
const root = document.createElement('div')
|
||||
@@ -107,4 +137,69 @@ describe('BatchAssignModelsDialog loading', () => {
|
||||
expect(endpointMocks.getProviderModels).toHaveBeenCalledWith('provider-1')
|
||||
expect(endpointMocks.getProviderKeys).toHaveBeenCalledWith('provider-1')
|
||||
})
|
||||
|
||||
it('pins already associated models to the top of the list', async () => {
|
||||
globalModelMocks.getGlobalModels.mockResolvedValue({
|
||||
models: [
|
||||
createGlobalModel('gm-zeta', 'zeta-model', 'Zeta'),
|
||||
createGlobalModel('gm-alpha', 'alpha-model', 'Alpha'),
|
||||
createGlobalModel('gm-mu', 'mu-model', 'Mu'),
|
||||
],
|
||||
total: 3,
|
||||
})
|
||||
endpointMocks.getProviderModels.mockResolvedValue([
|
||||
createProviderModel('pm-mu', 'gm-mu'),
|
||||
])
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
return () => h(BatchAssignModelsDialog, {
|
||||
open: true,
|
||||
providerId: 'provider-1',
|
||||
})
|
||||
},
|
||||
}))
|
||||
app.mount(root)
|
||||
mountedApps.push({ app, root })
|
||||
await settle()
|
||||
|
||||
expect(visibleModelIds(root)).toEqual(['gm-mu', 'gm-alpha', 'gm-zeta'])
|
||||
})
|
||||
|
||||
it('keeps selected matches pinned above other search results', async () => {
|
||||
globalModelMocks.getGlobalModels.mockResolvedValue({
|
||||
models: [
|
||||
createGlobalModel('gm-beta', 'beta-flash', 'Beta Flash'),
|
||||
createGlobalModel('gm-alpha', 'alpha-flash', 'Alpha Flash'),
|
||||
createGlobalModel('gm-other', 'other-model', 'Other'),
|
||||
],
|
||||
total: 3,
|
||||
})
|
||||
endpointMocks.getProviderModels.mockResolvedValue([
|
||||
createProviderModel('pm-beta', 'gm-beta'),
|
||||
])
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
return () => h(BatchAssignModelsDialog, {
|
||||
open: true,
|
||||
providerId: 'provider-1',
|
||||
})
|
||||
},
|
||||
}))
|
||||
app.mount(root)
|
||||
mountedApps.push({ app, root })
|
||||
await settle()
|
||||
|
||||
const search = root.querySelector('input') as HTMLInputElement
|
||||
search.value = 'flash'
|
||||
search.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await settle()
|
||||
|
||||
expect(visibleModelIds(root)).toEqual(['gm-beta', 'gm-alpha'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user