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 {
|
export interface ContentFilterOption {
|
||||||
id: string
|
id: string
|
||||||
label: 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',
|
id: 'content.filter.updates',
|
||||||
defaultMessage: 'Update available',
|
defaultMessage: 'Update available',
|
||||||
},
|
},
|
||||||
warnings: {
|
clientOnly: {
|
||||||
id: 'content.filter.warnings',
|
id: 'content.filter.client-only',
|
||||||
defaultMessage: 'Warnings',
|
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: {
|
duplicates: {
|
||||||
id: 'content.filter.duplicates',
|
id: 'content.filter.duplicates',
|
||||||
@@ -329,7 +334,11 @@ export function useContentPipeline(config: ContentPipelineConfig) {
|
|||||||
row2.push({ id: 'updates', label: formatMessage(filterMessages.updates) })
|
row2.push({ id: 'updates', label: formatMessage(filterMessages.updates) })
|
||||||
}
|
}
|
||||||
if (showWarningsFilter && typeFiltered.some((m) => getClientWarningType(m) !== null)) {
|
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) {
|
if (hasEnabled && hasDisabled) {
|
||||||
@@ -472,26 +481,20 @@ export function useContentPipeline(config: ContentPipelineConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleStatusFilter(filterId: string) {
|
function toggleStatusFilter(filterId: string) {
|
||||||
if (filterId === 'enabled' || filterId === 'disabled') {
|
const filters = selectedStatusFilters.value
|
||||||
const index = selectedStatusFilters.value.indexOf(filterId)
|
// Replace the array so both the pipeline and saved-filter watchers are notified.
|
||||||
const otherStatusFilter = filterId === 'enabled' ? 'disabled' : 'enabled'
|
if (filters.includes(filterId)) {
|
||||||
if (index === -1) {
|
selectedStatusFilters.value = filters.filter((id) => id !== filterId)
|
||||||
selectedStatusFilters.value = [
|
|
||||||
...selectedStatusFilters.value.filter((f) => f !== otherStatusFilter),
|
|
||||||
filterId,
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
selectedStatusFilters.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = selectedStatusFilters.value.indexOf(filterId)
|
if (filterId === 'enabled' || filterId === 'disabled') {
|
||||||
if (index === -1) {
|
const otherStatusFilter = filterId === 'enabled' ? 'disabled' : 'enabled'
|
||||||
selectedStatusFilters.value.push(filterId)
|
selectedStatusFilters.value = [...filters.filter((id) => id !== otherStatusFilter), filterId]
|
||||||
} else {
|
return
|
||||||
selectedStatusFilters.value.splice(index, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedStatusFilters.value = [...filters, filterId]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1110,6 +1110,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
|||||||
<button
|
<button
|
||||||
v-for="option in row2FilterOptions"
|
v-for="option in row2FilterOptions"
|
||||||
:key="option.id"
|
:key="option.id"
|
||||||
|
v-tooltip="option.tooltip"
|
||||||
class="relative pb-1 text-base font-semibold transition-colors"
|
class="relative pb-1 text-base font-semibold transition-colors"
|
||||||
:class="
|
:class="
|
||||||
selectedStatusFilters.includes(option.id)
|
selectedStatusFilters.includes(option.id)
|
||||||
|
|||||||
@@ -872,6 +872,12 @@
|
|||||||
"content.filter.updates": {
|
"content.filter.updates": {
|
||||||
"defaultMessage": "Update available"
|
"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": {
|
"content.filter.warnings": {
|
||||||
"defaultMessage": "Warnings"
|
"defaultMessage": "Warnings"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -848,6 +848,12 @@
|
|||||||
"content.filter.updates": {
|
"content.filter.updates": {
|
||||||
"defaultMessage": "可更新"
|
"defaultMessage": "可更新"
|
||||||
},
|
},
|
||||||
|
"content.filter.client-only": {
|
||||||
|
"defaultMessage": "仅客户端"
|
||||||
|
},
|
||||||
|
"content.filter.client-only-description": {
|
||||||
|
"defaultMessage": "包含仅供客户端或单人游戏使用的模组,以及依赖它们的模组。这些模组可能不兼容独立服务器。"
|
||||||
|
},
|
||||||
"content.filter.warnings": {
|
"content.filter.warnings": {
|
||||||
"defaultMessage": "警告"
|
"defaultMessage": "警告"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -698,6 +698,12 @@
|
|||||||
"content.filter.updates": {
|
"content.filter.updates": {
|
||||||
"defaultMessage": "更新"
|
"defaultMessage": "更新"
|
||||||
},
|
},
|
||||||
|
"content.filter.client-only": {
|
||||||
|
"defaultMessage": "僅用戶端"
|
||||||
|
},
|
||||||
|
"content.filter.client-only-description": {
|
||||||
|
"defaultMessage": "包含僅供用戶端或單人遊戲使用的模組,以及依賴它們的模組。這些模組可能不相容於獨立伺服器。"
|
||||||
|
},
|
||||||
"content.filter.warnings": {
|
"content.filter.warnings": {
|
||||||
"defaultMessage": "警告"
|
"defaultMessage": "警告"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user