Started simplifying the Assignee capability editor, want to make it generic and data driven

This commit is contained in:
Colin Dawson 2026-03-16 14:24:27 +00:00
parent 5bcdf9a5ac
commit 4c8ff7378b
3 changed files with 1 additions and 247 deletions

View File

@ -1,244 +0,0 @@
import React from "react";
import UserPicker from "../../../../../components/pickers/UserPicker";
import {
GeneralIdRef,
MakeGeneralIdRef,
} from "../../../../../utils/GeneralIdRef";
import { TaskDefinition } from "../../services/WorkflowTemplateService";
import {
CapabilityEditorProps,
capabilityEditorRegistryEntry,
defaultsContext,
} from "../useCapabilityDefaults";
import Button, { ButtonType } from "../../../../../components/common/Button";
import ValidationErrorIcon from "../../../../../components/validationErrorIcon";
import ErrorBlock from "../../../../../components/common/ErrorBlock";
import RolePicker from "../../../../../components/pickers/RolePicker";
import { getCurrentUser } from "../../../../frame/services/authenticationService";
import RaciPicker from "../../../../../components/pickers/RaciPicker";
// ---------------------------
// Domain Interfaces
// ---------------------------
export interface IApprovalTaskAssignee {
role?: GeneralIdRef | null;
user?: GeneralIdRef | null;
raci: string; // Raci enum as string
allowNoVerdict: boolean;
bypassable: boolean;
}
export interface IApprovalTaskAssigneesCapability {
assignees: IApprovalTaskAssignee[];
}
// ---------------------------
// Editor Component
// ---------------------------
export const AssigneesOfIApprovalTaskAssigneeEditor: React.FC<
CapabilityEditorProps
> = (props) => {
const { task, onChange, fieldErrors } = props;
const assignees = (task.config.assignees ?? []) as IApprovalTaskAssignee[];
function updateAssignee(index: number, updated: IApprovalTaskAssignee) {
const clone = structuredClone(task);
const list = clone.config.assignees ?? [];
list[index] = updated;
clone.config.assignees = list;
onChange(clone);
}
function addAssignee() {
const clone = structuredClone(task);
const list = clone.config.assignees ?? [];
list.push({
role: null,
user: null,
raci: "Responsible",
allowNoVerdict: false,
bypassable: false,
});
clone.config.assignees = list;
onChange(clone);
}
function removeAssignee(index: number) {
const clone = structuredClone(task);
const list = clone.config.assignees ?? [];
clone.config.assignees = list.filter((_, i) => i !== index);
onChange(clone);
}
const guid = task.config.guid as string;
const domain = MakeGeneralIdRef(getCurrentUser()?.domainid);
return (
<div>
Select assigness (you can select either a role or a user)
<table>
<thead>
<tr>
<th></th>
<th>Role</th>
<th>User</th>
<th>RACI</th>
<th>Allow No Verdict</th>
<th>Bypassable</th>
<th>
<Button onClick={addAssignee} buttonType={ButtonType.secondary}>
Add Assignee
</Button>
</th>
</tr>
</thead>
<tbody>
{assignees.map((assignee, index) => (
<tr key={index} className="align-top">
<td className="form-group">
<ValidationErrorIcon
visible={
!!fieldErrors?.[`${guid}.assignees[${index}].role`] ||
!!fieldErrors?.[`${guid}.assignees[${index}].user`] ||
!!fieldErrors?.[`${guid}.assignees[${index}].raci`]
}
/>
</td>
<td>
<RolePicker
includeLabel={false}
name="role"
label="Role"
value={assignee.role}
domain={domain}
error={fieldErrors?.[`${guid}.assignees[${index}].role`]}
onChange={(name: string, val: GeneralIdRef | null) =>
updateAssignee(index, { ...assignee, role: val })
}
/>
</td>
<td>
<UserPicker
includeLabel={false}
name="user"
label="User"
value={assignee.user}
domain={domain}
error={fieldErrors?.[`${guid}.assignees[${index}].user`]}
onChange={(name: string, val: GeneralIdRef | null) =>
updateAssignee(index, { ...assignee, user: val })
}
/>
</td>
<td>
<RaciPicker
includeLabel={false}
name="raci"
label="RACI"
value={assignee.raci}
error={fieldErrors?.[`${guid}.assignees[${index}].raci`]}
onChange={(name: string, val: string) =>
updateAssignee(index, { ...assignee, raci: val })
}
/>
</td>
<td>allowNoVerdict goes here.</td>
<td>Bypass goes here.</td>
<td className="form-group">
<Button
onClick={() => removeAssignee(index)}
buttonType={ButtonType.secondary}
>
Remove
</Button>
</td>
</tr>
))}
</tbody>
</table>
<ErrorBlock
error={Object.values(fieldErrors ?? {})
.filter((_, i) =>
Object.keys(fieldErrors ?? {}).some(
(key) => key === `${guid}.assignees`,
),
)
.join("; ")}
/>
</div>
);
};
// ---------------------------
// Validation
// ---------------------------
const runValidation = (
task: TaskDefinition,
tasks: TaskDefinition[],
): Promise<Record<string, string>> => {
const errors: Record<string, string> = {};
const guid = task.config.guid as string;
const assignees = task.config.assignees as ITaskAssignee[] | undefined;
if (!assignees || assignees.length === 0) {
errors[`${guid}.assignees`] = "At least one assignee is required.";
return Promise.resolve(errors);
}
assignees.forEach((a, i) => {
const noUserSelected = !a.user || a.user?.id === BigInt(0);
const noRoleSelected = !a.role || a.role?.id === BigInt(0);
if (!noUserSelected && !noRoleSelected) {
errors[`${guid}.assignees[${i}].user`] =
"Cannot select both a user and a role.";
errors[`${guid}.assignees[${i}].role`] =
"Cannot select both a user and a role.";
} else {
if (!(!noUserSelected || !noRoleSelected)) {
if (noUserSelected) {
errors[`${guid}.assignees[${i}].user`] = "A user must be selected.";
}
if (noRoleSelected) {
errors[`${guid}.assignees[${i}].role`] = "A role must be selected.";
}
}
}
if (!a.raci) {
errors[`${guid}.assignees[${i}].raci`] = "RACI is required.";
}
});
return errors;
};
// ---------------------------
// Defaults Assignment
// ---------------------------
export function defaultsAssignment(
task: TaskDefinition,
tasks: TaskDefinition[],
ctx: defaultsContext,
) {
task.config.assignees = [{ raci: "Responsible" } as ITaskAssignee];
//task.config.assignees = [];
}
// ---------------------------
// Registry Entry
// ---------------------------
export const assigneesOfIApprovalTaskAssigneeRegistryEntry: capabilityEditorRegistryEntry =
{
match: (cap) => cap === "IAssignees<ApprovalTaskAssignee>",
Editor: AssigneesOfIApprovalTaskAssigneeEditor,
DefaultsAssignment: defaultsAssignment,
ValidationRunner: runValidation,
};

View File

@ -229,7 +229,7 @@ export function defaultsAssignment(
export const assigneesOfITaskAssigneeRegistryEntry: capabilityEditorRegistryEntry =
{
match: (cap) => cap === "IAssignees<TaskAssignee>",
match: (cap) => cap.startsWith("IAssignees<"),
Editor: AssigneesOfITaskAssigneeEditor,
DefaultsAssignment: defaultsAssignment,
ValidationRunner: runValidation,

View File

@ -6,7 +6,6 @@ import { outcomeOfApprovalVerdictRegistryEntry } from "./CapabilityEditors/Outco
import { budgetEditorRegistryEntry } from "./CapabilityEditors/BudgetEditorRegistryEntry";
import { bypassableEditorRegistryEntry } from "./CapabilityEditors/BypassableEditor";
import { createStageEditorRegistryEntry } from "./CapabilityEditors/StageEditor";
import { assigneesOfIApprovalTaskAssigneeRegistryEntry } from "./CapabilityEditors/AssigneesOfIApprovalTaskAssigneeEditor";
type CapabilityEditorRegistry = Array<capabilityEditorRegistryEntry>;
@ -15,7 +14,6 @@ export const capabilityEditorRegistry: CapabilityEditorRegistry = [
tagsEditorRegistryEntry,
budgetEditorRegistryEntry,
assigneesOfITaskAssigneeRegistryEntry,
assigneesOfIApprovalTaskAssigneeRegistryEntry,
outcomeOfApprovalVerdictRegistryEntry,
// IFormTemplate: null, //ToDo implement this
bypassableEditorRegistryEntry,