Add governed reusable workflow definitions
This commit is contained in:
@@ -6,6 +6,8 @@ import type {
|
||||
} from "@govoplan/core-webui/definition-graph";
|
||||
|
||||
export type WorkflowStatus = "draft" | "active" | "archived";
|
||||
export type DefinitionScopeType = "system" | "tenant" | "group" | "user";
|
||||
export type DefinitionKind = "flow" | "template";
|
||||
export type WorkflowGraphNode = DefinitionGraphNode;
|
||||
export type WorkflowGraph = DefinitionGraph & {
|
||||
schema_version: 1;
|
||||
@@ -36,7 +38,7 @@ export type WorkflowRevision = {
|
||||
|
||||
export type WorkflowDefinition = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
tenant_id: string | null;
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
@@ -49,6 +51,32 @@ export type WorkflowDefinition = {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
revision: WorkflowRevision;
|
||||
governance: WorkflowGovernance;
|
||||
};
|
||||
|
||||
export type WorkflowActionDecision = {
|
||||
allowed: boolean;
|
||||
reason?: string | null;
|
||||
source_path: Array<Record<string, unknown>>;
|
||||
requirements: string[];
|
||||
details: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type WorkflowGovernance = {
|
||||
scope_type: DefinitionScopeType;
|
||||
scope_id?: string | null;
|
||||
definition_kind: DefinitionKind;
|
||||
inherit_to_lower_scopes: boolean;
|
||||
allow_start: boolean;
|
||||
allow_reuse: boolean;
|
||||
allow_automation: boolean;
|
||||
derived_from_definition_id?: string | null;
|
||||
derived_from_revision?: number | null;
|
||||
derived_from_hash?: string | null;
|
||||
derivation_provenance: Record<string, unknown>;
|
||||
actions: Record<string, WorkflowActionDecision>;
|
||||
automation_runtime_available: boolean;
|
||||
automation_runtime_reason?: string | null;
|
||||
};
|
||||
|
||||
export type WorkflowDefinitionPayload = {
|
||||
@@ -56,6 +84,13 @@ export type WorkflowDefinitionPayload = {
|
||||
description?: string | null;
|
||||
graph: WorkflowGraph;
|
||||
metadata: Record<string, unknown>;
|
||||
scope_type: DefinitionScopeType;
|
||||
scope_id?: string | null;
|
||||
definition_kind: DefinitionKind;
|
||||
inherit_to_lower_scopes: boolean;
|
||||
allow_start: boolean;
|
||||
allow_reuse: boolean;
|
||||
allow_automation: boolean;
|
||||
};
|
||||
|
||||
export async function listWorkflowNodeTypes(
|
||||
@@ -99,6 +134,31 @@ export function updateWorkflowDefinition(
|
||||
);
|
||||
}
|
||||
|
||||
export function deriveWorkflowDefinition(
|
||||
settings: ApiSettings,
|
||||
definitionId: string,
|
||||
payload: {
|
||||
key?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
source_revision?: number | null;
|
||||
metadata: Record<string, unknown>;
|
||||
scope_type: DefinitionScopeType;
|
||||
scope_id?: string | null;
|
||||
definition_kind: DefinitionKind;
|
||||
inherit_to_lower_scopes: boolean;
|
||||
allow_start: boolean;
|
||||
allow_reuse: boolean;
|
||||
allow_automation: boolean;
|
||||
}
|
||||
): Promise<WorkflowDefinition> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/workflow/definitions/${encodeURIComponent(definitionId)}/derive`,
|
||||
{ method: "POST", body: JSON.stringify(payload) }
|
||||
);
|
||||
}
|
||||
|
||||
export async function listWorkflowRevisions(
|
||||
settings: ApiSettings,
|
||||
definitionId: string
|
||||
|
||||
@@ -8,21 +8,26 @@ import {
|
||||
import {
|
||||
Archive,
|
||||
CheckCircle2,
|
||||
CopyPlus,
|
||||
GitFork,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Save,
|
||||
Settings2,
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import {
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
IconButton,
|
||||
LoadingFrame,
|
||||
StatusBadge,
|
||||
ToggleSwitch,
|
||||
hasScope,
|
||||
isApiError,
|
||||
useUnsavedChanges,
|
||||
@@ -35,6 +40,7 @@ import {
|
||||
archiveWorkflowDefinition,
|
||||
createWorkflowDefinition,
|
||||
deleteWorkflowDefinition,
|
||||
deriveWorkflowDefinition,
|
||||
listWorkflowDefinitions,
|
||||
listWorkflowNodeTypes,
|
||||
listWorkflowRevisions,
|
||||
@@ -93,13 +99,23 @@ export default function WorkflowPage({
|
||||
const [success, setSuccess] = useState("");
|
||||
const [diagnostics, setDiagnostics] = useState<WorkflowDiagnostic[]>([]);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [definitionSettingsOpen, setDefinitionSettingsOpen] = useState(false);
|
||||
const [deriveOpen, setDeriveOpen] = useState(false);
|
||||
|
||||
const canWrite = hasScope(auth, "workflow:definition:write")
|
||||
|| hasScope(auth, "workflow:instance:admin");
|
||||
const canEdit = canWrite && (
|
||||
!draft?.id || draft.governance?.actions.edit?.allowed !== false
|
||||
);
|
||||
const canReuse = Boolean(
|
||||
draft?.id
|
||||
&& canWrite
|
||||
&& draft.governance?.actions.derive?.allowed
|
||||
);
|
||||
const dirty = Boolean(draft)
|
||||
&& workflowFingerprint(draft) !== workflowFingerprint(savedDraft);
|
||||
const displayedGraph = historicalRevision?.graph ?? draft?.graph ?? null;
|
||||
const readOnly = !canWrite || historicalRevision !== null;
|
||||
const readOnly = !canEdit || historicalRevision !== null;
|
||||
const selectedNode = useMemo(
|
||||
() => displayedGraph?.nodes.find((node) => node.id === selectedNodeId) ?? null,
|
||||
[displayedGraph, selectedNodeId]
|
||||
@@ -205,7 +221,7 @@ export default function WorkflowPage({
|
||||
}, [savedDraft]);
|
||||
|
||||
const saveDraft = useCallback(async (): Promise<boolean> => {
|
||||
if (!draft || !canWrite || !draft.name.trim()) {
|
||||
if (!draft || !canEdit || !draft.name.trim()) {
|
||||
setError(
|
||||
!draft?.name.trim()
|
||||
? "Workflow name is required."
|
||||
@@ -236,7 +252,7 @@ export default function WorkflowPage({
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
}, [applyDefinition, canWrite, draft, settings]);
|
||||
}, [applyDefinition, canEdit, draft, settings]);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
@@ -413,6 +429,9 @@ export default function WorkflowPage({
|
||||
<small>
|
||||
{definition.key} · revision {definition.current_revision}
|
||||
</small>
|
||||
<small>
|
||||
{definition.governance.scope_type} · {definition.governance.definition_kind}
|
||||
</small>
|
||||
</span>
|
||||
<StatusBadge
|
||||
status={definition.status}
|
||||
@@ -502,7 +521,7 @@ export default function WorkflowPage({
|
||||
setHistoricalRevision(null);
|
||||
setDiagnostics([]);
|
||||
}}
|
||||
disabled={!canWrite}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<RotateCcw size={16} /> Restore
|
||||
</Button>
|
||||
@@ -510,10 +529,25 @@ export default function WorkflowPage({
|
||||
<Button onClick={() => void validate()} disabled={working}>
|
||||
<CheckCircle2 size={16} /> Validate
|
||||
</Button>
|
||||
<IconButton
|
||||
label="Definition settings"
|
||||
icon={<Settings2 size={16} />}
|
||||
variant="ghost"
|
||||
onClick={() => setDefinitionSettingsOpen(true)}
|
||||
/>
|
||||
{draft.id ? (
|
||||
<IconButton
|
||||
label="Reuse as scoped copy"
|
||||
icon={<CopyPlus size={16} />}
|
||||
variant="ghost"
|
||||
onClick={() => setDeriveOpen(true)}
|
||||
disabled={!canReuse}
|
||||
/>
|
||||
) : null}
|
||||
{draft.id && draft.status !== "archived" ? (
|
||||
<Button
|
||||
onClick={() => void archiveDefinition()}
|
||||
disabled={!canWrite || dirty || working || readOnly}
|
||||
disabled={!canEdit || dirty || working || readOnly}
|
||||
>
|
||||
<Archive size={16} /> Archive
|
||||
</Button>
|
||||
@@ -522,7 +556,18 @@ export default function WorkflowPage({
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void activate()}
|
||||
disabled={!canWrite || dirty || working || readOnly}
|
||||
disabled={
|
||||
!canEdit
|
||||
|| dirty
|
||||
|| working
|
||||
|| readOnly
|
||||
|| draft.definitionKind === "template"
|
||||
}
|
||||
disabledReason={
|
||||
draft.definitionKind === "template"
|
||||
? "Templates cannot be activated or started."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<CheckCircle2 size={16} /> Activate
|
||||
</Button>
|
||||
@@ -530,7 +575,7 @@ export default function WorkflowPage({
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void saveDraft()}
|
||||
disabled={!canWrite || !dirty || working || readOnly}
|
||||
disabled={!canEdit || !dirty || working || readOnly}
|
||||
>
|
||||
<Save size={16} /> Save
|
||||
</Button>
|
||||
@@ -540,7 +585,7 @@ export default function WorkflowPage({
|
||||
icon={<Trash2 size={16} />}
|
||||
variant="danger"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
disabled={!canWrite || working}
|
||||
disabled={!canEdit || working}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
@@ -674,10 +719,287 @@ export default function WorkflowPage({
|
||||
onCancel={() => setDeleteOpen(false)}
|
||||
onConfirm={() => void removeDefinition()}
|
||||
/>
|
||||
<WorkflowDefinitionSettingsDialog
|
||||
open={definitionSettingsOpen}
|
||||
draft={draft}
|
||||
editable={canEdit}
|
||||
onChange={(patch) => setDraft((current) => current ? { ...current, ...patch } : current)}
|
||||
onClose={() => setDefinitionSettingsOpen(false)}
|
||||
/>
|
||||
<DeriveWorkflowDialog
|
||||
open={deriveOpen}
|
||||
settings={settings}
|
||||
definition={draft?.id ? {
|
||||
id: draft.id,
|
||||
name: draft.name,
|
||||
revision: draft.currentRevision ?? 1
|
||||
} : null}
|
||||
onClose={() => setDeriveOpen(false)}
|
||||
onDerived={(definition) => {
|
||||
applyDefinition(definition);
|
||||
setDefinitions((current) => [
|
||||
definition,
|
||||
...current.filter((item) => item.id !== definition.id)
|
||||
]);
|
||||
setDeriveOpen(false);
|
||||
setSuccess("Created a pinned scoped copy.");
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowDefinitionSettingsDialog({
|
||||
open,
|
||||
draft,
|
||||
editable,
|
||||
onChange,
|
||||
onClose
|
||||
}: {
|
||||
open: boolean;
|
||||
draft: WorkflowDraft | null;
|
||||
editable: boolean;
|
||||
onChange: (patch: Partial<WorkflowDraft>) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const provenance = draft?.governance?.actions.edit?.source_path ?? [];
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title="Definition settings"
|
||||
className="workflow-definition-dialog"
|
||||
onClose={onClose}
|
||||
footer={<Button onClick={onClose}>Close</Button>}
|
||||
>
|
||||
{draft ? (
|
||||
<div className="workflow-definition-fields">
|
||||
<FormField
|
||||
label="Scope"
|
||||
help="The scope determines ownership, visibility, and the Policy inheritance path."
|
||||
>
|
||||
<select
|
||||
value={draft.scopeType}
|
||||
disabled={!editable || Boolean(draft.id)}
|
||||
onChange={(event) => onChange({
|
||||
scopeType: event.target.value as WorkflowDraft["scopeType"],
|
||||
scopeId: ""
|
||||
})}
|
||||
>
|
||||
<option value="system">System</option>
|
||||
<option value="tenant">Tenant</option>
|
||||
<option value="group">Group</option>
|
||||
<option value="user">User</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Scope ID">
|
||||
<input
|
||||
value={draft.scopeId}
|
||||
disabled={
|
||||
!editable
|
||||
|| Boolean(draft.id)
|
||||
|| draft.scopeType === "system"
|
||||
|| draft.scopeType === "tenant"
|
||||
}
|
||||
placeholder={
|
||||
draft.scopeType === "user"
|
||||
? "Current user when empty"
|
||||
: "Required for group"
|
||||
}
|
||||
onChange={(event) => onChange({ scopeId: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Definition kind"
|
||||
help="Templates can be reused or derived, but cannot be activated or started."
|
||||
>
|
||||
<select
|
||||
value={draft.definitionKind}
|
||||
disabled={!editable || Boolean(draft.id)}
|
||||
onChange={(event) => onChange({
|
||||
definitionKind: event.target.value as WorkflowDraft["definitionKind"],
|
||||
status: "draft"
|
||||
})}
|
||||
>
|
||||
<option value="flow">Complete flow</option>
|
||||
<option value="template">Template</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<div className="workflow-definition-toggles">
|
||||
<ToggleSwitch
|
||||
label="Visible to lower scopes"
|
||||
checked={draft.inheritToLowerScopes}
|
||||
disabled={!editable}
|
||||
onChange={(value) => onChange({ inheritToLowerScopes: value })}
|
||||
/>
|
||||
<ToggleSwitch
|
||||
label="Allow starts"
|
||||
checked={draft.allowStart}
|
||||
disabled={!editable}
|
||||
onChange={(value) => onChange({ allowStart: value })}
|
||||
/>
|
||||
<ToggleSwitch
|
||||
label="Allow reuse"
|
||||
checked={draft.allowReuse}
|
||||
disabled={!editable}
|
||||
onChange={(value) => onChange({ allowReuse: value })}
|
||||
/>
|
||||
<ToggleSwitch
|
||||
label="Allow automation"
|
||||
checked={draft.allowAutomation}
|
||||
disabled={!editable}
|
||||
onChange={(value) => onChange({ allowAutomation: value })}
|
||||
/>
|
||||
</div>
|
||||
{draft.governance?.automation_runtime_reason ? (
|
||||
<DismissibleAlert
|
||||
tone="warning"
|
||||
resetKey={draft.governance.automation_runtime_reason}
|
||||
>
|
||||
{draft.governance.automation_runtime_reason}
|
||||
</DismissibleAlert>
|
||||
) : null}
|
||||
{draft.governance?.derived_from_definition_id ? (
|
||||
<section className="workflow-provenance">
|
||||
<strong>Derived from</strong>
|
||||
<span>
|
||||
{draft.governance.derived_from_definition_id}
|
||||
{" · revision "}
|
||||
{draft.governance.derived_from_revision}
|
||||
</span>
|
||||
<code>{draft.governance.derived_from_hash}</code>
|
||||
</section>
|
||||
) : null}
|
||||
{provenance.length ? (
|
||||
<section className="workflow-provenance">
|
||||
<strong>Effective Policy path</strong>
|
||||
{provenance.map((item, index) => (
|
||||
<span key={`${String(item.path ?? item.scope_type)}-${index}`}>
|
||||
{String(item.label ?? item.path ?? item.scope_type)}
|
||||
</span>
|
||||
))}
|
||||
{!editable && draft.governance?.actions.edit?.reason ? (
|
||||
<small>{draft.governance.actions.edit.reason}</small>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function DeriveWorkflowDialog({
|
||||
open,
|
||||
settings,
|
||||
definition,
|
||||
onClose,
|
||||
onDerived
|
||||
}: {
|
||||
open: boolean;
|
||||
settings: ApiSettings;
|
||||
definition: { id: string; name: string; revision: number } | null;
|
||||
onClose: () => void;
|
||||
onDerived: (definition: WorkflowDefinition) => void;
|
||||
}) {
|
||||
const [name, setName] = useState("");
|
||||
const [kind, setKind] = useState<"flow" | "template">("flow");
|
||||
const [scopeType, setScopeType] = useState<"tenant" | "group" | "user">("tenant");
|
||||
const [scopeId, setScopeId] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setName(`${definition?.name ?? "Workflow"} copy`);
|
||||
setKind("flow");
|
||||
setScopeType("tenant");
|
||||
setScopeId("");
|
||||
setError("");
|
||||
}, [open, definition?.id]);
|
||||
|
||||
const derive = async () => {
|
||||
if (!definition || !name.trim()) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
onDerived(await deriveWorkflowDefinition(settings, definition.id, {
|
||||
name: name.trim(),
|
||||
source_revision: definition.revision,
|
||||
metadata: {},
|
||||
scope_type: scopeType,
|
||||
scope_id: scopeId.trim() || null,
|
||||
definition_kind: kind,
|
||||
inherit_to_lower_scopes: false,
|
||||
allow_start: true,
|
||||
allow_reuse: false,
|
||||
allow_automation: false
|
||||
}));
|
||||
} catch (deriveError) {
|
||||
setError(apiErrorMessage(deriveError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title="Reuse as scoped copy"
|
||||
className="workflow-definition-dialog"
|
||||
closeDisabled={busy}
|
||||
onClose={onClose}
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void derive()}
|
||||
disabled={busy || !definition || !name.trim()}
|
||||
>
|
||||
<CopyPlus size={16} /> Create copy
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="workflow-definition-fields">
|
||||
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||
<FormField label="Name">
|
||||
<input value={name} onChange={(event) => setName(event.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="Target scope">
|
||||
<select
|
||||
value={scopeType}
|
||||
onChange={(event) => setScopeType(event.target.value as typeof scopeType)}
|
||||
>
|
||||
<option value="tenant">Tenant</option>
|
||||
<option value="group">Group</option>
|
||||
<option value="user">User</option>
|
||||
</select>
|
||||
</FormField>
|
||||
{scopeType !== "tenant" ? (
|
||||
<FormField label="Scope ID">
|
||||
<input
|
||||
value={scopeId}
|
||||
placeholder={scopeType === "user" ? "Current user when empty" : "Group ID"}
|
||||
onChange={(event) => setScopeId(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
) : null}
|
||||
<FormField label="Copy kind">
|
||||
<select value={kind} onChange={(event) => setKind(event.target.value as typeof kind)}>
|
||||
<option value="flow">Complete flow</option>
|
||||
<option value="template">Template</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<small>
|
||||
The source graph revision, node-library version, content hash, and
|
||||
effective ancestor limits are retained as provenance.
|
||||
</small>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function startPaletteDrag(
|
||||
event: DragEvent<HTMLButtonElement>,
|
||||
type: string
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { createDefinitionGraphNode } from "@govoplan/core-webui/definition-graph";
|
||||
import type {
|
||||
DefinitionKind,
|
||||
DefinitionScopeType,
|
||||
WorkflowDefinition,
|
||||
WorkflowDefinitionPayload,
|
||||
WorkflowGovernance,
|
||||
WorkflowGraph,
|
||||
WorkflowGraphNode,
|
||||
WorkflowNodeType,
|
||||
@@ -17,6 +20,14 @@ export type WorkflowDraft = {
|
||||
activeRevision?: number | null;
|
||||
metadata: Record<string, unknown>;
|
||||
graph: WorkflowGraph;
|
||||
scopeType: DefinitionScopeType;
|
||||
scopeId: string;
|
||||
definitionKind: DefinitionKind;
|
||||
inheritToLowerScopes: boolean;
|
||||
allowStart: boolean;
|
||||
allowReuse: boolean;
|
||||
allowAutomation: boolean;
|
||||
governance: WorkflowGovernance | null;
|
||||
};
|
||||
|
||||
const inputPort = [{
|
||||
@@ -92,6 +103,14 @@ export function sampleWorkflowDraft(): WorkflowDraft {
|
||||
description: "",
|
||||
status: "draft",
|
||||
metadata: {},
|
||||
scopeType: "tenant",
|
||||
scopeId: "",
|
||||
definitionKind: "flow",
|
||||
inheritToLowerScopes: false,
|
||||
allowStart: true,
|
||||
allowReuse: false,
|
||||
allowAutomation: false,
|
||||
governance: null,
|
||||
graph: {
|
||||
schema_version: 1,
|
||||
nodes: [
|
||||
@@ -153,7 +172,15 @@ export function draftFromDefinition(
|
||||
currentRevision: definition.current_revision,
|
||||
activeRevision: definition.active_revision,
|
||||
metadata: structuredClone(definition.metadata),
|
||||
graph: structuredClone(definition.revision.graph)
|
||||
graph: structuredClone(definition.revision.graph),
|
||||
scopeType: definition.governance.scope_type,
|
||||
scopeId: definition.governance.scope_id ?? "",
|
||||
definitionKind: definition.governance.definition_kind,
|
||||
inheritToLowerScopes: definition.governance.inherit_to_lower_scopes,
|
||||
allowStart: definition.governance.allow_start,
|
||||
allowReuse: definition.governance.allow_reuse,
|
||||
allowAutomation: definition.governance.allow_automation,
|
||||
governance: definition.governance
|
||||
};
|
||||
}
|
||||
|
||||
@@ -164,7 +191,14 @@ export function workflowPayload(
|
||||
name: draft.name.trim(),
|
||||
description: draft.description.trim() || null,
|
||||
graph: draft.graph,
|
||||
metadata: draft.metadata
|
||||
metadata: draft.metadata,
|
||||
scope_type: draft.scopeType,
|
||||
scope_id: draft.scopeId.trim() || null,
|
||||
definition_kind: draft.definitionKind,
|
||||
inherit_to_lower_scopes: draft.inheritToLowerScopes,
|
||||
allow_start: draft.allowStart,
|
||||
allow_reuse: draft.allowReuse,
|
||||
allow_automation: draft.allowAutomation
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,7 +210,14 @@ export function workflowFingerprint(
|
||||
name: draft.name,
|
||||
description: draft.description,
|
||||
graph: draft.graph,
|
||||
metadata: draft.metadata
|
||||
metadata: draft.metadata,
|
||||
scopeType: draft.scopeType,
|
||||
scopeId: draft.scopeId,
|
||||
definitionKind: draft.definitionKind,
|
||||
inheritToLowerScopes: draft.inheritToLowerScopes,
|
||||
allowStart: draft.allowStart,
|
||||
allowReuse: draft.allowReuse,
|
||||
allowAutomation: draft.allowAutomation
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,53 @@
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.workflow-definition-dialog {
|
||||
width: min(620px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.workflow-definition-fields {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.workflow-definition-fields input,
|
||||
.workflow-definition-fields select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.workflow-definition-toggles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 18px;
|
||||
}
|
||||
|
||||
.workflow-provenance {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
border: var(--border-line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.workflow-provenance span,
|
||||
.workflow-provenance small {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.workflow-provenance code {
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.workflow-definition-toggles {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-page *,
|
||||
.workflow-page *::before,
|
||||
.workflow-page *::after {
|
||||
|
||||
Reference in New Issue
Block a user