Started simplifying the Assignee capability editor, want to make it generic and data driven
This commit is contained in:
parent
5bcdf9a5ac
commit
4c8ff7378b
@ -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,
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user