diff --git a/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx b/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx index 501edad..debdd4d 100644 --- a/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx +++ b/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx @@ -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 = ({ +export const TaskCoreEditor: React.FC = ({ task, allTasks, allowedTasks, @@ -28,8 +18,6 @@ export const TaskCoreEditor: React.FC = ({ shouldAssignDefaults, }) => { const { t: tTaskType } = useTranslation(Namespaces.TaskTypes); - const [fieldErrors, setFieldErrors] = useState>({}); - const prevErrorsRef = useRef>({}); // Generate a unique default name const nameExists = (tasks: TaskDefinition[], candidate: string): boolean => { @@ -49,7 +37,7 @@ export const TaskCoreEditor: React.FC = ({ 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 = ({ [allowedTasks, task.type, tTaskType], ); - const assignDefaults = ( - newConfig: Record, - 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) => { + 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 = ({ return { errors, isValid }; }, [task, allTasks]); - const prevInitialValidationRef = useRef<{ - isValid: boolean; - errors: Record; - } | 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 = ({ "name", "Name", InputType.text, - fieldErrors["name"], + fieldErrors, )} {renderTaskField( @@ -220,7 +169,7 @@ export const TaskCoreEditor: React.FC = ({ "description", "Description", InputType.textarea, - fieldErrors["description"], + fieldErrors, )} {renderTaskField( @@ -229,7 +178,7 @@ export const TaskCoreEditor: React.FC = ({ "predecessors", "Predecessors", InputType.multiselect, - fieldErrors["predecessors"], + fieldErrors, "", 0, { diff --git a/src/modules/manager/workflowTemplates/components/TasksEditor.tsx b/src/modules/manager/workflowTemplates/components/TasksEditor.tsx index 31432a1..5c18a5f 100644 --- a/src/modules/manager/workflowTemplates/components/TasksEditor.tsx +++ b/src/modules/manager/workflowTemplates/components/TasksEditor.tsx @@ -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; diff --git a/src/modules/manager/workflowTemplates/components/taskEditorHelpers.tsx b/src/modules/manager/workflowTemplates/components/taskEditorHelpers.tsx index 76775ff..a6bb93e 100644 --- a/src/modules/manager/workflowTemplates/components/taskEditorHelpers.tsx +++ b/src/modules/manager/workflowTemplates/components/taskEditorHelpers.tsx @@ -7,7 +7,7 @@ export const renderTaskField = ( field: string, label: string, type: InputType, - error?: string, + errors: Record, 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, diff --git a/src/modules/manager/workflowTemplates/components/useCapabilityDefaults.tsx b/src/modules/manager/workflowTemplates/components/useCapabilityDefaults.tsx new file mode 100644 index 0000000..00081d4 --- /dev/null +++ b/src/modules/manager/workflowTemplates/components/useCapabilityDefaults.tsx @@ -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) => void, + deps: unknown[], +) { + useEffect(() => { + if (!shouldAssignDefaults) return; + + const newConfig = { ...task.config }; + + assignDefaults(newConfig); + + onChange({ + ...task, + config: newConfig, + }); + }, [shouldAssignDefaults, task, onChange, assignDefaults, ...deps]); +} diff --git a/src/modules/manager/workflowTemplates/components/useValidation.tsx b/src/modules/manager/workflowTemplates/components/useValidation.tsx new file mode 100644 index 0000000..f9db3fb --- /dev/null +++ b/src/modules/manager/workflowTemplates/components/useValidation.tsx @@ -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>({}); + const prevErrorsRef = useRef>({}); + const prevInitialValidationRef = useRef(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 }; +}