Started working on the Tasks tab
This commit is contained in:
parent
854ba4bf1a
commit
6e939fae6e
13
src/Sass/addTaskButton.scss
Normal file
13
src/Sass/addTaskButton.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user