From d5416cbb47531f323eb37a826acd256a463e0e0b Mon Sep 17 00:00:00 2001 From: "Shaun A. Noordin" Date: Thu, 6 Jun 2024 15:46:55 +0100 Subject: [PATCH] Pages Editor: add Drawing Task (#7100) * pages-editor-pt23: fill in missing strings in TaskItem * Add UnknownTask * EditStepDialog: restyle TaskItem children * Tasks: change 'Main Text' title if step has many tasks * SingleQuestionTask, TextTask: add proptypes * Renamed SingleQuestionTask to QuestionTask. Restyle padding of EditTaskForm * Add DrawingTask, based on QuestionTask * QuestionTask: rename some stray 'single-question-task' ids * DrawingTask: remove 'required' checkbox * Restyle label/span.big elements * Minor refactor and documentation * DrawingTask: add option to change tool type and color * DrawingTask: style grid items * DrawingTasks: add min/max for tools * DrawingTask: set up tool type options * Minor restyle * DrawingTask: add color preview * DrawingTask: add default value for new tool * DrawingTask: restyle/reposition * Add DrawingToolIcon. Minor code cleanup. * DrawingIcon: add specific icons for specific tools * TaskItem: add placeholders for DrawingTask --- .../components/TasksPage/TasksPage.jsx | 8 +- .../EditStepDialog/EditStepDialog.jsx | 11 +- .../EditStepDialog/EditTaskForm.jsx | 18 +- .../EditStepDialog/types/DrawingTask.jsx | 347 ++++++++++++++++++ ...ingleQuestionTask.jsx => QuestionTask.jsx} | 38 +- .../EditStepDialog/types/TextTask.jsx | 25 +- .../EditStepDialog/types/UnknownTask.jsx | 37 ++ .../components/StepItem/TaskItem.jsx | 25 +- .../helpers/cleanupTasksAndSteps.js | 2 +- .../icons/DrawingToolIcon.jsx | 62 ++++ app/pages/lab-pages-editor/icons/GripIcon.jsx | 1 + app/pages/lab-pages-editor/icons/README.md | 3 + app/pages/lab-pages-editor/icons/TaskIcon.jsx | 1 + css/lab-pages-editor.styl | 104 +++++- 14 files changed, 644 insertions(+), 38 deletions(-) create mode 100644 app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/types/DrawingTask.jsx rename app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/types/{SingleQuestionTask.jsx => QuestionTask.jsx} (86%) create mode 100644 app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/types/UnknownTask.jsx create mode 100644 app/pages/lab-pages-editor/icons/DrawingToolIcon.jsx diff --git a/app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx b/app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx index 97ca412db8..1704f8760d 100644 --- a/app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx +++ b/app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx @@ -94,7 +94,7 @@ export default function TasksPage() { */ function updateTask(taskKey, task) { if (!workflow || !taskKey) return; - const newTasks = structuredClone(workflow.tasks); // Copy tasks + const newTasks = structuredClone(workflow.tasks); // Copy tasks. newTasks[taskKey] = task; update({ tasks: newTasks }); } @@ -114,11 +114,11 @@ export default function TasksPage() { if (!confirmed) return; // Delete the task. - const newTasks = structuredClone(workflow.tasks) || {}; + const newTasks = structuredClone(workflow.tasks) || {}; // Copy tasks. delete newTasks[taskKey]; // Delete the task reference in steps. - const newSteps = structuredClone(workflow.steps) || []; + const newSteps = structuredClone(workflow.steps) || []; // Copy steps. newSteps.forEach(step => { const stepBody = step[1] || {}; stepBody.taskKeys = (stepBody?.taskKeys || []).filter(key => key !== taskKey); @@ -215,7 +215,7 @@ export default function TasksPage() { const answer = task?.answers[answerIndex]; if (!task || !answer) return; - const newTasks = structuredClone(workflow.tasks); // Copy tasks + const newTasks = structuredClone(workflow.tasks); // Copy tasks. const newAnswers = task.answers.with(answerIndex, { ...answer, next }) // Copy, then modify, answers newTasks[taskKey] = { // Insert modified answers into the task inside the copied tasks. Phew! ...task, diff --git a/app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/EditStepDialog.jsx b/app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/EditStepDialog.jsx index 4d80a16667..d19b74fd44 100644 --- a/app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/EditStepDialog.jsx +++ b/app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/EditStepDialog.jsx @@ -6,8 +6,8 @@ import CloseIcon from '../../../../icons/CloseIcon.jsx'; const taskNames = { 'drawing': 'Drawing', - 'multiple': 'Multiple Answer Question', - 'single': 'Single Answer Question', + 'multiple': 'Question', + 'single': 'Question', 'text': 'Text', } @@ -50,7 +50,8 @@ function EditStepDialog({ const firstTask = allTasks?.[taskKeys?.[0]] const taskName = taskNames[firstTask?.type] || '???'; - const title = taskKeys?.length > 1 + const stepHasManyTasks = taskKeys?.length > 1 + const title = stepHasManyTasks ? 'Edit A Multi-Task Page' : `Edit ${taskName} Task`; @@ -79,15 +80,17 @@ function EditStepDialog({ className="dialog-body" onSubmit={onSubmit} > - {taskKeys.map((taskKey) => { + {taskKeys.map((taskKey, index) => { const task = allTasks[taskKey]; return ( ); diff --git a/app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/EditTaskForm.jsx b/app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/EditTaskForm.jsx index 237c374334..096d139921 100644 --- a/app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/EditTaskForm.jsx +++ b/app/pages/lab-pages-editor/components/TasksPage/components/EditStepDialog/EditTaskForm.jsx @@ -1,24 +1,29 @@ import PropTypes from 'prop-types'; -import SingleQuestionTask from './types/SingleQuestionTask.jsx'; +import DrawingTask from './types/DrawingTask.jsx'; +import QuestionTask from './types/QuestionTask.jsx'; import TextTask from './types/TextTask.jsx'; +import UnknownTask from './types/UnknownTask.jsx'; const taskTypes = { - 'multiple': SingleQuestionTask, // Shared with single answer question task - 'single': SingleQuestionTask, + 'drawing': DrawingTask, + 'multiple': QuestionTask, // Shared with single answer question task + 'single': QuestionTask, 'text': TextTask }; function EditTaskForm({ // It's not actually a form, but a fieldset that's part of a form. deleteTask, enforceLimitedBranchingRule, + stepHasManyTasks, task, taskKey, + taskIndexInStep, updateTask }) { if (!task || !taskKey) return
  • ERROR: could not render Task
  • ; - const TaskForm = taskTypes[task.type]; + const TaskForm = taskTypes[task.type] || UnknownTask; return (
    {}; + +const TOOL_COLOR_OPTIONS = [ + { value: '#ff0000', label: 'Red' }, + { value: '#ffff00', label: 'Yellow' }, + { value: '#00ff00', label: 'Green' }, + { value: '#00ffff', label: 'Cyan' }, + { value: '#0000ff', label: 'Blue' }, + { value: '#ff00ff', label: 'Magenta' } +]; + +const TOOL_TYPE_OPTIONS = [ + // Supported in PFE/FEM Lab and works in FEM Classifier: + 'circle', + 'ellipse', + 'line', + 'point', + 'polygon', + 'rectangle', + 'rotateRectangle', + + // Supported in PFE/FEM Lab, but doesn't work in FEM Classifier: + //'bezier', + //'column', + //'fullWidthLine', + //'fullHeightLine', + //'triangle', + //'pointGrid' + + // Only available via experimental tools + // TODO: figure out which ones of these should be standard. + // 'grid', + // 'freehandLine', <-- Maybe this one? + // 'freehandShape', + // 'freehandSegmentLine', + // 'freehandSegmentShape', + // 'anchoredEllipse', + // 'fan', + // 'transcriptionLine', + // 'temporalPoint', + // 'temporalRotateRectangle' +]; + +function DrawingTask({ + deleteTask = DEFAULT_HANDLER, + stepHasManyTasks = false, + task, + taskKey, + updateTask = DEFAULT_HANDLER +}) { + const [ tools, setTools ] = useState(task?.tools || []); + const [ help, setHelp ] = useState(task?.help || ''); + const [ instruction, setInstruction ] = useState(task?.instruction || ''); // TODO: figure out if FEM is standardising Question vs Instructions + const [ prevMarks, setPrevMarks ] = useState(!!task?.enableHidePrevMarks); + const title = stepHasManyTasks ? 'Drawing Task' : 'Main Text'; + + // Update is usually called manually onBlur, after user input is complete. + function update(optionalStateOverrides) { + const _tools = optionalStateOverrides?.tools || tools + // const nonEmptyTools = _tools.filter(({ label }) => label.trim().length > 0); + + const newTask = { + ...task, + type: 'drawing', + tools: _tools, + help, + instruction, + required: false, // On PFE/FEM Lab, this can't be changed. + enableHidePrevMarks: prevMarks + }; + updateTask(taskKey, newTask); + } + + function doDelete() { + deleteTask(taskKey); + } + + function addTool(e) { + const newTools = [ ...tools, { + color: '#00ff00', + details: [], + label: 'Tool name', + max: undefined, + min: undefined, + size: undefined, + type: 'point' + }]; + setTools(newTools); + + e.preventDefault(); + return false; + } + + function editTool(e) { + const index = e?.target?.dataset?.index; + const field = e?.target?.dataset?.field; + const value = e?.target?.value; + if (index === undefined || index < 0 || index >= tools.length) return; + + const tool = structuredClone(tools[index]) || {}; // Copy target tool. + + switch (field) { + case 'label': + case 'type': + case 'color': + case 'size': + tool[field] = value || ''; + break; + + case 'min': + case 'max': + tool[field] = parseInt(value) || undefined; + if (tool.min !== undefined && tool.max !== undefined && tool.max < tool.min) { + tool.max = tool.min; + } + break; + } + + setTools(tools.with(index, tool)); + } + + function deleteTool(e) { + const index = e?.target?.dataset?.index; + if (index === undefined || index < 0 || index >= tools.length) return; + + const newTools = tools.slice(); // Copy tools. + newTools.splice(index, 1); + setTools(newTools); + update({ tools: newTools }); // Use optional state override, since setTools() 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().) + // TODO: useEffect() means update() is called on the first render, which is unnecessary. Clean this up. + useEffect(update, [tools, prevMarks]); + + // TODO: DEBOUNCE FOR tools UPDATE, since typing into the Tool Name/Label causes way too many updates! + + return ( +
    +
    + +
    + {taskKey} + { setInstruction(e?.target?.value) }} + /> + +
    +
    +
    + Tool Configuration + + { setPrevMarks(!!e?.target?.checked); }} + /> + + +
    +
    +
      + {tools.map(({ color, details, label, max, min, size, type }, index) => ( +
    • + +
      + + +
      +
      +
      + +
      + + +
      +
      +
      + +
      +
       
      + +
      +
      +
      + + +
      +
      + + +
      + {(type === 'point') && ( +
      + + +
      + )} +
      +
    • + ))} +
    +
    +
    + + + Add another tool + +
    +
    + +