Refactored the code so that the capability editors have even less duplicated code
This commit is contained in:
parent
e7a084c301
commit
86ebdc8b72
@ -1,25 +1,15 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback } from "react";
|
||||||
import { InputType } from "../../../../../components/common/Input";
|
import { InputType } from "../../../../../components/common/Input";
|
||||||
import {
|
import { TaskDefinition } from "../../services/WorkflowTemplateService";
|
||||||
TaskDefinition,
|
|
||||||
TaskMetadata,
|
|
||||||
} from "../../services/WorkflowTemplateService";
|
|
||||||
import { renderTaskField } from "../taskEditorHelpers";
|
import { renderTaskField } from "../taskEditorHelpers";
|
||||||
import { TaskValidationResult } from "../TasksEditor";
|
import { CapabilityEditorProps } from "../TasksEditor";
|
||||||
import { Namespaces } from "../../../../../i18n/i18n";
|
import { Namespaces } from "../../../../../i18n/i18n";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getAllDescendants } from "../workflowGraphUtils";
|
import { getAllDescendants } from "../workflowGraphUtils";
|
||||||
|
import { useValidation } from "../useValidation";
|
||||||
|
import { useCapabilityDefaults } from "../useCapabilityDefaults";
|
||||||
|
|
||||||
interface TaskCoreEditorProps {
|
export const TaskCoreEditor: React.FC<CapabilityEditorProps> = ({
|
||||||
task: TaskDefinition;
|
|
||||||
allTasks: TaskDefinition[];
|
|
||||||
allowedTasks: TaskMetadata[];
|
|
||||||
onChange: (updated: TaskDefinition) => void;
|
|
||||||
onValidate: (result: TaskValidationResult) => void;
|
|
||||||
shouldAssignDefaults: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
|
||||||
task,
|
task,
|
||||||
allTasks,
|
allTasks,
|
||||||
allowedTasks,
|
allowedTasks,
|
||||||
@ -28,8 +18,6 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
|||||||
shouldAssignDefaults,
|
shouldAssignDefaults,
|
||||||
}) => {
|
}) => {
|
||||||
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
|
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
|
||||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
|
||||||
const prevErrorsRef = useRef<Record<string, string>>({});
|
|
||||||
|
|
||||||
// Generate a unique default name
|
// Generate a unique default name
|
||||||
const nameExists = (tasks: TaskDefinition[], candidate: string): boolean => {
|
const nameExists = (tasks: TaskDefinition[], candidate: string): boolean => {
|
||||||
@ -49,7 +37,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
|||||||
if (!displayName) return "New Task";
|
if (!displayName) return "New Task";
|
||||||
|
|
||||||
const base = `${tTaskType(displayName)} `;
|
const base = `${tTaskType(displayName)} `;
|
||||||
let index = 1;
|
let index = 0;
|
||||||
|
|
||||||
while (nameExists(tasks, `${base}${index}`)) {
|
while (nameExists(tasks, `${base}${index}`)) {
|
||||||
index++;
|
index++;
|
||||||
@ -60,49 +48,33 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
|||||||
[allowedTasks, task.type, tTaskType],
|
[allowedTasks, task.type, tTaskType],
|
||||||
);
|
);
|
||||||
|
|
||||||
const assignDefaults = (
|
const assignDefaults = useCallback(
|
||||||
newConfig: Record<string, unknown>,
|
(newConfig: Record<string, unknown>) => {
|
||||||
task: TaskDefinition,
|
const displayName = allowedTasks.find(
|
||||||
allTasks: TaskDefinition[],
|
(t) => t.taskType === task.type,
|
||||||
allowedTasks: TaskMetadata[],
|
)?.displayName;
|
||||||
formatNewTaskName: (tasks: TaskDefinition[]) => string,
|
|
||||||
) => {
|
|
||||||
const displayName = allowedTasks.find(
|
|
||||||
(t) => t.taskType === task.type,
|
|
||||||
)?.displayName;
|
|
||||||
|
|
||||||
// Assign default name
|
// Assign default name
|
||||||
if (displayName) {
|
if (displayName) {
|
||||||
newConfig.name = formatNewTaskName(allTasks);
|
newConfig.name = formatNewTaskName(allTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign default predecessor (the task immediately before this one)
|
// Assign default predecessor (the task immediately before this one)
|
||||||
const index = allTasks.findIndex((t) => t.config.guid === task.config.guid);
|
const index = allTasks.findIndex(
|
||||||
|
(t) => t.config.guid === task.config.guid,
|
||||||
|
);
|
||||||
|
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
const previousTask = allTasks[index - 1];
|
const previousTask = allTasks[index - 1];
|
||||||
newConfig.predecessors = [previousTask.config.guid as string];
|
newConfig.predecessors = [previousTask.config.guid as string];
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[formatNewTaskName, task, allTasks, allowedTasks],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useCapabilityDefaults(shouldAssignDefaults, task, onChange, assignDefaults, [
|
||||||
if (!shouldAssignDefaults) return;
|
|
||||||
|
|
||||||
const newConfig = { ...task.config };
|
|
||||||
|
|
||||||
assignDefaults(newConfig, task, allTasks, allowedTasks, formatNewTaskName);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
...task,
|
|
||||||
config: newConfig,
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
shouldAssignDefaults,
|
|
||||||
task,
|
|
||||||
allTasks,
|
allTasks,
|
||||||
allowedTasks,
|
allowedTasks,
|
||||||
formatNewTaskName,
|
|
||||||
onChange,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const runValidation = useCallback(() => {
|
const runValidation = useCallback(() => {
|
||||||
@ -156,30 +128,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
|||||||
return { errors, isValid };
|
return { errors, isValid };
|
||||||
}, [task, allTasks]);
|
}, [task, allTasks]);
|
||||||
|
|
||||||
const prevInitialValidationRef = useRef<{
|
const { fieldErrors } = useValidation(runValidation, onValidate, [
|
||||||
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;
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
task.config.guid,
|
task.config.guid,
|
||||||
task.config.name,
|
task.config.name,
|
||||||
task.config.description,
|
task.config.description,
|
||||||
@ -211,7 +160,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
|||||||
"name",
|
"name",
|
||||||
"Name",
|
"Name",
|
||||||
InputType.text,
|
InputType.text,
|
||||||
fieldErrors["name"],
|
fieldErrors,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{renderTaskField(
|
{renderTaskField(
|
||||||
@ -220,7 +169,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
|||||||
"description",
|
"description",
|
||||||
"Description",
|
"Description",
|
||||||
InputType.textarea,
|
InputType.textarea,
|
||||||
fieldErrors["description"],
|
fieldErrors,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{renderTaskField(
|
{renderTaskField(
|
||||||
@ -229,7 +178,7 @@ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
|||||||
"predecessors",
|
"predecessors",
|
||||||
"Predecessors",
|
"Predecessors",
|
||||||
InputType.multiselect,
|
InputType.multiselect,
|
||||||
fieldErrors["predecessors"],
|
fieldErrors,
|
||||||
"",
|
"",
|
||||||
0,
|
0,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,6 +5,15 @@ import {
|
|||||||
} from "../services/WorkflowTemplateService";
|
} from "../services/WorkflowTemplateService";
|
||||||
import { TaskCoreEditor } from "./CapabilityEditors/TaskCoreEditor";
|
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 {
|
export interface TaskValidationResult {
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const renderTaskField = (
|
|||||||
field: string,
|
field: string,
|
||||||
label: string,
|
label: string,
|
||||||
type: InputType,
|
type: InputType,
|
||||||
error?: string,
|
errors: Record<string, string>,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
maxLength?: number,
|
maxLength?: number,
|
||||||
extraProps?: {
|
extraProps?: {
|
||||||
@ -34,7 +34,7 @@ export const renderTaskField = (
|
|||||||
field,
|
field,
|
||||||
label,
|
label,
|
||||||
(task.config as any)[field],
|
(task.config as any)[field],
|
||||||
error,
|
errors[field],
|
||||||
type,
|
type,
|
||||||
handleChange,
|
handleChange,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@ -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]);
|
||||||
|
}
|
||||||
@ -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 };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user