Add project permission matrix tab consuming perm.check
Why: 方案 r1 U6/R4/KTD5——RDM #266 已交付与实际判定同源的矩阵解释读面,项目设置需消费入口(UI 不重算,服务端解释)。 Evidence: 红:3 failed(导出缺失,/tmp/fe-matrix-red.txt);绿:vitest 25 文件/127 测试全过(含 design-guard)+ npm run build 成功(tsc -b 干净);收据 /tmp/fe-matrix-vitest.txt、/tmp/fe-matrix-build.txt。 Impact: rdmManagement 增 RDM_PERM_MATRIX_COMMANDS/rdmPermMatrixReady/permCheckPayload/describeSource;RdmPermMatrixSection(成员选择默认自省+12 动作矩阵表+来源标签+拒绝原因);项目设置「项目权限」页签(rdmPermMatrixAvailable 门控);store 同源计算标志。样式仅复用既有类。 Refs: RDM #266;方案 r1 U6/R4
This commit is contained in:
@@ -7,7 +7,7 @@ import { store, useStore } from '../store'
|
||||
import type { SettingDesc } from '../contracts'
|
||||
import { SettingsShell, Btn, EmptyState } from '@ui'
|
||||
import { SettingInput, flatten, unflatten, groupNames } from './SettingsModal'
|
||||
import { RdmMembersSection, RdmDutiesSection } from './adminRdmSections'
|
||||
import { RdmMembersSection, RdmDutiesSection, RdmPermMatrixSection } from './adminRdmSections'
|
||||
|
||||
export function ProjectSettingsModal() {
|
||||
const projectId = useStore(s => s.ui.projectSettingsId)
|
||||
@@ -15,6 +15,7 @@ export function ProjectSettingsModal() {
|
||||
const project = projects.find(p => p.id === projectId)
|
||||
const rdmMembersAvailable = useStore(s => s.rdmMembersAvailable)
|
||||
const rdmDocReviewAvailable = useStore(s => s.rdmDocReviewAvailable)
|
||||
const rdmPermMatrixAvailable = useStore(s => s.rdmPermMatrixAvailable)
|
||||
const [tab, setTab] = useState('project')
|
||||
const [schema, setSchema] = useState<SettingDesc[]>([])
|
||||
const [draft, setDraft] = useState<Record<string, any>>({})
|
||||
@@ -61,15 +62,17 @@ export function ProjectSettingsModal() {
|
||||
// U6/KTD5:成员与文档职责页签按 RDM 命令面门控(普通基座不出现)。
|
||||
...(rdmMembersAvailable ? [{ key: 'members', label: '成员', icon: 'users' as const }] : []),
|
||||
...(rdmDocReviewAvailable ? [{ key: 'duties', label: '文档职责 · 整文审签', icon: 'shield' as const }] : []),
|
||||
...(rdmPermMatrixAvailable ? [{ key: 'matrix', label: '项目权限', icon: 'key' as const }] : []),
|
||||
]}
|
||||
footer={saved ? <span style={{ color: 'var(--ok)' }}>✓ 已保存</span> : '项目设置覆盖用户设置(new-run 类对新任务生效)'}
|
||||
>
|
||||
<div>
|
||||
{tab === 'members' && <RdmMembersSection projectId={projectId} />}
|
||||
{tab === 'duties' && <RdmDutiesSection projectId={projectId} />}
|
||||
{tab !== 'members' && tab !== 'duties' && !loaded ? (
|
||||
{tab === 'matrix' && <RdmPermMatrixSection projectId={projectId} />}
|
||||
{tab !== 'members' && tab !== 'duties' && tab !== 'matrix' && !loaded ? (
|
||||
<EmptyState icon="settings">加载中…</EmptyState>
|
||||
) : tab !== 'members' && tab !== 'duties' ? (
|
||||
) : tab !== 'members' && tab !== 'duties' && tab !== 'matrix' ? (
|
||||
<>
|
||||
<div className="empty-note" style={{ marginBottom: 10 }}>
|
||||
此处仅保存本项目的覆盖值;留空即沿用用户/系统设置。跨会话读取在「会话协作」组。
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
roleCreatePayload, roleActionPayload, roleAssignPayload, roleDisablePayload,
|
||||
memberAddPayload, memberRemovePayload,
|
||||
assignDocDutiesPayload, docSubmitPayload, docApprovePayload, docRejectPayload,
|
||||
docFinalPayload, docStatusPayload,
|
||||
docFinalPayload, docStatusPayload, permCheckPayload, describeSource,
|
||||
} from '../rdmManagement'
|
||||
|
||||
// ---- 系统管理:RDM 角色权限(仅管理员 + rdmRolesAvailable) ----
|
||||
@@ -307,3 +307,79 @@ export function RdmDutiesSection({ projectId }: { projectId: string }) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ---- 项目设置:项目权限(矩阵解释读面,与实际判定同源——UI 不重算) ----
|
||||
|
||||
interface MatrixAction { action: string; allowed: boolean; source: string; reason?: string }
|
||||
interface MatrixOut {
|
||||
projectId: string; member: string; role: string
|
||||
profRole: { roleId: string; name: string } | null
|
||||
actions: MatrixAction[]
|
||||
}
|
||||
|
||||
export function RdmPermMatrixSection({ projectId }: { projectId: string }) {
|
||||
const me = useStore(s => s.me)
|
||||
const [member, setMember] = useState('')
|
||||
const [members, setMembers] = useState<MemberRow[] | null>(null)
|
||||
const [matrix, setMatrix] = useState<MatrixOut | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
api.rdmCommand<{ members: MemberRow[] }>('rdm.project.member.list', { projectId })
|
||||
.then(out => {
|
||||
setMembers(out.members ?? [])
|
||||
// 默认自省:普通成员只看自己的矩阵;负责人/管理员可切换他人。
|
||||
if (me?.owner) setMember(prev => prev || me.owner)
|
||||
})
|
||||
.catch(e => setErr(String(e)))
|
||||
}, [projectId, me?.owner])
|
||||
|
||||
const query = async (m: string) => {
|
||||
if (!m) return
|
||||
setErr(''); setBusy(true); setMatrix(null)
|
||||
try {
|
||||
setMatrix(await api.rdmCommand<MatrixOut>('rdm.perm.check', permCheckPayload(projectId, m)))
|
||||
} catch (e) { setErr(String(e)) } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ad-create">
|
||||
<div className="ad-row">
|
||||
<select className="lg-input" value={member} onChange={e => { setMember(e.target.value); query(e.target.value) }}>
|
||||
<option value="">选择成员(默认自省)…</option>
|
||||
{(members ?? []).map(m => <option key={m.owner} value={m.owner}>{m.owner}</option>)}
|
||||
</select>
|
||||
<Btn variant="ghost" icon="eye" disabled={busy || !member} onClick={() => query(member)}>查看矩阵</Btn>
|
||||
</div>
|
||||
<p className="ad-hint">普通成员可查看自己的有效权限;查看他人矩阵须项目管理权。矩阵与实际判定同源(服务端解释,非前端推算)。</p>
|
||||
</div>
|
||||
{matrix && (
|
||||
<>
|
||||
<div className="ad-row">
|
||||
<span>成员 <b className="ad-mono">{matrix.member}</b> · 项目关系 <b>{matrix.role}</b>
|
||||
{matrix.profRole && <> · 专业角色 <b>{matrix.profRole.name}</b>({matrix.profRole.roleId})</>}
|
||||
</span>
|
||||
</div>
|
||||
<table className="ad-table">
|
||||
<thead><tr><th>动作</th><th>生效</th><th>来源</th><th>原因(拒绝时)</th></tr></thead>
|
||||
<tbody>
|
||||
{(matrix.actions ?? []).map(a => (
|
||||
<tr key={a.action}>
|
||||
<td className="ad-mono">{a.action}</td>
|
||||
<td>{a.allowed ? <span className="ad-chip ad-chip-admin">允许</span> : <span className="ad-chip ad-chip-warn">拒绝</span>}</td>
|
||||
<td className="ad-dim">{describeSource(a.source)}</td>
|
||||
<td className="ad-dim">{a.reason || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{!matrix && !err && members !== null && <p className="ad-hint">选择成员后展示 12 项动作的有效权限与第一拒绝原因。</p>}
|
||||
{err && <div className="lg-err"><Icon name="alert" size={14} />{err}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ export const RDM_MEMBER_COMMANDS = [
|
||||
'rdm.project.member.remove',
|
||||
] as const
|
||||
|
||||
/** 项目设置「项目权限」页消费的命令面(矩阵解释 + 成员清单)。 */
|
||||
export const RDM_PERM_MATRIX_COMMANDS = [
|
||||
'rdm.perm.check',
|
||||
'rdm.project.member.list',
|
||||
] as const
|
||||
|
||||
/** 项目设置「文档职责 · 整文审签」页消费的命令面。 */
|
||||
export const RDM_DOC_REVIEW_COMMANDS = [
|
||||
'rdm.review.assign.doc',
|
||||
@@ -48,6 +54,10 @@ export const rdmMembersReady = (cmds: CommandListItem[] | null | undefined) =>
|
||||
export const rdmDocReviewReady = (cmds: CommandListItem[] | null | undefined) =>
|
||||
ready(cmds, RDM_DOC_REVIEW_COMMANDS)
|
||||
|
||||
/** 项目权限矩阵页是否就绪。 */
|
||||
export const rdmPermMatrixReady = (cmds: CommandListItem[] | null | undefined) =>
|
||||
ready(cmds, RDM_PERM_MATRIX_COMMANDS)
|
||||
|
||||
/** RDM 动作清单(12 项,与 RDM perms.AllActions 对齐;角色动作编辑器选项)。 */
|
||||
export const ROLE_ACTIONS = [
|
||||
'project.create', 'project.archive', 'member.manage', 'duty.assign',
|
||||
@@ -81,6 +91,20 @@ export const docFinalPayload = (projectId: string, docId: string, round = 0) =>
|
||||
round > 0 ? { projectId, docId, round } : { projectId, docId }
|
||||
export const docStatusPayload = (projectId: string, docId: string) => ({ projectId, docId })
|
||||
|
||||
export const permCheckPayload = (projectId: string, member: string) => ({ projectId, member })
|
||||
|
||||
/** 矩阵来源标签(人话映射;未知来源回退原文——服务端新增来源时不至于空白)。 */
|
||||
export function describeSource(source: string): string {
|
||||
switch (source) {
|
||||
case 'admin': return '系统管理员'
|
||||
case 'prof-role-default': return '专业角色默认'
|
||||
case 'override': return '项目内覆盖'
|
||||
case 'relation-default': return '项目关系默认'
|
||||
case 'non-member': return '非项目成员'
|
||||
default: return source
|
||||
}
|
||||
}
|
||||
|
||||
/** 审阅人 CSV 解析:去空白、去空项、保序去重(表单输入 → reviewers 数组)。 */
|
||||
export function parseReviewers(csv: string): string[] {
|
||||
const seen = new Set<string>()
|
||||
|
||||
+4
-2
@@ -9,7 +9,7 @@ import { canFlushOutbox, deriveShellPhase } from './runstate'
|
||||
import { flashElement } from './scrollsync'
|
||||
import { isThemeChoice, resolveTheme, type ThemeChoice } from './theme'
|
||||
import { rdmAuditReady } from './workspaces'
|
||||
import { rdmRolesReady, rdmMembersReady, rdmDocReviewReady } from './rdmManagement'
|
||||
import { rdmRolesReady, rdmMembersReady, rdmDocReviewReady, rdmPermMatrixReady } from './rdmManagement'
|
||||
|
||||
type FontSizeChoice = 13 | 14 | 16 | 18
|
||||
const FONT_SIZE_CHOICES: readonly FontSizeChoice[] = [13, 14, 16, 18]
|
||||
@@ -120,6 +120,7 @@ interface AppState {
|
||||
rdmRolesAvailable: boolean
|
||||
rdmMembersAvailable: boolean
|
||||
rdmDocReviewAvailable: boolean
|
||||
rdmPermMatrixAvailable: boolean
|
||||
/** 管理区声明分区(WP3-I3):编译插件声明,入口全员可见、区内置权限不合并。 */
|
||||
adminSections: AdminZoneSection[]
|
||||
ui: UIState
|
||||
@@ -186,7 +187,7 @@ let state: AppState = {
|
||||
plan: null, todos: null, artifacts: [], surfaces: [], projectSurfaces: [], cards: [], projectCards: [], inboxScope: 'conv', runtimeMock: false, profiles: [], pluginPanels: [],
|
||||
serverWorkspaces: [],
|
||||
rdmAuditAvailable: false,
|
||||
rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false,
|
||||
rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false,
|
||||
adminSections: [],
|
||||
ui: loadUI(), connected: false, posting: false, error: null, toast: null,
|
||||
outbox: [], outboxDraining: false,
|
||||
@@ -805,6 +806,7 @@ export const store = {
|
||||
rdmRolesAvailable: rdmRolesReady(cmds),
|
||||
rdmMembersAvailable: rdmMembersReady(cmds),
|
||||
rdmDocReviewAvailable: rdmDocReviewReady(cmds),
|
||||
rdmPermMatrixAvailable: rdmPermMatrixReady(cmds),
|
||||
})
|
||||
} catch { set({ rdmAuditAvailable: false }) }
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user