Files
fusion/plugins/fusion-plugin-reports/src/approval.ts
Fusion 63fe25e014 feat(FN-3787): add approval workflow and share blocks to reports plugin
Added an approval workflow to the reports plugin comprising a state machine (`approval.ts`), share blocks logic (`share-blocks.ts`), API routes for approvals, and two new dashboard panels (ReportApprovalPanel and ShareBlocksPanel), with corresponding tests; also updated the plugin README and added a

Fusion-Task-Id: FN-3787
2026-05-10 15:34:09 -07:00

98 lines
3.0 KiB
TypeScript

import type { Report, ReportStatus } from "./store/report-types.js";
export type ApprovalState = "not_required" | "awaiting_approval" | "approved" | "rejected" | "published";
export type ApprovalAction = "approve" | "reject" | "publish";
export interface ApprovalDecision {
decidedBy: string;
decidedAt: string;
note?: string;
action: ApprovalAction;
}
export interface ApprovalSettings {
approvalRequired: boolean;
autoPublishOnApproval: boolean;
approverAgentIds: string[];
publishTargets: string[];
}
export interface ApprovalActor {
id: string;
type: "human" | "agent";
}
export type ApprovalError = "invalid_transition" | "unauthorized";
export function initializeApprovalState(reportStatus: ReportStatus, settings: ApprovalSettings): ApprovalState {
if (reportStatus !== "review_complete") return "not_required";
if (settings.approvalRequired) return "awaiting_approval";
return settings.autoPublishOnApproval ? "published" : "approved";
}
export function nextApprovalState(
current: ApprovalState,
action: ApprovalAction,
settings: ApprovalSettings,
actor: ApprovalActor,
): { next: ApprovalState } | { error: ApprovalError } {
if (!isAuthorized(settings, actor)) return { error: "unauthorized" };
if (current === "awaiting_approval" && action === "approve") {
return { next: settings.autoPublishOnApproval ? "published" : "approved" };
}
if (current === "awaiting_approval" && action === "reject") return { next: "rejected" };
if (current === "approved" && action === "publish") return { next: "published" };
return { error: "invalid_transition" };
}
export function applyDecision(
report: Report,
decision: ApprovalDecision,
settings: ApprovalSettings,
actor: ApprovalActor,
):
| { error: ApprovalError }
| {
updatedReport: Partial<Report>;
sideEffects: { publishTargets: string[] };
} {
const transition = nextApprovalState(report.approvalState, decision.action, settings, actor);
if ("error" in transition) return transition;
const approvalHistory = [...report.approvalHistory, decision];
const update: Partial<Report> = {
approvalState: transition.next,
approvalHistory,
};
if (transition.next === "approved") {
update.status = "approved";
update.approvedAt = decision.decidedAt;
update.approvedBy = decision.decidedBy;
}
if (transition.next === "published") {
update.status = "published";
update.publishedAt = decision.decidedAt;
if (decision.action === "approve") {
update.approvedAt = decision.decidedAt;
update.approvedBy = decision.decidedBy;
}
}
return {
updatedReport: update,
sideEffects: {
publishTargets: transition.next === "published" ? [...settings.publishTargets] : [],
},
};
}
function isAuthorized(settings: ApprovalSettings, actor: ApprovalActor): boolean {
if (!settings.approvalRequired) return true;
const approvers = settings.approverAgentIds;
if (approvers.length === 0) return actor.type === "human";
if (actor.type !== "agent") return false;
return approvers.includes(actor.id);
}