Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pages Editor: add "Delete Task from Page" functionality #7075

Merged
merged 6 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I like how the code comments in this function are easy to read through 👍

}

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];
});
Comment on lines 11 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just linking this FEM Issue about orphaned tasks for documentation. Your implementation here means any project using the Pages Editor solves the bug 🎉 Hopefully all FEM projects soon 🤞


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
Loading