The validation engine is now functional, and will allow the user to get a breadcrumb path to the actual problem, without being cluttered up with error messages.
This commit is contained in:
parent
2f300faf90
commit
1b4a834b19
@ -181,6 +181,7 @@
|
||||
"Subject": "Subject",
|
||||
"Support": "Support",
|
||||
"SupportingData": "Supporting Data",
|
||||
"TasksValidationError": "Tasks configuration is invalid",
|
||||
"TemplateIdCannotBeNull": "Template Id cannot be null",
|
||||
"TemplateUnknown": "Template unknown",
|
||||
"Text": "Text",
|
||||
|
||||
@ -32,3 +32,8 @@
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
margin-left: 6px;
|
||||
color: $red;
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { useHashSegment } from "../../utils/HashNavigationContext";
|
||||
import TabHeader from "./TabHeader";
|
||||
import Tab from "./Tab";
|
||||
|
||||
interface HorizontalTabsProps {
|
||||
children: JSX.Element[];
|
||||
children: React.ReactElement<typeof Tab>[];
|
||||
initialTab?: string;
|
||||
hashSegment?: number;
|
||||
activeTab?: string;
|
||||
@ -20,6 +21,7 @@ const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
|
||||
const hashValue = useHashSegment(
|
||||
hashSegment !== undefined ? hashSegment : -1,
|
||||
);
|
||||
|
||||
const [internalActiveTab, setInternalActiveTab] = useState<string>("");
|
||||
|
||||
const isControlled = activeTab !== undefined;
|
||||
@ -71,12 +73,14 @@ const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
|
||||
<div className="horizionalTabs">
|
||||
<ul className="tab-list">
|
||||
{children.map((child) => {
|
||||
const { id, label } = child.props;
|
||||
const tabId = id || label;
|
||||
const { id, label, hasError } = child.props;
|
||||
const tabId = id;
|
||||
return (
|
||||
<TabHeader
|
||||
id={id}
|
||||
key={label}
|
||||
label={label}
|
||||
hasError={hasError}
|
||||
isActive={tabId === currentTab}
|
||||
onClick={() => onClickTabItem(tabId)}
|
||||
/>
|
||||
|
||||
@ -117,7 +117,7 @@ function Input(props: InputProps) {
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
disabled={readOnly}
|
||||
value={showValue || defaultValue}
|
||||
value={showValue ?? ""}
|
||||
autoComplete={autoComplete}
|
||||
></textarea>
|
||||
)}
|
||||
|
||||
@ -3,10 +3,16 @@ import React from "react";
|
||||
interface TabProps {
|
||||
id?: string;
|
||||
label: string;
|
||||
hasError?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Tab({ id, label, children }: TabProps): JSX.Element {
|
||||
export default function Tab({
|
||||
id,
|
||||
label,
|
||||
hasError = false,
|
||||
children,
|
||||
}: TabProps): JSX.Element {
|
||||
return (
|
||||
<div data-tab-id={id} data-tab-label={label}>
|
||||
{children}
|
||||
|
||||
@ -1,17 +1,23 @@
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
interface TabHeaderProps {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
label: string;
|
||||
label: string | React.ReactNode;
|
||||
hasError: boolean;
|
||||
onClick: (label: string) => void;
|
||||
}
|
||||
|
||||
export default function TabHeader({
|
||||
id,
|
||||
isActive,
|
||||
label,
|
||||
hasError,
|
||||
onClick,
|
||||
}: TabHeaderProps) {
|
||||
const handleClick = useCallback(() => onClick(label), [onClick, label]);
|
||||
const handleClick = useCallback(() => onClick(id), [onClick, id]);
|
||||
|
||||
const className = isActive
|
||||
? "tab-list-item tab-list-active"
|
||||
@ -20,6 +26,11 @@ export default function TabHeader({
|
||||
return (
|
||||
<li className={className} onClick={handleClick}>
|
||||
{label}
|
||||
{hasError && (
|
||||
<span className="error-icon">
|
||||
<FontAwesomeIcon icon={faExclamationCircle} />
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../../i18n/i18n";
|
||||
import HorizontalTabs from "../../../components/common/HorizionalTabs";
|
||||
@ -124,9 +124,22 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
||||
form.handleSubmit(e, doSubmit);
|
||||
};
|
||||
|
||||
const { loaded, redirect, errors, data } = form.state;
|
||||
const [tasksValid, setTasksValid] = useState(true);
|
||||
|
||||
const handleTasksValidate = (isValid: boolean) => {
|
||||
console.log("Test", isValid);
|
||||
setTasksValid(isValid);
|
||||
};
|
||||
|
||||
const { loaded, redirect, errors: formErrors, data } = form.state;
|
||||
if (redirect) return <Navigate to={redirect} />;
|
||||
|
||||
let errors = { ...formErrors };
|
||||
|
||||
if (!tasksValid) {
|
||||
errors["tasks"] = t("TasksValidationError");
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Tabs
|
||||
// -----------------------------
|
||||
@ -141,12 +154,13 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
||||
/>
|
||||
</Tab>,
|
||||
|
||||
<Tab key="tasks" id="tasks" label={t("Tasks")}>
|
||||
<Tab key="tasks" id="tasks" label={t("Tasks")} hasError={!tasksValid}>
|
||||
<TasksTab
|
||||
data={data}
|
||||
errors={errors}
|
||||
isEditMode={editMode}
|
||||
onTasksChange={form.handleTasksChange}
|
||||
onValidate={handleTasksValidate}
|
||||
/>
|
||||
</Tab>,
|
||||
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { InputType } from "../../../../../components/common/Input";
|
||||
import { TaskDefinition } from "../../services/WorkflowTemplateService";
|
||||
import { renderTaskField } from "../taskEditorHelpers";
|
||||
import { TaskValidationResult } from "../TasksEditor";
|
||||
|
||||
interface TaskCoreEditorProps {
|
||||
task: TaskDefinition;
|
||||
allTasks: TaskDefinition[];
|
||||
onChange: (updated: TaskDefinition) => void;
|
||||
onValidate: (result: TaskValidationResult) => void;
|
||||
}
|
||||
|
||||
export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
|
||||
task,
|
||||
allTasks,
|
||||
onChange,
|
||||
onValidate,
|
||||
}) => {
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
const prevErrorsRef = useRef<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
// Validation rules
|
||||
if (task.config.description === "") {
|
||||
errors["description"] = "Description cannot be empty";
|
||||
}
|
||||
|
||||
const isValid = Object.keys(errors).length === 0;
|
||||
|
||||
// Compare with previous errors
|
||||
const prevErrors = prevErrorsRef.current;
|
||||
const errorsChanged =
|
||||
Object.keys(prevErrors).length !== Object.keys(errors).length ||
|
||||
Object.entries(errors).some(([key, value]) => prevErrors[key] !== value);
|
||||
|
||||
if (errorsChanged) {
|
||||
setFieldErrors(errors);
|
||||
onValidate({ isValid, errors });
|
||||
prevErrorsRef.current = errors;
|
||||
}
|
||||
}, [task.config.description, task.config.name, onValidate]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderTaskField(
|
||||
task,
|
||||
onChange,
|
||||
"name",
|
||||
"Name",
|
||||
InputType.text,
|
||||
fieldErrors["name"],
|
||||
)}
|
||||
|
||||
{renderTaskField(
|
||||
task,
|
||||
onChange,
|
||||
"description",
|
||||
"Description",
|
||||
InputType.textarea,
|
||||
fieldErrors["description"],
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -7,9 +7,12 @@ import AddTaskButton from "./AddTaskButton";
|
||||
import { Namespaces } from "../../../../i18n/i18n";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SelectableList } from "../../../../components/common/SelectableList";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
interface TaskListProps {
|
||||
tasks: TaskDefinition[];
|
||||
validTasksList: Record<string, boolean>;
|
||||
taskType: string;
|
||||
onChange: (tasks: TaskDefinition[]) => void;
|
||||
selectedTask: TaskDefinition | null;
|
||||
@ -18,6 +21,7 @@ interface TaskListProps {
|
||||
|
||||
const TaskList: React.FC<TaskListProps> = ({
|
||||
tasks,
|
||||
validTasksList,
|
||||
taskType,
|
||||
onChange,
|
||||
selectedTask,
|
||||
@ -57,7 +61,17 @@ const TaskList: React.FC<TaskListProps> = ({
|
||||
<SelectableList
|
||||
items={tasks}
|
||||
selectedValue={selectedTask}
|
||||
renderLabel={(x) => x.config.name as string}
|
||||
renderLabel={(x) => (
|
||||
<>
|
||||
{x.config.name as string}
|
||||
|
||||
{validTasksList[x.config.guid as string] === false && (
|
||||
<span className="error-icon">
|
||||
<FontAwesomeIcon icon={faExclamationCircle} />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
onSelect={(item) => onSelectTask(item)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
import { useState } from "react";
|
||||
import { TaskDefinition } from "../services/WorkflowTemplateService";
|
||||
import { TaskCoreEditor } from "./CapabilityEditors/TaskCoreEditor";
|
||||
|
||||
export interface TaskValidationResult {
|
||||
isValid: boolean;
|
||||
errors: Record<string, string>;
|
||||
}
|
||||
|
||||
interface TaskEditorProps {
|
||||
task: TaskDefinition;
|
||||
allTasks: TaskDefinition[];
|
||||
onChange: (updatedTask: TaskDefinition) => void;
|
||||
onValidate: (taskId: string, isValid: boolean) => void;
|
||||
}
|
||||
|
||||
export const TaskEditor: React.FC<TaskEditorProps> = ({
|
||||
task,
|
||||
allTasks,
|
||||
onChange,
|
||||
onValidate,
|
||||
}) => {
|
||||
const [validationMap, setValidationMap] = useState<
|
||||
Record<string, TaskValidationResult>
|
||||
>({});
|
||||
|
||||
const onCapabilityValidate = (
|
||||
capabilityName: string,
|
||||
result: TaskValidationResult,
|
||||
) => {
|
||||
setValidationMap((prev) => {
|
||||
const updated = { ...prev, [capabilityName]: result };
|
||||
|
||||
const allValid = Object.values(updated).every((r) => r.isValid);
|
||||
|
||||
onValidate(task.config.guid as string, allValid);
|
||||
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TaskCoreEditor
|
||||
task={task}
|
||||
allTasks={allTasks}
|
||||
onChange={onChange}
|
||||
onValidate={(result) => onCapabilityValidate("core", result)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -4,12 +4,14 @@ import {
|
||||
TaskDefinition,
|
||||
} from "../services/WorkflowTemplateService";
|
||||
import TaskList from "./TaskList";
|
||||
import { TaskEditor } from "./TasksEditor";
|
||||
|
||||
interface TasksTabProps {
|
||||
data: CreateWorkflowTemplateVersion;
|
||||
errors: Record<string, string>;
|
||||
isEditMode: boolean;
|
||||
onTasksChange: (name: string, value: TaskDefinition[]) => void;
|
||||
onValidate: (isValid: boolean) => void;
|
||||
}
|
||||
|
||||
const TasksTab: React.FC<TasksTabProps> = ({
|
||||
@ -17,9 +19,27 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
errors,
|
||||
isEditMode,
|
||||
onTasksChange,
|
||||
onValidate,
|
||||
}) => {
|
||||
const tasks = data.tasks;
|
||||
const [selectedTask, setSelectedTask] = useState<TaskDefinition | null>(null);
|
||||
const [taskValidation, setTaskValidation] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
|
||||
const handleTaskValidate = (taskId: string, isValid: boolean) => {
|
||||
setTaskValidation((prev) => {
|
||||
const updated = { ...prev, [taskId]: isValid };
|
||||
|
||||
// Compute overall validity
|
||||
const allValid = Object.values(updated).every((v) => v === true);
|
||||
|
||||
// Bubble up to parent
|
||||
onValidate(allValid);
|
||||
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tasks.length === 0) {
|
||||
@ -63,13 +83,29 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
<div className="fit-content-width">
|
||||
<TaskList
|
||||
tasks={tasks}
|
||||
validTasksList={taskValidation}
|
||||
taskType="GeneralTask"
|
||||
onChange={handleTasksChange}
|
||||
selectedTask={selectedTask}
|
||||
onSelectTask={setSelectedTask}
|
||||
/>
|
||||
</div>
|
||||
<div>{selectedTask?.config.name as string}</div>
|
||||
{selectedTask && (
|
||||
<TaskEditor
|
||||
task={selectedTask}
|
||||
allTasks={tasks}
|
||||
onChange={(updatedTask) => {
|
||||
const newTasks = tasks.map((t) =>
|
||||
(t.config as any).guid === (updatedTask.config as any).guid
|
||||
? updatedTask
|
||||
: t,
|
||||
);
|
||||
handleTasksChange(newTasks);
|
||||
setSelectedTask(updatedTask);
|
||||
}}
|
||||
onValidate={handleTaskValidate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import Input, { InputType } from "../../../../components/common/Input";
|
||||
import { TaskDefinition } from "../services/WorkflowTemplateService";
|
||||
|
||||
export const renderTaskField = (
|
||||
task: TaskDefinition,
|
||||
onChange: (updated: TaskDefinition) => void,
|
||||
field: string,
|
||||
label: string,
|
||||
type: InputType,
|
||||
error?: string,
|
||||
placeholder?: string,
|
||||
maxLength?: number,
|
||||
) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...task,
|
||||
config: {
|
||||
...task.config,
|
||||
[field]: e.target.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return renderTaskInput(
|
||||
field,
|
||||
label,
|
||||
(task.config as any)[field],
|
||||
error,
|
||||
type,
|
||||
handleChange,
|
||||
false,
|
||||
placeholder ?? "",
|
||||
maxLength ?? 0,
|
||||
);
|
||||
};
|
||||
|
||||
export const renderTaskInput = (
|
||||
name: string,
|
||||
label: string,
|
||||
value: string | number | undefined,
|
||||
error: string | undefined,
|
||||
type: InputType = InputType.text,
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
|
||||
readOnly: boolean = false,
|
||||
placeholder: string = "",
|
||||
maxLength: number = 0,
|
||||
) => {
|
||||
return (
|
||||
<Input
|
||||
includeLabel={true}
|
||||
type={type}
|
||||
name={name}
|
||||
label={label}
|
||||
value={value ?? ""}
|
||||
error={error}
|
||||
maxLength={maxLength}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
placeHolder={placeholder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user