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 calendarUtils = await readFile("website/src/features/toolboxes/modules/calendarUtils.js", "utf8");
|
||||||
const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "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 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 taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8");
|
||||||
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "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");
|
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(taskPlannerModule, /reorderFeatures/);
|
||||||
assert.match(taskPlannerItems, /export function TaskPlannerBranch/);
|
assert.match(taskPlannerItems, /export function TaskPlannerBranch/);
|
||||||
assert.match(taskPlannerItems, /function TaskPlannerItem/);
|
assert.match(taskPlannerItems, /function TaskPlannerItem/);
|
||||||
assert.match(taskPlannerItems, /task-planner-warning/);
|
assert.match(taskPlannerBundle, /task-planner-warning/);
|
||||||
assert.match(taskPlannerItems, /useInlineEdit/);
|
assert.match(taskPlannerBundle, /useInlineEdit/);
|
||||||
assert.match(taskPlannerItems, /commitOnEnter: false/);
|
assert.match(taskPlannerBundle, /commitOnEnter: false/);
|
||||||
assert.match(taskPlannerItems, /categoryLabel/);
|
assert.match(taskPlannerBundle, /categoryLabel/);
|
||||||
assert.match(taskPlannerItems, /prerequisite/);
|
assert.match(taskPlannerBundle, /prerequisite/);
|
||||||
assert.match(taskPlannerUtils, /export function getTaskParentMap/);
|
assert.match(taskPlannerUtils, /export function getTaskParentMap/);
|
||||||
assert.match(taskPlannerUtils, /export function applyDueResets/);
|
assert.match(taskPlannerUtils, /export function applyDueResets/);
|
||||||
assert.doesNotMatch(taskPlannerModule, /canMoveItem:/);
|
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 { Icon } from "../../../components/Icon.jsx";
|
||||||
import { InlineNotice } from "../../../components/AppOverlays.jsx";
|
|
||||||
import { Tooltip } from "../../../components/Tooltip.jsx";
|
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
|
import { MissingPrerequisitesTooltip, TaskPlannerItemDetails } from "./TaskPlannerItemPanels.jsx";
|
||||||
import {
|
import {
|
||||||
TASK_TYPES,
|
TASK_TYPES,
|
||||||
WEEK_DAYS,
|
|
||||||
countCompletedTaskTree,
|
countCompletedTaskTree,
|
||||||
countTaskTree,
|
countTaskTree,
|
||||||
getEffectiveTaskCategory,
|
|
||||||
getTaskById,
|
|
||||||
getTaskCategoryDataset,
|
getTaskCategoryDataset,
|
||||||
getTaskCategoryGroups,
|
getTaskCategoryGroups,
|
||||||
getTaskTitle,
|
|
||||||
getTaskTypeLabel,
|
getTaskTypeLabel,
|
||||||
getTasksForParent,
|
getTasksForParent,
|
||||||
isTaskResetSoon,
|
isTaskResetSoon,
|
||||||
|
|
@ -231,10 +226,6 @@ function TaskPlannerItem({
|
||||||
onSetTaskCategory,
|
onSetTaskCategory,
|
||||||
children
|
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 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 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));
|
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,
|
transform: (draft) => String(draft || "").trim() || task.title,
|
||||||
onCommit: (title) => onUpdateTask(task.id, (current) => ({ ...current, 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) {
|
function updateChecked(event) {
|
||||||
const checked = event.target.checked;
|
const checked = event.target.checked;
|
||||||
|
|
@ -314,13 +294,7 @@ function TaskPlannerItem({
|
||||||
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
|
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
{missingPrerequisites.length > 0 && (
|
{missingPrerequisites.length > 0 && (
|
||||||
<Tooltip
|
<MissingPrerequisitesTooltip missingPrerequisites={missingPrerequisites} tasks={tasks} textContent={textContent} />
|
||||||
className="task-planner-warning"
|
|
||||||
content={<MissingPrerequisitesTooltip missingPrerequisites={missingPrerequisites} tasks={tasks} textContent={textContent} />}
|
|
||||||
position="top-start"
|
|
||||||
>
|
|
||||||
{textContent.missingPrerequisiteBadge || "Pré requis"}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
<div className="task-planner-actions">
|
<div className="task-planner-actions">
|
||||||
<button
|
<button
|
||||||
|
|
@ -348,180 +322,25 @@ function TaskPlannerItem({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<InlineNotice className="task-planner-notice" message={noticeMessage} />
|
<TaskPlannerItemDetails
|
||||||
{descriptionOpen && (
|
task={task}
|
||||||
<div className="task-planner-description-panel">
|
tasks={tasks}
|
||||||
<label className={`task-planner-category-field ${categoryEditable ? "" : "is-disabled"}`}>
|
data={data}
|
||||||
<span>{textContent.categoryLabel || "Catégorie"}</span>
|
parentMap={parentMap}
|
||||||
<input
|
parentId={parentId}
|
||||||
{...categoryEdit.getInputProps({
|
textContent={textContent}
|
||||||
disabled: !categoryEditable,
|
descriptionOpen={descriptionOpen}
|
||||||
placeholder: textContent.categoryPlaceholder || "Nom de catégorie",
|
settingsOpen={settingsOpen}
|
||||||
"aria-label": `${textContent.categoryLabel || "Catégorie"} ${task.title}`
|
noticeMessage={noticeMessage}
|
||||||
})}
|
relations={relations}
|
||||||
/>
|
relationTargets={relationTargets}
|
||||||
</label>
|
onUpdateTask={onUpdateTask}
|
||||||
<label className="task-planner-description-field">
|
onAddLinkTarget={onAddLinkTarget}
|
||||||
<span>{textContent.descriptionLabel || "Description"}</span>
|
onRemoveLinkTarget={onRemoveLinkTarget}
|
||||||
<textarea
|
onSetPrerequisite={onSetPrerequisite}
|
||||||
{...descriptionEdit.getInputProps({
|
onSetTaskCategory={onSetTaskCategory}
|
||||||
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}
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
{children && <ul className="task-planner-list task-planner-child-list">{children}</ul>}
|
{children && <ul className="task-planner-list task-planner-child-list">{children}</ul>}
|
||||||
</li>
|
</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