diff --git a/@empirica-mocks/core/mocks.js b/@empirica-mocks/core/mocks.js index c3fdd61..48412c3 100644 --- a/@empirica-mocks/core/mocks.js +++ b/@empirica-mocks/core/mocks.js @@ -79,6 +79,8 @@ export function useStage() { setTreatment, templatesMap, setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex } = useContext(StageContext) // const stage1 = useContext(StageContext); // console.log("useStageMock", stage1) @@ -91,21 +93,25 @@ export function useStage() { //const treatmentString = localStorage.getItem("treatment"); //const treatment = JSON.parse(treatmentString); if (varName === "elements") { - var elements = treatment.treatments[0]?.gameStages[currentStageIndex]?.elements - elements = elements.flatMap((element) => { - if (element.template) { - return templatesMap.get(element.template); - } - return [element]; - }); + let elements = treatment.treatments[selectedTreatmentIndex]?.gameStages[currentStageIndex]?.elements; + if (Array.isArray(elements)) { + elements = elements.flatMap((element) => { + if (element.template) { + return templatesMap.get(element.template); + } + return [element]; + }); + } else { + elements = []; + } console.log("revised elements", elements) return elements; } else if (varName === "discussion") { - return treatment.treatments[0]?.gameStages[currentStageIndex]?.discussion + return treatment.treatments[selectedTreatmentIndex]?.gameStages[currentStageIndex]?.discussion; } else if (varName === "name") { - return treatment.treatments[0]?.gameStages[currentStageIndex]?.name + return treatment.treatments[selectedTreatmentIndex]?.gameStages[currentStageIndex]?.name; } else if (varName === "index") { - return currentStageIndex + return currentStageIndex; } }, diff --git a/cypress/e2e/filterTreatment.cy.ts b/cypress/e2e/filterTreatment.cy.ts index 80f719c..9e1713d 100644 --- a/cypress/e2e/filterTreatment.cy.ts +++ b/cypress/e2e/filterTreatment.cy.ts @@ -1,79 +1,181 @@ -describe('timeline filter treatment', () => { +describe('timeline filter stages and treatments', () => { beforeEach(() => { // load initial treatment file - let yamltreatment = "treatments: \n - name: filter_timeline_test\n playerCount: 1\ngameStages: []"; + let yamltreatment = "treatments:\n- name: treatment_one\n playerCount: 1\ngameStages: []\n"; cy.viewport(2000, 1000, { log: false }); cy.visit('http://localhost:3000/editor'); cy.typeInCodeEditor(`{ctrl+a}{del}${yamltreatment}`) // equivalent to clear() in cypress + cy.typeInCodeEditor(`{backspace}`); + let secondTreatment = "- name: treatment_two\n playerCount: 1\ngameStages: []\n"; + cy.typeInCodeEditor(`${secondTreatment}`); - // verify initial text in editor - - // text values from monaco-editor will include line numbers and no line breaks - // the yamltreatment variable has no line numbers and line breaks - // so right now comparison is only on the treatmentName - cy.containsInCodeEditor('filter_timeline_test') + // save treatment file + cy.containsInCodeEditor('treatment_one') cy.get('[data-cy="yaml-save"]').realClick() - // first stage + // ensure that the default treatment (treatment 0) is selected when the page is loaded + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '0'); + + // treatment one, first stage cy.get('[data-cy="add-stage-button"]').click(); cy.get('[data-cy="edit-stage-name-new"]').type("Role Assignment and General Instructions"); cy.get('[data-cy="edit-stage-duration-new"]').type("{backspace}300"); cy.get('[data-cy="edit-stage-save-new"]').click(); - // second stage + // treatment one, second stage cy.get('[data-cy="add-stage-button"]').click(); cy.get('[data-cy="edit-stage-name-new"]').type("Main Discussion"); cy.get('[data-cy="edit-stage-duration-new"]').type("{backspace}200"); cy.get('[data-cy="edit-stage-save-new"]').click(); - // third stage + // switch to the second treatment + cy.get('[data-cy="treatments-dropdown"] select').select('1'); + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '1'); + + // treatment two, first stage + cy.get('[data-cy="add-stage-button"]').click(); + cy.get('[data-cy="edit-stage-name-new"]').type("test"); + cy.get('[data-cy="edit-stage-duration-new"]').type("{backspace}200"); + cy.get('[data-cy="edit-stage-save-new"]').click(); + + // treatment two, second stage cy.get('[data-cy="add-stage-button"]').click(); - cy.get('[data-cy="edit-stage-name-new"]').type("Post Discussion Survey"); + cy.get('[data-cy="edit-stage-name-new"]').type("test2"); cy.get('[data-cy="edit-stage-duration-new"]').type("{backspace}200"); cy.get('[data-cy="edit-stage-save-new"]').click(); - // all sections should initially be visible + // verify all stages in second treatment are visible + cy.get('[data-cy="stage-0"]').contains("test").should("be.visible"); + cy.get('[data-cy="stage-1"]').contains("test2").should("be.visible"); + cy.get('[data-cy^="stage-"]').should('have.length', 2); + + // switch back to first treatment + cy.get('[data-cy="treatments-dropdown"] select').select('0'); + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '0'); + + // all stages in treatment 0 should initially be visible cy.get('[data-cy="stage-0"]').contains("Role Assignment and General Instructions").should("be.visible"); cy.get('[data-cy="stage-1"]').contains("Main Discussion").should("be.visible"); - cy.get('[data-cy="stage-2"]').contains("Post Discussion Survey").should("be.visible"); - cy.get('[data-cy^="stage-"]').should('have.length', 3); + cy.get('[data-cy^="stage-"]').should('have.length', 2); }); - it('should dynamically populate filter options based on treatment file', () => { - cy.get('[data-cy="filter-dropdown"] select').within(() => { - cy.get('option').should('have.length', 4); + it('should dynamically populate stage options in stages dropdown', () => { + cy.get('[data-cy="stages-dropdown"] select').within(() => { + cy.get('option').should('have.length', 3); cy.get('option').eq(0).should('have.text', 'All Stages'); cy.get('option').eq(1).should('have.text', 'Role Assignment and General Instructions'); cy.get('option').eq(2).should('have.text', 'Main Discussion'); - cy.get('option').eq(3).should('have.text', 'Post Discussion Survey'); }); }) - it('allows filtering stages by criteria', () => { + it('should dynamically populate treatment options in treatments dropdown', () => { + cy.get('[data-cy="treatments-dropdown"] select').within(() => { + cy.get('option').should('have.length', 2); + cy.get('option').eq(0).should('have.text', 'treatment_one'); + cy.get('option').eq(1).should('have.text', 'treatment_two'); + }); + }) + + it('allows filtering by stage name', () => { // default option - cy.get('[data-cy="filter-dropdown"] select').should('have.value', 'all'); + cy.get('[data-cy="stages-dropdown"] select').should('have.value', 'all'); - // select one section and ensure no other sections are visible - cy.get('[data-cy="filter-dropdown"] select').select('Main Discussion'); - cy.get('[data-cy="filter-dropdown"] select').should('have.value', 'Main Discussion'); + // select one stage and ensure no other stages are visible (assuming all stages have different names) + cy.get('[data-cy="stages-dropdown"] select').select('Main Discussion'); + cy.get('[data-cy="stages-dropdown"] select').should('have.value', 'Main Discussion'); cy.get('[data-cy="stage-0"]').should('not.exist'); - cy.get('[data-cy="stage-2"]').should('not.exist'); cy.get('[data-cy="stage-1"]').contains("Main Discussion").should("be.visible"); cy.get('[data-cy^="stage-"]').should('have.length', 1); + // verify that currentStageIndex in context and currentStageName in localStorage are correct after stage change + cy.window().its('stageContext').then((stageContext) => { + expect(stageContext.currentStageIndex).to.equal(1); + }); + cy.window().then((win) => { + expect(win.localStorage.getItem('currentStageName')).to.equal('Main Discussion'); + }); + // reset to all stages when filter option is cleared - cy.get('[data-cy="filter-dropdown"] select').select('all'); - cy.get('[data-cy="filter-dropdown"] select').should('have.value', 'all'); + cy.get('[data-cy="stages-dropdown"] select').select('all'); + cy.get('[data-cy="stages-dropdown"] select').should('have.value', 'all'); + cy.get('[data-cy="stage-0"]').contains("Role Assignment and General Instructions").should("be.visible"); + cy.get('[data-cy="stage-1"]').contains("Main Discussion").should("be.visible"); + cy.get('[data-cy^="stage-"]').should('have.length', 2); + + // verify that currentStageIndex and currentStageName are reset after clearing filter + cy.window().its('stageContext').then((stageContext) => { + expect(stageContext.currentStageIndex).to.equal(0); + }); + cy.window().then((win) => { + expect(win.localStorage.getItem('currentStageName')).to.equal('all'); + }); + }) + + it('allows filtering by treatment name', () => { + // Select the second treatment + cy.get('[data-cy="treatments-dropdown"] select').select('1'); + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '1'); + + // Verify that selectedTreatmentIndex in both context and localStorage is correct + cy.window().its('stageContext').then((stageContext) => { + expect(stageContext.selectedTreatmentIndex).to.equal(1); + }); + cy.window().then((win) => { + expect(win.localStorage.getItem('selectedTreatmentIndex')).to.equal('1'); + }); + + // Verify that the stages for the second treatment are displayed + cy.get('[data-cy="stage-0"]').contains("test").should("be.visible"); + cy.get('[data-cy="stage-1"]').contains("test2").should("be.visible"); + cy.get('[data-cy^="stage-"]').should('have.length', 2); + + // Verify that selecting stages works in second treatment + cy.get('[data-cy="stages-dropdown"] select').select('test2'); + cy.get('[data-cy="stages-dropdown"] select').should('have.value', 'test2'); + cy.get('[data-cy="stage-0"]').should('not.exist'); + cy.get('[data-cy="stage-1"]').contains("test").should("be.visible"); + cy.get('[data-cy^="stage-"]').should('have.length', 1); + + // Verify that currentStageIndex in context and currentStageName in localStorage are correct after stage change + cy.window().its('stageContext').then((stageContext) => { + expect(stageContext.currentStageIndex).to.equal(1); + }); + cy.window().then((win) => { + expect(win.localStorage.getItem('currentStageName')).to.equal('test2'); + }); + + // Switch back to the first treatment + cy.get('[data-cy="treatments-dropdown"] select').select('0'); + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '0'); + + // Verify that the selectedTreatmentIndex in context and localStorage is correct + cy.window().its('stageContext').then((stageContext) => { + expect(stageContext.selectedTreatmentIndex).to.equal(0); + }); + cy.window().then((win) => { + expect(win.localStorage.getItem('selectedTreatmentIndex')).to.equal('0'); + }); + + // Verify that currentStageIndex and currentStageName are reset after switching treatments + cy.window().its('stageContext').then((stageContext) => { + expect(stageContext.currentStageIndex).to.equal(0); + }); + cy.window().then((win) => { + expect(win.localStorage.getItem('currentStageName')).to.equal('all'); + }); + + // Verify that the stages for the first treatment are displayed cy.get('[data-cy="stage-0"]').contains("Role Assignment and General Instructions").should("be.visible"); cy.get('[data-cy="stage-1"]').contains("Main Discussion").should("be.visible"); - cy.get('[data-cy="stage-2"]').contains("Post Discussion Survey").should("be.visible"); - cy.get('[data-cy^="stage-"]').should('have.length', 3); + cy.get('[data-cy^="stage-"]').should('have.length', 2); }) - it('should persist selected filter after page reload', () => { - cy.get('[data-cy="filter-dropdown"] select').should('have.value', 'all'); + it('should persist selected stage after page reload', () => { + // Initial stage + cy.get('[data-cy="stages-dropdown"] select').should('have.value', 'all'); - cy.get('[data-cy="filter-dropdown"] select').select('Main Discussion'); + // Select a new stage + cy.get('[data-cy="stages-dropdown"] select').select('Main Discussion'); cy.get('[data-cy="stage-0"]').should('not.exist'); cy.get('[data-cy="stage-2"]').should('not.exist'); cy.get('[data-cy="stage-1"]').contains("Main Discussion").should("be.visible"); @@ -81,15 +183,58 @@ describe('timeline filter treatment', () => { cy.reload(); // reload page - cy.get('[data-cy="filter-dropdown"] select').select('Main Discussion'); - cy.get('[data-cy="filter-dropdown"] select').should('have.value', 'Main Discussion'); + // Verify that stage selection persists + cy.get('[data-cy="stages-dropdown"] select').should('have.value', 'Main Discussion'); cy.get('[data-cy="stage-0"]').should('not.exist'); cy.get('[data-cy="stage-2"]').should('not.exist'); cy.get('[data-cy="stage-1"]').contains("Main Discussion").should("be.visible"); cy.get('[data-cy^="stage-"]').should('have.length', 1); }) - // add test for same named stages + it('should persist selected treatment after page reload', () => { + // Initial treatment + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '0'); + + // Select a new treatment and verify timeline contents + cy.get('[data-cy="treatments-dropdown"] select').select('1'); + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '1'); + cy.get('[data-cy="stage-0"]').contains("test").should("be.visible"); + cy.get('[data-cy="stage-1"]').contains("test2").should("be.visible"); + cy.get('[data-cy^="stage-"]').should('have.length', 2); + + cy.reload(); // reload page + + // Verify that timeline persists + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '1'); + cy.get('[data-cy="stage-0"]').contains("test").should("be.visible"); + cy.get('[data-cy="stage-1"]').contains("test2").should("be.visible"); + cy.get('[data-cy^="stage-"]').should('have.length', 2); + }) + + it('should show a useful message when stages array is empty', () => { + // Add a treatment with an empty stages array + cy.typeInCodeEditor(`\n`); + cy.typeInCodeEditor(` `); + cy.typeInCodeEditor(` `); + let emptyStagesTreatment = "- name: treatment_three\n playerCount: 1\ngameStages: []\n"; + cy.typeInCodeEditor(`${emptyStagesTreatment}`); + cy.get('[data-cy="yaml-save"]').realClick(); + + // Select the treatment with empty stages + cy.get('[data-cy="treatments-dropdown"] select').select('2'); + cy.get('[data-cy="treatments-dropdown"] select').should('have.value', '2'); + + // Verify the message + cy.get('[data-cy="stages-dropdown"] select').should('contain', 'Nothing available'); + }) + + it('should show a useful message when treatments array is empty', () => { + // Load an empty treatments array + let emptyTreatments = "treatments: []\n"; + cy.typeInCodeEditor(`{ctrl+a}{del}${emptyTreatments}`); + cy.get('[data-cy="yaml-save"]').realClick(); - // make sure stageIndex is updated + // Verify the messages + cy.get('[data-cy="treatments-dropdown"] select').should('contain', 'Nothing available'); + }) }) \ No newline at end of file diff --git a/cypress/e2e/reorder.cy.ts b/cypress/e2e/reorder.cy.ts index 9634df1..321c3df 100644 --- a/cypress/e2e/reorder.cy.ts +++ b/cypress/e2e/reorder.cy.ts @@ -56,11 +56,11 @@ describe('timeline drag and drop', () => { cy.get('[data-cy^="element-0-"]').should('have.length', 2); // swap first element with second element - cy.get('[data-cy="element-0-0"]') - .focus() - .type(" ") // space bar selects item to move - .type("{downArrow}") // move element down one - .type(" "); // stop moving item + cy.get('[data-cy="element-0-0"]').as('dragElement'); + cy.get('@dragElement').focus(); + cy.get('@dragElement').type(" "); // space bar selects item to move + cy.get('@dragElement').type("{downArrow}"); // move element down one + cy.get('@dragElement').type(" "); // stop moving item cy.wait(1000); @@ -77,11 +77,11 @@ describe('timeline drag and drop', () => { cy.get('[data-cy^="stage-"]').should('have.length', 2); // swap first stage with second stage - cy.get('[data-cy="stage-0"]') - .focus() - .type(" ") - .type("{rightArrow}") // move stage 1 to the right - .type(" "); + cy.get('[data-cy="stage-0"]').as('dragStage'); + cy.get('@dragStage').focus(); + cy.get('@dragStage').type(" "); + cy.get('@dragStage').type("{rightArrow}"); + cy.get('@dragStage').type(" "); cy.wait(1000); @@ -99,11 +99,11 @@ describe('timeline drag and drop', () => { // try moving first element from stage 1 to stage 2 cy.wait(1000); - cy.get('[data-cy="element-0-0"]') - .focus() - .type(" ") - .type("{rightArrow}") - .type(" "); + cy.get('[data-cy="element-0-0"]').as('dragElement'); + cy.get('@dragElement').focus(); + cy.get('@dragElement').type(" "); + cy.get('@dragElement').type("{rightArrow}"); + cy.get('@dragElement').type(" "); cy.wait(1000); @@ -113,11 +113,11 @@ describe('timeline drag and drop', () => { cy.get('[data-cy^="element-0-"]').should('have.length', 2); // try moving second element outside of stage 1 - cy.get('[data-cy="element-0-1"]') - .focus() - .type(" ") - .type("{rightArrow}") - .type(" "); + cy.get('[data-cy="element-0-1"]').as('dragElement2'); + cy.get('@dragElement2').focus(); + cy.get('@dragElement2').type(" "); + cy.get('@dragElement2').type("{rightArrow}"); + cy.get('@dragElement2').type(" "); cy.wait(1000); @@ -134,11 +134,11 @@ describe('timeline drag and drop', () => { cy.get('[data-cy^="stage-"]').should('have.length', 2); // try dragging stage outside of timeline - cy.get('[data-cy="stage-0"]') - .focus() - .type(" ") - .type("{upArrow}") - .type(" "); + cy.get('[data-cy="stage-0"]').as('dragStage'); + cy.get('@dragStage').focus(); + cy.get('@dragStage').type(" "); + cy.get('@dragStage').type("{upArrow}"); + cy.get('@dragStage').type(" "); cy.wait(1000); diff --git a/deliberation-empirica b/deliberation-empirica index 43cef4d..22b49e0 160000 --- a/deliberation-empirica +++ b/deliberation-empirica @@ -1 +1 @@ -Subproject commit 43cef4d26dd8f59fc7dbc1c69cad351cc0db7f40 +Subproject commit 22b49e0bf872a758c8e6ba1fb25960e8e26c6770 diff --git a/src/app/editor/components/CodeEditor.tsx b/src/app/editor/components/CodeEditor.tsx index 9bec455..7aee9bd 100644 --- a/src/app/editor/components/CodeEditor.tsx +++ b/src/app/editor/components/CodeEditor.tsx @@ -19,18 +19,17 @@ export default function CodeEditor() { useEffect(() => { async function fetchDefaultTreatment() { var data = defaultTreatment - if (defaultTreatment) { - return // If defaultTreatment is already set, do nothing - } else { - const response = await fetch('/defaultTreatment.yaml') - const text = await response.text() - data = yaml.load(text) - setDefaultTreatment(data) - } + if (defaultTreatment) return // If defaultTreatment is already set, do nothing + + const response = await fetch('/defaultTreatment.yaml') + const text = await response.text() + data = yaml.load(text) + setDefaultTreatment(data) const storedCode = localStorage.getItem('code') || '' if (storedCode === '') { setCode(stringify(data)) + localStorage.setItem('code', stringify(data)) } else { setCode(storedCode) } diff --git a/src/app/editor/components/Dropdown.tsx b/src/app/editor/components/Dropdown.tsx new file mode 100644 index 0000000..ba96b6e --- /dev/null +++ b/src/app/editor/components/Dropdown.tsx @@ -0,0 +1,53 @@ +import React from 'react' + +interface DropdownProps { + label: string + options: string[] + value: string | number + onChange: (event: React.ChangeEvent) => void + dataCy: string +} + +const Dropdown = ({ + label, + options = [], + value, + onChange, + dataCy, +}: DropdownProps) => { + const isStageOptionsEmpty = options.length === 1 && options[0] === 'all' + + return ( +
+ + +
+ ) +} + +export default Dropdown \ No newline at end of file diff --git a/src/app/editor/components/EditElement.tsx b/src/app/editor/components/EditElement.tsx index e7d1cd6..3dbe6e8 100644 --- a/src/app/editor/components/EditElement.tsx +++ b/src/app/editor/components/EditElement.tsx @@ -22,6 +22,8 @@ export function EditElement({ editTreatment, templatesMap, setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex } = useContext(StageContext) const { @@ -33,11 +35,11 @@ export function EditElement({ } = useForm({ defaultValues: { name: - treatment?.treatments?.[0].gameStages[stageIndex]?.elements[ + treatment?.treatments?.[selectedTreatmentIndex].gameStages[stageIndex]?.elements[ elementIndex ]?.name || '', selectedOption: - treatment?.treatments?.[0].gameStages[stageIndex]?.elements[ + treatment?.treatments?.[selectedTreatmentIndex].gameStages[stageIndex]?.elements[ elementIndex ]?.type || 'Pick one', file: '', @@ -88,11 +90,11 @@ export function EditElement({ } if (elementIndex === -1) { - updatedTreatment?.treatments[0].gameStages[stageIndex]?.elements?.push( + updatedTreatment?.treatments[selectedTreatmentIndex].gameStages[stageIndex]?.elements?.push( inputs ) } else { - updatedTreatment.treatments[0].gameStages[stageIndex].elements[ + updatedTreatment.treatments[selectedTreatmentIndex].gameStages[stageIndex].elements[ elementIndex ] = inputs } @@ -106,7 +108,7 @@ export function EditElement({ ) if (confirm) { const updatedTreatment = JSON.parse(JSON.stringify(treatment)) // deep copy - updatedTreatment.treatments[0].gameStages[stageIndex].elements.splice( + updatedTreatment.treatments[selectedTreatmentIndex].gameStages[stageIndex].elements.splice( elementIndex, 1 ) // delete in place diff --git a/src/app/editor/components/EditStage.tsx b/src/app/editor/components/EditStage.tsx index 2bf075d..4e39497 100644 --- a/src/app/editor/components/EditStage.tsx +++ b/src/app/editor/components/EditStage.tsx @@ -26,6 +26,8 @@ export function EditStage({ editTreatment, templatesMap, setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex, } = useContext(StageContext) const { @@ -38,15 +40,15 @@ export function EditStage({ defaultValues: { name: stageIndex != -1 - ? treatment?.treatments?.[0].gameStages[stageIndex]?.name + ? treatment?.treatments?.[selectedTreatmentIndex].gameStages[stageIndex]?.name : '', duration: stageIndex != -1 - ? treatment?.treatments?.[0].gameStages[stageIndex]?.duration + ? treatment?.treatments?.[selectedTreatmentIndex].gameStages[stageIndex]?.duration : 0, elements: stageIndex != -1 - ? treatment?.treatments?.[0].gameStages[stageIndex]?.elements + ? treatment?.treatments?.[selectedTreatmentIndex].gameStages[stageIndex]?.elements : [], // desc: "", // discussion: { @@ -66,7 +68,7 @@ export function EditStage({ name: watch('name'), duration: watch('duration'), elements: - treatment?.treatments?.[0].gameStages[stageIndex]?.elements || [], + treatment?.treatments?.[selectedTreatmentIndex].gameStages[stageIndex]?.elements || [], // discussion: undefined, // desc: watch('desc'), } @@ -93,11 +95,11 @@ export function EditStage({ if (stageIndex === -1) { // create new stage - updatedTreatment?.treatments?.[0].gameStages?.push(inputs) + updatedTreatment?.treatments?.[selectedTreatmentIndex].gameStages?.push(inputs) } else { // modify existing stage - updatedTreatment.treatments[0].gameStages[stageIndex].name = watch('name') - updatedTreatment.treatments[0].gameStages[stageIndex].duration = + updatedTreatment.treatments[selectedTreatmentIndex].gameStages[stageIndex].name = watch('name') + updatedTreatment.treatments[selectedTreatmentIndex].gameStages[stageIndex].duration = watch('duration') // todo: add discussion component } @@ -110,7 +112,7 @@ export function EditStage({ ) if (confirm) { const updatedTreatment = JSON.parse(JSON.stringify(treatment)) // deep copy - updatedTreatment.treatments[0].gameStages.splice(stageIndex, 1) // delete in place + updatedTreatment.treatments[selectedTreatmentIndex].gameStages.splice(stageIndex, 1) // delete in place editTreatment(updatedTreatment) } } diff --git a/src/app/editor/components/ElementCard.tsx b/src/app/editor/components/ElementCard.tsx index 371258a..4a56db9 100644 --- a/src/app/editor/components/ElementCard.tsx +++ b/src/app/editor/components/ElementCard.tsx @@ -34,6 +34,8 @@ export function ElementCard({ setTreatment, templatesMap, setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex, } = useContext(StageContext) const editModalId = `modal-stage${stageIndex}-element-${elementIndex}` diff --git a/src/app/editor/components/RenderPanel.tsx b/src/app/editor/components/RenderPanel.tsx index b1b2a48..832f537 100644 --- a/src/app/editor/components/RenderPanel.tsx +++ b/src/app/editor/components/RenderPanel.tsx @@ -45,6 +45,10 @@ export function RenderPanel() { treatment, setTreatment, player, + templatesMap, + setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex, } = useContext(StageContext) console.log('RenderPanel.tsx current stage index', currentStageIndex) @@ -79,12 +83,12 @@ export function RenderPanel() { value={time + ' s'} setValue={setElapsed} maxValue={ - treatment.treatments?.[0].gameStages[currentStageIndex] + treatment.treatments?.[selectedTreatmentIndex].gameStages[currentStageIndex] ?.duration ?? 0 } /> {/* need to retrieve stage duration from treatment */} diff --git a/src/app/editor/components/StageCard.tsx b/src/app/editor/components/StageCard.tsx index 262b734..2e527fa 100644 --- a/src/app/editor/components/StageCard.tsx +++ b/src/app/editor/components/StageCard.tsx @@ -41,6 +41,8 @@ export function StageCard({ editTreatment, templatesMap, setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex, } = useContext(StageContext) const addElementOptions = [ @@ -98,7 +100,7 @@ export function StageCard({ // update treatment const updatedTreatment = JSON.parse(JSON.stringify(treatment)) - updatedTreatment.treatments[0].gameStages[stageIndex].elements = + updatedTreatment.treatments[selectedTreatmentIndex].gameStages[stageIndex].elements = updatedElements editTreatment(updatedTreatment) } @@ -154,8 +156,8 @@ export function StageCard({ {elements !== undefined && elements.map((element, index) => ( {(provided) => ( diff --git a/src/app/editor/components/Timeline.tsx b/src/app/editor/components/Timeline.tsx index 02ceca6..e203253 100644 --- a/src/app/editor/components/Timeline.tsx +++ b/src/app/editor/components/Timeline.tsx @@ -3,15 +3,11 @@ import React, { useEffect, useState, useContext, useCallback } from 'react' import { parse } from 'yaml' import { StageCard } from './StageCard' import TimelineTools from './TimelineTools' -import { stringify } from 'yaml' import { Modal } from './Modal' import { EditStage } from './EditStage' -import { - treatmentSchema, - TreatmentType, -} from '../../../../deliberation-empirica/server/src/preFlight/validateTreatmentFile' import { StageContext } from '@/editor/stageContext' import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd' +import Dropdown from './Dropdown' export default function Timeline({ setRenderPanelStage, @@ -19,8 +15,10 @@ export default function Timeline({ setRenderPanelStage: any }) { const [scale, setScale] = useState(1) // pixels per second - const [filterCriteria, setFilterCriteria] = useState('all') // state for selected filter - const [filterOptions, setFilterOptions] = useState([]) // state to store filter options (stage names) + const [stageOptions, setStageOptions] = useState([]) + const [treatmentOptions, setTreatmentOptions] = useState([]) + const [introSequenceOptions, setIntroSequenceOptions] = useState([]) + const [currentStageName, setCurrentStageName] = useState('all') // filter for stage names const { currentStageIndex, @@ -32,6 +30,10 @@ export default function Timeline({ editTreatment, templatesMap, setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex, + selectedIntroSequenceIndex, + setSelectedIntroSequenceIndex, } = useContext(StageContext) useEffect(() => { @@ -41,59 +43,118 @@ export default function Timeline({ const parsedCode = parse(codeStr) setTreatment(parsedCode) - const storedFilter = localStorage.getItem('filterCriteria') || 'all' // persist filter option - setFilterCriteria(storedFilter) + const storedFilter = localStorage.getItem('currentStageName') || 'all' + setCurrentStageName(storedFilter) - // generate dynamic selector options - if (parsedCode && parsedCode.treatments?.[0].gameStages) { - const stageNames = parsedCode.treatments[0].gameStages.map( - (stage: any) => stage.name - ) - setFilterOptions(['all', ...stageNames]) // 'all' as default - } - if (parsedCode?.templates) { - const templates = new Map() - parsedCode.templates.forEach((template: any) => { - templates.set(template.templateName, template.templateContent) - }) - setTemplatesMap(templates) - } + const storedTreatmentIndex = + parseInt(localStorage.getItem('selectedTreatmentIndex') || '0', 10) + setSelectedTreatmentIndex(storedTreatmentIndex) + + const storedIntroSequenceIndex = + parseInt(localStorage.getItem('selectedIntroSequenceIndex') || '0', 10) + setSelectedIntroSequenceIndex(storedIntroSequenceIndex) } - }, [setTreatment]) + }, [setTreatment, setSelectedTreatmentIndex, setSelectedIntroSequenceIndex]) + // think about using useMemo here const filterStages = useCallback( (treatment: any) => { - // think about using useMemo here - if (!treatment) return [] + if (!treatment || !treatment.gameStages) return [] const filteredStages = treatment.gameStages .map((stage: any, originalIndex: number) => ({ stage, originalIndex })) .filter(({ stage }: { stage: any }) => - filterCriteria === 'all' ? true : stage.name === filterCriteria + currentStageName === 'all' ? true : stage.name === currentStageName ) - console.log('Filtered Stages:', filteredStages) - return filteredStages }, - [filterCriteria] + [currentStageName] ) - // change stage index whenever filterCriteria changes useEffect(() => { - const filteredStages = filterStages(treatment?.treatments?.[0]) - if (filteredStages.length > 0) { - setCurrentStageIndex(filteredStages[0].originalIndex) + if (treatment) { + // Set treatment options + if (treatment.treatments) { + const treatmentNames = treatment.treatments.map( + (treatment: any) => treatment.name + ) + setTreatmentOptions(treatmentNames) + + const selectedTreatment = treatment.treatments[selectedTreatmentIndex] + const filteredStages = filterStages(selectedTreatment) + + if (filteredStages.length > 0) { + setCurrentStageIndex(filteredStages[0].originalIndex) // default to first filtered stage, in case some stages have the same name + } + + const stageNames = + selectedTreatment?.gameStages?.map((stage: any) => stage.name) || [] + setStageOptions(['all', ...stageNames]) + } + + // Set intro sequence options + if (treatment.introSequences) { + const sequenceNames = treatment.introSequences.map( + (sequence: any, index: number) => + sequence.fields?.sequenceName || `Sequence ${index + 1}` + ) + setIntroSequenceOptions(sequenceNames) + } + + // Set templates + if (treatment.templates) { + const templates = new Map() + treatment.templates.forEach((template: any) => { + templates.set(template.templateName, template.templateContent) + }) + setTemplatesMap(templates) + } } - }, [filterCriteria, treatment, setCurrentStageIndex, filterStages]) + }, [ + treatment, + selectedTreatmentIndex, + selectedIntroSequenceIndex, + currentStageName, + setCurrentStageIndex, + setTemplatesMap, + filterStages, + ]) if (!treatment) { return null } - function handleFilterChange(event: any) { - setFilterCriteria(event.target.value) - localStorage.setItem('filterCriteria', event.target.value) + function handleStageNameChange(event: any) { + const selectedStageName = event.target.value + setCurrentStageName(selectedStageName) + localStorage.setItem('currentStageName', selectedStageName) + + if (selectedStageName === 'all') { + setCurrentStageIndex(0) + } else { + const selectedTreatment = treatment.treatments[selectedTreatmentIndex] + const stageIndex = selectedTreatment.gameStages.findIndex( + (stage: any) => stage.name === selectedStageName + ) + setCurrentStageIndex(stageIndex) + } + } + + function handleTreatmentChange(event: React.ChangeEvent) { + const newIndex = parseInt(event.target.value, 10) + setCurrentStageIndex(0) + setSelectedTreatmentIndex(newIndex) + localStorage.setItem('selectedTreatmentIndex', newIndex.toString()) + setCurrentStageName('all') + localStorage.setItem('currentStageName', 'all') + } + + function handleIntroSequenceChange(event: React.ChangeEvent) { + const newIndex = parseInt(event.target.value, 10) + setSelectedIntroSequenceIndex(newIndex) + localStorage.setItem('selectedIntroSequenceIndex', newIndex.toString()) + setCurrentStageName('all') } // drag and drop handler @@ -105,52 +166,51 @@ export default function Timeline({ const sourceIndex = source.index const destIndex = destination.index - const updatedStages = Array.from(treatment.treatments[0].gameStages) + const updatedStages = Array.from( + treatment.treatments[selectedTreatmentIndex].gameStages + ) const [removed] = updatedStages.splice(sourceIndex, 1) updatedStages.splice(destIndex, 0, removed) // update treatment const updatedTreatment = JSON.parse(JSON.stringify(treatment)) // deep copy - updatedTreatment.treatments[0].gameStages = updatedStages + updatedTreatment.treatments[selectedTreatmentIndex].gameStages = + updatedStages editTreatment(updatedTreatment) } console.log('treatment', treatment) console.log('templatesMap', templatesMap) - //const parsedCode = ""; - - // TODO: add a page before this that lets the researcher select what treatment to work on - - // if we pass in a 'list' in our yaml (which we do when the treatments are in a list) then we take the first component of the treatment - - const addStageOptions = [ - { question: 'Name', responseType: 'text' }, - { question: 'Duration', responseType: 'text' }, - { question: 'Discussion', responseType: 'text' }, - ] - return (
- {/* select section dropdown */} -
- - + {/* filter dropdowns */} +
+ + + + +
- {filterStages(treatment?.treatments?.[0])?.map( - (obj: any, index: any) => ( - - {(provided) => ( -
- -
- )} -
- ) - )} + {filterStages( + treatment?.treatments?.[selectedTreatmentIndex] + )?.map((obj: any, index: any) => ( + + {(provided) => ( +
+ +
+ )} +
+ ))} {provided.placeholder}
)} @@ -221,4 +281,4 @@ export default function Timeline({
) -} +} \ No newline at end of file diff --git a/src/app/editor/stageContext.jsx b/src/app/editor/stageContext.jsx index 1ee9225..2ef8631 100644 --- a/src/app/editor/stageContext.jsx +++ b/src/app/editor/stageContext.jsx @@ -1,5 +1,5 @@ //import { set } from 'node_modules/cypress/types/lodash'; -import { createContext, useState } from 'react' +import { createContext, useState, useCallback, useMemo, useEffect } from 'react' import { stringify } from 'yaml' import { useGame, @@ -7,7 +7,7 @@ import { usePlayer, useRound, useStageTimer, -} from "@empirica/core/player/classic/react"; +} from '@empirica/core/player/classic/react' // export const StageContext = createContext({ // currentStageIndex: "default", @@ -21,16 +21,19 @@ const StageProvider = ({ children }) => { const [elapsed, setElapsed] = useState(0) const [treatment, setTreatment] = useState(null) const [templatesMap, setTemplatesMap] = useState(new Map()) + const [selectedTreatmentIndex, setSelectedTreatmentIndex] = useState(0) + const [selectedIntroSequenceIndex, setSelectedIntroSequenceIndex] = + useState(0) const player = usePlayer() // for updating code editor, requires reload - function editTreatment(newTreatment) { + const editTreatment = useCallback((newTreatment) => { setTreatment(newTreatment) localStorage.setItem('code', stringify(newTreatment)) window.location.reload() - } + }, [setTreatment]) - const contextValue = { + const contextValue = useMemo(() => ({ currentStageIndex, setCurrentStageIndex, elapsed, @@ -41,7 +44,31 @@ const StageProvider = ({ children }) => { player, templatesMap, setTemplatesMap, - } + selectedTreatmentIndex, + setSelectedTreatmentIndex, + selectedIntroSequenceIndex, + setSelectedIntroSequenceIndex, + }), [ + currentStageIndex, + setCurrentStageIndex, + elapsed, + setElapsed, + treatment, + setTreatment, + editTreatment, + player, + templatesMap, + setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex, + selectedIntroSequenceIndex, + setSelectedIntroSequenceIndex, + ]) + + // expose context values to the window object + useEffect(() => { + window.stageContext = contextValue + }, [contextValue]) return (