Simplify the settled project settings implementation

Why: Keep the completed settings flows readable without adding unused API exports or duplicate helpers.

Evidence: SELF /tmp/project-settings-simplify-green-20261014-973d52c1.json records settings tests/vet, 284 Frontend tests, typecheck, the unchanged knip baseline and diff checks. Reuse and quality passes were independent read-only agents; efficiency was reviewed inline after the host rejected another review thread.

Impact: Preserve scope rules, write guards, DTOs, errors and ordering. Reuse Core ownership constants and path lookup; remove unused exports and keep permission selection in a typed local state.

Refs: P1/P7, docs/records/2026-10-14-rdm-settings-completion-contract.md
This commit is contained in:
2026-10-14 02:51:51 +00:00
parent 53ceadf51e
commit 09c6ac41e0
6 changed files with 21 additions and 22 deletions
+2 -2
View File
@@ -8,7 +8,7 @@ import { assignDocDutiesPayload, docApprovePayload, docFinalPayload, docRejectPa
import { Feedback, personItem, projectContext, readableError, RoleFilter, selectedPeople,
useRequestFence, useRoleOptions, validVersion, type MemberList, type Person } from './rdmSettingsShared'
export interface Duties { author: string; reviewers: string[]; approver: string }
interface Duties { author: string; reviewers: string[]; approver: string }
type WireDuties = Partial<Duties> & { Author?: string; Reviewers?: string[]; Approver?: string }
export function normalizeDuties(value?: WireDuties | null): Duties {
return { author: value?.author ?? value?.Author ?? '', reviewers: value?.reviewers ?? value?.Reviewers ?? [], approver: value?.approver ?? value?.Approver ?? '' }
@@ -26,7 +26,7 @@ interface CatalogDoc {
sections: number; assigned: boolean; author?: string; reviewers?: string[]; approver?: string
activeRound?: number; activeSections?: number; status?: string; statusLabel?: string; dutyVersion?: number
}
export interface DutyTreeNode {
interface DutyTreeNode {
id: string; kind: 'stage' | 'type' | 'doc' | 'section'; label: string; selectable: boolean
docId?: string; sectionId?: string; children?: DutyTreeNode[]
}
+12 -13
View File
@@ -71,7 +71,7 @@ export function RdmPermMatrixSection({ project }: { project: Project }) {
const [roleId, setRoleId] = useState('')
const [projectRole, setProjectRole] = useState('')
const [group, setGroup] = useState('all')
const [expanded, setExpanded] = useState('')
const [expanded, setExpanded] = useState<{ member: string; action: string } | null>(null)
const [busy, setBusy] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
@@ -106,12 +106,12 @@ export function RdmPermMatrixSection({ project }: { project: Project }) {
}
useEffect(() => {
fence.invalidate(); setView('member'); setMember(me?.owner || ''); setBatch(null); setMatrix(null)
setCanConfigure(false); setExpanded(''); setQuery(''); setRoleId(''); setProjectRole(''); setBusy(false); setNotice(''); setError('')
setCanConfigure(false); setExpanded(null); setQuery(''); setRoleId(''); setProjectRole(''); setBusy(false); setNotice(''); setError('')
void queryMember(me?.owner || '')
}, [context])
useEffect(() => {
if (view !== 'matrix' || !canConfigure) return
fence.cancel('read'); setExpanded('')
fence.cancel('read'); setExpanded(null)
const timer = setTimeout(() => { void queryBatch() }, query ? 250 : 0)
return () => { clearTimeout(timer); fence.cancel('read') }
}, [view, query, roleId, projectRole, canConfigure])
@@ -141,21 +141,20 @@ export function RdmPermMatrixSection({ project }: { project: Project }) {
}
const visible = (actions: MatrixAction[]) => projectActions(actions).filter(action => group === 'all' || actionGroup(action.action) === group)
const matrixActions = [...new Map((batch?.rows ?? []).flatMap(row => visible(row.actions)).map(action => [action.action, action])).values()]
const expandedOwner = expanded.split('\u0000')[0]
const expandedAction = expanded.split('\u0000')[1]
const expandedRow = expanded ? batch?.rows.find(row => row.member === expanded.member) : undefined
return <div className="rdm-settings">
<div className="ad-create"><div className="ad-row">
<select aria-label="权限查看方式" className="lg-input" value={view} disabled={busy} onChange={e => {
const next = e.target.value as 'member' | 'matrix'
if (next === 'matrix' && !canConfigure) return
fence.cancel('read'); fence.cancel('mutation'); setExpanded(''); setNotice(''); setView(next)
fence.cancel('read'); fence.cancel('mutation'); setExpanded(null); setNotice(''); setView(next)
if (next === 'member') void queryMember(member || me?.owner || '')
}}><option value="member">按成员查看</option><option value="matrix" disabled={!canConfigure}>权限矩阵</option></select>
<select aria-label="权限分组" className="lg-input" value={group} onChange={e => { setGroup(e.target.value); setExpanded('') }}>
<select aria-label="权限分组" className="lg-input" value={group} onChange={e => { setGroup(e.target.value); setExpanded(null) }}>
<option value="all">全部项目操作</option><option value="project">项目与成员</option><option value="documents">文档与审签</option><option value="knowledge">知识与追溯</option>
</select>
<RoleFilter roles={roles} value={roleId} disabled={!canConfigure || !!roleError || busy} onChange={value => { fence.cancel('read'); setRoleId(value); setExpanded('') }} />
<select aria-label="项目身份筛选" className="lg-input" value={projectRole} disabled={!canConfigure || busy} onChange={e => { fence.cancel('read'); setProjectRole(e.target.value); setExpanded('') }}>
<RoleFilter roles={roles} value={roleId} disabled={!canConfigure || !!roleError || busy} onChange={value => { fence.cancel('read'); setRoleId(value); setExpanded(null) }} />
<select aria-label="项目身份筛选" className="lg-input" value={projectRole} disabled={!canConfigure || busy} onChange={e => { fence.cancel('read'); setProjectRole(e.target.value); setExpanded(null) }}>
<option value="">全部项目身份</option><option value="owner">项目负责人</option><option value="member">项目成员</option>
</select>
{view === 'member' && <Picker aria-label="选择权限成员" selected={member ? [member] : []}
@@ -180,14 +179,14 @@ export function RdmPermMatrixSection({ project }: { project: Project }) {
{matrixActions.map(action => {
const permission = row.actions.find(item => item.action === action.action && item.scope === 'project')
return <td key={action.action}>{permission ? <button className="ad-mini" aria-label={`${row.name || row.member} · ${actionLabel(action.action)} · ${permission.allowed ? '允许' : '禁止'}`}
onClick={() => setExpanded(`${row.member}\u0000${action.action}`)}>{permission.allowed ? '允许' : '禁止'}</button> : '未核验'}</td>
onClick={() => setExpanded({ member: row.member, action: action.action })}>{permission.allowed ? '允许' : '禁止'}</button> : '未核验'}</td>
})}</tr>)}</tbody>
</table></div>
{batch.rows.length === 0 && <p className="ad-hint">没有匹配的项目成员</p>}
{batch.next && <Btn disabled={loading || busy} onClick={() => queryBatch(batch.next)}>加载更多成员</Btn>}
{expanded && <div className="settings-group"><h4>{batch.rows.find(row => row.member === expandedOwner)?.name || expandedOwner} · 权限依据</h4>
<ActionDetailTable actions={batch.rows.find(row => row.member === expandedOwner)?.actions.filter(action => action.action === expandedAction) || []}
canManage={canConfigure && !writeError} busy={busy} onOverride={(action, mode) => override(expandedOwner, action, mode)} /></div>}
{expanded && <div className="settings-group"><h4>{expandedRow?.name || expanded.member} · 权限依据</h4>
<ActionDetailTable actions={expandedRow?.actions.filter(action => action.action === expanded.action) || []}
canManage={canConfigure && !writeError} busy={busy} onOverride={(action, mode) => override(expanded.member, action, mode)} /></div>}
</>}
<Feedback error={error || (canConfigure ? roleError : '')} notice={notice} />
</div>
+4 -4
View File
@@ -1,16 +1,16 @@
import { Btn, SettingsGroup, SettingRow } from '@ui'
import type { SettingDesc } from '../contracts'
export const groupNames: Record<string, string> = {
const groupNames: Record<string, string> = {
models: '模型与密钥', runtime: '运行环境', context: '上下文', retrieval: '检索',
ui: '界面', plugins: '系统插件', crossSession: '个人会话协作', tools: '工具服务', deliverables: '项目交付规则', telemetry: '运行观测',
}
export const optionLabels: Record<string, string> = { modern: '现代', formal: '正式', minimal: '简洁',
const optionLabels: Record<string, string> = { modern: '现代', formal: '正式', minimal: '简洁',
dark: '深色', light: '浅色', system: '跟随系统', zh: '中文', en: '英文', auto: '自动', off: '关闭', on: '开启',
disabled: '关闭', enabled: '开启', relevant: '相关会话', all: '全部本人会话', none: '关闭',
direct: '直接对话', squad: '协作小队', zhCN: '简体中文', 'zh-CN': '简体中文', 'en-US': '英语',
}
export function readableValue(value: unknown) {
function readableValue(value: unknown) {
if (value === true) return '开启'
if (value === false) return '关闭'
if (value === undefined || value === null || value === '') return '未配置'
@@ -62,7 +62,7 @@ export function flatten(map: Record<string, any>): Record<string, any> {
}
visit(map); return out
}
export function unflatten(map: Record<string, any>, preserveEmpty = false): Record<string, any> {
function unflatten(map: Record<string, any>, preserveEmpty = false): Record<string, any> {
const out: Record<string, any> = {}
for (const [path, value] of Object.entries(map)) {
if (value === undefined || (value === '' && !preserveEmpty)) continue
+1 -1
View File
@@ -7,7 +7,7 @@ import { BorrowPanels } from './BorrowPanels'
import { flatten, scopedDraft, scopedSchema, SettingsFields, personalSettingsMutation } from './SettingsFields'
import { Feedback, readableError, useRequestFence } from './rdmSettingsShared'
export { SettingInput, settingInputAttrs, flatten, unflatten, groupNames } from './SettingsFields'
export { SettingInput, settingInputAttrs } from './SettingsFields'
export function SettingsModal() {
const owner = useStore(s => s.me?.owner)
+1 -1
View File
@@ -14,7 +14,7 @@ export interface MemberList {
members: Person[]; next?: string; count?: number; accountDirectoryAvailable?: boolean
canManageMembers?: boolean; canAssignDuties?: boolean; canConfigurePermissions?: boolean
}
export interface RoleOption { roleId: string; name: string }
interface RoleOption { roleId: string; name: string }
export const projectRoleLabel = (role?: string) => role === 'owner' ? '项目负责人' : role === 'member' ? '项目成员' : '未加入项目'
export const personLabel = (person: Person) => person.name || person.owner
export function personItem(person: Person, adding = false): PickItem {
+1 -1
View File
@@ -146,7 +146,7 @@ export function describeSource(source: string): string {
}
}
export const ACTION_LABELS: Record<string, string> = {
const ACTION_LABELS: Record<string, string> = {
'project.create': '创建项目', 'project.archive': '归档项目', 'member.manage': '管理项目成员',
'duty.assign': '分配文档职责', 'stage.advance': '推进项目阶段', 'doc.read': '读取文档',
'doc.write': '编制与编辑文档', 'doc.publish': '发布受控文档', 'review.participate': '参与文档审签',