Approval steps can now be added to approval tasks
This commit is contained in:
parent
f1da101e4e
commit
dca30cfca0
@ -4,6 +4,7 @@
|
||||
"Active": "Active",
|
||||
"ActivityNameTemplate": "Activity Name Template",
|
||||
"Add": "Add",
|
||||
"AddWithValue": "Add {{value}}",
|
||||
"AddDomain": "Add Domain",
|
||||
"AddTask": "Add Task",
|
||||
"AddUser": "Add User",
|
||||
@ -210,4 +211,4 @@
|
||||
"e-print": "e-print",
|
||||
"e-suite": "e-suite",
|
||||
"e-suiteLogo": "e-suite logo"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"AdhocApprovalTask": "Adhoc Approval",
|
||||
"ApprovalTask": "Approval",
|
||||
"ApprovalStep": "Approval Step",
|
||||
"AssetUploadTask": "Asset Upload",
|
||||
"BasicTask": "Basic",
|
||||
"ContentCollationTask": "Content Collation",
|
||||
@ -12,4 +13,4 @@
|
||||
"StageTask": "Stage",
|
||||
"VisualBriefReviewTask": "Visual Brief Review",
|
||||
"VisualBriefUploadTask": "Visual Brief Upload"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import React from "react";
|
||||
import { TaskMetadata } from "../services/WorkflowTemplateService";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../../../i18n/i18n";
|
||||
import Button, { ButtonType } from "../../../../components/common/Button";
|
||||
|
||||
interface AddTaskButtonProps {
|
||||
tasksMetadata: TaskMetadata[];
|
||||
@ -22,11 +23,27 @@ const AddTaskButton: React.FC<AddTaskButtonProps> = ({
|
||||
setOpen(next);
|
||||
};
|
||||
|
||||
if (tasksMetadata.length === 1) {
|
||||
return (
|
||||
<Button
|
||||
buttonType={ButtonType.secondary}
|
||||
onClick={() => {
|
||||
onAdd(tasksMetadata[0]);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("AddWithValue", {
|
||||
value: tTaskType(tTaskType(tasksMetadata[0].displayName)),
|
||||
})}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="add-task-button">
|
||||
<button className="btn btn-secondary" type="button" onClick={toggle}>
|
||||
<Button buttonType={ButtonType.secondary} onClick={toggle}>
|
||||
{t("AddTask")}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{open && (
|
||||
<div className="dropdown-menu show">
|
||||
|
||||
@ -0,0 +1,248 @@
|
||||
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";
|
||||
import VerdictPicker from "../../../../../components/pickers/VerdictPicker";
|
||||
import { renderInput } from "../../../../../components/common/formHelpers";
|
||||
import { InputType } from "../../../../../components/common/Input";
|
||||
import { error } from "console";
|
||||
|
||||
// ---------------------------
|
||||
// Domain Interfaces
|
||||
// ---------------------------
|
||||
|
||||
export interface IApprovalTaskAssignee {
|
||||
role?: GeneralIdRef | null;
|
||||
contact?: 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,
|
||||
contact: 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 contact)
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Role</th>
|
||||
<th>Contact</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}].contact`] ||
|
||||
!!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="contact"
|
||||
label="Contact"
|
||||
value={assignee.contact}
|
||||
domain={domain}
|
||||
error={fieldErrors?.[`${guid}.assignees[${index}].contact`]}
|
||||
onChange={(name: string, val: GeneralIdRef | null) =>
|
||||
updateAssignee(index, { ...assignee, contact: 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 noContactSelected = !a.contact || a.contact?.id === BigInt(0);
|
||||
const noRoleSelected = !a.role || a.role?.id === BigInt(0);
|
||||
|
||||
if (!noContactSelected && !noRoleSelected) {
|
||||
errors[`${guid}.assignees[${i}].contact`] =
|
||||
"Cannot select both a contact and a role.";
|
||||
errors[`${guid}.assignees[${i}].role`] =
|
||||
"Cannot select both a contact and a role.";
|
||||
} else {
|
||||
if (!(!noContactSelected || !noRoleSelected)) {
|
||||
if (noContactSelected) {
|
||||
errors[`${guid}.assignees[${i}].contact`] =
|
||||
"A contact 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 =
|
||||
{
|
||||
Editor: AssigneesOfIApprovalTaskAssigneeEditor,
|
||||
DefaultsAssignment: defaultsAssignment,
|
||||
ValidationRunner: runValidation,
|
||||
};
|
||||
@ -199,7 +199,7 @@ export function defaultsAssignment(
|
||||
ctx: defaultsContext,
|
||||
) {
|
||||
task.config.outcomeActions = [] as IOutcomeAction[];
|
||||
task.config.overrideDefaultTaskProgression = true;
|
||||
task.config.overrideDefaultTaskProgression = false;
|
||||
}
|
||||
|
||||
export const outcomeOfApprovalVerdictRegistryEntry: capabilityEditorRegistryEntry =
|
||||
|
||||
@ -0,0 +1,124 @@
|
||||
import React, { useEffect } from "react";
|
||||
import templateVersionsService, {
|
||||
TaskDefinition,
|
||||
TaskMetadata,
|
||||
} from "../../services/WorkflowTemplateService";
|
||||
import AddTaskButton from "../AddTaskButton";
|
||||
import {
|
||||
CapabilityEditorProps,
|
||||
capabilityEditorRegistryEntry,
|
||||
defaultsContext,
|
||||
useCapabilityDefaults,
|
||||
validateTask,
|
||||
} from "../useCapabilityDefaults";
|
||||
|
||||
function createStageEditor(taskType: string): React.FC<CapabilityEditorProps> {
|
||||
return (props) => {
|
||||
const {
|
||||
task,
|
||||
onChange,
|
||||
onValidate,
|
||||
onValidateTask,
|
||||
onTaskAdded,
|
||||
fieldErrors,
|
||||
} = props;
|
||||
|
||||
const [tasksMetadata, setTasksMetadata] = React.useState<TaskMetadata[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTaskMetadata = async () => {
|
||||
const meta = await templateVersionsService.getTaskMetadata(taskType);
|
||||
setTasksMetadata(meta);
|
||||
};
|
||||
|
||||
fetchTaskMetadata();
|
||||
}, []);
|
||||
|
||||
const runDefaults = useCapabilityDefaults(tasksMetadata);
|
||||
|
||||
const handleAddTask = async (selectedType: TaskMetadata) => {
|
||||
const newTask: TaskDefinition = {
|
||||
type: selectedType.taskType,
|
||||
|
||||
config: {
|
||||
guid: crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
var childTasks = task.config.tasks as TaskDefinition[]; //Type assertion to satisfy the compiler, we know this will be the correct type.
|
||||
|
||||
//Assign the default values for the task here.
|
||||
selectedType.capabilities.forEach((capability) => {
|
||||
runDefaults(capability, newTask, childTasks);
|
||||
});
|
||||
|
||||
const updatedTasks = [...childTasks, newTask];
|
||||
const errors = await validateTask(newTask, updatedTasks, tasksMetadata);
|
||||
|
||||
const isValid = Object.keys(errors).length === 0;
|
||||
|
||||
// Mark the child task as valid/invalid
|
||||
onValidateTask?.(newTask.config.guid as string, isValid);
|
||||
|
||||
task.config.tasks = updatedTasks;
|
||||
|
||||
onChange(task);
|
||||
onTaskAdded?.(newTask.config.guid as string);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddTaskButton tasksMetadata={tasksMetadata} onAdd={handleAddTask} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function createValidationRunner(
|
||||
taskType: string,
|
||||
): (
|
||||
task: TaskDefinition,
|
||||
tasks: TaskDefinition[],
|
||||
) => Promise<Record<string, string>> {
|
||||
return async (task: TaskDefinition, tasks: TaskDefinition[]) => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
const meta = await templateVersionsService.getTaskMetadata(taskType);
|
||||
|
||||
if (task.config.tasks) {
|
||||
for (const childTask of task.config.tasks as TaskDefinition[]) {
|
||||
const childErrors = await validateTask(
|
||||
childTask,
|
||||
task.config.tasks as TaskDefinition[],
|
||||
meta,
|
||||
);
|
||||
|
||||
if (childErrors && Object.keys(childErrors).length > 0) {
|
||||
errors[`${task.config.guid}.tasks`] =
|
||||
`One or more child tasks are invalid.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
}
|
||||
|
||||
function defaultsAssignment(
|
||||
task: TaskDefinition,
|
||||
tasks: TaskDefinition[],
|
||||
ctx: defaultsContext,
|
||||
) {
|
||||
task.config.tasks = [] as TaskDefinition[];
|
||||
}
|
||||
|
||||
export function createStageEditorRegistryEntry(
|
||||
taskType: string,
|
||||
): capabilityEditorRegistryEntry {
|
||||
return {
|
||||
Editor: createStageEditor(taskType),
|
||||
DefaultsAssignment: defaultsAssignment,
|
||||
ValidationRunner: createValidationRunner(taskType),
|
||||
};
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import templateVersionsService, {
|
||||
TaskDefinition,
|
||||
TaskMetadata,
|
||||
} from "../../services/WorkflowTemplateService";
|
||||
import AddTaskButton from "../AddTaskButton";
|
||||
import {
|
||||
CapabilityEditorProps,
|
||||
capabilityEditorRegistryEntry,
|
||||
defaultsContext,
|
||||
TaskValidationResult,
|
||||
useCapabilityDefaults,
|
||||
validateTask,
|
||||
} from "../useCapabilityDefaults";
|
||||
|
||||
export const StageOfGeneralTaskEditor: React.FC<CapabilityEditorProps> = (
|
||||
props,
|
||||
) => {
|
||||
const { task, onChange, onValidate, onValidateTask, fieldErrors } = props;
|
||||
|
||||
const [tasksMetadata, setTasksMetadata] = React.useState<TaskMetadata[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTaskMetadata = async () => {
|
||||
const meta = await templateVersionsService.getTaskMetadata("GeneralTask");
|
||||
setTasksMetadata(meta);
|
||||
};
|
||||
|
||||
fetchTaskMetadata();
|
||||
}, []);
|
||||
|
||||
const runDefaults = useCapabilityDefaults(tasksMetadata);
|
||||
|
||||
const handleAddTask = async (selectedType: TaskMetadata) => {
|
||||
const newTask: TaskDefinition = {
|
||||
type: selectedType.taskType,
|
||||
|
||||
config: {
|
||||
guid: crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
var childTasks = task.config.tasks as TaskDefinition[]; //Type assertion to satisfy the compiler, we know this will be the correct type.
|
||||
|
||||
//Assign the default values for the task here.
|
||||
selectedType.capabilities.forEach((capability) => {
|
||||
runDefaults(capability, newTask, childTasks);
|
||||
});
|
||||
|
||||
const updatedTasks = [...childTasks, newTask];
|
||||
const errors = await validateTask(newTask, updatedTasks, tasksMetadata);
|
||||
|
||||
const isValid = Object.keys(errors).length === 0;
|
||||
|
||||
// Mark the child task as valid/invalid
|
||||
onValidateTask?.(newTask.config.guid as string, isValid);
|
||||
|
||||
task.config.tasks = updatedTasks;
|
||||
|
||||
onChange(task);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddTaskButton tasksMetadata={tasksMetadata} onAdd={handleAddTask} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const runValidation = async (
|
||||
task: TaskDefinition,
|
||||
tasks: TaskDefinition[],
|
||||
): Promise<Record<string, string>> => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
const meta = await templateVersionsService.getTaskMetadata("GeneralTask");
|
||||
|
||||
if (task.config.tasks) {
|
||||
for (const childTask of task.config.tasks as TaskDefinition[]) {
|
||||
const childErrors = await validateTask(
|
||||
childTask,
|
||||
task.config.tasks as TaskDefinition[],
|
||||
meta,
|
||||
);
|
||||
|
||||
if (childErrors && Object.keys(childErrors).length > 0) {
|
||||
errors[`${task.config.guid}.tasks`] =
|
||||
`One or more child tasks are invalid.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
export function defaultsAssignment(
|
||||
task: TaskDefinition,
|
||||
tasks: TaskDefinition[],
|
||||
ctx: defaultsContext,
|
||||
) {
|
||||
task.config.tasks = [] as TaskDefinition[];
|
||||
}
|
||||
|
||||
export const stageOfGeneralTaskEditorRegistryEntry: capabilityEditorRegistryEntry =
|
||||
{
|
||||
Editor: StageOfGeneralTaskEditor,
|
||||
DefaultsAssignment: defaultsAssignment,
|
||||
ValidationRunner: runValidation,
|
||||
};
|
||||
@ -12,24 +12,28 @@ interface TaskEditorProps {
|
||||
task: TaskDefinition;
|
||||
tasks: TaskDefinition[];
|
||||
siblingTasks: TaskDefinition[];
|
||||
parentTask: TaskDefinition | null;
|
||||
onChange: (updatedTask: TaskDefinition) => void;
|
||||
onValidate: (taskId: string, isValid: boolean) => void;
|
||||
onDelete: (taskId: string) => void;
|
||||
onTaskAdded?: (taskGuid: string) => void;
|
||||
}
|
||||
|
||||
const TaskEditorComponent: React.FC<TaskEditorProps> = ({
|
||||
task,
|
||||
tasks,
|
||||
siblingTasks,
|
||||
parentTask,
|
||||
onChange,
|
||||
onValidate,
|
||||
onDelete,
|
||||
onTaskAdded,
|
||||
}) => {
|
||||
const [tasksMetadata, setTasksMetadata] = React.useState<TaskMetadata[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTaskMetadata = async () => {
|
||||
const meta = await templateVersionsService.getTaskMetadata("GeneralTask");
|
||||
const meta = await templateVersionsService.getTaskMetadata("");
|
||||
setTasksMetadata(meta);
|
||||
};
|
||||
|
||||
@ -52,12 +56,23 @@ const TaskEditorComponent: React.FC<TaskEditorProps> = ({
|
||||
tasksMetadataList,
|
||||
);
|
||||
setFieldErrors(errors);
|
||||
onValidate(
|
||||
taskToValidate.config.guid as string,
|
||||
Object.keys(errors).length === 0,
|
||||
);
|
||||
const isValid = Object.keys(errors).length === 0;
|
||||
onValidate(taskToValidate.config.guid as string, isValid);
|
||||
|
||||
// If this task has a parent, re-validate the parent
|
||||
if (parentTask) {
|
||||
const parentErrors = await validateTask(
|
||||
parentTask,
|
||||
tasks,
|
||||
tasksMetadataList,
|
||||
);
|
||||
onValidate(
|
||||
parentTask.config.guid as string,
|
||||
Object.keys(parentErrors).length === 0,
|
||||
);
|
||||
}
|
||||
},
|
||||
[onValidate],
|
||||
[onValidate, parentTask, tasks],
|
||||
);
|
||||
|
||||
const tasksRef = React.useRef(tasks);
|
||||
@ -108,14 +123,16 @@ const TaskEditorComponent: React.FC<TaskEditorProps> = ({
|
||||
onChange={handleTaskChange}
|
||||
fieldErrors={fieldErrors}
|
||||
onValidateTask={onValidate}
|
||||
onTaskAdded={onTaskAdded}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ConfirmButton
|
||||
buttonType={ButtonType.primary}
|
||||
keyValue={task.config.guid as string}
|
||||
onClick={() => onDelete(task.config.guid as string)}
|
||||
>
|
||||
Delete {task.config.name || "Task"}
|
||||
Delete {(task.config.name as string) || "Task"}
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
);
|
||||
@ -126,17 +143,18 @@ export const TaskEditor = React.memo(
|
||||
(prevProps, nextProps) => {
|
||||
const taskIdChanged =
|
||||
prevProps.task.config.guid !== nextProps.task.config.guid;
|
||||
const metaChanged = prevProps.tasksMetadata !== nextProps.tasksMetadata;
|
||||
const onChangeChanged = prevProps.onChange !== nextProps.onChange;
|
||||
const onValidateChanged = prevProps.onValidate !== nextProps.onValidate;
|
||||
const parentChanged = prevProps.parentTask !== nextProps.parentTask;
|
||||
|
||||
// Skip tasks comparison - we use a ref to access it
|
||||
return !(
|
||||
taskIdChanged ||
|
||||
metaChanged ||
|
||||
onChangeChanged ||
|
||||
onValidateChanged ||
|
||||
prevProps.onDelete !== nextProps.onDelete
|
||||
parentChanged ||
|
||||
prevProps.onDelete !== nextProps.onDelete ||
|
||||
prevProps.onTaskAdded !== nextProps.onTaskAdded
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -26,8 +26,25 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
}) => {
|
||||
const tasks = data.tasks;
|
||||
const [selectedTask, setSelectedTask] = useState<TaskDefinition | null>(null);
|
||||
const [pendingSelectGuid, setPendingSelectGuid] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// If we have a pending task GUID to select, find it and select it
|
||||
if (pendingSelectGuid) {
|
||||
const { task: foundTask } = findTaskAndSiblings(
|
||||
pendingSelectGuid,
|
||||
tasks,
|
||||
null,
|
||||
);
|
||||
if (foundTask) {
|
||||
setSelectedTask(foundTask);
|
||||
setPendingSelectGuid(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't override user selection
|
||||
if (selectedTask) return;
|
||||
|
||||
@ -48,7 +65,7 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
|
||||
// Otherwise select first task
|
||||
setSelectedTask(tasks[0]);
|
||||
}, [tasks, taskValidation, selectedTask]);
|
||||
}, [tasks, taskValidation, selectedTask, pendingSelectGuid]);
|
||||
|
||||
const handleTasksChange = React.useCallback(
|
||||
(newTasks: TaskDefinition[]) => {
|
||||
@ -61,22 +78,27 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
const findTaskAndSiblings = (
|
||||
targetGuid: string,
|
||||
source: TaskDefinition[],
|
||||
): { task: TaskDefinition | null; siblings: TaskDefinition[] } => {
|
||||
parent: TaskDefinition | null = null,
|
||||
): {
|
||||
task: TaskDefinition | null;
|
||||
siblings: TaskDefinition[];
|
||||
parent: TaskDefinition | null;
|
||||
} => {
|
||||
for (const task of source) {
|
||||
if (task.config.guid === targetGuid) {
|
||||
return { task, siblings: source };
|
||||
return { task, siblings: source, parent };
|
||||
}
|
||||
|
||||
const childTasks = (task.config.tasks as TaskDefinition[]) ?? [];
|
||||
if (childTasks.length === 0) continue;
|
||||
|
||||
const result = findTaskAndSiblings(targetGuid, childTasks);
|
||||
const result = findTaskAndSiblings(targetGuid, childTasks, task);
|
||||
if (result.task) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { task: null, siblings: [] };
|
||||
return { task: null, siblings: [], parent: null };
|
||||
};
|
||||
|
||||
const handleTaskEditorChange = React.useCallback(
|
||||
@ -84,6 +106,7 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
const { siblings } = findTaskAndSiblings(
|
||||
updatedTask.config.guid as string,
|
||||
tasks,
|
||||
null,
|
||||
);
|
||||
|
||||
if (siblings.length === 0) {
|
||||
@ -242,12 +265,25 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
task={selectedTask}
|
||||
tasks={tasks}
|
||||
siblingTasks={
|
||||
findTaskAndSiblings(selectedTask.config.guid as string, tasks)
|
||||
.siblings
|
||||
findTaskAndSiblings(
|
||||
selectedTask.config.guid as string,
|
||||
tasks,
|
||||
null,
|
||||
).siblings
|
||||
}
|
||||
parentTask={
|
||||
findTaskAndSiblings(
|
||||
selectedTask.config.guid as string,
|
||||
tasks,
|
||||
null,
|
||||
).parent
|
||||
}
|
||||
onChange={handleTaskEditorChange}
|
||||
onValidate={onValidate}
|
||||
onDelete={handleTaskDelete}
|
||||
onTaskAdded={(taskGuid: string) => {
|
||||
setPendingSelectGuid(taskGuid);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -5,7 +5,8 @@ import { capabilityEditorRegistryEntry } from "./useCapabilityDefaults";
|
||||
import { outcomeOfApprovalVerdictRegistryEntry } from "./CapabilityEditors/OutcomeOfApprovalVerdictRegistryEntry";
|
||||
import { budgetEditorRegistryEntry } from "./CapabilityEditors/BudgetEditorRegistryEntry";
|
||||
import { bypassableEditorRegistryEntry } from "./CapabilityEditors/BypassableEditor";
|
||||
import { stageOfGeneralTaskEditorRegistryEntry } from "./CapabilityEditors/StageOfGeneralTaskEditor";
|
||||
import { createStageEditorRegistryEntry } from "./CapabilityEditors/StageEditor";
|
||||
import { assigneesOfIApprovalTaskAssigneeRegistryEntry } from "./CapabilityEditors/AssigneesOfIApprovalTaskAssigneeEditor";
|
||||
|
||||
export const capabilityEditorRegistry: Record<
|
||||
string,
|
||||
@ -15,9 +16,12 @@ export const capabilityEditorRegistry: Record<
|
||||
ITags: tagsEditorRegistryEntry,
|
||||
IBudget: budgetEditorRegistryEntry,
|
||||
"IAssignees<ITaskAssignee>": assigneesOfITaskAssigneeRegistryEntry,
|
||||
"IAssignees<IApprovalTaskAssignee>":
|
||||
assigneesOfIApprovalTaskAssigneeRegistryEntry,
|
||||
"IOutcome<ApprovalVerdict>": outcomeOfApprovalVerdictRegistryEntry,
|
||||
// IFormTemplate: null, //ToDo implement this
|
||||
IBypassable: bypassableEditorRegistryEntry,
|
||||
"IStage<GeneralTaskAttribute>": stageOfGeneralTaskEditorRegistryEntry,
|
||||
// "IStage<ApprovalTaskAttribute>": null, //ToDo implement this
|
||||
"IStage<GeneralTaskAttribute>": createStageEditorRegistryEntry("GeneralTask"),
|
||||
"IStage<ApprovalTaskAttribute>":
|
||||
createStageEditorRegistryEntry("ApprovalTask"),
|
||||
};
|
||||
|
||||
@ -18,6 +18,7 @@ export interface CapabilityEditorProps {
|
||||
onChange: (updated: TaskDefinition) => void;
|
||||
onValidate: (result: TaskValidationResult) => void;
|
||||
onValidateTask?: (taskId: string, isValid: boolean) => void;
|
||||
onTaskAdded?: (taskGuid: string) => void;
|
||||
fieldErrors: Record<string, string>;
|
||||
}
|
||||
|
||||
@ -56,7 +57,11 @@ export async function validateTask(
|
||||
continue;
|
||||
}
|
||||
|
||||
const validationErrors = await entry.ValidationRunner(task, tasks);
|
||||
const validationErrors = await entry.ValidationRunner(
|
||||
task,
|
||||
tasks,
|
||||
tasksMetadata,
|
||||
);
|
||||
Object.assign(errors, validationErrors);
|
||||
}
|
||||
return errors;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user