Started working on the assignments editor

This commit is contained in:
Colin Dawson 2026-02-15 22:15:11 +00:00
parent 0130a578ea
commit 3cc7a42f57
4 changed files with 224 additions and 1 deletions

View File

@ -11,6 +11,7 @@ interface UserPickerProps {
value: any;
domain?: GeneralIdRef;
onChange?: (name: string, value: GeneralIdRef) => void;
includeLabel?: boolean;
}
export default function UserPicker({
@ -20,6 +21,7 @@ export default function UserPicker({
value,
domain,
onChange,
includeLabel = true,
}: UserPickerProps) {
const [options, setOptions] = useState<Option[] | undefined>(undefined);
@ -59,6 +61,7 @@ export default function UserPicker({
options={options}
includeBlankFirstEntry={true}
onChange={handleChange}
includeLabel={includeLabel}
/>
);
}

View File

@ -104,7 +104,11 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
// -----------------------------
const doSubmit = async (buttonName: string) => {
try {
//const { name } = form.state.data;
const { data } = form.state;
var json = JSON.stringify(data);
console.log("Submitting workflow template:", json);
if (editMode) {
//await templateVersionsService.putTemplateVersion({ name });

View File

@ -0,0 +1,214 @@
import React from "react";
import UserPicker from "../../../../../components/pickers/UserPicker";
import { GeneralIdRef } from "../../../../../utils/GeneralIdRef";
import { TaskDefinition } from "../../services/WorkflowTemplateService";
import {
CapabilityEditorProps,
capabilityEditorRegistryEntry,
defaultsContext,
} from "../useCapabilityDefaults";
import Button, { ButtonType } from "../../../../../components/common/Button";
// TODO: Replace with your real RolePicker and RaciPicker
const RolePicker = (props: any) => (
<div style={{ marginBottom: 8 }}>
<input
type="text"
name={props.name}
value={props.value?.id ?? ""}
onChange={(e) =>
props.onChange(props.name, { id: BigInt(e.target.value) })
}
/>
</div>
);
const RaciPicker = (props: any) => (
<div style={{ marginBottom: 8 }}>
<select
name={props.name}
value={props.value}
onChange={(e) => props.onChange(props.name, e.target.value)}
>
<option value="Responsible">Responsible</option>
<option value="Accountable">Accountable</option>
<option value="Consulted">Consulted</option>
<option value="Informed">Informed</option>
</select>
</div>
);
// ---------------------------
// Domain Interfaces
// ---------------------------
export interface ITaskAssignee {
role?: GeneralIdRef | null;
contact?: GeneralIdRef | null;
raci: string; // Raci enum as string
}
export interface IAssigneesCapability {
assignees: ITaskAssignee[];
}
// ---------------------------
// Editor Component
// ---------------------------
export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
props,
) => {
const { task, onChange, fieldErrors } = props;
const assignees = (task.config.assignees ?? []) as ITaskAssignee[];
function updateAssignee(index: number, updated: ITaskAssignee) {
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,
contact: null,
raci: "Responsible",
});
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);
}
return (
<div>
Select assigness (you can select either a role or a contact)
<table>
<thead>
<tr>
<th>Role</th>
<th>Contact</th>
<th>RACI</th>
<th>
<Button onClick={addAssignee} buttonType={ButtonType.secondary}>
Add Assignee
</Button>
</th>
</tr>
</thead>
<tbody>
{assignees.map((assignee, index) => (
<tr key={index}>
<td>
<RolePicker
name="role"
label="Role"
value={assignee.role}
error={fieldErrors?.[`assignees[${index}].role`]}
onChange={(name: string, val: GeneralIdRef | null) =>
updateAssignee(index, { ...assignee, role: val })
}
/>
</td>
<td>
<UserPicker
includeLabel={false}
name="contact"
label="Contact"
value={assignee.contact}
error={fieldErrors?.[`assignees[${index}].contact`]}
onChange={(name: string, val: GeneralIdRef | null) =>
updateAssignee(index, { ...assignee, contact: val })
}
/>
</td>
<td>
<RaciPicker
name="raci"
label="RACI"
value={assignee.raci}
error={fieldErrors?.[`assignees[${index}].raci`]}
onChange={(name: string, val: string) =>
updateAssignee(index, { ...assignee, raci: val })
}
/>
</td>
<td>
<Button
onClick={() => removeAssignee(index)}
buttonType={ButtonType.secondary}
>
Remove
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
// ---------------------------
// Validation
// ---------------------------
const runValidation = (
task: TaskDefinition,
tasks: TaskDefinition[],
): Record<string, string> => {
const errors: Record<string, string> = {};
const assignees = task.config.assignees as ITaskAssignee[] | undefined;
if (!assignees || assignees.length === 0) {
errors["assignees"] = "At least one assignee is required.";
return errors;
}
assignees.forEach((a, i) => {
if (!a.role && !a.contact) {
//errors[`assignees[${i}]`] = "Either role or contact must be selected.";
errors[`assignees`] = "Either role or contact must be selected.";
}
if (!a.raci) {
//errors[`assignees[${i}].raci`] = "RACI is required.";
errors[`assignees`] = "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 AssigneesOfITaskAssigneeRegistryEntry: capabilityEditorRegistryEntry =
{
Editor: AssigneesOfITaskAssigneeEditor,
DefaultsAssignment: defaultsAssignment,
ValidationRunner: runValidation,
};

View File

@ -1,4 +1,5 @@
import { tagsEditorRegistryEntry } from "./CapabilityEditors/TagsEditor";
import { AssigneesOfITaskAssigneeRegistryEntry } from "./CapabilityEditors/AssigneesOfITaskAssigneeEditor";
import { taskCoreEditorRegistryEntry } from "./CapabilityEditors/TaskCoreEditor";
import { capabilityEditorRegistryEntry } from "./useCapabilityDefaults";
@ -8,4 +9,5 @@ export const capabilityEditorRegistry: Record<
> = {
ITask: taskCoreEditorRegistryEntry,
ITags: tagsEditorRegistryEntry,
"IAssignees<ITaskAssignee>": AssigneesOfITaskAssigneeRegistryEntry,
};