✨ feat(vscode): 看板默认显示团队工单,同事的卡只读并贴头像
看板范围默认从「只看我的」改为「团队全部」,同事移交或推进的工单不用 手动切开关就能看见。同事的卡片按 Gitea 归属(assignee 优先,无 assignee 退回创建者)判定为只读:禁止拖列、开会话、实施、合并分支、删 worktree、 删改工单,卡片右上角贴归属人头像,详情面板属性表整体消毒并显示只读徽章。 看 PR 改动、勾已查看、生成文件摘要属于本机审查动作,照常放行。 扩展侧在消息分发前统一拦截需要归属人身份的消息,兜住前端快照过期或工单 刚被重新指派的竞态;webhook 单卡上板时同步归属,避免增量卡漏进门禁。 头像由扩展侧带 token 抓回转 data URI 下发,绕开 webview CSP 对外链图片 的限制,按 URL 进程内缓存。 Claude-Session: https://claude.ai/code/session_01KSgcQwf1mWrb8qeTKFvJbV
This commit is contained in:
1 parent
e8963ac2a2
commit
9167b86708
26 files changed
+550
-34
No files matched your search
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isIssueMine, issueOwnerLogin } from './issueOwnership'
|
||||
|
||||
describe('isIssueMine', () => {
|
||||
it('assignee 含我 → 可写', () => {
|
||||
expect(isIssueMine({ poster: 'chehongwei', assignees: ['cruldra'] }, 'cruldra')).toBe(true)
|
||||
})
|
||||
|
||||
it('assignee 是别人 → 只读,创建者是我也不算', () => {
|
||||
expect(isIssueMine({ poster: 'cruldra', assignees: ['chehongwei'] }, 'cruldra')).toBe(false)
|
||||
})
|
||||
|
||||
it('无 assignee 时按创建者判定', () => {
|
||||
expect(isIssueMine({ poster: 'chehongwei' }, 'cruldra')).toBe(false)
|
||||
expect(isIssueMine({ poster: 'cruldra' }, 'cruldra')).toBe(true)
|
||||
})
|
||||
|
||||
it('身份未知 → 一律可写', () => {
|
||||
expect(isIssueMine({ poster: 'chehongwei' }, undefined)).toBe(true)
|
||||
})
|
||||
|
||||
it('youtrack 等无归属字段的工单 → 可写', () => {
|
||||
expect(isIssueMine({}, 'cruldra')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('issueOwnerLogin', () => {
|
||||
it('assignee 优先于创建者', () => {
|
||||
expect(issueOwnerLogin({ poster: 'cruldra', assignees: ['chehongwei'] })).toBe('chehongwei')
|
||||
})
|
||||
|
||||
it('无 assignee 时回退创建者', () => {
|
||||
expect(issueOwnerLogin({ poster: 'cruldra' })).toBe('cruldra')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 工单归属判定(扩展侧 `src/panel/issueOwnership.ts` 的镜像,两个包不共享
|
||||
* 源码,改一处要同步另一处)。
|
||||
*
|
||||
* 看板范围切到「团队全部」后同事的工单也会上板,这类卡片只读:不能拖列、
|
||||
* 不能开会话、不能合并或删除,只保留查看与接管。
|
||||
*/
|
||||
|
||||
import type { Issue } from '../types'
|
||||
|
||||
type OwnershipFacts = Pick<Issue, 'poster' | 'assignees'>
|
||||
|
||||
/** 该工单是否归当前登录者所有(可写)。身份未知时一律放行,不锁死自己的板。 */
|
||||
export function isIssueMine(issue: OwnershipFacts, me: string | undefined): boolean {
|
||||
if (me === undefined)
|
||||
return true
|
||||
const assignees = issue.assignees ?? []
|
||||
if (assignees.length > 0)
|
||||
return assignees.includes(me)
|
||||
return issue.poster === undefined || issue.poster === me
|
||||
}
|
||||
|
||||
/** 卡片头像取谁:assignee 优先(责任人),否则创建者。 */
|
||||
export function issueOwnerLogin(issue: OwnershipFacts): string | undefined {
|
||||
return (issue.assignees ?? [])[0] ?? issue.poster
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export interface ProfilesData {
|
||||
|
||||
export type ExtensionToWebview
|
||||
= | { type: 'issues/loading' }
|
||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string }
|
||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
||||
| { type: 'issues/error', message: string }
|
||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
|
||||
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { PropertyGroup } from '../components/property-grid'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toReadOnlySchema } from './readOnlySchema'
|
||||
|
||||
function schema(): PropertyGroup[] {
|
||||
return [{
|
||||
id: 'g',
|
||||
label: '组',
|
||||
properties: [
|
||||
{ key: 'autoReview', label: '自动审查', type: 'boolean' },
|
||||
{ key: 'session', label: '规划会话', type: 'action', onAction: () => {}, secondaryActions: [{ icon: null, onClick: () => {} }] },
|
||||
{ key: 'pr', label: '合并请求', type: 'pr-link', onAction: () => {} },
|
||||
{ key: 'plan', label: '计划', type: 'file-link', onOpen: () => {}, onReload: () => {} },
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
||||
describe('toReadOnlySchema', () => {
|
||||
it('所有字段变只读', () => {
|
||||
const out = toReadOnlySchema(schema())
|
||||
expect(out[0].properties.every(p => p.readOnly)).toBe(true)
|
||||
})
|
||||
|
||||
it('会话行的双击恢复被摘掉', () => {
|
||||
const out = toReadOnlySchema(schema())
|
||||
expect(out[0].properties.find(p => p.key === 'session')?.onAction).toBeUndefined()
|
||||
})
|
||||
|
||||
it('打开 PR、打开文件仍可用', () => {
|
||||
const out = toReadOnlySchema(schema())
|
||||
expect(out[0].properties.find(p => p.key === 'pr')?.onAction).toBeTypeOf('function')
|
||||
expect(out[0].properties.find(p => p.key === 'plan')?.onOpen).toBeTypeOf('function')
|
||||
})
|
||||
|
||||
it('右侧图标按钮全部禁用', () => {
|
||||
const out = toReadOnlySchema(schema())
|
||||
expect(out[0].properties.find(p => p.key === 'session')?.secondaryActions?.[0].disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('不改动原 schema', () => {
|
||||
const original = schema()
|
||||
toReadOnlySchema(original)
|
||||
expect(original[0].properties[0].readOnly).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 把详情面板的属性表变成只读版本:同事的工单在板上只能看。
|
||||
*
|
||||
* 一处消毒覆盖所有行,避免每加一行就得记得判断归属。放行的只有纯查看
|
||||
* 动作——双击 PR 编号跳浏览器、双击文件名打开本地文件;会改工单状态、
|
||||
* 开会话、删 worktree 的按钮一律禁用。
|
||||
*/
|
||||
|
||||
import type { PropertyGroup } from '../components/property-grid'
|
||||
|
||||
export function toReadOnlySchema(schema: PropertyGroup[]): PropertyGroup[] {
|
||||
return schema.map(group => ({
|
||||
...group,
|
||||
actions: undefined,
|
||||
properties: group.properties.map(prop => ({
|
||||
...prop,
|
||||
readOnly: true,
|
||||
// 'action' 行的双击是「在本机恢复会话」,别人的会话恢复不了也不该恢复。
|
||||
onAction: prop.type === 'action' ? undefined : prop.onAction,
|
||||
onReload: undefined,
|
||||
secondaryActions: prop.secondaryActions?.map(a => ({ ...a, disabled: true })),
|
||||
})),
|
||||
}))
|
||||
}
|
||||
Reference in new issue
Block a user