diff --git a/app/pages/lab-pages-editor/DataManager.jsx b/app/pages/lab-pages-editor/DataManager.jsx index 5c64ddd2be..b851584765 100644 --- a/app/pages/lab-pages-editor/DataManager.jsx +++ b/app/pages/lab-pages-editor/DataManager.jsx @@ -110,10 +110,11 @@ function DataManager({ return { project: apiData.project, + status: apiData.status, workflow: apiData.workflow, update }; - }, [apiData.project, apiData.workflow, updateCounter]); + }, [apiData.project, apiData.workflow, apiData.status, updateCounter]); if (!workflowId) return (
ERROR: no Workflow ID specified
); // if (!workflow) return null diff --git a/app/pages/lab-pages-editor/PagesEditor.jsx b/app/pages/lab-pages-editor/PagesEditor.jsx index e15641e1e5..2ddfc0225e 100644 --- a/app/pages/lab-pages-editor/PagesEditor.jsx +++ b/app/pages/lab-pages-editor/PagesEditor.jsx @@ -17,9 +17,17 @@ import TasksPage from './components/TasksPage'; import WorkflowSettingsPage from './components/WorkflowSettingsPage'; import strings from './strings.json'; +function getDefaultTab() { // Use ?tab=1 or tab='settings' to link directly to Workflow Settings + const params = new URLSearchParams(window?.location?.search); + const tab = params.get('tab'); + + if ([1, '1', 'settings', 'workflowsettings'].includes(tab)) return 1; + return 0; +} + function PagesEditor({ params }) { const { workflowID: workflowId, projectID: projectId } = params; - const [currentTab, setCurrentTab] = useState(0); + const [currentTab, setCurrentTab] = useState(getDefaultTab()); // Default tab is 0 const tabs = [ { id: 'pages-editor_workflow-header-tab-button_task', diff --git a/app/pages/lab-pages-editor/components/TasksPage/ExperimentalPanel.jsx b/app/pages/lab-pages-editor/components/TasksPage/ExperimentalPanel.jsx index 6d18b28da5..021800fb79 100644 --- a/app/pages/lab-pages-editor/components/TasksPage/ExperimentalPanel.jsx +++ b/app/pages/lab-pages-editor/components/TasksPage/ExperimentalPanel.jsx @@ -5,6 +5,7 @@ export default function ExperimentalPanel({ }) { function experimentalReset() { update({ + configuration: {}, first_task: '', tasks: {}, steps: [] diff --git a/app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx b/app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx index d040477d0a..97ca412db8 100644 --- a/app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx +++ b/app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx @@ -5,6 +5,7 @@ import createStep from '../../helpers/createStep.js'; import createTask from '../../helpers/createTask.js'; import getNewStepKey from '../../helpers/getNewStepKey.js'; import getNewTaskKey from '../../helpers/getNewTaskKey.js'; +import linkStepsInWorkflow from '../../helpers/linkStepsInWorkflow.js'; import moveItemInArray from '../../helpers/moveItemInArray.js'; import cleanupTasksAndSteps from '../../helpers/cleanupTasksAndSteps.js'; // import strings from '../../strings.json'; // TODO: move all text into strings @@ -13,6 +14,17 @@ import ExperimentalPanel from './ExperimentalPanel.jsx'; import EditStepDialog from './components/EditStepDialog'; import NewTaskDialog from './components/NewTaskDialog.jsx'; import StepItem from './components/StepItem'; +import WorkflowVersion from '../WorkflowVersion.jsx'; +import WrenchIcon from '../../icons/WrenchIcon.jsx'; + +// Use ?advanced=true to enable advanced mode. +// - switches from simpler "linear workflow" to "manual workflow". +// - enables Experimental Panel. +// - shows hidden options in workflow settings. +function getAdvancedMode() { + const params = new URLSearchParams(window?.location?.search); + return !!params.get('advanced'); +} export default function TasksPage() { const { workflow, update } = useWorkflowContext(); @@ -22,6 +34,13 @@ export default function TasksPage() { 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 firstStepKey = workflow?.steps?.[0]?.[0] || ''; const isActive = true; // TODO + const advancedMode = getAdvancedMode(); + + // A linear workflow means every step (except branching steps) will move into + // the next step in the workflow.steps array. e.g. step0.next = step1 + // A manual (i.e. non-linear) workflow asks the user to explicity spell out + // the next step of each step. + const isLinearWorkflow = !advancedMode; /* Adds a new Task of a specified type (with default settings) to a Step. @@ -32,7 +51,7 @@ export default function TasksPage() { if (!workflow) return; const newTaskKey = getNewTaskKey(workflow.tasks); const newTask = createTask(taskType); - const steps = workflow.steps?.slice() || []; + let steps = workflow.steps?.slice() || []; let step if (stepIndex < 0) { @@ -62,6 +81,10 @@ export default function TasksPage() { [newTaskKey]: newTask }; + if (linkStepsInWorkflow) { + steps = linkStepsInWorkflow(steps, tasks); + } + await update({ tasks, steps }); return (stepIndex < 0) ? steps.length - 1 : stepIndex; } @@ -117,7 +140,10 @@ export default function TasksPage() { const oldSteps = workflow.steps || []; if (from < 0 || to < 0 || from >= oldSteps.length || to >= oldSteps.length) return; - const steps = moveItemInArray(oldSteps, from, to); + let steps = moveItemInArray(oldSteps, from, to); + if (linkStepsInWorkflow) { + steps = linkStepsInWorkflow(steps, workflow.tasks); + } update({ steps }); } @@ -134,6 +160,9 @@ export default function TasksPage() { // cleanedupTasksAndSteps() will also remove tasks not associated with any step. const cleanedTasksAndSteps = cleanupTasksAndSteps(newTasks, newSteps); + if (linkStepsInWorkflow) { + cleanedTasksAndSteps.steps = linkStepsInWorkflow(cleanedTasksAndSteps.steps, cleanedTasksAndSteps.tasks); + } update(cleanedTasksAndSteps); } @@ -214,11 +243,13 @@ export default function TasksPage() {

{workflow.display_name}

- {`#${workflow.id}`} {(isActive) ? Active : Inactive}
-

Tasks

+
+

Tasks

+ +
+ {!(workflow.steps?.length > 0) && ( +
+ +

Start by adding tasks to build your Task Funnel here.

+
+ )}
); diff --git a/app/pages/lab-pages-editor/components/TasksPage/components/StepItem/SimpleNextControls.jsx b/app/pages/lab-pages-editor/components/TasksPage/components/StepItem/SimpleNextControls.jsx index 673ce34e51..c35df0a16e 100644 --- a/app/pages/lab-pages-editor/components/TasksPage/components/StepItem/SimpleNextControls.jsx +++ b/app/pages/lab-pages-editor/components/TasksPage/components/StepItem/SimpleNextControls.jsx @@ -5,12 +5,17 @@ const DEFAULT_HANDLER = () => {}; export default function SimpleNextControls({ allSteps = [], + isLastItem = false, + isLinearWorkflow = false, step, updateNextStepForStep = DEFAULT_HANDLER }) { if (!step) return null; const [ stepKey, stepBody ] = step; - + const isLinearItem = isLinearWorkflow && !isLastItem; + const showFakeSubmit = isLinearWorkflow && isLastItem; + const showNextPageDropdown = !isLinearWorkflow && !showFakeSubmit; + function onChange(e) { const next = e.target?.value; updateNextStepForStep(stepKey, next); @@ -18,35 +23,45 @@ export default function SimpleNextControls({ return (
- - - Submit - - {allSteps.map(([otherStepKey, otherStepBody]) => { - const taskKeys = otherStepBody?.taskKeys?.toString() || '(none)'; - return ( - - ); - })} - + + {allSteps.map(([otherStepKey, otherStepBody]) => { + const taskKeys = otherStepBody?.taskKeys?.toString() || '(none)'; + return ( + + ); + })} + + )} + {showFakeSubmit && ( +
Submit
+ )}
); } SimpleNextControls.propTypes = { allSteps: PropTypes.arrayOf(PropTypes.array), + isLastItem: PropTypes.bool, + isLinearWorkflow: PropTypes.bool, step: PropTypes.array, updateNextStepForStep: PropTypes.func }; \ No newline at end of file diff --git a/app/pages/lab-pages-editor/components/TasksPage/components/StepItem/StepItem.jsx b/app/pages/lab-pages-editor/components/TasksPage/components/StepItem/StepItem.jsx index f093961445..7830230713 100644 --- a/app/pages/lab-pages-editor/components/TasksPage/components/StepItem/StepItem.jsx +++ b/app/pages/lab-pages-editor/components/TasksPage/components/StepItem/StepItem.jsx @@ -24,6 +24,7 @@ function StepItem({ deleteStep = DEFAULT_HANDLER, moveStep = DEFAULT_HANDLER, openEditStepDialog = DEFAULT_HANDLER, + isLinearWorkflow = false, setActiveDragItem = DEFAULT_HANDLER, step, stepIndex, @@ -33,6 +34,7 @@ function StepItem({ const [stepKey, stepBody] = step || []; if (!stepKey || !stepBody || !allSteps || !allTasks) return
  • ERROR: could not render Step
  • ; + const isLastItem = stepIndex === allSteps.length - 1; const taskKeys = stepBody.taskKeys || []; function doDelete() { @@ -142,6 +144,8 @@ function StepItem({ {!branchingTaskKey && ( @@ -161,6 +165,7 @@ StepItem.propTypes = { allSteps: PropTypes.array, allTasks: PropTypes.object, deleteStep: PropTypes.func, + isLinearWorkflow: PropTypes.bool, moveStep: PropTypes.func, openEditStepDialog: PropTypes.func, setActiveDragItem: PropTypes.func, diff --git a/app/pages/lab-pages-editor/components/WorkflowSettingsPage/WorkflowSettingsPage.jsx b/app/pages/lab-pages-editor/components/WorkflowSettingsPage/WorkflowSettingsPage.jsx index 0385ef85ec..fbb84889d1 100644 --- a/app/pages/lab-pages-editor/components/WorkflowSettingsPage/WorkflowSettingsPage.jsx +++ b/app/pages/lab-pages-editor/components/WorkflowSettingsPage/WorkflowSettingsPage.jsx @@ -1,9 +1,21 @@ import { useWorkflowContext } from '../../context.js'; import AssociatedSubjectSets from './components/AssociatedSubjectSets.jsx'; import AssociatedTutorial from './components/AssociatedTutorial.jsx'; +import WorkflowVersion from '../WorkflowVersion.jsx'; + +// Use ?advanced=true to enable advanced mode. +// - switches from simpler "linear workflow" to "manual workflow". +// - enables Experimental Panel. +// - shows hidden options in workflow settings. +function getAdvancedMode() { + const params = new URLSearchParams(window?.location?.search); + return !!params.get('advanced'); +} export default function WorkflowSettingsPage() { const { workflow, update, project } = useWorkflowContext(); + const advancedMode = getAdvancedMode(); + const showSeparateFramesOptions = !!workflow?.configuration?.enable_switching_flipbook_and_separate; function onSubmit(e) { e.preventDefault(); @@ -18,6 +30,7 @@ export default function WorkflowSettingsPage() { const { updaterule } = e?.target?.dataset || {}; if (!key) return; + if (updaterule === 'checkbox') value = !!e?.target?.checked; if (updaterule === 'convert_to_number') value = parseInt(value); if (updaterule === 'undefined_if_empty') value = value || undefined; @@ -37,15 +50,24 @@ export default function WorkflowSettingsPage() { className="workflow-settings-page" onSubmit={onSubmit} > - +
    + +
    +
    + + #{workflow.id} +
    + +
    +
    @@ -74,26 +96,31 @@ export default function WorkflowSettingsPage() { aria-label="Retirement criteria" className="flex-item" defaultValue={workflow?.retirement?.criteria} + disabled={!advancedMode} aria-describedby="subject-retirement-info" name="retirement.criteria" onChange={doUpdate} > - - {/* TODO: this is just a POC - never_retire should be removed, even though it's a valid option on the API. */} + {/* Reason for removal (May 2024): standardisation. PFE/FEM Lab doesn't allow "never retire" option, nor setting the retirement count. */} + {(advancedMode || workflow?.retirement?.criteria === 'never_retire') && + + } - + {(workflow?.retirement?.criteria === 'classification_count') && ( + + )}

    If you'd like more complex retirement rules such as conditional @@ -104,54 +131,201 @@ export default function WorkflowSettingsPage() {

    +
    - Subject Viewer -

    - Choose how to display your subjects. - Refer to the Subject Viewer section of the Glossary for more info. + Multi-Image Options +

    + Choose how to display subjects with multiple images. If your subjects are in sequence, such as camera trap images, volunteers can play them like a .gif using the Flipbook viewer.

    +
    +
    -
    -
    - Multi-Image Options -

    - Choose how to display subjects with multiple images. - If your subjects are in a sequence, such as camera trap images, - volunteers can play them like a .gif using the Flipbook viewer. -

    -

    TODO

    +
    + + +
    + +
    + + +
    + +
    + + +
    + + {showSeparateFramesOptions && (<> +

    Show separate frames as:

    +
      +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    + )}

    -
    - Classification Tools -

    TODO

    +
    + Image Display Options +

    + Check "limit subject image height" if you want to limit subject height to always fit in the browser window. The max height will be the image's original pixel height. +

    +
    + + +
    +
    + + +
    -
    - Quicktalk -

    TODO

    -
    + {advancedMode && (<> {/* Reason for removal (Apr 2024): we want users to use automatic subject viewer selection, to reduce complexity and complications. */} +
    + +
    + Subject Viewer +

    + Choose how to display your subjects. + Refer to the Subject Viewer section of the Glossary for more info. +

    +
    + +
    +
    + )}
    diff --git a/app/pages/lab-pages-editor/components/WorkflowVersion.jsx b/app/pages/lab-pages-editor/components/WorkflowVersion.jsx new file mode 100644 index 0000000000..767bf931f3 --- /dev/null +++ b/app/pages/lab-pages-editor/components/WorkflowVersion.jsx @@ -0,0 +1,16 @@ +import { useWorkflowContext } from '../context.js'; +import LoadingIcon from '../icons/LoadingIcon.jsx' + +export default function WorkflowVersion({}) { + const { workflow, status } = useWorkflowContext(); + const isBusy = status === 'fetching' || status === 'updating'; + if (!workflow) return; + + return ( +
    + V. {workflow.version} + {isBusy && } +
    + ); + +} \ No newline at end of file diff --git a/app/pages/lab-pages-editor/icons/LoadingIcon.jsx b/app/pages/lab-pages-editor/icons/LoadingIcon.jsx new file mode 100644 index 0000000000..415673c2df --- /dev/null +++ b/app/pages/lab-pages-editor/icons/LoadingIcon.jsx @@ -0,0 +1,5 @@ +export default function LoadingIcon({ alt, ...rest }) { + return ( + + ); +} diff --git a/app/pages/lab-pages-editor/icons/WrenchIcon.jsx b/app/pages/lab-pages-editor/icons/WrenchIcon.jsx new file mode 100644 index 0000000000..26cf990b48 --- /dev/null +++ b/app/pages/lab-pages-editor/icons/WrenchIcon.jsx @@ -0,0 +1,5 @@ +export default function WrenchIcon({ alt, ...rest }) { + return ( + + ); +} diff --git a/css/lab-pages-editor.styl b/css/lab-pages-editor.styl index f41437a675..1ae24347eb 100644 --- a/css/lab-pages-editor.styl +++ b/css/lab-pages-editor.styl @@ -80,6 +80,7 @@ $fontWeightBoldPlus = 700 select border: 1.5px solid $grey1 + border-radius: $sizeXS color: $black font-size: $fontSizeM padding: $sizeS @@ -118,7 +119,7 @@ $fontWeightBoldPlus = 700 hr border-top: 1px solid $grey2 - .disabled, .disabled * + .disabled, .disabled *, [disabled] color: $grey1 .decoration-plus @@ -160,11 +161,17 @@ $fontWeightBoldPlus = 700 .justify-around justify-content: space-around + .position-relative + position: relative + .spacing-bottom-XS margin-bottom: $sizeXS .spacing-bottom-M margin-bottom: $sizeM + + .workflow-version + color: $teal // Component: Workflow Header // --------------------------------------------------------------------------- @@ -212,25 +219,33 @@ $fontWeightBoldPlus = 700 .workflow-settings-page display: grid gap: $sizeM - grid-template-columns: auto auto + grid-template-columns: 50% auto grid-template-rows: auto auto margin-top: $sizeS // Workflow Title - label[for=display_name] - display: block - font-size: $fontSizeL - font-weight: $fontWeightBoldPlus + .workflow-title grid-column: 1 / span 2 grid-row: 1 - text-transform: uppercase - input + label[for=display_name] + display: block + font-size: $fontSizeL + font-weight: $fontWeightBoldPlus + margin-bottom: $sizeXS + text-transform: uppercase + + input[name=display_name] border-radius: $sizeXS display: block font-size: $fontSizeL - margin-top: $sizeXS width: 100% + + .workflow-id + color: $grey2 + background: $white + position: absolute + right: $sizeS // Group of Config Options/Controls fieldset @@ -247,9 +262,12 @@ $fontWeightBoldPlus = 700 p font-size: $fontSizeS - .small-info + p.small-info font-size: $fontSizeS font-style: italic + + span.small-info + font-size: $fontSizeS .col-1 grid-column: 1 @@ -290,10 +308,6 @@ $fontWeightBoldPlus = 700 font-weight: $fontWeightBoldPlus text-transform: uppercase - .workflow-id - color: $grey4 - font-size: $fontSizeS - .status-active, .status-inactive font-size: $fontSizeS margin-left: $sizeS @@ -310,6 +324,17 @@ $fontWeightBoldPlus = 700 border-radius: $sizeXS color: $grey2 + .no-tasks-notice + padding: $sizeL + text-align: center + + .icon + color: $yellow + font-size: $sizeXXL + + p + color: $grey1 + dialog border: 1px solid $grey1 border-radius: $sizeS @@ -526,6 +551,7 @@ $fontWeightBoldPlus = 700 &.next-is-submit background: $white border: 2px solid $yellow + color: $black &.simple-next-controls display: flex @@ -569,6 +595,14 @@ $fontWeightBoldPlus = 700 padding: $sizeS $sizeM text-align: center + .fake-submit + background: $white + border-radius: $sizeM + border: 2px solid $yellow + color: $black + font-size: $fontSizeXS + padding: $sizeS $sizeM + .fake-text-input background: $white border: 1px solid $grey1