OutcomeOfApprovalVerdict should now be working

This commit is contained in:
Colin Dawson 2026-02-23 22:59:15 +00:00
parent a3430d221d
commit fb09052476
4 changed files with 127 additions and 84 deletions

View File

@ -1,6 +1,7 @@
{ {
"Approved": "Схвалено", "Approved": "Схвалено",
"ApprovedWithComments": "Схвалено з коментарями", "ApprovedWithComments": "Схвалено з коментарями",
"None": "Ніхто.",
"Pending": "У підготовці.", "Pending": "У підготовці.",
"Rejected": "Відхилено", "Rejected": "Відхилено",
"Reviewed": "Оглянуто." "Reviewed": "Оглянуто."

View File

@ -42,7 +42,7 @@ export interface InputProps {
placeHolder?: string; placeHolder?: string;
readOnly?: boolean; readOnly?: boolean;
type: InputType; type: InputType;
value?: string | number | readonly string[] | undefined; value?: string | number | readonly string[] | boolean | undefined;
defaultValue?: string | number | readonly string[] | undefined; defaultValue?: string | number | readonly string[] | undefined;
min?: number; min?: number;
max?: number; max?: number;
@ -56,6 +56,7 @@ export interface InputProps {
) => void; ) => void;
maxLength?: number; maxLength?: number;
options?: { value: string; label: string }[]; options?: { value: string; label: string }[];
title: string;
} }
function Input(props: InputProps) { function Input(props: InputProps) {
@ -74,6 +75,7 @@ function Input(props: InputProps) {
autoComplete, autoComplete,
onChange, onChange,
options, options,
title,
...rest ...rest
} = props; } = props;
@ -88,7 +90,7 @@ function Input(props: InputProps) {
const [showPasswordIcon, setShowPasswordIcon] = useState(faEyeSlash); const [showPasswordIcon, setShowPasswordIcon] = useState(faEyeSlash);
if (type === InputType.checkbox) { if (type === InputType.checkbox) {
checked = value === String(true); checked = value === true || value === "true";
showValue = undefined; showValue = undefined;
divClassName = "form-check allignedCheckBox"; divClassName = "form-check allignedCheckBox";
className = "form-check-input"; className = "form-check-input";
@ -154,7 +156,12 @@ function Input(props: InputProps) {
return ( return (
<div className={divClassName} hidden={hidden}> <div className={divClassName} hidden={hidden}>
{(includeLabel === true || includeLabel === undefined) && ( {(includeLabel === true || includeLabel === undefined) && (
<label className={labelClassName} htmlFor={name} hidden={hidden}> <label
className={labelClassName}
htmlFor={name}
hidden={hidden}
title={title}
>
{label} {label}
</label> </label>
)} )}
@ -178,6 +185,7 @@ function Input(props: InputProps) {
defaultValue={defaultValue} defaultValue={defaultValue}
maxLength={maxLength! > 0 ? maxLength : undefined} maxLength={maxLength! > 0 ? maxLength : undefined}
autoComplete={autoComplete} autoComplete={autoComplete}
title={title}
/> />
)} )}

View File

@ -1,8 +1,10 @@
import Button, { ButtonType } from "../../../../../components/common/Button"; import Button, { ButtonType } from "../../../../../components/common/Button";
import ErrorBlock from "../../../../../components/common/ErrorBlock"; import ErrorBlock from "../../../../../components/common/ErrorBlock";
import { InputType } from "../../../../../components/common/Input";
import VerdictPicker from "../../../../../components/pickers/VerdictPicker"; import VerdictPicker from "../../../../../components/pickers/VerdictPicker";
import ValidationErrorIcon from "../../../../../components/validationErrorIcon"; import ValidationErrorIcon from "../../../../../components/validationErrorIcon";
import { TaskDefinition } from "../../services/WorkflowTemplateService"; import { TaskDefinition } from "../../services/WorkflowTemplateService";
import { renderTaskField } from "../taskEditorHelpers";
import TaskPicker from "../TaskPicker"; import TaskPicker from "../TaskPicker";
import { import {
@ -27,6 +29,7 @@ export const outcomeEditor: React.FC<CapabilityEditorProps> = (props) => {
task: null, task: null,
}); });
clone.config.outcomeActions = list; clone.config.outcomeActions = list;
clone.config.overrideDefaultTaskProgression = false;
onChange(clone); onChange(clone);
} }
@ -51,82 +54,98 @@ export const outcomeEditor: React.FC<CapabilityEditorProps> = (props) => {
const otherTasks = tasks.filter((t) => (t.config.guid as string) !== guid); const otherTasks = tasks.filter((t) => (t.config.guid as string) !== guid);
return ( return (
<div> <>
<table> <div>
<thead> <table>
<tr> <thead>
<th></th> <tr>
<th>Verdict</th> <th></th>
<th>Task</th> <th>Verdict</th>
<th> <th>Task</th>
<Button onClick={addOutcome} buttonType={ButtonType.secondary}> <th>
Add Outcome <Button onClick={addOutcome} buttonType={ButtonType.secondary}>
</Button> Add Outcome
</th>
</tr>
</thead>
<tbody>
{outcomeActions.map((outcomeAction, index) => (
<tr key={index} className="align-top">
<td className="form-group">
<ValidationErrorIcon
visible={
!!fieldErrors?.[
`${guid}.outcomeActions[${index}].verdict`
] ||
!!fieldErrors?.[`${guid}.outcomeActions[${index}].task`]
}
/>
</td>
<td>
<VerdictPicker
includeLabel={false}
name="verdict"
label="Verdict"
value={outcomeAction.verdict}
error={
fieldErrors?.[`${guid}.outcomeActions[${index}].verdict`]
}
onChange={(name: string, val: string) =>
updateOutcome(index, { ...outcomeAction, verdict: val })
}
/>
</td>
<td>
<TaskPicker
includeLabel={false}
name="task"
label="Task"
value={(outcomeAction.task as string) ?? null}
tasks={otherTasks}
error={fieldErrors?.[`${guid}.outcomeActions[${index}].task`]}
onChange={(name, val) =>
updateOutcome(index, { ...outcomeAction, task: val })
}
/>
</td>
<td className="form-group">
<Button
onClick={() => removeOutcome(index)}
buttonType={ButtonType.secondary}
>
Remove
</Button> </Button>
</td> </th>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {outcomeActions.map((outcomeAction, index) => (
<ErrorBlock <tr key={index} className="align-top">
error={Object.values(fieldErrors ?? {}) <td className="form-group">
.filter((_, i) => <ValidationErrorIcon
Object.keys(fieldErrors ?? {}).some( visible={
(key) => key === `${guid}.outcomeActions[${i}]`, !!fieldErrors?.[
), `${guid}.outcomeActions[${index}].verdict`
) ] ||
.join("; ")} !!fieldErrors?.[`${guid}.outcomeActions[${index}].task`]
/> }
</div> />
</td>
<td>
<VerdictPicker
includeLabel={false}
name="verdict"
label="Verdict"
value={outcomeAction.verdict}
error={
fieldErrors?.[`${guid}.outcomeActions[${index}].verdict`]
}
onChange={(name: string, val: string) =>
updateOutcome(index, { ...outcomeAction, verdict: val })
}
/>
</td>
<td>
<TaskPicker
includeLabel={false}
name="task"
label="Task"
value={(outcomeAction.task as string) ?? null}
tasks={otherTasks}
error={
fieldErrors?.[`${guid}.outcomeActions[${index}].task`]
}
onChange={(name, val) =>
updateOutcome(index, { ...outcomeAction, task: val })
}
/>
</td>
<td className="form-group">
<Button
onClick={() => removeOutcome(index)}
buttonType={ButtonType.secondary}
>
Remove
</Button>
</td>
</tr>
))}
</tbody>
</table>
<ErrorBlock
error={Object.values(fieldErrors ?? {})
.filter((_, i) =>
Object.keys(fieldErrors ?? {}).some(
(key) => key === `${guid}.outcomeActions[${i}]`,
),
)
.join("; ")}
/>
</div>
{renderTaskField(
task,
onChange,
"overrideDefaultTaskProgression",
"Override Default Task Progression",
InputType.checkbox,
fieldErrors,
undefined, //placeholder
undefined, //maxLength
undefined, //extraProps
"Checking this will override the default task progression, allowing manual control over task flow. If unchecked, the default task progression will be followed if no outcome actions are triggered.", //title
)}
</>
); );
}; };
@ -178,7 +197,9 @@ export function defaultsAssignment(
task: TaskDefinition, task: TaskDefinition,
tasks: TaskDefinition[], tasks: TaskDefinition[],
ctx: defaultsContext, ctx: defaultsContext,
) {} ) {
task.config.overrideDefaultTaskProgression = true;
}
export const outcomeOfApprovalVerdictRegistryEntry: capabilityEditorRegistryEntry = export const outcomeOfApprovalVerdictRegistryEntry: capabilityEditorRegistryEntry =
{ {

View File

@ -13,21 +13,31 @@ export const renderTaskField = (
extraProps?: { extraProps?: {
options?: { value: string; label: string }[]; options?: { value: string; label: string }[];
}, },
title?: string,
) => { ) => {
const handleChange = ( const handleChange = (
e: React.ChangeEvent< e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>, >,
) => { ) => {
const newValue = e.target.value; let newValue: string | boolean;
onChange({ if (type === InputType.checkbox) {
...task, newValue = (e.target as HTMLInputElement).checked;
} else {
newValue = e.target.value;
}
const clone = structuredClone(task);
const updated = {
...clone,
config: { config: {
...task.config, ...clone.config,
[field]: newValue, [field]: newValue,
}, },
}); };
onChange(updated);
}; };
return renderTaskInput( return renderTaskInput(
@ -41,13 +51,14 @@ export const renderTaskField = (
placeholder ?? "", placeholder ?? "",
maxLength ?? 0, maxLength ?? 0,
extraProps, extraProps,
title,
); );
}; };
export const renderTaskInput = ( export const renderTaskInput = (
name: string, name: string,
label: string, label: string,
value: string | number | readonly string[] | undefined, value: string | number | readonly string[] | boolean | undefined,
error: string | undefined, error: string | undefined,
type: InputType = InputType.text, type: InputType = InputType.text,
onChange: ( onChange: (
@ -59,6 +70,7 @@ export const renderTaskInput = (
extraProps?: { extraProps?: {
options?: { value: string; label: string }[]; options?: { value: string; label: string }[];
}, },
title?: string,
) => { ) => {
const normalisedValue = const normalisedValue =
type === InputType.multiselect type === InputType.multiselect
@ -77,6 +89,7 @@ export const renderTaskInput = (
onChange={onChange} onChange={onChange}
readOnly={readOnly} readOnly={readOnly}
placeHolder={placeholder} placeHolder={placeholder}
title={title}
{...extraProps} {...extraProps}
/> />
); );