Skip to content

Commit

Permalink
Pages Editor: implement Associated Subject Sets & fetch more data (zo…
Browse files Browse the repository at this point in the history
…oniverse#7048)

* pages-editor-pt15: prepare WorkflowSettingsPage for setting Subject Sets

* Add AssociatedSubjectSets component

* DataManager: fetch project AND workflow resources at the same time

* DataManager: fix project missing after update. Add documentation about project resource.

* TasksPage: dynamically generate Preview URL

* AssociatedSubjectSets: fetch Subject Sets. Add status-banner design

* DataManager: add status banners for fetching/error states

* PagesEditor: tweak style to add vertical padding to main container

* AssociatedSubjectSets: enable checkboxes for linking Subject Sets

* AssociatedSubjectSets: implement linking/unlinking Subject Sets

* WorkflowSettingsPage: style input-group element

* WorkflowSettingsPage: remove test sections

* PagesEditor: revert default tab to be the Tasks tab

* TasksPage: extract getPreviewEnv() into an external helper file

* AssociatedSubjectSets: fix typo
  • Loading branch information
shaunanoordin authored Mar 1, 2024
1 parent a8bcd3d commit 6742908
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 104 deletions.
37 changes: 30 additions & 7 deletions app/pages/lab-pages-editor/DataManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
Data Manager
Responsible for fetching, updating, and saving Workflow resources from/to
the Panoptes service.
Also fetches the project resource for secondary purposes. (e.g. figuring out
which Subject Sets are available for the workflow, available experimental tools,
and the workflow's preview URL.)
*/

// ESLint: don't import global React, and don't use .defaultProps.
Expand All @@ -17,9 +21,11 @@ import { WorkflowContext } from './context.js';
function DataManager({
// key: to ensure DataManager renders FRESH (with states reset) whenever workflowId changes, use <DataManager key={workflowId} ... />
children = null,
projectId = '',
workflowId = ''
}) {
const [apiData, setApiData] = useState({
project: null,
workflow: null,
status: 'ready'
});
Expand All @@ -31,33 +37,42 @@ function DataManager({
// Also see general pattern notes:
// https://react.dev/reference/react/useEffect#fetching-data-with-effects
useEffect(() => {
async function fetchWorkflow() {
async function fetchProjectAndWorkflow() {
try {
setApiData({
project: null,
workflow: null,
status: 'fetching'
});

const wf = await apiClient.type('workflows').get(workflowId);
const [ proj, wf ] = await Promise.all([
apiClient.type('projects').get(projectId),
apiClient.type('workflows').get(workflowId)
]);
if (!proj) throw new Error('No project');
if (!wf) throw new Error('No workflow');

setApiData({
project: proj,
workflow: wf,
status: 'ready'
});

} catch (err) {
console.error('DataManager: ', err);
setApiData({
project: null,
workflow: null,
status: 'error'
});
}
}

fetchWorkflow();
fetchProjectAndWorkflow();
}, [workflowId]);

// Listen for workflow changes, and update a counter to prompt useMemo to update the context.
// NOTE: we're not listening for *project* changes, as the DataManager isn't meant to update that resource.
useEffect(() => {
const wf = apiData.workflow;
function onWorkflowChange() {
Expand All @@ -84,19 +99,21 @@ function DataManager({

const newWorkflow = await wf.update(data).save();

setApiData({
setApiData((prevState) => ({
...prevState,
workflow: newWorkflow, // Note: newWorkflow is actually the same as the old wf, so useMemo will have to listen to status changing instead.
status: 'ready'
});
}));

return newWorkflow;
}

return {
project: apiData.project,
workflow: apiData.workflow,
update
};
}, [apiData.workflow, updateCounter]);
}, [apiData.project, apiData.workflow, updateCounter]);

if (!workflowId) return (<div>ERROR: no Workflow ID specified</div>);
// if (!workflow) return null
Expand All @@ -105,7 +122,12 @@ function DataManager({
<WorkflowContext.Provider
value={contextData}
>
<div>{apiData.status}</div>
{apiData.status === 'fetching' && (
<div className="status-banner fetching">Fetching data...</div>
)}
{apiData.status === 'error' && (
<div className="status-banner error">ERROR: could not fetch data</div>
)}
{children}
</WorkflowContext.Provider>
);
Expand All @@ -116,6 +138,7 @@ DataManager.propTypes = {
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
projectId: PropTypes.string,
workflowId: PropTypes.string
};

Expand Down
3 changes: 2 additions & 1 deletion app/pages/lab-pages-editor/PagesEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import PropTypes from 'prop-types';
import DataManager from './DataManager.jsx';
import WorkflowHeader from './components/WorkflowHeader.jsx';
import TasksPage from './components/TasksPage';
import WorkflowSettingsPage from './components/WorkflowSettingsPage.jsx';
import WorkflowSettingsPage from './components/WorkflowSettingsPage';
import strings from './strings.json';

function PagesEditor({ params }) {
Expand All @@ -37,6 +37,7 @@ function PagesEditor({ params }) {
<div className="lab-pages-editor">
<DataManager
key={workflowId || '-'} //
projectId={projectId}
workflowId={workflowId}
>
<WorkflowHeader
Expand Down
6 changes: 4 additions & 2 deletions app/pages/lab-pages-editor/components/TasksPage/TasksPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 getPreviewEnv from '../../helpers/getPreviewEnv.js';
// import strings from '../../strings.json'; // TODO: move all text into strings

import EditStepDialog from './components/EditStepDialog';
Expand All @@ -15,7 +16,7 @@ import StepItem from './components/StepItem';
import ExternalLinkIcon from '../../icons/ExternalLinkIcon.jsx';

export default function TasksPage() {
const { workflow, update } = useWorkflowContext();
const { project, workflow, update } = useWorkflowContext();
const editStepDialog = useRef(null);
const newTaskDialog = useRef(null);
const [ activeStepIndex, setActiveStepIndex ] = useState(-1); // Tracks which Step is being edited.
Expand Down Expand Up @@ -140,7 +141,8 @@ export default function TasksPage() {
update({ tasks: newTasks });
}

const previewUrl = 'https://frontend.preview.zooniverse.org/projects/darkeshard/example-1982/classify/workflow/3711?env=staging';
const previewEnv = getPreviewEnv();
const previewUrl = `https://frontend.preview.zooniverse.org/projects/${project?.slug}/classify/workflow/${workflow?.id}${previewEnv}`;
if (!workflow) return null;

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useWorkflowContext } from '../context.js';
import strings from '../strings.json'; // TODO: move all text into strings
import { useWorkflowContext } from '../../context.js';
import AssociatedSubjectSets from './components/AssociatedSubjectSets.jsx';

export default function WorkflowSettingsPage() {
const { workflow, update } = useWorkflowContext();
const { workflow, update, project } = useWorkflowContext();

function onSubmit(e) {
e.preventDefault();
Expand Down Expand Up @@ -55,7 +55,7 @@ export default function WorkflowSettingsPage() {
<fieldset>
<legend>Associated Subject Sets</legend>
<p>Choose the set of subjects you want to use for this workflow. Add subject sets in the Subject Sets tab.</p>
<p>TODO</p>
<AssociatedSubjectSets project={project} workflow={workflow} />
</fieldset>

<fieldset>
Expand Down Expand Up @@ -147,99 +147,14 @@ export default function WorkflowSettingsPage() {

<hr />

<fieldset>
<fieldset className="disabled">
<legend>Classification Tools</legend>
<p>TEST: HTML and styling for checkbox input</p>
<label
htmlFor="placeholder-1"
className="flex-row align-start spacing-bottom-XS"
>
<input
type="checkbox"
id="placeholder-1"
name="placeholder-1"
onChange={testUpdate}
/>
<span>Placeholder 1</span>
</label>
<label
htmlFor="placeholder-2"
className="flex-row align-start spacing-bottom-XS"
>
<input
defaultChecked={true}
type="checkbox"
id="placeholder-2"
name="placeholder-2"
onChange={testUpdate}
/>
<span>
Placeholder 2 -
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel tellus quam.
</span>
</label>
<label
htmlFor="placeholder-3"
className="flex-row align-start spacing-bottom-XS"
>
<input
type="checkbox"
id="placeholder-3"
name="placeholder-3"
onChange={testUpdate}
/>
<span>
Placeholder 3 -
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel tellus quam.
Praesent lobortis dapibus nisi, ultricies blandit quam mattis vel.
Nunc consequat sem finibus, facilisis augue quis, euismod velit.
</span>
</label>
<p>TODO</p>
</fieldset>

<fieldset>
<fieldset className="disabled">
<legend>Quicktalk</legend>
<p>TEST: HTML and styling for radio options</p>
<label
className="flex-row align-start spacing-bottom-XS"
htmlFor="placeholder-4-a"
>
<input
type="radio"
id="placeholder-4-a"
name="placeholder-4"
onChange={testUpdate}
value="Option A"
/>
<span>Placeholder 4, Option A</span>
</label>
<label
className="flex-row align-start spacing-bottom-XS"
htmlFor="placeholder-4-b"
>
<input
defaultChecked={true}
type="radio"
id="placeholder-4-b"
name="placeholder-4"
onChange={testUpdate}
value="Option B"
/>
<span>Placeholder 4, Option B</span>
</label>
<label
className="flex-row align-start spacing-bottom-XS"
htmlFor="placeholder-4-c"
>
<input
type="radio"
id="placeholder-4-c"
name="placeholder-4"
onChange={testUpdate}
value="Option C"
/>
<span>Placeholder 4, Option C</span>
</label>
<p>TODO</p>
</fieldset>

</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useState, useEffect } from 'react';

const ARBITRARY_PAGE_SIZE = 250; // If a project has more than this number of subject sets, then we need a better solution.

export default function AssociatedSubjectSets({ project, workflow }) {
const [apiData, setApiData] = useState({
subjectSets: null,
status: 'ready'
});
const [linkedSubjectSets, setLinkedSubjectSets] = useState(workflow?.links?.subject_sets || []);

useEffect(() => {
async function fetchSubjectSets() {
try {
setApiData({
subjectSets: null,
status: 'fetching'
});

const ssets = await project.get('subject_sets', { sort: '+id', page_size: ARBITRARY_PAGE_SIZE });
if (!ssets) throw new Error('No subject sets');

setApiData({
subjectSets: ssets,
status: 'ready'
});

} catch (err) {
console.error('AssociatedSubjectSets: ', err);
setApiData({
subjectSets: null,
status: 'error'
});
}
}

fetchSubjectSets();
}, [project, workflow]);

function toggleSubjectSet(e) {
const subjectSetId = e?.currentTarget?.dataset?.subjectset;
if (!subjectSetId) return;
const alreadyLinked = linkedSubjectSets.includes(subjectSetId);

if (alreadyLinked) { // If already linked, remove it.
setLinkedSubjectSets(linkedSubjectSets.filter(sset => sset !== subjectSetId));
workflow.removeLink('subject_sets', [subjectSetId]);
} else { // If not yet linked, add it.
setLinkedSubjectSets([ ...linkedSubjectSets, subjectSetId]);
workflow.addLink('subject_sets', [subjectSetId]);
}
}

if (!project || !workflow) return null;

if (apiData.status === 'fetching') {
return (<div className="status-banner fetching">Fetching Subject Sets...</div>)
}

if (apiData.status === 'error') {
return (<div className="status-banner error">ERROR: couldn't fetch Subject Sets</div>)
}

return (
<ul className="input-group">
{apiData?.subjectSets?.map((subjectSet, index) => (
<li key={`associated-subject-set-${subjectSet.id}`}>
<input
checked={!!linkedSubjectSets.includes(subjectSet.id)}
data-subjectset={subjectSet.id}
id={`associated-subject-set-${subjectSet.id}`}
onChange={toggleSubjectSet}
type="checkbox"
/>
<label htmlFor={`associated-subject-set-${subjectSet.id}`}>
{subjectSet.display_name || '???'} (#{subjectSet.id})
</label>
</li>
))}
</ul>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import WorkflowSettingsPage from './WorkflowSettingsPage.jsx';

export default WorkflowSettingsPage;
21 changes: 21 additions & 0 deletions app/pages/lab-pages-editor/helpers/getPreviewEnv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Figures out of the "Preview Workflow" link requires "?env=staging" or
"?env=production"
*/
export default function getPreviewEnv() {
const hostname = window?.location?.hostname || '';
const params = new URLSearchParams(window?.location?.search);
const explicitEnv = params.get('env');

// If an explicit ?env=... is specified, use that.
if (explicitEnv) return `?env=${explicitEnv}`;

// The following URLs default to using staging:
// https://local.zooniverse.org:3735/lab/1982/workflows/editor/3711
// https://pr-7046.pfe-preview.zooniverse.org/lab/1982/workflows/editor/3711?env=staging
if (hostname.match(/^(local|.*\.pfe-preview)\.zooniverse\.org$/ig)) {
return '?env=staging'
}

return '';
}
Loading

0 comments on commit 6742908

Please sign in to comment.