fix: 修复内容筛选即时切换并将警告改为仅客户端
This commit is contained in:
1 parent
c2bc7c2441
commit
91f483b960
7 files changed
+179
-20
No files matched your search
@@ -38,4 +38,5 @@ export function canToggleContentItem(item: ContentItem): boolean {
|
||||
export interface ContentFilterOption {
|
||||
id: string
|
||||
label: string
|
||||
tooltip?: string
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { registerHooks } from 'node:module'
|
||||
import test, { type TestContext } from 'node:test'
|
||||
|
||||
import { effectScope, nextTick, ref } from 'vue'
|
||||
|
||||
import type { ContentItem } from '../types.ts'
|
||||
|
||||
// Keep the real Vue state, search, and filter pipeline; replace only the UI's
|
||||
// translation provider, which normally requires an application context.
|
||||
const hooks = registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier === '#ui/composables/i18n' || specifier === '../composables/i18n') {
|
||||
return {
|
||||
url: `data:text/javascript,${encodeURIComponent('export const defineMessage=x=>x, defineMessages=x=>x, useVIntl=()=>({formatMessage:x=>x.defaultMessage})')}`,
|
||||
shortCircuit: true,
|
||||
}
|
||||
}
|
||||
if (specifier.startsWith('#ui/')) {
|
||||
return nextResolve(
|
||||
new URL(`../../../../${specifier.slice(4)}.ts`, import.meta.url).href,
|
||||
context,
|
||||
)
|
||||
}
|
||||
if (specifier.startsWith('./') && !specifier.endsWith('.ts')) {
|
||||
return nextResolve(`${specifier}.ts`, context)
|
||||
}
|
||||
return nextResolve(specifier, context)
|
||||
},
|
||||
})
|
||||
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: {} })
|
||||
const { useContentPipeline } = await import('./content-pipeline.ts')
|
||||
hooks.deregister()
|
||||
if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow)
|
||||
else Reflect.deleteProperty(globalThis, 'window')
|
||||
|
||||
function item(id: string, overrides: Partial<ContentItem> = {}): ContentItem {
|
||||
return {
|
||||
id,
|
||||
project: { id, title: id, slug: id, icon_url: null },
|
||||
file_name: `${id}.jar`,
|
||||
project_type: 'mod',
|
||||
update: null,
|
||||
origin_provider: null,
|
||||
provider_refs: [],
|
||||
enabled: true,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function fixture(t: TestContext) {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const update = {
|
||||
provider: 'modrinth' as const,
|
||||
project_id: 'project',
|
||||
current_version_id: 'v1',
|
||||
target_version_id: 'v2',
|
||||
}
|
||||
const items = ref([
|
||||
item('ordinary'),
|
||||
item('update', { update }),
|
||||
item('warning', { environment: 'client_only' }),
|
||||
item('both', { update, pack_client_depends: true }),
|
||||
item('disabled', { enabled: false }),
|
||||
])
|
||||
const scope = effectScope()
|
||||
t.after(() => scope.stop())
|
||||
const pipeline = scope.run(() =>
|
||||
useContentPipeline({
|
||||
items,
|
||||
duplicateItems: ref([]),
|
||||
sortItems: (items) => items,
|
||||
getItemId: (item) => item.id,
|
||||
showUpdateFilter: true,
|
||||
showWarningsFilter: true,
|
||||
}),
|
||||
)!
|
||||
return {
|
||||
...pipeline,
|
||||
ids: () => pipeline.filteredItems.value.map((item) => item.id),
|
||||
async flush() {
|
||||
await nextTick()
|
||||
t.mock.timers.tick(100)
|
||||
await nextTick()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test('status filters update the visible list on every click without reloading items', async (t) => {
|
||||
const p = fixture(t)
|
||||
await p.flush()
|
||||
assert.equal(p.ids().length, 5)
|
||||
p.toggleStatusFilter('updates')
|
||||
await p.flush()
|
||||
assert.deepEqual(p.ids(), ['update', 'both'])
|
||||
p.toggleStatusFilter('warnings')
|
||||
await p.flush()
|
||||
assert.deepEqual(p.ids(), ['both'])
|
||||
p.toggleStatusFilter('updates')
|
||||
await p.flush()
|
||||
assert.deepEqual(p.ids(), ['warning', 'both'])
|
||||
p.toggleStatusFilter('warnings')
|
||||
await p.flush()
|
||||
assert.equal(p.ids().length, 5)
|
||||
})
|
||||
|
||||
test('All clears status filters and restores the visible list without a refresh', async (t) => {
|
||||
const p = fixture(t)
|
||||
await p.flush()
|
||||
p.toggleStatusFilter('warnings')
|
||||
await p.flush()
|
||||
assert.deepEqual(p.ids(), ['warning', 'both'])
|
||||
p.selectedStatusFilters.value = []
|
||||
await p.flush()
|
||||
assert.equal(p.ids().length, 5)
|
||||
})
|
||||
|
||||
test('enabled and disabled remain exclusive and can each be deselected', async (t) => {
|
||||
const p = fixture(t)
|
||||
await p.flush()
|
||||
p.toggleStatusFilter('enabled')
|
||||
await p.flush()
|
||||
assert.deepEqual(p.ids(), ['ordinary', 'update', 'warning', 'both'])
|
||||
p.toggleStatusFilter('enabled')
|
||||
await p.flush()
|
||||
assert.equal(p.ids().length, 5)
|
||||
p.toggleStatusFilter('enabled')
|
||||
p.toggleStatusFilter('disabled')
|
||||
await p.flush()
|
||||
assert.deepEqual(p.selectedStatusFilters.value, ['disabled'])
|
||||
assert.deepEqual(p.ids(), ['disabled'])
|
||||
p.toggleStatusFilter('disabled')
|
||||
await p.flush()
|
||||
assert.equal(p.ids().length, 5)
|
||||
})
|
||||
@@ -62,9 +62,14 @@ const filterMessages = defineMessages({
|
||||
id: 'content.filter.updates',
|
||||
defaultMessage: 'Update available',
|
||||
},
|
||||
warnings: {
|
||||
id: 'content.filter.warnings',
|
||||
defaultMessage: 'Warnings',
|
||||
clientOnly: {
|
||||
id: 'content.filter.client-only',
|
||||
defaultMessage: 'Client only',
|
||||
},
|
||||
clientOnlyDescription: {
|
||||
id: 'content.filter.client-only-description',
|
||||
defaultMessage:
|
||||
'Includes client-only or singleplayer-only mods and mods that depend on them. These may be incompatible with dedicated servers.',
|
||||
},
|
||||
duplicates: {
|
||||
id: 'content.filter.duplicates',
|
||||
@@ -329,7 +334,11 @@ export function useContentPipeline(config: ContentPipelineConfig) {
|
||||
row2.push({ id: 'updates', label: formatMessage(filterMessages.updates) })
|
||||
}
|
||||
if (showWarningsFilter && typeFiltered.some((m) => getClientWarningType(m) !== null)) {
|
||||
row2.push({ id: 'warnings', label: formatMessage(filterMessages.warnings) })
|
||||
row2.push({
|
||||
id: 'warnings',
|
||||
label: formatMessage(filterMessages.clientOnly),
|
||||
tooltip: formatMessage(filterMessages.clientOnlyDescription),
|
||||
})
|
||||
}
|
||||
|
||||
if (hasEnabled && hasDisabled) {
|
||||
@@ -472,26 +481,20 @@ export function useContentPipeline(config: ContentPipelineConfig) {
|
||||
}
|
||||
|
||||
function toggleStatusFilter(filterId: string) {
|
||||
if (filterId === 'enabled' || filterId === 'disabled') {
|
||||
const index = selectedStatusFilters.value.indexOf(filterId)
|
||||
const otherStatusFilter = filterId === 'enabled' ? 'disabled' : 'enabled'
|
||||
if (index === -1) {
|
||||
selectedStatusFilters.value = [
|
||||
...selectedStatusFilters.value.filter((f) => f !== otherStatusFilter),
|
||||
filterId,
|
||||
]
|
||||
} else {
|
||||
selectedStatusFilters.value.splice(index, 1)
|
||||
}
|
||||
const filters = selectedStatusFilters.value
|
||||
// Replace the array so both the pipeline and saved-filter watchers are notified.
|
||||
if (filters.includes(filterId)) {
|
||||
selectedStatusFilters.value = filters.filter((id) => id !== filterId)
|
||||
return
|
||||
}
|
||||
|
||||
const index = selectedStatusFilters.value.indexOf(filterId)
|
||||
if (index === -1) {
|
||||
selectedStatusFilters.value.push(filterId)
|
||||
} else {
|
||||
selectedStatusFilters.value.splice(index, 1)
|
||||
if (filterId === 'enabled' || filterId === 'disabled') {
|
||||
const otherStatusFilter = filterId === 'enabled' ? 'disabled' : 'enabled'
|
||||
selectedStatusFilters.value = [...filters.filter((id) => id !== otherStatusFilter), filterId]
|
||||
return
|
||||
}
|
||||
|
||||
selectedStatusFilters.value = [...filters, filterId]
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1110,6 +1110,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
<button
|
||||
v-for="option in row2FilterOptions"
|
||||
:key="option.id"
|
||||
v-tooltip="option.tooltip"
|
||||
class="relative pb-1 text-base font-semibold transition-colors"
|
||||
:class="
|
||||
selectedStatusFilters.includes(option.id)
|
||||
|
||||
@@ -872,6 +872,12 @@
|
||||
"content.filter.updates": {
|
||||
"defaultMessage": "Update available"
|
||||
},
|
||||
"content.filter.client-only": {
|
||||
"defaultMessage": "Client only"
|
||||
},
|
||||
"content.filter.client-only-description": {
|
||||
"defaultMessage": "Includes client-only or singleplayer-only mods and mods that depend on them. These may be incompatible with dedicated servers."
|
||||
},
|
||||
"content.filter.warnings": {
|
||||
"defaultMessage": "Warnings"
|
||||
},
|
||||
|
||||
@@ -848,6 +848,12 @@
|
||||
"content.filter.updates": {
|
||||
"defaultMessage": "可更新"
|
||||
},
|
||||
"content.filter.client-only": {
|
||||
"defaultMessage": "仅客户端"
|
||||
},
|
||||
"content.filter.client-only-description": {
|
||||
"defaultMessage": "包含仅供客户端或单人游戏使用的模组,以及依赖它们的模组。这些模组可能不兼容独立服务器。"
|
||||
},
|
||||
"content.filter.warnings": {
|
||||
"defaultMessage": "警告"
|
||||
},
|
||||
|
||||
@@ -698,6 +698,12 @@
|
||||
"content.filter.updates": {
|
||||
"defaultMessage": "更新"
|
||||
},
|
||||
"content.filter.client-only": {
|
||||
"defaultMessage": "僅用戶端"
|
||||
},
|
||||
"content.filter.client-only-description": {
|
||||
"defaultMessage": "包含僅供用戶端或單人遊戲使用的模組,以及依賴它們的模組。這些模組可能不相容於獨立伺服器。"
|
||||
},
|
||||
"content.filter.warnings": {
|
||||
"defaultMessage": "警告"
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user