Merge remote-tracking branch 'origin/main' into feature/iam-u4-role-permission-ui
This commit is contained in:
@@ -144,6 +144,18 @@ interface DutyCatalogDoc {
|
||||
assigned: boolean; author?: string; reviewers?: string[]; approver?: string; activeRound?: number
|
||||
}
|
||||
|
||||
interface DocsTemplateRow {
|
||||
templateId: string; version: string; title: string; docTypes: string[]; stage?: string
|
||||
}
|
||||
|
||||
/** rdm.docs.instantiate 参数:docType 取首个适用类型;空标题交由后端取模板标题。 */
|
||||
export function docsInstantiateParams(tpl: DocsTemplateRow, title: string): Record<string, unknown> {
|
||||
return {
|
||||
docType: tpl.docTypes[0], templateId: tpl.templateId, version: tpl.version,
|
||||
...(title.trim() ? { title: title.trim() } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
interface DutyGetOut {
|
||||
docId: string; title: string; assigned: boolean
|
||||
roles: { author: string; reviewers: string[]; approver: string }
|
||||
@@ -186,6 +198,9 @@ export function RdmDutiesSection({ project }: { project: Project }) {
|
||||
const [err, setErr] = useState('')
|
||||
const [notice, setNotice] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [tpls, setTpls] = useState<DocsTemplateRow[] | null>(null)
|
||||
const [tplSel, setTplSel] = useState('')
|
||||
const [newTitle, setNewTitle] = useState('')
|
||||
|
||||
const cmd = <T,>(id: string, params: Record<string, unknown>) => api.rdmProjectCommand<T>(project, id, params)
|
||||
const memberPage = useMemberPickerPage(members)
|
||||
@@ -254,6 +269,9 @@ export function RdmDutiesSection({ project }: { project: Project }) {
|
||||
cmd<{ members: { owner: string; role: string }[] }>('rdm.project.member.list', { projectId })
|
||||
.then(out => { if (currentProject(epoch)) setMembers(out.members ?? []) })
|
||||
.catch(() => { if (currentProject(epoch)) setMembers([]) })
|
||||
api.rdmCommand<DocsTemplateRow[]>('rdm.docs.templates')
|
||||
.then(rows => { if (currentProject(epoch)) setTpls(rows ?? []) })
|
||||
.catch(() => { if (currentProject(epoch)) setTpls([]) })
|
||||
return () => {
|
||||
requests.current.epoch++; requests.current.selection++; requests.current.status++
|
||||
requests.current.catalog++; requests.current.action++; requests.current.selected = ''
|
||||
@@ -297,8 +315,39 @@ export function RdmDutiesSection({ project }: { project: Project }) {
|
||||
cmd('rdm.review.assign.doc.batch', { projectId, docs: [...batchSel].map(d => ({
|
||||
docId: d, author: batchAuthor[0], reviewers: batchReviewers, approver: batchApprover[0] })) }))
|
||||
|
||||
/** U4/UI:按已发布 DHF 模板整份生成文档(章节骨架自动播种),随后选中新文档。 */
|
||||
const instantiate = () => {
|
||||
const tpl = (tpls ?? []).find(r => `${r.templateId}@${r.version}` === tplSel)
|
||||
if (!tpl) return
|
||||
const title = newTitle.trim()
|
||||
return run(`已按模板生成「${title || tpl.title}」`, async () => {
|
||||
const res = await cmd<{ docId: string }>('rdm.docs.instantiate', docsInstantiateParams(tpl, title))
|
||||
await refreshCatalog()
|
||||
await selectDoc(res.docId, true)
|
||||
})
|
||||
}
|
||||
const tpl = (tpls ?? []).find(r => `${r.templateId}@${r.version}` === tplSel)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="settings-group-title">按模板生成文档</h4>
|
||||
<div className="ad-create">
|
||||
<div className="ad-row">
|
||||
<select className="lg-input" aria-label="文档模板" value={tplSel}
|
||||
onChange={e => { setTplSel(e.target.value); setNewTitle('') }}>
|
||||
<option value="">{tpls === null ? '模板加载中…' : '选择模板…'}</option>
|
||||
{(tpls ?? []).map(r => (
|
||||
<option key={`${r.templateId}@${r.version}`} value={`${r.templateId}@${r.version}`}>
|
||||
{r.title}({r.templateId}@{r.version})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input className="lg-input" aria-label="文档标题" placeholder={`文档标题(默认:${tpl?.title ?? '模板标题'})`}
|
||||
value={newTitle} onChange={e => setNewTitle(e.target.value)} />
|
||||
<button className="lg-submit ad-create-btn" disabled={busy || !tpl} onClick={instantiate}>生成文档</button>
|
||||
</div>
|
||||
<p className="ad-hint">按已发布 DHF 模板整份生成:章节骨架自动播种;生成后在下方清单选中文档分配三权并走整文审签。</p>
|
||||
</div>
|
||||
<h4 className="settings-group-title">文档清单(职责分配对象为整份文档;搜索/浏览同一目录)</h4>
|
||||
<table className="ad-table">
|
||||
<thead><tr><th></th><th>文档</th><th>类型 / 阶段</th><th>当前职责</th><th>状态</th><th></th></tr></thead>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { docsInstantiateParams } from '../src/components/adminRdmSections'
|
||||
|
||||
const tpl = {
|
||||
templateId: 'dhf.2750fbaf1f67d10c', version: '1.0.0', title: '项目建议书',
|
||||
docTypes: ['rd_proposal'], stage: '0. 商务阶段',
|
||||
}
|
||||
|
||||
describe('docsInstantiateParams', () => {
|
||||
it('carries docType/templateId/version and trims the title', () => {
|
||||
expect(docsInstantiateParams(tpl, ' U5 试点项目建议书 ')).toEqual({
|
||||
docType: 'rd_proposal', templateId: 'dhf.2750fbaf1f67d10c',
|
||||
version: '1.0.0', title: 'U5 试点项目建议书',
|
||||
})
|
||||
})
|
||||
it('omits blank titles so the backend falls back to the template title', () => {
|
||||
const p = docsInstantiateParams(tpl, ' ')
|
||||
expect(Object.hasOwn(p, 'title')).toBe(false)
|
||||
expect(p).toEqual({ docType: 'rd_proposal', templateId: 'dhf.2750fbaf1f67d10c', version: '1.0.0' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user