Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: implement functionalities for live quiz interaction besides question answering workflow #4352

Merged
merged 12 commits into from
Nov 4, 2024

Conversation

sjschlapbach
Copy link
Member

@sjschlapbach sjschlapbach commented Nov 4, 2024

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a new FeedbackAreaSubscriber component for real-time feedback updates during quizzes.
    • Added changeLiveQuizSettings mutation to adjust live quiz settings.
    • Implemented CancelLiveQuiz mutation for canceling live quizzes.
    • Added GetCourseRunningLiveQuizzes query to retrieve live quizzes associated with a specific course.
  • Improvements

    • Updated multiple components to use quizId instead of sessionId, enhancing consistency across the application.
    • Enhanced type safety in various components and hooks by specifying more precise types for props.
    • Refactored LiveQuizElement and LiveQuizList components to focus on live quizzes instead of sessions.
    • Improved loading state management in CourseLiveQuizzes component for better user experience.
  • Localization

    • Updated terminology in German and English localization files to reflect "Active Live Quizzes" instead of "Active Sessions".

Copy link

coderabbitai bot commented Nov 4, 2024

📝 Walkthrough

Walkthrough

This pull request implements extensive changes across various components and services, primarily transitioning from session-based logic to quiz-based logic. Key modifications include renaming identifiers from sessionId to quizId, updating GraphQL mutations and queries, and restructuring components to improve clarity and maintainability. New components and hooks are introduced, such as FeedbackAreaSubscriber and useStudentOverviewSplit, to enhance feedback handling and data processing. The schema is also updated to reflect these changes, ensuring consistency across the application.

Changes

File Change Summary
apps/frontend-manage/src/components/interaction/AudienceInteraction.tsx Updated mutation operations from ChangeSessionSettingsDocument to ChangeLiveQuizSettingsDocument, renamed mutation hook from changeSessionSettings to changeQuizSettings, and changed variable names from sessionId to quizId.
apps/frontend-manage/src/components/interaction/feedbacks/FeedbackChannel.tsx Adjusted rendering and styling of feedback items, simplified FontAwesomeIcon rendering, and maintained existing feedback handling logic.
apps/frontend-pwa/src/components/groupActivity/GroupActivityListSubscriber.tsx Changed subscribeToMore prop type from any to (doc: SubscribeToMoreOptions) => any for better type safety.
apps/frontend-pwa/src/components/groupActivity/GroupActivitySubscriber.tsx Updated subscribeToMore prop type from any to (doc: SubscribeToMoreOptions) => any.
apps/frontend-pwa/src/components/liveSession/FeedbackArea.tsx Removed Subscriber component, restructured props, and updated to use quizId instead of sessionId.
apps/frontend-pwa/src/components/liveSession/FeedbackAreaSubscriber.tsx Introduced a new component for managing real-time feedback updates using subscriptions.
apps/frontend-pwa/src/components/liveSession/PublicFeedback.tsx Updated function signatures to use async/await syntax and removed FeedbackProps interface.
apps/frontend-pwa/src/components/microLearning/MicroLearningListSubscriber.tsx Changed subscribeToMore prop type from any to (doc: SubscribeToMoreOptions) => any.
apps/frontend-pwa/src/components/microLearning/MicroLearningSubscriber.tsx Updated subscribeToMore prop type from any to (doc: SubscribeToMoreOptions) => any.
apps/frontend-pwa/src/lib/hooks/useStudentOverviewSplit.tsx Introduced a custom hook for processing student participations data.
apps/frontend-pwa/src/pages/index.tsx Refactored logic for processing participations using the new useStudentOverviewSplit hook and removed local type definitions.
apps/frontend-pwa/src/pages/session/[id].tsx Updated document imports and changed variable names from sessionId to quizId.
packages/graphql/src/graphql/ops/MAddConfusionTimestep.graphql Changed parameter from $sessionId to $quizId.
packages/graphql/src/graphql/ops/MChangeLiveQuizSettings.graphql Renamed mutation from ChangeSessionSettings to ChangeLiveQuizSettings.
packages/graphql/src/graphql/ops/MCreateFeedback.graphql Changed parameter from sessionId to quizId.
packages/graphql/src/graphql/ops/QGetFeedbacks.graphql Updated parameter from $sessionId to $quizId.
packages/graphql/src/graphql/ops/QParticipations.graphql Replaced sessions field with liveQuizzes in the query.
packages/graphql/src/graphql/ops/SFeedbackAdded.graphql Changed parameter from $sessionId to $quizId.
packages/graphql/src/graphql/ops/SFeedbackCreated.graphql Changed parameter from $sessionId to $quizId.
packages/graphql/src/graphql/ops/SFeedbackRemoved.graphql Changed parameter from $sessionId to $quizId.
packages/graphql/src/graphql/ops/SFeedbackUpdated.graphql Changed parameter from $sessionId to $quizId.
packages/graphql/src/graphql/ops/SRunningSessionUpdated.graphql Renamed subscription from RunningSessionUpdated to RunningLiveQuizUpdated and changed parameter from $sessionId to $quizId.
packages/graphql/src/ops.schema.json Added liveQuizzes field to Course object type and updated mutation arguments.
packages/graphql/src/public/client.json Updated hash values for various keys related to feedback and quiz operations.
packages/graphql/src/public/schema.graphql Updated mutation and query signatures to reflect changes from sessionId to quizId.
packages/graphql/src/public/server.json Updated mutation signatures to replace sessionId with quizId and added new mutation ChangeLiveQuizSettings.
packages/graphql/src/schema/course.ts Added liveQuizzes field to ICourse interface.
packages/graphql/src/schema/liveQuiz.ts Changed ILiveQuiz interface visibility from private to public.
packages/graphql/src/schema/mutation.ts Updated mutation signatures to replace sessionId with quizId and renamed changeSessionSettings to changeLiveQuizSettings.
packages/graphql/src/schema/query.ts Updated feedbacks field argument from id to quizId.
packages/graphql/src/schema/subscription.ts Renamed subscription fields and updated arguments from sessionId to quizId.
packages/graphql/src/services/feedbacks.ts Updated functions to transition from session-based to quiz-based logic.
packages/graphql/src/services/liveQuizzes.ts Introduced new functions for managing live quizzes and updated existing functions to align with new logic.
packages/graphql/src/services/participants.ts Updated logic in getParticipations to filter based on liveQuizzes.
packages/graphql/src/services/sessions.ts Removed changeSessionSettings function and its associated interface.
packages/i18n/messages/de.ts Updated localization string from "Aktive Sessions" to "Aktive Live Quizzes".
packages/i18n/messages/en.ts Updated localization key from activeSessions to activeLiveQuizzes.
apps/frontend-manage/src/components/courses/CourseOverviewHeader.tsx Updated CourseOverviewHeaderProps to remove sessions and add liveQuizzes.
apps/frontend-manage/src/components/courses/LiveQuizElement.tsx Transitioned from Session type to LiveQuiz type, updating props and internal logic accordingly.
apps/frontend-manage/src/components/courses/LiveQuizList.tsx Updated component to focus on liveQuizzes, removing dependency on Session type.
apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx Updated liveQuiz prop type to `Pick<LiveQuiz, 'id'
apps/frontend-manage/src/components/courses/actions/RunningLiveQuizLink.tsx Updated liveQuiz prop type to `Pick<LiveQuiz, 'id'
apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx Updated liveQuiz prop type to `Pick<LiveQuiz, 'id'
apps/frontend-manage/src/components/sessions/cockpit/CancelLiveQuizModal.tsx Refactored to use CancelLiveQuizDocument and updated mutation logic.
apps/frontend-manage/src/components/sessions/cockpit/SessionAbortionConfirmations.tsx Updated import statement to reflect the new context of live quizzes.
apps/frontend-manage/src/components/sessions/cockpit/SessionTimeline.tsx Renamed state variable and modal component to align with live quiz context.
apps/frontend-manage/src/pages/courses/[id].tsx Updated default tab value from 'liveSessions' to 'liveQuizzes'.
packages/graphql/src/graphql/ops/MCancelLiveQuiz.graphql Added new mutation CancelLiveQuiz.
packages/graphql/src/graphql/ops/MCancelSession.graphql Removed the mutation CancelSession.
packages/graphql/src/graphql/ops/QGetSingleCourse.graphql Renamed sessions field to liveQuizzes and added new fields related to live quizzes.
packages/graphql/src/services/courses.ts Updated functions to transition from session-based to quiz-based logic, renaming properties accordingly.

Possibly related PRs

Suggested reviewers

  • rschlaefli

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@sjschlapbach sjschlapbach marked this pull request as ready for review November 4, 2024 14:30
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 19

🧹 Outside diff range and nitpick comments (44)
packages/graphql/src/graphql/ops/SRunningSessionUpdated.graphql (1)

1-13: Consider adding subscription documentation.

To improve maintainability, consider adding a documentation comment describing:

  • The purpose of this subscription
  • When it triggers
  • The meaning of each field

Add this documentation above the subscription:

+# Subscribes to updates of a running live quiz
+# Triggers when:
+# - Quiz status changes
+# - Time limit is updated
+# - Elements are modified
+# - Execution parameters change
 subscription RunningLiveQuizUpdated($quizId: String!) {
apps/frontend-pwa/src/components/microLearning/MicroLearningSubscriber.tsx (1)

Line range hint 18-48: Consider adding explicit subscription cleanup

While the current implementation works, it would be better to explicitly clean up the subscription when the component unmounts.

Here's how you could improve it:

 useEffect(() => {
-    subscribeToMore({
+    const unsubscribe = subscribeToMore({
       document: MicroLearningEndedDocument,
       variables: { activityId },
       updateQuery: (
         prev: { microLearning: MicroLearning },
         {
           subscriptionData,
         }: {
           subscriptionData: {
             data: { microLearningEnded: MicroLearning }
           }
         }
       ): { microLearning: MicroLearning } => {
         if (!subscriptionData.data) return prev

         // trigger toast for ended microlearning
         setEndedMicroLearning(true)

         // update the values returned by the course overview data query
         const updatedMicroLearning = {
           ...prev.microLearning,
           ...subscriptionData.data.microLearningEnded,
         }

         return { microLearning: updatedMicroLearning }
       },
     })
+    return () => {
+      if (typeof unsubscribe === 'function') {
+        unsubscribe()
+      }
+    }
   }, [activityId, subscribeToMore])
apps/frontend-pwa/src/components/groupActivity/GroupActivitySubscriber.tsx (2)

11-11: Consider further improving type safety.

While changing from any to (doc: SubscribeToMoreOptions) => any is a good improvement, we could make it even more type-safe by specifying the return type.

-  subscribeToMore: (doc: SubscribeToMoreOptions) => any
+  subscribeToMore: (doc: SubscribeToMoreOptions) => () => void

This explicitly shows that the function returns an unsubscribe callback.


Line range hint 20-45: Consider adding error handling for the subscription.

The subscription setup should handle potential errors to ensure robustness in the live quiz interaction workflow.

   useEffect(() => {
     subscribeToMore({
       document: SingleGroupActivityEndedDocument,
       variables: { activityId },
+      onError: (error) => {
+        console.error('Error in group activity subscription:', error)
+        // Consider adding error handling logic or notification
+      },
       updateQuery: (
         prev: { groupActivityDetails: GroupActivityDetails },
         {
apps/frontend-pwa/src/components/microLearning/MicroLearningListSubscriber.tsx (3)

Line range hint 1-13: Consider further improving type safety of subscribeToMore.

While the change from any to a specific function type is a good improvement, we can make it even more type-safe by specifying the return type.

Consider updating the type to:

-  subscribeToMore: (doc: SubscribeToMoreOptions) => any
+  subscribeToMore: (doc: SubscribeToMoreOptions) => () => void

This explicitly shows that the function returns an unsubscribe function, which is the actual return type from Apollo's subscribeToMore.


Line range hint 20-21: Add cleanup function to prevent memory leaks.

The useEffect hook should return a cleanup function to unsubscribe when the component unmounts.

Update the effect to include cleanup:

 useEffect(() => {
-  subscribeToMore({
+  const unsubscribe = subscribeToMore({
     document: MicroLearningEndedDocument,
     // ... rest of the subscription config
   })
+  return () => {
+    if (unsubscribe) unsubscribe()
+  }
 }, [activityId, subscribeToMore])

Line range hint 33-65: Simplify the participation update logic.

The nested mapping and filtering logic is complex and could be simplified for better readability and maintainability.

Consider refactoring the update logic:

-        const updatedParticipations: Participation[] = prev.participations.map(
-          (participation) => {
-            const microLearningIds = participation.course?.microLearnings?.map(
-              (ml) => ml.id
-            )
-            if (microLearningIds?.includes(activityId)) {
-              return {
-                ...participation,
-                course: {
-                  ...participation.course,
-                  microLearnings:
-                    participation.course?.microLearnings
-                      ?.map((ml) => {
-                        if (ml.id === activityId) {
-                          return undefined
-                        }
-                        return ml
-                      })
-                      .filter((ml) => typeof ml !== 'undefined') ?? [],
-                } as Course,
-              }
-            }
-            return participation
-          }
-        )
+        const updatedParticipations = prev.participations.map((participation) => {
+          if (!participation.course?.microLearnings?.some(ml => ml.id === activityId)) {
+            return participation
+          }
+          
+          return {
+            ...participation,
+            course: {
+              ...participation.course,
+              microLearnings: participation.course.microLearnings?.filter(
+                (ml) => ml.id !== activityId
+              ) ?? [],
+            } as Course,
+          }
+        })

This refactoring:

  1. Simplifies the logic by using some for existence check
  2. Directly filters out the ended micro-learning
  3. Maintains the same functionality with improved readability
apps/frontend-pwa/src/components/groupActivity/GroupActivityListSubscriber.tsx (3)

1-1: Consider further improving type safety.

While changing subscribeToMore from any to a typed function is a good improvement, we could make it even more type-safe by:

  1. Specifying the return type instead of using any
  2. Making it generic to handle different subscription data types

Consider this improvement:

-  subscribeToMore: (doc: SubscribeToMoreOptions) => any
+  subscribeToMore: <TSubscriptionData = any>(
+    doc: SubscribeToMoreOptions<any, any, TSubscriptionData>
+  ) => () => void

Also applies to: 11-11


Line range hint 22-45: Add duplicate protection to the "ended" subscription.

The "started" subscription has protection against duplicate triggers, but the "ended" subscription doesn't. This could lead to unnecessary updates and UI flickers.

Add similar protection:

       if (!subscriptionData.data) return prev

+      // Check for duplicate updates
+      const updatedActivity = subscriptionData.data.groupActivityEnded
+      if (
+        prev.groupActivities.some(
+          (activity) =>
+            activity.id === updatedActivity.id &&
+            activity.endedAt === updatedActivity.endedAt
+        )
+      ) {
+        return prev
+      }
+
       // trigger toast for ended group activity
-      const updatedActivity = subscriptionData.data.groupActivityEnded
       setEndedGroupActivity(updatedActivity.displayName)

Line range hint 15-89: Consider adding error handling and loading states.

The component would benefit from proper error handling for subscription failures and exposing loading states.

Consider these improvements:

  1. Add error handling:
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
  try {
    const unsubscribe1 = subscribeToMore({
      // ... existing config
      onError: (error) => {
        setError(error);
        console.error('Subscription error:', error);
      },
    });

    const unsubscribe2 = subscribeToMore({
      // ... existing config
      onError: (error) => {
        setError(error);
        console.error('Subscription error:', error);
      },
    });

    return () => {
      unsubscribe1();
      unsubscribe2();
    };
  } catch (error) {
    setError(error as Error);
    console.error('Subscription setup error:', error);
  }
}, [courseId, subscribeToMore]);
  1. Expose error state:
if (error) {
  return <div>Error: {error.message}</div>;
}
apps/frontend-pwa/src/components/liveSession/FeedbackAreaSubscriber.tsx (4)

10-16: Add JSDoc documentation and improve type safety.

Consider adding JSDoc documentation for the component and making the return type of subscribeToMore more specific than any.

+/**
+ * Subscribes to real-time feedback updates for a quiz
+ * @param {Object} props - Component props
+ * @param {string} props.quizId - Unique identifier of the quiz
+ * @param {Function} props.subscribeToMore - Apollo Client's subscribeToMore function
+ */
 function FeedbackAreaSubscriber({
   subscribeToMore,
   quizId,
 }: {
   quizId: string
-  subscribeToMore: (doc: SubscribeToMoreOptions) => any
+  subscribeToMore: (doc: SubscribeToMoreOptions) => () => void
 })

57-76: LGTM with a minor suggestion.

The update logic is correct. Consider using immutability helpers like Immer for more readable state updates.

+import produce from 'immer'

 updateQuery: (
   prev: { feedbacks: Feedback[] },
   {
     subscriptionData,
   }: { subscriptionData: { data: { feedbackUpdated: Feedback } } }
 ) => {
   if (!subscriptionData.data) return prev
   const updatedItem = subscriptionData.data.feedbackUpdated
-  return {
-    ...prev,
-    feedbacks: prev.feedbacks?.map((item) => {
-      if (item.id === updatedItem.id) return updatedItem
-      return item
-    }),
-  }
+  return produce(prev, (draft) => {
+    const index = draft.feedbacks.findIndex((item) => item.id === updatedItem.id)
+    if (index !== -1) draft.feedbacks[index] = updatedItem
+  })
 }

78-83: Use optional chaining for cleaner cleanup.

The static analysis tool correctly suggests using optional chaining for a more concise cleanup.

 return () => {
-  feedbackAdded && feedbackAdded()
-  feedbackRemoved && feedbackRemoved()
-  feedbackUpdated && feedbackUpdated()
+  feedbackAdded?.()
+  feedbackRemoved?.()
+  feedbackUpdated?.()
 }
🧰 Tools
🪛 Biome

[error] 79-79: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 80-80: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 81-81: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


85-88: Return null instead of an empty div.

Since this is a subscription-only component that doesn't render anything, return null instead of an empty div to avoid creating unnecessary DOM nodes.

-  return <div></div>
+  return null
packages/graphql/src/schema/subscription.ts (1)

39-50: Consider adding JSDoc comments for the feedback subscriptions

The changes look good, but adding documentation would help clarify the purpose and usage of these subscriptions, especially given the architectural changes.

+/**
+ * Subscription for newly created feedback in a live quiz.
+ * @param quizId - The ID of the live quiz to subscribe to
+ * @returns The newly created feedback
+ */
 feedbackCreated: t.field({
apps/frontend-manage/src/components/interaction/feedbacks/FeedbackChannel.tsx (3)

110-112: Consider consolidating spacing approach.

While the flex layout classes improve alignment, consider using either gap-based or margin-based spacing consistently across similar components for better maintainability.

-    className={{
-      root: 'flex h-9 w-9 items-center justify-center',
-    }}
+    className={{
+      root: 'flex h-9 w-9 items-center justify-center gap-2',
+    }}

119-121: Consider adding ARIA labels for accessibility.

The icon implementation is cleaner, but consider adding aria-labels to improve accessibility for screen readers.

-      <FontAwesomeIcon icon={faEye} />
+      <FontAwesomeIcon icon={faEye} aria-label="Make feedback visible" />
-      <FontAwesomeIcon icon={faEyeSlash} />
+      <FontAwesomeIcon icon={faEyeSlash} aria-label="Hide feedback" />

Line range hint 1-1: Address or remove TODO comments.

There are several TODO comments in the file that should be addressed or removed before merging:

  • Line 1: "TODO: notifications"
  • Line 73: "TODO: styling"
  • Line 82: "TODO: styling"

Would you like me to help implement any of these TODOs or create GitHub issues to track them?

packages/graphql/src/schema/liveQuiz.ts (1)

Line range hint 81-91: Consider adding JSDoc documentation.

Since this interface is now part of the public API, it would be beneficial to add JSDoc documentation describing its purpose and usage.

+/**
+ * Represents a live quiz with its associated blocks, feedbacks, and settings.
+ * Used for managing the state and interactions of an active quiz session.
+ */
 export interface ILiveQuiz extends DB.LiveQuiz {
apps/frontend-pwa/src/components/liveSession/PublicFeedback.tsx (1)

11-23: LGTM! Consider extracting type definition.

The explicit Promise return types and inline interface definition improve type safety and clarity. However, if these types are used elsewhere, consider extracting them into a separate types file for reusability.

// types.ts
export interface FeedbackHandlers {
  onUpvoteFeedback: (id: number, change: number) => Promise<void>
  onReactToFeedbackResponse: (
    id: number,
    upvoteChange: number,
    downvoteChange: number
  ) => Promise<void>
}
packages/graphql/src/schema/course.ts (1)

28-29: Track technical debt: Add more context to the TODO comment.

While the TODO comment indicates future removal of sessions, it would be helpful to:

  1. Add more context about the migration plan
  2. Link to a tracking issue or document

The addition of liveQuizzes property is well-structured and maintains consistency with other array properties in the interface.

apps/frontend-pwa/src/pages/session/[id].tsx (2)

Line range hint 92-92: Update sessionId to quizId in handleNewResponse.

The request body still uses sessionId while the codebase is transitioning to quiz-based naming.

Apply this diff to fix the request body:

 body: JSON.stringify({
   instanceId: instanceId,
-  sessionId: id,
+  quizId: id,
   response: { choices: answer },
 }),

Line range hint 214-214: Update component props to use quizId.

The following components still use sessionId props:

  • SessionLeaderboard component
  • QuestionArea component

This should be updated to maintain consistency with the quiz-based naming convention.

Apply these changes:

  1. Update SessionLeaderboard usage:
-<SessionLeaderboard sessionId={id} />
+<SessionLeaderboard quizId={id} />
  1. Update QuestionArea usage:
-sessionId={id}
+quizId={id}

Also applies to: 255-255

apps/frontend-manage/src/components/interaction/AudienceInteraction.tsx (4)

Line range hint 82-88: Consider grouping related mutation hooks.

While the mutation hooks are correctly updated, consider grouping them by functionality for better maintainability:

- const [changeQuizSettings] = useMutation(ChangeLiveQuizSettingsDocument)
- const [publishFeedback] = useMutation(PublishFeedbackDocument)
- const [pinFeedback] = useMutation(PinFeedbackDocument)
- const [resolveFeedback] = useMutation(ResolveFeedbackDocument)
- const [deleteFeedback] = useMutation(DeleteFeedbackDocument)
- const [deleteFeedbackResponse] = useMutation(DeleteFeedbackResponseDocument)
- const [respondToFeedback] = useMutation(RespondToFeedbackDocument)

+ // Quiz settings mutation
+ const [changeQuizSettings] = useMutation(ChangeLiveQuizSettingsDocument)
+ 
+ // Feedback management mutations
+ const [publishFeedback] = useMutation(PublishFeedbackDocument)
+ const [pinFeedback] = useMutation(PinFeedbackDocument)
+ const [resolveFeedback] = useMutation(ResolveFeedbackDocument)
+ 
+ // Feedback response mutations
+ const [respondToFeedback] = useMutation(RespondToFeedbackDocument)
+ const [deleteFeedbackResponse] = useMutation(DeleteFeedbackResponseDocument)
+ const [deleteFeedback] = useMutation(DeleteFeedbackDocument)

Line range hint 121-146: Consider adding error handling for quiz settings mutations.

While the mutation handlers are correctly updated, they lack error handling which could lead to a poor user experience if the mutations fail.

 onCheckedChange={(): void => {
   changeQuizSettings({
     variables: {
       id: quizId,
       isLiveQAEnabled: !isLiveQAEnabled,
     },
+    onError: (error) => {
+      // Handle error appropriately (e.g., show toast notification)
+      console.error('Failed to update quiz settings:', error)
+    }
   })
   push([
     'trackEvent',
     'Running Session',
     !isLiveQAEnabled
       ? 'Feedback Channel Activated'
       : 'Feedback Channel Deactivated',
   ])
 }}

Line range hint 255-283: Consider adding loading states for feedback mutations.

The feedback mutation handlers have been simplified, but they should handle loading states to provide better user feedback during operations.

 handlePinFeedback={(feedbackId: number, isPinned: boolean) => {
   pinFeedback({
     variables: { id: feedbackId, isPinned },
+    onCompleted: () => {
+      // Handle success (e.g., show success toast)
+    },
+    onError: (error) => {
+      // Handle error (e.g., show error toast)
+    }
   })
   push([
     'trackEvent',
     'Running Session',
     'Feedback Pinned',
     String(isPinned),
   ])
 }}

Similar improvements can be applied to publishFeedback and resolveFeedback handlers.


Line range hint 323-334: Consider adding loading state to confusion feedback toggle.

The confusion feedback toggle should indicate when the setting is being updated.

 <Switch
   data={{ cy: 'toggle-gamification' }}
   checked={isConfusionFeedbackEnabled}
+  disabled={loading}
   onCheckedChange={(): void => {
     changeQuizSettings({
       variables: {
         id: quizId,
         isConfusionFeedbackEnabled: !isConfusionFeedbackEnabled,
       },
+      onCompleted: () => {
+        // Handle success
+      },
+      onError: (error) => {
+        // Handle error
+      }
     })
     push([
       'trackEvent',
       'Running Session',
       'Feedback Moderation Toggled',
       String(!isConfusionFeedbackEnabled),
     ])
   }}
packages/graphql/src/public/client.json (1)

Line range hint 1-179: Consider documenting the breaking changes.

Given the significant transition from session-based to quiz-based logic, it would be helpful to document these changes in the GraphQL schema documentation or README to help API consumers adapt their implementations.

packages/graphql/src/schema/query.ts (1)

163-163: Consider adding JSDoc comments for better documentation.

Adding JSDoc comments would improve the documentation by clarifying the purpose and usage of the feedbacks field.

+  /**
+   * Retrieves feedbacks for a specific quiz
+   * @param quizId - The ID of the quiz to fetch feedbacks for
+   * @returns An array of Feedback objects associated with the quiz
+   */
   feedbacks: t.field({
     nullable: true,
     type: [Feedback],
     args: {
       quizId: t.arg.string({ required: true }),
     },
packages/graphql/src/services/participants.ts (1)

123-124: Consider adding isDeleted filter for consistency.

The inclusion of liveQuizzes with PublicationStatus.PUBLISHED filter looks good. However, for consistency with the microLearnings filter above, consider also filtering out deleted quizzes.

 liveQuizzes: {
-  where: { status: PublicationStatus.PUBLISHED },
+  where: { 
+    status: PublicationStatus.PUBLISHED,
+    isDeleted: false 
+  },
 },
packages/graphql/src/ops.schema.json (1)

Line range hint 24397-24419: LGTM: New subscription supports real-time quiz updates.

The runningLiveQuizUpdated subscription is well-structured for real-time quiz state updates.

Consider implementing proper error handling in the subscription resolver to handle scenarios like:

  • Quiz not found
  • Quiz ended
  • Connection issues
    This will ensure a better real-time experience for users.
apps/frontend-pwa/src/lib/hooks/useStudentOverviewSplit.tsx (3)

82-93: Refactor duplicated code when constructing course objects.

The code blocks for constructing courses (lines 82-93) and oldCourses (lines 99-110) are nearly identical, leading to code duplication. Consider extracting the shared logic into a helper function to improve maintainability and reduce redundancy.

You can create a helper function to map a participation to a LocalCourseType:

function mapParticipationToCourse(participation: Participation): LocalCourseType {
  return {
    id: participation.course?.id,
    displayName: participation.course?.displayName,
    startDate: participation.course?.startDate,
    endDate: participation.course?.endDate,
    isGamificationEnabled: participation.course?.isGamificationEnabled,
    isSubscribed: participation.subscriptions?.length > 0 ?? false,
  }
}

Then, update the assignments in the reducer:

 courses:
   (dayjs(course.endDate).isAfter(dayjs()) || dayjs(course.endDate).isSame(dayjs()))
-    ? [
-        ...acc.courses,
-        {
-          id: course.id,
-          displayName: course.displayName,
-          startDate: course.startDate,
-          endDate: course.endDate,
-          isGamificationEnabled: course.isGamificationEnabled,
-          isSubscribed:
-            (participation.subscriptions && participation.subscriptions.length > 0) ??
-            false,
-        },
-      ]
+    ? [...acc.courses, mapParticipationToCourse(participation)]
     : acc.courses,
 oldCourses:
   dayjs(course.endDate).isBefore(dayjs())
-    ? [
-        ...acc.oldCourses,
-        {
-          id: course.id,
-          displayName: course.displayName,
-          startDate: course.startDate,
-          endDate: course.endDate,
-          isGamificationEnabled: course.isGamificationEnabled,
-          isSubscribed:
-            (participation.subscriptions && participation.subscriptions.length > 0) ??
-            false,
-        },
-      ]
+    ? [...acc.oldCourses, mapParticipationToCourse(participation)]
     : acc.oldCourses,

Also applies to: 99-110


89-92: Simplify the logic for the 'isSubscribed' property.

Currently, the isSubscribed property uses a combination of logical operators and the nullish coalescing operator, which can be simplified. The expression (participation.subscriptions && participation.subscriptions.length > 0) ?? false can be refactored for clarity.

Apply this diff to simplify the logic:

- isSubscribed:
-   (participation.subscriptions &&
-     participation.subscriptions.length > 0) ??
-   false,
+ isSubscribed: participation.subscriptions?.length > 0 ?? false,

This change makes use of optional chaining and directly evaluates to a boolean value. Apply this change to both the courses and oldCourses sections.

Also applies to: 107-110


115-120: Optimize array concatenation for quizzes and micro-learnings.

When constructing activeLiveQuizzes and activeMicrolearning, you're using the spread operator to concatenate arrays, including an empty array when there are no items. This can be optimized by conditionally spreading the arrays only when they contain elements.

Modify the code as follows:

 activeLiveQuizzes: [
   ...acc.activeLiveQuizzes,
-  ...(course.liveQuizzes?.map((quiz) => ({
+  ...course.liveQuizzes?.map((quiz) => ({
     id: quiz.id,
     displayName: quiz.displayName,
     courseName: course.displayName,
-  })) ?? []),
+  })) ?? [],
 ],
 activeMicrolearning: [
   ...acc.activeMicrolearning,
-  ...(course.microLearnings?.map((micro) => ({
+  ...course.microLearnings?.map((micro) => ({
     id: micro.id,
     displayName: micro.displayName,
     scheduledStartAt: micro.scheduledStartAt,
     scheduledEndAt: micro.scheduledEndAt,
     courseName: course.displayName,
     isCompleted: participation.completedMicroLearnings?.includes(
       micro.id
     ),
-  })) ?? []),
+  })) ?? [],
 ],

By removing the unnecessary empty arrays, you prevent potential performance overhead.

Also applies to: 123-133

packages/graphql/src/services/feedbacks.ts (6)

71-71: Improve Error Handling by Throwing Specific Exceptions

Returning null when the quiz is not found or isLiveQAEnabled is false may not provide sufficient information to the caller. Consider throwing specific errors to indicate the exact issue.

Consider modifying the code as follows:

-if (!quiz || !quiz.isLiveQAEnabled) return null
+if (!quiz) {
+  throw new Error('Quiz not found')
+}
+if (!quiz.isLiveQAEnabled) {
+  throw new Error('Live Q&A is not enabled for this quiz')
+}

149-157: Consistent Parameter Typing for 'addConfusionTimestep' Function

Ensure that the parameter types are consistently defined. The inline types in the destructured parameters can be extracted into a separate interface for clarity.

Consider defining an interface for the parameters:

interface AddConfusionTimestepArgs {
  quizId: string
  difficulty: number
  speed: number
}

export async function addConfusionTimestep(
  { quizId, difficulty, speed }: AddConfusionTimestepArgs,
  ctx: Context
) {
  // function body
}

266-266: Improve Error Handling in 'resolveFeedback' Function

Returning null when the feedback is not found or the user is unauthorized may not provide sufficient context. Throwing specific errors can improve debuggability and user feedback.

Modify the code:

-if (!feedback || feedback.liveQuiz!.ownerId !== ctx.user.sub) return null
+if (!feedback) {
+  throw new Error('Feedback not found')
+}
+if (!feedback.liveQuiz || feedback.liveQuiz.ownerId !== ctx.user.sub) {
+  throw new Error('Unauthorized access')
+}

300-300: Handle Null Cases Explicitly in 'deleteFeedback'

To enhance clarity and error handling, explicitly check for null and throw meaningful errors instead of returning null.

Revise the condition:

-if (!feedback || feedback.liveQuiz!.ownerId !== ctx.user.sub) return null
+if (!feedback) {
+  throw new Error('Feedback not found')
+}
+if (!feedback.liveQuiz || feedback.liveQuiz.ownerId !== ctx.user.sub) {
+  throw new Error('Unauthorized access')
+}

65-67: Optimize Database Query by Selecting Only Necessary Fields

The current query fetches the entire liveQuiz object, which may include unnecessary data. To improve performance, select only the fields you need.

Modify the query:

-const quiz = await ctx.prisma.liveQuiz.findUnique({
-  where: {
-    id: quizId,
-  },
-})
+const quiz = await ctx.prisma.liveQuiz.findUnique({
+  where: { id: quizId },
+  select: { id: true, isLiveQAEnabled: true, isModerationEnabled: true },
+})

89-89: Consider Removing Debugging Statements Before Production Deployment

The console.log statement may have been used for debugging purposes. It's good practice to remove such statements to keep the production console clean.

-console.log('TRIGGERING FEEDBACK ADDED SUBSCRIPTION')
apps/frontend-pwa/src/components/liveSession/FeedbackArea.tsx (3)

108-108: Specify a proper type instead of using any for confusion

Using the any type can undermine TypeScript's type safety features. Define an appropriate type for confusion to improve code maintainability and readability.

For example, define an interface:

interface ConfusionData {
  prevSpeed: number
  prevDifficulty: number
  prevTimestamp: string
}

const confusion: ConfusionData | null = await localForage.getItem<ConfusionData>(
  `${quizId}-confusion`
)

27-41: Refactor color maps to reduce code duplication

The RANGE_COLOR_MAP and BORDER_COLOR_MAP constants have similar structures with overlapping values. Consider refactoring to reduce duplication by using a single source of truth or generating these maps dynamically.

For example:

const COLOR_VALUES = {
  '-2': { bg: 'bg-red-200', border: 'border-red-300' },
  '-1': { bg: 'bg-yellow-200', border: 'border-yellow-300' },
  '0': { bg: 'bg-green-200', border: 'border-green-300' },
  '1': { bg: 'bg-yellow-200', border: 'border-yellow-300' },
  '2': { bg: 'bg-red-200', border: 'border-red-300' },
}

const RANGE_COLOR_MAP = Object.fromEntries(
  Object.entries(COLOR_VALUES).map(([key, value]) => [key, value.bg])
)

const BORDER_COLOR_MAP = Object.fromEntries(
  Object.entries(COLOR_VALUES).map(([key, value]) => [key, value.border])
)

This approach centralizes the color values and makes it easier to maintain or update them in the future.


149-149: Await localForage.setItem to ensure data persistence

The localForage.setItem operation is asynchronous and should be awaited to guarantee that the data is saved before proceeding. This is crucial, especially if subsequent code relies on this data being stored.

Modify the code as follows:

- localForage.setItem(`${quizId}-confusion`, {
+ await localForage.setItem(`${quizId}-confusion`, {
    prevSpeed: speed,
    prevDifficulty: difficulty,
    prevTimestamp: dayjs().format(),
  })
packages/graphql/src/services/liveQuizzes.ts (1)

630-658: Optimize feedback aggregation using efficient filtering

In the aggregateFeedbacks function, filtering feedbacks with Array.filter and dayjs().diff can be inefficient for larger datasets. Consider sorting the feedbacks by createdAt in descending order and breaking the loop once feedbacks are outside the desired time window to improve performance.

Apply this diff to optimize the aggregation:

const aggregateFeedbacks = (feedbacks: ConfusionTimestep[]) => {
- // TODO: for improved efficiency, try to use descending feedback ordering
- // and break early once first is not within the filtering requirements anymore
- const recentFeedbacks = feedbacks.filter(
-   (feedback) =>
-     dayjs().diff(dayjs(feedback.createdAt)) > 0 &&
-     dayjs().diff(dayjs(feedback.createdAt)) < 1000 * 60 * 10
- )

+ const tenMinutesInMillis = 1000 * 60 * 10
+ const now = dayjs()
+ const recentFeedbacks = []
+ for (const feedback of feedbacks.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())) {
+   const diff = now.diff(dayjs(feedback.createdAt))
+   if (diff >= 0 && diff <= tenMinutesInMillis) {
+     recentFeedbacks.push(feedback)
+   } else if (diff > tenMinutesInMillis) {
+     break
+   }
+ }

  if (recentFeedbacks.length > 0) {
    const summedFeedbacks = recentFeedbacks.reduce(
      (previousValue, feedback) => {
        return {
          speed: previousValue.speed + feedback.speed,
          difficulty: previousValue.difficulty + feedback.difficulty,
          numberOfParticipants: previousValue.numberOfParticipants + 1,
        }
      },
      { speed: 0, difficulty: 0, numberOfParticipants: 0 }
    )
    return {
      ...summedFeedbacks,
      speed: summedFeedbacks.speed / summedFeedbacks.numberOfParticipants,
      difficulty:
        summedFeedbacks.difficulty / summedFeedbacks.numberOfParticipants,
    }
  }
  return { speed: 0, difficulty: 0, numberOfParticipants: 0 }
}
🛑 Comments failed to post (19)
packages/graphql/src/graphql/ops/MCreateFeedback.graphql (1)

1-2: 💡 Codebase verification

Inconsistent parameter naming found across GraphQL operations

The transition from sessionId to quizId is not consistently applied across the codebase. Several files still use sessionId:

  • Session block operations:
    • MActivateSessionBlock.graphql
    • MDeactivateSessionBlock.graphql
  • Session queries:
    • QGetSessionLeaderboard.graphql
    • QGetSessionEvaluation.graphql (uses both sessionId and id)
  • The GraphQL schema (schema.graphql) still defines operations with sessionId parameters

This indicates that the architecture transition from session-based to quiz-based logic is incomplete. The parameter renaming in MCreateFeedback.graphql appears to be part of an ongoing migration that needs to be completed across all related operations.

🔗 Analysis chain

LGTM! Parameter renaming aligns with architecture changes.

The transition from sessionId to quizId is consistent with the codebase-wide refactoring from session-based to quiz-based logic.

Let's verify that this schema change is consistently applied across related GraphQL operations:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify consistent parameter naming across GraphQL operations
# Expected: No remaining instances of sessionId in GraphQL operations

# Test 1: Check for any remaining sessionId parameters in mutations
echo "Checking mutations for sessionId..."
fd -e graphql -x rg -l 'mutation.*\$sessionId:' {}

# Test 2: Check for any remaining sessionId parameters in queries
echo "Checking queries for sessionId..."
fd -e graphql -x rg -l 'query.*\$sessionId:' {}

# Test 3: Check for any remaining sessionId field usage
echo "Checking field usage for sessionId..."
fd -e graphql -x rg -l '\(sessionId:' {}

Length of output: 833


Script:

#!/bin/bash
# Let's examine the content of these files to understand the context better

echo "=== Content of MDeactivateSessionBlock.graphql ==="
cat ./packages/graphql/src/graphql/ops/MDeactivateSessionBlock.graphql

echo -e "\n=== Content of MActivateSessionBlock.graphql ==="
cat ./packages/graphql/src/graphql/ops/MActivateSessionBlock.graphql

echo -e "\n=== Content of QGetSessionLeaderboard.graphql ==="
cat ./packages/graphql/src/graphql/ops/QGetSessionLeaderboard.graphql

echo -e "\n=== Content of QGetSessionEvaluation.graphql ==="
cat ./packages/graphql/src/graphql/ops/QGetSessionEvaluation.graphql

echo -e "\n=== Relevant parts of schema.graphql containing sessionId ==="
rg "sessionId" ./packages/graphql/src/public/schema.graphql -C 2

Length of output: 4927

packages/graphql/src/graphql/ops/SFeedbackAdded.graphql (1)

1-2: 💡 Codebase verification

Mixed usage of sessionId and quizId found in the codebase

The verification reveals inconsistent parameter naming across GraphQL operations:

  • Several operations still use sessionId:

    • activateSessionBlock, deactivateSessionBlock mutations
    • sessionLeaderboard query
    • Associated operation files (QGetSessionLeaderboard, QGetSessionEvaluation, etc.)
  • While feedback-related operations have been updated to use quizId:

    • All feedback subscriptions (Added/Created/Updated/Removed)
    • Feedback queries and mutations
    • Live quiz related operations

This mixed usage suggests an incomplete refactoring effort that could lead to runtime issues. The schema should consistently use either session-based or quiz-based terminology.

🔗 Analysis chain

LGTM! Verify schema consistency across the codebase.

The parameter and field renaming from sessionId to quizId aligns well with the refactoring objectives. The subscription structure remains robust with comprehensive fields for feedback handling.

Let's verify the schema changes are consistent across the codebase:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that all sessionId references in GraphQL files have been updated to quizId
# and there are no mixed usages that could cause runtime issues.

echo "Checking for any remaining sessionId references in GraphQL files..."
rg -g '*.graphql' -g '*.gql' 'sessionId'

echo "Checking for quizId usage consistency in GraphQL files..."
rg -g '*.graphql' -g '*.gql' 'quizId'

echo "Checking GraphQL schema for type definitions..."
fd -g 'schema.graphql' --exec cat {} \; | rg -A 5 -B 5 'type .*Feedback|input .*Feedback'

Length of output: 5968

packages/graphql/src/graphql/ops/SFeedbackUpdated.graphql (1)

1-2: 💡 Codebase verification

Warning: Inconsistent parameter naming across related operations

The verification revealed that while feedback-related operations consistently use quizId, there are still several session-related operations using sessionId. This indicates an incomplete transition:

  • QGetSessionLeaderboard.graphql: Uses sessionId parameter
  • QGetSessionEvaluation.graphql: Uses sessionId parameter
  • MDeactivateSessionBlock.graphql: Uses sessionId parameter
  • MActivateSessionBlock.graphql: Uses sessionId parameter

The parameter renaming in SFeedbackUpdated.graphql is part of a larger architectural change, but the codebase shows inconsistent implementation of this change.

🔗 Analysis chain

LGTM! Parameter renaming aligns with the quiz-based architecture.

The change from sessionId to quizId is consistent with the broader refactoring effort.

Let's verify that this renaming is consistently applied across all related GraphQL operations:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining sessionId references in GraphQL files
# and verify the consistency of quizId usage

# Search for any remaining sessionId references in GraphQL files
echo "Checking for remaining sessionId references in GraphQL operations:"
rg -t graphql 'sessionId' packages/graphql/src/graphql/ops/

# Verify consistent usage of quizId in feedback-related operations
echo "Verifying quizId usage in feedback-related operations:"
rg -t graphql 'quizId.*feedback' packages/graphql/src/graphql/ops/

Length of output: 454


Script:

#!/bin/bash
# Let's try without file type filtering since GraphQL extension isn't recognized
echo "Checking for remaining sessionId references:"
rg "sessionId" packages/graphql/src/graphql/ops/

echo -e "\nVerifying quizId usage in feedback operations:"
rg "quizId.*feedback|feedback.*quizId" packages/graphql/src/graphql/ops/

echo -e "\nListing all GraphQL operation files for context:"
find packages/graphql/src/graphql/ops/ -type f -name "*.graphql"

Length of output: 13528

apps/frontend-pwa/src/components/liveSession/FeedbackAreaSubscriber.tsx (2)

20-35: 🛠️ Refactor suggestion

Consider adding error handling and timestamp-based sorting.

The current implementation has two potential issues:

  1. No error handling for subscription setup failures
  2. Multiple rapid feedback additions might lead to inconsistent ordering

Consider this improvement:

 const feedbackAdded = subscribeToMore({
   document: FeedbackAddedDocument,
   variables: { quizId },
+  onError: (error) => {
+    console.error('Error subscribing to feedback updates:', error)
+  },
   updateQuery: (
     prev: { feedbacks: Feedback[] },
     {
       subscriptionData,
     }: { subscriptionData: { data: { feedbackAdded: Feedback } } }
   ) => {
     if (!subscriptionData.data) return prev
     const newItem = subscriptionData.data.feedbackAdded
     if (prev.feedbacks?.map((item) => item.id).includes(newItem.id))
       return prev
-    return { ...prev, feedbacks: [newItem, ...prev.feedbacks] }
+    const updatedFeedbacks = [newItem, ...prev.feedbacks]
+    // Sort by creation timestamp to ensure consistent ordering
+    updatedFeedbacks.sort((a, b) => b.createdAt - a.createdAt)
+    return { ...prev, feedbacks: updatedFeedbacks }
   },
 })

Committable suggestion skipped: line range outside the PR's diff.


37-55: 🛠️ Refactor suggestion

Improve type safety in ID handling.

The current implementation mixes string and number types for IDs, which could lead to subtle bugs.

Consider using consistent ID types:

 updateQuery: (
   prev: { feedbacks: Feedback[] },
   {
     subscriptionData,
   }: { subscriptionData: { data: { feedbackRemoved: string } } }
 ) => {
   if (!subscriptionData.data) return prev
   const removedItem = subscriptionData.data.feedbackRemoved
   return {
     ...prev,
     feedbacks: prev.feedbacks?.filter(
-      (item) => item.id !== parseInt(removedItem)
+      (item) => item.id.toString() !== removedItem
     ),
   }
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const feedbackRemoved = subscribeToMore({
      document: FeedbackRemovedDocument,
      variables: { quizId },
      updateQuery: (
        prev: { feedbacks: Feedback[] },
        {
          subscriptionData,
        }: { subscriptionData: { data: { feedbackRemoved: string } } }
      ) => {
        if (!subscriptionData.data) return prev
        const removedItem = subscriptionData.data.feedbackRemoved
        return {
          ...prev,
          feedbacks: prev.feedbacks?.filter(
            (item) => item.id.toString() !== removedItem
          ),
        }
      },
    })
apps/frontend-pwa/src/components/liveSession/PublicFeedback.tsx (2)

80-80: ⚠️ Potential issue

Add error handling and loading states for async operations.

The async operations lack error handling and user feedback. Consider implementing:

  1. Try-catch blocks for async operations
  2. Loading states during operations
  3. User feedback for success/failure

Example implementation:

  const onUpvote = async (previousValue: boolean) => {
+   try {
+     setIsLoading(true);
      const newUpvotes = { ...upvotes, upvote: !previousValue }
      setUpvotes(newUpvotes)
      await localForage.setItem(
        `${feedbackId}-upvotes`,
        JSON.stringify(newUpvotes)
      )
      await onUpvoteFeedback(feedbackId, previousValue ? -1 : 1)
+     // Optional: Show success toast/feedback
+   } catch (error) {
+     console.error('Failed to update upvote:', error);
+     setUpvotes({ ...upvotes }); // Rollback state
+     // Show error toast/feedback
+   } finally {
+     setIsLoading(false);
+   }
  }

Also applies to: 99-103, 123-127


170-171: 🛠️ Refactor suggestion

Prevent rapid clicks and show loading state.

The click handlers could lead to race conditions with rapid clicks. Consider:

  1. Debouncing/throttling click events
  2. Disabling buttons during operations
  3. Adding loading indicators

Example implementation:

+ const [isProcessing, setIsProcessing] = useState<Record<number, boolean>>({});

  <Button
    onClick={async () => {
+     if (isProcessing[response.id]) return;
+     setIsProcessing(prev => ({ ...prev, [response.id]: true }));
      try {
        await onResponseUpvote(upvotes[response.id], response.id);
+     } finally {
+       setIsProcessing(prev => ({ ...prev, [response.id]: false }));
+     }
    }}
+   disabled={isProcessing[response.id]}
    active={upvotes[response.id] === 1}
    className={{ root: 'mr-1 h-9 w-9' }}
  >
    <Button.Icon>
+     {isProcessing[response.id] ? <Spinner size="sm" /> : (
        <FontAwesomeIcon icon={faThumbsUp} size="lg" />
+     )}
    </Button.Icon>
  </Button>

Also applies to: 184-188

packages/graphql/src/schema/course.ts (1)

98-106: 💡 Codebase verification

Consider tracking migration TODOs centrally

The search results confirm multiple TODO comments related to the "live quiz migration" scattered across the codebase, particularly in:

  • packages/types/src/index.ts (6 instances)
  • packages/graphql/src/schema/ (multiple files)
  • packages/graphql/src/services/

Recommendations:

  • Create a central tracking issue for the live quiz migration
  • Update all related TODOs to reference the issue number (e.g., // TODO(#123): remove after migration)
  • Consider using a consistent format to make future cleanup easier
🔗 Analysis chain

Consider consolidating duplicate TODO comments.

The implementation of both fields is correct, but there are duplicate TODO comments about session removal. Consider:

  1. Centralizing the TODO tracking in a single location
  2. Using a more specific comment format that can be tracked by tools (e.g., // TODO(#issue): remove after migration)

Let's check for other similar TODOs in the codebase:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for similar TODO comments about session migration
rg -i "todo.*remove.*migration" --type ts

Length of output: 266


Script:

#!/bin/bash
# Let's try a more specific search for TODO comments about sessions
rg -i "todo.*session" --type ts

# Also search for similar migration-related TODOs
rg -i "todo.*migrat" --type ts

# Let's also check the specific file for context
cat "packages/graphql/src/schema/course.ts"

Length of output: 11599

apps/frontend-pwa/src/pages/session/[id].tsx (1)

31-52: ⚠️ Potential issue

Fix inconsistencies in the updateQuery function.

There are two issues in the updateQuery function:

  1. The returned object uses session property but the previous state uses studentLiveQuiz
  2. The spread operation is using prev.studentLiveQuiz but trying to assign it to a session property

Apply this diff to fix the property names:

 return Object.assign({}, prev, {
-  session: {
-    ...prev.studentLiveQuiz,
+  studentLiveQuiz: {
+    ...prev.studentLiveQuiz,
     activeBlock: subscriptionData.data.runningLiveQuizUpdated,
   },
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  id: string
  subscribeToMore: (doc: SubscribeToMoreOptions) => any
}) {
  useEffect(() => {
    subscribeToMore({
      document: RunningLiveQuizUpdatedDocument,
      variables: {
        quizId: id,
      },
      updateQuery: (
        prev: { studentLiveQuiz: LiveQuiz },
        {
          subscriptionData,
        }: {
          subscriptionData: { data: { runningLiveQuizUpdated: ElementBlock } }
        }
      ) => {
        if (!subscriptionData.data) return prev
        return Object.assign({}, prev, {
          studentLiveQuiz: {
            ...prev.studentLiveQuiz,
            activeBlock: subscriptionData.data.runningLiveQuizUpdated,
packages/i18n/messages/en.ts (1)

128-128: 💡 Codebase verification

Several occurrences of "session" still need to be updated to "quiz" for consistency

The search results reveal several instances where "session" terminology is still being used in both English and German translations. Here are the key areas that need attention:

  • In packages/i18n/messages/en.ts:

    • session object key and its nested translations
    • SESSION_EXEC constant
    • sessionName, sessionDisplayName, sessionControl, etc.
    • References to "session" in descriptive texts about Q&A and feedback channels
  • In packages/i18n/messages/de.ts:

    • Similar occurrences of "Session" in German translations that should be updated to "Quiz"
    • SESSION_EXEC and related terminology
    • Various UI elements and messages still using "Session" instead of "Quiz"
🔗 Analysis chain

LGTM! Verify other occurrences of "session" in translations.

The renaming from activeSessions to activeLiveQuizzes aligns with the PR objective of transitioning from session-based to quiz-based logic.

Let's verify if there are any remaining inconsistent usages of "session" that should be updated to "quiz":

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for remaining occurrences of "session" in translation files
# that might need to be updated for consistency.

# Test: Search for "session" in translation files
rg -i "session" "packages/i18n/messages/"

Length of output: 10374

apps/frontend-pwa/src/lib/hooks/useStudentOverviewSplit.tsx (2)

129-131: 🛠️ Refactor suggestion

Ensure 'isCompleted' always returns a boolean value.

The expression participation.completedMicroLearnings?.includes(micro.id) may return undefined if completedMicroLearnings is undefined. This could result in isCompleted being undefined instead of a boolean. To ensure isCompleted is always a boolean, provide a default value.

Apply this diff to guarantee a boolean value:

 isCompleted: participation.completedMicroLearnings?.includes(
   micro.id
- ),
+ ) ?? false,

This change ensures that if includes returns undefined, isCompleted will default to false.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            isCompleted: participation.completedMicroLearnings?.includes(
              micro.id
            ) ?? false,

77-79: 🛠️ Refactor suggestion

Correct the comment to reflect the actual date comparison logic.

The comment on line 77 states: // check if endDate of course is before today or today, but the condition checks if the endDate is after or same as today using isAfter(dayjs()) || isSame(dayjs()). The comment should be updated to accurately describe the logic.

Apply this diff to correct the comment:

-// check if endDate of course is before today or today
+// Check if the endDate of the course is today or in the future
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

          // Check if the endDate of the course is today or in the future
          dayjs(participation.course?.endDate).isAfter(dayjs()) ||
          dayjs(participation.course?.endDate).isSame(dayjs())
packages/graphql/src/services/feedbacks.ts (3)

111-111: ⚠️ Potential issue

Avoid Using Non-Null Assertion '!' on Potentially Undefined Properties

Using feedback.liveQuiz! assumes that liveQuiz is not null or undefined, which could lead to runtime errors if it is. Implement proper null checks to ensure robustness.

Modify the condition to include a null check:

-if (!feedback || feedback.liveQuiz!.ownerId !== ctx.user.sub) return null
+if (!feedback || !feedback.liveQuiz || feedback.liveQuiz.ownerId !== ctx.user.sub) return null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  if (!feedback || !feedback.liveQuiz || feedback.liveQuiz.ownerId !== ctx.user.sub) return null

231-231: ⚠️ Potential issue

Ensure Proper Null Checks Before Accessing 'ownerId' in 'pinFeedback'

As with previous instances, avoid using non-null assertions on liveQuiz. Implement null checks to prevent potential runtime errors.

Update the condition:

-if (!feedback || feedback.liveQuiz!.ownerId !== ctx.user.sub) return null
+if (!feedback || !feedback.liveQuiz || feedback.liveQuiz.ownerId !== ctx.user.sub) return null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  if (!feedback || !feedback.liveQuiz || feedback.liveQuiz.ownerId !== ctx.user.sub) return null

333-333: ⚠️ Potential issue

Avoid Non-Null Assertion in 'deleteFeedbackResponse' and Check for Null

Ensure that feedbackResponse.feedback.liveQuiz is not null before accessing ownerId to prevent runtime errors.

Update the condition:

-if (!feedbackResponse || feedbackResponse.feedback.liveQuiz!.ownerId !== ctx.user.sub) {
+if (
+  !feedbackResponse ||
+  !feedbackResponse.feedback.liveQuiz ||
+  feedbackResponse.feedback.liveQuiz.ownerId !== ctx.user.sub
+) {

Committable suggestion skipped: line range outside the PR's diff.

apps/frontend-pwa/src/components/liveSession/FeedbackArea.tsx (2)

77-84: 🛠️ Refactor suggestion

Add error handling for asynchronous mutations in onAddFeedback

The onAddFeedback function performs an asynchronous mutation without error handling. If the mutation fails, the user won't be notified, and potential errors will go unhandled. Consider adding a try-catch block to handle errors and provide user feedback.

You can modify the function as follows:

const onAddFeedback = async (input: string) => {
  if (!router.query.id) return
+ try {
    await createFeedback({
      variables: {
        quizId: router.query.id as string,
        content: input,
      },
    })
+ } catch (error) {
+   // Handle error, e.g., show notification to user
+   console.error('Failed to add feedback:', error)
+ }
}

Committable suggestion skipped: line range outside the PR's diff.


143-149: ⚠️ Potential issue

Await asynchronous operations and handle errors in handleNewConfusionTS

In the handleNewConfusionTS function, the addConfusionTimestep mutation and localForage.setItem are both asynchronous operations but are not being awaited. This may lead to unhandled promise rejections and unexpected behavior. To ensure proper execution and error handling, you should await these operations and include error handling.

Modify the code as follows:

- addConfusionTimestep({
+ await addConfusionTimestep({
    variables: {
      quizId,
      difficulty: difficulty,
      speed: speed,
    },
  })
- localForage.setItem(`${quizId}-confusion`, {
+ await localForage.setItem(`${quizId}-confusion`, {
    prevSpeed: speed,
    prevDifficulty: difficulty,
    prevTimestamp: dayjs().format(),
  })

Also, ensure that these await statements are within the existing try-catch block to handle any potential errors effectively.

Committable suggestion skipped: line range outside the PR's diff.

packages/graphql/src/services/liveQuizzes.ts (2)

518-547: ⚠️ Potential issue

Add error handling when the live quiz is not found

In the changeLiveQuizSettings function, if the live quiz with the specified id and ownerId does not exist, prisma.liveQuiz.update will throw an error. It's recommended to check whether the quiz exists before attempting to update it to provide a clearer error message.

Apply this diff to add error handling:

export async function changeLiveQuizSettings(
  {
    id,
    isLiveQAEnabled,
    isConfusionFeedbackEnabled,
    isModerationEnabled,
    isGamificationEnabled,
  }: {
    id: string
    isLiveQAEnabled?: boolean | null
    isConfusionFeedbackEnabled?: boolean | null
    isModerationEnabled?: boolean | null
    isGamificationEnabled?: boolean | null
  },
  ctx: ContextWithUser
) {
+ const existingQuiz = await ctx.prisma.liveQuiz.findFirst({
+   where: {
+     id,
+     ownerId: ctx.user.sub,
+   },
+ })
+ if (!existingQuiz) {
+   throw new GraphQLError('Live quiz not found or access denied')
+ }

  const quiz = await ctx.prisma.liveQuiz.update({
    where: {
      id,
      ownerId: ctx.user.sub,
    },
    data: {
      isLiveQAEnabled: isLiveQAEnabled ?? undefined,
      isConfusionFeedbackEnabled: isConfusionFeedbackEnabled ?? undefined,
      isModerationEnabled: isModerationEnabled ?? undefined,
      isGamificationEnabled: isGamificationEnabled ?? undefined,
    },
  })
  return quiz
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export async function changeLiveQuizSettings(
  {
    id,
    isLiveQAEnabled,
    isConfusionFeedbackEnabled,
    isModerationEnabled,
    isGamificationEnabled,
  }: {
    id: string
    isLiveQAEnabled?: boolean | null
    isConfusionFeedbackEnabled?: boolean | null
    isModerationEnabled?: boolean | null
    isGamificationEnabled?: boolean | null
  },
  ctx: ContextWithUser
) {
  const existingQuiz = await ctx.prisma.liveQuiz.findFirst({
    where: {
      id,
      ownerId: ctx.user.sub,
    },
  })
  if (!existingQuiz) {
    throw new GraphQLError('Live quiz not found or access denied')
  }

  const quiz = await ctx.prisma.liveQuiz.update({
    where: {
      id,
      ownerId: ctx.user.sub,
    },
    data: {
      isLiveQAEnabled: isLiveQAEnabled ?? undefined,
      isConfusionFeedbackEnabled: isConfusionFeedbackEnabled ?? undefined,
      isModerationEnabled: isModerationEnabled ?? undefined,
      isGamificationEnabled: isGamificationEnabled ?? undefined,
    },
  })
  return quiz
}

552-608: 🛠️ Refactor suggestion

Ensure consistent handling of deletion results

In the deleteLiveQuiz function, the return value is null in cases where the quiz cannot be deleted. For better clarity and consistency, consider throwing a GraphQLError when deletion is not possible due to the quiz's status or non-existence, so that the client can handle the error appropriately.

Apply this diff to improve error handling:

if (!liveQuiz) {
-  return null
+  throw new GraphQLError('Live quiz not found or access denied')
}

if (liveQuiz.status === PublicationStatus.PUBLISHED) {
  // running live quizzes cannot be deleted
-  return null
+  throw new GraphQLError('Cannot delete a running live quiz')
} else if (liveQuiz.status === PublicationStatus.ENDED) {
  // ... existing code ...
} else {
  // ... existing code ...
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export async function deleteLiveQuiz(
  { id }: { id: string },
  ctx: ContextWithUser
) {
  // fetch live quiz to check its status
  const liveQuiz = await ctx.prisma.liveQuiz.findUnique({
    where: {
      id,
      ownerId: ctx.user.sub,
    },
    select: {
      status: true,
    },
  })

  if (!liveQuiz) {
    throw new GraphQLError('Live quiz not found or access denied')
  }

  if (liveQuiz.status === PublicationStatus.PUBLISHED) {
    // running live quizzes cannot be deleted
    throw new GraphQLError('Cannot delete a running live quiz')
  } else if (liveQuiz.status === PublicationStatus.ENDED) {
    const deletedLiveQuiz = await ctx.prisma.liveQuiz.update({
      where: {
        id,
        ownerId: ctx.user.sub,
        status: PublicationStatus.ENDED,
      },
      data: {
        isDeleted: true,
      },
    })

    ctx.emitter.emit('invalidate', {
      typename: 'Session',
      id,
    })

    return deletedLiveQuiz
  } else {
    const deletedLiveQuiz = await ctx.prisma.liveQuiz.delete({
      where: {
        id,
        ownerId: ctx.user.sub,
        status: {
          in: [PublicationStatus.DRAFT, PublicationStatus.SCHEDULED],
        },
      },
    })

    ctx.emitter.emit('invalidate', {
      typename: 'LiveQuiz',
      id,
    })

    return deletedLiveQuiz
  }
}

Copy link

cypress bot commented Nov 4, 2024

klicker-uzh    Run #3529

Run Properties:  status check failed Failed #3529  •  git commit c308884154 ℹ️: Merge 94f151a2ae0ffcc01de2b2d47827a72b903ee4cc into 218bdeed6823e30ae1293ba549d1...
Project klicker-uzh
Branch Review live-quiz-interaction
Run status status check failed Failed #3529
Run duration 18m 11s
Commit git commit c308884154 ℹ️: Merge 94f151a2ae0ffcc01de2b2d47827a72b903ee4cc into 218bdeed6823e30ae1293ba549d1...
Committer Julius Schlapbach
View all properties for this run ↗︎

Test results
Tests that failed  Failures 12
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 128
View all changes introduced in this branch ↗︎

Tests for review

Failed  C-control-workflow.cy.ts • 2 failed tests

View Output

Test Artifacts
Test functionalities of frontend-control application > Create a new live quiz with the SC question Test Replay Screenshots
Test functionalities of frontend-control application > Generate a token to log into the control-frontend application, execute session Test Replay Screenshots
Failed  E-course-workflow.cy.ts • 1 failed test

View Output

Test Artifacts
Test course creation and editing functionalities > Create a course with live quiz, practice quiz, and microlearning, and delete it again Test Replay Screenshots
Failed  F-live-quiz-workflow.cy.ts • 9 failed tests

View Output

Test Artifacts
Different live-quiz workflows > Cleanup: Delete the created and completed live quiz Test Replay Screenshots
Different live-quiz workflows > Responds to the first block of the running session from the student view Test Replay Screenshots
Different live-quiz workflows > Test the live quiz functionalities on mobile devices Test Replay Screenshots
Different live-quiz workflows > Make feedbacks visible, respond to one and disable moderation Test Replay Screenshots
Different live-quiz workflows > Student answers questions in second block Test Replay Screenshots
Different live-quiz workflows > Verify that the feedbacks and the given feedback answer are visible to the student Test Replay Screenshots
Different live-quiz workflows > Close block and delete feedback / feedback response Test Replay Screenshots
Different live-quiz workflows > Check that the deleted feedbacks are not visible anymore Test Replay Screenshots
Different live-quiz workflows > Cleanup: Delete the live quiz used for the full cycle test Test Replay Screenshots

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 9

🧹 Outside diff range and nitpick comments (27)
packages/graphql/src/graphql/ops/MCancelLiveQuiz.graphql (1)

1-5: Consider enhancing the response type with status information.

While returning just the ID is sufficient for basic confirmation, consider adding fields like success or status to provide more detailed feedback about the cancellation operation. This could help in better error handling on the client side.

 mutation CancelLiveQuiz($id: String!) {
   cancelLiveQuiz(id: $id) {
     id
+    success
+    message
   }
 }
packages/graphql/src/graphql/ops/QGetCourseRunningLiveQuizzes.graphql (1)

1-11: Consider adding pagination and additional metadata fields.

For improved scalability and usability, consider:

  1. Adding pagination support for courses with many running quizzes
  2. Including additional metadata fields like:
    • Quiz start time
    • Current status
    • Number of participants
 query GetCourseRunningLiveQuizzes($courseId: String!) {
-  getCourseRunningLiveQuizzes(courseId: $courseId) {
+  getCourseRunningLiveQuizzes(
+    courseId: $courseId
+    first: $first
+    after: $cursor
+  ) {
+    edges {
+      node {
         id
         name
         displayName
+        startedAt
+        status
+        participantCount
         course {
           id
           displayName
         }
+      }
+    }
+    pageInfo {
+      hasNextPage
+      endCursor
+    }
   }
 }
apps/frontend-manage/src/components/courses/LiveQuizList.tsx (1)

1-1: Consider renaming SessionStatus and related constants to align with quiz-based terminology.

The codebase is transitioning from sessions to quizzes, but SessionStatus and sortingOrderSessions still use session-based terminology. This could lead to confusion.

Consider applying these changes:

-import { SessionStatus } from '@klicker-uzh/graphql/dist/ops'
+import { QuizStatus } from '@klicker-uzh/graphql/dist/ops'

-const sortingOrderSessions: Record<string, number> = {
-  [SessionStatus.Running]: 0,
-  [SessionStatus.Scheduled]: 1,
-  [SessionStatus.Prepared]: 2,
-  [SessionStatus.Completed]: 3,
+const sortingOrderQuizzes: Record<string, number> = {
+  [QuizStatus.Running]: 0,
+  [QuizStatus.Scheduled]: 1,
+  [QuizStatus.Prepared]: 2,
+  [QuizStatus.Completed]: 3,
}

Also applies to: 6-11

packages/graphql/src/graphql/ops/QGetSingleCourse.graphql (1)

30-32: LGTM! Consider documenting the feature flags.

The new boolean fields for controlling live quiz features are well-named and align with the PR objectives.

Consider adding GraphQL descriptions to document the purpose and impact of each feature flag. Example:

+  """
+  Controls whether live Q&A functionality is enabled for this quiz
+  """
   isLiveQAEnabled
+  """
+  Controls whether participants can provide confusion feedback
+  """
   isConfusionFeedbackEnabled
+  """
+  Controls whether content moderation is enabled for this quiz
+  """
   isModerationEnabled
apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx (3)

Line range hint 24-36: Simplify cache update logic and strengthen type safety.

The current implementation has redundant null checks and could be simplified:

  1. The type Pick<LiveQuiz, 'id' | 'name'> guarantees that both fields exist
  2. The null check condition might allow empty strings
  3. The null coalescing for empty array is repeated

Consider this more robust implementation:

  cache.writeQuery({
    query: GetUserRunningLiveQuizzesDocument,
    data: {
-     userRunningLiveQuizzes:
-       liveQuiz.id && liveQuiz.name
-         ? [
-             ...(data?.userRunningLiveQuizzes ?? []),
-             { id: liveQuiz.id, name: liveQuiz.name },
-           ]
-         : (data?.userRunningLiveQuizzes ?? []),
+     userRunningLiveQuizzes: [
+       ...(data?.userRunningLiveQuizzes ?? []),
+       { id: liveQuiz.id, name: liveQuiz.name },
+     ],
    },
  })

Line range hint 44-49: Remove unnecessary fallback and improve error handling.

Several improvements can be made to the click handler:

  1. The || '' fallback is unnecessary as id is guaranteed by the type
  2. Error handling should provide feedback to the user

Consider this improved implementation:

  onClick={async () => {
    try {
      await startLiveQuiz({
-       variables: { id: liveQuiz.id || '' },
+       variables: { id: liveQuiz.id },
      })
      router.push(`/sessions/${liveQuiz.id}/cockpit`)
    } catch (error) {
-     console.log(error)
+     // Assuming you have a toast notification system
+     showErrorToast(t('manage.sessions.startError'))
+     console.error('Failed to start live quiz:', error)
    }
  }}

Line range hint 41-65: Add loading state feedback for better UX.

While startingQuiz is tracked, there's no visual feedback to users during the loading state.

Consider adding loading indicators:

  <Button
    basic
    disabled={startingQuiz}
+   loading={startingQuiz}
    onClick={async () => {
      // ... existing code ...
    }}
    className={{ root: 'text-primary-100' }}
    data={{ cy: `start-live-quiz-${liveQuiz.name}` }}
  >
    <Button.Icon>
-     <FontAwesomeIcon icon={faPlay} />
+     <FontAwesomeIcon icon={faPlay} className={startingQuiz ? 'opacity-50' : ''} />
    </Button.Icon>
-   <Button.Label>{t('manage.sessions.startLiveQuiz')}</Button.Label>
+   <Button.Label>
+     {startingQuiz ? t('manage.sessions.starting') : t('manage.sessions.startLiveQuiz')}
+   </Button.Label>
  </Button>
apps/frontend-manage/src/components/sessions/cockpit/SessionAbortionConfirmations.tsx (1)

Rename component and related types to align with quiz-based terminology

The verification confirms the need for renaming to maintain consistency with quiz-based terminology. The changes are localized to two files with clear dependencies:

  • In SessionAbortionConfirmations.tsx:

    • Rename component to LiveQuizAbortionConfirmations
    • Rename props interface to LiveQuizAbortionConfirmationsProps
    • Rename file to LiveQuizAbortionConfirmations.tsx
  • In CancelLiveQuizModal.tsx:

    • Rename type to LiveQuizAbortionConfirmationType
    • Update import statement
    • Update component usage

This change aligns with the modal's existing naming pattern (CancelLiveQuizModal) and the GraphQL operations already using "LiveQuiz" terminology (GetLiveQuizSummary, CancelLiveQuiz).

🔗 Analysis chain

Line range hint 1-101: Consider renaming component for terminology consistency.

For consistency with the new quiz-based terminology, consider renaming:

  • Component from SessionAbortionConfirmations to LiveQuizAbortionConfirmations
  • Type from SessionAbortionConfirmationType to LiveQuizAbortionConfirmationType
  • File from SessionAbortionConfirmations.tsx to LiveQuizAbortionConfirmations.tsx

Let's verify the scope of this rename:


Let me check the CancelLiveQuizModal component to understand the complete context.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for other occurrences of SessionAbortion to assess the impact
# of the suggested rename

echo "Checking for SessionAbortion references:"
rg "SessionAbortion"

Length of output: 1742


Script:

#!/bin/bash
# Check the content of CancelLiveQuizModal to understand the component's context
cat apps/frontend-manage/src/components/sessions/cockpit/CancelLiveQuizModal.tsx

Length of output: 4342

apps/frontend-manage/src/components/sessions/cockpit/CancelLiveQuizModal.tsx (3)

Line range hint 1-20: Consider updating file path and type names for consistency.

The codebase is transitioning from session-based to quiz-based terminology, but some naming inconsistencies remain:

  • The file is still located in the sessions/cockpit directory
  • SessionAbortionConfirmationType interface still uses session-based terminology

Consider:

  1. Moving the file to a more appropriate directory (e.g., quizzes/cockpit)
  2. Renaming the interface to QuizAbortionConfirmationType

Line range hint 57-76: Update loading state variable name for consistency.

The loading state variable sessionDeleting should be renamed to quizDeleting to maintain consistency with the quiz-based terminology.

-  const [cancelLiveQuiz, { loading: sessionDeleting }] = useMutation(
+  const [cancelLiveQuiz, { loading: quizDeleting }] = useMutation(

Line range hint 124-135: Update data attributes and navigation path for consistency.

Several UI elements still use session-based terminology:

  1. The navigation path is still '/sessions'
  2. The data-cy attributes use 'session' in their names
-            router.push('/sessions')
+            router.push('/quizzes')
           setOpen(false)
           setConfirmations({ ...initialConfirmations })
         }}
         className={{
           root: 'bg-red-700 text-white hover:bg-red-800 hover:text-white disabled:bg-opacity-50 disabled:hover:cursor-not-allowed',
         }}
-        data={{ cy: 'confirm-cancel-session' }}
+        data={{ cy: 'confirm-cancel-quiz' }}
apps/frontend-pwa/src/pages/course/[courseId]/liveQuizzes.tsx (2)

35-46: Consider enhancing the loading state with a descriptive message.

While the loading state implementation is good, it could be more informative for users.

Consider updating the loader with a descriptive message:

  if (loading) {
    return (
      <Layout>
-       <Loader />
+       <div className="flex flex-col items-center gap-2">
+         <Loader />
+         <div className="text-sm text-gray-600">
+           {t('pwa.general.loadingLiveQuizzes')}
+         </div>
+       </div>
      </Layout>
    )
  }

Line range hint 123-129: Enhance error handling in getServerSideProps.

The current check might not catch all edge cases.

Consider more specific error handling:

- if (!result?.data?.getCourseRunningLiveQuizzes) {
+ if (!result?.data?.getCourseRunningLiveQuizzes || result.errors) {
    return {
      props: {
        isInactive: true,
+       error: result.errors?.[0]?.message ?? 'Failed to fetch live quizzes',
      },
    }
  }
apps/frontend-manage/src/components/courses/CourseOverviewHeader.tsx (1)

23-26: Consider adding JSDoc documentation for better type clarity.

The interface would benefit from JSDoc documentation explaining the purpose of each omitted field and why they're excluded.

+/**
+ * Props for the CourseOverviewHeader component
+ * @property {Course} course - Course object excluding leaderboard, liveQuizzes,
+ *                            practiceQuizzes, and microLearnings fields
+ */
interface CourseOverviewHeaderProps {
  course: Omit<
    Course,
    'leaderboard' | 'liveQuizzes' | 'practiceQuizzes' | 'microLearnings'
  >
apps/frontend-manage/src/components/sessions/cockpit/SessionTimeline.tsx (3)

320-324: Consider renaming sessionId prop for consistency.

While the modal component has been updated to reflect quiz-based terminology, the sessionId prop name remains unchanged. Consider renaming it to quizId for consistency with the broader refactoring effort.

 <CancelLiveQuizModal
   open={cancelLiveQuizModal}
   setOpen={setCancelLiveQuizModal}
-  sessionId={sessionId}
+  quizId={sessionId}
   title={sessionName}
 />

Line range hint 35-46: Update interface name and props for consistency.

The interface still uses session-based terminology while the codebase is transitioning to quiz-based naming. Consider updating the interface for consistency.

-interface SessionTimelineProps {
+interface QuizTimelineProps {
   blocks?: QuizTimelineBlock[]
-  sessionName: string
+  quizName: string
   handleEndSession: () => void
   handleTogglePublicEvaluation: () => void
   handleOpenBlock: (blockId: number) => void
   handleCloseBlock: (blockId: number) => void
   isEvaluationPublic?: boolean
-  sessionId: string
+  quizId: string
   startedAt?: string
   loading?: boolean
 }

This change would require updating the component's parameter destructuring and usage accordingly.


Line range hint 48-48: Rename component to QuizTimeline.

For consistency with the quiz-based terminology transition, consider renaming the component.

-function SessionTimeline({
+function QuizTimeline({
   sessionId,
   blocks = [],
   sessionName,
   // ... other props
 }: SessionTimelineProps): React.ReactElement {
   // ... component implementation
 }
 
-export default SessionTimeline
+export default QuizTimeline

Also applies to: 334-334

packages/graphql/src/services/liveQuizzes.ts (4)

Line range hint 26-207: Consider splitting the manipulateLiveQuiz function for better maintainability.

The function handles both creation and updates with complex logic. Consider splitting it into smaller, focused functions:

  • validateQuizElements
  • processQuizBlocks
  • createOrUpdateQuiz

This would improve readability and make the code easier to maintain.


Line range hint 322-518: Consider security improvements in quiz execution.

  1. The pin code generation uses Math.random() which isn't cryptographically secure. Consider using crypto.randomInt() instead.
  2. Redis keys should be prefixed with a version to prevent conflicts during deployments.

Apply this diff for secure pin code generation:

- const pinCode = 100000 + Math.floor(Math.random() * 900000)
+ const pinCode = 100000 + crypto.randomInt(900000)

809-811: Address the TODO comment for feedback aggregation performance.

The comment suggests using descending order for early breaking. This would indeed improve performance.

Would you like me to help implement this optimization or create a GitHub issue to track this improvement?


Line range hint 840-935: Improve type safety in student getter functions.

Replace any type with proper interfaces:

- let quizWithoutSolutions: any
+ interface QuizWithoutSolutions {
+   id: string
+   activeBlock?: {
+     elements: Array<{
+       elementData: ElementData
+       // ... other properties
+     }>
+     // ... other properties
+   }
+   // ... other properties
+ }
+ let quizWithoutSolutions: QuizWithoutSolutions
packages/graphql/src/services/sessions.ts (3)

Line range hint 45-303: Consider improving transaction safety in endSession function

The function handles multiple database operations, but not all of them are included in the transaction. This could lead to inconsistent state if some operations fail.

Consider wrapping all database operations (including the final session update) in a single transaction:

- await ctx.prisma.$transaction(promises)
- 
- const keys = await ctx.redisExec.keys(`s:${id}:*`)
- const pipe = ctx.redisExec.multi()
- for (const key of keys) {
-   pipe.unlink(key)
- }
- await pipe.exec()
- 
- const stoppedSession = await ctx.prisma.liveSession.update({
+ const stoppedSession = await ctx.prisma.$transaction(async (tx) => {
+   await Promise.all(promises)
+   
+   const keys = await ctx.redisExec.keys(`s:${id}:*`)
+   const pipe = ctx.redisExec.multi()
+   for (const key of keys) {
+     pipe.unlink(key)
+   }
+   await pipe.exec()
+   
+   return tx.liveSession.update({
    where: {
      id,
    },
    data: {
      status: SessionStatus.COMPLETED,
      finishedAt: new Date(),
      pinCode: null,
    },
+   })
  })

Line range hint 892-939: Optimize statistics computation performance

The computeStatistics function performs expensive calculations that could be optimized:

  1. Results could be memoized to prevent recalculation
  2. Edge cases could be handled more efficiently

Consider implementing memoization:

+ const memoizedResults = new Map()
+ 
  function computeStatistics(instance: PickedInstanceType) {
+   const cacheKey = JSON.stringify(instance.results)
+   if (memoizedResults.has(cacheKey)) {
+     return memoizedResults.get(cacheKey)
+   }
    
    // ... existing computation ...
    
+   memoizedResults.set(cacheKey, instance)
    return instance
  }

Line range hint 941-1108: Enhance HMAC validation security

The HMAC validation in getSessionEvaluation could be improved by using a timing-safe comparison to prevent timing attacks.

Consider using the crypto module's timingSafeEqual:

+ import { timingSafeEqual } from 'crypto'

  if (typeof hmac === 'string') {
    const hmacEncoder = createHmac('sha256', process.env.APP_SECRET as string)
    hmacEncoder.update(session.namespace + session.id)
    const sessionHmac = hmacEncoder.digest('hex')
-   if (sessionHmac !== hmac) {
+   if (!timingSafeEqual(Buffer.from(sessionHmac), Buffer.from(hmac))) {
      return null
    }
  }
apps/frontend-manage/src/components/courses/LiveQuizElement.tsx (2)

119-120: Update translation key to reflect 'live quiz' terminology

The translation key manage.sessions.nBlocksQuestions uses 'sessions' instead of 'liveQuizzes', which may lead to confusion given the refactoring from sessions to live quizzes. Consider updating the translation key to reflect the new terminology.


119-120: Consistently update text and translation keys from 'session' to 'live quiz'

There are instances where text and translation keys still reference 'session' instead of 'live quiz', which could cause confusion. For consistency, consider updating these references.

Examples:

  • Line 119-120: t('manage.sessions.nBlocksQuestions'
  • Line 143: getAccessLink uses quiz.name, ensure the associated text reflects 'live quiz'.
  • Line 150: getLTIAccessLink uses quiz.name, check the text for consistency.
  • Line 248: Deletion confirmation references 'deleteSession' in the translation key.
  • Line 292: The DeletionModal uses t('manage.sessions.deleteLiveQuiz'), ensure all related text and keys are updated accordingly.

Also applies to: 143-143, 150-150, 248-248, 292-292

packages/graphql/src/services/courses.ts (1)

712-720: Consider optimizing data retrieval for liveQuizzes.

In the include clause for liveQuizzes, you're fetching blocks and counting elements within each block. If not all of this data is necessary for your use case, consider selecting only the fields you need to improve query performance and reduce data transfer.

🛑 Comments failed to post (9)
packages/graphql/src/graphql/ops/QGetSingleCourse.graphql (1)

25-25: 💡 Codebase verification

Inconsistent renaming of 'sessions' to 'liveQuizzes' detected

The renaming is not consistently applied across the codebase. Found remaining instances of 'sessions':

  • packages/graphql/src/graphql/ops/QGetControlCourse.graphql: Still using 'sessions'
  • packages/graphql/src/public/schema.graphql: Contains both 'sessions' and 'liveQuizzes' fields

This inconsistency could lead to breaking changes and schema conflicts. The transition from sessions to liveQuizzes should be applied uniformly across all related GraphQL operations and schema definitions.

🔗 Analysis chain

Verify consistent renaming of 'sessions' to 'liveQuizzes' across the codebase.

The field renaming aligns with the transition to quiz-based logic.

Let's verify the consistency of this change:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining references to 'sessions' that should have been renamed
# Expect: No occurrences of old terminology in GraphQL files

# Search for 'sessions' in GraphQL files
echo "Checking for 'sessions' references in GraphQL files..."
rg -t graphql 'sessions' 

# Search for potential GraphQL query/mutation references in TypeScript/JavaScript files
echo "Checking for GetSingleCourse query usage..."
rg 'GetSingleCourse.*sessions'

Length of output: 426


Script:

#!/bin/bash
# Let's try a different approach with proper file type specifications

# Check for 'sessions' references in .graphql files
echo "Checking for 'sessions' in .graphql files..."
rg "sessions" --type-add 'graphql:*.graphql' -t graphql

# Check for 'GetSingleCourse' query usage with 'sessions'
echo "Checking for GetSingleCourse query usage with 'sessions'..."
rg "GetSingleCourse.*sessions" -g "*.{ts,tsx,js,jsx}"

# Check for 'liveQuizzes' to confirm the new terminology
echo "Checking for 'liveQuizzes' in .graphql files..."
rg "liveQuizzes" --type-add 'graphql:*.graphql' -t graphql

Length of output: 884

apps/frontend-manage/src/components/sessions/cockpit/CancelLiveQuizModal.tsx (1)

21-31: 🛠️ Refactor suggestion

Update prop name for consistency with quiz-based terminology.

The prop name sessionId should be updated to quizId to maintain consistency with the quiz-based terminology used throughout the codebase.

 function CancelLiveQuizModal({
-  sessionId,
+  quizId,
   title,
   open,
   setOpen,
 }: {
-  sessionId: string
+  quizId: string
   title: string
   open: boolean
   setOpen: (value: boolean) => void
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

function CancelLiveQuizModal({
  quizId,
  title,
  open,
  setOpen,
}: {
  quizId: string
  title: string
  open: boolean
  setOpen: (value: boolean) => void
}) {
apps/frontend-pwa/src/pages/course/[courseId]/liveQuizzes.tsx (2)

72-72: ⚠️ Potential issue

Fix inconsistent null checking for course data.

There's inconsistent handling of the course object between the Layout component and the course name display.

Apply consistent optional chaining:

- <Layout course={data.getCourseRunningLiveQuizzes[0].course ?? undefined}>
+ <Layout course={data.getCourseRunningLiveQuizzes[0].course}>

Also applies to: 77-77


48-52: 🛠️ Refactor suggestion

Simplify redundant conditions.

The conditions for checking empty quiz list are redundant.

Simplify the conditions:

  if (
    isInactive ||
    !data ||
-   !data.getCourseRunningLiveQuizzes?.length ||
-   data.getCourseRunningLiveQuizzes.length === 0
+   !data.getCourseRunningLiveQuizzes?.length
  ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  if (
    isInactive ||
    !data ||
    !data.getCourseRunningLiveQuizzes?.length
apps/frontend-manage/src/pages/courses/[id].tsx (1)

162-179: 🛠️ Refactor suggestion

Update translation key to maintain consistency.

While the tab configuration is correctly updated for the quiz-based workflow, the translation key t('manage.general.sessions') still references "sessions". This should be updated to maintain consistency with the new quiz-based terminology.

Consider updating the translation key to reflect the new terminology:

-label={t('manage.general.sessions')}
+label={t('manage.general.liveQuizzes')}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

          defaultValue="liveQuizzes"
          value={tabValue}
          onValueChange={(newValue: string) => setTabValue(newValue)}
          className={{ root: 'flex-1 basis-2/3' }}
        >
          <Tabs.TabList>
            <Tabs.Tab
              key="tab-liveQuizzes"
              value="liveQuizzes"
              label={t('manage.general.liveQuizzes')}
              className={{
                root: 'border border-solid',
                label: twMerge(
                  'whitespace-nowrap text-base',
                  tabValue === 'liveQuizzes' && 'font-bold'
                ),
              }}
              data={{ cy: 'tab-liveQuizzes' }}
packages/graphql/src/services/liveQuizzes.ts (1)

519-787: ⚠️ Potential issue

Address potential race condition in cancelLiveQuiz.

The Redis cleanup and database updates are not atomic. Consider using Redis MULTI/EXEC to ensure atomicity:

Apply this diff:

- const keys = await ctx.redisExec.keys(`s:${id}:*`)
- const pipe = ctx.redisExec.multi()
- for (const key of keys) {
-   pipe.unlink(key)
- }
- await pipe.exec()
+ await ctx.redisExec
+   .multi()
+   .keys(`s:${id}:*`)
+   .exec((err, results) => {
+     if (err) throw err
+     const keys = results[0]
+     if (keys.length > 0) {
+       return ctx.redisExec.unlink(...keys)
+     }
+   })

Committable suggestion skipped: line range outside the PR's diff.

apps/frontend-manage/src/components/courses/LiveQuizElement.tsx (2)

127-128: ⚠️ Potential issue

Handle PublicationStatus.Graded in conditional rendering

The component currently does not handle the PublicationStatus.Graded status in the conditional rendering logic. To ensure that quizzes with the 'Graded' status are properly managed, consider adding conditions to handle this status and render appropriate UI elements.

For example, you can update the conditions to include PublicationStatus.Graded where appropriate.

-              {(quiz.status === PublicationStatus.Scheduled ||
-                quiz.status === PublicationStatus.Draft) && (
+              {(quiz.status === PublicationStatus.Scheduled ||
+                quiz.status === PublicationStatus.Draft ||
+                quiz.status === PublicationStatus.Graded) && (

And similarly for other status checks:

-              {quiz.status === PublicationStatus.Ended && (
+              {(quiz.status === PublicationStatus.Ended ||
+                quiz.status === PublicationStatus.Graded) && (

Ensure that the UI renders correctly for quizzes with the 'Graded' status.

Also applies to: 192-192, 229-229


104-104: 🛠️ Refactor suggestion

Update data-cy attribute to use 'live-quiz' instead of 'session'

The data-cy attribute is currently using 'session', which is inconsistent with the updated terminology 'live-quiz'. For consistency and clarity, consider updating 'session' to 'live-quiz'.

Apply this diff to fix the inconsistency:

-          data-cy={`session-${quiz.name}`}
+          data-cy={`live-quiz-${quiz.name}`}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      data-cy={`live-quiz-${quiz.name}`}
packages/graphql/src/services/courses.ts (1)

793-798: ⚠️ Potential issue

Add null checks to prevent runtime errors when mapping liveQuizzes.

When mapping over course?.liveQuizzes, ensure that session.blocks and block._count are defined. Without null checks, there's a risk of encountering TypeError if any of these are null or undefined.

Apply this diff to include safe navigation and default values:

 const reducedLiveQuizzes = course?.liveQuizzes.map((session) => {
   return {
     ...session,
     numOfBlocks: session.blocks?.length ?? 0,
     numOfInstances: session.blocks?.reduce(
       (acc, block) => acc + (block._count?.elements ?? 0),
       0
     ),
   }
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  const reducedLiveQuizzes = course?.liveQuizzes.map((session) => {
    return {
      ...session,
      numOfBlocks: session.blocks?.length ?? 0,
      numOfInstances: session.blocks?.reduce(
        (acc, block) => acc + (block._count?.elements ?? 0),
        0
      ),
    }
  })

Copy link

sonarcloud bot commented Nov 4, 2024

@sjschlapbach sjschlapbach merged commit 6ce6287 into v3-new-live-quiz Nov 4, 2024
12 of 15 checks passed
@sjschlapbach sjschlapbach deleted the live-quiz-interaction branch November 4, 2024 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

1 participant