Skip to content

Commit

Permalink
Pages Editor: add "Delete Task from Page" functionality (#7075)
Browse files Browse the repository at this point in the history
* pages-editor-pt19: add Delete Task button

* cleanupTasksAndSteps: refactor to use structuredClone instead of array.slice()

* TasksPage: implement deleteTask()

* cleanupTasksAndSteps: remove steps without tasks

* cleanupTasksAndSteps: implement cleanup of orphaned tasks and steps

* TasksPage: deleteTask() closes dialog if last task in step. cleanupTasksAndSteps: fix order of actions.
  • Loading branch information
shaunanoordin authored Apr 25, 2024
1 parent a82d2f1 commit 5d5242a
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 24 deletions.
41 changes: 35 additions & 6 deletions app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default function TasksPage() {
const newTaskDialog = useRef(null);
const [ activeStepIndex, setActiveStepIndex ] = useState(-1); // Tracks which Step is being edited.
const [ activeDragItem, setActiveDragItem ] = useState(-1); // Keeps track of active item being dragged (StepItem). This is because "dragOver" CAN'T read the data from dragEnter.dataTransfer.getData().
const activeStepKey = workflow?.steps?.[activeStepIndex]?.[0];
const isActive = true; // TODO

/*
Expand Down Expand Up @@ -77,8 +76,39 @@ export default function TasksPage() {
}

function deleteTask(taskKey) {
if (!taskKey) return;
// TODO
// First check: does the task exist?
if (!workflow || !taskKey || !workflow?.tasks?.[taskKey]) return;

// Second check: is this the only task in the step?
const activeStepTaskKeys = workflow.steps?.[activeStepIndex]?.[1]?.taskKeys || [];
const onlyTaskInStep = !!(activeStepTaskKeys.length === 1 && activeStepTaskKeys[0] === taskKey);

// Third check: are you sure?
const confirmed = onlyTaskInStep
? confirm(`Delete Task ${taskKey}? This will also delete the Page.`)
: confirm(`Delete Task ${taskKey}?`);
if (!confirmed) return;

// Delete the task.
const newTasks = structuredClone(workflow.tasks || {});
delete newTasks[taskKey];

// Delete the task reference in steps.
const newSteps = structuredClone(workflow.steps || {});
newSteps.forEach(step => {
const stepBody = step[1] || {};
stepBody.taskKeys = (stepBody?.taskKeys || []).filter(key => key !== taskKey);
});

// Close the Edit Step Dialog, if necessary.
// Note that this will also trigger handleCloseEditStepDialog()
if (onlyTaskInStep) {
editStepDialog.current?.closeDialog();
}

// Cleanup, then commit.
const cleanedTasksAndSteps = cleanupTasksAndSteps(newTasks, newSteps);
update(cleanedTasksAndSteps);
}

function moveStep(from, to) {
Expand All @@ -92,16 +122,15 @@ export default function TasksPage() {
function deleteStep(stepIndex) {
if (!workflow) return;
const { steps, tasks } = workflow;
const [ stepKey, stepBody ] = steps[stepIndex] || [];
const tasksToBeDeleted = stepBody?.taskKeys || [];
const [ stepKey ] = steps[stepIndex] || [];

const confirmed = confirm(`Delete Page ${stepKey}?`);
if (!confirmed) return;

const newSteps = steps.toSpliced(stepIndex, 1); // Copy then delete Step at stepIndex
const newTasks = tasks ? { ...tasks } : {}; // Copy tasks
tasksToBeDeleted.forEach(taskKey => delete newTasks[taskKey]);

// cleanedupTasksAndSteps() will also remove tasks not associated with any step.
const cleanedTasksAndSteps = cleanupTasksAndSteps(newTasks, newSteps);
update(cleanedTasksAndSteps);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const DEFAULT_HANDLER = () => {};

function EditStepDialog({
allTasks = {},
deleteTask,
onClose = DEFAULT_HANDLER,
openNewTaskDialog = DEFAULT_HANDLER,
step = [],
Expand All @@ -26,6 +27,7 @@ function EditStepDialog({

useImperativeHandle(forwardedRef, () => {
return {
closeDialog,
openDialog
};
});
Expand Down Expand Up @@ -78,6 +80,7 @@ function EditStepDialog({
return (
<EditTaskForm
key={`editTaskForm-${taskKey}`}
deleteTask={deleteTask}
task={task}
taskKey={taskKey}
updateTask={updateTask}
Expand All @@ -94,7 +97,7 @@ function EditStepDialog({
Add New Task
</button>
<button
className="big teal-border"
className="big done"
onClick={closeDialog}
type="button"
>
Expand All @@ -107,6 +110,7 @@ function EditStepDialog({

EditStepDialog.propTypes = {
allTasks: PropTypes.object,
deleteTask: PropTypes.func,
onClose: PropTypes.func,
step: PropTypes.object,
stepIndex: PropTypes.number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const taskTypes = {
};

export default function EditTaskForm({ // It's not actually a form, but a fieldset that's part of a form.
deleteTask,
task,
taskKey,
updateTask
Expand All @@ -22,6 +23,7 @@ export default function EditTaskForm({ // It's not actually a form, but a field
<legend className="task-key">{taskKey}</legend>
{(TaskForm)
? <TaskForm
deleteTask={deleteTask}
task={task}
taskKey={taskKey}
updateTask={updateTask}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';

import DeleteIcon from '../../../../../icons/DeleteIcon.jsx';
import MinusIcon from '../../../../../icons/MinusIcon.jsx';
import PlusIcon from '../../../../../icons/PlusIcon.jsx';

Expand All @@ -8,6 +9,7 @@ const DEFAULT_HANDLER = () => {};
export default function SingleQuestionTask({
task,
taskKey,
deleteTask = DEFAULT_HANDLER,
updateTask = DEFAULT_HANDLER
}) {
const [ answers, setAnswers ] = useState(task?.answers || []);
Expand All @@ -30,6 +32,10 @@ export default function SingleQuestionTask({
updateTask(taskKey, newTask);
}

function doDelete() {
deleteTask(taskKey);
}

function addAnswer(e) {
const newAnswers = [ ...answers, { label: '', next: undefined }];
setAnswers(newAnswers);
Expand Down Expand Up @@ -84,8 +90,15 @@ export default function SingleQuestionTask({
onBlur={update}
onChange={(e) => { setQuestion(e?.target?.value) }}
/>
<button
aria-label={`Delete Task ${taskKey}`}
className="big"
onClick={doDelete}
type="button"
>
<DeleteIcon />
</button>
</div>
{/* <button>Delete</button> */}
</div>
<div className="input-row">
<span className="big">Choices</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useEffect, useState } from 'react';
import DeleteIcon from '../../../../../icons/DeleteIcon.jsx';

const DEFAULT_HANDLER = () => {};

export default function TextTask({
task,
taskKey,
deleteTask = DEFAULT_HANDLER,
updateTask = DEFAULT_HANDLER
}) {
const [ help, setHelp ] = useState(task?.help || '');
Expand All @@ -22,6 +24,10 @@ export default function TextTask({
updateTask(taskKey, newTask);
}

function doDelete() {
deleteTask(taskKey);
}

// For inputs that don't have onBlur, update triggers automagically.
// (You can't call update() in the onChange() right after setStateValue().)
useEffect(update, [required]);
Expand All @@ -45,8 +51,15 @@ export default function TextTask({
onBlur={update}
onChange={(e) => { setInstruction(e?.target?.value) }}
/>
<button
aria-label={`Delete Task ${taskKey}`}
className="big"
onClick={doDelete}
type="button"
>
<DeleteIcon />
</button>
</div>
{/* <button>Delete</button> */}
</div>
<div className="input-row">
<span className="narrow">
Expand Down
35 changes: 22 additions & 13 deletions app/pages/lab-pages-editor/helpers/cleanupTasksAndSteps.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
Clean up tasks and steps.
- TODO: Remove steps without tasks.
- TODO: Remove tasks not associated with any step.
- Remove steps without tasks.
- Remove tasks not associated with any step.
- Remove orphaned references in branching tasks.
- Remove orphaned references in steps.
Expand All @@ -10,32 +10,41 @@ Clean up tasks and steps.

export default function cleanupTasksAndSteps(tasks = {}, steps = []) {
const newTasks = structuredClone(tasks); // Copy tasks
let newSteps = steps.slice(); // Copy steps
let newSteps = structuredClone(steps); // Copy steps. This is a deep copy, compared to steps.slice()

// Remove steps without tasks.
newSteps = newSteps.filter(step => step?.[1]?.taskKeys?.length > 0);

// Remove tasks not associated with any step.
Object.keys(newTasks).forEach(taskKey => {
let existsInAnyStep = false;
newSteps.forEach(step => {
existsInAnyStep = existsInAnyStep || !!step?.[1]?.taskKeys?.includes(taskKey);
});
if (!existsInAnyStep) delete newTasks[taskKey];
});

const taskKeys = Object.keys(newTasks);
const stepKeys = newSteps.map(step => step[0]);

// Remove orphaned references in branching tasks.
Object.values(newTasks).forEach(taskBody => {
taskBody?.answers?.forEach(answer => {
Object.values(newTasks).forEach(task => {
task?.answers?.forEach(answer => {
// If the branching answer points to a non-existent Task Key or Step Key, remove the 'next'.
if (answer.next && !taskKeys.includes(answer.next) && !stepKeys.includes(answer.next)) {
delete answer.next;
}
})
});
});

// Remove orphaned references in steps.
newSteps = newSteps.map(step => {
const [stepKey, stepBody] = step;
const newStepBody = { ...stepBody };

// If the stepBody points to a non-existent Task Key or Step Key, remove the 'next'.
if (newStepBody.next && !taskKeys.includes(newStepBody.next) && !stepKeys.includes(newStepBody.next)) {
delete newStepBody.next;
const [stepKey, stepBody] = step;
if (stepBody.next && !taskKeys.includes(stepBody.next) && !stepKeys.includes(stepBody.next)) {
delete stepBody.next;
}

return [ stepKey, newStepBody ]
return [ stepKey, stepBody ]
})

return { tasks: newTasks, steps: newSteps };
Expand Down
5 changes: 3 additions & 2 deletions css/lab-pages-editor.styl
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,13 @@ $fontWeightBoldPlus = 700
button.big
border: 3px solid $grey1
font-size: $fontSizeM
padding: $sizeS $sizeXL
padding: $sizeS $sizeM
box-shadow: none
text-transform: uppercase

&.teal-border
&.done
border: 3px solid $teal
padding: $sizeS $sizeXL

.dialog-header
background: $teal
Expand Down

0 comments on commit 5d5242a

Please sign in to comment.