feat(web): resolve client duty_policy for author_only plans
Align TypeScript types and projectSettingsDuties with backend ResolveDocTaskDuty so author_only no longer fails as partial. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,17 +12,62 @@ export function classifyClientDuty(duty?: DocDuty): ClientDutyMode {
|
||||
return 'partial';
|
||||
}
|
||||
|
||||
/** 对齐 Go ResolveDocTaskDuty:author_only 仅要求编制人,有效三职派生为 A/A/A。 */
|
||||
export function resolveClientDocTaskDuty(task: DocTaskPlan): {
|
||||
mode: ClientDutyMode;
|
||||
effective: DocDuty;
|
||||
isAuthorOnly: boolean;
|
||||
} {
|
||||
if (!task.enabled) {
|
||||
return { mode: 'none', effective: {}, isAuthorOnly: false };
|
||||
}
|
||||
const policy = (task.duty_policy || '').trim();
|
||||
if (policy === 'author_only') {
|
||||
const author = task.duties?.author_user_id ?? 0;
|
||||
if (author <= 0) {
|
||||
return { mode: 'partial', effective: {}, isAuthorOnly: true };
|
||||
}
|
||||
return {
|
||||
mode: 'full',
|
||||
effective: {
|
||||
author_user_id: author,
|
||||
reviewer_user_id: author,
|
||||
approver_user_id: author,
|
||||
},
|
||||
isAuthorOnly: true,
|
||||
};
|
||||
}
|
||||
if (policy && policy !== 'separated') {
|
||||
return { mode: 'partial', effective: {}, isAuthorOnly: false };
|
||||
}
|
||||
const mode = classifyClientDuty(task.duties);
|
||||
return { mode, effective: { ...(task.duties || {}) }, isAuthorOnly: false };
|
||||
}
|
||||
|
||||
export function canToggleHumanApproval(task: Pick<DocTaskPlan, 'approval_configurable'>, planning: boolean): boolean {
|
||||
return planning && Boolean(task.approval_configurable);
|
||||
}
|
||||
|
||||
export function toggleHumanApproval(task: DocTaskPlan, managed: boolean): DocTaskPlan {
|
||||
if (!managed) {
|
||||
return { ...task, duties: {} };
|
||||
return { ...task, duty_policy: '', duties: {} };
|
||||
}
|
||||
return { ...task };
|
||||
}
|
||||
|
||||
/** 切换兼任模式:开启时只保留编制人并清空审核/批准;关闭时清除 duty_policy。 */
|
||||
export function toggleAuthorOnly(task: DocTaskPlan, enabled: boolean): DocTaskPlan {
|
||||
if (!enabled) {
|
||||
return { ...task, duty_policy: '' };
|
||||
}
|
||||
const author = task.duties?.author_user_id;
|
||||
return {
|
||||
...task,
|
||||
duty_policy: 'author_only',
|
||||
duties: author ? { author_user_id: author } : {},
|
||||
};
|
||||
}
|
||||
|
||||
export function clientDutyIssues(
|
||||
plan: WorkflowPlan,
|
||||
members: Array<{ userId: number }>,
|
||||
@@ -35,9 +80,50 @@ export function clientDutyIssues(
|
||||
.flatMap((stage) => stage.doc_tasks.filter((task) => task.enabled));
|
||||
|
||||
for (const task of enabled) {
|
||||
const mode = classifyClientDuty(task.duties);
|
||||
const field = `${task.task_key}.duties`;
|
||||
const label = task.task_title || task.task_key;
|
||||
const policy = (task.duty_policy || '').trim();
|
||||
|
||||
if (policy && policy !== 'separated' && policy !== 'author_only') {
|
||||
issues.push({
|
||||
code: 'UNKNOWN_DUTY_POLICY',
|
||||
field: `${task.task_key}.duty_policy`,
|
||||
message: `文档「${label}」的职责模式无效`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (policy === 'author_only') {
|
||||
const author = task.duties?.author_user_id ?? 0;
|
||||
if (author <= 0) {
|
||||
issues.push({
|
||||
code: 'AUTHOR_ONLY_AUTHOR_REQUIRED',
|
||||
field,
|
||||
message: `文档「${label}」兼任模式必须指定编制人`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const reviewer = task.duties?.reviewer_user_id ?? 0;
|
||||
const approver = task.duties?.approver_user_id ?? 0;
|
||||
if ((reviewer > 0 && reviewer !== author) || (approver > 0 && approver !== author)) {
|
||||
issues.push({
|
||||
code: 'AUTHOR_ONLY_DUTY_CONFLICT',
|
||||
field,
|
||||
message: `文档「${label}」兼任模式不能指定与编制人不同的审核人或批准人`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!memberIDs.has(author)) {
|
||||
issues.push({
|
||||
code: 'DUTY_ASSIGNEE_NOT_ACTIVE_MEMBER',
|
||||
field: `${task.task_key}.author`,
|
||||
message: `文档「${label}」的负责人不是当前成员`,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const { mode, effective } = resolveClientDocTaskDuty(task);
|
||||
if (mode === 'partial') {
|
||||
issues.push({
|
||||
code: 'DUTY_PARTIAL_ASSIGNMENT',
|
||||
@@ -55,7 +141,7 @@ export function clientDutyIssues(
|
||||
continue;
|
||||
}
|
||||
if (mode === 'full') {
|
||||
const ids = [task.duties.author_user_id, task.duties.reviewer_user_id, task.duties.approver_user_id];
|
||||
const ids = [effective.author_user_id, effective.reviewer_user_id, effective.approver_user_id];
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
issues.push({
|
||||
code: 'SEPARATION_OF_DUTY_VIOLATION',
|
||||
@@ -64,9 +150,9 @@ export function clientDutyIssues(
|
||||
});
|
||||
}
|
||||
for (const [role, id] of [
|
||||
['author', task.duties.author_user_id],
|
||||
['reviewer', task.duties.reviewer_user_id],
|
||||
['approver', task.duties.approver_user_id],
|
||||
['author', effective.author_user_id],
|
||||
['reviewer', effective.reviewer_user_id],
|
||||
['approver', effective.approver_user_id],
|
||||
] as const) {
|
||||
if (id && !memberIDs.has(id)) {
|
||||
issues.push({
|
||||
@@ -82,13 +168,16 @@ export function clientDutyIssues(
|
||||
}
|
||||
|
||||
export function needsThreeMembersForDuties(plan: WorkflowPlan, requireApprovalKeys: string[] = []): boolean {
|
||||
return plan.stages.some((stage) =>
|
||||
stage.enabled &&
|
||||
stage.doc_tasks.some((task) => {
|
||||
if (!task.enabled) return false;
|
||||
if (!task.approval_configurable || requireApprovalKeys.includes(task.task_key)) return true;
|
||||
return classifyClientDuty(task.duties) === 'full';
|
||||
}),
|
||||
return plan.stages.some(
|
||||
(stage) =>
|
||||
stage.enabled &&
|
||||
stage.doc_tasks.some((task) => {
|
||||
if (!task.enabled) return false;
|
||||
const { mode, isAuthorOnly } = resolveClientDocTaskDuty(task);
|
||||
if (isAuthorOnly) return false;
|
||||
if (!task.approval_configurable || requireApprovalKeys.includes(task.task_key)) return true;
|
||||
return mode === 'full';
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,15 @@ export interface DocDuty {
|
||||
approver_user_id?: number;
|
||||
}
|
||||
|
||||
export type DutyPolicy = 'separated' | 'author_only';
|
||||
|
||||
export interface DocTaskPlan {
|
||||
task_key: string;
|
||||
task_title?: string;
|
||||
enabled: boolean;
|
||||
approval_configurable?: boolean;
|
||||
/** 逐文档职责模式;缺省/空值按 separated 解读。 */
|
||||
duty_policy?: DutyPolicy | '';
|
||||
duties: DocDuty;
|
||||
}
|
||||
|
||||
@@ -48,6 +52,12 @@ export interface WorkflowEventSummary {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface GovernancePolicyCapability {
|
||||
allowed: boolean;
|
||||
revision: number;
|
||||
blocked_reason?: string;
|
||||
}
|
||||
|
||||
export interface ProjectSettings {
|
||||
project_id: number;
|
||||
lifecycle_status: string;
|
||||
@@ -62,6 +72,7 @@ export interface ProjectSettings {
|
||||
live_instance?: { id: number; status: string; config_revision: number };
|
||||
published_workflows?: PublishedOption[];
|
||||
recent_workflow_events?: WorkflowEventSummary[];
|
||||
author_only_policy?: GovernancePolicyCapability;
|
||||
}
|
||||
|
||||
export interface TerminationPreview {
|
||||
@@ -72,6 +83,12 @@ export interface TerminationPreview {
|
||||
impact?: string;
|
||||
}
|
||||
|
||||
export interface PreviewDocRef {
|
||||
task_key: string;
|
||||
title: string;
|
||||
author_user_id?: number;
|
||||
}
|
||||
|
||||
export interface ActivationPreview {
|
||||
confirmation_token: string;
|
||||
plan_hash: string;
|
||||
@@ -85,7 +102,9 @@ export interface ActivationPreview {
|
||||
governed_doc_tasks?: number;
|
||||
unmanaged_doc_tasks?: number;
|
||||
assigned_duties: number;
|
||||
unmanaged_documents?: Array<{ task_key: string; title: string }>;
|
||||
author_only_doc_tasks?: number;
|
||||
unmanaged_documents?: PreviewDocRef[];
|
||||
author_only_documents?: PreviewDocRef[];
|
||||
first_stage_key?: string;
|
||||
first_stage_name?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user