mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-09 01:36:45 +08:00
fix(classification): avoid duplicate category labels
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import type { StorageConf, TransferDirectoryConf } from '@/api/types'
|
||||
import type { ClassificationCategory } from '@/api/mediaClassification'
|
||||
import { manageStorage } from '@/api/manage'
|
||||
import { formatClassificationCategoryOptionTitle } from '@/utils/mediaClassification'
|
||||
import { nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storageRemoteDict } from '@/api/constants'
|
||||
@@ -228,7 +229,7 @@ const categoryItems = computed(() => [
|
||||
...props.categories
|
||||
.filter(category => category.enabled && category.media_type === props.directory.media_type)
|
||||
.map(category => ({
|
||||
title: `${category.name} · ${categoryPath(category)} · ${category.id}`,
|
||||
title: formatClassificationCategoryOptionTitle(category, { includeId: true, pathSeparator: '/' }),
|
||||
value: category.id,
|
||||
})),
|
||||
])
|
||||
|
||||
@@ -11,6 +11,7 @@ vi.mock('@/api/manage', () => ({
|
||||
}))
|
||||
|
||||
const categories: ClassificationCategory[] = [
|
||||
{ id: 'movie.base', media_type: '电影', name: '电影', path: ['电影'], enabled: true, labels: [] },
|
||||
{ id: 'movie.animation', media_type: '电影', name: '动画', path: ['电影', '动画'], enabled: true, labels: [] },
|
||||
{ id: 'movie.disabled', media_type: '电影', name: '停用', path: ['电影', '停用'], enabled: false, labels: [] },
|
||||
{ id: 'tv.animation', media_type: '电视剧', name: '动画', path: ['电视剧', '动画'], enabled: true, labels: [] },
|
||||
@@ -57,6 +58,7 @@ describe('DirectoryCard classification reference', () => {
|
||||
const categorySelect = within(screen.getByTestId('directory-category-select')).getByRole('combobox')
|
||||
await user.click(categorySelect)
|
||||
|
||||
expect(await screen.findByRole('option', { name: '电影 · movie.base' })).toBeInTheDocument()
|
||||
expect(await screen.findByRole('option', { name: '动画 · 电影/动画 · movie.animation' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('option', { name: /movie.disabled/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('option', { name: /tv.animation/ })).not.toBeInTheDocument()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ClassificationCategory, ClassificationMediaType } from '@/api/mediaClassificationTypes'
|
||||
import { formatClassificationCategoryOptionTitle } from '@/utils/mediaClassification'
|
||||
|
||||
/** 分类树编辑器输入属性。 */
|
||||
interface ClassificationCategoryEditorProps {
|
||||
@@ -141,8 +142,9 @@ function isCategoryProtected(categoryId: string): boolean {
|
||||
|
||||
/** 为 fallback 选择器生成不带内部稳定 ID 的可读标题。 */
|
||||
function fallbackItemTitle(category: ClassificationCategory): string {
|
||||
const path = category.path.length ? category.path.join(' / ') : t('setting.classification.category.pathUnset')
|
||||
return `${category.name} · ${path}`
|
||||
return formatClassificationCategoryOptionTitle(category, {
|
||||
emptyPathLabel: t('setting.classification.category.pathUnset'),
|
||||
})
|
||||
}
|
||||
|
||||
/** 返回指定媒体类型可选的稳定分类 ID 列表。 */
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { formatClassificationCategoryOptionTitle } from '@/utils/mediaClassification'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('formatClassificationCategoryOptionTitle', () => {
|
||||
it('omits a path that is identical to the category name', () => {
|
||||
expect(formatClassificationCategoryOptionTitle({ id: 'movie.base', name: '电影', path: ['电影'] })).toBe('电影')
|
||||
})
|
||||
|
||||
it('keeps hierarchical paths that add information to the category name', () => {
|
||||
expect(
|
||||
formatClassificationCategoryOptionTitle({
|
||||
id: 'movie.animation',
|
||||
name: '动画',
|
||||
path: ['电影', '动画'],
|
||||
}),
|
||||
).toBe('动画 · 电影 / 动画')
|
||||
})
|
||||
|
||||
it('can preserve a caller-specific path separator and stable ID', () => {
|
||||
expect(
|
||||
formatClassificationCategoryOptionTitle(
|
||||
{ id: 'movie.animation', name: '动画', path: ['电影', '动画'] },
|
||||
{ includeId: true, pathSeparator: '/' },
|
||||
),
|
||||
).toBe('动画 · 电影/动画 · movie.animation')
|
||||
})
|
||||
|
||||
it('uses the configured label only when the category has no path', () => {
|
||||
expect(
|
||||
formatClassificationCategoryOptionTitle(
|
||||
{ id: 'movie.unset', name: '未分类', path: [] },
|
||||
{ emptyPathLabel: '未设置路径' },
|
||||
),
|
||||
).toBe('未分类 · 未设置路径')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ClassificationCategory } from '@/api/mediaClassificationTypes'
|
||||
|
||||
/** 分类下拉标题的可选显示配置。 */
|
||||
interface ClassificationCategoryOptionTitleOptions {
|
||||
emptyPathLabel?: string
|
||||
includeId?: boolean
|
||||
pathSeparator?: string
|
||||
}
|
||||
|
||||
/** 生成分类选择器标题,避免分类名与完全相同的路径重复显示。 */
|
||||
export function formatClassificationCategoryOptionTitle(
|
||||
category: Pick<ClassificationCategory, 'name' | 'path' | 'id'>,
|
||||
options: ClassificationCategoryOptionTitleOptions = {},
|
||||
): string {
|
||||
const path = category.path.join(options.pathSeparator ?? ' / ')
|
||||
const displayPath = path && path !== category.name ? path : path ? '' : (options.emptyPathLabel ?? '')
|
||||
const parts = [category.name, displayPath]
|
||||
if (options.includeId) parts.push(category.id)
|
||||
return parts.filter(Boolean).join(' · ')
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import ClassificationPolicyControlPanel from '@/components/classification/Classi
|
||||
import ClassificationPreviewPanel from '@/components/classification/ClassificationPreviewPanel.vue'
|
||||
import ClassificationRuleEditor from '@/components/classification/ClassificationRuleEditor.vue'
|
||||
import { useMediaClassification } from '@/composables/useMediaClassification'
|
||||
import { formatClassificationCategoryOptionTitle } from '@/utils/mediaClassification'
|
||||
import { cloneDeep, isEqual } from 'lodash-es'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
@@ -222,7 +223,7 @@ function fallbackCategoryOptions(mediaType: ClassificationMediaType) {
|
||||
return (draftPolicy.value?.categories ?? [])
|
||||
.filter(category => category.enabled && category.media_type === mediaType)
|
||||
.map(category => ({
|
||||
title: category.path.length ? `${category.name} · ${category.path.join(' / ')}` : category.name,
|
||||
title: formatClassificationCategoryOptionTitle(category),
|
||||
value: category.id,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ describe('AccountSettingClassification', () => {
|
||||
name: 'musicbrainz 的电影来源兜底',
|
||||
})
|
||||
await user.click(musicbrainzFallback)
|
||||
await user.click(await screen.findByRole('option', { name: '电影 · 电影' }))
|
||||
await user.click(await screen.findByRole('option', { name: '电影' }))
|
||||
|
||||
const state = mocks.useMediaClassification.mock.results[0].value
|
||||
expect(state.draftPolicy.value.source_fallbacks.musicbrainz.电影).toBe('movie.base')
|
||||
|
||||
Reference in New Issue
Block a user