Refactored the code so that the capability editors have even less duplicated code

This commit is contained in:
Colin Dawson 2026-02-14 23:39:03 +00:00
parent e7a084c301
commit 86ebdc8b72
5 changed files with 98 additions and 85 deletions

View File

@ -1,25 +1,15 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback } from "react";
import { InputType } from "../../../../../components/common/Input";
import {
TaskDefinition,
TaskMetadata,
} from "../../services/WorkflowTemplateService";
import { TaskDefinition } from "../../services/WorkflowTemplateService";
import { renderTaskField } from "../taskEditorHelpers";
import { TaskValidationResult } from "../TasksEditor";
import { CapabilityEditorProps } from "../TasksEditor";
import { Namespaces } from "../../../../../i18n/i18n";
import { useTranslation } from "react-i18next";
import { getAllDescendants } from "../workflowGraphUtils";
import { useValidation } from "../useValidation";
import { useCapabilityDefaults } from "../useCapabilityDefaults";
interface TaskCoreEditorProps {
task: TaskDefinition;
allTasks: TaskDefinition[];
allowedTasks: TaskMetadata[];
onChange: (updated: TaskDefinition) => void;
onValidate: (result: TaskValidationResult) => void;
shouldAssignDefaults: boolean;
}
export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
export const TaskCoreEditor: React.FC<CapabilityEditorProps> = ({
task,
allTasks,
allowedTasks,
@ -28,8 +18,6 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
shouldAssignDefaults,
}) => {
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const prevErrorsRef = useRef<Record<string, string>>({});
// Generate a unique default name
const nameExists = (tasks: TaskDefinition[], candidate: string): boolean => {
@ -49,7 +37,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
if (!displayName) return "New Task";
const base = `${tTaskType(displayName)} `;
let index = 1;
let index = 0;
while (nameExists(tasks, `${base}${index}`)) {
index++;
@ -60,49 +48,33 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
[allowedTasks, task.type, tTaskType],
);
const assignDefaults = (
newConfig: Record<string, unknown>,
task: TaskDefinition,
allTasks: TaskDefinition[],
allowedTasks: TaskMetadata[],
formatNewTaskName: (tasks: TaskDefinition[]) => string,
) => {
const displayName = allowedTasks.find(
(t) => t.taskType === task.type,
)?.displayName;
const assignDefaults = useCallback(
(newConfig: Record<string, unknown>) => {
const displayName = allowedTasks.find(
(t) => t.taskType === task.type,
)?.displayName;
// Assign default name
if (displayName) {
newConfig.name = formatNewTaskName(allTasks);
}
// Assign default name
if (displayName) {
newConfig.name = formatNewTaskName(allTasks);
}
// Assign default predecessor (the task immediately before this one)
const index = allTasks.findIndex((t) => t.config.guid === task.config.guid);
// Assign default predecessor (the task immediately before this one)
const index = allTasks.findIndex(
(t) => t.config.guid === task.config.guid,
);
if (index > 0) {
const previousTask = allTasks[index - 1];
newConfig.predecessors = [previousTask.config.guid as string];
}
};
if (index > 0) {
const previousTask = allTasks[index - 1];
newConfig.predecessors = [previousTask.config.guid as string];
}
},
[formatNewTaskName, task, allTasks, allowedTasks],
);
useEffect(() => {
if (!shouldAssignDefaults) return;
const newConfig = { ...task.config };
assignDefaults(newConfig, task, allTasks, allowedTasks, formatNewTaskName);
onChange({
...task,
config: newConfig,
});
}, [
shouldAssignDefaults,
task,
useCapabilityDefaults(shouldAssignDefaults, task, onChange, assignDefaults, [
allTasks,
allowedTasks,
formatNewTaskName,
onChange,
]);
const runValidation = useCallback(() => {
@ -156,30 +128,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
return { errors, isValid };
}, [task, allTasks]);
const prevInitialValidationRef = useRef<{
isValid: boolean;
errors: Record<string, string>;
} | null>(null);
// Validate when task changes (new task selected / created)
useEffect(() => {
const result = runValidation();
const prev = prevInitialValidationRef.current;
const changed =
!prev ||
prev.isValid !== result.isValid ||
Object.keys(prev.errors).length !== Object.keys(result.errors).length ||
Object.entries(result.errors).some(([k, v]) => prev.errors[k] !== v);
if (changed) {
setFieldErrors(result.errors);
prevErrorsRef.current = result.errors;
onValidate(result);
prevInitialValidationRef.current = result;
}
}, [
const { fieldErrors } = useValidation(runValidation, onValidate, [
task.config.guid,
task.config.name,
task.config.description,
@ -211,7 +160,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
"name",
"Name",
InputType.text,
fieldErrors["name"],
fieldErrors,
)}
{renderTaskField(
@ -220,7 +169,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
"description",
"Description",
InputType.textarea,
fieldErrors["description"],
fieldErrors,
)}
{renderTaskField(
@ -229,7 +178,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
"predecessors",
"Predecessors",
InputType.multiselect,
fieldErrors["predecessors"],
fieldErrors,
"",
0,
{

View File

@ -5,6 +5,15 @@ import {
} from "../services/WorkflowTemplateService";
import { TaskCoreEditor } from "./CapabilityEditors/TaskCoreEditor";
export interface CapabilityEditorProps {
task: TaskDefinition;
allTasks: TaskDefinition[];
allowedTasks: TaskMetadata[];
onChange: (updated: TaskDefinition) => void;
onValidate: (result: TaskValidationResult) => void;
shouldAssignDefaults: boolean;
}
export interface TaskValidationResult {
isValid: boolean;
errors: Record<string, string>;

View File

@ -7,7 +7,7 @@ export const renderTaskField = (
field: string,
label: string,
type: InputType,
error?: string,
errors: Record<string, string>,
placeholder?: string,
maxLength?: number,
extraProps?: {
@ -34,7 +34,7 @@ export const renderTaskField = (
field,
label,
(task.config as any)[field],
error,
errors[field],
type,
handleChange,
false,

View File

@ -0,0 +1,23 @@
import { useEffect } from "react";
import { TaskDefinition } from "../services/WorkflowTemplateService";
export function useCapabilityDefaults(
shouldAssignDefaults: boolean,
task: TaskDefinition,
onChange: (updated: TaskDefinition) => void,
assignDefaults: (newConfig: Record<string, unknown>) => void,
deps: unknown[],
) {
useEffect(() => {
if (!shouldAssignDefaults) return;
const newConfig = { ...task.config };
assignDefaults(newConfig);
onChange({
...task,
config: newConfig,
});
}, [shouldAssignDefaults, task, onChange, assignDefaults, ...deps]);
}

View File

@ -0,0 +1,32 @@
import { useEffect, useRef, useState } from "react";
import { TaskValidationResult } from "./TasksEditor";
export function useValidation(
runValidation: () => TaskValidationResult,
onValidate: (result: TaskValidationResult) => void,
deps: unknown[],
) {
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const prevErrorsRef = useRef<Record<string, string>>({});
const prevInitialValidationRef = useRef<TaskValidationResult | null>(null);
useEffect(() => {
const result = runValidation();
const prev = prevInitialValidationRef.current;
const changed =
!prev ||
prev.isValid !== result.isValid ||
Object.keys(prev.errors).length !== Object.keys(result.errors).length ||
Object.entries(result.errors).some(([k, v]) => prev.errors[k] !== v);
if (changed) {
setFieldErrors(result.errors);
prevErrorsRef.current = result.errors;
onValidate(result);
prevInitialValidationRef.current = result;
}
}, deps);
return { fieldErrors };
}