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:
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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': '参与文档审签',
|
||||
|
||||
Reference in New Issue
Block a user