From 083d79caaa068e69986a6d63b21c069d187d0f25 Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Tue, 24 Feb 2026 20:56:18 +0000 Subject: [PATCH] Implemented most of the Budget capability editor Can now delete a task from the tasks list. --- public/locales/en/budgetOption.json | 5 ++ public/locales/en/priority.json | 5 ++ src/components/pickers/BudgetOptionPicker.tsx | 61 +++++++++++++ src/components/pickers/PriorityPicker.tsx | 61 +++++++++++++ src/i18n/i18n.ts | 7 +- .../BudgetEditorRegistryEntry.tsx | 87 +++++++++++++++++++ .../OutcomeOfApprovalVerdictRegistryEntry.tsx | 1 + .../workflowTemplates/components/TaskList.tsx | 28 +++--- .../components/TasksEditor.tsx | 13 ++- .../workflowTemplates/components/TasksTab.tsx | 21 +++++ .../components/capabilityEditorRegistry.ts | 2 + 11 files changed, 276 insertions(+), 15 deletions(-) create mode 100644 public/locales/en/budgetOption.json create mode 100644 public/locales/en/priority.json create mode 100644 src/components/pickers/BudgetOptionPicker.tsx create mode 100644 src/components/pickers/PriorityPicker.tsx create mode 100644 src/modules/manager/workflowTemplates/components/CapabilityEditors/BudgetEditorRegistryEntry.tsx diff --git a/public/locales/en/budgetOption.json b/public/locales/en/budgetOption.json new file mode 100644 index 0000000..343e6e4 --- /dev/null +++ b/public/locales/en/budgetOption.json @@ -0,0 +1,5 @@ +{ + "DoNotShow": "Do Not Show", + "ShowOnly": "Show Only", + "ShowAndEdit": "Show And Edit" +} diff --git a/public/locales/en/priority.json b/public/locales/en/priority.json new file mode 100644 index 0000000..b73bf9b --- /dev/null +++ b/public/locales/en/priority.json @@ -0,0 +1,5 @@ +{ + "Low": "Low", + "Normal": "Normal", + "High": "High" +} diff --git a/src/components/pickers/BudgetOptionPicker.tsx b/src/components/pickers/BudgetOptionPicker.tsx new file mode 100644 index 0000000..4d0b96b --- /dev/null +++ b/src/components/pickers/BudgetOptionPicker.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState, useCallback } from "react"; +import Select from "../common/Select"; +import Option from "../common/option"; +import { useTranslation } from "react-i18next"; +import { Namespaces } from "../../i18n/i18n"; + +interface BudgetOptionPickerProps { + name: string; + label: string; + error?: string; + value: string; + onChange?: (name: string, value: string) => void; + includeLabel?: boolean; +} +export default function BudgetOptionPicker({ + name, + label, + error, + value, + onChange, + includeLabel = true, +}: BudgetOptionPickerProps) { + const [options, setOptions] = useState(undefined); + const { t } = useTranslation(Namespaces.BudgetOption); + + useEffect(() => { + async function load() { + const opts: Option[] = [ + { _id: "DoNotShow", name: t("DoNotShow") }, + { _id: "ShowOnly", name: t("ShowOnly") }, + { _id: "ShowAndEdit", name: t("ShowAndEdit") }, + ]; + + setOptions(opts); + } + + load(); + }, [t]); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const input = e.currentTarget; + + if (onChange) onChange(input.name, input.value); + }, + [onChange], + ); + + return ( + + ); +} diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index a366962..f97607f 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -4,13 +4,16 @@ import { initReactI18next } from "react-i18next"; import HttpBackend from "i18next-http-backend"; import { determineInitialLocale } from "../modules/frame/services/lanugageService"; import { fallbackLng } from "./generatedLocales"; +import { PrintSpecificationsGlossary } from "../modules/manager/glossary/services/glossaryService"; export const Namespaces = { + BudgetOption: "budgetOption", Common: "common", - MailTypes: "mailTypes", HtmlIsland: "htmlIsland", - TaskTypes: "taskTypes", + MailTypes: "mailTypes", + Priority: "priority", Raci: "raci", + TaskTypes: "taskTypes", Verdict: "verdict", } as const; diff --git a/src/modules/manager/workflowTemplates/components/CapabilityEditors/BudgetEditorRegistryEntry.tsx b/src/modules/manager/workflowTemplates/components/CapabilityEditors/BudgetEditorRegistryEntry.tsx new file mode 100644 index 0000000..9be9920 --- /dev/null +++ b/src/modules/manager/workflowTemplates/components/CapabilityEditors/BudgetEditorRegistryEntry.tsx @@ -0,0 +1,87 @@ +import { InputType } from "../../../../../components/common/Input"; +import BudgetOptionPicker from "../../../../../components/pickers/BudgetOptionPicker"; +import PriorityPicker from "../../../../../components/pickers/PriorityPicker"; +import { TaskDefinition } from "../../services/WorkflowTemplateService"; +import { renderTaskField } from "../taskEditorHelpers"; +import { + CapabilityEditorProps, + capabilityEditorRegistryEntry, + defaultsContext, +} from "../useCapabilityDefaults"; + +export const BudgetEditor: React.FC = (props) => { + const { task, onChange, fieldErrors } = props; + + function updateField(fieldname: string, val: string) { + const clone = structuredClone(task); + clone.config[fieldname] = val; + onChange(clone); + } + + return ( + <> + {renderTaskField( + task, + onChange, + "unitsOfWork", + "Units Of Work", + InputType.number, + fieldErrors, + )} +
Cost goes here
+ + updateField("budgetOption", val) + } + /> + {renderTaskField( + task, + onChange, + "allowTimeTracking", + "Allow Time Tracking", + InputType.checkbox, + fieldErrors, + )} + updateField("priority", val)} + /> + + ); +}; + +const runValidation = ( + task: TaskDefinition, + tasks: TaskDefinition[], +): Record => { + const errors: Record = {}; + + return errors; +}; + +export function defaultsAssignment( + task: TaskDefinition, + tasks: TaskDefinition[], + ctx: defaultsContext, +) { + task.config.unitsOfWork = 1; + task.config.cost = { amount: 0, isoCurrencySymbol: "GBP" }; //todo remove this hard coded currency and make it user defined at some point + task.config.budgetOption = "DoNotShow"; + task.config.allowTimeTracking = false; + task.config.priority = "Normal"; +} + +export const budgetEditorRegistryEntry: capabilityEditorRegistryEntry = { + Editor: BudgetEditor, + DefaultsAssignment: defaultsAssignment, + ValidationRunner: runValidation, +}; diff --git a/src/modules/manager/workflowTemplates/components/CapabilityEditors/OutcomeOfApprovalVerdictRegistryEntry.tsx b/src/modules/manager/workflowTemplates/components/CapabilityEditors/OutcomeOfApprovalVerdictRegistryEntry.tsx index 5232f9d..02639da 100644 --- a/src/modules/manager/workflowTemplates/components/CapabilityEditors/OutcomeOfApprovalVerdictRegistryEntry.tsx +++ b/src/modules/manager/workflowTemplates/components/CapabilityEditors/OutcomeOfApprovalVerdictRegistryEntry.tsx @@ -198,6 +198,7 @@ export function defaultsAssignment( tasks: TaskDefinition[], ctx: defaultsContext, ) { + task.config.outcomeActions = [] as IOutcomeAction[]; task.config.overrideDefaultTaskProgression = true; } diff --git a/src/modules/manager/workflowTemplates/components/TaskList.tsx b/src/modules/manager/workflowTemplates/components/TaskList.tsx index d3d3f90..bde3ba3 100644 --- a/src/modules/manager/workflowTemplates/components/TaskList.tsx +++ b/src/modules/manager/workflowTemplates/components/TaskList.tsx @@ -5,8 +5,6 @@ import { } from "../services/WorkflowTemplateService"; import AddTaskButton from "./AddTaskButton"; import { SelectableList } from "../../../../components/common/SelectableList"; -import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { sortTasksTopologically } from "./workflowGraphUtils"; import { useCapabilityDefaults, validateTask } from "./useCapabilityDefaults"; import ValidationErrorIcon from "../../../../components/validationErrorIcon"; @@ -67,17 +65,23 @@ const TaskList: React.FC = ({ ( - <> - {x.config.name as string} + renderLabel={(x) => { + if (x) { + return ( + <> + {x.config.name as string} - { - - } - - )} + { + + } + + ); + } else return <>; + }} onSelect={(item) => onSelectTask(item)} /> diff --git a/src/modules/manager/workflowTemplates/components/TasksEditor.tsx b/src/modules/manager/workflowTemplates/components/TasksEditor.tsx index 2badfba..099328c 100644 --- a/src/modules/manager/workflowTemplates/components/TasksEditor.tsx +++ b/src/modules/manager/workflowTemplates/components/TasksEditor.tsx @@ -5,6 +5,8 @@ import { } from "../services/WorkflowTemplateService"; import { capabilityEditorRegistry } from "./capabilityEditorRegistry"; import { validateTask } from "./useCapabilityDefaults"; +import Button, { ButtonType } from "../../../../components/common/Button"; +import ConfirmButton from "../../../../components/common/ConfirmButton"; interface TaskEditorProps { task: TaskDefinition; @@ -12,6 +14,7 @@ interface TaskEditorProps { tasksMetadata: TaskMetadata[]; onChange: (updatedTask: TaskDefinition) => void; onValidate: (taskId: string, isValid: boolean) => void; + onDelete: (taskId: string) => void; } const TaskEditorComponent: React.FC = ({ @@ -20,6 +23,7 @@ const TaskEditorComponent: React.FC = ({ tasksMetadata, onChange, onValidate, + onDelete, }) => { const [fieldErrors, setFieldErrors] = React.useState>( {}, @@ -91,6 +95,12 @@ const TaskEditorComponent: React.FC = ({ /> ); })} + onDelete(task.config.guid as string)} + > + Delete {task.config.name || "Task"} + ); }; @@ -109,7 +119,8 @@ export const TaskEditor = React.memo( taskIdChanged || metaChanged || onChangeChanged || - onValidateChanged + onValidateChanged || + prevProps.onDelete !== nextProps.onDelete ); }, ); diff --git a/src/modules/manager/workflowTemplates/components/TasksTab.tsx b/src/modules/manager/workflowTemplates/components/TasksTab.tsx index 0e81959..8664cd9 100644 --- a/src/modules/manager/workflowTemplates/components/TasksTab.tsx +++ b/src/modules/manager/workflowTemplates/components/TasksTab.tsx @@ -86,6 +86,26 @@ const TasksTab: React.FC = ({ [tasks, handleTasksChange], ); + const handleTaskDelete = React.useCallback( + (taskId: string) => { + const newTasks = tasks.filter((t) => t.config.guid !== taskId); + + for (const t of newTasks) { + // If any task has a dependency on the deleted task, remove that dependency + if (t.config.predecessors) { + t.config.predecessors = (t.config.predecessors as string[]).filter( + (d) => d !== taskId, + ); + } + } + + onValidate(taskId, true); // Clear validation state for deleted task + setSelectedTask(null); + handleTasksChange(newTasks); + }, + [tasks, handleTasksChange], + ); + return (
@@ -107,6 +127,7 @@ const TasksTab: React.FC = ({ tasks={tasks} onChange={handleTaskEditorChange} onValidate={onValidate} + onDelete={handleTaskDelete} />
)} diff --git a/src/modules/manager/workflowTemplates/components/capabilityEditorRegistry.ts b/src/modules/manager/workflowTemplates/components/capabilityEditorRegistry.ts index 7b2fd08..a79c93f 100644 --- a/src/modules/manager/workflowTemplates/components/capabilityEditorRegistry.ts +++ b/src/modules/manager/workflowTemplates/components/capabilityEditorRegistry.ts @@ -3,6 +3,7 @@ import { assigneesOfITaskAssigneeRegistryEntry } from "./CapabilityEditors/Assig import { taskCoreEditorRegistryEntry } from "./CapabilityEditors/TaskCoreEditor"; import { capabilityEditorRegistryEntry } from "./useCapabilityDefaults"; import { outcomeOfApprovalVerdictRegistryEntry } from "./CapabilityEditors/OutcomeOfApprovalVerdictRegistryEntry"; +import { budgetEditorRegistryEntry } from "./CapabilityEditors/BudgetEditorRegistryEntry"; export const capabilityEditorRegistry: Record< string, @@ -10,6 +11,7 @@ export const capabilityEditorRegistry: Record< > = { ITask: taskCoreEditorRegistryEntry, ITags: tagsEditorRegistryEntry, + IBudget: budgetEditorRegistryEntry, "IAssignees": assigneesOfITaskAssigneeRegistryEntry, "IOutcome": outcomeOfApprovalVerdictRegistryEntry, };