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 { @mixin table {
background: $white; background: $white;
border-radius: $sm; border-radius: $sm;
} }
.tableBackground { .tableBackground {
@include table; @include table;
padding: $lge; padding: $lge;
} }
.tableInfo { .tableInfo {
display: inline-flex; display: inline-flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
} }
table { table {
@include table; @include table;
display: table; display: table;
width: $full-width; width: $full-width;
justify-content: left; justify-content: left;
border: $thin-border + $--es-mono-100; border: $thin-border + $--es-mono-100;
} }
thead { thead {
background-color: $background; background-color: $background;
height: $th-height; height: $th-height;
width: 100%; width: 100%;
th { th {
display: table-row; display: table-row;
justify-content: start; 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 React, { useCallback } from "react";
import ValidationErrorIcon from "../validationErrorIcon";
interface TabHeaderProps { interface TabHeaderProps {
id: string; id: string;
@ -26,11 +25,7 @@ export default function TabHeader({
return ( return (
<li className={className} onClick={handleClick}> <li className={className} onClick={handleClick}>
{label} {label}
{hasError && ( <ValidationErrorIcon visible={hasError} />
<span className="error-icon">
<FontAwesomeIcon icon={faExclamationCircle} />
</span>
)}
</li> </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 { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n"; import { Namespaces } from "../../../i18n/i18n";
import HorizontalTabs from "../../../components/common/HorizionalTabs"; import HorizontalTabs from "../../../components/common/HorizionalTabs";
@ -131,20 +131,12 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
//const [tasksValid, setTasksValid] = useState(true); //const [tasksValid, setTasksValid] = useState(true);
const handleTaskValidate = (taskId: string, isValid: boolean) => { const handleTaskValidate = useCallback((taskId: string, isValid: boolean) => {
setTaskValidation((prev) => { setTaskValidation((prev) => {
const updated = { ...prev, [taskId]: isValid }; 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; return updated;
}); });
}; }, []);
const { loaded, redirect, errors: formErrors, data } = form.state; const { loaded, redirect, errors: formErrors, data } = form.state;
if (redirect) return <Navigate to={redirect} />; if (redirect) return <Navigate to={redirect} />;

View File

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

View File

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

View File

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

View File

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