From 6e939fae6ea11c32479de4f42e31311eb95416a6 Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Thu, 12 Feb 2026 00:40:37 +0000 Subject: [PATCH] Started working on the Tasks tab --- src/Sass/addTaskButton.scss | 13 ++++ src/Sass/global.scss | 1 + src/components/common/HorizionalTabs.tsx | 41 ++++++++----- src/components/common/useForm.ts | 12 ++++ .../WorkflowTemplateDetails.tsx | 26 ++++++-- .../components/AddTaskButton.tsx | 61 +++++++++++++++++++ .../workflowTemplates/components/TaskList.tsx | 36 +++++++++++ .../workflowTemplates/components/TasksTab.tsx | 38 ++++++++++++ .../services/WorkflowTemplateService.ts | 43 ++++++++++++- 9 files changed, 249 insertions(+), 22 deletions(-) create mode 100644 src/Sass/addTaskButton.scss create mode 100644 src/modules/manager/workflowTemplates/components/AddTaskButton.tsx create mode 100644 src/modules/manager/workflowTemplates/components/TaskList.tsx create mode 100644 src/modules/manager/workflowTemplates/components/TasksTab.tsx diff --git a/src/Sass/addTaskButton.scss b/src/Sass/addTaskButton.scss new file mode 100644 index 0000000..de64948 --- /dev/null +++ b/src/Sass/addTaskButton.scss @@ -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; + } +} diff --git a/src/Sass/global.scss b/src/Sass/global.scss index 443af00..c681843 100644 --- a/src/Sass/global.scss +++ b/src/Sass/global.scss @@ -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 { diff --git a/src/components/common/HorizionalTabs.tsx b/src/components/common/HorizionalTabs.tsx index 50a2b1f..8add5e0 100644 --- a/src/components/common/HorizionalTabs.tsx +++ b/src/components/common/HorizionalTabs.tsx @@ -6,50 +6,61 @@ interface HorizontalTabsProps { children: JSX.Element[]; initialTab?: string; hashSegment?: number; + activeTab?: string; + onTabChange?: (tab: string) => void; } const HorizontalTabs: React.FC = ({ children, initialTab, hashSegment, + activeTab, + onTabChange, }) => { const hashValue = useHashSegment( hashSegment !== undefined ? hashSegment : -1, ); - const [activeTab, setActiveTab] = useState(""); + const [internalActiveTab, setInternalActiveTab] = useState(""); + + 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 = ({ onClickTabItem(tabId)} /> ); diff --git a/src/components/common/useForm.ts b/src/components/common/useForm.ts index 33799c7..7a89d45 100644 --- a/src/components/common/useForm.ts +++ b/src/components/common/useForm.ts @@ -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", { diff --git a/src/modules/manager/workflowTemplates/WorkflowTemplateDetails.tsx b/src/modules/manager/workflowTemplates/WorkflowTemplateDetails.tsx index 4b2f0dd..4eef8a3 100644 --- a/src/modules/manager/workflowTemplates/WorkflowTemplateDetails.tsx +++ b/src/modules/manager/workflowTemplates/WorkflowTemplateDetails.tsx @@ -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(); 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 }> = ({ , -
Tasks editor coming soon…
+
, @@ -163,7 +173,13 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({ {renderError("_general", errors)} - {tabs} + + {tabs} + diff --git a/src/modules/manager/workflowTemplates/components/AddTaskButton.tsx b/src/modules/manager/workflowTemplates/components/AddTaskButton.tsx new file mode 100644 index 0000000..eaf94ae --- /dev/null +++ b/src/modules/manager/workflowTemplates/components/AddTaskButton.tsx @@ -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 = ({ taskType, onAdd }) => { + const [open, setOpen] = React.useState(false); + const [items, setItems] = React.useState([]); + 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 ( +
+ + + {open && ( +
+ {loading &&
Loading…
} + + {!loading && + items.map((item) => ( + + ))} +
+ )} +
+ ); +}; + +export default AddTaskButton; diff --git a/src/modules/manager/workflowTemplates/components/TaskList.tsx b/src/modules/manager/workflowTemplates/components/TaskList.tsx new file mode 100644 index 0000000..16736a0 --- /dev/null +++ b/src/modules/manager/workflowTemplates/components/TaskList.tsx @@ -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 = ({ 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 ( +
+ + +
    + {tasks.map((task, index) => ( +
  • {task.type}
  • + ))} +
+
+ ); +}; + +export default TaskList; diff --git a/src/modules/manager/workflowTemplates/components/TasksTab.tsx b/src/modules/manager/workflowTemplates/components/TasksTab.tsx new file mode 100644 index 0000000..8840155 --- /dev/null +++ b/src/modules/manager/workflowTemplates/components/TasksTab.tsx @@ -0,0 +1,38 @@ +import { + CreateWorkflowTemplateVersion, + TaskDefinition, +} from "../services/WorkflowTemplateService"; +import TaskList from "./TaskList"; + +interface TasksTabProps { + data: CreateWorkflowTemplateVersion; + errors: Record; + isEditMode: boolean; + onTasksChange: (name: string, value: TaskDefinition[]) => void; +} + +const TasksTab: React.FC = ({ + data, + errors, + isEditMode, + onTasksChange, +}) => { + const tasks = data.tasks ?? []; + + const handleTasksChange = (newTasks: TaskDefinition[]) => { + // Update the parent form state + onTasksChange("tasks", newTasks); + }; + + return ( +
+ +
+ ); +}; + +export default TasksTab; diff --git a/src/modules/manager/workflowTemplates/services/WorkflowTemplateService.ts b/src/modules/manager/workflowTemplates/services/WorkflowTemplateService.ts index c897d9f..44c55f0 100644 --- a/src/modules/manager/workflowTemplates/services/WorkflowTemplateService.ts +++ b/src/modules/manager/workflowTemplates/services/WorkflowTemplateService.ts @@ -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> { + 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 = {}; + +export async function getTaskMetadata( + taskType: string, +): Promise { + // 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( + `${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;