diff --git a/cypress/e2e/AsStudent/signIn.cy.js b/cypress/e2e/AsStudent/signIn.cy.js index 65f3f19852..cbc6ff9a24 100644 --- a/cypress/e2e/AsStudent/signIn.cy.js +++ b/cypress/e2e/AsStudent/signIn.cy.js @@ -1,24 +1,7 @@ describe("Student Sign-In Test", function () { const userId = "cyuserId"; - // const studentUserId = "cyStudentUserId"; const courseId = "courseid1"; - // const doenetId = "activity1id"; - // const pageDoenetId = "_page1id"; - before(() => { - cy.signin({ userId }); - cy.clearAllOfAUsersCoursesAndItems({ userId }); - // cy.clearAllOfAUsersCoursesAndItems({ userId: studentUserId }); - cy.createCourse({ userId, courseId }); - }); - beforeEach(() => { - cy.signin({ userId }); - cy.clearIndexedDB(); - cy.clearAllOfAUsersActivities({ userId }); - // cy.clearAllOfAUsersActivities({ userId: studentUserId }); - // cy.createActivity({ courseId, doenetId, parentDoenetId:courseId, pageDoenetId }); - cy.visit(`/course?tool=people&courseId=${courseId}`); - }); Cypress.on("uncaught:exception", (err, runnable) => { // Returning false here prevents Cypress from failing the test @@ -26,6 +9,12 @@ describe("Student Sign-In Test", function () { }); it("Student can sign in after being added to a course", () => { + cy.createCourse({ userId, courseId }); + cy.signin({ userId }); + cy.clearIndexedDB(); + cy.clearAllOfAUsersActivities({ userId }); + cy.visit(`/course?tool=people&courseId=${courseId}`); + const emailAddress = "scoobydoo@doenet.org"; cy.get('[data-test="First"]').type("Scooby"); cy.get('[data-test="Last"]').type("Doo"); @@ -42,8 +31,17 @@ describe("Student Sign-In Test", function () { `SELECT signInCode FROM user_device ORDER BY id DESC LIMIT 1`, ).then((result) => { const code = result[0].signInCode; - cy.get('[data-test="signinCodeInput"]').type(code); - cy.get('[data-test="signInButton"]').click(); + // cy.get('[data-test="signinCodeInput"]').type(code); + cy.get('[data-test="code-input-0"]').type(code.charAt(0)); + cy.get('[data-test="code-input-1"]').type(code.charAt(1)); + cy.get('[data-test="code-input-2"]').type(code.charAt(2)); + cy.get('[data-test="code-input-3"]').type(code.charAt(3)); + cy.get('[data-test="code-input-4"]').type(code.charAt(4)); + cy.get('[data-test="code-input-5"]').type(code.charAt(5)); + cy.get('[data-test="code-input-6"]').type(code.charAt(6)); + cy.get('[data-test="code-input-7"]').type(code.charAt(7)); + cy.get('[data-test="code-input-8"]').type(code.charAt(8)); + cy.get('[data-test="submitCodeButton"]').click(); cy.get('[data-test="My Courses"]').click(); cy.get('[data-test="Course Label"]').should( "have.text", @@ -53,4 +51,56 @@ describe("Student Sign-In Test", function () { cy.document().should("contain.text", "Welcome"); }); }); + + it("Signed out to in to out with all entry errors", () => { + const emailAddress = "scrapydoo@doenet.org"; + const firstName = "Scrapy"; + const lastName = "Doo"; + //Delete entry so we will need to enter the name + cy.task( + "queryDb", + `DELETE FROM user WHERE email='${emailAddress}'`, + ).then(() => { + cy.visit(`/`); + cy.get('[data-test="Nav to signin"]').click(); + cy.get('[data-test="email input"]').type(emailAddress); + cy.get('[data-test="sendEmailButton"]').click(); + cy.wait(500); //Wait for it to be stored in db + cy.task( + "queryDb", + `SELECT signInCode FROM user_device ORDER BY id DESC LIMIT 1`, + ).then((result) => { + const code = result[0].signInCode; + //Try no code + cy.get('[data-test="submitCodeButton"]').click(); + cy.get('[data-test="code-err"]').should('contain', "Please enter the nine digits sent to your email."); + //Try only one number + cy.get('[data-test="code-input-0"]').type(code.charAt(0)); + cy.get('[data-test="submitCodeButton"]').click(); + cy.get('[data-test="code-err"]').should('contain', "Please enter all nine digits."); + + cy.get('[data-test="code-input-0"]').type(code.charAt(0)); + cy.get('[data-test="code-input-1"]').type(code.charAt(1)); + cy.get('[data-test="code-input-2"]').type(code.charAt(2)); + cy.get('[data-test="code-input-3"]').type(code.charAt(3)); + cy.get('[data-test="code-input-4"]').type(code.charAt(4)); + cy.get('[data-test="code-input-5"]').type(code.charAt(5)); + cy.get('[data-test="code-input-6"]').type(code.charAt(6)); + cy.get('[data-test="code-input-7"]').type(code.charAt(7)); + cy.get('[data-test="code-input-8"]').type(code.charAt(8)); + cy.get('[data-test="submitCodeButton"]').click(); + //Try no names + cy.get('[data-test="submitName"]').click(); + cy.get('[data-test="firstNameError"]').should('contain', 'Please enter your first name.') + cy.get('[data-test="lastNameError"]').should('contain', 'Please enter your last name.') + + cy.get('[data-test="firstNameInput"]').type(firstName); + cy.get('[data-test="lastNameInput"]').type(lastName); + cy.get('[data-test="submitName"]').click(); + cy.get('[data-test="AvatarMenuButton"]').click(); + cy.get('[data-test="AvatarMenuSignOut"]').click(); + cy.get('[data-test="homepage button"]').click(); + }); + }); + }); }); diff --git a/cypress/e2e/DoenetML/tagSpecific/ref.cy.js b/cypress/e2e/DoenetML/tagSpecific/ref.cy.js index b9ee80cb21..51df33e407 100644 --- a/cypress/e2e/DoenetML/tagSpecific/ref.cy.js +++ b/cypress/e2e/DoenetML/tagSpecific/ref.cy.js @@ -181,7 +181,7 @@ describe("ref Tag Tests", function () { cy.get(cesc("#\\/_ref1")) .should("have.text", "a Doenet doc") .invoke("attr", "href") - .then((href) => expect(href).eq("/portfolioviewer/abcdefg")); + .then((href) => expect(href).eq("/publicOverview/abcdefg")); }); it("url with no link text", () => { diff --git a/cypress/e2e/People/people.cy.js b/cypress/e2e/People/people.cy.js index d0cba4c7d1..4e2d46bbc3 100644 --- a/cypress/e2e/People/people.cy.js +++ b/cypress/e2e/People/people.cy.js @@ -80,6 +80,7 @@ describe("People Test", function () { cy.wait(1000); peopleInCsv.forEach((person) => { + console.log("person", person) cy.task( "queryDb", `SELECT * FROM course_user WHERE externalId="${person.externalId}"`, @@ -89,6 +90,7 @@ describe("People Test", function () { "queryDb", `SELECT * FROM user WHERE userId="${res[0].userId}"`, ).then((result) => { + console.log("result", result) expect(result[0].firstName).to.equals(person.first); expect(result[0].lastName).to.equals(person.last); expect(result[0].email).to.equals(person.email); diff --git a/cypress/e2e/Portfolio/ActivityControls.cy.js b/cypress/e2e/Portfolio/ActivityControls.cy.js index 6300de079a..4aeef6645b 100644 --- a/cypress/e2e/Portfolio/ActivityControls.cy.js +++ b/cypress/e2e/Portfolio/ActivityControls.cy.js @@ -21,25 +21,21 @@ describe("Activity Controls Tests", function () { return false; }); - it("Update Label of an Activity both ways", () => { + it("Update Label of an Activity twice", () => { const label1 = "Scooby Doo"; const label2 = "Duck Tails"; cy.get('[data-test="Portfolio"]').click(); cy.get('[data-test="Add Activity"]').click(); - cy.get('[data-test="Controls Button"]').click(); cy.get('[data-test="Activity Label"]').clear().type(label1); - cy.get('[data-test="Close Settings Button"]').click(); - cy.get('[data-test="Activity Label Editable"]').contains(label1); + // cy.get('[data-test="Close Settings Button"]').click(); //Why does this not work? + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Label"]').eq(0).contains(label1); - cy.get( - '[data-test="Activity Label Editable"] [data-test="Editable Preview"]', - ).click(); - cy.get('[data-test="Activity Label Editable"] [data-test="Editable Input"]') - .type(label2) - .blur(); - cy.wait(1000); //Need the interface to be faster to not have this - cy.get('[data-test="Controls Button"]').click(); - cy.get('[data-test="Activity Label"]').should("have.value", label2); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Settings Menu Item"]').click(); + cy.get('[data-test="Activity Label"]').clear().type(label2); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Label"]').eq(0).contains(label2); }); it("Learning Outcomes", () => { @@ -48,9 +44,9 @@ describe("Activity Controls Tests", function () { const learningOutcome3 = "Five, six, pick up sticks"; const learningOutcome4 = "Seven, eight, lay them straight"; const learningOutcome5 = "Nine, ten, a big fat hen"; + const learningOutcome6 = "The End"; cy.get('[data-test="Portfolio"]').click(); cy.get('[data-test="Add Activity"]').click(); - cy.get('[data-test="Controls Button"]').click(); cy.get('[data-test="add a learning outcome button"]').click(); cy.get('[data-test="add a learning outcome button"]').click(); cy.get('[data-test="add a learning outcome button"]').click(); @@ -61,12 +57,11 @@ describe("Activity Controls Tests", function () { cy.get('[data-test="learning outcome 2"]').type(learningOutcome3); cy.get('[data-test="learning outcome 3"]').type(learningOutcome4); cy.get('[data-test="learning outcome 4"]').type(learningOutcome5); - // cy.get('[data-test="add a learning outcome button"]').click(); - cy.get('[data-test="Close Settings Button"]').click(); - cy.wait(3000); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Settings Menu Item"]').click(); - cy.get('[data-test="Controls Button"]').click(); //Check the text. cy.get('[data-test="learning outcome 0"]').should( "have.value", @@ -89,7 +84,40 @@ describe("Activity Controls Tests", function () { learningOutcome5, ); - //Delete some + //Delete the first two + cy.get('[data-test="delete learning outcome 0 button"]').click(); + cy.get('[data-test="delete learning outcome 0 button"]').click(); + cy.get('[data-test="learning outcome 0"]').should( + "have.value", + learningOutcome3, + ); + cy.get('[data-test="learning outcome 1"]').should( + "have.value", + learningOutcome4, + ); + cy.get('[data-test="learning outcome 2"]').should( + "have.value", + learningOutcome5, + ); //Add another + cy.get('[data-test="add a learning outcome button"]').click(); + cy.get('[data-test="learning outcome 3"]').type(learningOutcome6); + + cy.get('[data-test="learning outcome 0"]').should( + "have.value", + learningOutcome3, + ); + cy.get('[data-test="learning outcome 1"]').should( + "have.value", + learningOutcome4, + ); + cy.get('[data-test="learning outcome 2"]').should( + "have.value", + learningOutcome5, + ); + cy.get('[data-test="learning outcome 3"]').should( + "have.value", + learningOutcome6, + ); }); }); diff --git a/cypress/e2e/Portfolio/ErrorsAndWarnings.cy.js b/cypress/e2e/Portfolio/ErrorsAndWarnings.cy.js index 2c83968767..f21252c926 100644 --- a/cypress/e2e/Portfolio/ErrorsAndWarnings.cy.js +++ b/cypress/e2e/Portfolio/ErrorsAndWarnings.cy.js @@ -5,7 +5,7 @@ describe("Porfolio Errors and Warnings ", function () { const userId2 = "cyuserId2"; before(() => { - // cy.clearAllOfAUsersActivities({userId}) + cy.clearAllOfAUsersActivities({ userId }) }); beforeEach(() => { cy.signin({ userId }); @@ -26,13 +26,12 @@ describe("Porfolio Errors and Warnings ", function () { cy.log("Create an activity"); cy.get('[data-test="Add Activity"]').click(); + cy.get('[data-test="Activity Label"]').clear().type(label); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); - cy.get( - '[data-test="Activity Label Editable"] [data-test="Editable Preview"]', - ).click(); - cy.get('[data-test="Activity Label Editable"] [data-test="Editable Input"]') - .type(label) - .blur(); + cy.get('[data-test="Activity Label Editable Preview"]').contains(label) cy.get(".cm-content").type(`

A good paragraph

{enter}`); @@ -78,8 +77,10 @@ describe("Porfolio Errors and Warnings ", function () { "contain.text", "ErrorLine #3 Invalid component type: ", ); + cy.get('[data-test="Error Button"]').click(); + + cy.get(".cm-content").type(`{ctrl+end}{leftarrow}{backspace}`); //Delete the / in invalid - cy.get(".cm-content").type(`{ctrl+end}{leftarrow}{leftarrow}{backspace}`); cy.get('[data-test="Viewer Update Button"]').click(); cy.get(cesc2("#/__error1")).should( @@ -120,7 +121,9 @@ describe("Porfolio Errors and Warnings ", function () { cy.get('[data-test="Warning Button"]').click(); cy.get('[data-test="Warning Content"]').should( "contain.text", - "WarningLine #5 Attribute ninputs is deprecated. Use numInputs instead. Its use will become an error in the next major version (0.7). Version 0.6 will be phased out in summer 2024.", + "WarningLine #4 Attribute ninputs is deprecated. Use numInputs instead. Its use will become an error in the next major version (0.7). Version 0.6 will be phased out in summer 2024.", ); + cy.get('[data-test="Warning Button"]').click(); + }); }); diff --git a/cypress/e2e/Portfolio/LoadAndSaveEditor.cy.js b/cypress/e2e/Portfolio/LoadAndSaveEditor.cy.js index aaea7e3959..1271940da8 100644 --- a/cypress/e2e/Portfolio/LoadAndSaveEditor.cy.js +++ b/cypress/e2e/Portfolio/LoadAndSaveEditor.cy.js @@ -28,32 +28,50 @@ describe("Load and Save Editor", function () { const largeNumber = `12345678901234567890123456789012345678901234567890123456789012345678901234567890`; cy.get('[data-test="Portfolio"]').click(); cy.get('[data-test="Add Activity"]').click(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); + cy.get(".cm-content").type(largeNumber); cy.get('[data-test="Viewer Update Button"]').click(); + cy.get(cesc2("#/_document1")).should("contains.text", largeNumber); + //Leave and come back + cy.get('[data-test="Logo Button"]').click(); cy.get('[data-test="Portfolio"]').click(); - cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') - .eq(0) - .click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); cy.get(cesc2("#/_document1")).should("contains.text", largeNumber); - // cy.get(cesc2(".cm-content")).should("have.text", largeNumber); }); - it("Quickly Save", () => { + it("Quickly Save and Refresh Save", () => { + const doenetML1 = "Draft content"; + const doenetML2 = "More Draft content"; + //Set it up cy.get('[data-test="Portfolio"]').click(); cy.get('[data-test="Add Activity"]').click(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); cy.get(".cm-content").type( - `{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

Draft content

`, + `{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

${doenetML1}

`, ); - // cy.get('[data-test="Viewer Update Button"]').click(); - // cy.get(cesc2("#/draft")).should("have.text", "Draft content"); + //Leave + cy.get('[data-test="Logo Button"]').click(); + + //Go Back + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); + + cy.get(cesc2("#/draft")).should("have.text", doenetML1); + + //Enter more and hit refresh + cy.get(".cm-content").clear().type( + `{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

${doenetML2}

`, + ); + cy.reload(); + cy.get(cesc2("#/draft")).should("have.text", doenetML2); - cy.get('[data-test="Portfolio"]').click(); - cy.wait(500); - cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') - .eq(0) - .click(); - cy.get(cesc2("#/draft")).should("have.text", "Draft content"); }); }); diff --git a/cypress/e2e/Portfolio/PortfolioVariants.cy.js b/cypress/e2e/Portfolio/PortfolioVariants.cy.js index 9b23e05bef..2754482792 100644 --- a/cypress/e2e/Portfolio/PortfolioVariants.cy.js +++ b/cypress/e2e/Portfolio/PortfolioVariants.cy.js @@ -4,8 +4,17 @@ describe("Portfolio Variant Tests", function () { const userId = "cyuserId"; const userId2 = "cyuserId2"; + before(() => { + // cy.clearAllOfAUsersActivities({userId}) + cy.signin({ userId }); + cy.clearAllOfAUsersCoursesAndItems({ userId }); + cy.clearAllOfAUsersCoursesAndItems({ userId: userId2 }); + }); beforeEach(() => { cy.signin({ userId }); + cy.clearIndexedDB(); + cy.clearAllOfAUsersActivities({ userId }); + cy.clearAllOfAUsersActivities({ userId: userId2 }); cy.visit(`/`); }); @@ -15,48 +24,36 @@ describe("Portfolio Variant Tests", function () { return false; }); - it("Portfolio Editor Varient Control Shows Up", () => { + it("Portfolio Editor Variant Control Shows Up", () => { const label = "Portfolio Variant Control"; const text1 = "Hello World"; cy.log("Make an activity in the portfolio"); cy.get('[data-test="Portfolio"]').click(); cy.get('[data-test="Add Activity"]').click(); + cy.get('[data-test="Activity Label"]').clear().type(label).blur(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); cy.get('[data-test="Variant Select Menu Button"]').should("not.exist"); - cy.get('[data-test="Controls Button"]').click(); - cy.get('[data-test="Activity Label"]').clear().type(label).blur(); - cy.get('[data-test="Close Settings Button"]').click(); - cy.log("Enter content without need of a variant"); - cy.get(".cm-content").type(`

${text1}

{enter}`); cy.get('[data-test="Viewer Update Button"]').click(); cy.get(cesc2("#/_document1")).should("contain", text1); - cy.get('[data-test="Variant Select Menu Button"]').should("not.exist"); cy.log("Enter content that does need of a variant"); - cy.get(".cm-content").type(`{ctrl+end} {enter}`); - + cy.get(".cm-content").clear().type(`{ctrl+end} {enter}`); cy.get('[data-test="Variant Select Menu Button"]').should("not.exist"); - cy.get('[data-test="Viewer Update Button"]').click(); - cy.get('[data-test="Variant Select Menu Button"]').should("exist"); cy.get(cesc2("#/_document1")).should("contain", "1"); cy.log("Change the variants with the control"); - - cy.get('[data-test="Variant Select Down Button"]').click(); - cy.get(cesc2("#/_document1")).should("contain", "2"); - - cy.get('[data-test="Variant Select Up Button"]').click(); - cy.get(cesc2("#/_document1")).should("contain", "1"); - cy.get('[data-test="Variant Select Menu Button"]').click(); cy.get('[data-test="Variant Select Menu Item 2"]').click(); @@ -71,33 +68,28 @@ describe("Portfolio Variant Tests", function () { cy.get(cesc2("#/_document1")).should("contain", "4"); cy.log("View Variant Select keeps sync with Edit"); - cy.get('[data-test="View Mode Button"]').click(); + cy.get('[data-test="Close Editor"]').click(); cy.get(cesc2("#/_document1")).should("contain", "4"); cy.get('[data-test="Variant Select Menu Button"]').should("contain", "d"); - cy.get('[data-test="Variant Select Down Button"]').click(); - cy.get(cesc2("#/_document1")).should("contain", "5"); - - cy.get('[data-test="Variant Select Up Button"]').click(); - cy.get(cesc2("#/_document1")).should("contain", "4"); - cy.get('[data-test="Variant Select Menu Button"]').click(); cy.get('[data-test="Variant Select Menu Item 5"]').click(); cy.get(cesc2("#/_document1")).should("contain", "6"); - cy.get('[data-test="Edit Mode Button"]').click(); + cy.get('[data-test="Edit"]').click(); cy.get(cesc2("#/_document1")).should("contain", "6"); cy.get('[data-test="Variant Select Menu Button"]').should("contain", "f"); - cy.get('[data-test="Controls Button"]').click(); + cy.get('[data-test="Settings Button"]').click(); cy.get(".chakra-checkbox__control").click(); cy.get('[data-test="Close Settings Button"]').click(); cy.log("sign in as someone else and open the public activity"); cy.signin({ userId2 }); + cy.get('[data-test="Logo Button"]').click(); cy.get('[data-test="Community"]').click(); @@ -110,11 +102,6 @@ describe("Portfolio Variant Tests", function () { cy.log("Change the variants using the selector"); cy.get('[data-test="Variant Select Menu Button"]').should("exist"); - cy.get('[data-test="Variant Select Down Button"]').click(); - cy.get(cesc2("#/_document1")).should("contain", "2"); - - cy.get('[data-test="Variant Select Up Button"]').click(); - cy.get(cesc2("#/_document1")).should("contain", "1"); cy.get('[data-test="Variant Select Menu Button"]').click(); cy.get('[data-test="Variant Select Menu Item 2"]').click(); @@ -129,12 +116,6 @@ describe("Portfolio Variant Tests", function () { cy.get('[data-test="Variant Select Menu Button"]').should("exist"); - cy.get('[data-test="Variant Select Down Button"]').click(); - cy.get(cesc2("#/_document1")).should("contain", "2"); - - cy.get('[data-test="Variant Select Up Button"]').click(); - cy.get(cesc2("#/_document1")).should("contain", "1"); - cy.get('[data-test="Variant Select Menu Button"]').click(); cy.get('[data-test="Variant Select Menu Item 2"]').click(); diff --git a/cypress/e2e/Portfolio/ShareActivities.cy.js b/cypress/e2e/Portfolio/ShareActivities.cy.js index ce2af6f785..e6df5c485e 100644 --- a/cypress/e2e/Portfolio/ShareActivities.cy.js +++ b/cypress/e2e/Portfolio/ShareActivities.cy.js @@ -81,13 +81,18 @@ describe("Share Activities Using Portfolio", function () { cy.get(".cm-content").type(activityContent); - cy.get('[data-test="AssignmentSettingsMenu Menu"]').click(); - cy.get('[data-test="Assign Activity"]').click(); + cy.get('[data-test="Controls Button"]').click(); + + cy.get('[data-test="Assign Tab"]').click(); + cy.get('[data-test="Assign Button"]').eq(1).click(); - cy.get('[data-test="Unassign Activity"]').should("be.visible"); + cy.get('[data-test="Unassign Activity Button"]').should("be.visible"); - cy.get('[data-test="Show DoenetML Source"]').click(); - cy.get('[data-test="Make Publicly Visible"]').click(); + cy.get('[data-test="General Tab"]').click(); + cy.get('[data-test="Public Checkbox"]').click(); + cy.get('[data-test="Show DoenetML Checkbox"]').click(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Close"]').click(); cy.log("remix the activity"); cy.get('[data-test="Crumb 0"]').click(); @@ -106,87 +111,88 @@ describe("Share Activities Using Portfolio", function () { cy.get('[data-test="heading2"]').contains("Public Course Activities"); cy.go("back"); - cy.get('[data-test="Remix Button"]').click(); + //TODO: Improve the actual courses to fix this + // cy.get('[data-test="Copy to Portfolio Button"]').click(); - cy.log("rename the 2nd activity and make it public"); - cy.get('[data-test="Controls Button"]').click(); + // cy.log("rename the 2nd activity and make it public"); + // cy.get('[data-test="Controls Button"]').click(); - cy.get('[data-test="Activity Label"]').clear().type(activityLabel2).blur(); - cy.get('[data-test="Public Checkbox"]').click(); - cy.get('[data-test="Close Settings Button"]').click(); + // cy.get('[data-test="Activity Label"]').clear().type(activityLabel2).blur(); + // cy.get('[data-test="Public Checkbox"]').click(); + // cy.get('[data-test="Close Settings Button"]').click(); - cy.log("sign in as another user and remix"); - cy.signin({ - userId: userId2, - firstName: user2FirstName, - lastName: user2LastName, - }); + // cy.log("sign in as another user and remix"); + // cy.signin({ + // userId: userId2, + // firstName: user2FirstName, + // lastName: user2LastName, + // }); - cy.visit(`/community`); + // cy.visit(`/community`); - cy.get('[data-test="Search"]').clear().type(`${activityLabel2}{enter}`); - cy.get('[data-test="Results All Matches"] [data-test="Card Image Link"]') - .eq(0) - .click(); + // cy.get('[data-test="Search"]').clear().type(`${activityLabel2}{enter}`); + // cy.get('[data-test="Results All Matches"] [data-test="Card Image Link"]') + // .eq(0) + // .click(); - cy.get('[data-test="info on contributors"]').contains(user1FullName); - cy.get('[data-test="info on contributors"]').contains(courseLabel); - cy.get('[data-test="contributors menu"]').click(); - cy.get('[data-test="contributors menu item 0"]').contains(user1FullName); - cy.get('[data-test="contributors menu item 1"]').contains(courseLabel); + // cy.get('[data-test="info on contributors"]').contains(user1FullName); + // cy.get('[data-test="info on contributors"]').contains(courseLabel); + // cy.get('[data-test="contributors menu"]').click(); + // cy.get('[data-test="contributors menu item 0"]').contains(user1FullName); + // cy.get('[data-test="contributors menu item 1"]').contains(courseLabel); - cy.get('[data-test="contributors menu item 1"]').click({ force: true }); - cy.get('[data-test="heading1"]').contains(courseLabel); - cy.get('[data-test="heading2"]').contains("Public Course Activities"); - cy.go("back"); + // cy.get('[data-test="contributors menu item 1"]').click({ force: true }); + // cy.get('[data-test="heading1"]').contains(courseLabel); + // cy.get('[data-test="heading2"]').contains("Public Course Activities"); + // cy.go("back"); - cy.get('[data-test="contributors menu"]').click(); - cy.get('[data-test="contributors menu item 0"]').click({ force: true }); - cy.get('[data-test="heading1"]').contains(user1FullName); - cy.get('[data-test="heading2"]').contains("User Portfolio"); - cy.go("back"); + // cy.get('[data-test="contributors menu"]').click(); + // cy.get('[data-test="contributors menu item 0"]').click({ force: true }); + // cy.get('[data-test="heading1"]').contains(user1FullName); + // cy.get('[data-test="heading2"]').contains("User Portfolio"); + // cy.go("back"); - cy.get('[data-test="Remix Button"]').click(); + // cy.get('[data-test="Copy to Portfolio Button"]').click(); - cy.log("label the third activity and examine public portfolio info"); + // cy.log("label the third activity and examine public portfolio info"); - cy.get('[data-test="Controls Button"]').click(); + // cy.get('[data-test="Controls Button"]').click(); - cy.get('[data-test="Activity Label"]').clear().type(activityLabel3).blur(); - cy.get('[data-test="Public Checkbox"]').click(); - cy.get('[data-test="Close Settings Button"]').click(); + // cy.get('[data-test="Activity Label"]').clear().type(activityLabel3).blur(); + // cy.get('[data-test="Public Checkbox"]').click(); + // cy.get('[data-test="Close Settings Button"]').click(); - cy.visit(`/community`); + // cy.visit(`/community`); - cy.get('[data-test="Search"]').clear().type(`${activityLabel3}{enter}`); - cy.get('[data-test="Results All Matches"] [data-test="Card Image Link"]') - .eq(0) - .click(); + // cy.get('[data-test="Search"]').clear().type(`${activityLabel3}{enter}`); + // cy.get('[data-test="Results All Matches"] [data-test="Card Image Link"]') + // .eq(0) + // .click(); - cy.get('[data-test="info on contributors"]').contains(user2FullName); - cy.get('[data-test="info on contributors"]').contains(user1FullName); - cy.get('[data-test="info on contributors"]').contains(", ..."); + // cy.get('[data-test="info on contributors"]').contains(user2FullName); + // cy.get('[data-test="info on contributors"]').contains(user1FullName); + // cy.get('[data-test="info on contributors"]').contains(", ..."); - cy.get('[data-test="contributors menu"]').click(); - cy.get('[data-test="contributors menu item 0"]').contains(user2FullName); - cy.get('[data-test="contributors menu item 1"]').contains(user1FullName); - cy.get('[data-test="contributors menu item 2"]').contains(courseLabel); + // cy.get('[data-test="contributors menu"]').click(); + // cy.get('[data-test="contributors menu item 0"]').contains(user2FullName); + // cy.get('[data-test="contributors menu item 1"]').contains(user1FullName); + // cy.get('[data-test="contributors menu item 2"]').contains(courseLabel); - cy.get('[data-test="contributors menu item 2"]').click({ force: true }); - cy.get('[data-test="heading1"]').contains(courseLabel); - cy.get('[data-test="heading2"]').contains("Public Course Activities"); - cy.go("back"); + // cy.get('[data-test="contributors menu item 2"]').click({ force: true }); + // cy.get('[data-test="heading1"]').contains(courseLabel); + // cy.get('[data-test="heading2"]').contains("Public Course Activities"); + // cy.go("back"); - cy.get('[data-test="contributors menu"]').click(); - cy.get('[data-test="contributors menu item 1"]').click({ force: true }); - cy.get('[data-test="heading1"]').contains(user1FullName); - cy.get('[data-test="heading2"]').contains("User Portfolio"); - cy.go("back"); + // cy.get('[data-test="contributors menu"]').click(); + // cy.get('[data-test="contributors menu item 1"]').click({ force: true }); + // cy.get('[data-test="heading1"]').contains(user1FullName); + // cy.get('[data-test="heading2"]').contains("User Portfolio"); + // cy.go("back"); - cy.get('[data-test="contributors menu"]').click(); - cy.get('[data-test="contributors menu item 0"]').click({ force: true }); - cy.get('[data-test="heading1"]').contains(user2FullName); - cy.get('[data-test="heading2"]').contains("User Portfolio"); + // cy.get('[data-test="contributors menu"]').click(); + // cy.get('[data-test="contributors menu item 0"]').click({ force: true }); + // cy.get('[data-test="heading1"]').contains(user2FullName); + // cy.get('[data-test="heading2"]').contains("User Portfolio"); }); it("Portfolio Settings Menu", () => { @@ -197,40 +203,45 @@ describe("Share Activities Using Portfolio", function () { cy.log("Create an activity"); cy.get('[data-test="Add Activity"]').click(); + cy.get('[data-test="Activity Label"]').clear().type(label).blur(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click({ force: true }); + cy.get(".cm-content").type( `

What is your name?

{enter}`, ); - cy.get( - '[data-test="Activity Label Editable"] [data-test="Editable Preview"]', - ).click(); - cy.get('[data-test="Activity Label Editable"] [data-test="Editable Input"]') - .type(label) - .blur(); + // cy.get( + // '[data-test="Activity Label Editable"] [data-test="Editable Preview"]', + // ).click(); + // cy.get('[data-test="Activity Label Editable"] [data-test="Editable Input"]') + // .type(label) + // .blur(); - cy.get('[data-test="Portfolio"]').click(); + // cy.get('[data-test="Portfolio"]').click(); - cy.get('[data-test="Private Activities"]').contains(label); - cy.get('[data-test="Public Activities"]').should("not.contain", label); + // cy.get('[data-test="Private Activities"]').contains(label); + // cy.get('[data-test="Public Activities"]').should("not.contain", label); - cy.get('[data-test="Private Activities"]') - .contains(label) - .parent() - .parent() - .find('[data-test="Card Menu Button"]') - .click() - .parent() - .find('[data-test="Settings Menu Item"]') - .click(); + // cy.get('[data-test="Private Activities"]') + // .contains(label) + // .parent() + // .parent() + // .find('[data-test="Card Menu Button"]') + // .click() + // .parent() + // .find('[data-test="Settings Menu Item"]') + // .click(); - cy.get('[data-test="Public Checkbox"]').click(); + // cy.get('[data-test="Public Checkbox"]').click(); - cy.get(".chakra-modal__close-btn").click(); - // // cy.get('[data-test="Close Settings Button"]').click(); //TODO use data-test + // cy.get(".chakra-modal__close-btn").click(); + // // // cy.get('[data-test="Close Settings Button"]').click(); //TODO use data-test - cy.get('[data-test="Public Activities"]').contains(label); - cy.get('[data-test="Private Activities"]').should("not.contain", label); + // cy.get('[data-test="Public Activities"]').contains(label); + // cy.get('[data-test="Private Activities"]').should("not.contain", label); }); it("Share activities and remix", () => { @@ -262,12 +273,15 @@ describe("Share Activities Using Portfolio", function () { cy.log("Create an activity"); cy.get('[data-test="Add Activity"]').click(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); cy.get(".cm-content").type( `

What is your name?

{enter}`, ); cy.get('[data-test="Viewer Update Button"]').click(); - cy.get('[data-test="Portfolio"]').click(); + cy.get('[data-test="Logo Button"]').click(); cy.get( '[data-test="Private Activities"] [data-test="Activity Card"]', @@ -277,25 +291,18 @@ describe("Share Activities Using Portfolio", function () { ).should("have.length", 0); cy.log("Edit the activity"); - cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') - .eq(0) - .click(); - - cy.get( - '[data-test="Public Activities"] [data-test="Activity Card"]', - ).should("have.length", 0); - - cy.get('[data-test="Viewer Update Button"]').should("have.length", 1); - cy.log("In the editor"); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); cy.get(".cm-content").type( `{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

Hello, !

`, - ); + { delay: 10 }); cy.get('[data-test="Viewer Update Button"]').click(); cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); - cy.get('[data-test="Portfolio"]').click(); + cy.get('[data-test="Logo Button"]').click(); + cy.get( '[data-test="Private Activities"] [data-test="Activity Card"]', @@ -305,10 +312,13 @@ describe("Share Activities Using Portfolio", function () { ).should("have.length", 0); cy.log("Make the activity public"); - cy.get( - '[data-test="Private Activities"] [data-test="Card Menu Button"]', - ).click(); - cy.get('[data-test="Make Public Menu Item"]').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); + cy.get('[data-test="Settings Button"]').click(); + cy.get('[data-test="Public Checkbox"]').click(); + + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Logo Button"]').click(); cy.get( '[data-test="Private Activities"] [data-test="Activity Card"]', @@ -332,34 +342,33 @@ describe("Share Activities Using Portfolio", function () { cy.get(".chakra-modal__close-btn").click(); //Couldn't figure out data-test on this one cy.log("Edit the public activity"); - cy.get('[data-test="Public Activities"] [data-test="Card Image Link"] ') - .eq(0) - .click(); + cy.get('[data-test="Public Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Edit Menu Item"]').eq(0).click(); cy.get(".cm-content").type( `{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

Draft content

`, - ); + { delay: 10 }); cy.get('[data-test="Viewer Update Button"]').click(); cy.get(cesc2("#/draft")).should("have.text", "Draft content"); - cy.get('[data-test="Portfolio"]').click(); + cy.get('[data-test="Logo Button"]').click(); cy.log("Create a private activity"); cy.get('[data-test="Add Activity"]').click(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Private Activities"] [data-test="Edit Menu Item"]').eq(0).click(); cy.get(".cm-content").type(`Stays private{enter}`); cy.get('[data-test="Viewer Update Button"]').click(); + cy.get('[data-test="Settings Button"]').click(); - cy.get('[data-test="Controls Button"]').click(); - - cy.get('[data-test="Activity Label"]') - .clear() - .type(stayPrivateLabel) - .blur(); + cy.get('[data-test="Activity Label"]').eq(1).clear().type(stayPrivateLabel).blur(); cy.get(".chakra-modal__close-btn").click(); - cy.get('[data-test="Portfolio"]').click(); + cy.get('[data-test="Logo Button"]').click(); + cy.get( '[data-test="Private Activities"] [data-test="Activity Card"]', @@ -423,132 +432,133 @@ describe("Share Activities Using Portfolio", function () { cy.get(cesc2("#/name")).type("Mom{enter}"); cy.get(cesc2("#/_p2")).should("have.text", "Hello, Mom!"); - cy.log("Remix"); - cy.get('[data-test="Remix Button"]').click(); - - cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); - cy.get(cesc2("#/draft")).should("not.exist"); - cy.get(cesc2("#/temp_content")).should("not.exist"); - - cy.log("Modify the source for real"); - cy.get(".cm-content").type( - `{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

Actual change

`, - ); - cy.get('[data-test="Viewer Update Button"]').click(); - cy.get(cesc2("#/actual_change")).should("have.text", "Actual change"); - - cy.log("Find activity in portfolio"); - cy.get('[data-test="Portfolio"]').click(); - - cy.get( - '[data-test="Public Activities"] [data-test="Activity Card"]', - ).should("have.length", 0); - cy.get( - '[data-test="Private Activities"] [data-test="Activity Card"]', - ).should("have.length", 1); - - cy.log("Edit the activity"); - cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') - .eq(0) - .click(); - - cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); - cy.get(cesc2("#/draft")).should("not.exist"); - cy.get(cesc2("#/temp_content")).should("not.exist"); - cy.get(cesc2("#/actual_change")).should("have.text", "Actual change"); - - cy.get(cesc2("#/name")).type("Dad{enter}"); - cy.get(cesc2("#/_p2")).should("have.text", "Hello, Dad!"); - - cy.log("Log back in as first user"); - cy.signin({ userId: user1 }); - cy.visit(`/`); - - cy.log("Verify activity is unchanged with draft content"); - cy.get('[data-test="Portfolio"]').click(); - cy.get('[data-test="Public Activities"] [data-test="Card Image Link"] ') - .eq(0) - .click(); - - cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); - cy.get(cesc2("#/draft")).should("have.text", "Draft content"); - cy.get(cesc2("#/temp_content")).should("not.exist"); - cy.get(cesc2("#/actual_change")).should("not.exist"); - - cy.log("Update public activity"); - cy.get('[data-test="Update Public Activity Button"]').click(); - - cy.get(cesc2("#/name")).type("Sis{enter}"); - cy.get(cesc2("#/_p2")).should("have.text", "Hello, Sis!"); - - cy.log("Log back in as second user"); - cy.signin({ userId: user2 }); - cy.visit(`/`); - - cy.log("Find new version of public activity Hello!"); - cy.get('[data-test="Community"]').click(); - - cy.get('[data-test="Search"]').type(`${helloLabel}{enter}`); - cy.get('[data-test="Results All Matches"] [data-test="Card Image Link"] ') - .eq(0) - .click(); - - cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); - cy.get(cesc2("#/draft")).should("have.text", "Draft content"); - cy.get(cesc2("#/temp_content")).should("not.exist"); - cy.get(cesc2("#/actual_change")).should("not.exist"); - - cy.get(cesc2("#/name")).type("Bro{enter}"); - cy.get(cesc2("#/_p2")).should("have.text", "Hello, Bro!"); - - cy.log("Remix"); - cy.get('[data-test="Remix Button"]').click(); - - cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); - cy.get(cesc2("#/draft")).should("have.text", "Draft content"); - cy.get(cesc2("#/temp_content")).should("not.exist"); - cy.get(cesc2("#/actual_change")).should("not.exist"); - - cy.log("Modify the source for real"); - cy.get(".cm-content").type( - `{ctrl+end}{rightarrow}{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

New change

`, - ); - cy.get('[data-test="Viewer Update Button"]').click(); - cy.get(cesc2("#/change2")).should("have.text", "New change"); - - cy.log("Find both activities in portfolio"); - cy.get('[data-test="Portfolio"]').click(); - - cy.get( - '[data-test="Public Activities"] [data-test="Activity Card"]', - ).should("have.length", 0); - cy.get( - '[data-test="Private Activities"] [data-test="Activity Card"]', - ).should("have.length", 2); - - cy.log("View the most recently remixed activity"); - cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') - .eq(0) - .click(); - - cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); - cy.get(cesc2("#/draft")).should("have.text", "Draft content"); - cy.get(cesc2("#/temp_content")).should("not.exist"); - cy.get(cesc2("#/actual_change")).should("not.exist"); - cy.get(cesc2("#/change2")).should("have.text", "New change"); - - cy.log("View the first remixed activity"); - cy.get('[data-test="Portfolio"]').click(); - - cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') - .eq(1) - .click(); - - cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); - cy.get(cesc2("#/draft")).should("not.exist"); - cy.get(cesc2("#/temp_content")).should("not.exist"); - cy.get(cesc2("#/actual_change")).should("have.text", "Actual change"); - cy.get(cesc2("#/change2")).should("not.exist"); + //TODO: this part fails for now. Needs to be moved off of Recoil. + // cy.log("Remix"); + // cy.get('[data-test="Copy to Portfolio Button"]').click(); + + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); + // cy.get(cesc2("#/draft")).should("not.exist"); + // cy.get(cesc2("#/temp_content")).should("not.exist"); + + // cy.log("Modify the source for real"); + // cy.get(".cm-content").type( + // `{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

Actual change

`, + // ); + // cy.get('[data-test="Viewer Update Button"]').click(); + // cy.get(cesc2("#/actual_change")).should("have.text", "Actual change"); + + // cy.log("Find activity in portfolio"); + // cy.get('[data-test="Portfolio"]').click(); + + // cy.get( + // '[data-test="Public Activities"] [data-test="Activity Card"]', + // ).should("have.length", 0); + // cy.get( + // '[data-test="Private Activities"] [data-test="Activity Card"]', + // ).should("have.length", 1); + + // cy.log("Edit the activity"); + // cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') + // .eq(0) + // .click(); + + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); + // cy.get(cesc2("#/draft")).should("not.exist"); + // cy.get(cesc2("#/temp_content")).should("not.exist"); + // cy.get(cesc2("#/actual_change")).should("have.text", "Actual change"); + + // cy.get(cesc2("#/name")).type("Dad{enter}"); + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, Dad!"); + + // cy.log("Log back in as first user"); + // cy.signin({ userId: user1 }); + // cy.visit(`/`); + + // cy.log("Verify activity is unchanged with draft content"); + // cy.get('[data-test="Portfolio"]').click(); + // cy.get('[data-test="Public Activities"] [data-test="Card Image Link"] ') + // .eq(0) + // .click(); + + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); + // cy.get(cesc2("#/draft")).should("have.text", "Draft content"); + // cy.get(cesc2("#/temp_content")).should("not.exist"); + // cy.get(cesc2("#/actual_change")).should("not.exist"); + + // cy.log("Update public activity"); + // cy.get('[data-test="Update Public Activity Button"]').click(); + + // cy.get(cesc2("#/name")).type("Sis{enter}"); + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, Sis!"); + + // cy.log("Log back in as second user"); + // cy.signin({ userId: user2 }); + // cy.visit(`/`); + + // cy.log("Find new version of public activity Hello!"); + // cy.get('[data-test="Community"]').click(); + + // cy.get('[data-test="Search"]').type(`${helloLabel}{enter}`); + // cy.get('[data-test="Results All Matches"] [data-test="Card Image Link"] ') + // .eq(0) + // .click(); + + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); + // cy.get(cesc2("#/draft")).should("have.text", "Draft content"); + // cy.get(cesc2("#/temp_content")).should("not.exist"); + // cy.get(cesc2("#/actual_change")).should("not.exist"); + + // cy.get(cesc2("#/name")).type("Bro{enter}"); + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, Bro!"); + + // cy.log("Remix"); + // cy.get('[data-test="Copy to Portfolio Button"]').click(); + + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); + // cy.get(cesc2("#/draft")).should("have.text", "Draft content"); + // cy.get(cesc2("#/temp_content")).should("not.exist"); + // cy.get(cesc2("#/actual_change")).should("not.exist"); + + // cy.log("Modify the source for real"); + // cy.get(".cm-content").type( + // `{ctrl+end}{rightarrow}{ctrl+end}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}

New change

`, + // ); + // cy.get('[data-test="Viewer Update Button"]').click(); + // cy.get(cesc2("#/change2")).should("have.text", "New change"); + + // cy.log("Find both activities in portfolio"); + // cy.get('[data-test="Portfolio"]').click(); + + // cy.get( + // '[data-test="Public Activities"] [data-test="Activity Card"]', + // ).should("have.length", 0); + // cy.get( + // '[data-test="Private Activities"] [data-test="Activity Card"]', + // ).should("have.length", 2); + + // cy.log("View the most recently remixed activity"); + // cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') + // .eq(0) + // .click(); + + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); + // cy.get(cesc2("#/draft")).should("have.text", "Draft content"); + // cy.get(cesc2("#/temp_content")).should("not.exist"); + // cy.get(cesc2("#/actual_change")).should("not.exist"); + // cy.get(cesc2("#/change2")).should("have.text", "New change"); + + // cy.log("View the first remixed activity"); + // cy.get('[data-test="Portfolio"]').click(); + + // cy.get('[data-test="Private Activities"] [data-test="Card Image Link"] ') + // .eq(1) + // .click(); + + // cy.get(cesc2("#/_p2")).should("have.text", "Hello, !"); + // cy.get(cesc2("#/draft")).should("not.exist"); + // cy.get(cesc2("#/temp_content")).should("not.exist"); + // cy.get(cesc2("#/actual_change")).should("have.text", "Actual change"); + // cy.get(cesc2("#/change2")).should("not.exist"); }); it("View solution in portfolio", () => { @@ -559,17 +569,17 @@ describe("Share Activities Using Portfolio", function () { cy.log("Create an activity with a solution"); cy.get('[data-test="Add Activity"]').click(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Private Activities"] [data-test="Edit Menu Item"]').eq(0).click(); cy.get(".cm-content").type( `

What is 1+1?

{enter}2`, ); - cy.get( - '[data-test="Activity Label Editable"] [data-test="Editable Preview"]', - ).click(); - cy.get('[data-test="Activity Label Editable"] [data-test="Editable Input"]') - .type(label) - .blur(); + cy.get('[data-test="Settings Button"]').click(); + cy.get('[data-test="Activity Label"]').eq(1).clear().type(label).blur(); + cy.get('.chakra-modal__close-btn').click(); cy.get('[data-test="Viewer Update Button"]').click(); @@ -586,7 +596,7 @@ describe("Share Activities Using Portfolio", function () { cy.get(cesc2("#/sol_button")).click(); cy.get(cesc2("#/ans")).should("not.exist"); - cy.get('[data-test="Portfolio"]').click(); + cy.get('[data-test="Logo Button"]').click(); cy.get('[data-test="Private Activities"]') .contains(label) @@ -639,124 +649,113 @@ describe("Share Activities Using Portfolio", function () { cy.log("Create an activity that will be linked to"); cy.get('[data-test="Add Activity"]').click(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Private Activities"] [data-test="Edit Menu Item"]').eq(0).click(); cy.get(".cm-content").type(`

Link to this page!

`); let linkedDoenetId; cy.url().then((url) => { - linkedDoenetId = url.match(/portfolioeditor\/(\w*)/)[1]; - }); + linkedDoenetId = url.match(/portfolioActivity\/(\w*)/)[1]; + cy.log("linkedDoenetId", linkedDoenetId) + cy.get('[data-test="Settings Button"]').click(); + cy.get('[data-test="Activity Label"]').eq(1).clear().type(label2).blur(); + cy.get('.chakra-modal__close-btn').click(); - cy.get( - '[data-test="Activity Label Editable"] [data-test="Editable Preview"]', - ).click(); - cy.get('[data-test="Activity Label Editable"] [data-test="Editable Input"]') - .type(label2) - .blur(); + cy.get('[data-test="Logo Button"]').click(); - cy.get('[data-test="Portfolio"]').click(); + cy.get('[data-test="Private Activities"]') + .contains(label2) + .parent() + .parent() + .find('[data-test="Card Menu Button"]') + .click() + .parent() + .find('[data-test="Make Public Menu Item"]') + .click(); - cy.get('[data-test="Private Activities"]') - .contains(label2) - .parent() - .parent() - .find('[data-test="Card Menu Button"]') - .click() - .parent() - .find('[data-test="Make Public Menu Item"]') - .click(); + cy.get('[data-test="Public Activities"]').contains(label2); + cy.get('[data-test="Private Activities"]').should("not.contain", label2); - cy.get('[data-test="Public Activities"]').contains(label2); - cy.get('[data-test="Private Activities"]').should("not.contain", label2); + cy.log("Create an activity that will link to other activity"); + cy.get('[data-test="Add Activity"]').click(); + cy.get('[data-test="Activity Label"]').eq(0).clear().type(label1).blur(); + cy.get('.chakra-modal__close-btn').click(); + cy.get('[data-test="Private Activities"] [data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Private Activities"] [data-test="Edit Menu Item"]').eq(0).click(); - cy.log("Create an activity that will link to other activity"); - cy.get('[data-test="Add Activity"]') - .click() - .then(() => { - cy.get(".cm-content").type( - `

We have a link to the activity.

-

We have a edit link to the activity.

`, - ); - }); - cy.get( - '[data-test="Activity Label Editable"] [data-test="Editable Preview"]', - ).click(); - cy.get('[data-test="Activity Label Editable"] [data-test="Editable Input"]') - .type(label1) - .blur(); + cy.get(".cm-content").type( + `

We have a link to the activity.

+

We have a edit link to the activity.

`, + ); - cy.get('[data-test="Viewer Update Button"]').click(); + cy.get('[data-test="Viewer Update Button"]').click(); - cy.get(cesc2("#/toDoc")).invoke("removeAttr", "target").click(); - cy.url().should("contain", "portfolioviewer"); - cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); + cy.get(cesc2("#/toDoc")).invoke("removeAttr", "target").click(); + cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); - cy.go("back"); + cy.go("back"); - cy.get(cesc2("#/toDocEdit")).invoke("removeAttr", "target").click(); - cy.url().should("contain", "portfolioeditor"); - cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); + cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); - cy.go("back"); + cy.go("back"); - cy.get('[data-test="Portfolio"]').click(); + cy.get('[data-test="Logo Button"]').click(); - cy.get('[data-test="Private Activities"]') - .contains(label1) - .parent() - .parent() - .find('[data-test="Card Menu Button"]') - .click() - .parent() - .find('[data-test="Make Public Menu Item"]') - .click(); + cy.get('[data-test="Private Activities"]') + .contains(label1) + .parent() + .parent() + .find('[data-test="Card Menu Button"]') + .click() + .parent() + .find('[data-test="Make Public Menu Item"]') + .click(); - cy.log("Test links from community page"); - cy.get("[data-test=Community").click(); - cy.get("[data-test=Search]").type(label1 + "{enter}"); + cy.log("Test links from community page"); + cy.get("[data-test=Community").click(); + cy.get("[data-test=Search]").type(label1 + "{enter}"); - cy.get('[data-test="Results All Matches"]') - .contains(label1) - .get('[data-test="Card Image Link"]') - .eq(0) - .click(); + cy.get('[data-test="Results All Matches"]') + .contains(label1) + .get('[data-test="Card Image Link"]') + .eq(0) + .click(); - cy.get(cesc2("#/toDoc")).invoke("removeAttr", "target").click(); - cy.url().should("contain", "portfolioviewer"); - cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); + //TODO: Return to this after further development - cy.go("back"); + // cy.go("back"); - cy.get(cesc2("#/toDocEdit")).invoke("removeAttr", "target").click(); - cy.url().should("contain", "portfolioeditor"); - cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); + // cy.get(cesc2("#/toDocEdit")).invoke("removeAttr", "target").click(); + // cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); - cy.log("Log on as other user"); - cy.signin({ userId: userId2 }); + // cy.log("Log on as other user"); + // cy.signin({ userId: userId2 }); - cy.visit(`/`); + // cy.visit(`/`); - cy.log("Test links from community page"); - cy.get("[data-test=Community").click(); - cy.get("[data-test=Search]").type(label1 + "{enter}"); + // cy.log("Test links from community page"); + // cy.get("[data-test=Community").click(); + // cy.get("[data-test=Search]").type(label1 + "{enter}"); - cy.get('[data-test="Results All Matches"]') - .contains(label1) - .get('[data-test="Card Image Link"]') - .eq(0) - .click(); + // cy.get('[data-test="Results All Matches"]') + // .contains(label1) + // .get('[data-test="Card Image Link"]') + // .eq(0) + // .click(); - cy.get(cesc2("#/toDoc")).invoke("removeAttr", "target").click(); - cy.url().should("contain", "portfolioviewer"); - cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); + // // cy.get(cesc2("#/toDoc")).invoke("removeAttr", "target").click(); + // cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); - cy.go("back"); + // cy.go("back"); + + // cy.log("Edit link should go to public editor"); + // cy.get(cesc2("#/toDocEdit")).invoke("removeAttr", "target").click(); + // cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); + }); - cy.log("Edit link should go to public editor"); - cy.get(cesc2("#/toDocEdit")).invoke("removeAttr", "target").click(); - cy.url().should("contain", "publiceditor"); - cy.get(cesc2("#/theP")).should("have.text", "Link to this page!"); }); }); diff --git a/dist/favicon.ico b/dist/favicon.ico deleted file mode 100644 index ecd8797621..0000000000 Binary files a/dist/favicon.ico and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 3c3e504443..b841d6ae99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,8 +63,8 @@ "react-is": "^18.2.0", "react-mathquill": "^1.0.3", "react-measure": "^2.5.2", - "react-router": "^6.9.0", - "react-router-dom": "^6.9.0", + "react-router": "^6.18.0", + "react-router-dom": "^6.18.0", "react-select": "^5.7.3", "react-table": "^7.7.0", "react-use-measure": "^2.1.1", @@ -109,7 +109,7 @@ }, "optionalDependencies": { "@esbuild/linux-arm64": "^0.17.19", - "cypress": "^12.12.0", + "cypress": "^13.3.3", "cypress-file-upload": "^5.0.8", "cypress-parallel": "^0.13.0", "cypress-plugin-tab": "^1.0.5", @@ -1973,8 +1973,9 @@ } }, "node_modules/@cypress/request": { - "version": "2.88.10", - "license": "Apache-2.0", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "optional": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -1990,9 +1991,9 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -2002,7 +2003,8 @@ }, "node_modules/@cypress/request/node_modules/form-data": { "version": "2.3.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "optional": true, "dependencies": { "asynckit": "^0.4.0", @@ -2014,11 +2016,18 @@ } }, "node_modules/@cypress/request/node_modules/qs": { - "version": "6.5.3", - "license": "BSD-3-Clause", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", "optional": true, + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/@cypress/xvfb": { @@ -3051,11 +3060,11 @@ } }, "node_modules/@remix-run/router": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz", - "integrity": "sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", + "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==", "engines": { - "node": ">=14" + "node": ">=14.0.0" } }, "node_modules/@rollup/plugin-alias": { @@ -3443,8 +3452,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.11.4", - "license": "MIT" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -4213,7 +4223,8 @@ }, "node_modules/asn1": { "version": "0.2.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "optional": true, "dependencies": { "safer-buffer": "~2.1.0" @@ -4221,7 +4232,8 @@ }, "node_modules/assert-plus": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "optional": true, "engines": { "node": ">=0.8" @@ -4291,15 +4303,17 @@ }, "node_modules/aws-sign2": { "version": "0.7.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "optional": true, "engines": { "node": "*" } }, "node_modules/aws4": { - "version": "1.11.0", - "license": "MIT", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "optional": true }, "node_modules/axe-core": { @@ -4734,7 +4748,8 @@ }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "optional": true, "dependencies": { "tweetnacl": "^0.14.3" @@ -4942,7 +4957,7 @@ }, "node_modules/call-bind": { "version": "1.0.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -4997,7 +5012,8 @@ }, "node_modules/caseless": { "version": "0.12.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "optional": true }, "node_modules/chai": { @@ -5508,6 +5524,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "optional": true + }, "node_modules/cors": { "version": "2.8.5", "license": "MIT", @@ -5603,15 +5625,15 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.12.0.tgz", - "integrity": "sha512-UU5wFQ7SMVCR/hyKok/KmzG6fpZgBHHfrXcHzDmPHWrT+UUetxFzQgt7cxCszlwfozckzwkd22dxMwl/vNkWRw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.3.tgz", + "integrity": "sha512-mbdkojHhKB1xbrj7CrKWHi22uFx9P9vQFiR0sYDZZoK99OMp9/ZYN55TO5pjbXmV7xvCJ4JwBoADXjOJK8aCJw==", "hasInstallScript": true, "optional": true, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -5644,9 +5666,10 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", @@ -5656,7 +5679,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^14.0.0 || ^16.0.0 || >=18.0.0" + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, "node_modules/cypress-file-upload": { @@ -6112,11 +6135,6 @@ "license": "MIT", "optional": true }, - "node_modules/cypress/node_modules/@types/node": { - "version": "14.18.32", - "license": "MIT", - "optional": true - }, "node_modules/cypress/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -6205,8 +6223,9 @@ } }, "node_modules/cypress/node_modules/semver": { - "version": "7.3.8", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "optional": true, "dependencies": { "lru-cache": "^6.0.0" @@ -6239,7 +6258,8 @@ }, "node_modules/dashdash": { "version": "1.14.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "optional": true, "dependencies": { "assert-plus": "^1.0.0" @@ -6479,7 +6499,8 @@ }, "node_modules/ecc-jsbn": { "version": "0.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "optional": true, "dependencies": { "jsbn": "~0.1.0", @@ -7772,7 +7793,8 @@ }, "node_modules/extend": { "version": "3.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "optional": true }, "node_modules/extend-shallow": { @@ -7865,10 +7887,11 @@ }, "node_modules/extsprintf": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "engines": [ "node >=0.6.0" ], - "license": "MIT", "optional": true }, "node_modules/fast-deep-equal": { @@ -8059,7 +8082,8 @@ }, "node_modules/forever-agent": { "version": "0.6.1", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "optional": true, "engines": { "node": "*" @@ -8232,7 +8256,7 @@ }, "node_modules/get-intrinsic": { "version": "1.1.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -8298,7 +8322,8 @@ }, "node_modules/getpass": { "version": "0.1.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "optional": true, "dependencies": { "assert-plus": "^1.0.0" @@ -8461,7 +8486,7 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8627,7 +8652,8 @@ }, "node_modules/http-signature": { "version": "1.3.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "optional": true, "dependencies": { "assert-plus": "^1.0.0", @@ -9178,7 +9204,8 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "optional": true }, "node_modules/is-unicode-supported": { @@ -9249,7 +9276,8 @@ }, "node_modules/isstream": { "version": "0.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "optional": true }, "node_modules/istanbul-lib-coverage": { @@ -9366,7 +9394,8 @@ }, "node_modules/jsbn": { "version": "0.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "optional": true }, "node_modules/jsesc": { @@ -9385,7 +9414,8 @@ }, "node_modules/json-schema": { "version": "0.4.0", - "license": "(AFL-2.1 OR BSD-3-Clause)", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "optional": true }, "node_modules/json-schema-traverse": { @@ -9407,7 +9437,8 @@ }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "optional": true }, "node_modules/json5": { @@ -9433,10 +9464,11 @@ }, "node_modules/jsprim": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "engines": [ "node >=0.6.0" ], - "license": "MIT", "optional": true, "dependencies": { "assert-plus": "1.0.0", @@ -10613,7 +10645,7 @@ }, "node_modules/object-inspect": { "version": "1.12.2", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10955,7 +10987,8 @@ }, "node_modules/performance-now": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "optional": true }, "node_modules/picocolors": { @@ -11210,6 +11243,15 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/progress": { "version": "2.0.3", "dev": true, @@ -11238,7 +11280,8 @@ }, "node_modules/psl": { "version": "1.9.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "optional": true }, "node_modules/pump": { @@ -11294,6 +11337,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "optional": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "devOptional": true, @@ -11539,29 +11588,29 @@ } }, "node_modules/react-router": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.10.0.tgz", - "integrity": "sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", + "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", "dependencies": { - "@remix-run/router": "1.5.0" + "@remix-run/router": "1.11.0" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", - "integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", + "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", "dependencies": { - "@remix-run/router": "1.5.0", - "react-router": "6.10.0" + "@remix-run/router": "1.11.0", + "react-router": "6.18.0" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8", @@ -11872,7 +11921,7 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/resize-observer-polyfill": { @@ -12322,7 +12371,7 @@ }, "node_modules/side-channel": { "version": "1.0.4", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.0", @@ -12720,8 +12769,9 @@ } }, "node_modules/sshpk": { - "version": "1.17.0", - "license": "MIT", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "dependencies": { "asn1": "~0.2.3", @@ -13226,15 +13276,27 @@ } }, "node_modules/tough-cookie": { - "version": "2.5.0", - "license": "BSD-3-Clause", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "optional": true, "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=0.8" + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "optional": true, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/tr46": { @@ -13285,7 +13347,8 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "optional": true, "dependencies": { "safe-buffer": "^5.0.1" @@ -13296,7 +13359,8 @@ }, "node_modules/tweetnacl": { "version": "0.14.5", - "license": "Unlicense", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "optional": true }, "node_modules/type-check": { @@ -13532,6 +13596,16 @@ "version": "0.1.0", "license": "MIT" }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "optional": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use": { "version": "3.1.1", "license": "MIT", @@ -13600,7 +13674,8 @@ }, "node_modules/uuid": { "version": "8.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "optional": true, "bin": { "uuid": "dist/bin/uuid" @@ -13636,10 +13711,11 @@ }, "node_modules/verror": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "engines": [ "node >=0.6.0" ], - "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", @@ -13647,11 +13723,6 @@ "extsprintf": "^1.2.0" } }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "license": "MIT", - "optional": true - }, "node_modules/vite": { "version": "4.2.1", "dev": true, @@ -15568,7 +15639,9 @@ "optional": true }, "@cypress/request": { - "version": "2.88.10", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "optional": true, "requires": { "aws-sign2": "~0.7.0", @@ -15584,15 +15657,17 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, "dependencies": { "form-data": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "optional": true, "requires": { "asynckit": "^0.4.0", @@ -15601,8 +15676,13 @@ } }, "qs": { - "version": "6.5.3", - "optional": true + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "optional": true, + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -16237,9 +16317,9 @@ } }, "@remix-run/router": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz", - "integrity": "sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", + "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==" }, "@rollup/plugin-alias": { "version": "3.1.9", @@ -16532,7 +16612,9 @@ "dev": true }, "@types/node": { - "version": "18.11.4" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "@types/normalize-package-data": { "version": "2.4.1" @@ -17058,6 +17140,8 @@ }, "asn1": { "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "optional": true, "requires": { "safer-buffer": "~2.1.0" @@ -17065,6 +17149,8 @@ }, "assert-plus": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "optional": true }, "assertion-error": { @@ -17101,10 +17187,14 @@ }, "aws-sign2": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "optional": true }, "aws4": { - "version": "1.11.0", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "optional": true }, "axe-core": { @@ -17395,6 +17485,8 @@ }, "bcrypt-pbkdf": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "optional": true, "requires": { "tweetnacl": "^0.14.3" @@ -17524,7 +17616,7 @@ }, "call-bind": { "version": "1.0.2", - "dev": true, + "devOptional": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -17548,6 +17640,8 @@ }, "caseless": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "optional": true }, "chai": { @@ -17880,6 +17974,12 @@ "version": "3.26.0", "dev": true }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "optional": true + }, "cors": { "version": "2.8.5", "requires": { @@ -17947,14 +18047,14 @@ "version": "5.3.6" }, "cypress": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.12.0.tgz", - "integrity": "sha512-UU5wFQ7SMVCR/hyKok/KmzG6fpZgBHHfrXcHzDmPHWrT+UUetxFzQgt7cxCszlwfozckzwkd22dxMwl/vNkWRw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.3.tgz", + "integrity": "sha512-mbdkojHhKB1xbrj7CrKWHi22uFx9P9vQFiR0sYDZZoK99OMp9/ZYN55TO5pjbXmV7xvCJ4JwBoADXjOJK8aCJw==", "optional": true, "requires": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -17987,19 +18087,16 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "dependencies": { - "@types/node": { - "version": "14.18.32", - "optional": true - }, "ansi-styles": { "version": "4.3.0", "optional": true, @@ -18048,7 +18145,9 @@ "optional": true }, "semver": { - "version": "7.3.8", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "optional": true, "requires": { "lru-cache": "^6.0.0" @@ -18405,6 +18504,8 @@ }, "dashdash": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "optional": true, "requires": { "assert-plus": "^1.0.0" @@ -18550,6 +18651,8 @@ }, "ecc-jsbn": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "optional": true, "requires": { "jsbn": "~0.1.0", @@ -19337,6 +19440,8 @@ }, "extend": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "optional": true }, "extend-shallow": { @@ -19397,6 +19502,8 @@ }, "extsprintf": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "optional": true }, "fast-deep-equal": { @@ -19519,6 +19626,8 @@ }, "forever-agent": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "optional": true }, "form-data": { @@ -19625,7 +19734,7 @@ }, "get-intrinsic": { "version": "1.1.3", - "dev": true, + "devOptional": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -19664,6 +19773,8 @@ }, "getpass": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "optional": true, "requires": { "assert-plus": "^1.0.0" @@ -19768,7 +19879,7 @@ }, "has-symbols": { "version": "1.0.3", - "dev": true + "devOptional": true }, "has-tostringtag": { "version": "1.0.0", @@ -19880,6 +19991,8 @@ }, "http-signature": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "optional": true, "requires": { "assert-plus": "^1.0.0", @@ -20191,6 +20304,8 @@ }, "is-typedarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "optional": true }, "is-unicode-supported": { @@ -20229,6 +20344,8 @@ }, "isstream": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "optional": true }, "istanbul-lib-coverage": { @@ -20309,6 +20426,8 @@ }, "jsbn": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "optional": true }, "jsesc": { @@ -20319,6 +20438,8 @@ }, "json-schema": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "optional": true }, "json-schema-traverse": { @@ -20334,6 +20455,8 @@ }, "json-stringify-safe": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "optional": true }, "json5": { @@ -20349,6 +20472,8 @@ }, "jsprim": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "optional": true, "requires": { "assert-plus": "1.0.0", @@ -21169,7 +21294,7 @@ }, "object-inspect": { "version": "1.12.2", - "dev": true + "devOptional": true }, "object-keys": { "version": "1.1.1", @@ -21372,6 +21497,8 @@ }, "performance-now": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "optional": true }, "picocolors": { @@ -21529,6 +21656,12 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "optional": true + }, "progress": { "version": "2.0.3", "dev": true @@ -21552,6 +21685,8 @@ }, "psl": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "optional": true }, "pump": { @@ -21591,6 +21726,12 @@ "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "optional": true + }, "queue-microtask": { "version": "1.2.3", "devOptional": true @@ -21745,20 +21886,20 @@ } }, "react-router": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.10.0.tgz", - "integrity": "sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", + "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", "requires": { - "@remix-run/router": "1.5.0" + "@remix-run/router": "1.11.0" } }, "react-router-dom": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", - "integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", + "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", "requires": { - "@remix-run/router": "1.5.0", - "react-router": "6.10.0" + "@remix-run/router": "1.11.0", + "react-router": "6.18.0" } }, "react-select": { @@ -21942,7 +22083,7 @@ }, "requires-port": { "version": "1.0.0", - "dev": true + "devOptional": true }, "resize-observer-polyfill": { "version": "1.5.1" @@ -22231,7 +22372,7 @@ }, "side-channel": { "version": "1.0.4", - "dev": true, + "devOptional": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -22506,7 +22647,9 @@ "dev": true }, "sshpk": { - "version": "1.17.0", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "requires": { "asn1": "~0.2.3", @@ -22849,11 +22992,23 @@ "dev": true }, "tough-cookie": { - "version": "2.5.0", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "optional": true, "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "optional": true + } } }, "tr46": { @@ -22893,6 +23048,8 @@ }, "tunnel-agent": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "optional": true, "requires": { "safe-buffer": "^5.0.1" @@ -22900,6 +23057,8 @@ }, "tweetnacl": { "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "optional": true }, "type-check": { @@ -23037,6 +23196,16 @@ "urix": { "version": "0.1.0" }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "optional": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "use": { "version": "3.1.1" }, @@ -23069,6 +23238,8 @@ }, "uuid": { "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "optional": true }, "v8-to-istanbul": { @@ -23092,17 +23263,13 @@ }, "verror": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "optional": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "optional": true - } } }, "vite": { diff --git a/package.json b/package.json index 1bd4fd65d0..174309c215 100644 --- a/package.json +++ b/package.json @@ -81,8 +81,8 @@ "react-is": "^18.2.0", "react-mathquill": "^1.0.3", "react-measure": "^2.5.2", - "react-router": "^6.9.0", - "react-router-dom": "^6.9.0", + "react-router": "^6.18.0", + "react-router-dom": "^6.18.0", "react-select": "^5.7.3", "react-table": "^7.7.0", "react-use-measure": "^2.1.1", @@ -127,7 +127,7 @@ }, "optionalDependencies": { "@esbuild/linux-arm64": "^0.17.19", - "cypress": "^12.12.0", + "cypress": "^13.3.3", "cypress-file-upload": "^5.0.8", "cypress-parallel": "^0.13.0", "cypress-plugin-tab": "^1.0.5", diff --git a/public/api/baseModel.php b/public/api/baseModel.php index 9b956b5168..7dcefb46b0 100644 --- a/public/api/baseModel.php +++ b/public/api/baseModel.php @@ -30,11 +30,14 @@ public static function runQuery($conn, $query) { public static function queryFetchAssoc($conn, $query) { $result = Base_Model::runQuery($conn, $query); if ($result->num_rows > 0) { - $rows = []; + $data = []; while($row = $result->fetch_assoc()){ - $rows[] = $row; + $data['rows'] = $row; + foreach($row as $key => $value){ + $data[$key][] = $value; + } } - return $rows; + return $data; } else { return []; } @@ -47,18 +50,23 @@ public static function queryFetchAssoc($conn, $query) { * * If more than one row is returned, throws an exception. */ - public static function queryExpectingOneRow($conn, $query) { - $rows = Base_Model::queryFetchAssoc($conn, $query); - - if (count($rows) == 1) { - return $rows[0]; - } else if (count($rows) == 0) { + public static function queryOneRowOrError($conn, $query) { + $data = Base_Model::queryFetchAssoc($conn, $query); + + if (count($data['rows']) == 1) { + return $data; + } else if (count($data['rows']) == 0) { return null; } else { + throw new Exception("Unexpected error, only expected one row from this query."); + error_log("Unexpected error, only expected one row from this query." . + "\n " . $conn->error . + "\n" . $query); } } + /** * Validate that a list of keys are present in a given associative array. * diff --git a/public/api/checkCredentials.php b/public/api/checkCredentials.php index 4efc6852aa..15213e03ad 100644 --- a/public/api/checkCredentials.php +++ b/public/api/checkCredentials.php @@ -6,70 +6,74 @@ header('Content-Type: application/json'); include "db_connection.php"; - +include "baseModel.php"; $emailaddress = mysqli_real_escape_string($conn,$_REQUEST["emailaddress"]); $nineCode = mysqli_real_escape_string($conn,$_REQUEST["nineCode"]); $deviceName = mysqli_real_escape_string($conn,$_REQUEST["deviceName"]); -//Check if expired -$sql = "SELECT TIMESTAMPDIFF(MINUTE, timestampOfSignInCode, NOW()) AS minutes -FROM user_device -WHERE email='$emailaddress' AND deviceName='$deviceName'"; +$response_arr; +try { + Base_Model::checkForRequiredInputs($_REQUEST,["emailaddress","nineCode","deviceName"]); -$result = $conn->query($sql); -$row = $result->fetch_assoc(); + //Check if expired + $sql = "SELECT TIMESTAMPDIFF(MINUTE, timestampOfSignInCode, NOW()) AS minutes + FROM user_device + WHERE email='$emailaddress' AND deviceName='$deviceName' + ORDER BY timestampOfSignInCode DESC + LIMIT 1 + "; -//Assume success and it already exists + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); + //Assume it already exists + $existed = true; + $hasFullName = false; + $reason = ""; -$success = 1; -$existed = 1; -$hasFullName = 0; -$reason = ""; + // throw new Exception("Code expired"); //DELETE ME!!! -//Check if it took longer than 10 minutes to enter the code -if ($row['minutes'] > 10){ - $success = 0; - $reason = "Code expired"; -}else{ + //Check if it took longer than 10 minutes to enter the code + if ($row['minutes'] > 10){ + throw new Exception("Code expired"); + } + + //Only the most recent one $sql = "SELECT signInCode AS nineCode FROM user_device - WHERE email='$emailaddress' AND deviceName='$deviceName'"; - $result = $conn->query($sql); - $row = $result->fetch_assoc(); + WHERE email='$emailaddress' AND deviceName='$deviceName' + ORDER BY timestampOfSignInCode DESC + LIMIT 1 + "; + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); if ($row["nineCode"] != $nineCode){ - $success = 0; - $reason = "Invalid Code"; - - }else{ + throw new Exception("Invalid Code"); + } //Valid code and not expired //Update signedIn on user_device table $sql = "UPDATE user_device SET signedIn='1' WHERE email='$emailaddress' AND deviceName='$deviceName'"; - $result = $conn->query($sql); + Base_Model::runQuery($conn,$sql); //Test if it's a new account - $sql = "SELECT firstName,lastName, screenName FROM user WHERE email='$emailaddress' "; - $result = $conn->query($sql); - $row = $result->fetch_assoc(); + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); if ($row["firstName"] != "" && $row["lastName"] != ""){ - $hasFullName = 1; + $hasFullName = true; } //Only new accounts won't have a screen name if ($row["screenName"] === null){ // New Account! - $existed = 0; + $existed = false; // Make a new profile // Random screen name @@ -83,28 +87,41 @@ $profile_pic = $profile_pics[$randomNumber]; // Store screen name and profile picture $sql = "UPDATE user SET screenName='$screen_name',profilePicture='$profile_pic' WHERE email='$emailaddress' "; - $result = $conn->query($sql); - } - - - - } - - -} + Base_Model::runQuery($conn,$sql); + } + $sql = "SELECT c.courseId + FROM course AS c + LEFT JOIN user AS u + ON u.userId = c.portfolioCourseForUserId + WHERE u.email = '$emailaddress'"; + $result = Base_Model::runQuery($conn,$sql); + $row = $result->fetch_assoc(); + $portfolioCourseId = "not_created"; + if ($result->num_rows > 0) { + $portfolioCourseId = $row['courseId']; + } $response_arr = array( - "success" => $success, + "success" => true, "existed" => $existed, "hasFullName" => $hasFullName, - "reason" => $reason, + "portfolioCourseId" => $portfolioCourseId, ); http_response_code(200); -// make it json format -echo json_encode($response_arr); -$conn->close(); +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); +} +?> diff --git a/public/api/getPortfolioActivity.php b/public/api/getPortfolioActivity.php new file mode 100644 index 0000000000..c43a39d03c --- /dev/null +++ b/public/api/getPortfolioActivity.php @@ -0,0 +1,110 @@ +query($sql); + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + if ($row['portfolioCourseForUserId'] != $userId){ + throw new Exception("You need to be the owner to view this overview."); + } + } + + $sql = " + SELECT label, + type, + courseId, + isDeleted, + isBanned, + isPublic, + CAST(jsonDefinition as CHAR) AS json, + CAST(learningOutcomes as CHAR) AS learningOutcomes, + imagePath + FROM course_content + WHERE doenetId = '$doenetId' + "; + $result = $conn->query($sql); + + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + + $label = $row['label']; + $type = $row['type']; + $courseId = $row['courseId']; + $isDeleted = $row['isDeleted']; + $isBanned = $row['isBanned']; + $isPublic = $row['isPublic']; + $json = json_decode($row["json"], true); + $imagePath = $row['imagePath']; + $learningOutcomes = json_decode($row['learningOutcomes'], true); + + + }else{ + throw new Exception("Activity not found."); + } + + + + $response_arr = [ + 'success' => true, + 'label' => $label, + 'courseId' => $courseId, + 'isDeleted' => $isDeleted, + 'isBanned' => $isBanned, + 'isPublic' => $isPublic, + 'json' => $json, + 'imagePath' => $imagePath, + 'activityData' => [ + 'type' => $type, + 'label' => $label, + 'imagePath' => $imagePath, + 'content' => $json['content'], + 'isSinglePage' => $json['isSinglePage'], + 'isPublic' => $isPublic, + 'version' => $json['version'], + 'learningOutcomes' => $learningOutcomes, + ], + ]; + // set response code - 200 OK + http_response_code(200); + +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); + +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); +} + +?> diff --git a/public/api/getPortfolioEditorData.php b/public/api/getPortfolioEditorData.php index 95debd94eb..11322ee72c 100644 --- a/public/api/getPortfolioEditorData.php +++ b/public/api/getPortfolioEditorData.php @@ -15,8 +15,14 @@ $response_arr; try { + if ($doenetId == ''){ + throw new Exception("Internal Error: missing doenetId"); + } + if ($publicEditor == ''){ + throw new Exception("Internal Error: missing publicEditor"); + } - //Check if it's in there portfolio + //Check if it's in their portfolio $sql = "SELECT cc.isPublic, diff --git a/public/api/getQuickCheckSignedIn.php b/public/api/getQuickCheckSignedIn.php index 6bcf783643..3fac4436e1 100644 --- a/public/api/getQuickCheckSignedIn.php +++ b/public/api/getQuickCheckSignedIn.php @@ -5,10 +5,18 @@ header('Access-Control-Allow-Credentials: true'); header('Content-Type: application/json'); -//ONLY TESTING IF THE SECURE SIGNED IN (JWT) COOKIE EXISTS -$signedIn = isset($_COOKIE["JWT"]); +$secureCookieExists = false; +if ($_COOKIE["JWT"] != NULL){ + $secureCookieExists = true; +} +$unsecureCookieExists = false; +if ($_COOKIE["JWT_JS"] != NULL){ + $unsecureCookieExists = true; +} -$response_arr = ['signedIn' => $signedIn]; +$response_arr = ['secureCookieExists' => $secureCookieExists, +'unsecureCookieExists' => $unsecureCookieExists, +]; // set response code - 200 OK http_response_code(200); diff --git a/public/api/jwt.php b/public/api/jwt.php index 7114779b3d..9f7e63f1e0 100644 --- a/public/api/jwt.php +++ b/public/api/jwt.php @@ -1,11 +1,6 @@ query($sql); -$row = $result->fetch_assoc(); + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); -//Check if it took longer than 10 minutes to enter the code -if ($row['minutes'] > 10) { - echo 'Code expired'; -} else { - $sql = "SELECT signInCode AS nineCode, userId - FROM user_device - WHERE email='$emailaddress' AND deviceName='$deviceName'"; - $result = $conn->query($sql); - $row = $result->fetch_assoc(); + //Check if it took longer than 10 minutes to enter the code + if ($row['minutes'] > 10) { + throw new Exception("Code expired."); + } + $sql = "SELECT signInCode AS nineCode,userId + FROM user_device + WHERE email='$emailaddress' AND deviceName='$deviceName' + ORDER BY timestampOfSignInCode DESC + LIMIT 1"; + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); $userId = $row['userId']; if ($row['nineCode'] != $nineCode) { - echo 'Invalid Code'; - } else { - //Valid code and not expired - http_response_code(200); + throw new Exception("Invalid Code."); + } + //Valid code and not expired + http_response_code(200); - $expirationTime = 0; - if ($stay == 1) { - $expirationTime = 2147483647; - } + $expirationTime = 0; + if ($stay == 1) { + $expirationTime = 2147483647; + } - $payload = [ - // "email" => $emailaddress, - 'userId' => $userId, - 'deviceName' => $deviceName, - // "expires" => $expirationTime - ]; - $jwt = JWT::encode($payload, $key); + $payload = [ + // "email" => $emailaddress, + 'userId' => $userId, + 'deviceName' => $deviceName, + // "expires" => $expirationTime + ]; + $jwt = JWT::encode($payload, $key); - $sql = "UPDATE user_device - SET signedIn = '1' - WHERE userId='$userId' AND deviceName='$deviceName'"; - $result = $conn->query($sql); + $sql = "UPDATE user_device + SET signedIn = '1' + WHERE userId='$userId' AND deviceName='$deviceName'"; + Base_Model::runQuery($conn,$sql); - $value = $jwt; + $value = $jwt; - $path = '/'; - //$domain = $ini_array['dbhost']; - $domain = $_SERVER["SERVER_NAME"]; - if ($domain == 'apache'){$domain = 'localhost';} - $isSecure = true; - if ($domain == 'apache') { - $domain = 'localhost'; - } - if ($domain == 'localhost') { - $isSecure = false; - } - $isHttpOnly = true; - setcookie( - 'JWT', - $value, - $expirationTime, - $path, - $domain, - $isSecure, - $isHttpOnly - ); - setcookie( - 'JWT_JS', - 1, - $expirationTime, - $path, - $domain, - $isSecure, - false - ); - header('Location: /signin'); //needs to store profile into localstorage + $path = '/'; + //$domain = $ini_array['dbhost']; + $domain = $_SERVER["SERVER_NAME"]; + if ($domain == 'apache'){$domain = 'localhost';} + $isSecure = true; + if ($domain == 'apache') { + $domain = 'localhost'; + } + if ($domain == 'localhost') { + $isSecure = false; + } + $isHttpOnly = true; + setcookie( + 'JWT', + $value, + $expirationTime, + $path, + $domain, + $isSecure, + $isHttpOnly + ); + setcookie( + 'JWT_JS', + 1, + $expirationTime, + $path, + $domain, + $isSecure, + false + ); - // setcookie("JWT", $value, array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>$isHttpOnly, "samesite"=>"strict")); - // setcookie("JWT_JS", 1, array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>false, "samesite"=>"strict")); + $response_arr = [ + 'success' => true, + ]; - // if ($newAccount == 1){ - // // header("Location: /accountsettings"); - // header("Location: /library"); - // }else{ - // // header("Location: /dashboard"); - // header("Location: /course"); - // } - } + http_response_code(200); + +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); + +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); } -$conn->close(); +?> diff --git a/public/api/saveUsersName.php b/public/api/saveUsersName.php index 06a704848e..0f1fbbabe8 100644 --- a/public/api/saveUsersName.php +++ b/public/api/saveUsersName.php @@ -6,34 +6,39 @@ header('Content-Type: application/json'); include "db_connection.php"; - - -$success = true; -$message = ''; - +include "baseModel.php"; $email = mysqli_real_escape_string($conn,$_REQUEST["email"]); $firstName = mysqli_real_escape_string($conn,$_REQUEST["firstName"]); $lastName = mysqli_real_escape_string($conn,$_REQUEST["lastName"]); - -$sql = " -UPDATE user -SET firstName='$firstName', -lastName='$lastName' -WHERE email='$email' -"; -$conn->query($sql); - -$response_arr = array( - 'success' => $success, - 'message' => $message, -); - -// set response code - 200 OK -http_response_code(200); - -// make it json format -echo json_encode($response_arr); - -$conn->close(); -?> +$response_arr; +try { + Base_Model::checkForRequiredInputs($_REQUEST,["email","firstName","lastName"]); + + $sql = " + UPDATE user + SET firstName='$firstName', + lastName='$lastName' + WHERE email='$email' + "; + Base_Model::runQuery($conn,$sql); + + $response_arr = array( + 'success' => true, + ); + + http_response_code(200); + +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); + +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); +} +?> \ No newline at end of file diff --git a/public/api/sendSignInEmail.php b/public/api/sendSignInEmail.php index 3e8ccba30e..d2e903d3e1 100644 --- a/public/api/sendSignInEmail.php +++ b/public/api/sendSignInEmail.php @@ -6,88 +6,120 @@ header('Content-Type: application/json'); include "db_connection.php"; +include "baseModel.php"; $emailaddress = mysqli_real_escape_string($conn,$_REQUEST["emailaddress"]); +$deviceName = mysqli_real_escape_string($conn,$_REQUEST["deviceName"]); -$deviceNames = include "deviceNames.php"; - -//Nine digit random number -$signInCode = rand(100000000,999999999); - -$sql = "SELECT email, userId -FROM user -WHERE email='$emailaddress'"; - -$result = $conn->query($sql); - -if ($result->num_rows > 0){ - //Already have an email with this account - $row = $result->fetch_assoc(); - $user_id = $row['userId']; - //unique deviceName - //Remove device names which are already in use - $sql = " - SELECT deviceName - FROM user_device - WHERE userId='$user_id' - "; - - $result = $conn->query($sql); - $used_deviceNames = array(); - while($row = $result->fetch_assoc()){ - array_push($used_deviceNames,$row['deviceName']); - } - $deviceNames = array_values(array_diff($deviceNames,$used_deviceNames)); - if (count($deviceNames) < 1){ - //Ran out of device names - $deviceName = include 'randomId.php'; + +$response_arr; +try { + Base_Model::checkForRequiredInputs($_REQUEST,["emailaddress"]); + + //Create a nine digit random number + $signInCode = rand(100000000,999999999); + + //Do we have an account with this email? + $sql = "SELECT email, userId + FROM user + WHERE email='$emailaddress'"; + + $userEmailArray = Base_Model::queryFetchAssoc($conn, $sql); + if (count($userEmailArray) < 1){ + //We need an account created + $user_id = include "randomId.php"; + $sql = "INSERT INTO user (userId,email) VALUE ('$user_id','$emailaddress')"; + Base_Model::runQuery($conn,$sql); }else{ - //Pick from what is left - $randomNumber = rand(0,(count($deviceNames) - 1)); - $deviceName = $deviceNames[$randomNumber]; + $user_id = $userEmailArray['userId'][0]; } + if (array_key_exists("deviceName",$_REQUEST)){ + //Already have a device name + //Just update the signInCode and timestampOfSignInCode + //of the latest entry of that device name + $sql = "UPDATE user_device + SET signInCode = '$signInCode', timestampOfSignInCode = NOW() + WHERE (userId, email, deviceName, timestampOfSignInCode) = ( + SELECT userId, email, deviceName, MAX(timestampOfSignInCode) + FROM ( + SELECT * FROM user_device + ) AS temp + WHERE userId = '$user_id' AND email = '$emailaddress' AND deviceName = '$deviceName' + ) + "; + Base_Model::runQuery($conn,$sql); -}else{ - //New email address - $user_id = include "randomId.php"; - $sql = "INSERT INTO user (userId,email) VALUE ('$user_id','$emailaddress')"; - $result = $conn->query($sql); - //Define device name - $randomNumber = rand(0,(count($deviceNames) - 1)); - $deviceName = $deviceNames[$randomNumber]; -} -$sql = "INSERT INTO user_device (userId,email,signInCode,timestampOfSignInCode, deviceName) - VALUE ('$user_id','$emailaddress','$signInCode',NOW(),'$deviceName')"; - $result = $conn->query($sql); + }else{ + //Select a device name + $deviceNames = include "deviceNames.php"; + //In order to maintain unique deviceNames + //remove device names which are already in use + $sql = " + SELECT deviceName + FROM user_device + WHERE userId='$user_id' + AND signedIn=1 + "; + + $devicesArray = Base_Model::queryFetchAssoc($conn, $sql); + $used_deviceNames = $devicesArray['deviceName'] != null ? $devicesArray['deviceName'] : []; + + $deviceNames = array_values(array_diff($deviceNames,$used_deviceNames)); + if (count($deviceNames) < 1){ + //Ran out of device names + $deviceName = include 'randomId.php'; + }else{ + //Pick from what is left + $randomNumber = rand(0,(count($deviceNames) - 1)); + $deviceName = $deviceNames[$randomNumber]; + } + + //Insert the device with the code so a user with the right code can sign in + $sql = "INSERT INTO user_device (userId,email,signInCode,timestampOfSignInCode, deviceName) + VALUE ('$user_id','$emailaddress','$signInCode',NOW(),'$deviceName')"; + Base_Model::runQuery($conn,$sql); + } + -// Generate and modify email content -$htmlContent = file_get_contents("signInEmail.html"); -$htmlContent = str_replace(array("deviceName", "signInCode"), array($deviceName, $signInCode), $htmlContent); + // Generate and modify email content + $htmlContent = file_get_contents("signInEmail.html"); + $htmlContent = str_replace(array("signInCode"), array($signInCode), $htmlContent); -$from = 'noreply@doenet.org'; -$fromName = 'Doenet Accounts'; -$subject = 'Sign-In Request'; + $from = 'noreply@doenet.org'; + $fromName = 'Doenet Accounts'; + $subject = 'Sign-In Request'; -// Set content-type header for sending HTML email -$headers = "MIME-Version: 1.0" . "\r\n"; -$headers .= "Content-type:text/html;charset=UTF-8" . "\r\n"; -$headers .= 'From: '.$fromName.'<'.$from.'>' . "\r\n"; + // Set content-type header for sending HTML email + $headers = "MIME-Version: 1.0" . "\r\n"; + $headers .= "Content-type:text/html;charset=UTF-8" . "\r\n"; + $headers .= 'From: '.$fromName.'<'.$from.'>' . "\r\n"; -//SEND EMAIL WITH CODE HERE -mail($emailaddress,$subject,$htmlContent, $headers); + //SEND EMAIL WITH CODE HERE + $mailSuccess = mail($emailaddress,$subject,$htmlContent, $headers); -$response_arr = array( - "success" => 1, - "deviceName" => $deviceName, - ); + if (!$mailSuccess && $mode != 'development'){ + throw new Exception("Sending Email Failed."); + } -// set response code - 200 OK -http_response_code(200); +$response_arr = [ + 'success' => true, + "deviceName" => $deviceName, + ]; -echo json_encode($response_arr); + http_response_code(200); +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); -$conn->close(); +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); +} diff --git a/public/api/signInEmail.html b/public/api/signInEmail.html index 04d0631196..27c70874a7 100644 --- a/public/api/signInEmail.html +++ b/public/api/signInEmail.html @@ -136,13 +136,10 @@ align="left" >

-
Welcome to Doenet, you've - requested a sign-in code for - deviceName.
+
Welcome to Doenet!

- Access code: + Sign-in code:
signInCode

diff --git a/public/api/signOut.php b/public/api/signOut.php index 7b9221be39..687314ea69 100644 --- a/public/api/signOut.php +++ b/public/api/signOut.php @@ -1,45 +1,54 @@ query($sql); //TODO: upgrade the script response - -// set response code - 200 OK -http_response_code(200); - -$path = '/'; -// $domain = $ini_array['dbhost']; -$domain = $_SERVER["SERVER_NAME"]; -if ($domain == 'apache'){$domain = 'localhost';} +var_dump($cookies); -$isSecure = true; -if ($domain=="localhost"){ - $isSecure = false; -} -$isHttpOnly = true; -$expirationTime = -3600; - -setcookie("JWT", "", $expirationTime, $path, $domain, $isSecure, $isHttpOnly); -setcookie("JWT_JS", "", $expirationTime, $path, $domain, $isSecure, false); -setcookie("EJWT", "", $expirationTime, $path, $domain, $isSecure, $isHttpOnly); -setcookie("EJWT_JS", "", $expirationTime, $path, $domain, $isSecure, false); -// setcookie("JWT", "", array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>$isHttpOnly, "samesite"=>"strict")); -// setcookie("JWT_JS", "", array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>false, "samesite"=>"strict")); -// setcookie("TrackingConsent", "", array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>false, "samesite"=>"strict")); -// make it json format -// echo json_encode($response_arr); - -$conn->close(); +?> diff --git a/src/Core/utils/buildActivityML.js b/src/Core/utils/buildActivityML.js new file mode 100644 index 0000000000..4e44e9634e --- /dev/null +++ b/src/Core/utils/buildActivityML.js @@ -0,0 +1,36 @@ +import { cidFromText } from "./cid"; + + +//Returns success and pageCID +//TODO: Currently only works for single page. Make it work with multipage +export async function buildSinglePageActivityML({ + activityId, + isAssigned, + courseId, + version, + doenetML, + itemWeights, //optional + shuffleItemWeights, //optional + numVariants, //optional +}) { + + const pageCID = await cidFromText(doenetML); + let attributeString = ` xmlns="https://doenet.org/spec/doenetml/v${version}" type="activity"`; + if (itemWeights) { + attributeString += ` itemWeights = "${itemWeights.join(" ")}"`; + } + if (shuffleItemWeights) { + attributeString += ` shuffleItemWeights="true"`; + } + if (numVariants !== undefined) { + attributeString += ` numVariants="${numVariants}"`; + } + + attributeString += ` isSinglePage="true"`; + + const pageML = ` \n`; + let activityDoenetML = `\n${pageML}`; + + return { success: true, pageCID, activityDoenetML }; +} + diff --git a/src/Tools/_framework/ChakraBasedComponents/AccountMenu.jsx b/src/Tools/_framework/ChakraBasedComponents/AccountMenu.jsx new file mode 100644 index 0000000000..797c1ee696 --- /dev/null +++ b/src/Tools/_framework/ChakraBasedComponents/AccountMenu.jsx @@ -0,0 +1,66 @@ +import { + Avatar, + Center, + Menu, + MenuButton, + MenuItem, + MenuList, + Text, + VStack, + useColorMode, + Button, + ButtonGroup, +} from "@chakra-ui/react"; +import React from "react"; +import { FaMoon, FaRobot, FaSun } from "react-icons/fa"; + +export default function AccountMenu({ firstName, lastName, email }) { + const { colorMode, toggleColorMode, setColorMode } = useColorMode(); + + return ( +
+ + + + + + + + + {firstName} {lastName} + + {email} + + + + {/* */} + + + + Sign Out + + + +
+ ); +} diff --git a/src/Tools/_framework/ChakraBasedComponents/AlertQueue.jsx b/src/Tools/_framework/ChakraBasedComponents/AlertQueue.jsx index 07d43a2b7a..e9e14bbfa6 100644 --- a/src/Tools/_framework/ChakraBasedComponents/AlertQueue.jsx +++ b/src/Tools/_framework/ChakraBasedComponents/AlertQueue.jsx @@ -8,13 +8,23 @@ import { } from "@chakra-ui/react"; import React from "react"; -export function AlertQueue({ alerts = [], setAlerts = () => {} }) { +export function AlertQueue({ + alerts = [], + setAlerts = () => {}, + short = false, +}) { return ( <> {alerts.map(({ type, title, description, id }) => { return ( - + {title} @@ -26,7 +36,7 @@ export function AlertQueue({ alerts = [], setAlerts = () => {} }) { data-test="Alert Close Button" position="absolute" right="8px" - top="8px" + top={short ? "0px" : "8px"} onClick={() => { setAlerts((preAlerts) => preAlerts.filter((alert) => alert.id !== id), diff --git a/src/Tools/_framework/ChakraBasedComponents/VariantSelect.jsx b/src/Tools/_framework/ChakraBasedComponents/VariantSelect.jsx index 755f09b916..cfbf1d0a02 100644 --- a/src/Tools/_framework/ChakraBasedComponents/VariantSelect.jsx +++ b/src/Tools/_framework/ChakraBasedComponents/VariantSelect.jsx @@ -1,17 +1,13 @@ -import { - ChevronDownIcon, - TriangleDownIcon, - TriangleUpIcon, -} from "@chakra-ui/icons"; +import { ChevronDownIcon } from "@chakra-ui/icons"; import { Button, HStack, - IconButton, Input, Menu, MenuButton, MenuItem, MenuList, + Text, Tooltip, } from "@chakra-ui/react"; import React, { useEffect, useRef, useState } from "react"; @@ -44,6 +40,9 @@ export default function VariantSelect({ return ( <> + + Variant + { setShowTooltip(false); @@ -57,8 +56,7 @@ export default function VariantSelect({ } @@ -105,44 +103,6 @@ export default function VariantSelect({ })} - - } - m={0} - onClick={() => { - if (index == array.length - 1) { - return; - } - const nextIndex = index + 1; - setIndex(nextIndex); - setValue(array[nextIndex]); - setInputValue(""); - onChange(nextIndex); - }} - /> - } - m={0} - onClick={() => { - if (index < 1) { - return; - } - const nextIndex = index - 1; - setIndex(nextIndex); - setValue(array[nextIndex]); - setInputValue(""); - onChange(nextIndex); - }} - /> ); diff --git a/src/Tools/_framework/HeaderControls/PublicNavigation.jsx b/src/Tools/_framework/HeaderControls/PublicNavigation.jsx index 31a3d4c866..27e838d88d 100644 --- a/src/Tools/_framework/HeaderControls/PublicNavigation.jsx +++ b/src/Tools/_framework/HeaderControls/PublicNavigation.jsx @@ -42,7 +42,7 @@ export default function PublicNavigation() { @@ -378,6 +391,7 @@ export function Portfolio() { setDoenetId={setDoenetId} onClose={settingsOnClose} onOpen={settingsOnOpen} + isNewActivity={newActivityDoenetId == activity.doenetId} /> ); })} diff --git a/src/Tools/_framework/Paths/PortfolioActivity.jsx b/src/Tools/_framework/Paths/PortfolioActivity.jsx new file mode 100644 index 0000000000..ba8c755f6b --- /dev/null +++ b/src/Tools/_framework/Paths/PortfolioActivity.jsx @@ -0,0 +1,2491 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { + redirect, + useLoaderData, + useNavigate, + useLocation, +} from "react-router"; +import { DoenetML } from "../../../Viewer/DoenetML"; +import CodeMirror from "../CodeMirror"; + +import { Form, useBeforeUnload, useFetcher } from "react-router-dom"; +import { + Link, + Box, + Button, + Center, + Editable, + EditableInput, + EditablePreview, + Flex, + Grid, + GridItem, + HStack, + Icon, + IconButton, + Select, + Spacer, + Tooltip, + VStack, + useEditableControls, + Text, + Drawer, + DrawerOverlay, + DrawerContent, + DrawerCloseButton, + DrawerHeader, + DrawerBody, + Tabs, + TabList, + Tab, + TabPanels, + TabPanel, + FormControl, + FormLabel, + Card, + Image, + Input, + FormErrorMessage, + Checkbox, + MenuList, + MenuItem, + MenuButton, + Menu, + CardBody, + InputRightElement, + InputGroup, + Progress, + useDisclosure, + useMediaQuery, +} from "@chakra-ui/react"; +import axios from "axios"; +import VariantSelect from "../ChakraBasedComponents/VariantSelect"; +import findFirstPageIdInContent from "../../../_utils/findFirstPage"; +import AccountMenu from "../ChakraBasedComponents/AccountMenu"; +import { + CheckIcon, + CloseIcon, + EditIcon, + QuestionOutlineIcon, + WarningTwoIcon, +} from "@chakra-ui/icons"; +import { SlLayers } from "react-icons/sl"; +import { FaCog, FaFileImage } from "react-icons/fa"; +import { BsClipboardPlus, BsGripVertical } from "react-icons/bs"; +import ErrorWarningPopovers from "../ChakraBasedComponents/ErrorWarningPopovers"; +import { useSetRecoilState } from "recoil"; +import { textEditorDoenetMLAtom } from "../../../_sharedRecoil/EditorViewerRecoil"; +import { useSaveDraft } from "../../../_utils/hooks/useSaveDraft"; +import { RxUpdate } from "react-icons/rx"; +import { cidFromText } from "../../../Core/utils/cid"; +import { AlertQueue } from "../ChakraBasedComponents/AlertQueue"; +import { HiOutlineX, HiPlus } from "react-icons/hi"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { GoKebabVertical } from "react-icons/go"; +import { MdOutlineCloudUpload } from "react-icons/md"; +import { useDropzone } from "react-dropzone"; +import { useCourse } from "../../../_reactComponents/Course/CourseActions"; +import { useSearchParams } from "react-router-dom"; + +import { FiBook } from "react-icons/fi"; +import Papa from "papaparse"; +import RouterLogo from "../RouterLogo"; +import { buildSinglePageActivityML } from "../../../Core/utils/buildActivityML"; + +export async function loader({ params, request }) { + let doenetId = params.doenetId; + let pageId = params.pageId; + const url = new URL(request.url); + let editModeInit = false; + if (url.searchParams.get("edit") == "true") { + editModeInit = true; + } + + try { + const { data } = await axios.get( + `/api/getPortfolioActivity.php?doenetId=${doenetId}`, + ); + + const { + label, + courseId, + // isDeleted, + // isBanned, + // isPublic, + json, + imagePath, + activityData, + } = data; + + let publicDoenetML = null; + let draftDoenetML = ""; + + //Links to activity shouldn't need to know the pageId so they use and underscore + if (pageId == "_") { + let nextPageId = findFirstPageIdInContent(json.content); + + //TODO: code what should happen when there are only orders and no pageIds + if (nextPageId != "_") { + return redirect(`/portfolioActivity/${doenetId}/${nextPageId}`); + } + } + + const response = await axios.get("/api/getPorfolioCourseId.php"); + let { firstName, lastName, email, portfolioCourseId } = response.data; + + const draftDoenetMLResponse = await axios.get( + `/media/byPageId/${pageId}.doenet`, + { transformResponse: (data) => data.toString() }, + ); + draftDoenetML = draftDoenetMLResponse.data; + + const lastKnownCid = await cidFromText(draftDoenetML); + + let onLoadPublicAndDraftAreTheSame = false; + + if (json.assignedCid != null) { + const { data: activityML } = await axios.get( + `/media/${json.assignedCid}.doenet`, + ); + + // console.log("activityML", activityML); + //Find the first page's doenetML + const regex = //; + const pageIds = activityML.match(regex); + + const pageCID = pageIds[1]; + + if (lastKnownCid == pageCID) { + onLoadPublicAndDraftAreTheSame = true; + } + + //Get the doenetML of the pageId. + //we need transformResponse because + //large numbers are simplified with toString if used on doenetMLResponse.data + //which was causing errors + + const publicDoenetMLResponse = await axios.get( + `/media/${pageCID}.doenet`, + { + transformResponse: (data) => data.toString(), + }, + ); + publicDoenetML = publicDoenetMLResponse.data; + } + + const supportingFileResp = await axios.get( + "/api/loadSupportingFileInfo.php", + { + params: { doenetId: params.doenetId }, + }, + ); + + let supportingFileData = supportingFileResp.data; + + //Win, Mac or Linux + let platform = "Linux"; + if (navigator.platform.indexOf("Win") != -1) { + platform = "Win"; + } else if (navigator.platform.indexOf("Mac") != -1) { + platform = "Mac"; + } + + return { + success: true, + message: "", + pageId, + doenetId, + publicDoenetML, + draftDoenetML, + label, + courseId, + // isDeleted, + // isBanned, + // isPublic, + json, + imagePath, + firstName, + lastName, + email, + platform, + lastKnownCid, + activityData, + supportingFileData, + editModeInit, + onLoadPublicAndDraftAreTheSame, + portfolioCourseId, + }; + } catch (e) { + return { success: false, message: e.response.data.message }; + } +} + +export async function action({ params, request }) { + const formData = await request.formData(); + let formObj = Object.fromEntries(formData); + const _action = formObj._action; + + //Don't let label be blank and trim it + let label = formObj?.label?.trim(); + if (label == "") { + label = "Untitled"; + } + + try { + if (formObj._action == "update label") { + const resp = await axios.get("/api/updatePortfolioActivityLabel.php", { + params: { doenetId: params.doenetId, label }, + }); + return { + _action: formObj._action, + label, + keyToUpdate: "activityLabel", + success: resp.data.success, + }; + } + + if (formObj._action == "publish draft") { + const { success, pageCID, activityDoenetML } = + await buildSinglePageActivityML({ + activityId: params.doenetId, + pageId: params.pageId, + isAssigned: true, + courseId: formObj.courseId, + version: formObj.version, + doenetML: formObj.doenetML, + }); + + //Make activity public + await axios.post("/api/updateContentSettingsByKey.php", { + doenetId: params.doenetId, + isPublic: "1", + }); + + //Save PageDoenetML + await axios.post("/api/saveDoenetML.php", { + doenetML: formObj.doenetML, + pageId: params.pageId, + courseId: formObj.courseId, + saveAsCid: true, + }); + + //Save ActivityDoenetML + await axios.post("/api/saveCompiledActivity.php", { + courseId: formObj.courseId, + doenetId: params.doenetId, + isAssigned: true, + activityDoenetML, + }); + + return { + _action: formObj._action, + success, + pageCID, + }; + } + + if (formObj._action == "update content via keyToUpdate") { + let value = formObj.value; + if (formObj.keyToUpdate == "learningOutcomes") { + value = JSON.parse(formObj.value); + } + + const resp = await axios.post("/api/updateContentSettingsByKey.php", { + doenetId: formObj.doenetId, + [formObj.keyToUpdate]: value, + }); + + return { + _action: formObj._action, + keyToUpdate: formObj.keyToUpdate, + value: formObj.value, + success: resp.data.success, + }; + } + + if (formObj._action == "update description") { + const resp = await axios.get("/api/updateFileDescription.php", { + params: { + doenetId: formObj.doenetId, + cid: formObj.cid, + description: formObj.description, + }, + }); + + return { + _action: formObj._action, + success: resp.data.success, + }; + } + + if (_action == "remove file") { + await axios.get("/api/deleteFile.php", { + params: { doenetId: formObj.doenetId, cid: formObj.cid }, + }); + + return { + success: true, + _action, + fileRemovedCid: formObj.cid, + }; + } + + if (_action == "noop") { + return { nothingToReturn: true }; + } + } catch (e) { + return { + success: false, + _action: formObj._action, + message: e.response.data.message, + }; + } +} + +//This is separate as wasn't updating when defaultValue was changed +function EditableActivityLabel({ setMainAlerts }) { + const { label: loaderLabel } = useLoaderData(); + const [label, setLabel] = useState(loaderLabel); + const fetcher = useFetcher(); + + useEffect(() => { + if (fetcher.state == "loading") { + const { success, message } = fetcher.data; + if (success) { + setMainAlerts([ + { + type: "success", + id: "label", + title: "Label Updated!", + }, + ]); + } else { + setMainAlerts([ + { + type: "error", + id: "label", + title: message, + }, + ]); + } + } + }, [fetcher.state, fetcher.data, setMainAlerts]); + + let lastActivityDataLabel = useRef(loaderLabel); + + //Update when something else updates the label + if (loaderLabel != lastActivityDataLabel.current) { + if (label != loaderLabel) { + setLabel(loaderLabel); + } + } + lastActivityDataLabel.current = loaderLabel; + + function EditableControls() { + const { isEditing, getEditButtonProps } = useEditableControls(); + + return isEditing ? ( + } + {...getEditButtonProps()} + /> + ) : ( + } + {...getEditButtonProps()} + /> + ); + } + + return ( + { + setLabel(value); + }} + onSubmit={(value) => { + let submitValue = value; + if (submitValue == "") { + submitValue = "Untitled"; + setLabel("Untitled"); + } + + //Only fire when label changed + if (loaderLabel != submitValue) { + setMainAlerts([ + { + type: "info", + id: "Label", + title: "Attempting to update label", + }, + ]); + + fetcher.submit( + { _action: "update label", label: submitValue }, + { method: "post" }, + ); + } + }} + > + + + + + + + ); +} + +function formatBytes(bytes) { + var marker = 1024; // Change to 1000 if required + var decimal = 1; // Change as required + var kiloBytes = marker; + var megaBytes = marker * marker; + var gigaBytes = marker * marker * marker; + var teraBytes = marker * marker * marker * marker; + + if (bytes < kiloBytes) return bytes + " Bytes"; + else if (bytes < megaBytes) + return (bytes / kiloBytes).toFixed(decimal) + " KB"; + else if (bytes < gigaBytes) + return (bytes / megaBytes).toFixed(decimal) + " MB"; + else if (bytes < teraBytes) + return (bytes / gigaBytes).toFixed(decimal) + " GB"; + else return (bytes / teraBytes).toFixed(decimal) + " TB"; +} + +export function GeneralActivityControls({ + fetcher, + courseId, + doenetId, + activityData, + setPublicAndDraftAreTheSame, + setAlerts, +}) { + let { isPublic, label, imagePath: dataImagePath } = activityData; + if (!isPublic && activityData?.public) { + isPublic = activityData.public; + } + + let numberOfFilesUploading = useRef(0); + let [imagePath, setImagePath] = useState(dataImagePath); + let [successMessage, setSuccessMessage] = useState(""); + let [keyToUpdateState, setKeyToUpdateState] = useState(""); + + useEffect(() => { + if (fetcher.state == "loading") { + const { success, keyToUpdate, message } = fetcher.data; + if (success && keyToUpdate == keyToUpdateState) { + setAlerts([ + { + type: "success", + id: keyToUpdateState, + title: successMessage, + }, + ]); + } else if (!success && keyToUpdate == keyToUpdateState) { + setAlerts([ + { + type: "error", + id: keyToUpdateState, + title: message, + }, + ]); + } else { + console.log("else fetcher.data", fetcher.data); + // throw Error(message); + } + } + }, [ + fetcher.state, + fetcher.data, + keyToUpdateState, + successMessage, + setAlerts, + ]); + + const onDrop = useCallback( + async (files) => { + let success = true; + const file = files[0]; + if (files.length > 1) { + success = false; + //Should we just grab the first one and ignore the rest + console.log("Only one file upload allowed!"); + } + + //Only upload one batch at a time + if (numberOfFilesUploading.current > 0) { + console.log( + "Already uploading files. Please wait before sending more.", + ); + success = false; + } + + //If any settings aren't right then abort + if (!success) { + return; + } + + numberOfFilesUploading.current = 1; + + let image = await window.BrowserImageResizer.readAndCompressImage(file, { + quality: 0.9, + maxWidth: 350, + maxHeight: 234, + debug: true, + }); + // const convertToBase64 = (blob) => { + // return new Promise((resolve) => { + // var reader = new FileReader(); + // reader.onload = function () { + // resolve(reader.result); + // }; + // reader.readAsDataURL(blob); + // }); + // }; + // let base64Image = await convertToBase64(image); + // console.log("image",image) + // console.log("base64Image",base64Image) + + //Upload files + const reader = new FileReader(); + reader.readAsDataURL(image); //This one could be used with image source to preview image + + reader.onabort = () => {}; + reader.onerror = () => {}; + reader.onload = () => { + const uploadData = new FormData(); + // uploadData.append('file',file); + uploadData.append("file", image); + uploadData.append("doenetId", doenetId); + + axios + .post("/api/activityThumbnailUpload.php", uploadData) + .then((resp) => { + let { data } = resp; + // console.log("RESPONSE data>", data); + + //uploads are finished clear it out + numberOfFilesUploading.current = 0; + let { success, cid, msg, asFileName } = data; + if (success) { + setImagePath(`/media/${cid}.jpg`); + //Refresh images in portfolio + fetcher.submit( + { + _action: "noop", + }, + { method: "post" }, + ); + setAlerts([ + { + type: "success", + id: cid, + title: "Activity thumbnail updated!", + }, + ]); + } else { + setAlerts([{ type: "error", id: cid, title: msg }]); + } + }); + }; + }, + [doenetId], + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + + let learningOutcomesInit = activityData.learningOutcomes; + if (learningOutcomesInit == null) { + learningOutcomesInit = [""]; + } + let [learningOutcomes, setLearningOutcomes] = useState(learningOutcomesInit); + + let [labelValue, setLabel] = useState(label); + let [labelIsInvalid, setLabelIsInvalid] = useState(false); + + let [checkboxIsPublic, setCheckboxIsPublic] = useState(isPublic); + const { compileActivity, updateAssignItem } = useCourse(courseId); + + function saveActivityLabel() { + // Turn on/off label error messages and + // only set the value if it's not blank + if (labelValue == "") { + setLabelIsInvalid(true); + } else { + if (labelIsInvalid) { + setLabelIsInvalid(false); + } + + //Alert Messages + setSuccessMessage("Activity Label Updated"); + setKeyToUpdateState("activityLabel"); + setAlerts([ + { + type: "info", + id: "activityLabel", + title: "Attempting to update activity label.", + }, + ]); + + fetcher.submit( + { _action: "update label", label: labelValue }, + { method: "post" }, + ); + } + } + + function saveLearningOutcomes({ nextLearningOutcomes } = {}) { + let learningOutcomesToSubmit = learningOutcomes; + if (nextLearningOutcomes) { + learningOutcomesToSubmit = nextLearningOutcomes; + } + + let serializedLearningOutcomes = JSON.stringify(learningOutcomesToSubmit); + fetcher.submit( + { + _action: "update content via keyToUpdate", + keyToUpdate: "learningOutcomes", + value: serializedLearningOutcomes, + doenetId, + }, + { method: "post" }, + ); + } + + return ( + <> +
+ + Thumbnail + + {isDragActive ? ( + + + + + + Drop Image Here + + + ) : ( + + + + Activity Card Image + + )} + + + + + Label + + { + setLabel(e.target.value); + }} + onBlur={saveActivityLabel} + onKeyDown={(e) => { + if (e.key == "Enter") { + saveActivityLabel(); + } + }} + /> + + Error - A label for the activity is required. + + + + + Learning Outcomes + + {learningOutcomes.map((outcome, i) => { + return ( + + { + setLearningOutcomes((prev) => { + let next = [...prev]; + next[i] = e.target.value; + return next; + }); + }} + onBlur={(e) => { + //Only update when changed + if (e.target.value != activityData.learningOutcomes[i]) { + //Alert Messages + setSuccessMessage( + `Updated learning outcome #${i + 1}.`, + ); + setKeyToUpdateState("learningOutcomes"); + setAlerts([ + { + type: "info", + id: "learningOutcomes", + title: `Attempting to update learning outcome #${ + i + 1 + }.`, + }, + ]); + saveLearningOutcomes({ + nextLearningOutcomes: learningOutcomes, + }); + } + }} + onKeyDown={(e) => { + if (e.key == "Enter") { + //Alert Messages + setSuccessMessage( + `Updated learning outcome #${i + 1}.`, + ); + setKeyToUpdateState("learningOutcomes"); + setAlerts([ + { + type: "info", + id: "learningOutcomes", + title: `Attempting to update learning outcome #${ + i + 1 + }.`, + }, + ]); + saveLearningOutcomes({ + nextLearningOutcomes: learningOutcomes, + }); + } + }} + placeholder={`Learning Outcome #${i + 1}`} + data-text={`Learning Outcome #${i}`} + /> + } + onClick={() => { + let nextLearningOutcomes = [...learningOutcomes]; + if (learningOutcomes.length < 2) { + nextLearningOutcomes = [""]; + } else { + nextLearningOutcomes.splice(i, 1); + } + //Alert Messages + setSuccessMessage(`Deleted learning outcome #${i + 1}.`); + setKeyToUpdateState("learningOutcomes"); + setAlerts([ + { + type: "info", + id: "learningOutcomes", + title: `Attempting to delete learning outcome #${ + i + 1 + }.`, + }, + ]); + + setLearningOutcomes(nextLearningOutcomes); + saveLearningOutcomes({ nextLearningOutcomes }); + }} + /> + + ); + })} + +
+ 9} + data-test={`add a learning outcome button`} + variant="outline" + width="80%" + size="xs" + icon={} + onClick={() => { + let nextLearningOutcomes = [...learningOutcomes]; + if (learningOutcomes.length < 9) { + nextLearningOutcomes.push(""); + } + + //Alert Messages + setSuccessMessage("Blank learning outcome added."); + setKeyToUpdateState("learningOutcomes"); + setAlerts([ + { + type: "info", + id: "learningOutcomes", + title: "Attempting to add a learning outcome.", + }, + ]); + + setLearningOutcomes(nextLearningOutcomes); + saveLearningOutcomes({ nextLearningOutcomes }); + }} + /> +
+
+
+ + Visibility + { + let nextIsPublic = "0"; + if (e.target.checked) { + nextIsPublic = "1"; + setPublicAndDraftAreTheSame(true); + //Process making activity public here + compileActivity({ + activityDoenetId: doenetId, + isAssigned: true, + courseId, + activity: { + version: activityData.version, + isSinglePage: true, + content: activityData.content, + }, + // successCallback: () => { + // addToast('Activity Assigned.', toastType.INFO); + // }, + }); + updateAssignItem({ + doenetId, + isAssigned: true, + successCallback: () => { + //addToast(assignActivityToast, toastType.INFO); + }, + }); + } + let title = "Setting Activity as public."; + let nextSuccessMessage = "Activity is public."; + if (nextIsPublic == "0") { + title = "Setting Activity as private."; + nextSuccessMessage = "Activity is private."; + } + + //Alert Messages + setSuccessMessage(nextSuccessMessage); + setKeyToUpdateState("isPublic"); + setAlerts([ + { + type: "info", + id: "isPublic", + title, + }, + ]); + + setCheckboxIsPublic(nextIsPublic); + fetcher.submit( + { + _action: "update content via keyToUpdate", + keyToUpdate: "isPublic", + value: nextIsPublic, + doenetId, + }, + { method: "post" }, + ); + }} + > + Public{" "} + + + + + + + +
+ + ); +} + +function SupportFilesControls({ alerts, setAlerts }) { + const { supportingFileData, doenetId } = useLoaderData(); + const { supportingFiles, userQuotaBytesAvailable, quotaBytes } = + supportingFileData; + + const fetcher = useFetcher(); + //Update messages after action completes + if (fetcher.data) { + if (fetcher.data._action == "remove file") { + let newAlerts = [...alerts]; + const index = newAlerts.findIndex( + (obj) => obj.id == fetcher.data.fileRemovedCid && obj.stage == 1, + ); + if (index !== -1) { + newAlerts.splice(index, 1, { + id: newAlerts[index].id, + type: "success", + title: `Removed`, + description: newAlerts[index].description, + stage: 2, + }); + setAlerts(newAlerts); + } + } else if (fetcher.data._action == "update description") { + //Guard against infinite loops + if (alerts[0]?.description != "Updated file description.") { + setAlerts([ + { + type: "success", + id: `update file description`, + description: "Updated file description.", + }, + ]); + } + } + } + + function updateFileDescription({ cid, description }) { + setAlerts([ + { + type: "info", + id: `update file description`, + description: "Attempting to update file description.", + }, + ]); + fetcher.submit( + { + _action: "update description", + doenetId, + cid, + description, + }, + { method: "post" }, + ); + } + + const onDrop = useCallback(async (acceptedFiles) => { + acceptedFiles.forEach((file) => { + const reader = new FileReader(); + + reader.onabort = () => console.log("file reading was aborted"); + reader.onerror = () => console.log("file reading has failed"); + reader.onload = async (event) => { + let columnTypes = ""; + if (file.type == "text/csv") { + const dataURL = event.target.result; + const csvString = atob(dataURL.split(",")[1]); + const parsedData = Papa.parse(csvString, { + dynamicTyping: true, + }).data; + columnTypes = parsedData + .slice(1)[0] + .reduce((acc, val) => { + if (typeof val === "number") { + return `${acc}Number `; + } else { + return `${acc}Text `; + } + }, "") + .trim(); + } + const uploadData = new FormData(); + uploadData.append("file", file); + uploadData.append("doenetId", doenetId); + uploadData.append("columnTypes", columnTypes); + + let resp = await axios.post("/api/supportFileUpload.php", uploadData); + + if (resp.data.success) { + setAlerts([ + { + id: `uploadsuccess${resp.data.cid}`, + type: "success", + title: `File '${resp.data.asFileName}' Uploaded Successfully`, + description: "", + }, + ]); + } else { + setAlerts([ + { + id: resp.data.asFileName, + type: "error", + title: resp.data.msg, + description: "", + }, + ]); + } + + fetcher.submit({ _action: "noop" }, { method: "post" }); + }; + reader.readAsDataURL(file); //This one could be used with image source to preview image + }); + }, []); + + const { fileRejections, getRootProps, getInputProps, isDragActive } = + useDropzone({ + onDrop, + maxFiles: 1, + maxSize: 1048576, + accept: ".csv,.jpg,.png", + }); + + let handledTooMany = false; + fileRejections.map((rejection) => { + if (rejection.errors[0].code == "too-many-files") { + if (alerts[0]?.id != "too-many-files" && !handledTooMany) { + handledTooMany = true; + setAlerts([ + { + id: "too-many-files", + type: "error", + title: "Can only upload one file at a time.", + description: "", + }, + ]); + } + } else { + const index = alerts.findIndex((obj) => obj.id == rejection.file.name); + if (index == -1) { + setAlerts([ + { + id: rejection.file.name, + type: "error", + title: `Can't Upload '${rejection.file.name}'`, + description: rejection.errors[0].message, + }, + ]); + } + } + }); + + return ( + <> + + + Account Space Available + {/* Note: I wish we could change this color */} + + + + +
+ + + {isDragActive ? ( + + + + + + Drop Files + + + ) : ( + + + + + + + Drop a file here, + + + or click to select a file + + + )} +
+ + + {supportingFiles.map((file, i) => { + let previewImagePath = `/media/${file.fileName}`; + + let fileNameNoExtension = file.fileName.split(".")[0]; + + let doenetMLCode = ``; + + if (file.fileType == "text/csv") { + previewImagePath = "/activity_default.jpg"; + //Fix the name so it can't break the rules + const doenetMLName = file.description + .replace(/[^a-zA-Z0-9]/g, "_") + .replace(/^([^a-zA-Z])/, "d$1"); + + doenetMLCode = ``; + } + //Only allow to copy doenetML if they entered a description + if (file.description == "") { + return ( + + + Support File Image + + + + +
+ + File name: {file.asFileName} + +
+ + } + variant="ghost" + /> + + { + setAlerts([ + { + id: file.cid, + type: "info", + title: "Removing", + description: file.asFileName, + stage: 1, + }, + ]); + fetcher.submit( + { + _action: "remove file", + doenetId, + cid: file.cid, + }, + { method: "post" }, + ); + }} + > + Remove + + + +
+ + {file.fileType == "text/csv" ? ( + <>DoenetML Name needed to use file + ) : ( + <>Alt Text Description required to use file + )} + + + { + updateFileDescription({ + cid: file.cid, + description: e?.target?.value, + }); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + updateFileDescription({ + cid: file.cid, + description: e?.target?.value, + }); + } + }} + /> + + {/* Fires on blur */} + + + + + +
+
+
+
+ ); + } + + return ( + + + Support File Image + + + + + + {/* TODO: Make this editable */} + { + updateFileDescription({ + cid: file.cid, + description: value, + }); + }} + > + + + + + {file.fileType == "text/csv" ? ( + <>{file.fileType} + ) : ( + <> + {file.fileType} {file.width} x {file.height} + + )} + + + + + + + } + variant="ghost" + /> + + { + setAlerts([ + { + id: file.cid, + type: "info", + title: "Removing", + description: file.description, + stage: 1, + }, + ]); + fetcher.submit( + { + _action: "remove file", + doenetId, + cid: file.cid, + }, + { method: "post" }, + ); + }} + > + Remove + + + + { + setAlerts([ + { + id: file.cid, + type: "info", + title: "DoenetML Code copied to the clipboard", + description: `for ${file.description}`, + }, + ]); + }} + text={doenetMLCode} + > + + + + + + + + + ); + })} +
+ + ); +} + +function PortfolioActivitySettingsDrawer({ + isOpen, + onClose, + finalFocusRef, + controlsTabsLastIndex, + setPublicAndDraftAreTheSame, +}) { + const { courseId, doenetId, activityData } = useLoaderData(); + //Need fetcher at this level to get label refresh + //when close drawer after changing label + const fetcher = useFetcher(); + let [alerts, setAlerts] = useState([]); + + return ( + + + + + +
+ Activity Controls +
+ {alerts.length > 0 ? ( + + ) : ( + + )} +
+ + + + + (controlsTabsLastIndex.current = 0)} + data-test="General Tab" + > + General + + (controlsTabsLastIndex.current = 1)} + data-test="Support Files Tab" + > + Support Files + + + + + + + + + + + + + + +
+
+ ); +} + +export function PortfolioActivity() { + const { + success, + message, + doenetId, + publicDoenetML, + draftDoenetML, + label, + courseId, + firstName, + lastName, + email, + platform, + activityData, + editModeInit, + onLoadPublicAndDraftAreTheSame, + portfolioCourseId, + // pageId, + // isDeleted, + // isBanned, + // isPublic, + // lastKnownCid, + } = useLoaderData(); + // const { signedIn } = useOutletContext(); + + if (!success) { + throw new Error(message); + } + + const [narrowMode] = useMediaQuery("(max-width: 1000px)"); + const fetcher = useFetcher(); + + const [editMode, setEditMode] = useState(editModeInit); + const [publicAndDraftAreTheSame, setPublicAndDraftAreTheSame] = useState( + onLoadPublicAndDraftAreTheSame, + ); + + let [mainAlerts, setMainAlerts] = useState([]); + let [successMessage, setSuccessMessage] = useState(""); + let [keyToUpdateState, setKeyToUpdateState] = useState(""); + + useEffect(() => { + if (fetcher.state == "loading") { + const { success, keyToUpdate, message, _action } = fetcher.data; + if ( + success && + (keyToUpdate == keyToUpdateState || _action == keyToUpdateState) + ) { + setMainAlerts([ + { + type: "success", + id: keyToUpdateState, + title: successMessage, + }, + ]); + } else if ( + !success && + (keyToUpdate == keyToUpdateState || _action == keyToUpdateState) + ) { + setMainAlerts([ + { + type: "error", + id: keyToUpdateState, + title: message, + }, + ]); + } else { + console.log("else fetcher.data", fetcher.data); + // throw Error(message); + } + } + }, [ + fetcher.state, + fetcher.data, + keyToUpdateState, + successMessage, + setMainAlerts, + ]); + + //Warning: this will reboot codeMirror Editor sending cursor to the top + let initializeEditorDoenetML = useRef(draftDoenetML); + let textEditorDoenetML = useRef(draftDoenetML); + + const [viewerDoenetML, setViewerDoenetML] = useState(draftDoenetML); + const [layer, setLayer] = useState("draft"); + + const { + isOpen: controlsAreOpen, + onOpen: controlsOnOpen, + onClose: controlsOnClose, + } = useDisclosure(); + const controlsBtnRef = useRef(null); + let controlsTabsLastIndex = useRef(0); + + const [errorsAndWarnings, setErrorsAndWarningsCallback] = useState({ + errors: [], + warnings: [], + }); + + const warningsLevel = 1; //TODO: eventually give user ability adjust warning level filter + const warningsObjs = errorsAndWarnings.warnings.filter( + (w) => w.level <= warningsLevel, + ); + const errorsObjs = [...errorsAndWarnings.errors]; + + useEffect(() => { + const handleDocumentKeyDown = (event) => { + if ( + (platform == "Mac" && event.metaKey && event.code === "KeyU") || + (platform != "Mac" && event.ctrlKey && event.code === "KeyU") + ) { + event.preventDefault(); + event.stopPropagation(); + if (controlsAreOpen) { + controlsOnClose(); + } else { + controlsOnOpen(); + } + } + }; + + window.addEventListener("keydown", handleDocumentKeyDown); + + return () => { + window.removeEventListener("keydown", handleDocumentKeyDown); + }; + }, [ + textEditorDoenetML, + platform, + controlsOnOpen, + controlsOnClose, + controlsAreOpen, + ]); + + useEffect(() => { + document.title = `${label} - Doenet`; + }, [label]); + + const mainAlertQueue = ( + + ); + + return ( + <> + + + + + + + + + + + + + {!narrowMode && ( + + {mainAlertQueue} + + )} + + + + + {editMode ? ( + + ) : ( + <> + + {publicDoenetML == null ? ( + Draft + ) : ( + + )} + + )} + + + + + + + + + + + + + + + + + + + ); +} + +function ViewerPanel({ + layer, + editMode, + setEditMode, + viewerDoenetML, + setErrorsAndWarningsCallback, + narrowMode, +}) { + const navigate = useNavigate(); + const location = useLocation(); + let [_, setSearchParams] = useSearchParams(); + + const [variants, setVariants] = useState({ + index: 1, + numVariants: 1, + allPossibleVariants: ["a"], + }); + + //Not narrow + let wrappingHeight = "calc(100vh - 50px)"; + if (narrowMode) { + //Narrow not editting + wrappingHeight = "calc(100vh - 90px)"; + if (editMode) { + //Narrow and editting + wrappingHeight = "calc(50vh - 45px)"; + } + } + + return ( + + 1 ? "space-between" : "flex-end"} + > + {variants.numVariants > 1 && ( + + + setVariants((prev) => { + let next = { ...prev }; + next.index = index + 1; + return next; + }) + } + /> + + )} + {editMode ? ( + + ) : ( + + + + )} + + + + + + + ); +} + +function EditorPanel({ + textEditorDoenetML, + setViewerDoenetML, + initializeEditorDoenetML, + setEditMode, + warningsObjs, + errorsObjs, + narrowMode, + setPublicAndDraftAreTheSame, +}) { + const { + pageId, + // doenetId, + // publicDoenetML, + // draftDoenetML, + courseId, + platform, + lastKnownCid, + } = useLoaderData(); + + let [codeChanged, setCodeChanged] = useState(false); + const codeChangedRef = useRef(null); //To keep value up to date in the code mirror function + codeChangedRef.current = codeChanged; + const setEditorDoenetML = useSetRecoilState(textEditorDoenetMLAtom); + let [_, setSearchParams] = useSearchParams(); + + let editorRef = useRef(null); + let timeout = useRef(null); + + let lastKnownCidRef = useRef(lastKnownCid); + let backupOldDraft = useRef(true); + let inTheMiddleOfSaving = useRef(false); + let postponedSaving = useRef(false); + + const { saveDraft } = useSaveDraft(); + + const handleSaveDraft = useCallback(async () => { + const doenetML = textEditorDoenetML.current; + const lastKnownCid = lastKnownCidRef.current; + const backup = backupOldDraft.current; + + if (inTheMiddleOfSaving.current) { + postponedSaving.current = true; + } else { + inTheMiddleOfSaving.current = true; + let result = await saveDraft({ + pageId, + courseId, + backup, + lastKnownCid, + doenetML, + }); + + if (result.success) { + backupOldDraft.current = false; + lastKnownCidRef.current = result.cid; + } + inTheMiddleOfSaving.current = false; + timeout.current = null; + + //If we postponed then potentially + //some changes were saved again while we were saving + //so save again + if (postponedSaving.current) { + postponedSaving.current = false; + handleSaveDraft(); + } + } + }, [ + pageId, + courseId, + saveDraft, + textEditorDoenetML, + setPublicAndDraftAreTheSame, + ]); + + //Save on refresh or leaving the site + useBeforeUnload(() => { + if (codeChanged) { + handleSaveDraft(); + } + }); + + //Save when navigating somewhere else in Doenet.org + useEffect(() => { + return () => { + if (codeChanged) { + handleSaveDraft(); + } + }; + }, [handleSaveDraft, codeChanged]); + + useEffect(() => { + const handleEditorKeyDown = (event) => { + if ( + (platform == "Mac" && event.metaKey && event.code === "KeyS") || + (platform != "Mac" && event.ctrlKey && event.code === "KeyS") + ) { + event.preventDefault(); + event.stopPropagation(); + setViewerDoenetML(textEditorDoenetML.current); + setCodeChanged(false); + clearTimeout(timeout.current); + handleSaveDraft(); + } + }; + + window.addEventListener("keydown", handleEditorKeyDown); + + return () => { + window.removeEventListener("keydown", handleEditorKeyDown); + }; + }, [textEditorDoenetML, platform, handleSaveDraft, setViewerDoenetML]); + + return ( + + + + + + + + + + + Documentation + + + + + + + + + { + setPublicAndDraftAreTheSame(false); //Any text change can be published + textEditorDoenetML.current = value; + setEditorDoenetML(value); + if (!codeChangedRef.current) { + setCodeChanged(true); + } + // Debounce save to server at 3 seconds + clearTimeout(timeout.current); + timeout.current = setTimeout(async function () { + handleSaveDraft(); + }, 3000); //3 seconds + }} + /> + + + + + + + + + + ); +} + +const clamp = ( + value, + min = Number.POSITIVE_INFINITY, + max = Number.NEGATIVE_INFINITY, +) => { + return Math.min(Math.max(value, min), max); +}; + +const MainContent = ({ + layer, + editMode, + setEditMode, + viewerDoenetML, + setErrorsAndWarningsCallback, + textEditorDoenetML, + setViewerDoenetML, + initializeEditorDoenetML, + warningsObjs, + errorsObjs, + narrowMode, + mainAlertQueue, + setPublicAndDraftAreTheSame, +}) => { + const centerWidth = "10px"; + const wrapperRef = useRef(); + + const calculateTemplateColumns = useCallback( + ({ leftPixels, rightPixels, browserWidth }) => { + //Not in edit mode or smaller than the stacked layout breakpoint + if (!editMode || narrowMode) { + return; + } + //Lock to not squish either side too much + if (leftPixels < 200) { + leftPixels = 200; + } + if (rightPixels < 350) { + leftPixels = browserWidth - 350; + } + + if (leftPixels >= 850) { + leftPixels = 850; + } + + let proportion = clamp(leftPixels / browserWidth, 0, 1); + + return `${proportion}fr ${centerWidth} ${1 - proportion}fr`; + }, + [editMode, narrowMode], + ); + + function updateWrapper({ leftPixels, rightPixels, browserWidth }) { + //Not in edit mode or smaller than the stacked layout breakpoint + if (!editMode || narrowMode) { + return; + } + //Lock to not squish either side too much + if (leftPixels < 200) { + leftPixels = 200; + } + if (rightPixels < 350) { + leftPixels = browserWidth - 350; + } + + if (leftPixels >= 850) { + leftPixels = 850; + } + + let proportion = clamp(leftPixels / browserWidth, 0, 1); + //using a ref to save without react refresh + wrapperRef.current.style.gridTemplateColumns = calculateTemplateColumns({ + leftPixels, + rightPixels, + browserWidth, + }); + wrapperRef.current.proportion = proportion; + } + + //Listen to resize to enforce min sizes + useEffect(() => { + window.addEventListener("resize", handleWindowResize); + return () => { + window.removeEventListener("resize", handleWindowResize); + }; + }); + + useEffect(() => { + let templateAreas = `"viewer"`; + let templateColumns = `1fr`; + let templateRows = `1fr`; + if (editMode) { + if (narrowMode) { + templateAreas = `"alerts" + "viewer" + "textEditor"`; + templateColumns = `1fr`; + templateRows = `40px .5fr .5fr`; + } else { + templateAreas = `"viewer middleGutter textEditor"`; + const browserWidth = wrapperRef.current.clientWidth; + let leftPixels = 0.5 * browserWidth; + let rightPixels = wrapperRef.current.clientWidth - leftPixels; + templateColumns = calculateTemplateColumns({ + leftPixels, + rightPixels, + browserWidth, + }); + templateRows = `1fr`; + } + } else { + if (narrowMode) { + templateAreas = `"alerts" + "viewer"`; + templateColumns = `1fr`; + templateRows = `40px 1fr`; + } + } + + wrapperRef.current.style.gridTemplateColumns = templateColumns; + wrapperRef.current.style.gridTemplateAreas = templateAreas; + wrapperRef.current.style.gridTemplateRows = templateRows; + }, [editMode, narrowMode, calculateTemplateColumns]); + + useEffect(() => { + wrapperRef.current.handleClicked = false; + wrapperRef.current.handleDragged = false; + // wrapperRef.current.proportion = 0.5; + const proportion = 0.5; + + const browserWidth = wrapperRef.current.clientWidth; + let leftPixels = proportion * browserWidth; + let rightPixels = wrapperRef.current.clientWidth - leftPixels; + + updateWrapper({ leftPixels, rightPixels, browserWidth }); + }, []); + + const handleWindowResize = () => { + const browserWidth = wrapperRef.current.clientWidth; + const currentProportion = wrapperRef.current.proportion; + let leftPixels = currentProportion * browserWidth; + let rightPixels = wrapperRef.current.clientWidth - leftPixels; + + updateWrapper({ leftPixels, rightPixels, browserWidth }); + }; + + const onMouseDown = (event) => { + event.preventDefault(); + wrapperRef.current.handleClicked = true; + }; + + const onMouseMove = (event) => { + //TODO: minimum movment calc + if (wrapperRef.current.handleClicked) { + event.preventDefault(); + wrapperRef.current.handleDragged = true; + + let leftPixels = event.clientX - wrapperRef.current.offsetLeft; + let rightPixels = wrapperRef.current.clientWidth - leftPixels; + const browserWidth = wrapperRef.current.clientWidth; + + updateWrapper({ leftPixels, rightPixels, browserWidth }); + } + }; + + const onMouseUp = () => { + if (wrapperRef.current.handleClicked) { + wrapperRef.current.handleClicked = false; + if (wrapperRef.current.handleDragged) { + wrapperRef.current.handleDragged = false; + } + } + }; + + const onDoubleClick = () => { + const proportion = 0.5; + const browserWidth = wrapperRef.current.clientWidth; + let leftPixels = proportion * browserWidth; + let rightPixels = wrapperRef.current.clientWidth - leftPixels; + + updateWrapper({ leftPixels, rightPixels, browserWidth }); + }; + + return ( + + {narrowMode && ( + + {mainAlertQueue} + + )} + + + + {editMode && ( + <> + {!narrowMode && ( + +
+ +
+
+ )} + + + + + + )} +
+ ); +}; diff --git a/src/Tools/_framework/Paths/PortfolioActivityEditor.jsx b/src/Tools/_framework/Paths/PortfolioActivityEditor.jsx index 66e1bc1b73..37eef15b8e 100644 --- a/src/Tools/_framework/Paths/PortfolioActivityEditor.jsx +++ b/src/Tools/_framework/Paths/PortfolioActivityEditor.jsx @@ -1562,9 +1562,7 @@ export function PortfolioActivityEditor() { bg="doenet.lightBlue" margin="10px 0px 0px 0px" //Only need when there is an outline > - + @@ -1840,7 +1838,7 @@ export function PortfolioActivityEditor() { location={location} navigate={navigate} linkSettings={{ - viewURL: "/portfolioviewer", + viewURL: "/publicOverview", editURL: "/publiceditor", }} scrollableContainer={ diff --git a/src/Tools/_framework/Paths/PublicActivity.jsx b/src/Tools/_framework/Paths/PublicActivity.jsx new file mode 100644 index 0000000000..a5ef5e6f37 --- /dev/null +++ b/src/Tools/_framework/Paths/PublicActivity.jsx @@ -0,0 +1,343 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + redirect, + useLoaderData, + useNavigate, + useLocation, + useOutletContext, +} from "react-router"; +import styled from "styled-components"; +import { DoenetML } from "../../../Viewer/DoenetML"; + +import { useRecoilState } from "recoil"; +import { checkIfUserClearedOut } from "../../../_utils/applicationUtils"; +import { Form } from "react-router-dom"; +import { + Box, + Button, + Flex, + Grid, + GridItem, + HStack, + Select, + Text, + VStack, +} from "@chakra-ui/react"; +import { pageToolViewAtom } from "../NewToolRoot"; +import axios from "axios"; +import VariantSelect from "../ChakraBasedComponents/VariantSelect"; +import findFirstPageIdInContent from "../../../_utils/findFirstPage"; +// import ContributorsMenu from "../ChakraBasedComponents/ContributorsMenu"; + +export async function loader({ params }) { + try { + const { data } = await axios.get( + `/api/getPortfolioActivity.php?doenetId=${params.doenetId}`, + ); + + const { label, courseId, isDeleted, isBanned, isPublic, json, imagePath } = + data; + + const { data: activityML } = await axios.get( + `/media/${data.json.assignedCid}.doenet`, + ); + + // console.log("activityML", activityML); + //Find the first page's doenetML + const regex = //; + const pageIds = activityML.match(regex); + + let pageId = findFirstPageIdInContent(data.json.content); + + const pageCId = pageIds[1]; + + // const { data: publicDoenetML } = await axios.get( + // `/media/${pageCId}.doenet`, + // ); + + //Get the doenetML of the pageId. + //we need transformResponse because + //large numbers are simplified with toString if used on doenetMLResponse.data + //which was causing errors + const publicDoenetMLResponse = await axios.get(`/media/${pageCId}.doenet`, { + transformResponse: (data) => data.toString(), + }); + let publicDoenetML = publicDoenetMLResponse.data; + + const draftDoenetMLResponse = await axios.get( + `/media/byPageId/${pageId}.doenet`, + { transformResponse: (data) => data.toString() }, + ); + let draftDoenetML = draftDoenetMLResponse.data; + console.log("publicDoenetML", publicDoenetML); + console.log("draftDoenetML", draftDoenetML); + + return { + success: true, + message: "", + pageDoenetId: pageId, + doenetId: params.doenetId, + publicDoenetML, + draftDoenetML, + label, + courseId, + isDeleted, + isBanned, + isPublic, + json, + imagePath, + }; + } catch (e) { + return { success: false, message: e.response.data.message }; + } +} + +//TODO: stub for edit overview future feature +export async function action({ request }) { + const formData = await request.formData(); + let formObj = Object.fromEntries(formData); + + return formObj; +} + +const HeaderSectionRight = styled.div` + margin: 5px; + height: 30px; + display: flex; + justify-content: flex-end; +`; + +export function PublicActivity() { + const { + success, + message, + pageDoenetId, + doenetId, + publicDoenetML, + draftDoenetML, + label, + courseId, + isDeleted, + isBanned, + isPublic, + json, + imagePath, + } = useLoaderData(); + + // const { signedIn } = useOutletContext(); + + if (!success) { + throw new Error(message); + } + + const [doenetML, setDoenetML] = useState(publicDoenetML); + + const navigate = useNavigate(); + const location = useLocation(); + + const [recoilPageToolView, setRecoilPageToolView] = + useRecoilState(pageToolViewAtom); + + let navigateTo = useRef(""); + + if (navigateTo.current != "") { + const newHref = navigateTo.current; + navigateTo.current = ""; + location.href = newHref; + } + + useEffect(() => { + document.title = `${label} - Doenet`; + }, [label]); + + const [variants, setVariants] = useState({ + index: 1, + numVariants: 1, + allPossibleVariants: ["a"], + }); + + return ( + <> + + + + + + + + + + + {label} + + + + + + + + + + + + + + + + + + + + {variants.numVariants > 1 && ( + + + setVariants((prev) => { + let next = { ...prev }; + next.index = index + 1; + return next; + }) + } + /> + + )} + 1 + ? "calc(100vh - 192px)" + : "calc(100vh - 160px)" + } + background="var(--canvas)" + borderWidth="1px" + borderStyle="solid" + borderColor="doenet.mediumGray" + width="100%" + overflow="scroll" + > + + + + + + + + + + ); +} diff --git a/src/Tools/_framework/Paths/PortfolioActivityViewer.jsx b/src/Tools/_framework/Paths/PublicActivityOverview.jsx similarity index 98% rename from src/Tools/_framework/Paths/PortfolioActivityViewer.jsx rename to src/Tools/_framework/Paths/PublicActivityOverview.jsx index 728d9be33a..5a4ec321a5 100644 --- a/src/Tools/_framework/Paths/PortfolioActivityViewer.jsx +++ b/src/Tools/_framework/Paths/PublicActivityOverview.jsx @@ -82,7 +82,7 @@ const HeaderSectionRight = styled.div` justify-content: flex-end; `; -export function PortfolioActivityViewer() { +export function PublicActivityOverview() { const { success, message, @@ -189,12 +189,12 @@ export function PortfolioActivityViewer() {
@@ -213,7 +213,7 @@ export function PortfolioActivityViewer() { }); }} > - Sign In To Remix + Sign In To Copy )}
diff --git a/src/Tools/_framework/Paths/PublicEditor.jsx b/src/Tools/_framework/Paths/PublicEditor.jsx index e630107c0e..3bd3631091 100644 --- a/src/Tools/_framework/Paths/PublicEditor.jsx +++ b/src/Tools/_framework/Paths/PublicEditor.jsx @@ -220,7 +220,7 @@ export function PublicEditor() { variant="outline" leftIcon={} onClick={() => { - navigate(`/portfolioviewer/${doenetId}`); + navigate(`/publicOverview/${doenetId}`); }} > View @@ -248,12 +248,12 @@ export function PublicEditor() { - This is a public editor. Remix to save changes. + This is a public editor. Copy to portfolio to save changes. {signedIn ? ( ) : ( )} @@ -397,7 +397,7 @@ export function PublicEditor() { location={location} navigate={navigate} linkSettings={{ - viewURL: "/portfolioviewer", + viewURL: "/publicOverview", editURL: "/publiceditor", }} /> @@ -420,7 +420,7 @@ export function PublicEditor() { p="4px 5px 0px 5px" h="32px" bg="#EDF2F7" - href="https://www.doenet.org/portfolioviewer/_7KL7tiBBS2MhM6k1OrPt4" + href="https://www.doenet.org/publicOverview/_7KL7tiBBS2MhM6k1OrPt4" isExternal data-test="Documentation Link" > diff --git a/src/Tools/_framework/Paths/PublicPortfolio.jsx b/src/Tools/_framework/Paths/PublicPortfolio.jsx index be8c0da873..1d2f3b2ce8 100644 --- a/src/Tools/_framework/Paths/PublicPortfolio.jsx +++ b/src/Tools/_framework/Paths/PublicPortfolio.jsx @@ -136,7 +136,7 @@ export function PublicPortfolio() { <> {publicActivities.map((activity) => { const { doenetId, label, imagePath } = activity; - const imageLink = `/portfolioviewer/${doenetId}`; + const imageLink = `/publicOverview/${doenetId}`; return ( + + + + + Doenet Logo + + + + + Doenet Logo + + Sign In via Email + + + Email address + { + let nextValue = e.target.value; + //Clear error if email is now good + if (emailError != null && emailRegex.test(nextValue)) { + setEmailError(null); + } + setEmailAddress(nextValue); + }} + /> + {emailError} + + + setIsChecked(e.target.checked)} + > + Stay Signed In + + + + + + + + + + + + + + ); +} diff --git a/src/Tools/_framework/Paths/SignInCode.jsx b/src/Tools/_framework/Paths/SignInCode.jsx new file mode 100644 index 0000000000..7c1d4602dd --- /dev/null +++ b/src/Tools/_framework/Paths/SignInCode.jsx @@ -0,0 +1,224 @@ +import { + AbsoluteCenter, + Box, + Button, + Card, + CardBody, + CardFooter, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + HStack, + Heading, + Image, + PinInput, + PinInputField, + Spinner, + Stack, +} from "@chakra-ui/react"; +import axios from "axios"; +import React, { useState } from "react"; +import { redirect } from "react-router"; +import { useFetcher } from "react-router-dom"; + +export async function action({ request }) { + const formData = await request.formData(); + const formObj = Object.fromEntries(formData); + const url = new URL(request.url); + const emailAddress = url.searchParams.get("email"); + const deviceName = url.searchParams.get("device"); + const staySignedIn = url.searchParams.get("stay"); + + try { + if (formObj._action == "send new code") { + let { data } = await axios.get("/api/sendSignInEmail.php", { + params: { emailaddress: emailAddress, deviceName }, + }); + + return { + success: true, + _action: formObj._action, + }; + } else if (formObj._action == "submit code") { + //TODO: need check credentials to give back the portfolio course id + let { data } = await axios.get("/api/checkCredentials.php", { + params: { + emailaddress: emailAddress, + nineCode: formObj.code, + deviceName: deviceName, + }, + }); + + //Attempt to store cookies! + const { data: jwtdata } = await axios.get( + `/api/jwt.php?emailaddress=${encodeURIComponent( + emailAddress, + )}&nineCode=${encodeURIComponent( + formObj.code, + )}&deviceName=${deviceName}&newAccount=${data.existed}&stay=${ + staySignedIn == "true" ? "1" : "0" + }`, + { withCredentials: true }, + ); + + //Redirect to portfolio + //or ask for name + if (data.hasFullName) { + //Redirect to portfolio + return redirect(`/portfolio/${data.portfolioCourseId}`); + } else { + //Redirect to askname + return redirect( + `/signinName?email=${encodeURIComponent( + emailAddress, + )}&portfolioId=${encodeURIComponent(data.portfolioCourseId)}`, + ); + } + } + } catch (e) { + return { + success: false, + message: e.response.data.message, + _action: formObj._action, + }; + } +} + +export function SignInCode() { + const fetcher = useFetcher(); + + const [code, setCode] = useState(""); + const [codeError, setCodeError] = useState(null); + const [isDisabled, setIsDisabled] = useState(false); + const [isExpired, setIsExpired] = useState(false); + + //Handle code entry errors + if (fetcher.data?.success === false && fetcher.state === "idle") { + //Guard against an infinite loop + if (codeError !== fetcher.data.message) { + setCodeError(fetcher.data.message); + setIsDisabled(false); + if (fetcher.data.message == "Code expired.") { + setIsExpired(true); + } + } + } + + return ( + <> + + + + + Doenet Logo + + + + + Doenet Logo + + Check your email for the code. + + + Sign-in code (9 digit code): + + setCode(code)} + > + + + + + + + + + + + + + {codeError} + + + + + + + {isExpired ? ( + + ) : ( + + )} + + + + + + + + ); +} diff --git a/src/Tools/_framework/Paths/SignInName.jsx b/src/Tools/_framework/Paths/SignInName.jsx new file mode 100644 index 0000000000..a302ab3c9b --- /dev/null +++ b/src/Tools/_framework/Paths/SignInName.jsx @@ -0,0 +1,172 @@ +import { + AbsoluteCenter, + Box, + Button, + Card, + CardBody, + CardFooter, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Heading, + Image, + Input, + Spinner, + Stack, +} from "@chakra-ui/react"; +import axios from "axios"; +import React, { useState } from "react"; +import { redirect, useLoaderData } from "react-router"; +import { useFetcher } from "react-router-dom"; + +export async function action({ request }) { + const formData = await request.formData(); + const formObj = Object.fromEntries(formData); + const url = new URL(request.url); + const portfolioId = url.searchParams.get("portfolioId"); + const emailAddress = url.searchParams.get("email"); + + try { + if (formObj._action == "submit name") { + let { data } = await axios.get("/api/saveUsersName.php", { + params: { + firstName: formObj.firstName, + lastName: formObj.lastName, + email: emailAddress, + }, + }); + + //Redirect to portfolio + return redirect(`/portfolio/${portfolioId}`); + } + + return { success: true }; + } catch (e) { + return { + success: false, + message: e.response.data.message, + _action: formObj._action, + }; + } +} + +export function SignInName() { + const fetcher = useFetcher(); + // let formObj = {}; + // if (fetcher.formData !== undefined) { + // formObj = Object.fromEntries(fetcher.formData); + // } + + const [firstName, setFirstName] = useState(""); + const [firstNameError, setFirstNameError] = useState(null); + const [lastName, setLastName] = useState(""); + const [lastNameError, setLastNameError] = useState(null); + const [isDisabled, setIsDisabled] = useState(false); + + return ( + <> + + + + + Doenet Logo + + + + + Doenet Logo + + Please Enter Your Name. + + + First Name: + { + if (e.target.value != "") { + setFirstNameError(null); + } + setFirstName(e.target.value); + }} + /> + + {firstNameError} + + + + Last Name: + { + if (e.target.value != "") { + setLastNameError(null); + } + setLastName(e.target.value); + }} + /> + + {lastNameError} + + + + + + + + + + + + + + + ); +} diff --git a/src/Tools/_framework/Paths/SignOut.jsx b/src/Tools/_framework/Paths/SignOut.jsx new file mode 100644 index 0000000000..e2d6c5163a --- /dev/null +++ b/src/Tools/_framework/Paths/SignOut.jsx @@ -0,0 +1,112 @@ +import { + AbsoluteCenter, + Box, + Button, + Card, + CardBody, + CardFooter, + Flex, + Heading, + Image, + ListItem, + Stack, + Text, + UnorderedList, +} from "@chakra-ui/react"; +import React from "react"; +import { useLoaderData, useNavigate } from "react-router"; +import { + checkIfUserClearedOut, + clearUsersInformationFromTheBrowser, +} from "../../../_utils/applicationUtils"; + +export async function loader() { + await clearUsersInformationFromTheBrowser(); + const isSignedOutObj = await checkIfUserClearedOut(); + return { isSignedOutObj }; +} + +//TODO: inform if not signed out +export function SignOut() { + const { isSignedOutObj } = useLoaderData(); + const navigate = useNavigate(); + + return ( + <> + + + + + Doenet Logo + + + + + Doenet Logo + + {isSignedOutObj.cookieRemoved && + isSignedOutObj.userInformationIsCompletelyRemoved ? ( + <> + + You are Signed Out! + + + + + + + + ) : ( + <> + + You are NOT Signed Out! + + + + Hit refresh to try again. + + + + Errors + + {isSignedOutObj.messageArray.map((msg, i) => { + return ( + + {msg} + + ); + })} + + + )} + + + + + + + ); +} diff --git a/src/Tools/_framework/Paths/SiteHeader.jsx b/src/Tools/_framework/Paths/SiteHeader.jsx index e5764f2fc9..f2788fb318 100644 --- a/src/Tools/_framework/Paths/SiteHeader.jsx +++ b/src/Tools/_framework/Paths/SiteHeader.jsx @@ -189,7 +189,7 @@ export function SiteHeader(props) { {signedIn ? (
- + @@ -225,7 +225,11 @@ export function SiteHeader(props) { */} - + Sign Out diff --git a/src/Tools/_framework/RouterLogo.jsx b/src/Tools/_framework/RouterLogo.jsx index 7a8b9b6a03..97dd2e1764 100644 --- a/src/Tools/_framework/RouterLogo.jsx +++ b/src/Tools/_framework/RouterLogo.jsx @@ -17,9 +17,10 @@ const LogoButton = styled.button` border-radius: 10px; align-items: center; border-style: none; + // border-radius: 50%; // margin-top: 8px; - // margin-left: 90px; + margin-left: 5px; cursor: ${(props) => (props.hasLink ? "pointer" : "default")}; &:focus { outline: 2px solid var(--canvastext); @@ -27,15 +28,16 @@ const LogoButton = styled.button` } `; -export default function RouterLogo({ hasLink = true }) { +export default function RouterLogo({ to, hasLink = true }) { let navigate = useNavigate(); return ( { if (hasLink) { - navigate("/"); + navigate(to); } }} /> diff --git a/src/Tools/cypressTest/CypressTest.jsx b/src/Tools/cypressTest/CypressTest.jsx index 7d49c26a3a..d5dda19479 100644 --- a/src/Tools/cypressTest/CypressTest.jsx +++ b/src/Tools/cypressTest/CypressTest.jsx @@ -434,7 +434,7 @@ function Test() { location={location} navigate={navigate} linkSettings={{ - viewURL: "/portfolioviewer", + viewURL: "/publicOverview", editURL: "/publiceditor", }} darkMode={darkMode} diff --git a/src/Tools/test/DoenetTest.jsx b/src/Tools/test/DoenetTest.jsx index eadff468fa..e4334a04f3 100644 --- a/src/Tools/test/DoenetTest.jsx +++ b/src/Tools/test/DoenetTest.jsx @@ -394,7 +394,7 @@ function Test() { location={location} navigate={navigate} linkSettings={{ - viewURL: "/portfolioviewer", + viewURL: "/publicOverview", editURL: "/publiceditor", }} darkMode={darkMode} diff --git a/src/Viewer/PageViewer.jsx b/src/Viewer/PageViewer.jsx index eebc579068..e8ca54e10d 100644 --- a/src/Viewer/PageViewer.jsx +++ b/src/Viewer/PageViewer.jsx @@ -59,7 +59,7 @@ export function PageViewer({ apiURLs = {}, location = {}, navigate, - linkSettings = { viewURL: "/portfolioviewer", editURL: "/publiceditor" }, + linkSettings = { viewURL: "/publicOverview", editURL: "/publiceditor" }, errorsActivitySpecific = {}, scrollableContainer, darkMode, diff --git a/src/_reactComponents/PanelHeaderComponents/Carousel.jsx b/src/_reactComponents/PanelHeaderComponents/Carousel.jsx index 966c9976d1..9daab879a7 100644 --- a/src/_reactComponents/PanelHeaderComponents/Carousel.jsx +++ b/src/_reactComponents/PanelHeaderComponents/Carousel.jsx @@ -49,7 +49,13 @@ export function Carousel({ title = "", data = [] }) { return ( <> - + {title} @@ -110,7 +116,7 @@ export function Carousel({ title = "", data = [] }) { /; + const pageIds = activityML.match(regex); + + let pageId = findFirstPageIdInContent(data.json.content); + + const pageCId = pageIds[1]; + + // const { data: publicDoenetML } = await axios.get( + // `/media/${pageCId}.doenet`, + // ); + + //Get the doenetML of the pageId. + //we need transformResponse because + //large numbers are simplified with toString if used on doenetMLResponse.data + //which was causing errors + const publicDoenetMLResponse = await axios.get(`/media/${pageCId}.doenet`, { + transformResponse: (data) => data.toString(), + }); + let publicDoenetML = publicDoenetMLResponse.data; + + const draftDoenetMLResponse = await axios.get( + `/media/byPageId/${pageId}.doenet`, + { transformResponse: (data) => data.toString() }, + ); + let draftDoenetML = draftDoenetMLResponse.data; + console.log("publicDoenetML", publicDoenetML); + console.log("draftDoenetML", draftDoenetML); + + return { + success: true, + message: "", + pageDoenetId: pageId, + doenetId: params.doenetId, + publicDoenetML, + draftDoenetML, + label, + courseId, + isDeleted, + isBanned, + isPublic, + json, + imagePath, + }; + } catch (e) { + return { success: false, message: e.response.data.message }; + } +} + +//TODO: stub for edit overview future feature +export async function action({ request }) { + const formData = await request.formData(); + let formObj = Object.fromEntries(formData); + + return formObj; +} + +const HeaderSectionRight = styled.div` + margin: 5px; + height: 30px; + display: flex; + justify-content: flex-end; +`; + +export function PortfolioActivity() { + const { + success, + message, + pageDoenetId, + doenetId, + publicDoenetML, + draftDoenetML, + label, + courseId, + isDeleted, + isBanned, + isPublic, + json, + imagePath, + } = useLoaderData(); + + // const { signedIn } = useOutletContext(); + + if (!success) { + throw new Error(message); + } + + const [doenetML, setDoenetML] = useState(publicDoenetML); + + const navigate = useNavigate(); + const location = useLocation(); + + const [recoilPageToolView, setRecoilPageToolView] = + useRecoilState(pageToolViewAtom); + + let navigateTo = useRef(""); + + if (navigateTo.current != "") { + const newHref = navigateTo.current; + navigateTo.current = ""; + location.href = newHref; + } + + useEffect(() => { + document.title = `${label} - Doenet`; + }, [label]); + + const [variants, setVariants] = useState({ + index: 1, + numVariants: 1, + allPossibleVariants: ["a"], + }); + + return ( + <> + + + + + + + + + + + {label} + + + + + + + + + + + + + + + + + + + + {variants.numVariants > 1 && ( + + + setVariants((prev) => { + let next = { ...prev }; + next.index = index + 1; + return next; + }) + } + /> + + )} + 1 + ? "calc(100vh - 192px)" + : "calc(100vh - 160px)" + } + background="var(--canvas)" + borderWidth="1px" + borderStyle="solid" + borderColor="doenet.mediumGray" + width="100%" + overflow="scroll" + > + + + + + + + + + + ); +} diff --git a/src/_reactComponents/PanelHeaderComponents/RecoilActivityCard.jsx b/src/_reactComponents/PanelHeaderComponents/RecoilActivityCard.jsx index 529215a6b0..8c01794e57 100644 --- a/src/_reactComponents/PanelHeaderComponents/RecoilActivityCard.jsx +++ b/src/_reactComponents/PanelHeaderComponents/RecoilActivityCard.jsx @@ -12,9 +12,11 @@ import { MenuButton, Icon, MenuList, + Center, + VStack, } from "@chakra-ui/react"; import { GoKebabVertical } from "react-icons/go"; -import { Link, useFetcher } from "react-router-dom"; +import { Link, useFetcher, useNavigate } from "react-router-dom"; import { // itemByDoenetId, useCourse, @@ -35,13 +37,11 @@ export default function RecoilActivityCard({ setDoenetId, onClose, onOpen, + isNewActivity = false, }) { const fetcher = useFetcher(); - // const setItemByDoenetId = useSetRecoilState(itemByDoenetId(doenetId)); const { compileActivity, updateAssignItem } = useCourse(courseId); - - // const [recoilPageToolView, setRecoilPageToolView] = - // useRecoilState(pageToolViewAtom); + const navigate = useNavigate(); let navigateTo = useRef(""); @@ -52,9 +52,9 @@ export default function RecoilActivityCard({ location.href = newHref; } - return ( + const cardJSX = ( - + Delete + navigate(`/portfolioActivity/${doenetId}/_`)} + > + Overview + + + navigate( + `/portfolioActivity/${doenetId}/${pageDoenetId}?edit=true`, + ) + } + > + Edit + { @@ -171,4 +187,28 @@ export default function RecoilActivityCard({ ); + + if (isNewActivity) { + return ( + + {cardJSX} +
+ NEW +
+
+ ); + } else { + return <>{cardJSX}; + } } diff --git a/src/_utils/applicationUtils.js b/src/_utils/applicationUtils.js index a4eebc6818..46939c7565 100644 --- a/src/_utils/applicationUtils.js +++ b/src/_utils/applicationUtils.js @@ -3,7 +3,7 @@ import { clear as idb_clear, keys as idb_keys } from "idb-keyval"; export async function clearUsersInformationFromTheBrowser() { localStorage.clear(); //Clear out the profile of the last exam taker - await axios.get("/api/signOut.php"); + await axios.get("/api/signOut.php", { withCredentials: true }); //Clear all cookies await idb_clear(); return true; } @@ -20,6 +20,11 @@ export async function checkIfUserClearedOut() { //Check for local storage //TODO: find something is stored in localStorage and test if this clears it let localStorageRemoved = localStorage.length == 0; + //Chakra UI will put darkmode back so check that + if (localStorage.length === 1 && localStorage.key(0) === 'chakra-ui-color-mode') { + localStorageRemoved = true; + } + if (!localStorageRemoved) { messageArray.push("local storage not removed"); } @@ -27,15 +32,13 @@ export async function checkIfUserClearedOut() { //Check for cookie //Ask the server without hitting the database const { data } = await axios.get("/api/getQuickCheckSignedIn.php"); - const secureCookieRemoved = !data?.signedIn; - const vanillaCookies = document.cookie.split(";"); - const vanillaCookieRemoved = - vanillaCookies.length === 1 && vanillaCookies[0] === ""; + const secureCookieRemoved = !data?.secureCookieExists; + const unsecureCookieRemoved = !data?.unsecureCookieExists; - let cookieRemoved = vanillaCookieRemoved && secureCookieRemoved; + let cookieRemoved = unsecureCookieRemoved && secureCookieRemoved; - if (!vanillaCookieRemoved) { + if (!unsecureCookieRemoved) { messageArray.push("cookie not removed"); } diff --git a/src/index.jsx b/src/index.jsx index 94174762bf..76fbb0a1c6 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -5,8 +5,11 @@ import { redirect, RouterProvider, } from "react-router-dom"; +import { ChakraProvider, extendTheme } from "@chakra-ui/react"; + import { RecoilRoot } from "recoil"; import { createRoot } from "react-dom/client"; +import ErrorPage from "./Tools/_framework/Paths/ErrorPage"; import ToolRoot from "./Tools/_framework/NewToolRoot"; import { MathJaxContext } from "better-react-mathjax"; @@ -42,16 +45,19 @@ import { PublicPortfolio, } from "./Tools/_framework/Paths/PublicPortfolio"; import { - loader as portfolioActivityViewerLoader, - action as portfolioActivityViewerAction, - PortfolioActivityViewer, -} from "./Tools/_framework/Paths/PortfolioActivityViewer"; -import { ChakraProvider, extendTheme } from "@chakra-ui/react"; + loader as publicActivityOverviewLoader, + action as publicActivityOverviewAction, + PublicActivityOverview, +} from "./Tools/_framework/Paths/PublicActivityOverview"; +import { + loader as portfolioActivityLoader, + action as portfolioActivityAction, + PortfolioActivity, +} from "./Tools/_framework/Paths/PortfolioActivity"; import { action as editorSupportPanelAction, loader as editorSupportPanelLoader, } from "./Tools/_framework/Panels/NewSupportPanel"; -import ErrorPage from "./Tools/_framework/Paths/ErrorPage"; import "@fontsource/jost"; import { @@ -80,6 +86,22 @@ import { CourseLinkPageViewer, loader as courseLinkPageViewerLoader, } from "./Tools/_framework/Paths/CourseLinkPageViewer"; +import { + SignIn, + action as signInAction, +} from "./Tools/_framework/Paths/SignIn"; +import { + SignInCode, + action as signInCodeAction, +} from "./Tools/_framework/Paths/SignInCode"; +import { + SignInName, + action as signInNameAction, +} from "./Tools/_framework/Paths/SignInName"; +import { + SignOut, + loader as signOutLoader, +} from "./Tools/_framework/Paths/SignOut"; { /* */ @@ -237,14 +259,15 @@ const router = createBrowserRouter([ ), }, { - path: "portfolioviewer/:doenetId", - loader: portfolioActivityViewerLoader, - action: portfolioActivityViewerAction, + path: "publicOverview/:doenetId", + loader: publicActivityOverviewLoader, + action: publicActivityOverviewAction, errorElement: ( ), + element: ( // (mathJax.Hub.processSectionDelay = 0)} > - + // @@ -319,6 +342,62 @@ const router = createBrowserRouter([ }, ], }, + { + path: "signin", + action: signInAction, + errorElement: ( + + + + ), + element: ( + + + + ), + }, + { + path: "signinCode", + action: signInCodeAction, + errorElement: ( + + + + ), + element: ( + + + + ), + }, + { + path: "signinName", + action: signInNameAction, + errorElement: ( + + + + ), + element: ( + + + + ), + }, + { + path: "signout", + loader: signOutLoader, + errorElement: ( + + + + ), + element: ( + + + + ), + }, { path: "public", loader: editorSupportPanelLoader, @@ -334,6 +413,43 @@ const router = createBrowserRouter([ ), }, + { + path: "portfolioActivity/:doenetId/", + loader: ({ params }) => { + let doenetId = params.doenetId; + return redirect(`/portfolioActivity/${doenetId}/_`); + }, + + errorElement: ( + + + + ), + + element: null, + }, + { + path: "portfolioActivity/:doenetId/:pageId", + loader: portfolioActivityLoader, + action: portfolioActivityAction, + errorElement: ( + + + + ), + + element: ( + (mathJax.Hub.processSectionDelay = 0)} + > + + + + + ), + }, { path: "/courselinkpageviewer/:doenetId", loader: courseLinkPageViewerLoader, @@ -420,7 +536,6 @@ const router = createBrowserRouter([ ), }, - { path: "*", element: (