Added a standard component for the validationErrorIcon.

Fixed a rampant re rendering issue.
This commit is contained in:
Colin Dawson 2026-02-16 22:16:18 +00:00
parent 849d0177ec
commit f530fc7efa
8 changed files with 111 additions and 86 deletions

View File

@ -1,36 +1,39 @@
@import './global.scss';
@import "./global.scss";
@mixin table {
background: $white;
border-radius: $sm;
background: $white;
border-radius: $sm;
}
.tableBackground {
@include table;
padding: $lge;
@include table;
padding: $lge;
}
.tableInfo {
display: inline-flex;
flex-direction: column;
justify-content: space-between;
display: inline-flex;
flex-direction: column;
justify-content: space-between;
}
table {
@include table;
display: table;
width: $full-width;
justify-content: left;
border: $thin-border + $--es-mono-100;
@include table;
display: table;
width: $full-width;
justify-content: left;
border: $thin-border + $--es-mono-100;
}
thead {
background-color: $background;
height: $th-height;
width: 100%;
th {
display: table-row;
justify-content: start;
}
background-color: $background;
height: $th-height;
width: 100%;
th {
display: table-row;
justify-content: start;
}
}
.align-top {
vertical-align: top;
}

View File

@ -1,6 +1,5 @@
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useCallback } from "react";
import ValidationErrorIcon from "../validationErrorIcon";
interface TabHeaderProps {
id: string;
@ -26,11 +25,7 @@ export default function TabHeader({
return (
<li className={className} onClick={handleClick}>
{label}
{hasError && (
<span className="error-icon">
<FontAwesomeIcon icon={faExclamationCircle} />
</span>
)}
<ValidationErrorIcon visible={hasError} />
</li>
);
}

View File

@ -0,0 +1,21 @@
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React from "react";
interface ValidationErrorIconProps {
visible: boolean;
}
const ValidationErrorIcon: React.FC<ValidationErrorIconProps> = ({
visible,
}) => {
if (!visible) return null;
return (
<span className="error-icon">
<FontAwesomeIcon icon={faExclamationCircle} />
</span>
);
};
export default ValidationErrorIcon;

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n";
import HorizontalTabs from "../../../components/common/HorizionalTabs";
@ -131,20 +131,12 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
//const [tasksValid, setTasksValid] = useState(true);
const handleTaskValidate = (taskId: string, isValid: boolean) => {
const handleTaskValidate = useCallback((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
//setTasksValid(allValid);
//setTaskValidation(updated);
return updated;
});
};
}, []);
const { loaded, redirect, errors: formErrors, data } = form.state;
if (redirect) return <Navigate to={redirect} />;

View File

@ -8,33 +8,39 @@ import {
defaultsContext,
} from "../useCapabilityDefaults";
import Button, { ButtonType } from "../../../../../components/common/Button";
import ValidationErrorIcon from "../../../../../components/validationErrorIcon";
import ErrorBlock from "../../../../../components/common/ErrorBlock";
// TODO: Replace with your real RolePicker and RaciPicker
const RolePicker = (props: any) => (
<div style={{ marginBottom: 8 }}>
<input
type="text"
name={props.name}
value={props.value?.id ?? ""}
onChange={(e) =>
props.onChange(props.name, { id: BigInt(e.target.value) })
}
/>
<div className="form-group">
<div style={{ marginBottom: 8 }}>
<input
type="text"
name={props.name}
value={props.value?.id ?? ""}
onChange={(e) =>
props.onChange(props.name, { id: BigInt(e.target.value) })
}
/>
</div>
</div>
);
const RaciPicker = (props: any) => (
<div style={{ marginBottom: 8 }}>
<select
name={props.name}
value={props.value}
onChange={(e) => props.onChange(props.name, e.target.value)}
>
<option value="Responsible">Responsible</option>
<option value="Accountable">Accountable</option>
<option value="Consulted">Consulted</option>
<option value="Informed">Informed</option>
</select>
<div className="form-group">
<div style={{ marginBottom: 8 }}>
<select
name={props.name}
value={props.value}
onChange={(e) => props.onChange(props.name, e.target.value)}
>
<option value="Responsible">Responsible</option>
<option value="Accountable">Accountable</option>
<option value="Consulted">Consulted</option>
<option value="Informed">Informed</option>
</select>
</div>
</div>
);
@ -97,6 +103,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
<table>
<thead>
<tr>
<th></th>
<th>Role</th>
<th>Contact</th>
<th>RACI</th>
@ -109,7 +116,16 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
</thead>
<tbody>
{assignees.map((assignee, index) => (
<tr key={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
name="role"
@ -144,7 +160,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
}
/>
</td>
<td>
<td className="form-group">
<Button
onClick={() => removeAssignee(index)}
buttonType={ButtonType.secondary}
@ -156,6 +172,15 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
))}
</tbody>
</table>
<ErrorBlock
error={Object.values(fieldErrors ?? {})
.filter((_, i) =>
Object.keys(fieldErrors ?? {}).some(
(key) => key === `${guid}.assignees`,
),
)
.join("; ")}
/>
</div>
);
};

View File

@ -9,6 +9,7 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { sortTasksTopologically } from "./workflowGraphUtils";
import { useCapabilityDefaults, validateTask } from "./useCapabilityDefaults";
import ValidationErrorIcon from "../../../../components/validationErrorIcon";
interface TaskListProps {
tasks: TaskDefinition[];
@ -70,11 +71,11 @@ const TaskList: React.FC<TaskListProps> = ({
<>
{x.config.name as string}
{validTasksList[x.config.guid as string] === false && (
<span className="error-icon">
<FontAwesomeIcon icon={faExclamationCircle} />
</span>
)}
{
<ValidationErrorIcon
visible={validTasksList[x.config.guid as string] === false}
/>
}
</>
)}
onSelect={(item) => onSelectTask(item)}

View File

@ -27,26 +27,23 @@ export const TaskEditor: React.FC<TaskEditorProps> = ({
const runValidation = useCallback(
(
bubbleUp = true,
taskToValidate: TaskDefinition,
tasksList: TaskDefinition[],
tasksMetadataList: TaskMetadata[],
) => {
const errors = validateTask(taskToValidate, tasksList, tasksMetadataList);
setFieldErrors(errors);
if (bubbleUp) {
onValidate(
taskToValidate.config.guid as string,
Object.keys(errors).length === 0,
);
}
onValidate(
taskToValidate.config.guid as string,
Object.keys(errors).length === 0,
);
},
[onValidate],
);
React.useEffect(() => {
runValidation(true, task, tasks, tasksMetadata);
}, [runValidation, task, tasks, tasksMetadata]);
runValidation(task, tasks, tasksMetadata);
}, [task.config.guid, runValidation]);
const handleTaskChange = (updatedTask: TaskDefinition) => {
// Update the task list
@ -54,16 +51,7 @@ export const TaskEditor: React.FC<TaskEditorProps> = ({
t.config.guid === updatedTask.config.guid ? updatedTask : t,
);
runValidation(true, updatedTask, updatedTasks, tasksMetadata);
// // Run validation
// const errors = validateTask(updatedTask, updatedTasks, tasksMetadata);
// setFieldErrors(errors);
// // Bubble validity up
// onValidate(
// updatedTask.config.guid as string,
// Object.keys(errors).length === 0,
// );
runValidation(updatedTask, updatedTasks, tasksMetadata);
// Bubble updated task up
onChange(updatedTask);

View File

@ -66,7 +66,7 @@ const TasksTab: React.FC<TasksTabProps> = ({
};
return (
<div className="two-column-grid">
<div className="two-column-grid no-scroll">
<div className="fit-content-width`">
<TaskList
tasks={tasks}