From e60d07bfda32c44027f0a89617c193b293aa6431 Mon Sep 17 00:00:00 2001 From: Henrik Nygren Date: Fri, 6 Mar 2026 08:32:40 +0200 Subject: [PATCH 1/2] Refactor ExamPageShell to replace useHydrateAtoms with useEffect for setting organizationSlug and viewParams. This change improves state management and simplifies the component's logic. --- .../(course-material)/exams/ExamPageShell.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/services/main-frontend/src/app/org/[organizationSlug]/(course-material)/exams/ExamPageShell.tsx b/services/main-frontend/src/app/org/[organizationSlug]/(course-material)/exams/ExamPageShell.tsx index 3e5e4cabe97..93e81416e02 100644 --- a/services/main-frontend/src/app/org/[organizationSlug]/(course-material)/exams/ExamPageShell.tsx +++ b/services/main-frontend/src/app/org/[organizationSlug]/(course-material)/exams/ExamPageShell.tsx @@ -3,7 +3,6 @@ import { css } from "@emotion/css" import { isPast } from "date-fns" import { useAtomValue, useSetAtom } from "jotai" -import { useHydrateAtoms } from "jotai/utils" import React, { useCallback, useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" @@ -66,16 +65,13 @@ export default function ExamPageShell({ [examId, mode], ) - useHydrateAtoms( - useMemo( - () => - [ - [organizationSlugAtom, organizationSlug], - [viewParamsAtom, viewParams], - ] as const, - [organizationSlug, viewParams], - ), - ) + const setOrganizationSlug = useSetAtom(organizationSlugAtom) + const setViewParams = useSetAtom(viewParamsAtom) + + useEffect(() => { + setOrganizationSlug(organizationSlug) + setViewParams(viewParams) + }, [organizationSlug, viewParams, setOrganizationSlug, setViewParams]) const courseMaterialState = useAtomValue(courseMaterialAtom) const triggerRefetch = useSetAtom(refetchViewAtom) From f7ff1af3d5600434e4f4f42aada498253a889d75 Mon Sep 17 00:00:00 2001 From: Henrik Nygren Date: Fri, 6 Mar 2026 09:10:19 +0200 Subject: [PATCH 2/2] Fix race conditions when loading exercises --- .../PeerOrSelfReviewViewImpl.tsx | 18 +++++++++++++++- .../moocfi/ExerciseBlock/index.tsx | 21 ++++++++++++++++++- .../common/src/locales/en/main-frontend.json | 2 ++ .../common/src/locales/en/shared-module.json | 14 ++++++------- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/services/main-frontend/src/components/course-material/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewViewImpl.tsx b/services/main-frontend/src/components/course-material/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewViewImpl.tsx index 838004b1a2c..5c70dcc16b2 100644 --- a/services/main-frontend/src/components/course-material/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewViewImpl.tsx +++ b/services/main-frontend/src/components/course-material/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewViewImpl.tsx @@ -192,10 +192,26 @@ const PeerOrSelfReviewViewImpl: React.FC } + if (!query.data) { + return ( +
+ + +
+ ) + } + if (!peerOrSelfReviewData?.answer_to_review?.course_material_exercise_tasks) { return (
diff --git a/services/main-frontend/src/components/course-material/ContentRenderer/moocfi/ExerciseBlock/index.tsx b/services/main-frontend/src/components/course-material/ContentRenderer/moocfi/ExerciseBlock/index.tsx index fd84f138716..f9b2294f59d 100644 --- a/services/main-frontend/src/components/course-material/ContentRenderer/moocfi/ExerciseBlock/index.tsx +++ b/services/main-frontend/src/components/course-material/ContentRenderer/moocfi/ExerciseBlock/index.tsx @@ -161,6 +161,7 @@ const ExerciseBlock: React.FC< const courseMaterialState = useAtomValue(courseMaterialAtom) const showExercise = Boolean(courseMaterialState.examData?.id) || + courseMaterialState.status === "loading" || (loginState.signedIn ? !!courseMaterialState.settings : true) const [postThisStateToIFrame, dispatch] = useReducer( exerciseBlockPostThisStateToIFrameReducer, @@ -301,9 +302,27 @@ const ExerciseBlock: React.FC< if (getCourseMaterialExercise.isError) { return } - if (getCourseMaterialExercise.isLoading || !getCourseMaterialExercise.data) { + if (getCourseMaterialExercise.isLoading) { return } + if (!getCourseMaterialExercise.data) { + return ( +
+ + +
+ ) + } const courseInstanceId = courseMaterialState.instance?.id const chapterStatus = chapterId diff --git a/shared-module/packages/common/src/locales/en/main-frontend.json b/shared-module/packages/common/src/locales/en/main-frontend.json index 5cd97c4bee0..ba60f3d708c 100644 --- a/shared-module/packages/common/src/locales/en/main-frontend.json +++ b/shared-module/packages/common/src/locales/en/main-frontend.json @@ -148,6 +148,7 @@ "button-text-signed-in": "Signed in", "button-text-submit": "Submit", "button-text-submit-and-reset": "Submit and reset", + "button-text-try-again": "Try again", "button-text-update": "Update", "button-text-upload-image": "Upload image", "button-text-verify": "Verify", @@ -426,6 +427,7 @@ "error-field-value-between": "{{field}} should be a value between {{lower}} and {{upper}}.", "error-hide-details": "Hide details", "error-issue": "Issue: {{issue}}", + "error-loading-exercise": "Error loading exercise.", "error-loading-organizations": "Error loading organizations.", "error-message": "Message: {{message}}", "error-min-length": "{{field}} must be at least {{count}} characters.", diff --git a/shared-module/packages/common/src/locales/en/shared-module.json b/shared-module/packages/common/src/locales/en/shared-module.json index 5aaff76b76a..fb734772875 100644 --- a/shared-module/packages/common/src/locales/en/shared-module.json +++ b/shared-module/packages/common/src/locales/en/shared-module.json @@ -35,23 +35,23 @@ "dialog-title-prompt": "Enter value", "download-csv": "Download CSV", "dropdown-menu": "Dropdown menu", - "dynamic-loading-slow-warning": "Loading a part of the application is taking longer than expected.", - "dynamic-loading-very-slow-warning": "This may be due to network issues. If loading does not finish soon, please reload the page.", - "dynamic-loading-possible-causes-title": "If this keeps spinning, possible causes include:", "dynamic-loading-cause-client-boundary": "A Server/Client boundary issue (for example, a missing \"use client\" on a parent client component).", "dynamic-loading-cause-export-mismatch": "The dynamically imported module doesn’t export what the loader expects (default vs named export mismatch).", + "dynamic-loading-cause-hydration-script": "The chunk downloaded but the client runtime did not finish running or hydrating (for example due to blocked scripts, extensions, or a strict Content Security Policy).", "dynamic-loading-cause-runtime-error": "The component throws during initialization or mount (sometimes only visible in production logs).", "dynamic-loading-cause-suspense-forever": "The component suspends forever (waiting on a promise or data fetch that never resolves), so the fallback stays on screen.", - "dynamic-loading-cause-hydration-script": "The chunk downloaded but the client runtime did not finish running or hydrating (for example due to blocked scripts, extensions, or a strict Content Security Policy).", - "dynamic-loading-fallback-title": "We were unable to load this part of the page.", - "dynamic-loading-fallback-reason": "Reason: {{reason}}", "dynamic-loading-detected-import-rejected": "We tried to load this part of the app, but the import failed.", "dynamic-loading-detected-invalid-export": "We loaded the module, but it did not export a usable React component.", - "dynamic-loading-detected-resolved-no-commit": "We loaded the code, but the component has not finished rendering yet.", "dynamic-loading-detected-render-error": "The component code loaded, but an error occurred while rendering it.", + "dynamic-loading-detected-resolved-no-commit": "We loaded the code, but the component has not finished rendering yet.", "dynamic-loading-detected-resolved-no-commit-details": "Import resolved; waiting for first React commit (mount).", + "dynamic-loading-fallback-reason": "Reason: {{reason}}", + "dynamic-loading-fallback-title": "We were unable to load this part of the page.", + "dynamic-loading-possible-causes-title": "If this keeps spinning, possible causes include:", "dynamic-loading-reload": "Reload page", "dynamic-loading-retrying": "Retrying to load this part of the app ({{attempt}}/{{max}})…", + "dynamic-loading-slow-warning": "Loading a part of the application is taking longer than expected.", + "dynamic-loading-very-slow-warning": "This may be due to network issues. If loading does not finish soon, please reload the page.", "editable": "Editable", "email": "Email", "email-templates": "Email templates",