diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 842ae2b4..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: 2 - -updates: - - directory: "/" - package-ecosystem: "npm" - open-pull-requests-limit: 5 - target-branch: "dev" - commit-message: - prefix: "chore" - include: "scope" - reviewers: - - "PedroChaparro" - schedule: - timezone: "America/Bogota" - time: "12:00" - interval: "daily" diff --git a/.gitignore b/.gitignore index 4dde0f7e..bc47e16b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ dist-ssr # Test data e2e/laboratories/data/java-downloaded.zip +e2e/laboratories/data/Test block name.zip +e2e/grades/data/downloaded-java-tests.zip diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1dcfd5..f70b6b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,45 @@ -# [0.40.0](https://github.com/upb-code-labs/react-client/compare/v0.39.0...v0.40.0) (2024-01-13) +# [0.46.0](https://github.com/upb-code-labs/react-client/compare/v0.45.0...v0.46.0) (2024-01-20) ### Features -* Toggle enrolled students status ([#241](https://github.com/upb-code-labs/react-client/issues/241)) ([cd5c754](https://github.com/upb-code-labs/react-client/commit/cd5c7541c254597852aadfe3f96a1a13c156f871)) +* Grade student ([#263](https://github.com/upb-code-labs/react-client/issues/263)) ([bba5534](https://github.com/upb-code-labs/react-client/commit/bba553440c9c24dbf1236019f124952efae58185)) -# [0.39.0](https://github.com/upb-code-labs/react-client/compare/v0.38.0...v0.39.0) (2024-01-13) +# [0.45.0](https://github.com/upb-code-labs/react-client/compare/v0.44.0...v0.45.0) (2024-01-19) ### Features -* Delete rubric ([#240](https://github.com/upb-code-labs/react-client/issues/240)) ([53a0984](https://github.com/upb-code-labs/react-client/commit/53a0984a8182ce747f956f270c88a7a5e7f7ff00)) +* List students' grades ([#262](https://github.com/upb-code-labs/react-client/issues/262)) ([508a9c2](https://github.com/upb-code-labs/react-client/commit/508a9c2d263d21a6db458c0092aa048f6107b44c)) -# [0.38.0](https://github.com/upb-code-labs/react-client/compare/v0.37.0...v0.38.0) (2024-01-13) +# [0.44.0](https://github.com/upb-code-labs/react-client/compare/v0.43.0...v0.44.0) (2024-01-17) ### Features -* Custom error component ([#237](https://github.com/upb-code-labs/react-client/issues/237)) ([56d5733](https://github.com/upb-code-labs/react-client/commit/56d57331c02e0798b06cce2912203065d4d8b589)) +* Swap the index of two blocks ([#256](https://github.com/upb-code-labs/react-client/issues/256)) ([842114e](https://github.com/upb-code-labs/react-client/commit/842114e4017978869c3a1a4b77e190332436f440)) -# [0.37.0](https://github.com/upb-code-labs/react-client/compare/v0.36.0...v0.37.0) (2024-01-12) +# [0.43.0](https://github.com/upb-code-labs/react-client/compare/v0.42.0...v0.43.0) (2024-01-17) ### Features -* Laboratory progress and statistics ([#232](https://github.com/upb-code-labs/react-client/issues/232)) ([52406a5](https://github.com/upb-code-labs/react-client/commit/52406a57c8474e8d9d2f5de10bb46ed3048fe8ab)) +* Update password ([#255](https://github.com/upb-code-labs/react-client/issues/255)) ([ad92b51](https://github.com/upb-code-labs/react-client/commit/ad92b515fc1d91cbd085bf6cc475b0d64e0b7900)) -# [0.36.0](https://github.com/upb-code-labs/react-client/compare/v0.35.0...v0.36.0) (2024-01-09) +# [0.42.0](https://github.com/upb-code-labs/react-client/compare/v0.41.0...v0.42.0) (2024-01-17) ### Features -* Get submission status ([#227](https://github.com/upb-code-labs/react-client/issues/227)) ([feff79c](https://github.com/upb-code-labs/react-client/commit/feff79c31521fdeb2bb430fadc9671a10f048256)) +* Update profile ([#254](https://github.com/upb-code-labs/react-client/issues/254)) ([1954d1d](https://github.com/upb-code-labs/react-client/commit/1954d1dfcd7f62d830829fa645bf3427734a3411)) diff --git a/e2e/grades/data/java-tests.zip b/e2e/grades/data/java-tests.zip new file mode 100644 index 00000000..a7e3d11f Binary files /dev/null and b/e2e/grades/data/java-tests.zip differ diff --git a/e2e/grades/grades-workflow.spec.ts b/e2e/grades/grades-workflow.spec.ts new file mode 100644 index 00000000..4c6b98f5 --- /dev/null +++ b/e2e/grades/grades-workflow.spec.ts @@ -0,0 +1,496 @@ +import test, { expect } from "@playwright/test"; +import { + getDefaultPassword, + getDevelopmentAdminCredentials, + getRandomEmail, + getRandomName, + getRandomUniversityID +} from "e2e/Utils"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +test.describe.serial("Grades workflow", () => { + const teacherEmail = getRandomEmail(); + + const courseName = "Test course"; + const rubricName = "Test rubric"; + const laboratoryName = "Test laboratory"; + const testBlockName = "Test block name"; + + const studentFullName = getRandomName(); + const studentEmail = getRandomEmail(); + + test("Register test teacher", async ({ page }) => { + // Login as an admin + const adminCredentials = getDevelopmentAdminCredentials(); + await page.goto("/login"); + await page.getByLabel("Email").fill(adminCredentials.email); + await page.getByLabel("Password").fill(adminCredentials.password); + await page.getByRole("button", { name: "Submit" }).click(); + + // Register a teacher + await page + .getByRole("link", { name: "Register Teachers", exact: true }) + .click(); + await page.getByLabel("Full name").fill(getRandomName()); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + }); + + test("Register test student", async ({ page }) => { + await page.goto("/register/students"); + await page.getByLabel("Full name").fill(studentFullName); + await page.getByLabel("Institutional ID").fill(getRandomUniversityID()); + await page.getByLabel("Email").fill(studentEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + }); + + test("Create test rubric", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Create a rubric + await page.getByRole("link", { name: "Rubrics", exact: true }).click(); + await page.getByRole("button", { name: "Create rubric" }).click(); + await page.getByLabel("Name").fill("Test rubric"); + await page.getByRole("button", { name: "Create" }).click(); + }); + + test("Create test course", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Create a course + const createCourseButton = page.getByRole("button", { + name: "Create a new course" + }); + await createCourseButton.click(); + await page.getByLabel("Name").fill(courseName); + await page.getByRole("button", { name: "Create" }).click(); + }); + + test("Create test laboratory", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the course page + await page.getByRole("link", { name: courseName }).click(); + + // Open the modal + const createLaboratoryButton = page.getByRole("button", { + name: "Create Laboratory" + }); + await createLaboratoryButton.click(); + await page.getByLabel("Name").fill(laboratoryName); + await page.getByLabel("Opening date").fill("2023-12-01T00:00"); + await page.getByLabel("Closing date").fill("2032-12-01T23:59"); + await page.getByRole("button", { name: "Create" }).click(); + + // Select the rubric + const editLaboratoryButton = page.getByRole("link", { + name: `Edit ${laboratoryName} laboratory` + }); + await expect(editLaboratoryButton).toBeVisible(); + await editLaboratoryButton.click(); + + const rubricSelect = page.getByRole("combobox", { name: "Rubric" }); + await expect(rubricSelect).toBeVisible(); + await rubricSelect.click(); + + const rubricOption = page.getByLabel(rubricName); + await expect(rubricOption).toBeVisible(); + await rubricOption.click(); + + // Save the changes + const saveButton = page.getByRole("button", { name: "Save changes" }); + await expect(saveButton).toBeVisible(); + await saveButton.click(); + + // Assert a toast is shown + await expect( + page.getByText("Laboratory details updated successfully") + ).toBeVisible(); + }); + + test("Create test block", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the course page + await page.getByRole("link", { name: courseName }).click(); + + // Enter to the edit page of the laboratory + const editLaboratoryButton = page.getByRole("link", { + name: `Edit ${laboratoryName} laboratory` + }); + await expect(editLaboratoryButton).toBeVisible(); + await editLaboratoryButton.click(); + + // Add the test block + const addTestBlockButton = page.getByRole("button", { + name: "Add test block" + }); + await addTestBlockButton.click(); + + // set the name + const addTestBlockModal = page.getByRole("dialog"); + await addTestBlockModal.getByLabel("Name").fill(testBlockName); + + // select the language + const languageSelect = addTestBlockModal.getByRole("combobox", { + name: "Language" + }); + await expect(languageSelect).toBeVisible(); + languageSelect.click(); + + const javaOption = page.getByLabel("Java"); + await expect(javaOption).toBeVisible(); + await javaOption.click(); + + // Upload tests file + const zipFile = join(__dirname, "data", "java-tests.zip"); + const zipFileInput = addTestBlockModal.getByLabel("Test file"); + + const fileChooserPromise = page.waitForEvent("filechooser"); + await zipFileInput.click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(zipFile); + + // Submit the form + const submitButton = page.getByRole("button", { name: "Add" }); + await expect(submitButton).toBeVisible(); + await submitButton.click(); + + // Assert the test block is shown + const nameInput = page.getByLabel("Block name"); + await expect(nameInput).toBeVisible(); + await expect(nameInput).toHaveValue(testBlockName); + }); + + test("Enroll student in test course", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the participants view + await page.getByRole("link", { name: courseName }).click(); + await page.getByRole("link", { name: "Manage participants" }).click(); + + // Open the dialog + await page + .getByRole("button", { name: "Enroll student", exact: true }) + .click(); + + // Search the student + await page + .getByLabel("Search students by full name", { exact: true }) + .fill(studentFullName); + + // Select the student + const studentButton = page.getByRole("button", { + name: new RegExp(studentFullName) + }); + + // Enroll the student + await studentButton.click(); + }); + + test("Student submits a solution", async ({ page }) => { + // Login as a student + await page.goto("/login"); + await page.getByLabel("Email").fill(studentEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the course page + await page.getByRole("link", { name: courseName }).click(); + + // Go to the laboratory page + await page + .getByRole("link", { name: `Complete ${laboratoryName} laboratory` }) + .click(); + + // Assert the test block is shown + const testBlockNameInput = page.getByLabel("Test name"); + await expect(testBlockNameInput).toBeVisible(); + await expect(testBlockNameInput).toHaveValue(testBlockName); + + // Upload the solution + const solutionFile = join(__dirname, "data", "java-tests.zip"); + const solutionFileInput = page.getByLabel("Submission file"); + + const fileChooserPromise = page.waitForEvent("filechooser"); + await solutionFileInput.click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(solutionFile); + + // Submit the form + const submitButton = page.getByRole("button", { name: "Submit" }); + await expect(submitButton).toBeVisible(); + await submitButton.click(); + + // update the timeout + const ONE_MINUTE_IN_MS = 60000; + page.setDefaultTimeout(ONE_MINUTE_IN_MS); + + // Assert the status phases are shown + // Pending phase + const pendingPhaseElm = page.getByTestId("pending-phase"); + await expect(pendingPhaseElm).toBeVisible(); + await expect(pendingPhaseElm).toHaveAttribute("data-reached", "true"); + + // Running phase + const runningPhaseElm = page.getByTestId("running-phase"); + await expect(runningPhaseElm).toBeVisible(); + await expect(runningPhaseElm).toHaveAttribute("data-reached", "true"); + + // Ready phase + const readyPhaseElm = page.getByTestId("ready-phase"); + await expect(readyPhaseElm).toBeVisible(); + await expect(readyPhaseElm).toHaveAttribute("data-reached", "true"); + + // TODO: Assert the student can download the submitted file + /* + const formTab = page.getByRole("tab", { + name: "Test block 1 submission form", + exact: true + }); + await formTab.click(); + + const downloadButton = page.getByRole("button", { + name: "Download current submission file for block number 1" + }); + await expect(downloadButton).toBeVisible(); + + const downloadPromise = page.waitForEvent("download"); + + await downloadButton.click(); + const download = await downloadPromise; + await download.saveAs(join(__dirname, "data", "downloaded-java-tests.zip")); + */ + }); + + test("Student appears in the grades list", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the course page + await page.getByRole("link", { name: courseName }).click(); + + // Go to the grades view of the laboratory + const laboratoryRow = page.getByRole("row", { + name: new RegExp(`^\\s*${laboratoryName}`), + exact: true + }); + await expect(laboratoryRow).toBeVisible(); + + const laboratoryGradesButton = laboratoryRow.getByRole("link", { + name: `Go to the grades of ${laboratoryName}`, + exact: true + }); + await laboratoryGradesButton.click(); + + // Assert the student is listed + const studentRow = page.getByRole("row", { + name: new RegExp(`^\\s*${studentFullName}`), + exact: true + }); + await expect(studentRow).toBeVisible(); + + await expect( + studentRow.getByRole("cell", { name: studentFullName, exact: true }) + ).toBeVisible(); + await expect( + studentRow.getByRole("cell", { + name: "N/A", + exact: true + }) + ).toBeVisible(); + await expect( + studentRow.getByRole("link", { + name: `Edit grade for student ${studentFullName}`, + exact: true + }) + ).toBeVisible(); + }); + + test("Teacher can edit the grade", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the course page + await page.getByRole("link", { name: courseName }).click(); + + // Go to the grades view of the laboratory + const laboratoryRow = page.getByRole("row", { + name: new RegExp(`^\\s*${laboratoryName}`), + exact: true + }); + await expect(laboratoryRow).toBeVisible(); + + const laboratoryGradesButton = laboratoryRow.getByRole("link", { + name: `Go to the grades of ${laboratoryName}`, + exact: true + }); + await laboratoryGradesButton.click(); + + // Edit the grade + const studentRow = page.getByRole("row", { + name: new RegExp(`^\\s*${studentFullName}`), + exact: true + }); + await expect(studentRow).toBeVisible(); + + const editGradeButton = studentRow.getByRole("link", { + name: `Edit grade for student ${studentFullName}`, + exact: true + }); + await editGradeButton.click(); + + // ## Select a criteria from the rubric + let criteriaCard = page.getByRole("button", { + name: "Select criteria 1 of objective 1", + exact: true + }); + await criteriaCard.click(); + + const criteriaWeightElm = page.getByLabel( + "Criteria 1 of objective 1 weight" + ); + expect(criteriaWeightElm).not.toBeNull(); + + const criteriaWeight = await criteriaWeightElm.getAttribute("value"); + expect(criteriaWeight).not.toBeNull(); + + // Assert a toast is shown + await expect(page.getByText("Criteria has been selected")).toBeVisible(); + + // Assert the weight was added to the student grade + const studentGradeInput = page.getByLabel("Grade", { exact: true }); + await expect(studentGradeInput).toBeVisible(); + await expect(studentGradeInput).toHaveValue(criteriaWeight!); + + // ## Deselect the criteria + criteriaCard = page.getByRole("button", { + name: "De-select criteria 1 of objective 1", + exact: true + }); + await criteriaCard.click(); + + // Assert a toast is shown + await expect(page.getByText("Criteria has been de-selected")).toBeVisible(); + + // Assert the weight was removed from the student grade + await expect(studentGradeInput).toBeVisible(); + await expect(studentGradeInput).toHaveValue("0"); + + // ## Add a comment + const commentInput = page.getByLabel("Comment"); + await expect(commentInput).toBeVisible(); + await commentInput.fill("This is a comment left by the teacher"); + + const updateCommentButton = page.getByRole("button", { + name: "Update comment", + exact: true + }); + await updateCommentButton.click(); + + // Assert a toast is shown + await expect( + page.getByText("The comment has been set successfully") + ).toBeVisible(); + + // Select the criteria again + criteriaCard = page.getByRole("button", { + name: "Select criteria 1 of objective 1", + exact: true + }); + await criteriaCard.click(); + }); + + test("Teacher can download student submissions", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the course page + await page.getByRole("link", { name: courseName }).click(); + + // Go to the grades view of the laboratory + const laboratoryRow = page.getByRole("row", { + name: new RegExp(`^\\s*${laboratoryName}`), + exact: true + }); + + const laboratoryGradesButton = laboratoryRow.getByRole("link", { + name: `Go to the grades of ${laboratoryName}`, + exact: true + }); + await laboratoryGradesButton.click(); + + // Go to the grade of the student + const studentRow = page.getByRole("row", { + name: new RegExp(`^\\s*${studentFullName}`), + exact: true + }); + + const editGradeButton = studentRow.getByRole("link", { + name: `Edit grade for student ${studentFullName}`, + exact: true + }); + + await editGradeButton.click(); + + // Click on the Submission tab + const submissionTab = page.getByRole("tab", { + name: "Submissions", + exact: true + }); + await submissionTab.click(); + + // Assert the teacher can download the submission + const downloadButton = page.getByRole("button", { + name: `Download code sent by the student for the ${testBlockName} test block`, + exact: true + }); + await expect(downloadButton).toBeVisible(); + + const downloadPromise = page.waitForEvent("download"); + + await downloadButton.click(); + const download = await downloadPromise; + await download.saveAs(join(__dirname, "data", "downloaded-java-tests.zip")); + + // Assert a toast is shown + await expect( + page.getByText("The submission archive has been downloaded successfully") + ).toBeVisible(); + }); +}); diff --git a/e2e/laboratories/edit-laboratory.spec.ts b/e2e/laboratories/edit-laboratory.spec.ts index a9849023..554efeab 100644 --- a/e2e/laboratories/edit-laboratory.spec.ts +++ b/e2e/laboratories/edit-laboratory.spec.ts @@ -136,22 +136,6 @@ test.describe.serial("Edit laboratory workflow", () => { await expect( page.getByText("The markdown block has been updated successfully") ).toBeVisible(); - - // ### Delete the markdown block - await blockDropdownButton.click(); - const deleteBlockButton = page.getByRole("menuitem", { - name: "Delete block" - }); - await expect(deleteBlockButton).toBeVisible(); - await deleteBlockButton.click(); - - // Assert an alert is shown - await expect( - page.getByText("The markdown block has been deleted successfully") - ).toBeVisible(); - - // Assert the block is not shown - await expect(page.getByLabel(markdownBlockContentLabel)).not.toBeVisible(); }); test("Add and edit test blocks", async ({ page }) => { @@ -200,15 +184,18 @@ test.describe.serial("Edit laboratory workflow", () => { await javaOption.click(); // Assert the teacher can download the template - const downloadButton = addTestBlockModal.getByRole("button", { - name: "Download template" - }); - await expect(downloadButton).toBeVisible(); + const languageTemplateDownloadButton = addTestBlockModal.getByRole( + "button", + { + name: "Download template" + } + ); + await expect(languageTemplateDownloadButton).toBeVisible(); // Assert the template is downloaded const downloadPromise = page.waitForEvent("download"); - await downloadButton.click(); + await languageTemplateDownloadButton.click(); const download = await downloadPromise; await download.saveAs(join(__dirname, "data", "java-downloaded.zip")); @@ -236,7 +223,7 @@ test.describe.serial("Edit laboratory workflow", () => { // Assert teachers can download the language template from the test block const blockLanguageDownloadButton = page.getByLabel( - "Download language template for block number 1" + "Download language template for block number 2" ); await expect(blockLanguageDownloadButton).toBeVisible(); @@ -249,6 +236,23 @@ test.describe.serial("Edit laboratory workflow", () => { join(__dirname, "data", "java-downloaded.zip") ); + // Assert teachers can download the tests archive from the test block + const blockTestsArchiveDownloadButton = page.getByLabel( + "Download tests archive for block number 2" + ); + await expect(blockTestsArchiveDownloadButton).toBeVisible(); + + // Assert the tests archive is downloaded + const blockTestsArchiveDownloadPromise = page.waitForEvent("download"); + + await blockTestsArchiveDownloadButton.click(); + + const blockTestsArchiveDownloadEvent = + await blockTestsArchiveDownloadPromise; + await blockTestsArchiveDownloadEvent.saveAs( + join(__dirname, "data", `${testBlockName}.zip`) + ); + // ## Edit the test block // Update the block name const nameInput = page.getByLabel("Block name"); @@ -257,7 +261,7 @@ test.describe.serial("Edit laboratory workflow", () => { // Open block dropdown const blockDropdownButton = page.getByRole("button", { - name: "Toggle options for block number 1" + name: "Toggle options for block number 2" }); await expect(blockDropdownButton).toBeVisible(); await blockDropdownButton.click(); @@ -273,15 +277,169 @@ test.describe.serial("Edit laboratory workflow", () => { await expect( page.getByText("The test block has been updated successfully") ).toBeVisible(); + }); - // ## Delete the test block - await blockDropdownButton.click(); - const deleteBlockButton = page.getByRole("menuitem", { + test("Swap index of blocks", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(teacherPassword); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the course page + await page.getByRole("link", { name: courseName }).click(); + + // Enter to the edit page + const editLaboratoryButton = page.getByRole("link", { + name: `Edit ${laboratoryName} laboratory` + }); + await expect(editLaboratoryButton).toBeVisible(); + await editLaboratoryButton.click(); + + // Move the first block (markdown) to the second position + let markdownBlockDropdownMenu = page.getByRole("button", { + name: "Toggle options for block number 1" + }); + await expect(markdownBlockDropdownMenu).toBeVisible(); + await markdownBlockDropdownMenu.click(); + + const moveDownMarkdownBlockButton = page.getByRole("menuitem", { + name: "Move down" + }); + await expect(moveDownMarkdownBlockButton).toBeVisible(); + await moveDownMarkdownBlockButton.click(); + + // Assert an alert is shown + await expect( + page.getByText("The markdown block has been moved down successfully") + ).toBeVisible(); + + // Now, the test block is in the first position and the markdown block is in the second position + // Assert the markdown block is in the second position + let markdownBlockContentLabel = "Laboratory block 2 markdown content"; + await expect(page.getByLabel(markdownBlockContentLabel)).toBeVisible(); + + // Move the first block (test) to the second position + let testBlockDropdownButton = page.getByRole("button", { + name: "Toggle options for block number 1" + }); + await expect(testBlockDropdownButton).toBeVisible(); + await testBlockDropdownButton.click(); + + const moveDownTestBlockButton = page.getByRole("menuitem", { + name: "Move down" + }); + await expect(moveDownTestBlockButton).toBeVisible(); + await moveDownTestBlockButton.click(); + + // Assert an alert is shown + await expect( + page.getByText("The test block has been moved down successfully") + ).toBeVisible(); + + // Now, the test block is in the second position and the markdown block is in the first position + // Assert the markdown block is in the first position + markdownBlockContentLabel = "Laboratory block 1 markdown content"; + await expect(page.getByLabel(markdownBlockContentLabel)).toBeVisible(); + + // Move the second block (test) to the first position + testBlockDropdownButton = page.getByRole("button", { + name: "Toggle options for block number 2" + }); + await expect(testBlockDropdownButton).toBeVisible(); + await testBlockDropdownButton.click(); + + const moveUpTestBlockButton = page.getByRole("menuitem", { + name: "Move up" + }); + await expect(moveUpTestBlockButton).toBeVisible(); + await moveUpTestBlockButton.click(); + + // Assert an alert is shown + await expect( + page.getByText("The test block has been moved up successfully") + ).toBeVisible(); + + // Now, the test block is in the first position and the markdown block is in the second position + // Assert the markdown block is in the second position + markdownBlockContentLabel = "Laboratory block 2 markdown content"; + await expect(page.getByLabel(markdownBlockContentLabel)).toBeVisible(); + + // Move the second block (markdown) to the first position + markdownBlockDropdownMenu = page.getByRole("button", { + name: "Toggle options for block number 2" + }); + await expect(markdownBlockDropdownMenu).toBeVisible(); + await markdownBlockDropdownMenu.click(); + + const moveUpMarkdownBlockButton = page.getByRole("menuitem", { + name: "Move up" + }); + await expect(moveUpMarkdownBlockButton).toBeVisible(); + await moveUpMarkdownBlockButton.click(); + + // Assert an alert is shown + await expect( + page.getByText("The markdown block has been moved up successfully") + ).toBeVisible(); + + // Now, the test block is in the second position and the markdown block is in the first position + // Assert the markdown block is in the first position + markdownBlockContentLabel = "Laboratory block 1 markdown content"; + await expect(page.getByLabel(markdownBlockContentLabel)).toBeVisible(); + }); + + test("Delete blocks", async ({ page }) => { + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(teacherEmail); + await page.getByLabel("Password").fill(teacherPassword); + await page.getByRole("button", { name: "Submit" }).click(); + + // Go to the course page + await page.getByRole("link", { name: courseName }).click(); + + // Enter to the edit page + const editLaboratoryButton = page.getByRole("link", { + name: `Edit ${laboratoryName} laboratory` + }); + await expect(editLaboratoryButton).toBeVisible(); + await editLaboratoryButton.click(); + + // Delete the markdown block + const markdownBlockDropdownMenu = page.getByRole("button", { + name: "Toggle options for block number 1" + }); + await expect(markdownBlockDropdownMenu).toBeVisible(); + await markdownBlockDropdownMenu.click(); + + const deleteMarkdownBlockButton = page.getByRole("menuitem", { name: "Delete block" }); + await expect(deleteMarkdownBlockButton).toBeVisible(); + await deleteMarkdownBlockButton.click(); - await expect(deleteBlockButton).toBeVisible(); - await deleteBlockButton.click(); + // Assert an alert is shown + await expect( + page.getByText("The markdown block has been deleted successfully") + ).toBeVisible(); + + // Assert the block is not shown + const markdownBlockContentLabel = "Laboratory block 1 markdown content"; + await expect(page.getByLabel(markdownBlockContentLabel)).not.toBeVisible(); + + // Delete the test block + const testBlockDropdownMenu = page.getByRole("button", { + name: "Toggle options for block number 1" + }); + await expect(testBlockDropdownMenu).toBeVisible(); + await testBlockDropdownMenu.click(); + + const deleteTestBlockButton = page.getByRole("menuitem", { + name: "Delete block" + }); + await expect(deleteTestBlockButton).toBeVisible(); + await deleteTestBlockButton.click(); // Assert an alert is shown await expect( @@ -289,6 +447,7 @@ test.describe.serial("Edit laboratory workflow", () => { ).toBeVisible(); // Assert the block is not shown - await expect(page.getByLabel("Block name")).not.toBeVisible(); + const testBlockNameLabel = "Block name"; + await expect(page.getByLabel(testBlockNameLabel)).not.toBeVisible(); }); }); diff --git a/e2e/register/RegisterAdmins.spec.ts b/e2e/register/RegisterAdmins.spec.ts index bef4d95d..1f203376 100644 --- a/e2e/register/RegisterAdmins.spec.ts +++ b/e2e/register/RegisterAdmins.spec.ts @@ -114,4 +114,38 @@ test.describe.serial("Admin registration", () => { await logout.click(); await page.waitForURL(/\/login$/); }); + + test("Admins can update their profile", async ({ page }) => { + // Logout from the current admin account + const logout = page.getByRole("link", { name: "Logout", exact: true }); + await expect(logout).toBeVisible(); + await logout.click(); + await page.waitForURL(/\/login$/); + + // Login as the new admin + await page.goto("/login"); + await page.getByLabel("Email").fill(newAdminEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + await page.waitForURL(/\/admins$/); + + // Navigate to the profile view + await page.getByRole("link", { name: "Profile", exact: true }).click(); + await page.waitForURL(/\/profile$/); + + // Update the profile + const newName = getRandomName(); + const newEmail = getRandomEmail(); + + await page.getByLabel("Full name").fill(newName); + await page.getByLabel("Email").fill(newEmail); + await page.getByLabel("Password confirmation").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Update" }).click(); + + // Assert the alert is shown + await expect( + page.getByText("Your profile has been updated successfully") + ).toBeVisible(); + }); }); diff --git a/e2e/register/RegisterStudents.spec.ts b/e2e/register/RegisterStudents.spec.ts index 44954be0..62ee9a11 100644 --- a/e2e/register/RegisterStudents.spec.ts +++ b/e2e/register/RegisterStudents.spec.ts @@ -28,7 +28,7 @@ test("The fields are validated", async ({ page }) => { }); test.describe.serial("Student registration", () => { - const newStudentEmail = getRandomEmail(); + let newStudentEmail = getRandomEmail(); const newStudentID = getRandomUniversityID(); test("An student can register", async ({ page, baseURL }) => { @@ -83,7 +83,7 @@ test.describe.serial("Student registration", () => { await page.getByRole("button", { name: "Submit" }).click(); // Assert the wrong credentials alert is shown - await expect(page.getByText("Invalid credentials")).toBeVisible(); + await expect(page.getByText("Credentials are wrong")).toBeVisible(); // Fill the form with the correct credentials await page.getByLabel("Password").fill(getDefaultPassword()); @@ -110,4 +110,77 @@ test.describe.serial("Student registration", () => { await page.getByRole("link", { name: "Logout", exact: true }).click(); await page.waitForURL(/\/login$/); }); + + test("Student can update their profile", async ({ page }) => { + await page.goto("/login"); + await page.getByLabel("Email").fill(newStudentEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + await page.waitForURL(/\/courses$/); + + // Navigate to the profile view + await page.getByRole("link", { name: "Profile", exact: true }).click(); + await page.waitForURL(/\/profile$/); + + // Update the profile + const newName = getRandomName(); + const newEmail = getRandomEmail(); + const newID = getRandomUniversityID(); + + await page.getByLabel("Full name").fill(newName); + await page.getByLabel("Email").fill(newEmail); + await page.getByLabel("Institutional ID").fill(newID); + await page.getByLabel("Password confirmation").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Update" }).click(); + + // Assert the alert is shown + await expect( + page.getByText("Your profile has been updated successfully") + ).toBeVisible(); + + newStudentEmail = newEmail; + }); + + test("Students can update their password", async ({ page }) => { + await page.goto("/login"); + await page.getByLabel("Email").fill(newStudentEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + await page.waitForURL(/\/courses$/); + + // Navigate to the profile view + await page.getByRole("link", { name: "Profile", exact: true }).click(); + await page.waitForURL(/\/profile$/); + + // Select the tab to change the password + await page.getByRole("tab", { name: "Change password" }).click(); + + // Change the password + await page.getByLabel("Current password").fill(getDefaultPassword()); + + const newPassword = getDefaultPassword() + "/*updated*/"; + await page.getByLabel("New password", { exact: true }).fill(newPassword); + await page + .getByLabel("Confirm new password", { exact: true }) + .fill(newPassword); + + await page.getByRole("button", { name: "Update" }).click(); + + // Assert the alert is shown + await expect( + page.getByText("Your password has been updated") + ).toBeVisible(); + + // Logout + await page.getByRole("link", { name: "Logout", exact: true }).click(); + await page.waitForURL(/\/login$/); + + // Login with the new password + await page.getByLabel("Email").fill(newStudentEmail); + await page.getByLabel("Password").fill(newPassword); + await page.getByRole("button", { name: "Submit" }).click(); + await page.waitForURL(/\/courses$/); + }); }); diff --git a/e2e/register/RegisterTeachers.spec.ts b/e2e/register/RegisterTeachers.spec.ts index 52d366ff..707ace30 100644 --- a/e2e/register/RegisterTeachers.spec.ts +++ b/e2e/register/RegisterTeachers.spec.ts @@ -113,4 +113,38 @@ test.describe.serial("Teacher registration", () => { await logout.click(); await page.waitForURL(/\/login$/); }); + + test("Teachers can update their profile", async ({ page }) => { + // Logout from the admin account + const logout = page.getByRole("link", { name: "Logout", exact: true }); + await expect(logout).toBeVisible(); + await logout.click(); + await page.waitForURL(/\/login$/); + + // Login as a teacher + await page.goto("/login"); + await page.getByLabel("Email").fill(newTeacherEmail); + await page.getByLabel("Password").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Submit" }).click(); + + await page.waitForURL(/\/courses$/); + + // Navigate to the profile view + await page.getByRole("link", { name: "Profile", exact: true }).click(); + await page.waitForURL(/\/profile$/); + + // Update the profile + const newName = getRandomName(); + const newEmail = getRandomEmail(); + + await page.getByLabel("Full name").fill(newName); + await page.getByLabel("Email").fill(newEmail); + await page.getByLabel("Password confirmation").fill(getDefaultPassword()); + await page.getByRole("button", { name: "Update" }).click(); + + // Assert the alert is shown + await expect( + page.getByText("Your profile has been updated successfully") + ).toBeVisible(); + }); }); diff --git a/package.json b/package.json index 56e50718..9c27a499 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-client", "private": true, - "version": "0.40.0", + "version": "0.46.0", "type": "module", "scripts": { "dev": "vite", @@ -16,7 +16,6 @@ "format:fix": "prettier src/ --write" }, "dependencies": { - "@fontsource/ibm-plex-mono": "5.0.8", "@hookform/resolvers": "3.3.4", "@radix-ui/react-alert-dialog": "1.0.5", "@radix-ui/react-dialog": "1.0.5", @@ -27,9 +26,9 @@ "@radix-ui/react-separator": "1.0.3", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-tabs": "^1.0.4", - "@tanstack/react-query": "^5.17.9", - "@tanstack/react-query-devtools": "^5.17.9", - "@tanstack/react-table": "8.11.4", + "@tanstack/react-query": "^5.17.15", + "@tanstack/react-query-devtools": "^5.17.18", + "@tanstack/react-table": "8.11.6", "@uiw/react-markdown-preview": "5.0.7", "@uiw/react-md-editor": "4.0.3", "axios": "1.6.5", @@ -37,41 +36,41 @@ "class-variance-authority": "0.7.0", "clsx": "2.1.0", "dayjs": "1.11.10", - "lucide-react": "0.309.0", + "lucide-react": "0.312.0", "react": "18.2.0", "react-chartjs-2": "5.2.0", "react-dom": "18.2.0", "react-hook-form": "7.49.3", "react-lazily": "^0.9.2", - "react-router-dom": "6.21.2", + "react-router-dom": "6.21.3", "rehype-external-links": "3.0.0", "rehype-sanitize": "6.0.0", "sonner": "1.3.1", "tailwind-merge": "2.2.0", "tailwindcss-animate": "1.0.7", "zod": "3.22.4", - "zustand": "4.4.7" + "zustand": "4.5.0" }, "devDependencies": { "@faker-js/faker": "8.3.1", - "@playwright/test": "1.40.1", + "@playwright/test": "1.41.1", "@trivago/prettier-plugin-sort-imports": "4.3.0", - "@types/node": "20.11.0", - "@types/react": "18.2.47", + "@types/node": "20.11.5", + "@types/react": "18.2.48", "@types/react-dom": "18.2.18", - "@typescript-eslint/eslint-plugin": "6.18.1", - "@typescript-eslint/parser": "6.18.1", + "@typescript-eslint/eslint-plugin": "6.19.0", + "@typescript-eslint/parser": "6.19.0", "@vitejs/plugin-react-swc": "3.5.0", - "autoprefixer": "10.4.16", + "autoprefixer": "10.4.17", "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.5", "postcss": "8.4.33", - "prettier": "3.1.1", + "prettier": "3.2.4", "prettier-plugin-tailwindcss": "0.5.11", "tailwindcss": "3.4.1", "typescript": "5.3.3", - "vite": "5.0.11" + "vite": "5.0.12" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6675fef..876d88a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,54 +5,51 @@ settings: excludeLinksFromLockfile: false dependencies: - '@fontsource/ibm-plex-mono': - specifier: 5.0.8 - version: 5.0.8 '@hookform/resolvers': specifier: 3.3.4 version: 3.3.4(react-hook-form@7.49.3) '@radix-ui/react-alert-dialog': specifier: 1.0.5 - version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dialog': specifier: 1.0.5 - version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dropdown-menu': specifier: 2.0.6 - version: 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-label': specifier: 2.0.2 - version: 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-scroll-area': specifier: 1.0.5 - version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-select': specifier: 2.0.0 - version: 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-separator': specifier: 1.0.3 - version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: 1.0.2 - version: 1.0.2(@types/react@18.2.47)(react@18.2.0) + version: 1.0.2(@types/react@18.2.48)(react@18.2.0) '@radix-ui/react-tabs': specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-query': - specifier: ^5.17.9 - version: 5.17.9(react@18.2.0) + specifier: ^5.17.15 + version: 5.17.15(react@18.2.0) '@tanstack/react-query-devtools': - specifier: ^5.17.9 - version: 5.17.9(@tanstack/react-query@5.17.9)(react@18.2.0) + specifier: ^5.17.18 + version: 5.17.18(@tanstack/react-query@5.17.15)(react@18.2.0) '@tanstack/react-table': - specifier: 8.11.4 - version: 8.11.4(react-dom@18.2.0)(react@18.2.0) + specifier: 8.11.6 + version: 8.11.6(react-dom@18.2.0)(react@18.2.0) '@uiw/react-markdown-preview': specifier: 5.0.7 - version: 5.0.7(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@uiw/react-md-editor': specifier: 4.0.3 - version: 4.0.3(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + version: 4.0.3(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) axios: specifier: 1.6.5 version: 1.6.5 @@ -69,8 +66,8 @@ dependencies: specifier: 1.11.10 version: 1.11.10 lucide-react: - specifier: 0.309.0 - version: 0.309.0(react@18.2.0) + specifier: 0.312.0 + version: 0.312.0(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -87,8 +84,8 @@ dependencies: specifier: ^0.9.2 version: 0.9.2(react-dom@18.2.0)(react@18.2.0) react-router-dom: - specifier: 6.21.2 - version: 6.21.2(react-dom@18.2.0)(react@18.2.0) + specifier: 6.21.3 + version: 6.21.3(react-dom@18.2.0)(react@18.2.0) rehype-external-links: specifier: 3.0.0 version: 3.0.0 @@ -108,40 +105,40 @@ dependencies: specifier: 3.22.4 version: 3.22.4 zustand: - specifier: 4.4.7 - version: 4.4.7(@types/react@18.2.47)(react@18.2.0) + specifier: 4.5.0 + version: 4.5.0(@types/react@18.2.48)(react@18.2.0) devDependencies: '@faker-js/faker': specifier: 8.3.1 version: 8.3.1 '@playwright/test': - specifier: 1.40.1 - version: 1.40.1 + specifier: 1.41.1 + version: 1.41.1 '@trivago/prettier-plugin-sort-imports': specifier: 4.3.0 - version: 4.3.0(prettier@3.1.1) + version: 4.3.0(prettier@3.2.4) '@types/node': - specifier: 20.11.0 - version: 20.11.0 + specifier: 20.11.5 + version: 20.11.5 '@types/react': - specifier: 18.2.47 - version: 18.2.47 + specifier: 18.2.48 + version: 18.2.48 '@types/react-dom': specifier: 18.2.18 version: 18.2.18 '@typescript-eslint/eslint-plugin': - specifier: 6.18.1 - version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3) + specifier: 6.19.0 + version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': - specifier: 6.18.1 - version: 6.18.1(eslint@8.56.0)(typescript@5.3.3) + specifier: 6.19.0 + version: 6.19.0(eslint@8.56.0)(typescript@5.3.3) '@vitejs/plugin-react-swc': specifier: 3.5.0 - version: 3.5.0(vite@5.0.11) + version: 3.5.0(vite@5.0.12) autoprefixer: - specifier: 10.4.16 - version: 10.4.16(postcss@8.4.33) + specifier: 10.4.17 + version: 10.4.17(postcss@8.4.33) eslint: specifier: 8.56.0 version: 8.56.0 @@ -158,11 +155,11 @@ devDependencies: specifier: 8.4.33 version: 8.4.33 prettier: - specifier: 3.1.1 - version: 3.1.1 + specifier: 3.2.4 + version: 3.2.4 prettier-plugin-tailwindcss: specifier: 0.5.11 - version: 0.5.11(@trivago/prettier-plugin-sort-imports@4.3.0)(prettier@3.1.1) + version: 0.5.11(@trivago/prettier-plugin-sort-imports@4.3.0)(prettier@3.2.4) tailwindcss: specifier: 3.4.1 version: 3.4.1 @@ -170,8 +167,8 @@ devDependencies: specifier: 5.3.3 version: 5.3.3 vite: - specifier: 5.0.11 - version: 5.0.11(@types/node@20.11.0) + specifier: 5.0.12 + version: 5.0.12(@types/node@20.11.5) packages: @@ -591,10 +588,6 @@ packages: resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} dev: false - /@fontsource/ibm-plex-mono@5.0.8: - resolution: {integrity: sha512-AoR7K2YtlGKy3LApyLYjqjyz0o8XtkRvbB/juMsGSGNayBBAly140ykZls+VxcXjZSLQpp2RyxKo6lYOuHPmUw==} - dev: false - /@hookform/resolvers@3.3.4(react-hook-form@7.49.3): resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} peerDependencies: @@ -670,12 +663,12 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 - /@playwright/test@1.40.1: - resolution: {integrity: sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==} + /@playwright/test@1.41.1: + resolution: {integrity: sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==} engines: {node: '>=16'} hasBin: true dependencies: - playwright: 1.40.1 + playwright: 1.41.1 dev: true /@radix-ui/number@1.0.1: @@ -690,7 +683,7 @@ packages: '@babel/runtime': 7.23.7 dev: false - /@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==} peerDependencies: '@types/react': '*' @@ -705,18 +698,18 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: '@types/react': '*' @@ -730,14 +723,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: '@types/react': '*' @@ -751,17 +744,17 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: '@types/react': '*' @@ -771,11 +764,11 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-context@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-context@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: '@types/react': '*' @@ -785,11 +778,11 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} peerDependencies: '@types/react': '*' @@ -804,26 +797,26 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 aria-hidden: 1.2.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.5(@types/react@18.2.47)(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.48)(react@18.2.0) dev: false - /@radix-ui/react-direction@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-direction@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: '@types/react': '*' @@ -833,11 +826,11 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} peerDependencies: '@types/react': '*' @@ -852,17 +845,17 @@ packages: dependencies: '@babel/runtime': 7.23.7 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} peerDependencies: '@types/react': '*' @@ -877,19 +870,19 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: '@types/react': '*' @@ -899,11 +892,11 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} peerDependencies: '@types/react': '*' @@ -917,16 +910,16 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-id@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-id@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} peerDependencies: '@types/react': '*' @@ -936,12 +929,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} peerDependencies: '@types/react': '*' @@ -955,14 +948,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.2 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} peerDependencies: '@types/react': '*' @@ -977,30 +970,30 @@ packages: dependencies: '@babel/runtime': 7.23.7 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 aria-hidden: 1.2.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.5(@types/react@18.2.47)(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.48)(react@18.2.0) dev: false - /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} peerDependencies: '@types/react': '*' @@ -1015,22 +1008,22 @@ packages: dependencies: '@babel/runtime': 7.23.7 '@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.47)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.48)(react@18.2.0) '@radix-ui/rect': 1.0.1 - '@types/react': 18.2.47 + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} peerDependencies: '@types/react': '*' @@ -1044,14 +1037,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} peerDependencies: '@types/react': '*' @@ -1065,15 +1058,15 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: '@types/react': '*' @@ -1087,14 +1080,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: '@types/react': '*' @@ -1109,21 +1102,21 @@ packages: dependencies: '@babel/runtime': 7.23.7 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-scroll-area@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-scroll-area@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==} peerDependencies: '@types/react': '*' @@ -1139,20 +1132,20 @@ packages: '@babel/runtime': 7.23.2 '@radix-ui/number': 1.0.1 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-select@2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-select@2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==} peerDependencies: '@types/react': '*' @@ -1168,32 +1161,32 @@ packages: '@babel/runtime': 7.23.2 '@radix-ui/number': 1.0.1 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 aria-hidden: 1.2.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.5(@types/react@18.2.47)(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.48)(react@18.2.0) dev: false - /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} peerDependencies: '@types/react': '*' @@ -1207,14 +1200,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.2 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-slot@1.0.2(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-slot@1.0.2(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: '@types/react': '*' @@ -1224,12 +1217,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.2 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} peerDependencies: '@types/react': '*' @@ -1244,20 +1237,20 @@ packages: dependencies: '@babel/runtime': 7.23.7 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: '@types/react': '*' @@ -1267,11 +1260,11 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} peerDependencies: '@types/react': '*' @@ -1281,12 +1274,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} peerDependencies: '@types/react': '*' @@ -1296,12 +1289,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} peerDependencies: '@types/react': '*' @@ -1311,11 +1304,11 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} peerDependencies: '@types/react': '*' @@ -1325,11 +1318,11 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: '@types/react': '*' @@ -1340,11 +1333,11 @@ packages: dependencies: '@babel/runtime': 7.23.7 '@radix-ui/rect': 1.0.1 - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-use-size@1.0.1(@types/react@18.2.47)(react@18.2.0): + /@radix-ui/react-use-size@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} peerDependencies: '@types/react': '*' @@ -1354,12 +1347,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.47)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 react: 18.2.0 dev: false - /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} peerDependencies: '@types/react': '*' @@ -1373,8 +1366,8 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.7 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.47 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.48 '@types/react-dom': 18.2.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -1610,52 +1603,52 @@ packages: resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==} dev: true - /@tanstack/query-core@5.17.9: - resolution: {integrity: sha512-8xcvpWIPaRMDNLMvG9ugcUJMgFK316ZsqkPPbsI+TMZsb10N9jk0B6XgPk4/kgWC2ziHyWR7n7wUhxmD0pChQw==} + /@tanstack/query-core@5.17.15: + resolution: {integrity: sha512-QURxpu77/ICA4d61aPvV7EcJ2MwmksxUejKBaq/xLcO2TUJAlXf4PFKHC/WxnVFI/7F1jeLx85AO3Vpk0+uBXw==} dev: false /@tanstack/query-devtools@5.17.7: resolution: {integrity: sha512-TfgvOqza5K7Sk6slxqkRIvXlEJoUoPSsGGwpuYSrpqgSwLSSvPPpZhq7hv7hcY5IvRoTNGoq6+MT01C/jILqoQ==} dev: false - /@tanstack/react-query-devtools@5.17.9(@tanstack/react-query@5.17.9)(react@18.2.0): - resolution: {integrity: sha512-1viWP/jlO0LaeCdtTFqtF1k2RfM3KVpvwVffWv+PMNkS2u4s8YGUM17r3p82udbF9BY1mE7aHqQ3MM1errF5lQ==} + /@tanstack/react-query-devtools@5.17.18(@tanstack/react-query@5.17.15)(react@18.2.0): + resolution: {integrity: sha512-La1+8aKacGBZ4qvEdXx6ugVbHhm48fC/aBKfaHIkVB6lWKGiYq0pJm+kmzcRKsHmVz5BwfFoz6HCCnR5kByPnw==} peerDependencies: - '@tanstack/react-query': ^5.17.9 + '@tanstack/react-query': ^5.17.15 react: ^18.0.0 dependencies: '@tanstack/query-devtools': 5.17.7 - '@tanstack/react-query': 5.17.9(react@18.2.0) + '@tanstack/react-query': 5.17.15(react@18.2.0) react: 18.2.0 dev: false - /@tanstack/react-query@5.17.9(react@18.2.0): - resolution: {integrity: sha512-M5E9gwUq1Stby/pdlYjBlL24euIVuGbWKIFCbtnQxSdXI4PgzjTSdXdV3QE6fc+itF+TUvX/JPTKIwq8yuBXcg==} + /@tanstack/react-query@5.17.15(react@18.2.0): + resolution: {integrity: sha512-9qur91mOihaUN7pXm6ioDtS+4qgkBcCiIaZyvi3lZNcQZsrMGCYZ+eP3hiFrV4khoJyJrFUX1W0NcCVlgwNZxQ==} peerDependencies: react: ^18.0.0 dependencies: - '@tanstack/query-core': 5.17.9 + '@tanstack/query-core': 5.17.15 react: 18.2.0 dev: false - /@tanstack/react-table@8.11.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-P9OIM+4zM66wrm/27mV+UnjoFn9A/oiH7tIrtymvuq7uMAhrn20hOQCyjExM1XhRhjxyUHAXdhIjJJ3rL/FTnA==} + /@tanstack/react-table@8.11.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i0heTVZtuHF9VPMwcYoZ21hbzGDmLjxHYemDMvbGpjk5fUZ8nLF3S8qjVRU79XfPW8KK9o7iTU2fGFVQQmxMSQ==} engines: {node: '>=12'} peerDependencies: react: '>=16' react-dom: '>=16' dependencies: - '@tanstack/table-core': 8.11.4 + '@tanstack/table-core': 8.11.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@tanstack/table-core@8.11.4: - resolution: {integrity: sha512-wvEl2wXJdFt3FZ+CRtOen2t6M9LwHwLKZ4BehMS2voGkCIApbTNAX9AnKZS2GNq9OjXHSj0dEQfTtZvXNk4LBQ==} + /@tanstack/table-core@8.11.6: + resolution: {integrity: sha512-69WEY1PaZROaGYUrseng4/4sMYnRGhDe1vM6888CnWekGz/wuCnvqwOoOuKGYivnaiI4BVmZq4WKWhvahyj3/g==} engines: {node: '>=12'} dev: false - /@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.1.1): + /@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.2.4): resolution: {integrity: sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==} peerDependencies: '@vue/compiler-sfc': 3.x @@ -1670,7 +1663,7 @@ packages: '@babel/types': 7.17.0 javascript-natural-sort: 0.7.1 lodash: 4.17.21 - prettier: 3.1.1 + prettier: 3.2.4 transitivePeerDependencies: - supports-color dev: true @@ -1717,8 +1710,8 @@ packages: resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} dev: false - /@types/node@20.11.0: - resolution: {integrity: sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==} + /@types/node@20.11.5: + resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} dependencies: undici-types: 5.26.5 dev: true @@ -1733,10 +1726,10 @@ packages: /@types/react-dom@18.2.18: resolution: {integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==} dependencies: - '@types/react': 18.2.47 + '@types/react': 18.2.48 - /@types/react@18.2.47: - resolution: {integrity: sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==} + /@types/react@18.2.48: + resolution: {integrity: sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==} dependencies: '@types/prop-types': 15.7.10 '@types/scheduler': 0.16.6 @@ -1757,8 +1750,8 @@ packages: resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} dev: false - /@typescript-eslint/eslint-plugin@6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==} + /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -1769,11 +1762,11 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.18.1(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/scope-manager': 6.18.1 - '@typescript-eslint/type-utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.18.1 + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 debug: 4.3.4 eslint: 8.56.0 graphemer: 1.4.0 @@ -1786,8 +1779,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.18.1(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==} + /@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1796,10 +1789,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.18.1 - '@typescript-eslint/types': 6.18.1 - '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.18.1 + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 debug: 4.3.4 eslint: 8.56.0 typescript: 5.3.3 @@ -1807,16 +1800,16 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager@6.18.1: - resolution: {integrity: sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==} + /@typescript-eslint/scope-manager@6.19.0: + resolution: {integrity: sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.18.1 - '@typescript-eslint/visitor-keys': 6.18.1 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/visitor-keys': 6.19.0 dev: true - /@typescript-eslint/type-utils@6.18.1(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==} + /@typescript-eslint/type-utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1825,8 +1818,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3) - '@typescript-eslint/utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) debug: 4.3.4 eslint: 8.56.0 ts-api-utils: 1.0.3(typescript@5.3.3) @@ -1835,13 +1828,13 @@ packages: - supports-color dev: true - /@typescript-eslint/types@6.18.1: - resolution: {integrity: sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==} + /@typescript-eslint/types@6.19.0: + resolution: {integrity: sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.18.1(typescript@5.3.3): - resolution: {integrity: sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==} + /@typescript-eslint/typescript-estree@6.19.0(typescript@5.3.3): + resolution: {integrity: sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -1849,8 +1842,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.18.1 - '@typescript-eslint/visitor-keys': 6.18.1 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/visitor-keys': 6.19.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -1862,8 +1855,8 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.18.1(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==} + /@typescript-eslint/utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1871,9 +1864,9 @@ packages: '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.5 - '@typescript-eslint/scope-manager': 6.18.1 - '@typescript-eslint/types': 6.18.1 - '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) eslint: 8.56.0 semver: 7.5.4 transitivePeerDependencies: @@ -1881,11 +1874,11 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@6.18.1: - resolution: {integrity: sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==} + /@typescript-eslint/visitor-keys@6.19.0: + resolution: {integrity: sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/types': 6.19.0 eslint-visitor-keys: 3.4.3 dev: true @@ -1893,7 +1886,7 @@ packages: resolution: {integrity: sha512-IXR+N363nLTR3ilklmM+B0nk774jVE/muOrBYt4Rdww/Pf3uP9XHyv2x6YZrbDh29F7w9BkzQyB8QF6WDShmJA==} dev: false - /@uiw/react-markdown-preview@5.0.7(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@uiw/react-markdown-preview@5.0.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EmNI3LPM5Ff5ikcHJHcoZW268gpeAUPISfIwQaPjjHf/ET4aHNyo8sFBGV0+ycAaS52fXl2cvF+k/JweuMVVeQ==} peerDependencies: react: '>=16.8.0' @@ -1903,7 +1896,7 @@ packages: '@uiw/copy-to-clipboard': 1.0.16 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-markdown: 9.0.1(@types/react@18.2.47)(react@18.2.0) + react-markdown: 9.0.1(@types/react@18.2.48)(react@18.2.0) rehype-attr: 3.0.3 rehype-autolink-headings: 7.1.0 rehype-ignore: 2.0.2 @@ -1918,14 +1911,14 @@ packages: - supports-color dev: false - /@uiw/react-md-editor@4.0.3(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0): + /@uiw/react-md-editor@4.0.3(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-TvChXxUBUS21Rk0cVC0aeJoWcFZ/G0xN/Hc4Lv9FGFK8wPOHESd7Bcq4jNRHJ6lEzE/+d4Wh00lEVNKj+rQyBw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: '@babel/runtime': 7.23.7 - '@uiw/react-markdown-preview': 5.0.7(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) + '@uiw/react-markdown-preview': 5.0.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) rehype: 13.0.1 @@ -1938,13 +1931,13 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - /@vitejs/plugin-react-swc@3.5.0(vite@5.0.11): + /@vitejs/plugin-react-swc@3.5.0(vite@5.0.12): resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==} peerDependencies: vite: ^4 || ^5 dependencies: '@swc/core': 1.3.96 - vite: 5.0.11(@types/node@20.11.0) + vite: 5.0.12(@types/node@20.11.5) transitivePeerDependencies: - '@swc/helpers' dev: true @@ -2024,15 +2017,15 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false - /autoprefixer@10.4.16(postcss@8.4.33): - resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==} + /autoprefixer@10.4.17(postcss@8.4.33): + resolution: {integrity: sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 dependencies: - browserslist: 4.22.1 - caniuse-lite: 1.0.30001561 + browserslist: 4.22.2 + caniuse-lite: 1.0.30001579 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -2087,15 +2080,15 @@ packages: dependencies: fill-range: 7.0.1 - /browserslist@4.22.1: - resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + /browserslist@4.22.2: + resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001561 - electron-to-chromium: 1.4.581 - node-releases: 2.0.13 - update-browserslist-db: 1.0.13(browserslist@4.22.1) + caniuse-lite: 1.0.30001579 + electron-to-chromium: 1.4.640 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.22.2) dev: true /callsites@3.1.0: @@ -2107,8 +2100,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - /caniuse-lite@1.0.30001561: - resolution: {integrity: sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==} + /caniuse-lite@1.0.30001579: + resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} dev: true /ccount@2.0.1: @@ -2315,8 +2308,8 @@ packages: esutils: 2.0.3 dev: true - /electron-to-chromium@1.4.581: - resolution: {integrity: sha512-6uhqWBIapTJUxgPTCHH9sqdbxIMPt7oXl0VcAL1kOtlU6aECdcMncCrX5Z7sHQ/invtrC9jUQUef7+HhO8vVFw==} + /electron-to-chromium@1.4.640: + resolution: {integrity: sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==} dev: true /entities@4.5.0: @@ -3074,8 +3067,8 @@ packages: yallist: 4.0.0 dev: true - /lucide-react@0.309.0(react@18.2.0): - resolution: {integrity: sha512-zNVPczuwFrCfksZH3zbd1UDE6/WYhYAdbe2k7CImVyPAkXLgIwbs6eXQ4loigqDnUFjyFYCI5jZ1y10Kqal0dg==} + /lucide-react@0.312.0(react@18.2.0): + resolution: {integrity: sha512-3UZsqyswRXjW4t+nw+InICewSimjPKHuSxiFYqTshv9xkK3tPPntXk/lvXc9pKlXIxm3v9WKyoxcrB6YHhP+dg==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 dependencies: @@ -3569,8 +3562,8 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /node-releases@2.0.13: - resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true /normalize-path@3.0.0: @@ -3698,18 +3691,18 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - /playwright-core@1.40.1: - resolution: {integrity: sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==} + /playwright-core@1.41.1: + resolution: {integrity: sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==} engines: {node: '>=16'} hasBin: true dev: true - /playwright@1.40.1: - resolution: {integrity: sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==} + /playwright@1.41.1: + resolution: {integrity: sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==} engines: {node: '>=16'} hasBin: true dependencies: - playwright-core: 1.40.1 + playwright-core: 1.41.1 optionalDependencies: fsevents: 2.3.2 dev: true @@ -3782,7 +3775,7 @@ packages: engines: {node: '>= 0.8.0'} dev: true - /prettier-plugin-tailwindcss@0.5.11(@trivago/prettier-plugin-sort-imports@4.3.0)(prettier@3.1.1): + /prettier-plugin-tailwindcss@0.5.11(@trivago/prettier-plugin-sort-imports@4.3.0)(prettier@3.2.4): resolution: {integrity: sha512-AvI/DNyMctyyxGOjyePgi/gqj5hJYClZ1avtQvLlqMT3uDZkRbi4HhGUpok3DRzv9z7Lti85Kdj3s3/1CeNI0w==} engines: {node: '>=14.21.3'} peerDependencies: @@ -3831,12 +3824,12 @@ packages: prettier-plugin-twig-melody: optional: true dependencies: - '@trivago/prettier-plugin-sort-imports': 4.3.0(prettier@3.1.1) - prettier: 3.1.1 + '@trivago/prettier-plugin-sort-imports': 4.3.0(prettier@3.2.4) + prettier: 3.2.4 dev: true - /prettier@3.1.1: - resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} + /prettier@3.2.4: + resolution: {integrity: sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==} engines: {node: '>=14'} hasBin: true dev: true @@ -3902,14 +3895,14 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-markdown@9.0.1(@types/react@18.2.47)(react@18.2.0): + /react-markdown@9.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} peerDependencies: '@types/react': '>=18' react: '>=18' dependencies: '@types/hast': 3.0.3 - '@types/react': 18.2.47 + '@types/react': 18.2.48 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.0 html-url-attributes: 3.0.0 @@ -3924,7 +3917,7 @@ packages: - supports-color dev: false - /react-remove-scroll-bar@2.3.4(@types/react@18.2.47)(react@18.2.0): + /react-remove-scroll-bar@2.3.4(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} engines: {node: '>=10'} peerDependencies: @@ -3934,13 +3927,13 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 - react-style-singleton: 2.2.1(@types/react@18.2.47)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) tslib: 2.6.2 dev: false - /react-remove-scroll@2.5.5(@types/react@18.2.47)(react@18.2.0): + /react-remove-scroll@2.5.5(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} engines: {node: '>=10'} peerDependencies: @@ -3950,17 +3943,17 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 - react-remove-scroll-bar: 2.3.4(@types/react@18.2.47)(react@18.2.0) - react-style-singleton: 2.2.1(@types/react@18.2.47)(react@18.2.0) + react-remove-scroll-bar: 2.3.4(@types/react@18.2.48)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) tslib: 2.6.2 - use-callback-ref: 1.3.0(@types/react@18.2.47)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.47)(react@18.2.0) + use-callback-ref: 1.3.0(@types/react@18.2.48)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.48)(react@18.2.0) dev: false - /react-router-dom@6.21.2(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-tE13UukgUOh2/sqYr6jPzZTzmzc70aGRP4pAjG2if0IP3aUT+sBtAKUJh0qMh0zylJHGLmzS+XWVaON4UklHeg==} + /react-router-dom@6.21.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' @@ -3969,11 +3962,11 @@ packages: '@remix-run/router': 1.14.2 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router: 6.21.2(react@18.2.0) + react-router: 6.21.3(react@18.2.0) dev: false - /react-router@6.21.2(react@18.2.0): - resolution: {integrity: sha512-jJcgiwDsnaHIeC+IN7atO0XiSRCrOsQAHHbChtJxmgqG2IaYQXSnhqGb5vk2CU/wBQA12Zt+TkbuJjIn65gzbA==} + /react-router@6.21.3(react@18.2.0): + resolution: {integrity: sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' @@ -3982,7 +3975,7 @@ packages: react: 18.2.0 dev: false - /react-style-singleton@2.2.1(@types/react@18.2.47)(react@18.2.0): + /react-style-singleton@2.2.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: @@ -3992,7 +3985,7 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.47 + '@types/react': 18.2.48 get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 @@ -4516,13 +4509,13 @@ packages: unist-util-visit-parents: 6.0.1 dev: false - /update-browserslist-db@1.0.13(browserslist@4.22.1): + /update-browserslist-db@1.0.13(browserslist@4.22.2): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.22.1 + browserslist: 4.22.2 escalade: 3.1.1 picocolors: 1.0.0 dev: true @@ -4533,7 +4526,7 @@ packages: punycode: 2.3.1 dev: true - /use-callback-ref@1.3.0(@types/react@18.2.47)(react@18.2.0): + /use-callback-ref@1.3.0(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==} engines: {node: '>=10'} peerDependencies: @@ -4543,12 +4536,12 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 tslib: 2.6.2 dev: false - /use-sidecar@1.1.2(@types/react@18.2.47)(react@18.2.0): + /use-sidecar@1.1.2(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} peerDependencies: @@ -4558,7 +4551,7 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.47 + '@types/react': 18.2.48 detect-node-es: 1.1.0 react: 18.2.0 tslib: 2.6.2 @@ -4597,8 +4590,8 @@ packages: vfile-message: 4.0.2 dev: false - /vite@5.0.11(@types/node@20.11.0): - resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} + /vite@5.0.12(@types/node@20.11.5): + resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -4625,7 +4618,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.0 + '@types/node': 20.11.5 esbuild: 0.19.5 postcss: 8.4.33 rollup: 4.4.1 @@ -4665,12 +4658,12 @@ packages: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false - /zustand@4.4.7(@types/react@18.2.47)(react@18.2.0): - resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} + /zustand@4.5.0(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==} engines: {node: '>=12.7.0'} peerDependencies: '@types/react': '>=16.8' - immer: '>=9.0' + immer: '>=9.0.6' react: '>=16.8' peerDependenciesMeta: '@types/react': @@ -4680,7 +4673,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.47 + '@types/react': 18.2.48 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: false diff --git a/public/images/blank-canvas-image.svg b/public/images/blank-canvas-image.svg new file mode 100644 index 00000000..12c15976 --- /dev/null +++ b/public/images/blank-canvas-image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/EmptyContentText.tsx b/src/components/EmptyContentText.tsx index 9f90bfae..913f7696 100644 --- a/src/components/EmptyContentText.tsx +++ b/src/components/EmptyContentText.tsx @@ -1,7 +1,13 @@ interface EmptyContentTextProps { text: string; + className?: string; } -export const EmptyContentText = ({ text }: EmptyContentTextProps) => { - return

{text} 🤷

; +export const EmptyContentText = ({ + text, + className +}: EmptyContentTextProps) => { + return ( +

{text} 🤷

+ ); }; diff --git a/src/main.tsx b/src/main.tsx index 305e2f41..9a2ff268 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -13,6 +13,7 @@ import { CourseParticipants } from "@/screens/course-page/participants/CoursePar import { CoursesHome } from "@/screens/courses-list/CoursesHome"; import { EditLaboratory } from "@/screens/edit-laboratory/EditLaboratory"; import { EditRubricView } from "@/screens/edit-rubric/EditRubricView"; +import { ProfileView } from "@/screens/profile/ProfileView"; import { RubricsHome } from "@/screens/rubrics-list/RubricsHome"; import { FormContainer } from "@/screens/session/FormContainer"; import { Login } from "@/screens/session/login/Login"; @@ -20,8 +21,6 @@ import { Logout } from "@/screens/session/logout/Logout"; import { RegisterAdminForm } from "@/screens/session/register-admin/Form"; import { RegisterStudentForm } from "@/screens/session/register-student/Form"; import { RegisterTeacherForm } from "@/screens/session/register-teacher/Form"; -// Import fonts -import "@fontsource/ibm-plex-mono/400.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { Home } from "lucide-react"; @@ -31,6 +30,8 @@ import { Toaster } from "sonner"; // Apply global styles import "./global.css"; +import { EditStudentGradeView } from "./screens/edit-student-grade/EditStudentGradeView"; +import { LaboratoryGrades } from "./screens/laboratory-grades/LaboratoryGrades"; import { LaboratoryProgressView } from "./screens/laboratory-progress/LaboratoryProgressView"; // Define query client @@ -88,6 +89,14 @@ ReactDOM.createRoot(document.getElementById("root")!).render( } /> + + + + } + /> } /> + + + + } + /> + + + + } + /> { + const { submissionUUID } = testBlock; + + const joinedBlockName = testBlock.name.replace(/\s/g, "_").toLowerCase(); + downloadSubmissionArchive(submissionUUID!, joinedBlockName); + }; + const handleSubmit = async ( data: z.infer ) => { @@ -179,7 +189,8 @@ export const TestPreviewBlockForm = ({ {testBlock.submissionUUID && ( diff --git a/src/screens/complete-laboratory/components/test-block/TestStatus.tsx b/src/screens/complete-laboratory/components/test-block/TestStatus.tsx index f47e28c0..f2e2da5d 100644 --- a/src/screens/complete-laboratory/components/test-block/TestStatus.tsx +++ b/src/screens/complete-laboratory/components/test-block/TestStatus.tsx @@ -94,16 +94,20 @@ export const TestStatus = ({ }; return ( -
    +
      {status.map((phase, index) => { return ( - + className="rounded-md p-2 hover:bg-gray-100" + > + + ); })}
    diff --git a/src/screens/complete-laboratory/components/test-block/TestStatusPhase.tsx b/src/screens/complete-laboratory/components/test-block/TestStatusPhase.tsx index 7f406aa8..d0460581 100644 --- a/src/screens/complete-laboratory/components/test-block/TestStatusPhase.tsx +++ b/src/screens/complete-laboratory/components/test-block/TestStatusPhase.tsx @@ -27,7 +27,7 @@ export const TestStatusPhase = ({ const isRunningNow = currentPhase.submissionStatus === "running"; const wasRan = currentPhase.submissionStatus === "ready"; - const mayShowTestOutput = wasRan && phaseName === "ready"; + const shouldShowTestOutput = wasRan && phaseName === "ready"; const getPhaseIconColorClasses = (): string => { if (wasCurrentStatusPassed) { @@ -72,9 +72,13 @@ export const TestStatusPhase = ({ }; return ( -
  1. +
    {/* Status header */} -
    +
    @@ -82,20 +86,20 @@ export const TestStatusPhase = ({
    {phaseName}
    - {/* Status content */} - {mayShowTestOutput && ( + {/* Status output */} + {shouldShowTestOutput && (
    -
    +          
                 {currentPhase.testsOutput}
               
    )} -
  2. + ); }; diff --git a/src/screens/course-page/laboratories/components/CourseLaboratoriesTable.tsx b/src/screens/course-page/laboratories/components/CourseLaboratoriesTable.tsx index 51555af9..bc567a4f 100644 --- a/src/screens/course-page/laboratories/components/CourseLaboratoriesTable.tsx +++ b/src/screens/course-page/laboratories/components/CourseLaboratoriesTable.tsx @@ -12,7 +12,12 @@ import { AuthContext } from "@/context/AuthContext"; import { LaboratoryBaseInfo } from "@/types/entities/laboratory-entities"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { BarChartBigIcon, BookOpenCheck, Edit } from "lucide-react"; +import { + BarChartBigIcon, + BookOpenCheck, + Edit, + GraduationCapIcon +} from "lucide-react"; import { useContext } from "react"; import { Link, useParams } from "react-router-dom"; @@ -47,6 +52,13 @@ export const CourseLaboratoriesTable = ({ > Edit + + Grades + { onMutate: async (data: EnrolledStudent) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ - queryKey: ["course-students", courseUUID] + queryKey: ["course-students", courseUUID], + exact: true }); // Keep the current value diff --git a/src/screens/edit-laboratory/components/markdown-block/MarkdownBlockDropDown.tsx b/src/screens/edit-laboratory/components/markdown-block/MarkdownBlockDropDown.tsx index a94e25dd..c843c53c 100644 --- a/src/screens/edit-laboratory/components/markdown-block/MarkdownBlockDropDown.tsx +++ b/src/screens/edit-laboratory/components/markdown-block/MarkdownBlockDropDown.tsx @@ -1,6 +1,7 @@ import { EditLaboratoryContext } from "@/context/laboratories/EditLaboratoryContext"; import { EditLaboratoryActionType } from "@/hooks/laboratories/editLaboratoryTypes"; import { deleteMarkdownBlockService } from "@/services/blocks/delete-markdown-block.service"; +import { swapBlocksIndexService } from "@/services/blocks/swap-blocks-index.service"; import { updateMarkdownBlockContentService } from "@/services/blocks/update-markdown-block-content.service"; import { Laboratory, @@ -101,6 +102,84 @@ export const MarkdownBlockDropDown = ({ } }); + // Move markdown block up mutation + const { mutate: moteTestBlockUpMutation } = useMutation({ + mutationFn: swapBlocksIndexService, + onError: (error) => { + toast.error(error.message); + }, + onSuccess() { + const thisBlock = laboratory!.blocks[blockIndex]; + const prevBlock = laboratory!.blocks[blockIndex - 1]; + + // Update the global laboratory state + laboratoryStateDispatcher({ + type: EditLaboratoryActionType.SWAP_BLOCKS, + payload: { + uuid1: thisBlock.uuid, + uuid2: prevBlock.uuid + } + }); + + // Update the laboratory query + queryClient.setQueryData( + ["laboratory", laboratory!.uuid], + (oldData: Laboratory) => { + return { + ...oldData, + blocks: oldData.blocks.map((b) => { + if (b.uuid === thisBlock.uuid) return prevBlock; + if (b.uuid === prevBlock.uuid) return thisBlock; + return b; + }) + }; + } + ); + + // Show success message + toast.success("The markdown block has been moved up successfully"); + } + }); + + // Move markdown block down mutation + const { mutate: moteTestBlockDownMutation } = useMutation({ + mutationFn: swapBlocksIndexService, + onError: (error) => { + toast.error(error.message); + }, + onSuccess() { + const thisBlock = laboratory!.blocks[blockIndex]; + const nextBlock = laboratory!.blocks[blockIndex + 1]; + + // Update the global laboratory state + laboratoryStateDispatcher({ + type: EditLaboratoryActionType.SWAP_BLOCKS, + payload: { + uuid1: thisBlock.uuid, + uuid2: nextBlock.uuid + } + }); + + // Update the laboratory query + queryClient.setQueryData( + ["laboratory", laboratory!.uuid], + (oldData: Laboratory) => { + return { + ...oldData, + blocks: oldData.blocks.map((b) => { + if (b.uuid === thisBlock.uuid) return nextBlock; + if (b.uuid === nextBlock.uuid) return thisBlock; + return b; + }) + }; + } + ); + + // Show success message + toast.success("The markdown block has been moved down successfully"); + } + }); + return ( @@ -114,11 +193,27 @@ export const MarkdownBlockDropDown = ({ Block options - + + moteTestBlockUpMutation({ + first_block_uuid: laboratory!.blocks[blockIndex - 1].uuid, + second_block_uuid: blockUUID + }) + } + > Move up - + + moteTestBlockDownMutation({ + first_block_uuid: blockUUID, + second_block_uuid: laboratory!.blocks[blockIndex + 1].uuid + }) + } + > Move down diff --git a/src/screens/edit-laboratory/components/test-block/EditableTestBlockForm.tsx b/src/screens/edit-laboratory/components/test-block/EditableTestBlockForm.tsx index ab183c72..ccefc686 100644 --- a/src/screens/edit-laboratory/components/test-block/EditableTestBlockForm.tsx +++ b/src/screens/edit-laboratory/components/test-block/EditableTestBlockForm.tsx @@ -18,11 +18,12 @@ import { import { CONSTANTS } from "@/config/constants"; import { EditLaboratoryContext } from "@/context/laboratories/EditLaboratoryContext"; import { EditLaboratoryActionType } from "@/hooks/laboratories/editLaboratoryTypes"; +import { downloadTestsArchiveService } from "@/services/blocks/download-tests-archive.service"; import { updateTestBlockService } from "@/services/blocks/update-test-block.service"; import { getSupportedLanguagesService } from "@/services/languages/get-supported-languages.service"; import { useSupportedLanguagesStore } from "@/stores/supported-languages-store"; import { Laboratory, TestBlock } from "@/types/entities/laboratory-entities"; -import { downloadLanguageTemplate } from "@/utils/utils"; +import { downloadBlob, downloadLanguageTemplate } from "@/utils/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { DownloadIcon } from "lucide-react"; @@ -177,6 +178,26 @@ export const EditableTestBlockForm = ({ downloadLanguageTemplate(selectedLanguageUUID, selectedLanguageName); }; + const handleDownloadTestsArchive = async () => { + const { success, message, testsArchive } = + await downloadTestsArchiveService(testBlock.uuid); + + if (!success) { + toast.error(message); + return; + } + + const nameToDownload = `${testBlock.name.replace( + /\s/g, + "-" + )}-tests`.toLowerCase(); + + downloadBlob({ + file: testsArchive, + fileName: `${nameToDownload}.zip` + }); + }; + return (
    @@ -269,10 +290,12 @@ export const EditableTestBlockForm = ({ }} /> - {/* TODO: Download the current test archive from the server */} diff --git a/src/screens/edit-laboratory/components/test-block/TestBlockDropDown.tsx b/src/screens/edit-laboratory/components/test-block/TestBlockDropDown.tsx index 73597772..609da033 100644 --- a/src/screens/edit-laboratory/components/test-block/TestBlockDropDown.tsx +++ b/src/screens/edit-laboratory/components/test-block/TestBlockDropDown.tsx @@ -9,6 +9,7 @@ import { import { EditLaboratoryContext } from "@/context/laboratories/EditLaboratoryContext"; import { EditLaboratoryActionType } from "@/hooks/laboratories/editLaboratoryTypes"; import { deleteTestBlockService } from "@/services/blocks/delete-test-block.service"; +import { swapBlocksIndexService } from "@/services/blocks/swap-blocks-index.service"; import { Laboratory } from "@/types/entities/laboratory-entities"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { ArrowDown, ArrowUp, MoreVertical, Save, Trash2 } from "lucide-react"; @@ -72,6 +73,84 @@ export const TestBlockDropDown = ({ } }); + // Move test block up mutation + const { mutate: moteTestBlockUpMutation } = useMutation({ + mutationFn: swapBlocksIndexService, + onError: (error) => { + toast.error(error.message); + }, + onSuccess() { + const thisBlock = laboratory!.blocks[blockIndex]; + const prevBlock = laboratory!.blocks[blockIndex - 1]; + + // Update the global laboratory state + laboratoryStateDispatcher({ + type: EditLaboratoryActionType.SWAP_BLOCKS, + payload: { + uuid1: thisBlock.uuid, + uuid2: prevBlock.uuid + } + }); + + // Update the laboratory query + queryClient.setQueryData( + ["laboratory", laboratory!.uuid], + (oldData: Laboratory) => { + return { + ...oldData, + blocks: oldData.blocks.map((b) => { + if (b.uuid === thisBlock.uuid) return prevBlock; + if (b.uuid === prevBlock.uuid) return thisBlock; + return b; + }) + }; + } + ); + + // Show success message + toast.success("The test block has been moved up successfully"); + } + }); + + // Move test block down mutation + const { mutate: moteTestBlockDownMutation } = useMutation({ + mutationFn: swapBlocksIndexService, + onError: (error) => { + toast.error(error.message); + }, + onSuccess() { + const thisBlock = laboratory!.blocks[blockIndex]; + const nextBlock = laboratory!.blocks[blockIndex + 1]; + + // Update the global laboratory state + laboratoryStateDispatcher({ + type: EditLaboratoryActionType.SWAP_BLOCKS, + payload: { + uuid1: thisBlock.uuid, + uuid2: nextBlock.uuid + } + }); + + // Update the laboratory query + queryClient.setQueryData( + ["laboratory", laboratory!.uuid], + (oldData: Laboratory) => { + return { + ...oldData, + blocks: oldData.blocks.map((b) => { + if (b.uuid === thisBlock.uuid) return nextBlock; + if (b.uuid === nextBlock.uuid) return thisBlock; + return b; + }) + }; + } + ); + + // Show success message + toast.success("The test block has been moved down successfully"); + } + }); + return ( @@ -85,11 +164,27 @@ export const TestBlockDropDown = ({ Block options - + + moteTestBlockUpMutation({ + first_block_uuid: laboratory!.blocks[blockIndex - 1].uuid, + second_block_uuid: blockUUID + }) + } + > Move up - + + moteTestBlockDownMutation({ + first_block_uuid: blockUUID, + second_block_uuid: laboratory!.blocks[blockIndex + 1].uuid + }) + } + > Move down diff --git a/src/screens/edit-student-grade/EditStudentGradeView.tsx b/src/screens/edit-student-grade/EditStudentGradeView.tsx new file mode 100644 index 00000000..b69bfc06 --- /dev/null +++ b/src/screens/edit-student-grade/EditStudentGradeView.tsx @@ -0,0 +1,173 @@ +import { CustomError } from "@/components/CustomError"; +import { buttonVariants } from "@/components/ui/button"; +import { getGradeOfStudentInLaboratoryService } from "@/services/grades/get-grade-of-student-in-laboratory.service"; +import { getLaboratoryInformationByUUIDService } from "@/services/laboratories/get-laboratory-information-by-uuid.service"; +import { getRubricByUUIDService } from "@/services/rubrics/get-rubric-by-uuid.service"; +import { useQuery } from "@tanstack/react-query"; +import { ArrowLeftIcon } from "lucide-react"; +import { Link, useParams } from "react-router-dom"; +import { toast } from "sonner"; + +import { NoRubricChosen } from "./components/NoRubricChosen"; +import { GradingRubric } from "./components/grading-rubric/GradingRubric"; +import { GradingSidebar } from "./components/grading-sidebar/GradingSidebar"; + +const handleViewError = ( + error: Error, + { + redirectURL, + redirectText + }: { + redirectURL: string; + redirectText: string; + } +) => { + const { message } = error; + toast.error(message); + + return ( +
    + +
    + ); +}; + +export const EditStudentGradeView = () => { + // Get the UUIDs from the URL + const { courseUUID, laboratoryUUID, studentUUID } = useParams<{ + courseUUID: string; + laboratoryUUID: string; + studentUUID: string; + }>(); + + const { + data: laboratoryInformation, + isLoading: isLoadingLabInfo, + isError: isErrorLabInfo, + error: errorLabInfo + } = useQuery({ + queryKey: ["laboratory-information", laboratoryUUID], + queryFn: () => getLaboratoryInformationByUUIDService(laboratoryUUID!) + }); + + const laboratoryRubricUUID = laboratoryInformation?.rubric_uuid; + + const { + data: studentGrade, + isLoading: isLoadingStudentGrade, + isError: isErrorStudentGrade, + error: errorStudentGrade + } = useQuery({ + queryKey: ["student-grade", laboratoryUUID, studentUUID], + queryFn: () => + getGradeOfStudentInLaboratoryService({ + laboratoryUUID: laboratoryUUID!, + rubricUUID: laboratoryRubricUUID!, + studentUUID: studentUUID! + }), + // Fetch the grade after the laboratory information is fetched and only if the laboratory has a rubric + enabled: !!laboratoryRubricUUID + }); + + const { + data: rubric, + isLoading: isLoadingRubric, + isError: isErrorRubric, + error: errorRubric + } = useQuery({ + queryKey: ["rubric", laboratoryRubricUUID], + queryFn: () => getRubricByUUIDService(laboratoryRubricUUID!), + // Fetch the rubric after the laboratory information is fetched and only if the laboratory has a rubric + enabled: !!laboratoryRubricUUID + }); + + // Handle error state + if (isErrorLabInfo) { + return handleViewError(errorLabInfo, { + redirectURL: `/courses/${courseUUID}/laboratories/${laboratoryUUID}/grades`, + redirectText: "Go back to grades" + }); + } + + if (isErrorStudentGrade) { + return handleViewError(errorStudentGrade, { + redirectURL: `/courses/${courseUUID}/laboratories/${laboratoryUUID}/grades`, + redirectText: "Go back to grades" + }); + } + + if (isErrorRubric) { + return handleViewError(errorRubric, { + redirectURL: `/courses/${courseUUID}/laboratories/${laboratoryUUID}/grades`, + redirectText: "Go back to grades" + }); + } + + // If the rubric is not loading but is undefined, return the custom error component + if (!rubric) { + return ; + } + + // Handle loading state + const isLoading = + isLoadingLabInfo || isLoadingRubric || isLoadingStudentGrade; + + if (isLoading) return

    Loading...

    ; + + // If the student grade is not loading but is undefined, return an error + if (!studentGrade) { + return handleViewError( + new Error("We had an error loading the student grade"), + { + redirectURL: `/courses/${courseUUID}/laboratories/${laboratoryUUID}/grades`, + redirectText: "Go back to grades" + } + ); + } + + // Map the selected criteria to their objectives + const selectedCriteriaByObjectiveMap: Record = + studentGrade.selected_criteria.reduce( + (acc, criteria) => { + acc[criteria.objective_uuid] = criteria.criteria_uuid; + return acc; + }, + {} as Record + ); + + return ( +
    +
    + + Go back + +
    +
    +
    + +
    +
    + +
    +
    +
    + ); +}; diff --git a/src/screens/edit-student-grade/components/NoRubricChosen.tsx b/src/screens/edit-student-grade/components/NoRubricChosen.tsx new file mode 100644 index 00000000..de19b9be --- /dev/null +++ b/src/screens/edit-student-grade/components/NoRubricChosen.tsx @@ -0,0 +1,28 @@ +import { buttonVariants } from "@/components/ui/button"; +import { Link } from "react-router-dom"; + +export const NoRubricChosen = () => { + return ( +
    +
    +

    No rubric chosen

    +

    + In order to provide feedback to your students, you must create and / + or choose a rubric for this laboratory. +

    + + Create a rubric + +
    +
    + Blank canvas illustration +
    +
    + ); +}; diff --git a/src/screens/edit-student-grade/components/grading-rubric/GradingRubric.tsx b/src/screens/edit-student-grade/components/grading-rubric/GradingRubric.tsx new file mode 100644 index 00000000..426b41ac --- /dev/null +++ b/src/screens/edit-student-grade/components/grading-rubric/GradingRubric.tsx @@ -0,0 +1,41 @@ +import { Rubric } from "@/types/entities/rubric-entities"; + +import { GradingRubricRow } from "./GradingRubricObjectiveRow"; + +interface SelectableRubricProps { + selectedCriteriaByObjective: Record; + laboratoryUUID: string; + studentUUID: string; + isLoading: boolean; + rubric: Rubric; +} + +export const GradingRubric = ({ + selectedCriteriaByObjective, + laboratoryUUID, + studentUUID, + isLoading, + rubric +}: SelectableRubricProps) => { + // TODO: Use a proper loading component + if (isLoading) { + return
    Loading rubric...
    ; + } + + return ( +
    + {rubric.objectives.map((objective, index) => ( + + ))} +
    + ); +}; diff --git a/src/screens/edit-student-grade/components/grading-rubric/GradingRubricCriteriaCard.tsx b/src/screens/edit-student-grade/components/grading-rubric/GradingRubricCriteriaCard.tsx new file mode 100644 index 00000000..b205efeb --- /dev/null +++ b/src/screens/edit-student-grade/components/grading-rubric/GradingRubricCriteriaCard.tsx @@ -0,0 +1,257 @@ +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + selectedCriteriaInGrade, + studentGradeResponse +} from "@/services/grades/get-grade-of-student-in-laboratory.service"; +import { selectCriteriaToGradeService } from "@/services/grades/select-criteria-to-grade.service"; +import { Criteria } from "@/types/entities/rubric-entities"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useId } from "react"; +import { toast } from "sonner"; + +interface gradingRubricCriteriaRequiredUUID { + laboratoryUUID: string; + objectiveUUID: string; + studentUUID: string; +} + +interface gradingRubricCriteriaCardProps { + objectiveCriteriaList: Criteria[]; + uuids: gradingRubricCriteriaRequiredUUID; + criteriaIndex: number; + objectiveIndex: number; + isSelected?: boolean; +} + +export const GradingRubricCriteriaCard = ({ + objectiveCriteriaList, + uuids: { laboratoryUUID, objectiveUUID, studentUUID }, + criteriaIndex, + objectiveIndex, + isSelected = false +}: gradingRubricCriteriaCardProps) => { + const criteria = objectiveCriteriaList[criteriaIndex]; + + const criteriaWeightId = useId(); + const criteriaDescriptionId = useId(); + + // Select criteria mutation + const queryClient = useQueryClient(); + const { mutate: selectCriteriaMutation } = useMutation({ + mutationFn: selectCriteriaToGradeService, + // Optimistic update + onMutate: async (args) => { + // Cancel outgoing re-fetches + await queryClient.cancelQueries({ + queryKey: ["student-grade", laboratoryUUID, studentUUID], + exact: true + }); + + // Get the current value + const previousGrade = queryClient.getQueryData([ + "student-grade", + laboratoryUUID, + studentUUID + ]); + + // Optimistically update the value + if (previousGrade) { + const { selected_criteria: currentSelectedCriteriaList } = + previousGrade; + + // Check if the objective the criteria belongs to is already in the list + const includesCriteriaObjective = currentSelectedCriteriaList.some( + (c) => c.objective_uuid === objectiveUUID + ); + + let updatedSelectedCriteriaList: selectedCriteriaInGrade[]; + let weightToSubtractFromStudentGrade = 0; + + if (includesCriteriaObjective) { + // Find the weight of the criteria that is currently selected + const currentSelectedCriteriaUUID = currentSelectedCriteriaList.find( + (c) => c.objective_uuid === objectiveUUID + )!.criteria_uuid; + + weightToSubtractFromStudentGrade = + objectiveCriteriaList.find( + (c) => c.uuid === currentSelectedCriteriaUUID + )?.weight || 0; + + // Update the UUID of the criteria that is currently selected for the objective + updatedSelectedCriteriaList = currentSelectedCriteriaList.map((c) => { + const isCurrentCriteria = c.objective_uuid === objectiveUUID; + + if (!isCurrentCriteria) return c; + + return { + ...c, + criteria_uuid: args.criteria_uuid + }; + }); + } else { + // Add the criteria to the list + updatedSelectedCriteriaList = [ + ...currentSelectedCriteriaList, + { + objective_uuid: objectiveUUID, + criteria_uuid: args.criteria_uuid + } as selectedCriteriaInGrade + ]; + } + + queryClient.setQueryData( + ["student-grade", laboratoryUUID, studentUUID], + { + // Keep the grade comment + ...previousGrade, + // Update the grade of the student + grade: + previousGrade.grade - + weightToSubtractFromStudentGrade + + criteria.weight, + // Update the selected criteria list + selected_criteria: updatedSelectedCriteriaList + } + ); + } + + // Pass the previous state to the next callbacks + return { previousGrade }; + }, + onError: (error, _, context) => { + // Rollback to the previous value + if (context?.previousGrade) { + queryClient.setQueryData( + ["student-grade", laboratoryUUID, studentUUID], + context.previousGrade + ); + } + + // Show an error toast + toast.error(error.message); + }, + onSuccess: () => { + // Show a success toast + toast.success("Criteria has been selected"); + } + }); + + // De-select criteria mutation + const { mutate: deSelectCriteriaMutation } = useMutation({ + mutationFn: selectCriteriaToGradeService, + // Optimistic update + onMutate: async (_) => { + // Cancel outgoing re-fetches + await queryClient.cancelQueries({ + queryKey: ["student-grade", laboratoryUUID, studentUUID], + exact: true + }); + + // Get the current value + const previousGrade = queryClient.getQueryData([ + "student-grade", + laboratoryUUID, + studentUUID + ]); + + // Optimistically update the value + if (previousGrade) { + const { selected_criteria: currentSelectedCriteriaList } = + previousGrade; + + const updatedSelectedCriteriaList = currentSelectedCriteriaList.filter( + (c) => c.objective_uuid !== objectiveUUID + ); + + queryClient.setQueryData( + ["student-grade", laboratoryUUID, studentUUID], + { + // Keep the grade comment + ...previousGrade, + // Update the grade of the student + grade: previousGrade.grade - criteria.weight, + // Update the selected criteria list + selected_criteria: updatedSelectedCriteriaList + } + ); + } + + // Pass the previous state to the next callbacks + return { previousGrade }; + }, + onError: (error, _, context) => { + // Rollback to the previous value + if (context?.previousGrade) { + queryClient.setQueryData( + ["student-grade", laboratoryUUID, studentUUID], + context.previousGrade + ); + } + + // Show an error toast + toast.error(error.message); + }, + onSuccess: () => { + // Show a success toast + toast.success("Criteria has been de-selected"); + } + }); + + // Handlers + const handleCriteriaCardClick = () => { + if (isSelected) { + deSelectCriteriaMutation({ + // Set the criteria UUID to null to de-select it + criteria_uuid: null, + laboratoryUUID, + objective_uuid: objectiveUUID, + studentUUID + }); + } else { + selectCriteriaMutation({ + criteria_uuid: criteria.uuid, + laboratoryUUID, + objective_uuid: objectiveUUID, + studentUUID + }); + } + }; + + return ( +
    +

    Criteria {criteriaIndex + 1}

    +
    + + +
    +
    + +