✨ feat(vscode): 看板顶部加筛选框,按标题 / 工单号 / PR 号过滤卡片
工单多了以后靠肉眼在四列里翻太慢。顶部输入框按空白分词逐词 AND 匹配 标题、工单号、PR 号,`#427` 与 `427` 等价,纯数字按包含匹配,记不全 编号也能搜到。筛选只收窄渲染,拖拽与依赖锁定仍按全量列表算;列头角标 在筛选时显示「可见 / 总数」。
This commit is contained in:
1 parent
73f2c3ec66
commit
9e255f517b
6 files changed
+167
-8
No files matched your search
@@ -0,0 +1,60 @@
|
||||
import type { Issue } from '../types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { filterIssues } from './issueFilter'
|
||||
|
||||
function issue(number: number, title: string, extra: Partial<Issue> = {}): Issue {
|
||||
return {
|
||||
id: `repo#${number}`,
|
||||
number,
|
||||
title,
|
||||
column: 'todo',
|
||||
htmlUrl: '',
|
||||
...extra,
|
||||
}
|
||||
}
|
||||
|
||||
const board = [
|
||||
issue(426, '编程大赛:报名与提交作品'),
|
||||
issue(427, '编程大赛-作品墙分类', { pr: '318' }),
|
||||
issue(419, '管理后台统计各模型用量'),
|
||||
issue(42, 'auto 模型:自动为用户会话选模型'),
|
||||
]
|
||||
|
||||
function numbers(query: string): number[] {
|
||||
return filterIssues(board, query).map(i => i.number)
|
||||
}
|
||||
|
||||
describe('filterIssues', () => {
|
||||
it('空查询放行全部,且原样返回同一个数组', () => {
|
||||
expect(filterIssues(board, ' ')).toBe(board)
|
||||
})
|
||||
|
||||
it('按标题子串匹配', () => {
|
||||
expect(numbers('作品')).toEqual([426, 427])
|
||||
})
|
||||
|
||||
it('标题匹配不区分大小写', () => {
|
||||
expect(numbers('AUTO')).toEqual([42])
|
||||
})
|
||||
|
||||
it('按工单号匹配,带不带 # 都行', () => {
|
||||
expect(numbers('#427')).toEqual([427])
|
||||
expect(numbers('427')).toEqual([427])
|
||||
})
|
||||
|
||||
it('数字按包含匹配,短号能带出长号', () => {
|
||||
expect(numbers('42')).toEqual([426, 427, 42])
|
||||
})
|
||||
|
||||
it('按 PR 号匹配', () => {
|
||||
expect(numbers('318')).toEqual([427])
|
||||
})
|
||||
|
||||
it('多个词之间是 AND', () => {
|
||||
expect(numbers('编程 分类')).toEqual([427])
|
||||
})
|
||||
|
||||
it('没有命中时返回空', () => {
|
||||
expect(numbers('不存在的东西')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Issue } from '../types'
|
||||
|
||||
/**
|
||||
* 看板顶部筛选框的匹配规则。
|
||||
*
|
||||
* 查询按空白切成多个词,逐词 AND:工单要留在板上,每个词都得命中它的
|
||||
* 标题、工单号或 PR 号之一。词首的 `#` 先去掉,`#427` 与 `427` 等价。
|
||||
* 纯数字的词按「包含」匹配编号,`42` 既能带出 #42 也能带出 #427,
|
||||
* 免得用户记不全编号就搜不到。
|
||||
*/
|
||||
export function parseFilterTerms(query: string): string[] {
|
||||
return query.trim().toLowerCase().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
function matchesTerm(issue: Issue, term: string): boolean {
|
||||
if (issue.title.toLowerCase().includes(term))
|
||||
return true
|
||||
const digits = term.replace(/^#/, '')
|
||||
if (digits === '' || !/^\d+$/.test(digits))
|
||||
return false
|
||||
return String(issue.number).includes(digits) || (issue.pr ?? '').includes(digits)
|
||||
}
|
||||
|
||||
export function matchesIssueFilter(issue: Issue, terms: string[]): boolean {
|
||||
return terms.every(term => matchesTerm(issue, term))
|
||||
}
|
||||
|
||||
/** 空查询原样返回,调用方据此免掉一次无意义的数组拷贝。 */
|
||||
export function filterIssues(issues: Issue[], query: string): Issue[] {
|
||||
const terms = parseFilterTerms(query)
|
||||
if (terms.length === 0)
|
||||
return issues
|
||||
return issues.filter(issue => matchesIssueFilter(issue, terms))
|
||||
}
|
||||
Reference in new issue
Block a user