Refactor task planner item panels
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
126c673a2d
commit
a796edcb02
3 changed files with 274 additions and 207 deletions
|
|
@ -161,6 +161,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
const calendarUtils = await readFile("website/src/features/toolboxes/modules/calendarUtils.js", "utf8");
|
||||
const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8");
|
||||
const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8");
|
||||
const taskPlannerItemPanels = await readFile("website/src/features/toolboxes/modules/TaskPlannerItemPanels.jsx", "utf8");
|
||||
const taskPlannerBundle = `${taskPlannerItems}\n${taskPlannerItemPanels}`;
|
||||
const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8");
|
||||
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8");
|
||||
const goalTreeParts = await readFile("website/src/features/toolboxes/modules/GoalTreeParts.jsx", "utf8");
|
||||
|
|
@ -365,11 +367,11 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(taskPlannerModule, /reorderFeatures/);
|
||||
assert.match(taskPlannerItems, /export function TaskPlannerBranch/);
|
||||
assert.match(taskPlannerItems, /function TaskPlannerItem/);
|
||||
assert.match(taskPlannerItems, /task-planner-warning/);
|
||||
assert.match(taskPlannerItems, /useInlineEdit/);
|
||||
assert.match(taskPlannerItems, /commitOnEnter: false/);
|
||||
assert.match(taskPlannerItems, /categoryLabel/);
|
||||
assert.match(taskPlannerItems, /prerequisite/);
|
||||
assert.match(taskPlannerBundle, /task-planner-warning/);
|
||||
assert.match(taskPlannerBundle, /useInlineEdit/);
|
||||
assert.match(taskPlannerBundle, /commitOnEnter: false/);
|
||||
assert.match(taskPlannerBundle, /categoryLabel/);
|
||||
assert.match(taskPlannerBundle, /prerequisite/);
|
||||
assert.match(taskPlannerUtils, /export function getTaskParentMap/);
|
||||
assert.match(taskPlannerUtils, /export function applyDueResets/);
|
||||
assert.doesNotMatch(taskPlannerModule, /canMoveItem:/);
|
||||
|
|
|
|||
246
website/src/features/toolboxes/modules/TaskPlannerItemPanels.jsx
Normal file
246
website/src/features/toolboxes/modules/TaskPlannerItemPanels.jsx
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
// Rôle : regroupe les panneaux secondaires d'une tâche du task planner.
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { InlineNotice } from "../../../components/AppOverlays.jsx";
|
||||
import { Tooltip } from "../../../components/Tooltip.jsx";
|
||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||
import {
|
||||
WEEK_DAYS,
|
||||
getEffectiveTaskCategory,
|
||||
getTaskById,
|
||||
getTaskTitle
|
||||
} from "./taskPlannerUtils.js";
|
||||
|
||||
export function MissingPrerequisitesTooltip({ missingPrerequisites, tasks, textContent }) {
|
||||
return (
|
||||
<Tooltip
|
||||
className="task-planner-warning"
|
||||
content={(
|
||||
<span className="task-planner-prerequisite-tooltip">
|
||||
<strong>{textContent.missingPrerequisiteTitle || "Pré requis non effectué"}</strong>
|
||||
<span>
|
||||
{missingPrerequisites.map((relation) => (
|
||||
<span key={relation.id}>{getTaskTitle(tasks, relation.toTaskId)}</span>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
position="top-start"
|
||||
>
|
||||
{textContent.missingPrerequisiteBadge || "Pré requis"}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function TaskPlannerItemDetails({
|
||||
task,
|
||||
tasks,
|
||||
data,
|
||||
parentMap,
|
||||
parentId,
|
||||
textContent,
|
||||
descriptionOpen,
|
||||
settingsOpen,
|
||||
noticeMessage,
|
||||
relations,
|
||||
relationTargets,
|
||||
onUpdateTask,
|
||||
onAddLinkTarget,
|
||||
onRemoveLinkTarget,
|
||||
onSetPrerequisite,
|
||||
onSetTaskCategory
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<InlineNotice className="task-planner-notice" message={noticeMessage} />
|
||||
{descriptionOpen && (
|
||||
<TaskDescriptionPanel
|
||||
task={task}
|
||||
tasks={tasks}
|
||||
parentMap={parentMap}
|
||||
parentId={parentId}
|
||||
textContent={textContent}
|
||||
onUpdateTask={onUpdateTask}
|
||||
onSetTaskCategory={onSetTaskCategory}
|
||||
/>
|
||||
)}
|
||||
{settingsOpen && (
|
||||
<div className="task-planner-task-settings">
|
||||
<TaskResetSettings task={task} data={data} textContent={textContent} onUpdateTask={onUpdateTask} />
|
||||
<LinksEditor
|
||||
label={textContent.linksLabel || "Parent"}
|
||||
relations={relations}
|
||||
task={task}
|
||||
tasks={tasks}
|
||||
parentMap={parentMap}
|
||||
relationTargets={relationTargets}
|
||||
emptyLabel={textContent.emptyLinks || "Aucun parent"}
|
||||
addLabel={textContent.addLinkLabel || "Définir un parent"}
|
||||
removeLabel={textContent.removeRelationTitle || "Retirer"}
|
||||
prerequisiteLabel={textContent.prerequisiteCheckboxLabel || "Pré requis"}
|
||||
onAddLinkTarget={onAddLinkTarget}
|
||||
onRemoveLinkTarget={onRemoveLinkTarget}
|
||||
onSetPrerequisite={onSetPrerequisite}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDescriptionPanel({ task, tasks, parentMap, parentId, textContent, onUpdateTask, onSetTaskCategory }) {
|
||||
const parentTask = parentId ? getTaskById(tasks, parentId) : null;
|
||||
const inheritedCategory = parentTask ? getEffectiveTaskCategory(parentTask, tasks, parentMap) : "";
|
||||
const categoryValue = parentTask ? inheritedCategory : task.category || "";
|
||||
const categoryEditable = !parentTask;
|
||||
const categoryEdit = useInlineEdit({
|
||||
value: categoryValue,
|
||||
onCommit: (category) => {
|
||||
if (categoryEditable) onSetTaskCategory(task.id, category);
|
||||
}
|
||||
});
|
||||
const descriptionEdit = useInlineEdit({
|
||||
value: task.description,
|
||||
onCommit: (description) => onUpdateTask(task.id, (current) => ({ ...current, description })),
|
||||
commitOnEnter: false
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="task-planner-description-panel">
|
||||
<label className={`task-planner-category-field ${categoryEditable ? "" : "is-disabled"}`}>
|
||||
<span>{textContent.categoryLabel || "Catégorie"}</span>
|
||||
<input
|
||||
{...categoryEdit.getInputProps({
|
||||
disabled: !categoryEditable,
|
||||
placeholder: textContent.categoryPlaceholder || "Nom de catégorie",
|
||||
"aria-label": `${textContent.categoryLabel || "Catégorie"} ${task.title}`
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label className="task-planner-description-field">
|
||||
<span>{textContent.descriptionLabel || "Description"}</span>
|
||||
<textarea
|
||||
{...descriptionEdit.getInputProps({
|
||||
className: "task-planner-description",
|
||||
placeholder: textContent.descriptionPlaceholder || "Description",
|
||||
"aria-label": `${textContent.descriptionLabel || "Description"} ${task.title}`
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskResetSettings({ task, data, textContent, onUpdateTask }) {
|
||||
if (task.type === "daily") {
|
||||
const hasOverride = Boolean(task.dailyResetTime);
|
||||
return (
|
||||
<div className="task-planner-reset-override">
|
||||
<span>{textContent.taskDailyResetTimeLabel || "Heure quotidienne"}</span>
|
||||
<div className="task-planner-reset-control">
|
||||
<input
|
||||
type="time"
|
||||
value={task.dailyResetTime || data.resetTime}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => {
|
||||
const nextTask = { ...current };
|
||||
if (event.target.value && event.target.value !== data.resetTime) nextTask.dailyResetTime = event.target.value;
|
||||
else delete nextTask.dailyResetTime;
|
||||
return nextTask;
|
||||
})}
|
||||
aria-label={`${textContent.taskDailyResetTimeLabel || "Heure quotidienne"} ${task.title}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdateTask(task.id, (current) => {
|
||||
const nextTask = { ...current };
|
||||
delete nextTask.dailyResetTime;
|
||||
return nextTask;
|
||||
})}
|
||||
disabled={!hasOverride}
|
||||
title={textContent.inheritResetTime || "Utiliser l'heure globale"}
|
||||
aria-label={`${textContent.inheritResetTime || "Utiliser l'heure globale"} ${task.title}`}
|
||||
>
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (task.type === "weekly") {
|
||||
return (
|
||||
<label className="task-planner-reset-override">
|
||||
<span>{textContent.taskWeeklyResetDayLabel || "Jour hebdo"}</span>
|
||||
<select
|
||||
value={task.weeklyResetDay ?? ""}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => {
|
||||
const nextTask = { ...current };
|
||||
if (event.target.value === "") {
|
||||
delete nextTask.weeklyResetDay;
|
||||
} else {
|
||||
nextTask.weeklyResetDay = Number(event.target.value);
|
||||
}
|
||||
return nextTask;
|
||||
})}
|
||||
>
|
||||
<option value="">{textContent.inheritWeeklyResetDay || "Global"}</option>
|
||||
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-planner-reset-override is-empty">
|
||||
<span>{textContent.taskResetLabel || "Réinitialisation"}</span>
|
||||
<p>{textContent.noTaskResetLabel || "Aucun reset"}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets, emptyLabel, addLabel, removeLabel, prerequisiteLabel, onAddLinkTarget, onRemoveLinkTarget, onSetPrerequisite }) {
|
||||
const descendantIds = new Set();
|
||||
function collectDescendants(taskId) {
|
||||
tasks.filter((item) => parentMap.get(item.id) === taskId).forEach((child) => {
|
||||
descendantIds.add(child.id);
|
||||
collectDescendants(child.id);
|
||||
});
|
||||
}
|
||||
collectDescendants(task.id);
|
||||
const availableTargets = relationTargets.filter((target) => !descendantIds.has(target.id));
|
||||
|
||||
return (
|
||||
<div className="task-planner-relations">
|
||||
<span>{label}</span>
|
||||
<select
|
||||
value=""
|
||||
onChange={(event) => {
|
||||
onAddLinkTarget(task.id, event.target.value);
|
||||
event.target.value = "";
|
||||
}}
|
||||
aria-label={addLabel}
|
||||
>
|
||||
<option value="">{addLabel}</option>
|
||||
{availableTargets.map((target) => <option key={target.id} value={target.id}>{target.title}</option>)}
|
||||
</select>
|
||||
<div>
|
||||
{relations.length ? relations.map((relation) => (
|
||||
<div className="task-planner-relation-chip" key={relation.id}>
|
||||
<span>{getTaskTitle(tasks, relation.toTaskId)}</span>
|
||||
<label title={prerequisiteLabel}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={relation.prerequisite}
|
||||
onChange={(event) => onSetPrerequisite(task.id, relation.toTaskId, event.target.checked)}
|
||||
aria-label={`${prerequisiteLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}
|
||||
/>
|
||||
<span>{prerequisiteLabel}</span>
|
||||
</label>
|
||||
<button type="button" onClick={() => onRemoveLinkTarget(task.id, relation.toTaskId)} title={removeLabel} aria-label={`${removeLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}>
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
)) : <em>{emptyLabel}</em>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +1,13 @@
|
|||
// Rôle : rend les branches, items et panneaux secondaires du task planner.
|
||||
// Rôle : rend les branches et lignes de tâches du task planner.
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { InlineNotice } from "../../../components/AppOverlays.jsx";
|
||||
import { Tooltip } from "../../../components/Tooltip.jsx";
|
||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||
import { MissingPrerequisitesTooltip, TaskPlannerItemDetails } from "./TaskPlannerItemPanels.jsx";
|
||||
import {
|
||||
TASK_TYPES,
|
||||
WEEK_DAYS,
|
||||
countCompletedTaskTree,
|
||||
countTaskTree,
|
||||
getEffectiveTaskCategory,
|
||||
getTaskById,
|
||||
getTaskCategoryDataset,
|
||||
getTaskCategoryGroups,
|
||||
getTaskTitle,
|
||||
getTaskTypeLabel,
|
||||
getTasksForParent,
|
||||
isTaskResetSoon,
|
||||
|
|
@ -231,10 +226,6 @@ function TaskPlannerItem({
|
|||
onSetTaskCategory,
|
||||
children
|
||||
}) {
|
||||
const parentTask = parentId ? getTaskById(tasks, parentId) : null;
|
||||
const inheritedCategory = parentTask ? getEffectiveTaskCategory(parentTask, tasks, parentMap) : "";
|
||||
const categoryValue = parentTask ? inheritedCategory : task.category || "";
|
||||
const categoryEditable = !parentTask;
|
||||
const relations = data.relations.filter((relation) => relation.fromTaskId === task.id);
|
||||
const missingPrerequisites = relations.filter((relation) => relation.prerequisite && !tasks.find((item) => item.id === relation.toTaskId)?.checked);
|
||||
const relationTargets = tasks.filter((item) => item.id !== task.id && !relations.some((relation) => relation.toTaskId === item.id));
|
||||
|
|
@ -253,17 +244,6 @@ function TaskPlannerItem({
|
|||
transform: (draft) => String(draft || "").trim() || task.title,
|
||||
onCommit: (title) => onUpdateTask(task.id, (current) => ({ ...current, title }))
|
||||
});
|
||||
const categoryEdit = useInlineEdit({
|
||||
value: categoryValue,
|
||||
onCommit: (category) => {
|
||||
if (categoryEditable) onSetTaskCategory(task.id, category);
|
||||
}
|
||||
});
|
||||
const descriptionEdit = useInlineEdit({
|
||||
value: task.description,
|
||||
onCommit: (description) => onUpdateTask(task.id, (current) => ({ ...current, description })),
|
||||
commitOnEnter: false
|
||||
});
|
||||
|
||||
function updateChecked(event) {
|
||||
const checked = event.target.checked;
|
||||
|
|
@ -314,13 +294,7 @@ function TaskPlannerItem({
|
|||
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
|
||||
</select>
|
||||
{missingPrerequisites.length > 0 && (
|
||||
<Tooltip
|
||||
className="task-planner-warning"
|
||||
content={<MissingPrerequisitesTooltip missingPrerequisites={missingPrerequisites} tasks={tasks} textContent={textContent} />}
|
||||
position="top-start"
|
||||
>
|
||||
{textContent.missingPrerequisiteBadge || "Pré requis"}
|
||||
</Tooltip>
|
||||
<MissingPrerequisitesTooltip missingPrerequisites={missingPrerequisites} tasks={tasks} textContent={textContent} />
|
||||
)}
|
||||
<div className="task-planner-actions">
|
||||
<button
|
||||
|
|
@ -348,180 +322,25 @@ function TaskPlannerItem({
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<InlineNotice className="task-planner-notice" message={noticeMessage} />
|
||||
{descriptionOpen && (
|
||||
<div className="task-planner-description-panel">
|
||||
<label className={`task-planner-category-field ${categoryEditable ? "" : "is-disabled"}`}>
|
||||
<span>{textContent.categoryLabel || "Catégorie"}</span>
|
||||
<input
|
||||
{...categoryEdit.getInputProps({
|
||||
disabled: !categoryEditable,
|
||||
placeholder: textContent.categoryPlaceholder || "Nom de catégorie",
|
||||
"aria-label": `${textContent.categoryLabel || "Catégorie"} ${task.title}`
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label className="task-planner-description-field">
|
||||
<span>{textContent.descriptionLabel || "Description"}</span>
|
||||
<textarea
|
||||
{...descriptionEdit.getInputProps({
|
||||
className: "task-planner-description",
|
||||
placeholder: textContent.descriptionPlaceholder || "Description",
|
||||
"aria-label": `${textContent.descriptionLabel || "Description"} ${task.title}`
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{settingsOpen && (
|
||||
<div className="task-planner-task-settings">
|
||||
<TaskResetSettings task={task} data={data} textContent={textContent} onUpdateTask={onUpdateTask} />
|
||||
<LinksEditor
|
||||
label={textContent.linksLabel || "Parent"}
|
||||
relations={relations}
|
||||
<TaskPlannerItemDetails
|
||||
task={task}
|
||||
tasks={tasks}
|
||||
data={data}
|
||||
parentMap={parentMap}
|
||||
parentId={parentId}
|
||||
textContent={textContent}
|
||||
descriptionOpen={descriptionOpen}
|
||||
settingsOpen={settingsOpen}
|
||||
noticeMessage={noticeMessage}
|
||||
relations={relations}
|
||||
relationTargets={relationTargets}
|
||||
emptyLabel={textContent.emptyLinks || "Aucun parent"}
|
||||
addLabel={textContent.addLinkLabel || "Définir un parent"}
|
||||
removeLabel={textContent.removeRelationTitle || "Retirer"}
|
||||
prerequisiteLabel={textContent.prerequisiteCheckboxLabel || "Pré requis"}
|
||||
onUpdateTask={onUpdateTask}
|
||||
onAddLinkTarget={onAddLinkTarget}
|
||||
onRemoveLinkTarget={onRemoveLinkTarget}
|
||||
onSetPrerequisite={onSetPrerequisite}
|
||||
onSetTaskCategory={onSetTaskCategory}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children && <ul className="task-planner-list task-planner-child-list">{children}</ul>}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function MissingPrerequisitesTooltip({ missingPrerequisites, tasks, textContent }) {
|
||||
return (
|
||||
<span className="task-planner-prerequisite-tooltip">
|
||||
<strong>{textContent.missingPrerequisiteTitle || "Pré requis non effectué"}</strong>
|
||||
<span>
|
||||
{missingPrerequisites.map((relation) => (
|
||||
<span key={relation.id}>{getTaskTitle(tasks, relation.toTaskId)}</span>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskResetSettings({ task, data, textContent, onUpdateTask }) {
|
||||
if (task.type === "daily") {
|
||||
const hasOverride = Boolean(task.dailyResetTime);
|
||||
return (
|
||||
<div className="task-planner-reset-override">
|
||||
<span>{textContent.taskDailyResetTimeLabel || "Heure quotidienne"}</span>
|
||||
<div className="task-planner-reset-control">
|
||||
<input
|
||||
type="time"
|
||||
value={task.dailyResetTime || data.resetTime}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => {
|
||||
const nextTask = { ...current };
|
||||
if (event.target.value && event.target.value !== data.resetTime) nextTask.dailyResetTime = event.target.value;
|
||||
else delete nextTask.dailyResetTime;
|
||||
return nextTask;
|
||||
})}
|
||||
aria-label={`${textContent.taskDailyResetTimeLabel || "Heure quotidienne"} ${task.title}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdateTask(task.id, (current) => {
|
||||
const nextTask = { ...current };
|
||||
delete nextTask.dailyResetTime;
|
||||
return nextTask;
|
||||
})}
|
||||
disabled={!hasOverride}
|
||||
title={textContent.inheritResetTime || "Utiliser l'heure globale"}
|
||||
aria-label={`${textContent.inheritResetTime || "Utiliser l'heure globale"} ${task.title}`}
|
||||
>
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (task.type === "weekly") {
|
||||
return (
|
||||
<label className="task-planner-reset-override">
|
||||
<span>{textContent.taskWeeklyResetDayLabel || "Jour hebdo"}</span>
|
||||
<select
|
||||
value={task.weeklyResetDay ?? ""}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => {
|
||||
const nextTask = { ...current };
|
||||
if (event.target.value === "") {
|
||||
delete nextTask.weeklyResetDay;
|
||||
} else {
|
||||
nextTask.weeklyResetDay = Number(event.target.value);
|
||||
}
|
||||
return nextTask;
|
||||
})}
|
||||
>
|
||||
<option value="">{textContent.inheritWeeklyResetDay || "Global"}</option>
|
||||
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-planner-reset-override is-empty">
|
||||
<span>{textContent.taskResetLabel || "Réinitialisation"}</span>
|
||||
<p>{textContent.noTaskResetLabel || "Aucun reset"}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets, emptyLabel, addLabel, removeLabel, prerequisiteLabel, onAddLinkTarget, onRemoveLinkTarget, onSetPrerequisite }) {
|
||||
const descendantIds = new Set();
|
||||
function collectDescendants(taskId) {
|
||||
tasks.filter((item) => parentMap.get(item.id) === taskId).forEach((child) => {
|
||||
descendantIds.add(child.id);
|
||||
collectDescendants(child.id);
|
||||
});
|
||||
}
|
||||
collectDescendants(task.id);
|
||||
const availableTargets = relationTargets.filter((target) => !descendantIds.has(target.id));
|
||||
|
||||
return (
|
||||
<div className="task-planner-relations">
|
||||
<span>{label}</span>
|
||||
<select
|
||||
value=""
|
||||
onChange={(event) => {
|
||||
onAddLinkTarget(task.id, event.target.value);
|
||||
event.target.value = "";
|
||||
}}
|
||||
aria-label={addLabel}
|
||||
>
|
||||
<option value="">{addLabel}</option>
|
||||
{availableTargets.map((target) => <option key={target.id} value={target.id}>{target.title}</option>)}
|
||||
</select>
|
||||
<div>
|
||||
{relations.length ? relations.map((relation) => (
|
||||
<div className="task-planner-relation-chip" key={relation.id}>
|
||||
<span>{getTaskTitle(tasks, relation.toTaskId)}</span>
|
||||
<label title={prerequisiteLabel}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={relation.prerequisite}
|
||||
onChange={(event) => onSetPrerequisite(task.id, relation.toTaskId, event.target.checked)}
|
||||
aria-label={`${prerequisiteLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}
|
||||
/>
|
||||
<span>{prerequisiteLabel}</span>
|
||||
</label>
|
||||
<button type="button" onClick={() => onRemoveLinkTarget(task.id, relation.toTaskId)} title={removeLabel} aria-label={`${removeLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}>
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
)) : <em>{emptyLabel}</em>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue