Implemented most of the Budget capability editor
Can now delete a task from the tasks list.
This commit is contained in:
parent
6734e9b83a
commit
083d79caaa
5
public/locales/en/budgetOption.json
Normal file
5
public/locales/en/budgetOption.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"DoNotShow": "Do Not Show",
|
||||
"ShowOnly": "Show Only",
|
||||
"ShowAndEdit": "Show And Edit"
|
||||
}
|
||||
5
public/locales/en/priority.json
Normal file
5
public/locales/en/priority.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"Low": "Low",
|
||||
"Normal": "Normal",
|
||||
"High": "High"
|
||||
}
|
||||
61
src/components/pickers/BudgetOptionPicker.tsx
Normal file
61
src/components/pickers/BudgetOptionPicker.tsx
Normal file
@ -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<Option[] | undefined>(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<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
if (onChange) onChange(input.name, input.value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
value={value}
|
||||
options={options}
|
||||
includeBlankFirstEntry={false}
|
||||
onChange={handleChange}
|
||||
includeLabel={includeLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
61
src/components/pickers/PriorityPicker.tsx
Normal file
61
src/components/pickers/PriorityPicker.tsx
Normal file
@ -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 PriorityPickerProps {
|
||||
name: string;
|
||||
label: string;
|
||||
error?: string;
|
||||
value: string;
|
||||
onChange?: (name: string, value: string) => void;
|
||||
includeLabel?: boolean;
|
||||
}
|
||||
export default function PriorityPicker({
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
value,
|
||||
onChange,
|
||||
includeLabel = true,
|
||||
}: PriorityPickerProps) {
|
||||
const [options, setOptions] = useState<Option[] | undefined>(undefined);
|
||||
const { t } = useTranslation(Namespaces.Priority);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const opts: Option[] = [
|
||||
{ _id: "Low", name: t("Low") },
|
||||
{ _id: "Normal", name: t("Normal") },
|
||||
{ _id: "High", name: t("High") },
|
||||
];
|
||||
|
||||
setOptions(opts);
|
||||
}
|
||||
|
||||
load();
|
||||
}, [t]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
if (onChange) onChange(input.name, input.value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
value={value}
|
||||
options={options}
|
||||
includeBlankFirstEntry={false}
|
||||
onChange={handleChange}
|
||||
includeLabel={includeLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<CapabilityEditorProps> = (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,
|
||||
)}
|
||||
<div>Cost goes here</div>
|
||||
<BudgetOptionPicker
|
||||
includeLabel={true}
|
||||
name="budgetOption"
|
||||
label="Budget Option"
|
||||
value={task.config.budgetOption as string}
|
||||
error={fieldErrors["budgetOption"]}
|
||||
onChange={(name: string, val: string) =>
|
||||
updateField("budgetOption", val)
|
||||
}
|
||||
/>
|
||||
{renderTaskField(
|
||||
task,
|
||||
onChange,
|
||||
"allowTimeTracking",
|
||||
"Allow Time Tracking",
|
||||
InputType.checkbox,
|
||||
fieldErrors,
|
||||
)}
|
||||
<PriorityPicker
|
||||
includeLabel={true}
|
||||
name="priority"
|
||||
label="Priority"
|
||||
value={task.config.priority as string}
|
||||
error={fieldErrors["priority"]}
|
||||
onChange={(name: string, val: string) => updateField("priority", val)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const runValidation = (
|
||||
task: TaskDefinition,
|
||||
tasks: TaskDefinition[],
|
||||
): Record<string, string> => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
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,
|
||||
};
|
||||
@ -198,6 +198,7 @@ export function defaultsAssignment(
|
||||
tasks: TaskDefinition[],
|
||||
ctx: defaultsContext,
|
||||
) {
|
||||
task.config.outcomeActions = [] as IOutcomeAction[];
|
||||
task.config.overrideDefaultTaskProgression = true;
|
||||
}
|
||||
|
||||
|
||||
@ -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<TaskListProps> = ({
|
||||
<SelectableList
|
||||
items={sortedTasks}
|
||||
selectedValue={selectedTask}
|
||||
renderLabel={(x) => (
|
||||
renderLabel={(x) => {
|
||||
if (x) {
|
||||
return (
|
||||
<>
|
||||
{x.config.name as string}
|
||||
|
||||
{
|
||||
<ValidationErrorIcon
|
||||
visible={validTasksList[x.config.guid as string] === false}
|
||||
visible={
|
||||
validTasksList[x.config.guid as string] === false
|
||||
}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
);
|
||||
} else return <></>;
|
||||
}}
|
||||
onSelect={(item) => onSelectTask(item)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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<TaskEditorProps> = ({
|
||||
@ -20,6 +23,7 @@ const TaskEditorComponent: React.FC<TaskEditorProps> = ({
|
||||
tasksMetadata,
|
||||
onChange,
|
||||
onValidate,
|
||||
onDelete,
|
||||
}) => {
|
||||
const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>(
|
||||
{},
|
||||
@ -91,6 +95,12 @@ const TaskEditorComponent: React.FC<TaskEditorProps> = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ConfirmButton
|
||||
buttonType={ButtonType.primary}
|
||||
onClick={() => onDelete(task.config.guid as string)}
|
||||
>
|
||||
Delete {task.config.name || "Task"}
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -109,7 +119,8 @@ export const TaskEditor = React.memo(
|
||||
taskIdChanged ||
|
||||
metaChanged ||
|
||||
onChangeChanged ||
|
||||
onValidateChanged
|
||||
onValidateChanged ||
|
||||
prevProps.onDelete !== nextProps.onDelete
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -86,6 +86,26 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
[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 (
|
||||
<div className="two-column-grid no-scroll">
|
||||
<div className="fit-content-width`">
|
||||
@ -107,6 +127,7 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
||||
tasks={tasks}
|
||||
onChange={handleTaskEditorChange}
|
||||
onValidate={onValidate}
|
||||
onDelete={handleTaskDelete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -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<ITaskAssignee>": assigneesOfITaskAssigneeRegistryEntry,
|
||||
"IOutcome<ApprovalVerdict>": outcomeOfApprovalVerdictRegistryEntry,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user