fix(web): 激活前预检人工审批并消除密钥框密码警告
未纳入人工审批的计划会被服务端拒绝为 400。配置页在请求前拦截并提示;管理台密钥输入放入表单,避免浏览器把密码框当成未托管字段。
This commit is contained in:
@@ -181,13 +181,19 @@ export const EnvSettingsSection: React.FC = () => {
|
||||
{s.secret && !s.hasValue ? ' · 未配置' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', flexShrink: 0 }}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (canSave && savingKey !== s.key) void handleSave(s);
|
||||
}}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '10px', flexShrink: 0 }}
|
||||
>
|
||||
<input
|
||||
type={s.secret ? 'password' : 'text'}
|
||||
value={String(draft ?? '')}
|
||||
placeholder={s.secret ? (s.hasValue ? '留空保持原值 / 填写新值' : '填写密钥') : undefined}
|
||||
disabled={!s.writable}
|
||||
autoComplete="off"
|
||||
autoComplete={s.secret ? 'new-password' : 'off'}
|
||||
onChange={(e) => setDrafts((prev) => ({ ...prev, [s.key]: e.target.value }))}
|
||||
style={{
|
||||
width: '260px',
|
||||
@@ -202,9 +208,8 @@ export const EnvSettingsSection: React.FC = () => {
|
||||
/>
|
||||
{s.writable && (
|
||||
<button
|
||||
type="button"
|
||||
type="submit"
|
||||
disabled={!canSave || savingKey === s.key}
|
||||
onClick={() => void handleSave(s)}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
borderRadius: '6px',
|
||||
@@ -219,7 +224,7 @@ export const EnvSettingsSection: React.FC = () => {
|
||||
{savingKey === s.key ? '保存中…' : '保存'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -33,7 +33,9 @@ import {
|
||||
clientDutyIssues,
|
||||
formatDutyIssues,
|
||||
locateDutyIssue,
|
||||
needsGovernedDocument,
|
||||
needsThreeMembersForDuties,
|
||||
NO_GOVERNED_DOCUMENT_MESSAGE,
|
||||
resolveClientDocTaskDuty,
|
||||
toggleAuthorOnly,
|
||||
toggleHumanApproval,
|
||||
@@ -226,6 +228,10 @@ export const ProjectSettingsModal: React.FC<ProjectSettingsModalProps> = ({
|
||||
setError(`当前项目只有 ${members.length} 名有效成员。纳入人工审批的文档需要分别指定编制、审核、批准负责人。`);
|
||||
return;
|
||||
}
|
||||
if (!liveEdit && !needsGovernedDocument(plan)) {
|
||||
setError(NO_GOVERNED_DOCUMENT_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -641,6 +647,20 @@ export const ProjectSettingsModal: React.FC<ProjectSettingsModalProps> = ({
|
||||
<p style={{ fontSize: '13px', color: 'var(--text-muted)', margin: 0 }}>
|
||||
允许裁剪的文档可选择是否纳入人工审批。关闭后仍会生成并进入版本账本,只是不走三权。
|
||||
</p>
|
||||
{!liveEdit && !needsGovernedDocument(plan) && (
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
background: 'var(--warning-50, #fffbeb)',
|
||||
border: '1px solid var(--warning-200, #fde68a)',
|
||||
borderRadius: '8px',
|
||||
color: 'var(--warning-800, #92400e)',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
{NO_GOVERNED_DOCUMENT_MESSAGE}。请勾选「纳入人工审批」并指定负责人;成员不足三人时可用「由编制人兼任审核、批准」。
|
||||
</div>
|
||||
)}
|
||||
{needsThreeMembersForDuties(plan, requireApprovalKeys) && members.length < 3 && (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
classifyClientDuty,
|
||||
clientDutyIssues,
|
||||
locateDutyIssue,
|
||||
needsGovernedDocument,
|
||||
needsThreeMembersForDuties,
|
||||
resolveClientDocTaskDuty,
|
||||
toggleAuthorOnly,
|
||||
@@ -67,6 +68,36 @@ describe('projectSettingsDuties', () => {
|
||||
assert.equal(locateDutyIssue(switchedOn, 'research')?.code, 'DUTY_ASSIGNMENT_REQUIRED');
|
||||
});
|
||||
|
||||
it('blocks activation preview when no document is under human approval', () => {
|
||||
const unmanaged = plan([
|
||||
{ task_key: 'research', enabled: true, approval_configurable: true, duties: {} },
|
||||
{ task_key: 'quote', enabled: true, approval_configurable: true, duties: {} },
|
||||
]);
|
||||
assert.equal(needsGovernedDocument(unmanaged), false);
|
||||
assert.deepEqual(clientDutyIssues(unmanaged, [{ userId: 1 }]), []);
|
||||
|
||||
const authorOnly = plan([
|
||||
{
|
||||
task_key: 'intake',
|
||||
enabled: true,
|
||||
approval_configurable: true,
|
||||
duty_policy: 'author_only',
|
||||
duties: { author_user_id: 7 },
|
||||
},
|
||||
]);
|
||||
assert.equal(needsGovernedDocument(authorOnly), true);
|
||||
|
||||
const separated = plan([
|
||||
{
|
||||
task_key: 'intake',
|
||||
enabled: true,
|
||||
approval_configurable: true,
|
||||
duties: { author_user_id: 1, reviewer_user_id: 2, approver_user_id: 3 },
|
||||
},
|
||||
]);
|
||||
assert.equal(needsGovernedDocument(separated), true);
|
||||
});
|
||||
|
||||
it('allows an all-none configurable draft without three members', () => {
|
||||
const draft = plan([
|
||||
{ task_key: 'research', enabled: true, approval_configurable: true, duties: {} },
|
||||
|
||||
@@ -189,6 +189,17 @@ export function clientDutyIssues(
|
||||
return issues;
|
||||
}
|
||||
|
||||
/** 对齐服务端 validateActivationPlan:激活前至少一份文档为完整人工审批(含兼任派生的 A/A/A)。 */
|
||||
export const NO_GOVERNED_DOCUMENT_MESSAGE = '至少一份文档必须纳入人工审批才能激活工作流';
|
||||
|
||||
export function needsGovernedDocument(plan: WorkflowPlan): boolean {
|
||||
return plan.stages.some(
|
||||
(stage) =>
|
||||
stage.enabled &&
|
||||
stage.doc_tasks.some((task) => task.enabled && resolveClientDocTaskDuty(task).mode === 'full'),
|
||||
);
|
||||
}
|
||||
|
||||
export function needsThreeMembersForDuties(plan: WorkflowPlan, requireApprovalKeys: string[] = []): boolean {
|
||||
return plan.stages.some(
|
||||
(stage) =>
|
||||
|
||||
Reference in New Issue
Block a user