Upgraded the TaskNameDisplayPanel to be more reusable.

This commit is contained in:
Colin Dawson 2026-03-14 23:02:42 +00:00
parent 7ee7776c78
commit 74c105a42e
6 changed files with 101 additions and 9 deletions

View File

@ -63,3 +63,28 @@
min-height: 0; min-height: 0;
} }
} }
.workflow-tasks-grid {
grid-template-columns: max-content minmax(0, 1fr);
> .workflow-tasks-list-host {
width: max-content;
}
}
.task-list-container--expand-to-content {
width: max-content;
.selectable-list {
width: max-content;
}
.task-name-display-panel {
width: max-content;
}
.task-name-display-panel__name {
overflow: visible;
text-overflow: clip;
}
}

View File

@ -25,6 +25,18 @@
white-space: nowrap; white-space: nowrap;
} }
.validation-error-icon--reserve-space {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 1rem;
width: 1rem;
.error-icon {
margin-left: 0;
}
}
.task-name-display-panel--wrap { .task-name-display-panel--wrap {
white-space: normal; white-space: normal;

View File

@ -4,16 +4,26 @@ import React from "react";
interface ValidationErrorIconProps { interface ValidationErrorIconProps {
visible: boolean; visible: boolean;
reserveSpace?: boolean;
} }
const ValidationErrorIcon: React.FC<ValidationErrorIconProps> = ({ const ValidationErrorIcon: React.FC<ValidationErrorIconProps> = ({
visible, visible,
reserveSpace = false,
}) => { }) => {
if (!visible) return null; if (!visible && !reserveSpace) return null;
const wrapperClassName = reserveSpace
? "validation-error-icon validation-error-icon--reserve-space"
: "validation-error-icon";
return ( return (
<span className="error-icon"> <span className={wrapperClassName} aria-hidden={!visible}>
<FontAwesomeIcon icon={faExclamationCircle} /> {visible && (
<span className="error-icon">
<FontAwesomeIcon icon={faExclamationCircle} />
</span>
)}
</span> </span>
); );
}; };

View File

@ -93,7 +93,7 @@ const TaskList: React.FC<TaskListProps> = ({
const sortedTasks = sortTasksTopologically(tasks); const sortedTasks = sortTasksTopologically(tasks);
return ( return (
<div className="task-list-container"> <div className="task-list-container task-list-container--expand-to-content">
<div className="task-list-header"> <div className="task-list-header">
<AddTaskButton tasksMetadata={tasksMetadata} onAdd={handleAddTask} /> <AddTaskButton tasksMetadata={tasksMetadata} onAdd={handleAddTask} />
</div> </div>
@ -111,6 +111,7 @@ const TaskList: React.FC<TaskListProps> = ({
showValidationErrorIcon={ showValidationErrorIcon={
validTasksList[x.config.guid as string] === false validTasksList[x.config.guid as string] === false
} }
reserveValidationErrorIconSpace={true}
/> />
); );
} else return <></>; } else return <></>;

View File

@ -11,14 +11,19 @@ export interface TaskNameDisplayPanel {
task: TaskDefinition; task: TaskDefinition;
showValidationErrorIcon: boolean; showValidationErrorIcon: boolean;
allowWordWrap?: boolean; allowWordWrap?: boolean;
reserveValidationErrorIconSpace?: boolean;
} }
const TaskNameDisplayPanel: React.FC<TaskNameDisplayPanel> = ({ const TaskNameDisplayPanel: React.FC<TaskNameDisplayPanel> = ({
task, task,
showValidationErrorIcon = false, showValidationErrorIcon = false,
allowWordWrap = false, allowWordWrap = false,
reserveValidationErrorIconSpace = false,
}) => { }) => {
const [tasksMetadata, setTasksMetadata] = React.useState<TaskMetadata[]>([]); const [tasksMetadata, setTasksMetadata] = React.useState<TaskMetadata[]>([]);
const [isNameTruncated, setIsNameTruncated] = React.useState(false);
const taskNameRef = React.useRef<HTMLSpanElement | null>(null);
const taskName = task.config.name as string;
useEffect(() => { useEffect(() => {
const fetchTaskMetadata = async () => { const fetchTaskMetadata = async () => {
@ -42,13 +47,52 @@ const TaskNameDisplayPanel: React.FC<TaskNameDisplayPanel> = ({
? "task-name-display-panel task-name-display-panel--wrap" ? "task-name-display-panel task-name-display-panel--wrap"
: "task-name-display-panel"; : "task-name-display-panel";
const updateTruncationState = React.useCallback(() => {
const el = taskNameRef.current;
if (!el || allowWordWrap) {
setIsNameTruncated(false);
return;
}
setIsNameTruncated(el.scrollWidth > el.clientWidth);
}, [allowWordWrap]);
useEffect(() => {
updateTruncationState();
}, [taskName, allowWordWrap, updateTruncationState]);
useEffect(() => {
const el = taskNameRef.current;
if (!el || allowWordWrap || typeof ResizeObserver === "undefined") {
return;
}
const observer = new ResizeObserver(() => {
updateTruncationState();
});
observer.observe(el);
return () => {
observer.disconnect();
};
}, [allowWordWrap, updateTruncationState]);
return ( return (
<div className={panelClassName}> <div className={panelClassName}>
<FontAwesomeStringIcon icon={meta?.icon} /> <FontAwesomeStringIcon icon={meta?.icon} />
<span className="task-name-display-panel__name"> <span
{task.config.name as string} ref={taskNameRef}
className="task-name-display-panel__name"
title={isNameTruncated ? taskName : undefined}
>
{taskName}
</span> </span>
{<ValidationErrorIcon visible={showValidationErrorIcon} />} <ValidationErrorIcon
visible={showValidationErrorIcon}
reserveSpace={reserveValidationErrorIconSpace}
/>
</div> </div>
); );
}; };

View File

@ -248,8 +248,8 @@ const TasksTab: React.FC<TasksTabProps> = ({
); );
return ( return (
<div className="two-column-grid no-scroll"> <div className="two-column-grid no-scroll workflow-tasks-grid">
<div className="fit-content-width`"> <div className="fit-content-width workflow-tasks-list-host">
<TaskList <TaskList
tasks={tasks} tasks={tasks}
validTasksList={taskValidation} validTasksList={taskValidation}