Skip to content

Commit

Permalink
Pages Editor: implement Edit Task Form (#6981)
Browse files Browse the repository at this point in the history
* pages-editor-pt12: TextTask can now commit updates

* TextTask: fix 'required' toggle

* TextTask: cleanup HTML. remove unused elements.

* EditTaskForm: style up

* EditTaskForm: style update

* EditStepDialog: use proper Task names

* EditStepDialog: design update. Add more visible Task Key

* EditTaskForm: restyling fieldset.

* EditStepDialog: add optional 'Done' footer to close dialog

* EditStepDialog: restyle button

* EditStepDialog: minor restyles

* EditTaskForm: add SingleQuestionTask

* SingleQuestionTask: add ability to list and add choices

* SingleQuestionTask: implement ability to edit answers (choices)

* SingleQuestionTask: implement ability to delete answers

* Cleanup: remove eslint

* SingleQuestionTask: add Plus and Minus icons

* SingleQuestionTask: implement ability to delete choices
  • Loading branch information
shaunanoordin authored Feb 12, 2024
1 parent 404ec1f commit 0e611cf
Show file tree
Hide file tree
Showing 17 changed files with 346 additions and 43 deletions.
14 changes: 14 additions & 0 deletions app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default function TasksPage() {
update({ steps });
}

// aka openEditStepDialog
function editStep(stepIndex) {
setActiveStepIndex(stepIndex);
editStepDialog.current?.openDialog();
Expand All @@ -71,6 +72,17 @@ export default function TasksPage() {
newTaskDialog.current?.openDialog();
}

function deleteTask(taskKey) {
// TODO
}

function updateTask(taskKey, task) {
if (!taskKey) return;
const tasks = JSON.parse(JSON.stringify(workflow?.tasks || {}));
tasks[taskKey] = task
update({tasks});
}

if (!workflow) return null;

return (
Expand Down Expand Up @@ -123,6 +135,8 @@ export default function TasksPage() {
allTasks={workflow.tasks}
step={workflow.steps[activeStepIndex]}
stepIndex={activeStepIndex}
deleteTask={deleteTask}
updateTask={updateTask}
/>

{/* EXPERIMENTAL */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import PropTypes from 'prop-types';
import EditTaskForm from './EditTaskForm.jsx';
import CloseIcon from '../../../../icons/CloseIcon.jsx';

const taskNames = {
'drawing': 'Drawing',
'single': 'Single Question',
'text': 'Text',
}

function EditStepDialog({
allTasks = {},
step = [],
stepIndex = -1
stepIndex = -1,
updateTask
}, forwardedRef) {
const [ stepKey, stepBody ] = step ;
const taskKeys = stepBody?.taskKeys || [];
Expand All @@ -28,7 +35,9 @@ function EditStepDialog({
};
});

const title = 'Create a (???) Task'
const firstTask = allTasks?.[taskKeys?.[0]]
const taskName = taskNames[firstTask?.type] || '???';
const title = `Edit ${taskName} Task`;

return (
<dialog
Expand Down Expand Up @@ -62,13 +71,21 @@ function EditStepDialog({
key={`editTaskForm-${taskKey}`}
task={task}
taskKey={taskKey}
updateTask={updateTask}
/>
);
})}
<div className="edit-task-form-controls">
<button>Save</button>
</div>
</form>
<div className="dialog-footer flex-row">
<div className="flex-item" />
<button
className="big"
onClick={closeDialog}
type="button"
>
Done
</button>
</div>
</dialog>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import SingleQuestionTask from './types/SingleQuestionTask.jsx';
import TextTask from './types/TextTask.jsx';

const taskTypes = {
'single': SingleQuestionTask,
'text': TextTask
};

export default function EditTaskForm({ // It's not actually a form, but a fieldset that's part of a form.
task,
taskKey
taskKey,
updateTask
}) {
if (!task || !taskKey) return <li>ERROR: could not render Task</li>;

const TaskForm = taskTypes[task.type];

return (
<fieldset
className="edit-task-form"
>
<legend>{taskKey}</legend>
<legend className="task-key">{taskKey}</legend>
{(TaskForm)
? <TaskForm />
? <TaskForm
task={task}
taskKey={taskKey}
updateTask={updateTask}
/>
: null
}
</fieldset>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { useEffect, useState } from 'react';

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

export default function SingleQuestionTask({
task,
taskKey,
updateTask = () => {}
}) {
const [ answers, setAnswers ] = useState(task?.answers || []);
const [ help, setHelp ] = useState(task?.help || '');
const [ question, setQuestion ] = useState(task?.question || ''); // TODO: figure out if FEM is standardising Question vs Instructions
const [ required, setRequired ] = useState(!!task?.required);

// Update is usually called manually onBlur, after user input is complete.
function update(optionalStateOverrides) {
const _answers = optionalStateOverrides?.answers || answers
const nonEmptyAnswers = _answers.filter(({ label }) => label.trim().length > 0);

const newTask = {
...task,
answers: nonEmptyAnswers,
help,
question,
required
};
updateTask(taskKey, newTask);
}

function addAnswer(e) {
const newAnswers = [ ...answers, { label: '', next: undefined }];
setAnswers(newAnswers);

e.preventDefault();
return false;
}

function editAnswer(e) {
const index = e?.target?.dataset?.index;
if (index === undefined || index < 0 || index >= answers.length) return;

const answer = answers[index];
const newLabel = e?.target?.value || '';

setAnswers(answers.with(index, { ...answer, label: newLabel }));
}

function deleteAnswer(e) {
const index = e?.target?.dataset?.index;
if (index === undefined || index < 0 || index >= answers.length) return;

const newAnswers = answers.slice()
newAnswers.splice(index, 1);
setAnswers(newAnswers);
update({ answer: newAnswers }); // Use optional state override, since setAnswers() won't reflect new values in this step of the lifecycle.

e.preventDefault();
return false;
}

// For inputs that don't have onBlur, update triggers automagically.
// (You can't call update() in the onChange() right after setStateValue().)
useEffect(update, [required]);

return (
<div className="single-question-task">
<div className="input-row">
<label
className="big"
htmlFor={`task-${taskKey}-instruction`}
>
Main Text
</label>
<div className="flex-row">
<span className="task-key">{taskKey}</span>
<input
className="flex-item"
id={`task-${taskKey}-question`}
type="text"
value={question}
onBlur={update}
onChange={(e) => { setQuestion(e?.target?.value) }}
/>
</div>
{/* <button>Delete</button> */}
</div>
<div className="input-row">
<label className="big">Choices</label>
<div className="flex-row">
<button
aria-label="Add choice"
className="big"
onClick={addAnswer}
type="button"
>
<PlusIcon />
</button>
<label className="narrow">
<input
type="checkbox"
checked={required}
onChange={(e) => {
setRequired(!!e?.target?.checked);
}}
/>
<span>
Required
</span>
</label>
</div>
</div>
<div className="input-row">
<ul>
{answers.map(({ label, next }, index) => (
<li
aria-label={`Choice ${index}`}
className="flex-row"
key={`single-question-task-answer-${index}`}
>
<input
className="flex-item"
onChange={editAnswer}
onBlur={update}
type="text"
value={label}
data-index={index}
/>
<button
aria-label={`Delete choice ${index}`}
onClick={deleteAnswer}
className="big"
data-index={index}
>
<MinusIcon data-index={index} />
</button>
</li>
))}
</ul>
</div>
<div className="input-row">
<label
className="big"
htmlFor={`task-${taskKey}-help`}
>
Help Text
</label>
<textarea
id={`task-${taskKey}-help`}
value={help}
onBlur={update}
onChange={(e) => { setHelp(e?.target?.value) }}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,78 @@
import { useEffect, useState } from 'react';

export default function TextTask({
task,
taskKey
taskKey,
updateTask = () => {}
}) {
const [ help, setHelp ] = useState(task?.help || '');
const [ instruction, setInstruction ] = useState(task?.instruction || '');
const [ required, setRequired ] = useState(!!task?.required);

// Update is usually called manually onBlur, after user input is complete.
function update() {
const newTask = {
...task,
help,
instruction,
required
};
updateTask(taskKey, newTask);
}

// For inputs that don't have onBlur, update triggers automagically.
// (You can't call update() in the onChange() right after setStateValue().)
useEffect(update, [required]);

return (
<div>
<div>
<label>Main Text</label>
<div className="text-task">
<div className="input-row">
<label
className="big"
htmlFor={`task-${taskKey}-instruction`}
>
Main Text
</label>
<div className="flex-row">
<span className="task-key">{taskKey}</span>
<input
className="flex-item"
id={`task-${taskKey}-instruction`}
type="text"
value={instruction}
onBlur={update}
onChange={(e) => { setInstruction(e?.target?.value) }}
/>
<button>Delete</button>
</div>
{/* <button>Delete</button> */}
</div>
<div>
<label>
<input type="checkbox" />
Required
<div className="input-row">
<label className="narrow">
<input
type="checkbox"
checked={required}
onChange={(e) => {
setRequired(!!e?.target?.checked);
}}
/>
<span>
Required
</span>
</label>
</div>
<div>
<label>Help Text</label>
<textarea />
<div className="input-row">
<label
className="big"
htmlFor={`task-${taskKey}-help`}
>
Help Text
</label>
<textarea
id={`task-${taskKey}-help`}
value={help}
onBlur={update}
onChange={(e) => { setHelp(e?.target?.value) }}
/>
</div>
</div>
);
Expand Down
2 changes: 0 additions & 2 deletions app/pages/lab-pages-editor/icons/CloseIcon.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable react/react-in-jsx-scope */

export default function CloseIcon({ alt }) {
return (
<span className="icon fa fa-close" aria-label={alt} role={!!alt ? 'img' : undefined} />
Expand Down
2 changes: 0 additions & 2 deletions app/pages/lab-pages-editor/icons/CopyIcon.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable react/react-in-jsx-scope */

export default function CopyIcon({ alt }) {
return (
<span className="icon fa fa-copy" aria-label={alt} role={!!alt ? 'img' : undefined} />
Expand Down
2 changes: 0 additions & 2 deletions app/pages/lab-pages-editor/icons/DeleteIcon.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable react/react-in-jsx-scope */

export default function DeleteIcon({ alt }) {
return (
<span className="icon fa fa-trash" aria-label={alt} role={!!alt ? 'img' : undefined} />
Expand Down
2 changes: 0 additions & 2 deletions app/pages/lab-pages-editor/icons/EditIcon.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable react/react-in-jsx-scope */

export default function EditIcon({ alt }) {
return (
<span className="icon fa fa-pencil" aria-label={alt} role={!!alt ? 'img' : undefined} />
Expand Down
Loading

0 comments on commit 0e611cf

Please sign in to comment.