feat: select governed workflow scope targets

This commit is contained in:
2026-07-29 14:32:54 +02:00
parent b015569b5e
commit c45e2b808c
7 changed files with 194 additions and 26 deletions

View File

@@ -21,7 +21,7 @@
"lucide-react": "^1.23.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1",
"react-router-dom": ">=7.18.2 <8",
"typescript": "^5.7.2"
},
"peerDependenciesMeta": {

View File

@@ -1,4 +1,9 @@
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
import {
apiFetch,
apiReferenceOptionProvider,
type ApiSettings,
type ReferenceOptionProvider
} from "@govoplan/core-webui";
import type {
DefinitionGraph,
DefinitionGraphNode,
@@ -216,3 +221,14 @@ export function validateWorkflowDefinition(
body: JSON.stringify({ graph })
});
}
export function workflowScopeReferenceProvider(
settings: ApiSettings,
scopeType: "user" | "group"
): ReferenceOptionProvider {
return apiReferenceOptionProvider(
settings,
"/api/v1/workflow/scope-targets",
{ scope_type: scopeType }
);
}

View File

@@ -26,6 +26,7 @@ import {
FormField,
IconButton,
LoadingFrame,
ReferenceSelect,
StatusBadge,
ToggleSwitch,
hasScope,
@@ -46,6 +47,7 @@ import {
listWorkflowRevisions,
updateWorkflowDefinition,
validateWorkflowDefinition,
workflowScopeReferenceProvider,
type WorkflowDefinition,
type WorkflowDiagnostic,
type WorkflowNodeType,
@@ -721,6 +723,7 @@ export default function WorkflowPage({
/>
<WorkflowDefinitionSettingsDialog
open={definitionSettingsOpen}
settings={settings}
draft={draft}
editable={canEdit}
onChange={(patch) => setDraft((current) => current ? { ...current, ...patch } : current)}
@@ -751,18 +754,30 @@ export default function WorkflowPage({
function WorkflowDefinitionSettingsDialog({
open,
settings,
draft,
editable,
onChange,
onClose
}: {
open: boolean;
settings: ApiSettings;
draft: WorkflowDraft | null;
editable: boolean;
onChange: (patch: Partial<WorkflowDraft>) => void;
onClose: () => void;
}) {
const provenance = draft?.governance?.actions.edit?.source_path ?? [];
const referenceScopeType = draft?.scopeType === "group" ? "group" : "user";
const scopeProvider = useMemo(
() => workflowScopeReferenceProvider(settings, referenceScopeType),
[
referenceScopeType,
settings.accessToken,
settings.apiBaseUrl,
settings.apiKey
]
);
return (
<Dialog
open={open}
@@ -791,23 +806,34 @@ function WorkflowDefinitionSettingsDialog({
<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" || draft.scopeType === "group" ? (
<FormField
label={draft.scopeType === "user" ? "User" : "Group"}
help={
draft.scopeType === "user"
? "Current user when empty"
: "Required for group"
? "The stable account ID is stored; directory labels are presentation-only."
: "Only groups available in the active tenant can be selected."
}
onChange={(event) => onChange({ scopeId: event.target.value })}
/>
</FormField>
>
<ReferenceSelect
value={draft.scopeId}
onChange={(value) => onChange({ scopeId: value })}
provider={scopeProvider}
aria-label={
draft.scopeType === "user"
? "Definition user"
: "Definition group"
}
placeholder={
draft.scopeType === "user"
? "Select a user"
: "Select a group"
}
disabled={!editable || Boolean(draft.id)}
required
/>
</FormField>
) : null}
<FormField
label="Definition kind"
help="Templates can be reused or derived, but cannot be activated or started."
@@ -907,6 +933,19 @@ function DeriveWorkflowDialog({
const [scopeId, setScopeId] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const scopeProvider = useMemo(
() =>
workflowScopeReferenceProvider(
settings,
scopeType === "group" ? "group" : "user"
),
[
scopeType,
settings.accessToken,
settings.apiBaseUrl,
settings.apiKey
]
);
useEffect(() => {
if (!open) return;
@@ -954,7 +993,12 @@ function DeriveWorkflowDialog({
<Button
variant="primary"
onClick={() => void derive()}
disabled={busy || !definition || !name.trim()}
disabled={
busy
|| !definition
|| !name.trim()
|| (scopeType !== "tenant" && !scopeId)
}
>
<CopyPlus size={16} /> Create copy
</Button>
@@ -969,7 +1013,10 @@ function DeriveWorkflowDialog({
<FormField label="Target scope">
<select
value={scopeType}
onChange={(event) => setScopeType(event.target.value as typeof scopeType)}
onChange={(event) => {
setScopeType(event.target.value as typeof scopeType);
setScopeId("");
}}
>
<option value="tenant">Tenant</option>
<option value="group">Group</option>
@@ -977,11 +1024,19 @@ function DeriveWorkflowDialog({
</select>
</FormField>
{scopeType !== "tenant" ? (
<FormField label="Scope ID">
<input
<FormField label={scopeType === "user" ? "User" : "Group"}>
<ReferenceSelect
value={scopeId}
placeholder={scopeType === "user" ? "Current user when empty" : "Group ID"}
onChange={(event) => setScopeId(event.target.value)}
onChange={(value) => setScopeId(value)}
provider={scopeProvider}
aria-label={
scopeType === "user" ? "Copy target user" : "Copy target group"
}
placeholder={
scopeType === "user" ? "Select a user" : "Select a group"
}
disabled={busy}
required
/>
</FormField>
) : null}