Started working on the Tasks tab

This commit is contained in:
Colin Dawson 2026-02-12 00:40:37 +00:00
parent 854ba4bf1a
commit 6e939fae6e
9 changed files with 249 additions and 22 deletions

View File

@ -0,0 +1,13 @@
.add-task-button {
position: relative;
display: inline-block;
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
padding: 10px;
z-index: 1000;
display: block;
}
}

View File

@ -28,6 +28,7 @@
@import "./horizionalTabs";
@import "./_expandableCell.scss";
@import "./_errorLogs.scss";
@import "./addTaskButton.scss";
//Changes needed to make MS Edge behave the same as other browsers
input::-ms-reveal {

View File

@ -6,50 +6,61 @@ interface HorizontalTabsProps {
children: JSX.Element[];
initialTab?: string;
hashSegment?: number;
activeTab?: string;
onTabChange?: (tab: string) => void;
}
const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
children,
initialTab,
hashSegment,
activeTab,
onTabChange,
}) => {
const hashValue = useHashSegment(
hashSegment !== undefined ? hashSegment : -1,
);
const [activeTab, setActiveTab] = useState<string>("");
const [internalActiveTab, setInternalActiveTab] = useState<string>("");
const isControlled = activeTab !== undefined;
const currentTab = isControlled ? activeTab : internalActiveTab;
// Set initial tab on mount
useEffect(() => {
if (children.length > 0) {
// Only use hash if hashSegment was explicitly provided
if (!isControlled && children.length > 0) {
const useHash = hashSegment !== undefined;
// Validate that the hash matches one of our tab IDs
const hashMatchesTab =
useHash &&
hashValue &&
children.some((child) => child.props.id === hashValue);
// Use id if available, otherwise fall back to label
const firstTabId = children[0].props.id || children[0].props.label;
const tabToSelect =
(hashMatchesTab ? hashValue : initialTab) || firstTabId;
setActiveTab(tabToSelect);
}
}, [children, initialTab, hashValue, hashSegment]);
const onClickTabItem = useCallback((tab: string) => {
setActiveTab((prev) => (prev !== tab ? tab : prev));
}, []);
setInternalActiveTab(tabToSelect);
}
}, [children, initialTab, hashValue, hashSegment, isControlled]);
const onClickTabItem = useCallback(
(tab: string) => {
if (isControlled) {
onTabChange?.(tab);
} else {
setInternalActiveTab((prev) => (prev !== tab ? tab : prev));
}
},
[isControlled, onTabChange],
);
const activeTabChildren = useMemo(() => {
const match = children.find((child) => {
const tabId = child.props.id || child.props.label;
return tabId === activeTab;
return tabId === currentTab;
});
return match ? match.props.children : <></>;
}, [children, activeTab]);
}, [children, currentTab]);
// If only one tab, just render its content
if (children.length === 1) {
@ -66,7 +77,7 @@ const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
<TabHeader
key={label}
label={label}
isActive={tabId === activeTab}
isActive={tabId === currentTab}
onClick={() => onClickTabItem(tabId)}
/>
);

View File

@ -553,6 +553,17 @@ export const useForm = (initialState: FormState): UseFormReturn => {
[state.data, validate, setState],
);
const handleTasksChange = useCallback(
(name: string, value: TaskDefinition[]) => {
const data: FormData = { ...state.data };
data[name] = value;
const errors = validate(data);
setState({ data, errors });
},
[state.data, validate, setState],
);
const api: any = {
state,
schema: schemaRef.current,
@ -575,6 +586,7 @@ export const useForm = (initialState: FormState): UseFormReturn => {
handleUserPickerChange,
handleSsoProviderPickerChange,
handleToggleChange,
handleTasksChange,
setState,
};
Object.defineProperty(api, "schema", {

View File

@ -7,6 +7,7 @@ import GeneralTab from "./components/GeneralTab";
import { Navigate, useParams } from "react-router-dom";
import templateVersionsService, {
CreateWorkflowTemplateVersion,
TaskDefinition,
} from "./services/WorkflowTemplateService";
import Loading from "../../../components/common/Loading";
import { toast } from "react-toastify";
@ -19,12 +20,15 @@ import { useForm } from "../../../components/common/useForm";
import ErrorBlock from "../../../components/common/ErrorBlock";
import authentication from "../../frame/services/authenticationService";
import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef";
import { CustomFieldValue } from "../glossary/services/glossaryService";
import TasksTab from "./components/TasksTab";
const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
editMode,
}) => {
const { t } = useTranslation<typeof Namespaces.Common>();
const { userId } = useParams<{ userId: string }>();
const [activeTab, setActiveTab] = React.useState("general");
// useForm promoted to the parent
const form = useForm({
@ -34,6 +38,7 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
domainId: [] as CustomFieldValue[],
activityNameTemplate: "",
description: "",
tasks: [] as TaskDefinition[],
} as CreateWorkflowTemplateVersion,
errors: {},
redirect: "",
@ -52,7 +57,7 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
.required()
.max(450)
.label(t("ActivityNameTemplate")),
description: Joi.string().required().label(t("Description")),
description: Joi.string().required().allow("").label(t("Description")),
domainId: Joi.required(),
};
@ -97,10 +102,10 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
const { name } = form.state.data;
if (editMode) {
await templateVersionsService.putTemplateVersion({ name });
//await templateVersionsService.putTemplateVersion({ name });
toast.info(t("WorkflowTemplateEdited"));
} else {
await templateVersionsService.postTemplateVersion({ name });
//await templateVersionsService.postTemplateVersion({ name });
toast.info(t("WorkflowTemplateAdded"));
}
@ -134,7 +139,12 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
</Tab>,
<Tab key="tasks" id="tasks" label={t("Tasks")}>
<div>Tasks editor coming soon</div>
<TasksTab
data={data}
errors={errors}
isEditMode={editMode}
onTasksChange={form.handleTasksChange}
/>
</Tab>,
<Tab key="fields" id="fields" label={t("Fields")}>
@ -163,7 +173,13 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
{renderError("_general", errors)}
<HorizontalTabs hashSegment={0}>{tabs}</HorizontalTabs>
<HorizontalTabs
hashSegment={0}
activeTab={activeTab}
onTabChange={setActiveTab}
>
{tabs}
</HorizontalTabs>
</form>
</div>
</Loading>

View File

@ -0,0 +1,61 @@
import React from "react";
import templateVersionsService, {
TaskMetadata,
} from "../services/WorkflowTemplateService";
interface AddTaskButtonProps {
taskType: string;
onAdd: (selectedType: string) => void;
}
const AddTaskButton: React.FC<AddTaskButtonProps> = ({ taskType, onAdd }) => {
const [open, setOpen] = React.useState(false);
const [items, setItems] = React.useState<TaskMetadata[]>([]);
const [loading, setLoading] = React.useState(false);
const toggle = async () => {
const next = !open;
setOpen(next);
// Fetch only when opening AND only once
if (next && items.length === 0) {
setLoading(true);
try {
const meta = await templateVersionsService.getTaskMetadata(taskType);
setItems(meta);
} finally {
setLoading(false);
}
}
};
return (
<div className="add-task-button">
<button className="btn btn-secondary" type="button" onClick={toggle}>
Add Task
</button>
{open && (
<div className="dropdown-menu show">
{loading && <div className="dropdown-item">Loading</div>}
{!loading &&
items.map((item) => (
<button
key={item.taskType}
className="dropdown-item"
onClick={() => {
onAdd(item.taskType);
setOpen(false);
}}
>
{item.displayName}
</button>
))}
</div>
)}
</div>
);
};
export default AddTaskButton;

View File

@ -0,0 +1,36 @@
import React from "react";
import { TaskDefinition } from "../services/WorkflowTemplateService";
import AddTaskButton from "./AddTaskButton";
interface TaskListProps {
tasks: TaskDefinition[];
taskType: string;
onChange: (tasks: TaskDefinition[]) => void;
}
const TaskList: React.FC<TaskListProps> = ({ tasks, taskType, onChange }) => {
const handleAddTask = (selectedType: string) => {
const newTask: TaskDefinition = {
type: selectedType,
// Fill in any other required fields with defaults
// e.g. name: "", config: {}, etc.
};
console.log("Add Task clicked");
onChange([...tasks, newTask]);
};
return (
<div>
<AddTaskButton taskType={taskType} onAdd={handleAddTask} />
<ul>
{tasks.map((task, index) => (
<li key={index}>{task.type}</li>
))}
</ul>
</div>
);
};
export default TaskList;

View File

@ -0,0 +1,38 @@
import {
CreateWorkflowTemplateVersion,
TaskDefinition,
} from "../services/WorkflowTemplateService";
import TaskList from "./TaskList";
interface TasksTabProps {
data: CreateWorkflowTemplateVersion;
errors: Record<string, string>;
isEditMode: boolean;
onTasksChange: (name: string, value: TaskDefinition[]) => void;
}
const TasksTab: React.FC<TasksTabProps> = ({
data,
errors,
isEditMode,
onTasksChange,
}) => {
const tasks = data.tasks ?? [];
const handleTasksChange = (newTasks: TaskDefinition[]) => {
// Update the parent form state
onTasksChange("tasks", newTasks);
};
return (
<div>
<TaskList
tasks={tasks}
taskType="GeneralTask"
onChange={handleTasksChange}
/>
</div>
);
};
export default TasksTab;

View File

@ -6,6 +6,7 @@ import {
MakeGeneralIdRefParams,
} from "../../../../utils/GeneralIdRef";
import MapToJson from "../../../../utils/MapToJson";
import { CustomFieldValue } from "../../glossary/services/glossaryService";
const apiEndpoint = "/WorkflowTemplate";
@ -27,13 +28,24 @@ export type ReadWorkflowTemplateVersion = {
description: string;
};
export interface TaskDefinition<TConfig = Record<string, unknown>> {
type: string;
config?: TConfig;
}
export interface CreateWorkflowTemplateVersion extends FormData {
name: string;
domainId: GeneralIdRef;
domainId: CustomFieldValue[];
activityNameTemplate: string;
description: string;
//Tasks //Need to get this working when I do the tasks tab
tasks: TaskDefinition[];
}
export interface TaskMetadata {
taskType: string;
displayName: string;
capabilities: string[];
}
export async function getTemplates(
@ -130,6 +142,32 @@ export async function deleteTemplateVersion(
});
}
const taskMetadataCache: Record<string, TaskMetadata[]> = {};
export async function getTaskMetadata(
taskType: string,
): Promise<TaskMetadata[]> {
// Normalize the key (case-insensitive)
const key = taskType.trim().toLowerCase();
// Return cached result if available
if (taskMetadataCache[key]) {
return taskMetadataCache[key];
}
// Otherwise fetch from API
const response = await httpService.get<TaskMetadata[]>(
`${apiEndpoint}/taskMetadata?taskType=${taskType}`,
);
const data = response.data;
// Cache it
taskMetadataCache[key] = data;
return data;
}
const templateVersionsService = {
getTemplates,
getTemplateVersions,
@ -137,6 +175,7 @@ const templateVersionsService = {
postTemplateVersion,
putTemplateVersion,
deleteTemplateVersion,
getTaskMetadata,
};
export default templateVersionsService;