refactor: retain workflow as optional editor
This commit is contained in:
@@ -49,10 +49,13 @@ import {
|
||||
createWorkflowDefinition,
|
||||
deleteWorkflowDefinition,
|
||||
deriveWorkflowDefinition,
|
||||
getWorkflowRevision,
|
||||
listWorkflowDefinitions,
|
||||
listWorkflowNodeTypes,
|
||||
listWorkflowRevisions,
|
||||
reconcileWorkflowStandards,
|
||||
renderWorkflowBpmn,
|
||||
resetWorkflowDefinitionToStandard,
|
||||
updateWorkflowDefinition,
|
||||
validateWorkflowDefinition,
|
||||
workflowScopeReferenceProvider,
|
||||
@@ -118,6 +121,8 @@ export default function WorkflowPage({
|
||||
const [definitionSettingsOpen, setDefinitionSettingsOpen] = useState(false);
|
||||
const [deriveOpen, setDeriveOpen] = useState(false);
|
||||
const [runsOpen, setRunsOpen] = useState(false);
|
||||
const [resetOpen, setResetOpen] = useState(false);
|
||||
const [compareOpen, setCompareOpen] = useState(false);
|
||||
const bpmnFileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const canWrite = hasScope(auth, "workflow:definition:write")
|
||||
@@ -125,6 +130,10 @@ export default function WorkflowPage({
|
||||
const canEdit = canWrite && (
|
||||
!draft?.id || draft.governance?.actions.edit?.allowed !== false
|
||||
);
|
||||
const canActivate = Boolean(
|
||||
canWrite
|
||||
&& (draft?.standard?.kind === "baseline" || canEdit)
|
||||
);
|
||||
const canReuse = Boolean(
|
||||
draft?.id
|
||||
&& canWrite
|
||||
@@ -142,6 +151,10 @@ export default function WorkflowPage({
|
||||
() => definitions.find((item) => item.id === draft?.id) ?? null,
|
||||
[definitions, draft?.id]
|
||||
);
|
||||
const baselineDefinition = useMemo(() => {
|
||||
const baselineId = selectedDefinition?.standard?.baseline_definition_id;
|
||||
return definitions.find((item) => item.id === baselineId) ?? null;
|
||||
}, [definitions, selectedDefinition]);
|
||||
const dirty = Boolean(draft)
|
||||
&& workflowFingerprint(draft) !== workflowFingerprint(savedDraft);
|
||||
const displayedGraph = historicalRevision?.graph ?? draft?.graph ?? null;
|
||||
@@ -480,6 +493,42 @@ export default function WorkflowPage({
|
||||
}
|
||||
};
|
||||
|
||||
const reconcileStandards = async () => {
|
||||
setWorking(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
const result = await reconcileWorkflowStandards(settings);
|
||||
await reload(draft?.id);
|
||||
setSuccess(
|
||||
`Module standards: ${result.created} installed, ${result.updated} updated, ${result.blocked} blocked.`
|
||||
);
|
||||
} catch (reconcileError) {
|
||||
setError(apiErrorMessage(reconcileError));
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetToStandard = async () => {
|
||||
if (!draft?.id) return;
|
||||
setWorking(true);
|
||||
setError("");
|
||||
try {
|
||||
const baseline = await resetWorkflowDefinitionToStandard(
|
||||
settings,
|
||||
draft.id
|
||||
);
|
||||
setResetOpen(false);
|
||||
await reload(baseline.id);
|
||||
setSuccess("The local override was archived and the module standard restored.");
|
||||
} catch (resetError) {
|
||||
setError(apiErrorMessage(resetError));
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateGraph = (graph: WorkflowDraft["graph"]) => {
|
||||
if (graphReadOnly) return;
|
||||
setDraft((current) => current ? { ...current, graph } : current);
|
||||
@@ -532,6 +581,15 @@ export default function WorkflowPage({
|
||||
onClick={() => void reload(draft?.id)}
|
||||
disabled={loading || working}
|
||||
/>
|
||||
{hasScope(auth, "workflow:instance:admin") ? (
|
||||
<IconButton
|
||||
label="Reconcile module standards"
|
||||
icon={<GitFork size={16} />}
|
||||
variant="ghost"
|
||||
onClick={() => void reconcileStandards()}
|
||||
disabled={loading || working}
|
||||
/>
|
||||
) : null}
|
||||
<IconButton
|
||||
label="New workflow"
|
||||
icon={<Plus size={17} />}
|
||||
@@ -567,6 +625,14 @@ export default function WorkflowPage({
|
||||
<small>
|
||||
{definition.governance.scope_type} · {definition.governance.definition_kind}
|
||||
</small>
|
||||
{definition.standard ? (
|
||||
<small>
|
||||
{definition.standard.kind === "baseline"
|
||||
? `Standard · ${definition.standard.origin_module_id}`
|
||||
: `Override · ${definition.standard.origin_module_id}`}
|
||||
{definition.standard.update_available ? " · update available" : ""}
|
||||
</small>
|
||||
) : null}
|
||||
</span>
|
||||
<StatusBadge
|
||||
status={definition.status}
|
||||
@@ -634,6 +700,24 @@ export default function WorkflowPage({
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
{draft.standard?.kind === "override"
|
||||
&& draft.standard.update_available ? (
|
||||
<Button
|
||||
onClick={() => setCompareOpen(true)}
|
||||
disabled={working}
|
||||
>
|
||||
<GitFork size={16} /> Compare update
|
||||
</Button>
|
||||
) : null}
|
||||
{draft.standard?.kind === "override"
|
||||
&& draft.standard.reset_available ? (
|
||||
<Button
|
||||
onClick={() => setResetOpen(true)}
|
||||
disabled={working || dirty}
|
||||
>
|
||||
<RotateCcw size={16} /> Reset
|
||||
</Button>
|
||||
) : null}
|
||||
{historicalRevision ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -703,7 +787,9 @@ export default function WorkflowPage({
|
||||
disabled={!canReuse}
|
||||
/>
|
||||
) : null}
|
||||
{draft.id && draft.status !== "archived" ? (
|
||||
{draft.id
|
||||
&& draft.status !== "archived"
|
||||
&& draft.standard?.kind !== "baseline" ? (
|
||||
<Button
|
||||
onClick={() => void archiveDefinition()}
|
||||
disabled={!canEdit || dirty || working || readOnly}
|
||||
@@ -711,15 +797,18 @@ export default function WorkflowPage({
|
||||
<Archive size={16} /> Archive
|
||||
</Button>
|
||||
) : null}
|
||||
{draft.id && draft.status !== "active" ? (
|
||||
{draft.id && (
|
||||
draft.status !== "active"
|
||||
|| draft.activeRevision !== draft.currentRevision
|
||||
) ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void activate()}
|
||||
disabled={
|
||||
!canEdit
|
||||
!canActivate
|
||||
|| dirty
|
||||
|| working
|
||||
|| readOnly
|
||||
|| historicalRevision !== null
|
||||
|| draft.definitionKind === "template"
|
||||
}
|
||||
disabledReason={
|
||||
@@ -740,7 +829,7 @@ export default function WorkflowPage({
|
||||
>
|
||||
<Save size={16} /> Save
|
||||
</Button>
|
||||
{draft.id ? (
|
||||
{draft.id && draft.standard?.kind !== "baseline" ? (
|
||||
<IconButton
|
||||
label="Delete workflow"
|
||||
icon={<Trash2 size={16} />}
|
||||
@@ -885,6 +974,22 @@ export default function WorkflowPage({
|
||||
onCancel={() => setDeleteOpen(false)}
|
||||
onConfirm={() => void removeDefinition()}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={resetOpen}
|
||||
title="Reset to module standard"
|
||||
message={`Archive ${draft?.name ?? "this override"} and restore the latest module standard? Historical revisions and running instances remain unchanged.`}
|
||||
confirmLabel="Reset"
|
||||
busy={working}
|
||||
onCancel={() => setResetOpen(false)}
|
||||
onConfirm={() => void resetToStandard()}
|
||||
/>
|
||||
<WorkflowStandardComparisonDialog
|
||||
open={compareOpen}
|
||||
settings={settings}
|
||||
override={selectedDefinition}
|
||||
baseline={baselineDefinition}
|
||||
onClose={() => setCompareOpen(false)}
|
||||
/>
|
||||
<WorkflowDefinitionSettingsDialog
|
||||
open={definitionSettingsOpen}
|
||||
settings={settings}
|
||||
@@ -932,6 +1037,103 @@ export default function WorkflowPage({
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowStandardComparisonDialog({
|
||||
open,
|
||||
settings,
|
||||
override,
|
||||
baseline,
|
||||
onClose
|
||||
}: {
|
||||
open: boolean;
|
||||
settings: ApiSettings;
|
||||
override: WorkflowDefinition | null;
|
||||
baseline: WorkflowDefinition | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [pinned, setPinned] = useState<WorkflowRevision | null>(null);
|
||||
const [latest, setLatest] = useState<WorkflowRevision | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const standard = override?.standard;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !baseline || !standard) return;
|
||||
let cancelled = false;
|
||||
setError("");
|
||||
const pinnedRevision = standard.pinned_baseline_revision
|
||||
?? standard.active_baseline_revision
|
||||
?? 1;
|
||||
void Promise.all([
|
||||
getWorkflowRevision(settings, baseline.id, pinnedRevision),
|
||||
getWorkflowRevision(
|
||||
settings,
|
||||
baseline.id,
|
||||
standard.latest_baseline_revision
|
||||
)
|
||||
]).then(([pinnedItem, latestItem]) => {
|
||||
if (cancelled) return;
|
||||
setPinned(pinnedItem);
|
||||
setLatest(latestItem);
|
||||
}).catch((loadError) => {
|
||||
if (!cancelled) setError(apiErrorMessage(loadError));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [baseline, open, settings, standard]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title="Compare module standard update"
|
||||
className="workflow-standard-comparison-dialog"
|
||||
onClose={onClose}
|
||||
footer={<Button onClick={onClose}>Close</Button>}
|
||||
>
|
||||
{error ? (
|
||||
<DismissibleAlert tone="danger" resetKey={error}>
|
||||
{error}
|
||||
</DismissibleAlert>
|
||||
) : null}
|
||||
<div className="workflow-standard-comparison">
|
||||
<StandardRevisionColumn
|
||||
label="Pinned standard"
|
||||
revision={pinned}
|
||||
/>
|
||||
<StandardRevisionColumn
|
||||
label="Local override"
|
||||
revision={override?.revision ?? null}
|
||||
/>
|
||||
<StandardRevisionColumn
|
||||
label="Latest standard"
|
||||
revision={latest}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function StandardRevisionColumn({
|
||||
label,
|
||||
revision
|
||||
}: {
|
||||
label: string;
|
||||
revision: WorkflowRevision | null;
|
||||
}) {
|
||||
return (
|
||||
<section className="workflow-standard-revision">
|
||||
<header>
|
||||
<strong>{label}</strong>
|
||||
<small>
|
||||
{revision
|
||||
? `Revision ${revision.revision} · ${revision.content_hash.slice(0, 12)}`
|
||||
: "Loading..."}
|
||||
</small>
|
||||
</header>
|
||||
<pre>{revision ? JSON.stringify(revision.graph, null, 2) : ""}</pre>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowDefinitionSettingsDialog({
|
||||
open,
|
||||
settings,
|
||||
|
||||
Reference in New Issue
Block a user