Merge pull request 'Show an honest rejected state on the RDM roles panel' (#75) from feature/rdm-roles-reject-state into main
This commit was merged in pull request #75.
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
memberAddPayload, memberRemovePayload,
|
||||
assignDocDutiesPayload, docSubmitPayload, docApprovePayload, docRejectPayload,
|
||||
docFinalPayload, docStatusPayload, permCheckPayload, describeSource,
|
||||
rolesListPlaceholder, describeRoleListError,
|
||||
} from '../rdmManagement'
|
||||
|
||||
// ---- 系统管理:RDM 角色权限(仅管理员 + rdmRolesAvailable) ----
|
||||
@@ -44,19 +45,23 @@ export function RdmRolesPanel() {
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const out = await api.rdmCommand<{ roles: RoleView[] }>('rdm.role.list')
|
||||
setRoles(out.roles ?? [])
|
||||
} catch (e) { setErr(String(e)) }
|
||||
setErr(''); setRoles(out.roles ?? [])
|
||||
} catch (e) { setRoles(null); setErr(describeRoleListError(String(e))) }
|
||||
}
|
||||
useEffect(() => { refresh() }, [])
|
||||
|
||||
const run = async (label: string, fn: () => Promise<unknown>) => {
|
||||
setErr(''); setNotice(''); setBusy(true)
|
||||
try { await fn(); setNotice(label); await refresh() }
|
||||
catch (e) { setErr(String(e)) } finally { setBusy(false) }
|
||||
catch (e) { setErr(describeRoleListError(String(e))) } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
if (!me?.isAdmin) return <EmptyState icon="shield">仅平台管理员可管理专业角色</EmptyState>
|
||||
|
||||
// 占位行单一出口:加载中仅在无错误时成立(拒绝后永远转圈是误导——见
|
||||
// rolesListPlaceholder 注释)。
|
||||
const placeholder = rolesListPlaceholder(roles, err)
|
||||
|
||||
const sel = roles?.find(r => r.roleId === editRole)
|
||||
const editCurrent = sel?.actions?.[editAction]
|
||||
|
||||
@@ -92,7 +97,7 @@ export function RdmRolesPanel() {
|
||||
</tr>
|
||||
))}
|
||||
{roles !== null && roles.length === 0 && <tr><td colSpan={6} className="ad-dim">暂无专业角色</td></tr>}
|
||||
{roles === null && <tr><td colSpan={6} className="ad-dim">加载中…</td></tr>}
|
||||
{placeholder && <tr><td colSpan={6} className="ad-dim">{placeholder}</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -105,6 +105,23 @@ export function describeSource(source: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** 角色清单表格占位行文案:加载中仅在无错误时成立——rdm.role.list 被拒后
|
||||
* 停留「加载中…」是 2026-10-13 8788 锁死事故的误导性表象(表格永远在转圈)。 */
|
||||
export function rolesListPlaceholder(roles: unknown[] | null, err: string): string {
|
||||
if (roles !== null) return roles.length === 0 ? '暂无专业角色' : ''
|
||||
return err ? '角色清单未加载(见下方提示)' : '加载中…'
|
||||
}
|
||||
|
||||
/** rdm.role.list 错误文案映射:域管理员权限拒绝给人话 + 恢复通道;其余错误
|
||||
* 原样保留(服务端 reason 变化不至于空白)。台账单管理员制见 RDM G1-4/U1。 */
|
||||
export function describeRoleListError(raw: string): string {
|
||||
if (raw.includes('不是系统管理员')) {
|
||||
return '当前账号不是 RDM 域管理员(台账单管理员制),无法读取角色清单;'
|
||||
+ '请联系现任管理员经 rdm.perm.transfer 移交,或由部署侧受信通道恢复'
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
/** 审阅人 CSV 解析:去空白、去空项、保序去重(表单输入 → reviewers 数组)。 */
|
||||
export function parseReviewers(csv: string): string[] {
|
||||
const seen = new Set<string>()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// RDM 角色权限面板拒绝态(2026-10-13 8788 dev 锁死复现):rdm.role.list 被拒后
|
||||
// 表格停留「加载中…」且错误为原始 400 JSON——测纯函数两件事:占位行不再假装
|
||||
// 加载中;域管理员权限拒绝映射为人话 + 恢复通道。UI 不重算权限(KTD5),文案
|
||||
// 只描述拒绝事实,不判权限。
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { rolesListPlaceholder, describeRoleListError } from '../src/rdmManagement'
|
||||
|
||||
describe('rolesListPlaceholder', () => {
|
||||
it('keeps 加载中 only while loading without error', () => {
|
||||
expect(rolesListPlaceholder(null, '')).toBe('加载中…')
|
||||
})
|
||||
|
||||
it('replaces the loading row with a failure row once the fetch rejected', () => {
|
||||
expect(rolesListPlaceholder(null, 'Error: 400: {"receipt":…}')).toBe('角色清单未加载(见下方提示)')
|
||||
})
|
||||
|
||||
it('returns the empty label for an empty list and nothing otherwise', () => {
|
||||
expect(rolesListPlaceholder([], '')).toBe('暂无专业角色')
|
||||
expect(rolesListPlaceholder([{ roleId: 'r1' }], '')).toBe('')
|
||||
// 已有数据时即便残留 err 也不再渲染占位行(表格有内容即事实来源)
|
||||
expect(rolesListPlaceholder([{ roleId: 'r1' }], 'stale')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('describeRoleListError', () => {
|
||||
const denied = 'Error: 400: {"receipt":{"commandId":"rdm.role.list","state":"rejected",'
|
||||
+ '"reason":"rejected: 权限拒绝:weichen 不是系统管理员(全局动作 project.settings)"}}'
|
||||
|
||||
it('maps the domain-admin denial to a readable message with the transfer channel', () => {
|
||||
const got = describeRoleListError(denied)
|
||||
expect(got).toContain('不是 RDM 域管理员')
|
||||
expect(got).toContain('rdm.perm.transfer')
|
||||
expect(got).not.toContain('"receipt"')
|
||||
})
|
||||
|
||||
it('passes other errors through untouched', () => {
|
||||
expect(describeRoleListError('Error: 500: boom')).toBe('Error: 500: boom')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user