fix(classification): preserve condition groups when toggling rules

This commit is contained in:
jxxghp
2026-09-04 17:08:47 +08:00
parent 199eee0a22
commit afeae0b4ce
4 changed files with 40 additions and 5 deletions
@@ -63,9 +63,10 @@ function cloneCondition(node: ClassificationConditionNode): ClassificationCondit
}
const group = node as ClassificationConditionGroup
if (group.all !== undefined) return { all: group.all?.map(cloneCondition) ?? group.all }
if (group.any !== undefined) return { any: group.any?.map(cloneCondition) ?? group.any }
if (group.not !== undefined) return { not: group.not ? cloneCondition(group.not) : group.not }
// API 序列化会为未选中的分支保留 null;只能复制真正有值的分支,避免覆盖 any/not。
if (group.all !== undefined && group.all !== null) return { all: group.all.map(cloneCondition) }
if (group.any !== undefined && group.any !== null) return { any: group.any.map(cloneCondition) }
if (group.not !== undefined && group.not !== null) return { not: cloneCondition(group.not) }
return {}
}
@@ -210,6 +210,25 @@ describe('ClassificationRuleEditor', () => {
)
})
it('切换规则开关时保留条件组的有效分支', async () => {
const user = userEvent.setup()
const condition = {
all: null,
any: [{ field: 'media.genre_names', operator: 'contains_any', value: ['动画'] }],
not: null,
} as ClassificationRule['when']
const editor = await renderEditor([createRule({ when: condition })])
await user.click(screen.getByRole('checkbox', { name: '启用规则 电影规则' }))
expect(editor.latestRules()[0]).toEqual(
expect.objectContaining({
enabled: false,
when: { any: [{ field: 'media.genre_names', operator: 'contains_any', value: ['动画'] }] },
}),
)
})
it('按媒体类型过滤分类目标,并在目标失效时自动清空', async () => {
const user = userEvent.setup()
const editor = await renderEditor([createRule()])
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import api from '@/api'
import api, { getApiErrorMessage } from '@/api'
import type { TransferDirectoryConf } from '@/api/types'
import type {
ClassificationCategory,
@@ -361,7 +361,7 @@ async function validateCurrentDraft(): Promise<void> {
} catch (error) {
console.error(error)
validatedDraftSnapshot.value = null
toast.error(t('setting.classification.validationRequestFailed'))
toast.error(getApiErrorMessage(error) || t('setting.classification.validationRequestFailed'))
}
}
@@ -12,6 +12,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
apiGet: vi.fn(),
apiErrorMessage: vi.fn(),
analyzeImpact: vi.fn(),
initialize: vi.fn(),
loadHistory: vi.fn(),
@@ -29,6 +30,7 @@ const mocks = vi.hoisted(() => ({
vi.mock('@/api', () => ({
default: { get: mocks.apiGet },
getApiErrorMessage: mocks.apiErrorMessage,
}))
vi.mock('@/composables/useMediaClassification', () => ({
@@ -243,6 +245,7 @@ describe('AccountSettingClassification', () => {
},
],
})
mocks.apiErrorMessage.mockReset().mockReturnValue(undefined)
mocks.analyzeImpact.mockReset()
mocks.initialize.mockReset().mockResolvedValue(undefined)
mocks.loadHistory.mockReset().mockResolvedValue(undefined)
@@ -448,6 +451,18 @@ describe('AccountSettingClassification', () => {
expect(mocks.toastInfo).toHaveBeenCalledWith('已恢复当前活动策略')
})
it('surfaces the server reason when validation rejects a changed rule draft', async () => {
const user = userEvent.setup()
mocks.validateDraft.mockRejectedValueOnce(new Error('validation rejected'))
mocks.apiErrorMessage.mockReturnValueOnce('请求参数不正确')
await renderWithProviders(AccountSettingClassification)
await openWorkspace('验证发布')
await user.click(screen.getByRole('button', { name: '校验草稿' }))
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('请求参数不正确'))
})
it('updates source fallbacks through stable category IDs', async () => {
const user = userEvent.setup()
await renderWithProviders(AccountSettingClassification)