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 "./horizionalTabs";
|
||||||
@import "./_expandableCell.scss";
|
@import "./_expandableCell.scss";
|
||||||
@import "./_errorLogs.scss";
|
@import "./_errorLogs.scss";
|
||||||
|
@import "./addTaskButton.scss";
|
||||||
|
|
||||||
//Changes needed to make MS Edge behave the same as other browsers
|
//Changes needed to make MS Edge behave the same as other browsers
|
||||||
input::-ms-reveal {
|
input::-ms-reveal {
|
||||||
|
|||||||
@ -6,50 +6,61 @@ interface HorizontalTabsProps {
|
|||||||
children: JSX.Element[];
|
children: JSX.Element[];
|
||||||
initialTab?: string;
|
initialTab?: string;
|
||||||
hashSegment?: number;
|
hashSegment?: number;
|
||||||
|
activeTab?: string;
|
||||||
|
onTabChange?: (tab: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
|
const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
|
||||||
children,
|
children,
|
||||||
initialTab,
|
initialTab,
|
||||||
hashSegment,
|
hashSegment,
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
}) => {
|
}) => {
|
||||||
const hashValue = useHashSegment(
|
const hashValue = useHashSegment(
|
||||||
hashSegment !== undefined ? hashSegment : -1,
|
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
|
// Set initial tab on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (children.length > 0) {
|
if (!isControlled && children.length > 0) {
|
||||||
// Only use hash if hashSegment was explicitly provided
|
|
||||||
const useHash = hashSegment !== undefined;
|
const useHash = hashSegment !== undefined;
|
||||||
|
|
||||||
// Validate that the hash matches one of our tab IDs
|
|
||||||
const hashMatchesTab =
|
const hashMatchesTab =
|
||||||
useHash &&
|
useHash &&
|
||||||
hashValue &&
|
hashValue &&
|
||||||
children.some((child) => child.props.id === 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 firstTabId = children[0].props.id || children[0].props.label;
|
||||||
|
|
||||||
const tabToSelect =
|
const tabToSelect =
|
||||||
(hashMatchesTab ? hashValue : initialTab) || firstTabId;
|
(hashMatchesTab ? hashValue : initialTab) || firstTabId;
|
||||||
setActiveTab(tabToSelect);
|
|
||||||
}
|
|
||||||
}, [children, initialTab, hashValue, hashSegment]);
|
|
||||||
|
|
||||||
const onClickTabItem = useCallback((tab: string) => {
|
setInternalActiveTab(tabToSelect);
|
||||||
setActiveTab((prev) => (prev !== tab ? tab : prev));
|
}
|
||||||
}, []);
|
}, [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 activeTabChildren = useMemo(() => {
|
||||||
const match = children.find((child) => {
|
const match = children.find((child) => {
|
||||||
const tabId = child.props.id || child.props.label;
|
const tabId = child.props.id || child.props.label;
|
||||||
return tabId === activeTab;
|
return tabId === currentTab;
|
||||||
});
|
});
|
||||||
return match ? match.props.children : <></>;
|
return match ? match.props.children : <></>;
|
||||||
}, [children, activeTab]);
|
}, [children, currentTab]);
|
||||||
|
|
||||||
// If only one tab, just render its content
|
// If only one tab, just render its content
|
||||||
if (children.length === 1) {
|
if (children.length === 1) {
|
||||||
@ -66,7 +77,7 @@ const HorizontalTabs: React.FC<HorizontalTabsProps> = ({
|
|||||||
<TabHeader
|
<TabHeader
|
||||||
key={label}
|
key={label}
|
||||||
label={label}
|
label={label}
|
||||||
isActive={tabId === activeTab}
|
isActive={tabId === currentTab}
|
||||||
onClick={() => onClickTabItem(tabId)}
|
onClick={() => onClickTabItem(tabId)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -553,6 +553,17 @@ export const useForm = (initialState: FormState): UseFormReturn => {
|
|||||||
[state.data, validate, setState],
|
[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 = {
|
const api: any = {
|
||||||
state,
|
state,
|
||||||
schema: schemaRef.current,
|
schema: schemaRef.current,
|
||||||
@ -575,6 +586,7 @@ export const useForm = (initialState: FormState): UseFormReturn => {
|
|||||||
handleUserPickerChange,
|
handleUserPickerChange,
|
||||||
handleSsoProviderPickerChange,
|
handleSsoProviderPickerChange,
|
||||||
handleToggleChange,
|
handleToggleChange,
|
||||||
|
handleTasksChange,
|
||||||
setState,
|
setState,
|
||||||
};
|
};
|
||||||
Object.defineProperty(api, "schema", {
|
Object.defineProperty(api, "schema", {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import GeneralTab from "./components/GeneralTab";
|
|||||||
import { Navigate, useParams } from "react-router-dom";
|
import { Navigate, useParams } from "react-router-dom";
|
||||||
import templateVersionsService, {
|
import templateVersionsService, {
|
||||||
CreateWorkflowTemplateVersion,
|
CreateWorkflowTemplateVersion,
|
||||||
|
TaskDefinition,
|
||||||
} from "./services/WorkflowTemplateService";
|
} from "./services/WorkflowTemplateService";
|
||||||
import Loading from "../../../components/common/Loading";
|
import Loading from "../../../components/common/Loading";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@ -19,12 +20,15 @@ import { useForm } from "../../../components/common/useForm";
|
|||||||
import ErrorBlock from "../../../components/common/ErrorBlock";
|
import ErrorBlock from "../../../components/common/ErrorBlock";
|
||||||
import authentication from "../../frame/services/authenticationService";
|
import authentication from "../../frame/services/authenticationService";
|
||||||
import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef";
|
import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef";
|
||||||
|
import { CustomFieldValue } from "../glossary/services/glossaryService";
|
||||||
|
import TasksTab from "./components/TasksTab";
|
||||||
|
|
||||||
const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
||||||
editMode,
|
editMode,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
const { userId } = useParams<{ userId: string }>();
|
const { userId } = useParams<{ userId: string }>();
|
||||||
|
const [activeTab, setActiveTab] = React.useState("general");
|
||||||
|
|
||||||
// useForm promoted to the parent
|
// useForm promoted to the parent
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -34,6 +38,7 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
|||||||
domainId: [] as CustomFieldValue[],
|
domainId: [] as CustomFieldValue[],
|
||||||
activityNameTemplate: "",
|
activityNameTemplate: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
tasks: [] as TaskDefinition[],
|
||||||
} as CreateWorkflowTemplateVersion,
|
} as CreateWorkflowTemplateVersion,
|
||||||
errors: {},
|
errors: {},
|
||||||
redirect: "",
|
redirect: "",
|
||||||
@ -52,7 +57,7 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
|||||||
.required()
|
.required()
|
||||||
.max(450)
|
.max(450)
|
||||||
.label(t("ActivityNameTemplate")),
|
.label(t("ActivityNameTemplate")),
|
||||||
description: Joi.string().required().label(t("Description")),
|
description: Joi.string().required().allow("").label(t("Description")),
|
||||||
domainId: Joi.required(),
|
domainId: Joi.required(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,10 +102,10 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
|||||||
const { name } = form.state.data;
|
const { name } = form.state.data;
|
||||||
|
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
await templateVersionsService.putTemplateVersion({ name });
|
//await templateVersionsService.putTemplateVersion({ name });
|
||||||
toast.info(t("WorkflowTemplateEdited"));
|
toast.info(t("WorkflowTemplateEdited"));
|
||||||
} else {
|
} else {
|
||||||
await templateVersionsService.postTemplateVersion({ name });
|
//await templateVersionsService.postTemplateVersion({ name });
|
||||||
toast.info(t("WorkflowTemplateAdded"));
|
toast.info(t("WorkflowTemplateAdded"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +139,12 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
|||||||
</Tab>,
|
</Tab>,
|
||||||
|
|
||||||
<Tab key="tasks" id="tasks" label={t("Tasks")}>
|
<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>,
|
||||||
|
|
||||||
<Tab key="fields" id="fields" label={t("Fields")}>
|
<Tab key="fields" id="fields" label={t("Fields")}>
|
||||||
@ -163,7 +173,13 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
|
|||||||
|
|
||||||
{renderError("_general", errors)}
|
{renderError("_general", errors)}
|
||||||
|
|
||||||
<HorizontalTabs hashSegment={0}>{tabs}</HorizontalTabs>
|
<HorizontalTabs
|
||||||
|
hashSegment={0}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
>
|
||||||
|
{tabs}
|
||||||
|
</HorizontalTabs>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Loading>
|
</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,
|
MakeGeneralIdRefParams,
|
||||||
} from "../../../../utils/GeneralIdRef";
|
} from "../../../../utils/GeneralIdRef";
|
||||||
import MapToJson from "../../../../utils/MapToJson";
|
import MapToJson from "../../../../utils/MapToJson";
|
||||||
|
import { CustomFieldValue } from "../../glossary/services/glossaryService";
|
||||||
|
|
||||||
const apiEndpoint = "/WorkflowTemplate";
|
const apiEndpoint = "/WorkflowTemplate";
|
||||||
|
|
||||||
@ -27,13 +28,24 @@ export type ReadWorkflowTemplateVersion = {
|
|||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface TaskDefinition<TConfig = Record<string, unknown>> {
|
||||||
|
type: string;
|
||||||
|
config?: TConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateWorkflowTemplateVersion extends FormData {
|
export interface CreateWorkflowTemplateVersion extends FormData {
|
||||||
name: string;
|
name: string;
|
||||||
domainId: GeneralIdRef;
|
domainId: CustomFieldValue[];
|
||||||
activityNameTemplate: string;
|
activityNameTemplate: string;
|
||||||
description: 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(
|
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 = {
|
const templateVersionsService = {
|
||||||
getTemplates,
|
getTemplates,
|
||||||
getTemplateVersions,
|
getTemplateVersions,
|
||||||
@ -137,6 +175,7 @@ const templateVersionsService = {
|
|||||||
postTemplateVersion,
|
postTemplateVersion,
|
||||||
putTemplateVersion,
|
putTemplateVersion,
|
||||||
deleteTemplateVersion,
|
deleteTemplateVersion,
|
||||||
|
getTaskMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default templateVersionsService;
|
export default templateVersionsService;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user