Added the approval IOutcome<verdict> capability to the codebase.
This commit is contained in:
parent
5a76155e8d
commit
a3430d221d
8
public/locales/bg/verdict.json
Normal file
8
public/locales/bg/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Утврђено",
|
||||||
|
"ApprovedWithComments": "Усвојено уз коментаре",
|
||||||
|
"None": "Ниједан.",
|
||||||
|
"Pending": "У очекивању",
|
||||||
|
"Rejected": "Одбијено",
|
||||||
|
"Reviewed": "Прегледано."
|
||||||
|
}
|
||||||
8
public/locales/cs/verdict.json
Normal file
8
public/locales/cs/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Schváleno",
|
||||||
|
"ApprovedWithComments": "Schváleno s poznámkami",
|
||||||
|
"None": "",
|
||||||
|
"Pending": "Čeká se na další kroky.",
|
||||||
|
"Rejected": "Odmítnuto.",
|
||||||
|
"Reviewed": "Přezkoumáno"
|
||||||
|
}
|
||||||
8
public/locales/de/verdict.json
Normal file
8
public/locales/de/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Genehmigt",
|
||||||
|
"ApprovedWithComments": "Genehmigt mit Anmerkungen",
|
||||||
|
"None": "Nichts.",
|
||||||
|
"Pending": "Wird noch bearbeitet.",
|
||||||
|
"Rejected": "Abgelehnt",
|
||||||
|
"Reviewed": "Überprüft."
|
||||||
|
}
|
||||||
8
public/locales/en/verdict.json
Normal file
8
public/locales/en/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Approved",
|
||||||
|
"ApprovedWithComments": "Approved with Comments",
|
||||||
|
"None": "None",
|
||||||
|
"Pending": "Pending",
|
||||||
|
"Rejected": "Rejected",
|
||||||
|
"Reviewed": "Reviewed"
|
||||||
|
}
|
||||||
8
public/locales/es/verdict.json
Normal file
8
public/locales/es/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Aprobado",
|
||||||
|
"ApprovedWithComments": "Aprobado con comentarios.",
|
||||||
|
"None": "Ninguno",
|
||||||
|
"Pending": "Pendiente",
|
||||||
|
"Rejected": "Rechazado",
|
||||||
|
"Reviewed": "Revisado"
|
||||||
|
}
|
||||||
8
public/locales/fr/verdict.json
Normal file
8
public/locales/fr/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Approuvé",
|
||||||
|
"ApprovedWithComments": "Approuvé avec des observations.",
|
||||||
|
"None": "Aucun",
|
||||||
|
"Pending": "En attente",
|
||||||
|
"Rejected": "Rejeté",
|
||||||
|
"Reviewed": "Revisé"
|
||||||
|
}
|
||||||
8
public/locales/ko/verdict.json
Normal file
8
public/locales/ko/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "승인됨",
|
||||||
|
"ApprovedWithComments": "의견을 반영하여 승인되었습니다.",
|
||||||
|
"None": "",
|
||||||
|
"Pending": "대기 중",
|
||||||
|
"Rejected": "거절당했습니다.",
|
||||||
|
"Reviewed": "리뷰 완료"
|
||||||
|
}
|
||||||
8
public/locales/nl/verdict.json
Normal file
8
public/locales/nl/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Goedkeurd",
|
||||||
|
"ApprovedWithComments": "Goedkeurd met opmerkingen",
|
||||||
|
"None": "Niet van toepassing.",
|
||||||
|
"Pending": "In afwachting",
|
||||||
|
"Rejected": "Afgekeurd",
|
||||||
|
"Reviewed": "Beoordeeld."
|
||||||
|
}
|
||||||
8
public/locales/pl/verdict.json
Normal file
8
public/locales/pl/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Zatwierdzone",
|
||||||
|
"ApprovedWithComments": "Zatwierdzone z komentarzami.",
|
||||||
|
"None": "Nie ma nic.",
|
||||||
|
"Pending": "W oczekiwaniu.",
|
||||||
|
"Rejected": "Odrzucono.",
|
||||||
|
"Reviewed": "Przegłoszono."
|
||||||
|
}
|
||||||
8
public/locales/pt/verdict.json
Normal file
8
public/locales/pt/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Aprovado",
|
||||||
|
"ApprovedWithComments": "Aprovado com comentários.",
|
||||||
|
"None": "Nenhum.",
|
||||||
|
"Pending": "A aguardar",
|
||||||
|
"Rejected": "Rejeitado",
|
||||||
|
"Reviewed": "Revisado"
|
||||||
|
}
|
||||||
7
public/locales/uk/verdict.json
Normal file
7
public/locales/uk/verdict.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"Approved": "Схвалено",
|
||||||
|
"ApprovedWithComments": "Схвалено з коментарями",
|
||||||
|
"Pending": "У підготовці.",
|
||||||
|
"Rejected": "Відхилено",
|
||||||
|
"Reviewed": "Оглянуто."
|
||||||
|
}
|
||||||
8
public/locales/ur/verdict.json
Normal file
8
public/locales/ur/verdict.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Approved": "مंजूर कर दिया गया।",
|
||||||
|
"ApprovedWithComments": "تجویز دیا गया है, साथ ही कुछ टिप्पणियाँ भी की गई हैं।",
|
||||||
|
"None": "کुछ नहीं।",
|
||||||
|
"Pending": "**पेंडिंग**",
|
||||||
|
"Rejected": "رد دیا گया।",
|
||||||
|
"Reviewed": "سमीक्षा की गई।"
|
||||||
|
}
|
||||||
64
src/components/pickers/VerdictPicker.tsx
Normal file
64
src/components/pickers/VerdictPicker.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
|
import Select from "../common/Select";
|
||||||
|
import Option from "../common/option";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Namespaces } from "../../i18n/i18n";
|
||||||
|
|
||||||
|
interface VerdictPickerProps {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
error?: string;
|
||||||
|
value: string;
|
||||||
|
onChange?: (name: string, value: string) => void;
|
||||||
|
includeLabel?: boolean;
|
||||||
|
}
|
||||||
|
export default function VerdictPicker({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
includeLabel = true,
|
||||||
|
}: VerdictPickerProps) {
|
||||||
|
const [options, setOptions] = useState<Option[] | undefined>(undefined);
|
||||||
|
const { t } = useTranslation(Namespaces.Verdict);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
const opts: Option[] = [
|
||||||
|
{ _id: "None", name: t("None") },
|
||||||
|
{ _id: "Pending", name: t("Pending") },
|
||||||
|
{ _id: "ApprovedWithComments", name: t("ApprovedWithComments") },
|
||||||
|
{ _id: "Approved", name: t("Approved") },
|
||||||
|
{ _id: "Rejected", name: t("Rejected") },
|
||||||
|
{ _id: "Reviewed", name: t("Reviewed") },
|
||||||
|
];
|
||||||
|
|
||||||
|
setOptions(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const input = e.currentTarget;
|
||||||
|
|
||||||
|
if (onChange) onChange(input.name, input.value);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
name={name}
|
||||||
|
label={label}
|
||||||
|
error={error}
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
includeBlankFirstEntry={false}
|
||||||
|
onChange={handleChange}
|
||||||
|
includeLabel={includeLabel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,188 @@
|
|||||||
|
import Button, { ButtonType } from "../../../../../components/common/Button";
|
||||||
|
import ErrorBlock from "../../../../../components/common/ErrorBlock";
|
||||||
|
import VerdictPicker from "../../../../../components/pickers/VerdictPicker";
|
||||||
|
import ValidationErrorIcon from "../../../../../components/validationErrorIcon";
|
||||||
|
import { TaskDefinition } from "../../services/WorkflowTemplateService";
|
||||||
|
import TaskPicker from "../TaskPicker";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CapabilityEditorProps,
|
||||||
|
capabilityEditorRegistryEntry,
|
||||||
|
defaultsContext,
|
||||||
|
} from "../useCapabilityDefaults";
|
||||||
|
|
||||||
|
export interface IOutcomeAction {
|
||||||
|
verdict: string;
|
||||||
|
task: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const outcomeEditor: React.FC<CapabilityEditorProps> = (props) => {
|
||||||
|
const { task, tasks, onChange, fieldErrors } = props;
|
||||||
|
|
||||||
|
function addOutcome() {
|
||||||
|
const clone = structuredClone(task);
|
||||||
|
const list = (clone.config.outcomeActions ?? []) as IOutcomeAction[];
|
||||||
|
list.push({
|
||||||
|
verdict: "None",
|
||||||
|
task: null,
|
||||||
|
});
|
||||||
|
clone.config.outcomeActions = list;
|
||||||
|
onChange(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOutcome(index: number, updated: IOutcomeAction) {
|
||||||
|
const clone = structuredClone(task);
|
||||||
|
const list = (clone.config.outcomeActions ?? []) as IOutcomeAction[];
|
||||||
|
list[index] = updated;
|
||||||
|
clone.config.outcomeActions = list;
|
||||||
|
onChange(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOutcome(index: number) {
|
||||||
|
const clone = structuredClone(task);
|
||||||
|
const list = clone.config.outcomeActions ?? [];
|
||||||
|
clone.config.outcomeActions = list.filter((_, i) => i !== index);
|
||||||
|
onChange(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outcomeActions = (task.config.outcomeActions ?? []) as IOutcomeAction[];
|
||||||
|
const guid = task.config.guid as string;
|
||||||
|
|
||||||
|
const otherTasks = tasks.filter((t) => (t.config.guid as string) !== guid);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Verdict</th>
|
||||||
|
<th>Task</th>
|
||||||
|
<th>
|
||||||
|
<Button onClick={addOutcome} buttonType={ButtonType.secondary}>
|
||||||
|
Add Outcome
|
||||||
|
</Button>
|
||||||
|
</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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ErrorBlock
|
||||||
|
error={Object.values(fieldErrors ?? {})
|
||||||
|
.filter((_, i) =>
|
||||||
|
Object.keys(fieldErrors ?? {}).some(
|
||||||
|
(key) => key === `${guid}.outcomeActions[${i}]`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.join("; ")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runValidation = (
|
||||||
|
task: TaskDefinition,
|
||||||
|
tasks: TaskDefinition[],
|
||||||
|
): Record<string, string> => {
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
|
const guid = task.config.guid as string;
|
||||||
|
const outcomeActions =
|
||||||
|
(task.config.outcomeActions as IOutcomeAction[]) ?? undefined;
|
||||||
|
|
||||||
|
if (!outcomeActions) {
|
||||||
|
// No outcome actions is valid, it just means there are no outcomes configured
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rule 1: Task must be selected ---
|
||||||
|
outcomeActions.forEach((o, index) => {
|
||||||
|
if (!o.task) {
|
||||||
|
errors[`${guid}.outcomeActions[${index}].task`] = "Can not be empty";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Rule 2: Detect duplicates (verdict + task) ---
|
||||||
|
const keyCounts = new Map<string, number>();
|
||||||
|
|
||||||
|
outcomeActions.forEach((o) => {
|
||||||
|
if (!o.task) return; // skip empties; already handled above
|
||||||
|
const key = `${o.verdict}|${o.task}`;
|
||||||
|
keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Any key with count > 1 is invalid
|
||||||
|
outcomeActions.forEach((o, index) => {
|
||||||
|
if (!o.task) return;
|
||||||
|
const key = `${o.verdict}|${o.task}`;
|
||||||
|
if ((keyCounts.get(key) ?? 0) > 1) {
|
||||||
|
const base = `${guid}.outcomeActions[${index}]`;
|
||||||
|
errors[`${base}.verdict`] = "Duplicate verdict/task combination";
|
||||||
|
errors[`${base}.task`] = "Duplicate verdict/task combination";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function defaultsAssignment(
|
||||||
|
task: TaskDefinition,
|
||||||
|
tasks: TaskDefinition[],
|
||||||
|
ctx: defaultsContext,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export const outcomeOfApprovalVerdictRegistryEntry: capabilityEditorRegistryEntry =
|
||||||
|
{
|
||||||
|
Editor: outcomeEditor,
|
||||||
|
DefaultsAssignment: defaultsAssignment,
|
||||||
|
ValidationRunner: runValidation,
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user