diff --git a/src/commons/achievement/AchievementOverview.tsx b/src/commons/achievement/AchievementOverview.tsx index 0bcbee3b35..d902c3e447 100644 --- a/src/commons/achievement/AchievementOverview.tsx +++ b/src/commons/achievement/AchievementOverview.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { AchievementUser } from 'src/features/achievement/AchievementTypes'; -import { FETCH_TOTAL_XP, FETCH_TOTAL_XP_ADMIN } from '../application/types/SessionTypes'; +import { fetchTotalXp, fetchTotalXpAdmin } from '../application/actions/SessionActions'; import { useTypedSelector } from '../utils/Hooks'; import AchievementLevel from './overview/AchievementLevel'; @@ -20,9 +20,9 @@ const AchievementOverview: React.FC = ({ name, userState }) => { useEffect(() => { // If user is student, fetch assessment details from assessment route instead, as seen below if (crid && crid !== userCrid) { - dispatch({ type: FETCH_TOTAL_XP_ADMIN, payload: crid }); + dispatch(fetchTotalXpAdmin(crid)); } else { - dispatch({ type: FETCH_TOTAL_XP }); + dispatch(fetchTotalXp()); } }, [crid, userCrid, dispatch]); diff --git a/src/commons/achievement/AchievementView.tsx b/src/commons/achievement/AchievementView.tsx index 425027d7e2..7644a40989 100644 --- a/src/commons/achievement/AchievementView.tsx +++ b/src/commons/achievement/AchievementView.tsx @@ -9,8 +9,12 @@ import { getAbilityGlow } from '../../features/achievement/AchievementConstants'; import { AchievementStatus, AchievementUser } from '../../features/achievement/AchievementTypes'; -import { FETCH_ASSESSMENT, FETCH_ASSESSMENT_ADMIN } from '../application/types/SessionTypes'; -import { Assessment, FETCH_ASSESSMENT_OVERVIEWS } from '../assessment/AssessmentTypes'; +import { + fetchAssessment, + fetchAssessmentAdmin, + fetchAssessmentOverviews +} from '../application/actions/SessionActions'; +import { Assessment } from '../assessment/AssessmentTypes'; import { useTypedSelector } from '../utils/Hooks'; import AchievementCommentCard from './AchievementCommentCard'; import { prettifyDate } from './utils/DateHelper'; @@ -36,16 +40,17 @@ const AchievementView: React.FC = ({ focusUuid, userState }) => { const dispatch = useDispatch(); useEffect(() => { - dispatch({ type: FETCH_ASSESSMENT_OVERVIEWS }); + dispatch(fetchAssessmentOverviews()); if (!assessmentId) { return; } if (isAdminView) { // Fetch selected user's assessment from admin route - dispatch({ type: FETCH_ASSESSMENT_ADMIN, payload: { assessmentId, courseRegId } }); + // Safe to use non-null assertion (refer to `isAdminView` declaration above) + dispatch(fetchAssessmentAdmin(assessmentId, courseRegId!)); } else { // If user is student, fetch assessment details from assessment route instead, as seen below - dispatch({ type: FETCH_ASSESSMENT, payload: { assessmentId } }); + dispatch(fetchAssessment(assessmentId)); } }, [dispatch, assessmentId, courseRegId, isAdminView]); diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 85b6098adc..1292c661aa 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -1,4 +1,5 @@ import { createAction } from '@reduxjs/toolkit'; +import { createActions } from 'src/commons/redux/utils'; import { paginationToBackendParams, unpublishedToBackendParams @@ -20,247 +21,134 @@ import { import { generateOctokitInstance } from '../../utils/GitHubPersistenceHelper'; import { Role } from '../ApplicationTypes'; import { - ACKNOWLEDGE_NOTIFICATIONS, AdminPanelCourseRegistration, - BULK_UPLOAD_TEAM, - CHECK_ANSWER_LAST_MODIFIED_AT, CourseRegistration, - CREATE_TEAM, - DELETE_ASSESSMENT_CONFIG, - DELETE_TEAM, - DELETE_TIME_OPTIONS, - DELETE_USER_COURSE_REGISTRATION, - FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, - FETCH_ASSESSMENT, - FETCH_ASSESSMENT_ADMIN, - FETCH_ASSESSMENT_CONFIGS, - FETCH_ASSESSMENT_OVERVIEWS, - FETCH_AUTH, - FETCH_CONFIGURABLE_NOTIFICATION_CONFIGS, - FETCH_COURSE_CONFIG, - FETCH_GRADING, - FETCH_GRADING_OVERVIEWS, - FETCH_NOTIFICATION_CONFIGS, - FETCH_NOTIFICATIONS, - FETCH_STUDENTS, - FETCH_TEAM_FORMATION_OVERVIEW, - FETCH_TEAM_FORMATION_OVERVIEWS, - FETCH_TOTAL_XP, - FETCH_TOTAL_XP_ADMIN, - FETCH_USER_AND_COURSE, - LOGIN, - LOGIN_GITHUB, - LOGOUT_GITHUB, - LOGOUT_GOOGLE, NotificationConfiguration, NotificationPreference, - PUBLISH_GRADING, - REAUTOGRADE_ANSWER, - REAUTOGRADE_SUBMISSION, - REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, - SET_ADMIN_PANEL_COURSE_REGISTRATIONS, - SET_ASSESSMENT_CONFIGURATIONS, - SET_CONFIGURABLE_NOTIFICATION_CONFIGS, - SET_COURSE_CONFIGURATION, - SET_COURSE_REGISTRATION, - SET_GITHUB_ACCESS_TOKEN, - SET_GITHUB_OCTOKIT_OBJECT, - SET_GOOGLE_USER, - SET_NOTIFICATION_CONFIGS, - SET_TOKENS, - SET_USER, - SUBMIT_ANSWER, - SUBMIT_ASSESSMENT, - SUBMIT_GRADING, - SUBMIT_GRADING_AND_CONTINUE, TimeOption, Tokens, - UNPUBLISH_GRADING, - UNSUBMIT_SUBMISSION, UPDATE_ASSESSMENT, - UPDATE_ASSESSMENT_CONFIGS, - UPDATE_ASSESSMENT_OVERVIEWS, - UPDATE_COURSE_CONFIG, UPDATE_COURSE_RESEARCH_AGREEMENT, - UPDATE_GRADING, - UPDATE_GRADING_OVERVIEWS, - UPDATE_LATEST_VIEWED_COURSE, - UPDATE_NOTIFICATION_CONFIG, - UPDATE_NOTIFICATION_PREFERENCES, - UPDATE_NOTIFICATIONS, - UPDATE_STUDENTS, - UPDATE_TEAM, - UPDATE_TEAM_FORMATION_OVERVIEW, - UPDATE_TEAM_FORMATION_OVERVIEWS, - UPDATE_TIME_OPTIONS, UPDATE_TOTAL_XP, - UPDATE_USER_ROLE, UpdateCourseConfiguration, User } from '../types/SessionTypes'; -export const fetchAuth = createAction(FETCH_AUTH, (code: string, providerId?: string) => ({ - payload: { code, providerId } -})); - -export const fetchUserAndCourse = createAction(FETCH_USER_AND_COURSE, () => ({ payload: {} })); - -export const fetchCourseConfig = createAction(FETCH_COURSE_CONFIG, () => ({ payload: {} })); - -export const fetchAssessment = createAction( - FETCH_ASSESSMENT, - (assessmentId: number, assessmentPassword?: string) => ({ - payload: { assessmentId, assessmentPassword } - }) -); - -export const fetchAssessmentAdmin = createAction( - FETCH_ASSESSMENT_ADMIN, - (assessmentId: number, courseRegId: number) => ({ payload: { assessmentId, courseRegId } }) -); - -export const fetchAssessmentOverviews = createAction(FETCH_ASSESSMENT_OVERVIEWS, () => ({ - payload: {} -})); - -export const fetchTotalXp = createAction(FETCH_TOTAL_XP, () => ({ payload: {} })); - -export const fetchTotalXpAdmin = createAction(FETCH_TOTAL_XP_ADMIN, (courseRegId: number) => ({ - payload: courseRegId -})); - -export const fetchGrading = createAction(FETCH_GRADING, (submissionId: number) => ({ - payload: submissionId -})); - -/** - * @param filterToGroup - param that when set to true, only shows submissions under the group - * of the grader - * @param publishedFilter - backend params to filter to unpublished - * @param pageParams - param that contains offset and pageSize, informing backend about how - * many entries, starting from what offset, to get - * @param filterParams - param that contains columnFilters converted into JSON for - * processing into query parameters - */ -export const fetchGradingOverviews = createAction( - FETCH_GRADING_OVERVIEWS, - ( +const newActions = createActions('session', { + fetchAuth: (code: string, providerId?: string) => ({ code, providerId }), + fetchUserAndCourse: () => ({}), + fetchCourseConfig: () => ({}), + fetchAssessment: (assessmentId: number, assessmentPassword?: string) => ({ + assessmentId, + assessmentPassword + }), + fetchAssessmentAdmin: (assessmentId: number, courseRegId: number) => ({ + assessmentId, + courseRegId + }), + fetchAssessmentOverviews: () => ({}), + fetchTotalXp: () => ({}), + fetchTotalXpAdmin: (courseRegId: number) => courseRegId, + fetchGrading: (submissionId: number) => submissionId, + /** + * @param filterToGroup - param that when set to true, only shows submissions under the group + * of the grader + * @param publishedFilter - backend params to filter to unpublished + * @param pageParams - param that contains offset and pageSize, informing backend about how + * many entries, starting from what offset, to get + * @param filterParams - param that contains columnFilters converted into JSON for + * processing into query parameters + */ + fetchGradingOverviews: ( filterToGroup = true, publishedFilter = unpublishedToBackendParams(false), pageParams = paginationToBackendParams(0, 10), filterParams = {} - ) => ({ payload: { filterToGroup, publishedFilter, pageParams, filterParams } }) -); - -export const fetchTeamFormationOverviews = createAction( - FETCH_TEAM_FORMATION_OVERVIEWS, - (filterToGroup = true) => ({ payload: filterToGroup }) -); - -export const fetchStudents = createAction(FETCH_STUDENTS, () => ({ payload: {} })); - -export const login = createAction(LOGIN, (providerId: string) => ({ payload: providerId })); - -export const logoutGoogle = createAction(LOGOUT_GOOGLE, () => ({ payload: {} })); - -export const loginGitHub = createAction(LOGIN_GITHUB, () => ({ payload: {} })); - -export const logoutGitHub = createAction(LOGOUT_GITHUB, () => ({ payload: {} })); - -export const setTokens = createAction(SET_TOKENS, ({ accessToken, refreshToken }: Tokens) => ({ - payload: { accessToken, refreshToken } -})); - -export const setUser = createAction(SET_USER, (user: User) => ({ payload: user })); - -export const setCourseConfiguration = createAction( - SET_COURSE_CONFIGURATION, - (courseConfiguration: UpdateCourseConfiguration) => ({ payload: courseConfiguration }) -); - -export const setCourseRegistration = createAction( - SET_COURSE_REGISTRATION, - (courseRegistration: Partial) => ({ payload: courseRegistration }) -); - -export const setAssessmentConfigurations = createAction( - SET_ASSESSMENT_CONFIGURATIONS, - (assessmentConfigurations: AssessmentConfiguration[]) => ({ payload: assessmentConfigurations }) -); - -export const setConfigurableNotificationConfigs = createAction( - SET_CONFIGURABLE_NOTIFICATION_CONFIGS, - (notificationConfigs: NotificationConfiguration[]) => ({ payload: notificationConfigs }) -); - -export const setNotificationConfigs = createAction( - SET_NOTIFICATION_CONFIGS, - (notificationConfigs: NotificationConfiguration[]) => ({ payload: notificationConfigs }) -); - -export const setAdminPanelCourseRegistrations = createAction( - SET_ADMIN_PANEL_COURSE_REGISTRATIONS, - (courseRegistrations: AdminPanelCourseRegistration[]) => ({ payload: courseRegistrations }) -); - -export const setGoogleUser = createAction(SET_GOOGLE_USER, (user?: string) => ({ payload: user })); - -export const setGitHubOctokitObject = createAction( - SET_GITHUB_OCTOKIT_OBJECT, - (authToken?: string) => ({ payload: generateOctokitInstance(authToken || '') }) -); - -export const setGitHubAccessToken = createAction(SET_GITHUB_ACCESS_TOKEN, (authToken?: string) => ({ - payload: authToken -})); - -export const removeGitHubOctokitObjectAndAccessToken = createAction( - REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, - () => ({ payload: {} }) -); - -export const submitAnswer = createAction( - SUBMIT_ANSWER, - (id: number, answer: string | number | ContestEntry[]) => ({ payload: { id, answer } }) -); - -export const checkAnswerLastModifiedAt = createAction( - CHECK_ANSWER_LAST_MODIFIED_AT, - (id: number, lastModifiedAt: string, saveAnswer: Function) => ({ - payload: { id, lastModifiedAt, saveAnswer } - }) -); - -export const submitAssessment = createAction(SUBMIT_ASSESSMENT, (id: number) => ({ payload: id })); - -export const submitGrading = createAction( - SUBMIT_GRADING, - (submissionId: number, questionId: number, xpAdjustment: number = 0, comments?: string) => ({ - payload: { submissionId, questionId, xpAdjustment, comments } - }) -); - -export const submitGradingAndContinue = createAction( - SUBMIT_GRADING_AND_CONTINUE, - (submissionId: number, questionId: number, xpAdjustment: number = 0, comments?: string) => ({ - payload: { submissionId, questionId, xpAdjustment, comments } - }) -); - -export const reautogradeSubmission = createAction( - REAUTOGRADE_SUBMISSION, - (submissionId: number) => ({ payload: submissionId }) -); - -export const reautogradeAnswer = createAction( - REAUTOGRADE_ANSWER, - (submissionId: number, questionId: number) => ({ payload: { submissionId, questionId } }) -); - -export const updateAssessmentOverviews = createAction( - UPDATE_ASSESSMENT_OVERVIEWS, - (overviews: AssessmentOverview[]) => ({ payload: overviews }) -); + ) => ({ filterToGroup, publishedFilter, pageParams, filterParams }), + fetchTeamFormationOverviews: (filterToGroup = true) => filterToGroup, + fetchStudents: () => ({}), + login: (providerId: string) => providerId, + logoutGoogle: () => ({}), + loginGitHub: () => ({}), + logoutGitHub: () => ({}), + setTokens: ({ accessToken, refreshToken }: Tokens) => ({ accessToken, refreshToken }), + setUser: (user: User) => user, + setCourseConfiguration: (courseConfiguration: UpdateCourseConfiguration) => courseConfiguration, + setCourseRegistration: (courseRegistration: Partial) => courseRegistration, + setAssessmentConfigurations: (assessmentConfigurations: AssessmentConfiguration[]) => + assessmentConfigurations, + setConfigurableNotificationConfigs: (notificationConfigs: NotificationConfiguration[]) => + notificationConfigs, + setNotificationConfigs: (notificationConfigs: NotificationConfiguration[]) => notificationConfigs, + setAdminPanelCourseRegistrations: (courseRegistrations: AdminPanelCourseRegistration[]) => + courseRegistrations, + setGoogleUser: (user?: string) => user, + setGitHubOctokitObject: (authToken?: string) => generateOctokitInstance(authToken || ''), + setGitHubAccessToken: (authToken?: string) => authToken, + removeGitHubOctokitObjectAndAccessToken: () => ({}), + submitAnswer: (id: number, answer: string | number | ContestEntry[]) => ({ id, answer }), + checkAnswerLastModifiedAt: (id: number, lastModifiedAt: string, saveAnswer: Function) => ({ + id, + lastModifiedAt, + saveAnswer + }), + submitAssessment: (id: number) => id, + submitGrading: ( + submissionId: number, + questionId: number, + xpAdjustment: number = 0, + comments?: string + ) => ({ submissionId, questionId, xpAdjustment, comments }), + submitGradingAndContinue: ( + submissionId: number, + questionId: number, + xpAdjustment: number = 0, + comments?: string + ) => ({ submissionId, questionId, xpAdjustment, comments }), + reautogradeSubmission: (submissionId: number) => submissionId, + reautogradeAnswer: (submissionId: number, questionId: number) => ({ submissionId, questionId }), + updateAssessmentOverviews: (overviews: AssessmentOverview[]) => overviews +}); + +// For compatibility with existing code (reducer) +export const { + fetchAuth, + fetchUserAndCourse, + fetchCourseConfig, + fetchAssessment, + fetchAssessmentAdmin, + fetchAssessmentOverviews, + fetchTotalXp, + fetchTotalXpAdmin, + fetchGrading, + fetchGradingOverviews, + fetchTeamFormationOverviews, + fetchStudents, + login, + logoutGoogle, + loginGitHub, + logoutGitHub, + setTokens, + setUser, + setCourseConfiguration, + setCourseRegistration, + setAssessmentConfigurations, + setConfigurableNotificationConfigs, + setNotificationConfigs, + setAdminPanelCourseRegistrations, + setGoogleUser, + setGitHubOctokitObject, + setGitHubAccessToken, + removeGitHubOctokitObjectAndAccessToken, + submitAnswer, + checkAnswerLastModifiedAt, + submitAssessment, + submitGrading, + submitGradingAndContinue, + reautogradeSubmission, + reautogradeAnswer, + updateAssessmentOverviews +} = newActions; export const updateTotalXp = createAction(UPDATE_TOTAL_XP, (totalXp: number) => ({ payload: totalXp @@ -270,160 +158,101 @@ export const updateAssessment = createAction(UPDATE_ASSESSMENT, (assessment: Ass payload: assessment })); -export const updateGradingOverviews = createAction( - UPDATE_GRADING_OVERVIEWS, - (overviews: GradingOverviews) => ({ payload: overviews }) -); - -export const fetchTeamFormationOverview = createAction( - FETCH_TEAM_FORMATION_OVERVIEW, - (assessmentId: number) => ({ payload: { assessmentId } }) -); - -export const createTeam = createAction( - CREATE_TEAM, - (assessment: AssessmentOverview, teams: OptionType[][]) => ({ payload: { assessment, teams } }) -); - -export const updateTeam = createAction( - UPDATE_TEAM, - (teamId: number, assessment: AssessmentOverview, teams: OptionType[][]) => ({ - payload: { teamId, assessment, teams } - }) -); - -export const deleteTeam = createAction(DELETE_TEAM, (teamId: number) => ({ payload: { teamId } })); - -export const bulkUploadTeam = createAction( - BULK_UPLOAD_TEAM, - (assessment: AssessmentOverview, file: File, students: User[] | undefined) => ({ - payload: { assessment, file, students } - }) -); - -export const updateTeamFormationOverviews = createAction( - UPDATE_TEAM_FORMATION_OVERVIEWS, - (overviews: TeamFormationOverview[]) => ({ payload: overviews }) -); - -export const updateTeamFormationOverview = createAction( - UPDATE_TEAM_FORMATION_OVERVIEW, - (overview: TeamFormationOverview) => ({ payload: overview }) -); - -export const updateStudents = createAction(UPDATE_STUDENTS, (students: User[]) => ({ - payload: students -})); - -/** - * An extra id parameter is included here because of - * no id for Grading. - */ -export const updateGrading = createAction( - UPDATE_GRADING, - (submissionId: number, grading: GradingQuery) => ({ payload: { submissionId, grading } }) -); - -export const unsubmitSubmission = createAction(UNSUBMIT_SUBMISSION, (submissionId: number) => ({ - payload: { submissionId } -})); - -/** - * Publishing / unpublishing actions - */ - -export const publishGrading = createAction(PUBLISH_GRADING, (submissionId: number) => ({ - payload: { submissionId } -})); - -export const unpublishGrading = createAction(UNPUBLISH_GRADING, (submissionId: number) => ({ - payload: { submissionId } -})); - -/** - * Notification actions - */ - -export const fetchNotifications = createAction(FETCH_NOTIFICATIONS, () => ({ payload: {} })); - -export const acknowledgeNotifications = createAction( - ACKNOWLEDGE_NOTIFICATIONS, - (withFilter?: NotificationFilterFunction) => ({ payload: { withFilter } }) -); - -export const updateNotifications = createAction( - UPDATE_NOTIFICATIONS, - (notifications: Notification[]) => ({ payload: notifications }) -); - -export const updateLatestViewedCourse = createAction( - UPDATE_LATEST_VIEWED_COURSE, - (courseId: number) => ({ payload: { courseId } }) -); - -export const updateCourseConfig = createAction( - UPDATE_COURSE_CONFIG, - (courseConfiguration: UpdateCourseConfiguration) => ({ payload: courseConfiguration }) -); - -export const fetchAssessmentConfigs = createAction(FETCH_ASSESSMENT_CONFIGS, () => ({ - payload: {} -})); - -export const updateAssessmentConfigs = createAction( - UPDATE_ASSESSMENT_CONFIGS, - (assessmentConfigs: AssessmentConfiguration[]) => ({ payload: assessmentConfigs }) -); - -export const updateNotificationConfigs = createAction( - UPDATE_NOTIFICATION_CONFIG, - (notificationConfigs: NotificationConfiguration[]) => ({ payload: notificationConfigs }) -); - -export const updateNotificationPreferences = createAction( - UPDATE_NOTIFICATION_PREFERENCES, - (notificationPreferences: NotificationPreference[], courseRegId: number) => ({ - payload: { notificationPreferences, courseRegId } - }) -); - -export const deleteAssessmentConfig = createAction( - DELETE_ASSESSMENT_CONFIG, - (assessmentConfig: AssessmentConfiguration) => ({ payload: assessmentConfig }) -); - -export const fetchAdminPanelCourseRegistrations = createAction( - FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, - () => ({ payload: {} }) -); - -export const fetchConfigurableNotificationConfigs = createAction( - FETCH_CONFIGURABLE_NOTIFICATION_CONFIGS, - (courseRegId: number) => ({ payload: { courseRegId } }) -); - -export const fetchNotificationConfigs = createAction(FETCH_NOTIFICATION_CONFIGS, () => ({ - payload: {} -})); - -export const updateTimeOptions = createAction(UPDATE_TIME_OPTIONS, (timeOptions: TimeOption[]) => ({ - payload: timeOptions -})); - -export const deleteTimeOptions = createAction(DELETE_TIME_OPTIONS, (timeOptionIds: number[]) => ({ - payload: timeOptionIds -})); - -export const updateUserRole = createAction(UPDATE_USER_ROLE, (courseRegId: number, role: Role) => ({ - payload: { courseRegId, role } -})); - -export const deleteUserCourseRegistration = createAction( - DELETE_USER_COURSE_REGISTRATION, - (courseRegId: number) => ({ payload: { courseRegId } }) -); +const newActions2 = createActions('session', { + updateGradingOverviews: (overviews: GradingOverviews) => overviews, + fetchTeamFormationOverview: (assessmentId: number) => ({ assessmentId }), + createTeam: (assessment: AssessmentOverview, teams: OptionType[][]) => ({ assessment, teams }), + updateTeam: (teamId: number, assessment: AssessmentOverview, teams: OptionType[][]) => ({ + teamId, + assessment, + teams + }), + deleteTeam: (teamId: number) => ({ teamId }), + bulkUploadTeam: (assessment: AssessmentOverview, file: File, students: User[] | undefined) => ({ + assessment, + file, + students + }), + updateTeamFormationOverviews: (overviews: TeamFormationOverview[]) => overviews, + updateTeamFormationOverview: (overview: TeamFormationOverview) => overview, + updateStudents: (students: User[]) => students, + /** + * An extra id parameter is included here because of + * no id for Grading. + */ + updateGrading: (submissionId: number, grading: GradingQuery) => ({ submissionId, grading }), + unsubmitSubmission: (submissionId: number) => ({ submissionId }), + // Publishing / unpublishing actions + publishGrading: (submissionId: number) => ({ submissionId }), + unpublishGrading: (submissionId: number) => ({ submissionId }), + // Notification actions + fetchNotifications: () => ({}), + acknowledgeNotifications: (withFilter?: NotificationFilterFunction) => ({ withFilter }), + updateNotifications: (notifications: Notification[]) => notifications, + updateLatestViewedCourse: (courseId: number) => ({ courseId }), + updateCourseConfig: (courseConfiguration: UpdateCourseConfiguration) => courseConfiguration, + fetchAssessmentConfigs: () => ({}), + updateAssessmentConfigs: (assessmentConfigs: AssessmentConfiguration[]) => assessmentConfigs, + updateNotificationConfigs: (notificationConfigs: NotificationConfiguration[]) => + notificationConfigs, + updateNotificationPreferences: ( + notificationPreferences: NotificationPreference[], + courseRegId: number + ) => ({ notificationPreferences, courseRegId }), + deleteAssessmentConfig: (assessmentConfig: AssessmentConfiguration) => assessmentConfig, + fetchAdminPanelCourseRegistrations: () => ({}), + fetchConfigurableNotificationConfigs: (courseRegId: number) => ({ courseRegId }), + fetchNotificationConfigs: () => ({}), + updateTimeOptions: (timeOptions: TimeOption[]) => timeOptions, + deleteTimeOptions: (timeOptionIds: number[]) => timeOptionIds, + updateUserRole: (courseRegId: number, role: Role) => ({ courseRegId, role }), + deleteUserCourseRegistration: (courseRegId: number) => ({ courseRegId }) +}); export const updateCourseResearchAgreement = createAction( UPDATE_COURSE_RESEARCH_AGREEMENT, (agreedToResearch: boolean) => ({ payload: { agreedToResearch } }) ); + +// For compatibility with existing code (reducer) +export const { + updateGradingOverviews, + fetchTeamFormationOverview, + createTeam, + updateTeam, + deleteTeam, + bulkUploadTeam, + updateTeamFormationOverviews, + updateTeamFormationOverview, + updateStudents, + updateGrading, + unsubmitSubmission, + publishGrading, + unpublishGrading, + fetchNotifications, + acknowledgeNotifications, + updateNotifications, + updateLatestViewedCourse, + updateCourseConfig, + fetchAssessmentConfigs, + updateAssessmentConfigs, + updateNotificationConfigs, + updateNotificationPreferences, + deleteAssessmentConfig, + fetchAdminPanelCourseRegistrations, + fetchConfigurableNotificationConfigs, + fetchNotificationConfigs, + updateTimeOptions, + deleteTimeOptions, + updateUserRole, + deleteUserCourseRegistration +} = newActions2; + +// For compatibility with existing code (actions helper) +export default { + ...newActions, + updateTotalXp, + updateAssessment, + ...newActions2, + updateCourseResearchAgreement +}; diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index f83c160065..fafee13960 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -17,50 +17,8 @@ import { import { Notification } from '../../../notificationBadge/NotificationBadgeTypes'; import { GameState, Role, Story } from '../../ApplicationTypes'; import { - ACKNOWLEDGE_NOTIFICATIONS, - DELETE_ASSESSMENT_CONFIG, - DELETE_USER_COURSE_REGISTRATION, - FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, - FETCH_ASSESSMENT, - FETCH_ASSESSMENT_CONFIGS, - FETCH_ASSESSMENT_OVERVIEWS, - FETCH_AUTH, - FETCH_COURSE_CONFIG, - FETCH_GRADING, - FETCH_GRADING_OVERVIEWS, - FETCH_NOTIFICATIONS, - FETCH_STUDENTS, - FETCH_TEAM_FORMATION_OVERVIEWS, - FETCH_USER_AND_COURSE, - LOGIN, - REAUTOGRADE_ANSWER, - REAUTOGRADE_SUBMISSION, - SET_ADMIN_PANEL_COURSE_REGISTRATIONS, - SET_ASSESSMENT_CONFIGURATIONS, - SET_COURSE_CONFIGURATION, - SET_COURSE_REGISTRATION, - SET_GITHUB_ACCESS_TOKEN, - SET_GITHUB_OCTOKIT_OBJECT, - SET_TOKENS, - SET_USER, - SUBMIT_ANSWER, - SUBMIT_ASSESSMENT, - SUBMIT_GRADING, - SUBMIT_GRADING_AND_CONTINUE, - UNSUBMIT_SUBMISSION, UPDATE_ASSESSMENT, - UPDATE_ASSESSMENT_CONFIGS, - UPDATE_ASSESSMENT_OVERVIEWS, - UPDATE_COURSE_CONFIG, UPDATE_COURSE_RESEARCH_AGREEMENT, - UPDATE_GRADING, - UPDATE_GRADING_OVERVIEWS, - UPDATE_LATEST_VIEWED_COURSE, - UPDATE_NOTIFICATIONS, - UPDATE_STUDENTS, - UPDATE_TEAM_FORMATION_OVERVIEW, - UPDATE_TEAM_FORMATION_OVERVIEWS, - UPDATE_USER_ROLE, User } from '../../types/SessionTypes'; import { @@ -114,7 +72,7 @@ test('acknowledgeNotifications generates correct action object', () => { const action = acknowledgeNotifications(); expect(action).toEqual({ - type: ACKNOWLEDGE_NOTIFICATIONS, + type: acknowledgeNotifications.type, payload: { withFilter: undefined } @@ -125,7 +83,7 @@ test('fetchAuth generates correct action object', () => { const code = 'luminus-code-test'; const action = fetchAuth(code); expect(action).toEqual({ - type: FETCH_AUTH, + type: fetchAuth.type, payload: { code } }); }); @@ -133,7 +91,7 @@ test('fetchAuth generates correct action object', () => { test('fetchUserAndCourse generates correct action object', () => { const action = fetchUserAndCourse(); expect(action).toEqual({ - type: FETCH_USER_AND_COURSE, + type: fetchUserAndCourse.type, payload: {} }); }); @@ -141,7 +99,7 @@ test('fetchUserAndCourse generates correct action object', () => { test('fetchCourseConfig generates correct action object', () => { const action = fetchCourseConfig(); expect(action).toEqual({ - type: FETCH_COURSE_CONFIG, + type: fetchCourseConfig.type, payload: {} }); }); @@ -150,7 +108,7 @@ test('fetchAssessment generates correct action object', () => { const id = 3; const action = fetchAssessment(id); expect(action).toEqual({ - type: FETCH_ASSESSMENT, + type: fetchAssessment.type, payload: { assessmentId: id } }); }); @@ -158,7 +116,7 @@ test('fetchAssessment generates correct action object', () => { test('fetchAssessmentOverviews generates correct action object', () => { const action = fetchAssessmentOverviews(); expect(action).toEqual({ - type: FETCH_ASSESSMENT_OVERVIEWS, + type: fetchAssessmentOverviews.type, payload: {} }); }); @@ -167,7 +125,7 @@ test('fetchGrading generates correct action object', () => { const submissionId = 5; const action = fetchGrading(submissionId); expect(action).toEqual({ - type: FETCH_GRADING, + type: fetchGrading.type, payload: submissionId }); }); @@ -175,7 +133,7 @@ test('fetchGrading generates correct action object', () => { test('fetchGradingOverviews generates correct default action object', () => { const action = fetchGradingOverviews(); expect(action).toEqual({ - type: FETCH_GRADING_OVERVIEWS, + type: fetchGradingOverviews.type, payload: { filterToGroup: true, publishedFilter: unpublishedToBackendParams(false), @@ -192,7 +150,7 @@ test('fetchGradingOverviews generates correct action object', () => { const filterParams = { abc: 'xxx', def: 'yyy' }; const action = fetchGradingOverviews(filterToGroup, publishedFilter, pageParams, filterParams); expect(action).toEqual({ - type: FETCH_GRADING_OVERVIEWS, + type: fetchGradingOverviews.type, payload: { filterToGroup: filterToGroup, publishedFilter: publishedFilter, @@ -205,7 +163,7 @@ test('fetchGradingOverviews generates correct action object', () => { test('fetchTeamFormationOverviews generates correct default action object', () => { const action = fetchTeamFormationOverviews(); expect(action).toEqual({ - type: FETCH_TEAM_FORMATION_OVERVIEWS, + type: fetchTeamFormationOverviews.type, payload: true }); }); @@ -214,7 +172,7 @@ test('fetchTeamFormationOverviews generates correct action object', () => { const filterToGroup = false; const action = fetchTeamFormationOverviews(filterToGroup); expect(action).toEqual({ - type: FETCH_TEAM_FORMATION_OVERVIEWS, + type: fetchTeamFormationOverviews.type, payload: filterToGroup }); }); @@ -222,7 +180,7 @@ test('fetchTeamFormationOverviews generates correct action object', () => { test('fetchStudents generates correct action object', () => { const action = fetchStudents(); expect(action).toEqual({ - type: FETCH_STUDENTS, + type: fetchStudents.type, payload: {} }); }); @@ -231,7 +189,7 @@ test('fetchNotifications generates correct action object', () => { const action = fetchNotifications(); expect(action).toEqual({ - type: FETCH_NOTIFICATIONS, + type: fetchNotifications.type, payload: {} }); }); @@ -239,7 +197,7 @@ test('fetchNotifications generates correct action object', () => { test('login action generates correct action object', () => { const action = login('provider'); expect(action).toEqual({ - type: LOGIN, + type: login.type, payload: 'provider' }); }); @@ -249,7 +207,7 @@ test('setTokens generates correct action object', () => { const refreshToken = 'refresh-token-test'; const action = setTokens({ accessToken, refreshToken }); expect(action).toEqual({ - type: SET_TOKENS, + type: setTokens.type, payload: { accessToken, refreshToken @@ -281,7 +239,7 @@ test('setUser generates correct action object', () => { }; const action = setUser(user); expect(action).toEqual({ - type: SET_USER, + type: setUser.type, payload: user }); }); @@ -302,7 +260,7 @@ test('setCourseConfiguration generates correct action object', () => { }; const action = setCourseConfiguration(courseConfig); expect(action).toEqual({ - type: SET_COURSE_CONFIGURATION, + type: setCourseConfiguration.type, payload: courseConfig }); }); @@ -328,7 +286,7 @@ test('setCourseRegistration generates correct action object', () => { }; const action = setCourseRegistration(courseRegistration); expect(action).toEqual({ - type: SET_COURSE_REGISTRATION, + type: setCourseRegistration.type, payload: courseRegistration }); }); @@ -371,7 +329,7 @@ test('setAssessmentConfigurations generates correct action object', () => { ]; const action = setAssessmentConfigurations(assesmentConfigurations); expect(action).toEqual({ - type: SET_ASSESSMENT_CONFIGURATIONS, + type: setAssessmentConfigurations.type, payload: assesmentConfigurations }); }); @@ -395,7 +353,7 @@ test('setAdminPanelCourseRegistrations generates correct action object', async ( ]; const action = setAdminPanelCourseRegistrations(userCourseRegistrations); expect(action).toEqual({ - type: SET_ADMIN_PANEL_COURSE_REGISTRATIONS, + type: setAdminPanelCourseRegistrations.type, payload: userCourseRegistrations }); }); @@ -403,7 +361,7 @@ test('setAdminPanelCourseRegistrations generates correct action object', async ( test('setGitHubOctokitInstance generates correct action object', async () => { const authToken = 'testAuthToken12345'; const action = setGitHubOctokitObject(authToken); - expect(action.type).toEqual(SET_GITHUB_OCTOKIT_OBJECT); + expect(action.type).toEqual(setGitHubOctokitObject.type); const authObject = (await action.payload.auth()) as any; expect(authObject.token).toBe('testAuthToken12345'); @@ -414,7 +372,7 @@ test('setGitHubAccessToken generates correct action object', () => { const authToken = 'testAuthToken12345'; const action = setGitHubAccessToken(authToken); expect(action).toEqual({ - type: SET_GITHUB_ACCESS_TOKEN, + type: setGitHubAccessToken.type, payload: authToken }); }); @@ -424,7 +382,7 @@ test('submitAnswer generates correct action object', () => { const answer = 'test-answer-here'; const action = submitAnswer(id, answer); expect(action).toEqual({ - type: SUBMIT_ANSWER, + type: submitAnswer.type, payload: { id, answer @@ -436,7 +394,7 @@ test('submitAssessment generates correct action object', () => { const id = 7; const action = submitAssessment(id); expect(action).toEqual({ - type: SUBMIT_ASSESSMENT, + type: submitAssessment.type, payload: id }); }); @@ -447,7 +405,7 @@ test('submitGrading generates correct action object with default values', () => const action = submitGrading(submissionId, questionId); expect(action).toEqual({ - type: SUBMIT_GRADING, + type: submitGrading.type, payload: { submissionId, questionId, @@ -463,7 +421,7 @@ test('submitGradingAndContinue generates correct action object with default valu const action = submitGradingAndContinue(submissionId, questionId); expect(action).toEqual({ - type: SUBMIT_GRADING_AND_CONTINUE, + type: submitGradingAndContinue.type, payload: { submissionId, questionId, @@ -480,7 +438,7 @@ test('submitGrading generates correct action object', () => { const comments = 'my comment'; const action = submitGrading(submissionId, questionId, xpAdjustment, comments); expect(action).toEqual({ - type: SUBMIT_GRADING, + type: submitGrading.type, payload: { submissionId, questionId, @@ -497,7 +455,7 @@ test('submitGradingAndContinue generates correct action object', () => { const comments = 'another comment'; const action = submitGradingAndContinue(submissionId, questionId, xpAdjustment, comments); expect(action).toEqual({ - type: SUBMIT_GRADING_AND_CONTINUE, + type: submitGradingAndContinue.type, payload: { submissionId, questionId, @@ -511,7 +469,7 @@ test('reautogradeSubmission generates correct action object', () => { const submissionId = 123; const action = reautogradeSubmission(submissionId); expect(action).toEqual({ - type: REAUTOGRADE_SUBMISSION, + type: reautogradeSubmission.type, payload: submissionId }); }); @@ -521,7 +479,7 @@ test('reautogradeAnswer generates correct action object', () => { const questionId = 456; const action = reautogradeAnswer(submissionId, questionId); expect(action).toEqual({ - type: REAUTOGRADE_ANSWER, + type: reautogradeAnswer.type, payload: { submissionId, questionId } }); }); @@ -530,7 +488,7 @@ test('unsubmitSubmission generates correct action object', () => { const submissionId = 10; const action = unsubmitSubmission(submissionId); expect(action).toEqual({ - type: UNSUBMIT_SUBMISSION, + type: unsubmitSubmission.type, payload: { submissionId } @@ -561,7 +519,7 @@ test('updateAssessmentOverviews generates correct action object', () => { ]; const action = updateAssessmentOverviews(overviews); expect(action).toEqual({ - type: UPDATE_ASSESSMENT_OVERVIEWS, + type: updateAssessmentOverviews.type, payload: overviews }); }); @@ -617,7 +575,7 @@ test('updateGradingOverviews generates correct action object', () => { const action = updateGradingOverviews(overviews); expect(action).toEqual({ - type: UPDATE_GRADING_OVERVIEWS, + type: updateGradingOverviews.type, payload: overviews }); }); @@ -627,7 +585,7 @@ test('updateStudents generates correct action object', () => { const action = updateStudents(students); expect(action).toEqual({ - type: UPDATE_STUDENTS, + type: updateStudents.type, payload: students }); }); @@ -644,7 +602,7 @@ test('updateTeamFormationOverview generates correct action object', () => { const action = updateTeamFormationOverview(overview); expect(action).toEqual({ - type: UPDATE_TEAM_FORMATION_OVERVIEW, + type: updateTeamFormationOverview.type, payload: overview }); }); @@ -663,7 +621,7 @@ test('updateTeamFormationOverviews generates correct action object', () => { const action = updateTeamFormationOverviews(overviews); expect(action).toEqual({ - type: UPDATE_TEAM_FORMATION_OVERVIEWS, + type: updateTeamFormationOverviews.type, payload: overviews }); }); @@ -705,7 +663,7 @@ test('updateGrading generates correct action object', () => { const action = updateGrading(submissionId, grading); expect(action).toEqual({ - type: UPDATE_GRADING, + type: updateGrading.type, payload: { submissionId, grading @@ -734,7 +692,7 @@ test('updateNotifications generates correct action object', () => { const action = updateNotifications(notifications); expect(action).toEqual({ - type: UPDATE_NOTIFICATIONS, + type: updateNotifications.type, payload: notifications }); }); @@ -743,7 +701,7 @@ test('updateLatestViewedCourse generates correct action object', () => { const courseId = 2; const action = updateLatestViewedCourse(courseId); expect(action).toEqual({ - type: UPDATE_LATEST_VIEWED_COURSE, + type: updateLatestViewedCourse.type, payload: { courseId } }); }); @@ -764,7 +722,7 @@ test('updateCourseConfig generates correct action object', () => { }; const action = updateCourseConfig(courseConfig); expect(action).toEqual({ - type: UPDATE_COURSE_CONFIG, + type: updateCourseConfig.type, payload: courseConfig }); }); @@ -772,7 +730,7 @@ test('updateCourseConfig generates correct action object', () => { test('fetchAssessmentConfig generates correct action object', () => { const action = fetchAssessmentConfigs(); expect(action).toEqual({ - type: FETCH_ASSESSMENT_CONFIGS, + type: fetchAssessmentConfigs.type, payload: {} }); }); @@ -848,7 +806,7 @@ test('updateAssessmentTypes generates correct action object', () => { ]; const action = updateAssessmentConfigs(assessmentConfigs); expect(action).toEqual({ - type: UPDATE_ASSESSMENT_CONFIGS, + type: updateAssessmentConfigs.type, payload: assessmentConfigs }); }); @@ -867,7 +825,7 @@ test('deleteAssessmentConfig generates correct action object', () => { }; const action = deleteAssessmentConfig(assessmentConfig); expect(action).toEqual({ - type: DELETE_ASSESSMENT_CONFIG, + type: deleteAssessmentConfig.type, payload: assessmentConfig }); }); @@ -875,7 +833,7 @@ test('deleteAssessmentConfig generates correct action object', () => { test('fetchAdminPanelCourseRegistrations generates correct action object', () => { const action = fetchAdminPanelCourseRegistrations(); expect(action).toEqual({ - type: FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, + type: fetchAdminPanelCourseRegistrations.type, payload: {} }); }); @@ -885,7 +843,7 @@ test('updateUserRole generates correct action object', () => { const role = Role.Staff; const action = updateUserRole(courseRegId, role); expect(action).toEqual({ - type: UPDATE_USER_ROLE, + type: updateUserRole.type, payload: { courseRegId, role } }); }); @@ -903,7 +861,7 @@ test('deleteUserCourseRegistration generates correct action object', () => { const courseRegId = 1; const action = deleteUserCourseRegistration(courseRegId); expect(action).toEqual({ - type: DELETE_USER_COURSE_REGISTRATION, + type: deleteUserCourseRegistration.type, payload: { courseRegId } }); }); diff --git a/src/commons/application/reducers/RootReducer.ts b/src/commons/application/reducers/RootReducer.ts index 6d44a73187..504bd70c02 100644 --- a/src/commons/application/reducers/RootReducer.ts +++ b/src/commons/application/reducers/RootReducer.ts @@ -7,20 +7,20 @@ import { StoriesReducer as stories } from '../../../features/stories/StoriesRedu import { FileSystemReducer as fileSystem } from '../../fileSystem/FileSystemReducer'; import { SideContentReducer as sideContent } from '../../sideContent/SideContentReducer'; import { WorkspaceReducer as workspaces } from '../../workspace/WorkspaceReducer'; +import { OverallState } from '../ApplicationTypes'; import { RouterReducer as router } from './CommonsReducer'; import { SessionsReducer as session } from './SessionsReducer'; -const createRootReducer = () => - combineReducers({ - router, - achievement, - dashboard, - playground, - session, - stories, - workspaces, - fileSystem, - sideContent - }); +const rootReducer = combineReducers({ + router, + achievement, + dashboard, + playground, + session, + stories, + workspaces, + fileSystem, + sideContent +}); -export default createRootReducer; +export default rootReducer; diff --git a/src/commons/application/reducers/__tests__/SessionReducer.ts b/src/commons/application/reducers/__tests__/SessionReducer.ts index 5b1116747a..fd45b94bcc 100644 --- a/src/commons/application/reducers/__tests__/SessionReducer.ts +++ b/src/commons/application/reducers/__tests__/SessionReducer.ts @@ -8,23 +8,22 @@ import { ProgressStatuses } from '../../../assessment/AssessmentTypes'; import { Notification } from '../../../notificationBadge/NotificationBadgeTypes'; +import { + setAdminPanelCourseRegistrations, + setAssessmentConfigurations, + setCourseConfiguration, + setCourseRegistration, + setGitHubAccessToken, + setTokens, + setUser, + updateAssessmentOverviews, + updateGrading, + updateGradingOverviews, + updateNotifications +} from '../../actions/SessionActions'; import { defaultSession, GameState, Role, Story } from '../../ApplicationTypes'; import { LOG_OUT } from '../../types/CommonsTypes'; -import { - SessionState, - SET_ADMIN_PANEL_COURSE_REGISTRATIONS, - SET_ASSESSMENT_CONFIGURATIONS, - SET_COURSE_CONFIGURATION, - SET_COURSE_REGISTRATION, - SET_GITHUB_ACCESS_TOKEN, - SET_TOKENS, - SET_USER, - UPDATE_ASSESSMENT, - UPDATE_ASSESSMENT_OVERVIEWS, - UPDATE_GRADING, - UPDATE_GRADING_OVERVIEWS, - UPDATE_NOTIFICATIONS -} from '../../types/SessionTypes'; +import { SessionState, UPDATE_ASSESSMENT } from '../../types/SessionTypes'; import { SessionsReducer } from '../SessionsReducer'; test('LOG_OUT works correctly on default session', () => { @@ -42,7 +41,7 @@ test('SET_TOKEN sets accessToken and refreshToken correctly', () => { const refreshToken = 'refresh_token_test'; const action = { - type: SET_TOKENS, + type: setTokens.type, payload: { accessToken, refreshToken @@ -80,7 +79,7 @@ test('SET_USER works correctly', () => { }; const action = { - type: SET_USER, + type: setUser.type, payload } as const; const result: SessionState = SessionsReducer(defaultSession, action); @@ -106,7 +105,7 @@ test('SET_COURSE_CONFIGURATION works correctly', () => { assessmentTypes: ['Missions', 'Quests', 'Paths', 'Contests', 'Others'] }; const action = { - type: SET_COURSE_CONFIGURATION, + type: setCourseConfiguration.type, payload } as const; const result: SessionState = SessionsReducer(defaultSession, action); @@ -136,7 +135,7 @@ test('SET_COURSE_REGISTRATION works correctly', () => { agreedToReseach: true }; const action = { - type: SET_COURSE_REGISTRATION, + type: setCourseRegistration.type, payload } as const; const result: SessionState = SessionsReducer(defaultSession, action); @@ -194,7 +193,7 @@ test('SET_ASSESSMENT_CONFIGURATIONS works correctly', () => { ]; const action = { - type: SET_ASSESSMENT_CONFIGURATIONS, + type: setAssessmentConfigurations.type, payload } as const; const result: SessionState = SessionsReducer(defaultSession, action); @@ -224,7 +223,7 @@ test('SET_ADMIN_PANEL_COURSE_REGISTRATIONS works correctly', () => { ]; const action = { - type: SET_ADMIN_PANEL_COURSE_REGISTRATIONS, + type: setAdminPanelCourseRegistrations.type, payload } as const; const result: SessionState = SessionsReducer(defaultSession, action); @@ -238,7 +237,7 @@ test('SET_ADMIN_PANEL_COURSE_REGISTRATIONS works correctly', () => { test('SET_GITHUB_ACCESS_TOKEN works correctly', () => { const token = 'githubAccessToken'; const action = { - type: SET_GITHUB_ACCESS_TOKEN, + type: setGitHubAccessToken.type, payload: token } as const; const result: SessionState = SessionsReducer(defaultSession, action); @@ -377,7 +376,7 @@ const assessmentOverviewsTest2: AssessmentOverview[] = [ test('UPDATE_ASSESSMENT_OVERVIEWS works correctly in inserting assessment overviews', () => { const action = { - type: UPDATE_ASSESSMENT_OVERVIEWS, + type: updateAssessmentOverviews.type, payload: assessmentOverviewsTest1 } as const; @@ -396,7 +395,7 @@ test('UPDATE_ASSESSMENT_OVERVIEWS works correctly in updating assessment overvie }; const assessmentOverviewsPayload = [...assessmentOverviewsTest2, ...assessmentOverviewsTest1]; const action = { - type: UPDATE_ASSESSMENT_OVERVIEWS, + type: updateAssessmentOverviews.type, payload: assessmentOverviewsPayload } as const; @@ -468,7 +467,7 @@ const gradingTest2: GradingQuery = { test('UPDATE_GRADING works correctly in inserting gradings', () => { const submissionId = 23; const action = { - type: UPDATE_GRADING, + type: updateGrading.type, payload: { submissionId, grading: gradingTest1 @@ -491,7 +490,7 @@ test('UPDATE_GRADING works correctly in inserting gradings and retains old data' }; const action = { - type: UPDATE_GRADING, + type: updateGrading.type, payload: { submissionId: submissionId2, grading: gradingTest2 @@ -513,7 +512,7 @@ test('UPDATE_GRADING works correctly in updating gradings', () => { }; const action = { - type: UPDATE_GRADING, + type: updateGrading.type, payload: { submissionId, grading: gradingTest2 @@ -579,7 +578,7 @@ const gradingOverviewTest2: GradingOverview[] = [ test('UPDATE_GRADING_OVERVIEWS works correctly in inserting grading overviews', () => { const action = { - type: UPDATE_GRADING_OVERVIEWS, + type: updateGradingOverviews.type, payload: { count: gradingOverviewTest1.length, data: gradingOverviewTest1 @@ -606,7 +605,7 @@ test('UPDATE_GRADING_OVERVIEWS works correctly in updating grading overviews', ( data: [...gradingOverviewTest2, ...gradingOverviewTest1] }; const action = { - type: UPDATE_GRADING_OVERVIEWS, + type: updateGradingOverviews.type, payload: gradingOverviewsPayload } as const; const result: SessionState = SessionsReducer(newDefaultSession, action); @@ -633,7 +632,7 @@ test('UPDATE_NOTIFICATIONS works correctly in updating notifications', () => { ]; const action = { - type: UPDATE_NOTIFICATIONS, + type: updateNotifications.type, payload: notifications } as const; diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index f59d8ddade..50071ecfd0 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -12,76 +12,9 @@ import { import { Notification } from '../../notificationBadge/NotificationBadgeTypes'; import { GameState, Role, Story } from '../ApplicationTypes'; -export const BULK_UPLOAD_TEAM = 'BULK_UPLOAD_TEAM'; -export const CHECK_ANSWER_LAST_MODIFIED_AT = 'CHECK_ANSWER_LAST_MODIFIED_AT'; -export const CREATE_TEAM = 'CREATE_TEAM'; -export const DELETE_TEAM = 'DELETE_TEAM'; -export const UPDATE_TEAM = 'UPDATE_TEAM'; -export const FETCH_AUTH = 'FETCH_AUTH'; -export const FETCH_USER_AND_COURSE = 'FETCH_USER_AND_COURSE'; -export const FETCH_COURSE_CONFIG = 'FETCH_COURSE_CONFIG'; -export const FETCH_ASSESSMENT = 'FETCH_ASSESSMENT'; -export const FETCH_ASSESSMENT_ADMIN = 'FETCH_ASSESSMENT_ADMIN'; -export const FETCH_ASSESSMENT_OVERVIEWS = 'FETCH_ASSESSMENT_OVERVIEWS'; -export const FETCH_TOTAL_XP = 'FETCH_TOTAL_XP'; -export const FETCH_TOTAL_XP_ADMIN = 'FETCH_TOTAL_XP_ADMIN'; -export const FETCH_GRADING = 'FETCH_GRADING'; -export const FETCH_GRADING_OVERVIEWS = 'FETCH_GRADING_OVERVIEWS'; -export const FETCH_STUDENTS = 'FETCH_STUDENTS'; -export const FETCH_TEAM_FORMATION_OVERVIEW = 'FETCH_TEAM_FORMATION_OVERVIEW'; -export const FETCH_TEAM_FORMATION_OVERVIEWS = 'FETCH_TEAM_FORMATION_OVERVIEWS'; -export const LOGIN = 'LOGIN'; -export const LOGOUT_GOOGLE = 'LOGOUT_GOOGLE'; -export const LOGIN_GITHUB = 'LOGIN_GITHUB'; -export const LOGOUT_GITHUB = 'LOGOUT_GITHUB'; -export const PUBLISH_GRADING = 'PUBLISH_GRADING'; -export const SET_TOKENS = 'SET_TOKENS'; -export const SET_USER = 'SET_USER'; -export const SET_COURSE_CONFIGURATION = 'SET_COURSE_CONFIGURATION'; -export const SET_COURSE_REGISTRATION = 'SET_COURSE_REGISTRATION'; -export const SET_ASSESSMENT_CONFIGURATIONS = 'SET_ASSESSMENT_CONFIGURATIONS'; -export const SET_ADMIN_PANEL_COURSE_REGISTRATIONS = 'SET_ADMIN_PANEL_COURSE_REGISTRATIONS'; -export const SET_GOOGLE_USER = 'SET_GOOGLE_USER'; -export const SET_GITHUB_OCTOKIT_OBJECT = 'SET_GITHUB_OCTOKIT_OBJECT'; -export const SET_GITHUB_ACCESS_TOKEN = 'SET_GITHUB_ACCESS_TOKEN'; -export const SUBMIT_ANSWER = 'SUBMIT_ANSWER'; -export const SUBMIT_ASSESSMENT = 'SUBMIT_ASSESSMENT'; -export const SUBMIT_GRADING = 'SUBMIT_GRADING'; -export const SUBMIT_GRADING_AND_CONTINUE = 'SUBMIT_GRADING_AND_CONTINUE'; -export const REAUTOGRADE_SUBMISSION = 'REAUTOGRADE_SUBMISSION'; -export const REAUTOGRADE_ANSWER = 'REAUTOGRADE_ANSWER'; -export const REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN = - 'REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN'; -export const UNPUBLISH_GRADING = 'UNPUBLISH_GRADING'; -export const UNSUBMIT_SUBMISSION = 'UNSUBMIT_SUBMISSION'; -export const UPDATE_ASSESSMENT_OVERVIEWS = 'UPDATE_ASSESSMENT_OVERVIEWS'; export const UPDATE_TOTAL_XP = 'UPDATE_TOTAL_XP'; export const UPDATE_ASSESSMENT = 'UPDATE_ASSESSMENT'; -export const UPDATE_GRADING_OVERVIEWS = 'UPDATE_GRADING_OVERVIEWS'; -export const UPDATE_GRADING = 'UPDATE_GRADING'; -export const UPDATE_TEAM_FORMATION_OVERVIEW = 'UPDATE_TEAM_FORMATION_OVERVIEW'; -export const UPDATE_TEAM_FORMATION_OVERVIEWS = 'UPDATE_TEAM_FORMATION_OVERVIEWS'; -export const UPDATE_STUDENTS = 'UPDATE_STUDENTS'; -export const FETCH_NOTIFICATIONS = 'FETCH_NOTIFICATIONS'; -export const ACKNOWLEDGE_NOTIFICATIONS = 'ACKNOWLEDGE_NOTIFICATIONS'; -export const UPDATE_NOTIFICATIONS = 'UPDATE_NOTIFICATIONS'; -export const UPDATE_LATEST_VIEWED_COURSE = 'UPDATE_LATEST_VIEWED_COURSE'; -export const UPDATE_COURSE_CONFIG = 'UPDATE_COURSE_CONFIG'; -export const FETCH_ASSESSMENT_CONFIGS = 'FETCH_ASSESSMENT_CONFIGS'; -export const UPDATE_ASSESSMENT_CONFIGS = 'UPDATE_ASSESSMENT_CONFIGS'; -export const DELETE_ASSESSMENT_CONFIG = 'DELETE_ASSESSMENT_CONFIG'; -export const FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS = 'FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS'; -export const UPDATE_USER_ROLE = 'UPDATE_USER_ROLE'; export const UPDATE_COURSE_RESEARCH_AGREEMENT = 'UPDATE_COURSE_RESEARCH_AGREEMENT'; -export const DELETE_USER_COURSE_REGISTRATION = 'DELETE_USER_COURSE_REGISTRATION'; -export const FETCH_CONFIGURABLE_NOTIFICATION_CONFIGS = 'FETCH_CONFIGURABLE_NOTIFICATION_CONFIGS'; -export const FETCH_NOTIFICATION_CONFIGS = 'FETCH_NOTIFICATION_CONFIGS'; -export const SET_NOTIFICATION_CONFIGS = 'SET_NOTIFICATION_CONFIGS'; -export const SET_CONFIGURABLE_NOTIFICATION_CONFIGS = 'SET_CONFIGURABLE_NOTIFICATION_CONFIG'; -export const UPDATE_NOTIFICATION_CONFIG = 'UPDATE_NOTIFICATION_CONFIG'; -export const UPDATE_NOTIFICATION_PREFERENCES = 'UPDATE_NOTIFICATION_PREFERENCES'; -export const DELETE_TIME_OPTIONS = 'DELETE_TIME_OPTIONS'; -export const UPDATE_TIME_OPTIONS = 'UPDATE_TIME_OPTIONS'; export type SessionState = { // Tokens diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index e61ea9e5b5..09bf818ed4 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -2,9 +2,6 @@ import { Chapter, SourceError, Variant } from 'js-slang/dist/types'; import { ExternalLibrary, ExternalLibraryName } from '../application/types/ExternalTypes'; -export const FETCH_ASSESSMENT_OVERVIEWS = 'FETCH_ASSESSMENT_OVERVIEWS'; -export const SUBMIT_ASSESSMENT = 'SUBMIT_ASSESSMENT'; - export enum AssessmentStatuses { attempting = 'attempting', attempted = 'attempted', diff --git a/src/commons/mocks/BackendMocks.ts b/src/commons/mocks/BackendMocks.ts index 1cbdce64a1..4738ebeef9 100644 --- a/src/commons/mocks/BackendMocks.ts +++ b/src/commons/mocks/BackendMocks.ts @@ -7,6 +7,32 @@ import { GradingQuery, GradingQuestion } from '../../features/grading/GradingTypes'; +import { + acknowledgeNotifications, + bulkUploadTeam, + createTeam, + deleteTeam, + fetchAdminPanelCourseRegistrations, + fetchAssessment, + fetchAssessmentOverviews, + fetchAuth, + fetchCourseConfig, + fetchGrading, + fetchGradingOverviews, + fetchNotifications, + fetchStudents, + fetchTeamFormationOverviews, + fetchUserAndCourse, + submitAnswer, + submitAssessment, + submitGrading, + submitGradingAndContinue, + unsubmitSubmission, + updateAssessmentConfigs, + updateCourseConfig, + updateLatestViewedCourse, + updateTeam +} from '../application/actions/SessionActions'; import { OverallState, Role, @@ -14,39 +40,12 @@ import { styliseSublanguage, SupportedLanguage } from '../application/ApplicationTypes'; -import { - ACKNOWLEDGE_NOTIFICATIONS, - AdminPanelCourseRegistration, - BULK_UPLOAD_TEAM, - CREATE_TEAM, - DELETE_TEAM, - FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, - FETCH_ASSESSMENT, - FETCH_AUTH, - FETCH_COURSE_CONFIG, - FETCH_GRADING, - FETCH_GRADING_OVERVIEWS, - FETCH_NOTIFICATIONS, - FETCH_STUDENTS, - FETCH_TEAM_FORMATION_OVERVIEWS, - FETCH_USER_AND_COURSE, - SUBMIT_ANSWER, - SUBMIT_GRADING, - SUBMIT_GRADING_AND_CONTINUE, - Tokens, - UNSUBMIT_SUBMISSION, - UPDATE_ASSESSMENT_CONFIGS, - UPDATE_COURSE_CONFIG, - UPDATE_LATEST_VIEWED_COURSE, - UPDATE_TEAM -} from '../application/types/SessionTypes'; +import { AdminPanelCourseRegistration, Tokens } from '../application/types/SessionTypes'; import { AssessmentOverview, AssessmentStatuses, - FETCH_ASSESSMENT_OVERVIEWS, ProgressStatuses, - Question, - SUBMIT_ASSESSMENT + Question } from '../assessment/AssessmentTypes'; import { Notification, @@ -78,8 +77,10 @@ import { mockUser } from './UserMocks'; +// TODO: Removal/implementation pending on outcome of +// https://github.com/source-academy/frontend/issues/2974 export function* mockBackendSaga(): SagaIterator { - yield takeEvery(FETCH_AUTH, function* (action: ReturnType) { + yield takeEvery(fetchAuth.type, function* (action: ReturnType) { const tokens: Tokens = { accessToken: 'accessToken', refreshToken: 'refreshToken' @@ -114,50 +115,56 @@ export function* mockBackendSaga(): SagaIterator { yield put(actions.updateSublanguage(sublanguage)); }; - yield takeEvery(FETCH_USER_AND_COURSE, mockGetUserAndCourse); + yield takeEvery(fetchUserAndCourse.type, mockGetUserAndCourse); - yield takeEvery(FETCH_COURSE_CONFIG, function* () { + yield takeEvery(fetchCourseConfig.type, function* () { const courseConfiguration = { ...mockCourseConfigurations[0] }; yield put(actions.setCourseConfiguration(courseConfiguration)); }); - yield takeEvery(FETCH_ASSESSMENT_OVERVIEWS, function* () { + yield takeEvery(fetchAssessmentOverviews.type, function* () { yield put(actions.updateAssessmentOverviews([...mockAssessmentOverviews])); }); - yield takeEvery(FETCH_ASSESSMENT, function* (action: ReturnType) { - const { assessmentId: id } = action.payload; - const assessment = mockAssessments[id - 1]; - yield put(actions.updateAssessment({ ...assessment })); - }); + yield takeEvery( + fetchAssessment.type, + function* (action: ReturnType) { + const { assessmentId: id } = action.payload; + const assessment = mockAssessments[id - 1]; + yield put(actions.updateAssessment({ ...assessment })); + } + ); - yield takeEvery(SUBMIT_ANSWER, function* (action: ReturnType): any { - const questionId = action.payload.id; - const answer = action.payload.answer; - // Now, update the answer for the question in the assessment in the store - const assessmentId = yield select( - (state: OverallState) => state.workspaces.assessment.currentAssessment! - ); - const assessment = yield select( - (state: OverallState) => state.session.assessments[assessmentId] - ); - const newQuestions = assessment.questions.slice().map((question: Question) => { - if (question.id === questionId) { - question.answer = answer; - } - return question; - }); - const newAssessment = { - ...assessment, - questions: newQuestions - }; - yield put(actions.updateAssessment(newAssessment)); - yield call(showSuccessMessage, 'Saved!', 1000); - return yield put(actions.updateHasUnsavedChanges('assessment' as WorkspaceLocation, false)); - }); + yield takeEvery( + submitAnswer.type, + function* (action: ReturnType): any { + const questionId = action.payload.id; + const answer = action.payload.answer; + // Now, update the answer for the question in the assessment in the store + const assessmentId = yield select( + (state: OverallState) => state.workspaces.assessment.currentAssessment! + ); + const assessment = yield select( + (state: OverallState) => state.session.assessments[assessmentId] + ); + const newQuestions = assessment.questions.slice().map((question: Question) => { + if (question.id === questionId) { + question.answer = answer; + } + return question; + }); + const newAssessment = { + ...assessment, + questions: newQuestions + }; + yield put(actions.updateAssessment(newAssessment)); + yield call(showSuccessMessage, 'Saved!', 1000); + return yield put(actions.updateHasUnsavedChanges('assessment' as WorkspaceLocation, false)); + } + ); yield takeEvery( - SUBMIT_ASSESSMENT, + submitAssessment.type, function* (action: ReturnType): any { const assessmentId = action.payload; @@ -178,7 +185,7 @@ export function* mockBackendSaga(): SagaIterator { ); yield takeEvery( - FETCH_GRADING_OVERVIEWS, + fetchGradingOverviews.type, function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); const { filterToGroup, pageParams, filterParams } = action.payload; @@ -192,7 +199,7 @@ export function* mockBackendSaga(): SagaIterator { ); yield takeEvery( - FETCH_TEAM_FORMATION_OVERVIEWS, + fetchTeamFormationOverviews.type, function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); const filterToGroup = action.payload; @@ -205,7 +212,7 @@ export function* mockBackendSaga(): SagaIterator { } ); - yield takeEvery(CREATE_TEAM, function* (action: ReturnType): any { + yield takeEvery(createTeam.type, function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); const { assessment, teams } = action.payload; @@ -218,7 +225,7 @@ export function* mockBackendSaga(): SagaIterator { }); yield takeEvery( - BULK_UPLOAD_TEAM, + bulkUploadTeam.type, function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); const { assessment, file } = action.payload; @@ -232,7 +239,7 @@ export function* mockBackendSaga(): SagaIterator { } ); - yield takeEvery(UPDATE_TEAM, function* (action: ReturnType): any { + yield takeEvery(updateTeam.type, function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); const { teamId, assessment, teams } = action.payload; @@ -244,7 +251,7 @@ export function* mockBackendSaga(): SagaIterator { } }); - yield takeEvery(DELETE_TEAM, function* (action: ReturnType): any { + yield takeEvery(deleteTeam.type, function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); const { teamId } = action.payload; @@ -255,7 +262,7 @@ export function* mockBackendSaga(): SagaIterator { }); yield takeEvery( - FETCH_STUDENTS, + fetchStudents.type, function* (action: ReturnType): any { const accessToken = yield select((state: OverallState) => state.session.accessToken); const students = yield call(() => mockFetchStudents(accessToken)); @@ -265,17 +272,20 @@ export function* mockBackendSaga(): SagaIterator { } ); - yield takeEvery(FETCH_GRADING, function* (action: ReturnType): any { - const submissionId = action.payload; - const accessToken = yield select((state: OverallState) => state.session.accessToken); - const grading = yield call(() => mockFetchGrading(accessToken, submissionId)); - if (grading !== null) { - yield put(actions.updateGrading(submissionId, grading)); + yield takeEvery( + fetchGrading.type, + function* (action: ReturnType): any { + const submissionId = action.payload; + const accessToken = yield select((state: OverallState) => state.session.accessToken); + const grading = yield call(() => mockFetchGrading(accessToken, submissionId)); + if (grading !== null) { + yield put(actions.updateGrading(submissionId, grading)); + } } - }); + ); yield takeEvery( - UNSUBMIT_SUBMISSION, + unsubmitSubmission.type, function* (action: ReturnType) { const { submissionId } = action.payload; const overviews: GradingOverviews = yield select( @@ -357,19 +367,19 @@ export function* mockBackendSaga(): SagaIterator { ); }; - yield takeEvery(SUBMIT_GRADING, sendGrade); + yield takeEvery(submitGrading.type, sendGrade); - yield takeEvery(SUBMIT_GRADING_AND_CONTINUE, sendGradeAndContinue); + yield takeEvery(submitGradingAndContinue.type, sendGradeAndContinue); yield takeEvery( - FETCH_NOTIFICATIONS, + fetchNotifications.type, function* (action: ReturnType) { yield put(actions.updateNotifications([...mockNotifications])); } ); yield takeEvery( - ACKNOWLEDGE_NOTIFICATIONS, + acknowledgeNotifications.type, function* (action: ReturnType) { const notificationFilter: NotificationFilterFunction | undefined = action.payload.withFilter; @@ -398,7 +408,7 @@ export function* mockBackendSaga(): SagaIterator { ); yield takeEvery( - UPDATE_LATEST_VIEWED_COURSE, + updateLatestViewedCourse.type, function* (action: ReturnType) { const { courseId } = action.payload; const idx = courseId - 1; // zero-indexed @@ -424,7 +434,7 @@ export function* mockBackendSaga(): SagaIterator { ); yield takeEvery( - UPDATE_COURSE_CONFIG, + updateCourseConfig.type, function* (action: ReturnType) { const courseConfig = action.payload; @@ -434,7 +444,7 @@ export function* mockBackendSaga(): SagaIterator { ); yield takeEvery( - UPDATE_ASSESSMENT_CONFIGS, + updateAssessmentConfigs.type, function* (action: ReturnType): any { const assessmentConfig = action.payload; @@ -444,7 +454,7 @@ export function* mockBackendSaga(): SagaIterator { ); yield takeEvery( - FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, + fetchAdminPanelCourseRegistrations.type, function* (action: ReturnType) { const courseRegistrations: AdminPanelCourseRegistration[] = [ ...mockAdminPanelCourseRegistrations diff --git a/src/commons/redux/utils.ts b/src/commons/redux/utils.ts new file mode 100644 index 0000000000..f53b24d640 --- /dev/null +++ b/src/commons/redux/utils.ts @@ -0,0 +1,100 @@ +import { + ActionCreatorWithOptionalPayload, + ActionCreatorWithoutPayload, + ActionCreatorWithPreparedPayload, + createAction +} from '@reduxjs/toolkit'; +import * as Sentry from '@sentry/browser'; +import { SagaIterator } from 'redux-saga'; +import { StrictEffect, takeEvery } from 'redux-saga/effects'; + +/** + * Creates actions, given a base name and base actions + * @param baseName The base name of the actions + * @param baseActions The base actions. Use a falsy value to create an action without a payload. + * @returns An object containing the actions + */ +export function createActions>( + baseName: BaseName, + baseActions: BaseActions +) { + return Object.entries(baseActions).reduce( + (res, [name, func]) => ({ + ...res, + [name]: func + ? createAction(`${baseName}/${name}`, (...args: any) => ({ payload: func(...args) })) + : createAction(`${baseName}/${name}`) + }), + {} as { + [K in keyof BaseActions]: K extends string + ? BaseActions[K] extends (...args: any) => any + ? ActionCreatorWithPreparedPayload< + Parameters, + ReturnType, + `${BaseName}/${K}` + > + : ActionCreatorWithoutPayload<`${BaseName}/${K}`> + : never; + } + ); +} + +export function combineSagaHandlers< + TActions extends Record< + string, + ActionCreatorWithPreparedPayload | ActionCreatorWithoutPayload + > +>( + actions: TActions, + handlers: { + // TODO: Maybe this can be stricter? And remove the optional type after + // migration is fully done + [K in keyof TActions]?: (action: ReturnType) => SagaIterator; + }, + others?: (takeEvery: typeof saferTakeEvery) => SagaIterator +): () => SagaIterator { + const sagaHandlers = Object.entries(handlers).map(([actionName, saga]) => + saferTakeEvery(actions[actionName], saga) + ); + return function* (): SagaIterator { + yield* sagaHandlers; + if (others) { + const obj = others(saferTakeEvery); + while (true) { + const { done, value } = obj.next(); + if (done) break; + yield value; + } + } + }; +} + +export function saferTakeEvery< + Action extends + | ActionCreatorWithOptionalPayload + | ActionCreatorWithPreparedPayload +>(actionPattern: Action, fn: (action: ReturnType) => Generator>) { + function* wrapper(action: ReturnType) { + try { + yield* fn(action); + } catch (error) { + handleUncaughtError(error); + } + } + + console.log(actionPattern); + return takeEvery(actionPattern.type, wrapper); +} + +function handleUncaughtError(error: any) { + if (process.env.NODE_ENV === 'development') { + // react-error-overlay is a "special" package that's automatically included + // in development mode by CRA + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + import('react-error-overlay').then(reo => reo.reportRuntimeError(error)); + } + Sentry.captureException(error); + console.error(error); +} diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index d48d5efddb..c872ec39ea 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -1,8 +1,13 @@ /*eslint no-eval: "error"*/ /*eslint-env browser*/ import { SagaIterator } from 'redux-saga'; -import { call, put, select } from 'redux-saga/effects'; -import { ADD_NEW_USERS_TO_COURSE, CREATE_COURSE } from 'src/features/academy/AcademyTypes'; +import { all, call, fork, put, select } from 'redux-saga/effects'; +import { + ADD_NEW_STORIES_USERS_TO_COURSE, + ADD_NEW_USERS_TO_COURSE, + CREATE_COURSE +} from 'src/features/academy/AcademyTypes'; +import { postNewStoriesUsers } from 'src/features/stories/storiesComponents/BackendAccess'; import { UsernameRoleGroup } from 'src/pages/academy/adminPanel/subcomponents/AddUserPanel'; import { @@ -33,57 +38,17 @@ import { } from '../../features/sourceRecorder/SourceRecorderTypes'; import { DELETE_SOURCECAST_ENTRY } from '../../features/sourceRecorder/sourcereel/SourcereelTypes'; import { TeamFormationOverview } from '../../features/teamFormation/TeamFormationTypes'; +import SessionActions from '../application/actions/SessionActions'; import { OverallState, Role } from '../application/ApplicationTypes'; import { RouterState } from '../application/types/CommonsTypes'; import { - ACKNOWLEDGE_NOTIFICATIONS, AdminPanelCourseRegistration, - BULK_UPLOAD_TEAM, - CHECK_ANSWER_LAST_MODIFIED_AT, CourseConfiguration, CourseRegistration, - CREATE_TEAM, - DELETE_ASSESSMENT_CONFIG, - DELETE_TEAM, - DELETE_TIME_OPTIONS, - DELETE_USER_COURSE_REGISTRATION, - FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, - FETCH_ASSESSMENT, - FETCH_ASSESSMENT_ADMIN, - FETCH_ASSESSMENT_CONFIGS, - FETCH_AUTH, - FETCH_CONFIGURABLE_NOTIFICATION_CONFIGS, - FETCH_COURSE_CONFIG, - FETCH_GRADING, - FETCH_GRADING_OVERVIEWS, - FETCH_NOTIFICATION_CONFIGS, - FETCH_NOTIFICATIONS, - FETCH_STUDENTS, - FETCH_TEAM_FORMATION_OVERVIEW, - FETCH_TEAM_FORMATION_OVERVIEWS, - FETCH_TOTAL_XP, - FETCH_TOTAL_XP_ADMIN, - FETCH_USER_AND_COURSE, NotificationConfiguration, - PUBLISH_GRADING, - REAUTOGRADE_ANSWER, - REAUTOGRADE_SUBMISSION, - SUBMIT_ANSWER, - SUBMIT_GRADING, - SUBMIT_GRADING_AND_CONTINUE, TimeOption, Tokens, - UNPUBLISH_GRADING, - UNSUBMIT_SUBMISSION, - UPDATE_ASSESSMENT_CONFIGS, - UPDATE_COURSE_CONFIG, UPDATE_COURSE_RESEARCH_AGREEMENT, - UPDATE_LATEST_VIEWED_COURSE, - UPDATE_NOTIFICATION_CONFIG, - UPDATE_NOTIFICATION_PREFERENCES, - UPDATE_TEAM, - UPDATE_TIME_OPTIONS, - UPDATE_USER_ROLE, UpdateCourseConfiguration, User } from '../application/types/SessionTypes'; @@ -92,15 +57,14 @@ import { AssessmentConfiguration, AssessmentOverview, AssessmentStatuses, - FETCH_ASSESSMENT_OVERVIEWS, ProgressStatuses, - Question, - SUBMIT_ASSESSMENT + Question } from '../assessment/AssessmentTypes'; import { Notification, NotificationFilterFunction } from '../notificationBadge/NotificationBadgeTypes'; +import { combineSagaHandlers } from '../redux/utils'; import { actions } from '../utils/ActionsHelper'; import { computeRedirectUri, getClientId, getDefaultProvider } from '../utils/AuthHelper'; import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; @@ -178,8 +142,8 @@ export function* routerNavigate(path: string) { return router?.navigate(path); } -function* BackendSaga(): SagaIterator { - yield takeEvery(FETCH_AUTH, function* (action: ReturnType): any { +const newBackendSagaOne = combineSagaHandlers(SessionActions, { + fetchAuth: function* (action): any { const { code, providerId: payloadProviderId } = action.payload; const providerId = payloadProviderId || (getDefaultProvider() || [null])[0]; @@ -245,53 +209,48 @@ function* BackendSaga(): SagaIterator { * - However, the current router instance we have access to HERE in this saga via `routerNavigate` is the old router instance. * - Thus handling navigation in allows us to directly access the latest router via `useNavigate`. */ - }); + }, + fetchUserAndCourse: function* (action) { + const tokens: Tokens = yield selectTokens(); - yield takeEvery( - FETCH_USER_AND_COURSE, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); + const { + user, + courseRegistration, + courseConfiguration, + assessmentConfigurations + }: { + user: User | null; + courseRegistration: CourseRegistration | null; + courseConfiguration: CourseConfiguration | null; + assessmentConfigurations: AssessmentConfiguration[] | null; + } = yield call(getUser, tokens); - const { - user, - courseRegistration, - courseConfiguration, - assessmentConfigurations - }: { - user: User | null; - courseRegistration: CourseRegistration | null; - courseConfiguration: CourseConfiguration | null; - assessmentConfigurations: AssessmentConfiguration[] | null; - } = yield call(getUser, tokens); - - if (!user) { - return; - } + if (!user) { + return; + } - yield put(actions.setUser(user)); + yield put(actions.setUser(user)); - // Handle case where user does not have a latest viewed course in the backend - // but is enrolled in some course (this happens occationally due to e.g. removal from a course) - if (courseConfiguration === null && user.courses.length > 0) { - yield put(actions.updateLatestViewedCourse(user.courses[0].courseId)); - } + // Handle case where user does not have a latest viewed course in the backend + // but is enrolled in some course (this happens occationally due to e.g. removal from a course) + if (courseConfiguration === null && user.courses.length > 0) { + yield put(actions.updateLatestViewedCourse(user.courses[0].courseId)); + } + + if (courseRegistration && courseConfiguration && assessmentConfigurations) { + yield put(actions.setCourseRegistration(courseRegistration)); + yield put(actions.setCourseConfiguration(courseConfiguration)); + yield put(actions.setAssessmentConfigurations(assessmentConfigurations)); - if (courseRegistration && courseConfiguration && assessmentConfigurations) { - yield put(actions.setCourseRegistration(courseRegistration)); - yield put(actions.setCourseConfiguration(courseConfiguration)); - yield put(actions.setAssessmentConfigurations(assessmentConfigurations)); - - if (courseConfiguration.enableStories) { - yield put(actions.getStoriesUser()); - // TODO: Fetch associated stories group ID - } else { - yield put(actions.clearStoriesUserAndGroup()); - } + if (courseConfiguration.enableStories) { + yield put(actions.getStoriesUser()); + // TODO: Fetch associated stories group ID + } else { + yield put(actions.clearStoriesUserAndGroup()); } } - ); - - yield takeEvery(FETCH_COURSE_CONFIG, function* () { + }, + fetchCourseConfig: function* () { const tokens: Tokens = yield selectTokens(); const { config }: { config: CourseConfiguration | null } = yield call(getCourseConfig, tokens); if (config) { @@ -304,9 +263,8 @@ function* BackendSaga(): SagaIterator { yield put(actions.clearStoriesUserAndGroup()); } } - }); - - yield takeEvery(FETCH_ASSESSMENT_OVERVIEWS, function* () { + }, + fetchAssessmentOverviews: function* () { const tokens: Tokens = yield selectTokens(); const assessmentOverviews: AssessmentOverview[] | null = yield call( @@ -316,32 +274,26 @@ function* BackendSaga(): SagaIterator { if (assessmentOverviews) { yield put(actions.updateAssessmentOverviews(assessmentOverviews)); } - }); - - yield takeEvery(FETCH_TOTAL_XP, function* () { + }, + fetchTotalXp: function* () { const tokens: Tokens = yield selectTokens(); const res: { totalXp: number } = yield call(getTotalXp, tokens); if (res) { yield put(actions.updateTotalXp(res.totalXp)); } - }); - - yield takeEvery( - FETCH_TOTAL_XP_ADMIN, - function* (action: ReturnType) { - const tokens: Tokens = yield selectTokens(); + }, + fetchTotalXpAdmin: function* (action) { + const tokens: Tokens = yield selectTokens(); - const courseRegId = action.payload; + const courseRegId = action.payload; - const res: { totalXp: number } = yield call(getTotalXp, tokens, courseRegId); - if (res) { - yield put(actions.updateTotalXp(res.totalXp)); - } + const res: { totalXp: number } = yield call(getTotalXp, tokens, courseRegId); + if (res) { + yield put(actions.updateTotalXp(res.totalXp)); } - ); - - yield takeEvery(FETCH_ASSESSMENT, function* (action: ReturnType) { + }, + fetchAssessment: function* (action) { const tokens: Tokens = yield selectTokens(); const { assessmentId, assessmentPassword } = action.payload; @@ -356,28 +308,23 @@ function* BackendSaga(): SagaIterator { if (assessment) { yield put(actions.updateAssessment(assessment)); } - }); - - yield takeEvery( - FETCH_ASSESSMENT_ADMIN, - function* (action: ReturnType) { - const tokens: Tokens = yield selectTokens(); + }, + fetchAssessmentAdmin: function* (action) { + const tokens: Tokens = yield selectTokens(); - const { assessmentId, courseRegId } = action.payload; + const { assessmentId, courseRegId } = action.payload; - const assessment: Assessment | null = yield call( - getAssessment, - assessmentId, - tokens, - courseRegId - ); - if (assessment) { - yield put(actions.updateAssessment(assessment)); - } + const assessment: Assessment | null = yield call( + getAssessment, + assessmentId, + tokens, + courseRegId + ); + if (assessment) { + yield put(actions.updateAssessment(assessment)); } - ); - - yield takeEvery(SUBMIT_ANSWER, function* (action: ReturnType): any { + }, + submitAnswer: function* (action) { const tokens: Tokens = yield selectTokens(); const questionId = action.payload.id; const answer = action.payload.answer; @@ -409,98 +356,81 @@ function* BackendSaga(): SagaIterator { yield put(actions.updateAssessment(newAssessment)); return yield put(actions.updateHasUnsavedChanges('assessment' as WorkspaceLocation, false)); - }); - - yield takeEvery( - CHECK_ANSWER_LAST_MODIFIED_AT, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const questionId = action.payload.id; - const lastModifiedAt = action.payload.lastModifiedAt; - const saveAnswer = action.payload.saveAnswer; - - const resp: boolean | null = yield call( - checkAnswerLastModifiedAt, - questionId, - lastModifiedAt, - tokens - ); - saveAnswer(resp); - } - ); - - yield takeEvery( - SUBMIT_ASSESSMENT, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const assessmentId = action.payload; - - const resp: Response | null = yield call(postAssessment, assessmentId, tokens); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } - - yield call(showSuccessMessage, 'Submitted!', 2000); + }, + checkAnswerLastModifiedAt: function* (action) { + const tokens: Tokens = yield selectTokens(); + const questionId = action.payload.id; + const lastModifiedAt = action.payload.lastModifiedAt; + const saveAnswer = action.payload.saveAnswer; - // Now, update the status of the assessment overview in the store - const overviews: AssessmentOverview[] = yield select( - (state: OverallState) => state.session.assessmentOverviews - ); - const newOverviews = overviews.map(overview => { - if (overview.id === assessmentId) { - return { ...overview, status: AssessmentStatuses.submitted }; - } - return overview; - }); + const resp: boolean | null = yield call( + checkAnswerLastModifiedAt, + questionId, + lastModifiedAt, + tokens + ); + saveAnswer(resp); + }, + submitAssessment: function* (action) { + const tokens: Tokens = yield selectTokens(); + const assessmentId = action.payload; - return yield put(actions.updateAssessmentOverviews(newOverviews)); + const resp: Response | null = yield call(postAssessment, assessmentId, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); - yield takeEvery( - FETCH_GRADING_OVERVIEWS, - function* (action: ReturnType) { - const tokens: Tokens = yield selectTokens(); + yield call(showSuccessMessage, 'Submitted!', 2000); - const role: Role = yield select((state: OverallState) => state.session.role!); - if (role === Role.Student) { - return; + // Now, update the status of the assessment overview in the store + const overviews: AssessmentOverview[] = yield select( + (state: OverallState) => state.session.assessmentOverviews + ); + const newOverviews = overviews.map(overview => { + if (overview.id === assessmentId) { + return { ...overview, status: AssessmentStatuses.submitted }; } + return overview; + }); - const { filterToGroup, publishedFilter, pageParams, filterParams } = action.payload; + return yield put(actions.updateAssessmentOverviews(newOverviews)); + }, + fetchGradingOverviews: function* (action) { + const tokens: Tokens = yield selectTokens(); - const gradingOverviews: GradingOverviews | null = yield call( - getGradingOverviews, - tokens, - filterToGroup, - publishedFilter, - pageParams, - filterParams - ); - if (gradingOverviews) { - yield put(actions.updateGradingOverviews(gradingOverviews)); - } + const role: Role = yield select((state: OverallState) => state.session.role!); + if (role === Role.Student) { + return; } - ); - yield takeEvery( - FETCH_TEAM_FORMATION_OVERVIEW, - function* (action: ReturnType) { - const tokens: Tokens = yield selectTokens(); - const { assessmentId } = action.payload; + const { filterToGroup, publishedFilter, pageParams, filterParams } = action.payload; - const teamFormationOverview: TeamFormationOverview | null = yield call( - getTeamFormationOverview, - assessmentId, - tokens - ); - if (teamFormationOverview) { - yield put(actions.updateTeamFormationOverview(teamFormationOverview)); - } + const gradingOverviews: GradingOverviews | null = yield call( + getGradingOverviews, + tokens, + filterToGroup, + publishedFilter, + pageParams, + filterParams + ); + if (gradingOverviews) { + yield put(actions.updateGradingOverviews(gradingOverviews)); } - ); + }, + fetchTeamFormationOverview: function* (action) { + const tokens: Tokens = yield selectTokens(); + const { assessmentId } = action.payload; - yield takeEvery(FETCH_TEAM_FORMATION_OVERVIEWS, function* () { + const teamFormationOverview: TeamFormationOverview | null = yield call( + getTeamFormationOverview, + assessmentId, + tokens + ); + if (teamFormationOverview) { + yield put(actions.updateTeamFormationOverview(teamFormationOverview)); + } + }, + fetchTeamFormationOverviews: function* () { const tokens: Tokens = yield selectTokens(); const role: Role = yield select((state: OverallState) => state.session.role!); @@ -515,9 +445,8 @@ function* BackendSaga(): SagaIterator { if (teamFormationOverviews) { yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); } - }); - - yield takeEvery(FETCH_STUDENTS, function* (): any { + }, + fetchStudents: function* () { const tokens: Tokens = yield selectTokens(); const role: Role = yield select((state: OverallState) => state.session.role!); if (role === Role.Student) { @@ -527,9 +456,8 @@ function* BackendSaga(): SagaIterator { if (students) { yield put(actions.updateStudents(students)); } - }); - - yield takeEvery(CREATE_TEAM, function* (action: ReturnType): any { + }, + createTeam: function* (action) { const tokens: Tokens = yield selectTokens(); const { assessment, teams } = action.payload; @@ -548,37 +476,32 @@ function* BackendSaga(): SagaIterator { if (resp && resp.status === 409) { return yield call(showWarningMessage, resp.statusText); } - }); - - yield takeEvery( - BULK_UPLOAD_TEAM, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { assessment, file, students } = action.payload; - - const resp: Response | null = yield call( - postUploadTeams, - assessment.id, - file, - students, - tokens - ); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } - const teamFormationOverviews: TeamFormationOverview[] | null = yield call( - getTeamFormationOverviews, - tokens - ); + }, + bulkUploadTeam: function* (action) { + const tokens: Tokens = yield selectTokens(); + const { assessment, file, students } = action.payload; - yield call(showSuccessMessage, 'Team created successfully', 1000); - if (teamFormationOverviews) { - yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); - } + const resp: Response | null = yield call( + postUploadTeams, + assessment.id, + file, + students, + tokens + ); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); + const teamFormationOverviews: TeamFormationOverview[] | null = yield call( + getTeamFormationOverviews, + tokens + ); - yield takeEvery(UPDATE_TEAM, function* (action: ReturnType): any { + yield call(showSuccessMessage, 'Team created successfully', 1000); + if (teamFormationOverviews) { + yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); + } + }, + updateTeam: function* (action) { const tokens: Tokens = yield selectTokens(); const { teamId, assessment, teams } = action.payload; const resp: Response | null = yield call(putTeams, assessment.id, teamId, teams, tokens); @@ -594,9 +517,8 @@ function* BackendSaga(): SagaIterator { if (teamFormationOverviews) { yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); } - }); - - yield takeEvery(DELETE_TEAM, function* (action: ReturnType): any { + }, + deleteTeam: function* (action) { const tokens: Tokens = yield selectTokens(); const { teamId } = action.payload; @@ -613,9 +535,8 @@ function* BackendSaga(): SagaIterator { if (teamFormationOverviews) { yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); } - }); - - yield takeEvery(FETCH_GRADING, function* (action: ReturnType) { + }, + fetchGrading: function* (action) { const tokens: Tokens = yield selectTokens(); const id = action.payload; @@ -623,251 +544,218 @@ function* BackendSaga(): SagaIterator { if (grading) { yield put(actions.updateGrading(id, grading)); } - }); - + }, /** * Unsubmits the submission and updates the grading overviews of the new status. */ - yield takeEvery( - UNSUBMIT_SUBMISSION, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { submissionId } = action.payload; - - const resp: Response | null = yield postUnsubmit(submissionId, tokens); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } - - const overviews: GradingOverview[] = yield select( - (state: OverallState) => state.session.gradingOverviews?.data || [] - ); - const newOverviews = overviews.map(overview => { - if (overview.submissionId === submissionId) { - return { ...overview, progress: ProgressStatuses.attempted }; - } - return overview; - }); - - const totalPossibleEntries = yield select( - (state: OverallState) => state.session.gradingOverviews?.count - ); + unsubmitSubmission: function* (action) { + const tokens: Tokens = yield selectTokens(); + const { submissionId } = action.payload; - yield call(showSuccessMessage, 'Unsubmit successful', 1000); - yield put( - actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews }) - ); + const resp: Response | null = yield postUnsubmit(submissionId, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); - - yield takeEvery( - PUBLISH_GRADING, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { submissionId } = action.payload; - const resp: Response | null = yield publishGrading(submissionId, tokens); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); + const overviews: GradingOverview[] = yield select( + (state: OverallState) => state.session.gradingOverviews?.data || [] + ); + const newOverviews = overviews.map(overview => { + if (overview.submissionId === submissionId) { + return { ...overview, progress: ProgressStatuses.attempted }; } + return overview; + }); - const overviews: GradingOverview[] = yield select( - (state: OverallState) => state.session.gradingOverviews?.data || [] - ); - const newOverviews = overviews.map(overview => { - if (overview.submissionId === submissionId) { - return { ...overview, progress: ProgressStatuses.published }; - } - return overview; - }); + const totalPossibleEntries = yield select( + (state: OverallState) => state.session.gradingOverviews?.count + ); - const totalPossibleEntries = yield select( - (state: OverallState) => state.session.gradingOverviews?.count - ); + yield call(showSuccessMessage, 'Unsubmit successful', 1000); + yield put(actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews })); + }, + publishGrading: function* (action) { + const tokens: Tokens = yield selectTokens(); + const { submissionId } = action.payload; - yield call(showSuccessMessage, 'Publish grading successful', 1000); - yield put( - actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews }) - ); + const resp: Response | null = yield publishGrading(submissionId, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); - yield takeEvery( - UNPUBLISH_GRADING, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { submissionId } = action.payload; - - const resp: Response | null = yield unpublishGrading(submissionId, tokens); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); + const overviews: GradingOverview[] = yield select( + (state: OverallState) => state.session.gradingOverviews?.data || [] + ); + const newOverviews = overviews.map(overview => { + if (overview.submissionId === submissionId) { + return { ...overview, progress: ProgressStatuses.published }; } + return overview; + }); - const overviews: GradingOverview[] = yield select( - (state: OverallState) => state.session.gradingOverviews?.data || [] - ); - const newOverviews = overviews.map(overview => { - if (overview.submissionId === submissionId) { - return { ...overview, progress: ProgressStatuses.graded }; - } - return overview; - }); - - const totalPossibleEntries = yield select( - (state: OverallState) => state.session.gradingOverviews?.count - ); - - yield call(showSuccessMessage, 'Unpublish grading successful', 1000); - yield put( - actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews }) - ); - } - ); - - const sendGrade = function* ( - action: - | ReturnType - | ReturnType - ): any { - const role: Role = yield select((state: OverallState) => state.session.role!); - if (role === Role.Student) { - return yield call(showWarningMessage, 'Only staff can submit answers.'); - } + const totalPossibleEntries = yield select( + (state: OverallState) => state.session.gradingOverviews?.count + ); - const { submissionId, questionId, xpAdjustment, comments } = action.payload; + yield call(showSuccessMessage, 'Publish grading successful', 1000); + yield put(actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews })); + }, + unpublishGrading: function* (action) { const tokens: Tokens = yield selectTokens(); + const { submissionId } = action.payload; - const resp: Response | null = yield postGrading( - submissionId, - questionId, - xpAdjustment, - tokens, - comments - ); + const resp: Response | null = yield unpublishGrading(submissionId, tokens); if (!resp || !resp.ok) { return yield handleResponseError(resp); } - yield call(showSuccessMessage, 'Submitted!', 1000); - - // Now, update the grade for the question in the Grading in the store - const grading: GradingQuery = yield select( - (state: OverallState) => state.session.gradings[submissionId] + const overviews: GradingOverview[] = yield select( + (state: OverallState) => state.session.gradingOverviews?.data || [] ); - const newGrading = grading.answers.slice().map((gradingQuestion: GradingQuestion) => { - if (gradingQuestion.question.id === questionId) { - gradingQuestion.grade = { - xpAdjustment, - xp: gradingQuestion.grade.xp, - comments, - gradedAt: new Date().toISOString() - }; + const newOverviews = overviews.map(overview => { + if (overview.submissionId === submissionId) { + return { ...overview, progress: ProgressStatuses.graded }; } - return gradingQuestion; + return overview; }); - yield put( - actions.updateGrading(submissionId, { answers: newGrading, assessment: grading.assessment }) + const totalPossibleEntries = yield select( + (state: OverallState) => state.session.gradingOverviews?.count ); - }; - - const sendGradeAndContinue = function* ( - action: ReturnType - ) { - yield* sendGrade(action); - const { submissionId } = action.payload; - const [currentQuestion, courseId]: [number | undefined, number] = yield select( - (state: OverallState) => [state.workspaces.grading.currentQuestion, state.session.courseId!] - ); + yield call(showSuccessMessage, 'Unpublish grading successful', 1000); + yield put(actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews })); + }, + submitGrading: sendGrade, + submitGradingAndContinue: sendGradeAndContinue +}); + +function* sendGrade( + action: + | ReturnType + | ReturnType +): any { + const role: Role = yield select((state: OverallState) => state.session.role!); + if (role === Role.Student) { + return yield call(showWarningMessage, 'Only staff can submit answers.'); + } - /** - * Move to next question for grading: this only works because the - * SUBMIT_GRADING_AND_CONTINUE Redux action is currently only - * used in the Grading Workspace - * - * If the questionId is out of bounds, the componentDidUpdate callback of - * GradingWorkspace will cause a redirect back to '/academy/grading' - */ - yield routerNavigate( - `/courses/${courseId}/grading/${submissionId}/${(currentQuestion || 0) + 1}` - ); - }; + const { submissionId, questionId, xpAdjustment, comments } = action.payload; + const tokens: Tokens = yield selectTokens(); - yield takeEvery(SUBMIT_GRADING, sendGrade); + const resp: Response | null = yield postGrading( + submissionId, + questionId, + xpAdjustment, + tokens, + comments + ); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } - yield takeEvery(SUBMIT_GRADING_AND_CONTINUE, sendGradeAndContinue); + yield call(showSuccessMessage, 'Submitted!', 1000); - yield takeEvery( - REAUTOGRADE_SUBMISSION, - function* (action: ReturnType) { - const submissionId = action.payload; - const tokens: Tokens = yield selectTokens(); - const resp: Response | null = yield call(postReautogradeSubmission, submissionId, tokens); + // Now, update the grade for the question in the Grading in the store + const grading: GradingQuery = yield select( + (state: OverallState) => state.session.gradings[submissionId] + ); + const newGrading = grading.answers.slice().map((gradingQuestion: GradingQuestion) => { + if (gradingQuestion.question.id === questionId) { + gradingQuestion.grade = { + xpAdjustment, + xp: gradingQuestion.grade.xp, + comments, + gradedAt: new Date().toISOString() + }; + } + return gradingQuestion; + }); - yield call(handleReautogradeResponse, resp); - } + yield put( + actions.updateGrading(submissionId, { answers: newGrading, assessment: grading.assessment }) ); +} - yield takeEvery( - REAUTOGRADE_ANSWER, - function* (action: ReturnType) { - const { submissionId, questionId } = action.payload; - const tokens: Tokens = yield selectTokens(); - const resp: Response | null = yield call( - postReautogradeAnswer, - submissionId, - questionId, - tokens - ); +function* sendGradeAndContinue(action: ReturnType) { + yield* sendGrade(action); - yield call(handleReautogradeResponse, resp); - } + const { submissionId } = action.payload; + const [currentQuestion, courseId]: [number | undefined, number] = yield select( + (state: OverallState) => [state.workspaces.grading.currentQuestion, state.session.courseId!] ); - yield takeEvery( - FETCH_NOTIFICATIONS, - function* (action: ReturnType) { - const tokens: Tokens = yield selectTokens(); - const notifications: Notification[] = yield call(getNotifications, tokens); - - yield put(actions.updateNotifications(notifications)); - } + /** + * Move to next question for grading: this only works because the + * SUBMIT_GRADING_AND_CONTINUE Redux action is currently only + * used in the Grading Workspace + * + * If the questionId is out of bounds, the componentDidUpdate callback of + * GradingWorkspace will cause a redirect back to '/academy/grading' + */ + yield routerNavigate( + `/courses/${courseId}/grading/${submissionId}/${(currentQuestion || 0) + 1}` ); +} - yield takeEvery( - ACKNOWLEDGE_NOTIFICATIONS, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const notificationFilter: NotificationFilterFunction | undefined = action.payload.withFilter; - const notifications: Notification[] = yield select( - (state: OverallState) => state.session.notifications - ); +const newBackendSagaTwo = combineSagaHandlers(SessionActions, { + reautogradeSubmission: function* (action) { + const submissionId = action.payload; + const tokens: Tokens = yield selectTokens(); + const resp: Response | null = yield call(postReautogradeSubmission, submissionId, tokens); - let notificationsToAcknowledge = notifications; + yield call(handleReautogradeResponse, resp); + }, + reautogradeAnswer: function* (action) { + const { submissionId, questionId } = action.payload; + const tokens: Tokens = yield selectTokens(); + const resp: Response | null = yield call( + postReautogradeAnswer, + submissionId, + questionId, + tokens + ); - if (notificationFilter) { - notificationsToAcknowledge = notificationFilter(notifications); - } + yield call(handleReautogradeResponse, resp); + }, + fetchNotifications: function* (action) { + const tokens: Tokens = yield selectTokens(); + const notifications: Notification[] = yield call(getNotifications, tokens); - if (notificationsToAcknowledge.length === 0) { - return; - } + yield put(actions.updateNotifications(notifications)); + }, + acknowledgeNotifications: function* (action) { + const tokens: Tokens = yield selectTokens(); + const notificationFilter: NotificationFilterFunction | undefined = action.payload.withFilter; + const notifications: Notification[] = yield select( + (state: OverallState) => state.session.notifications + ); - const ids = notificationsToAcknowledge.map(n => n.id); - const newNotifications: Notification[] = notifications.filter( - notification => !ids.includes(notification.id) - ); + let notificationsToAcknowledge = notifications; - yield put(actions.updateNotifications(newNotifications)); + if (notificationFilter) { + notificationsToAcknowledge = notificationFilter(notifications); + } - const resp: Response | null = yield call(postAcknowledgeNotifications, tokens, ids); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + if (notificationsToAcknowledge.length === 0) { + return; } - ); + const ids = notificationsToAcknowledge.map(n => n.id); + const newNotifications: Notification[] = notifications.filter( + notification => !ids.includes(notification.id) + ); + + yield put(actions.updateNotifications(newNotifications)); + + const resp: Response | null = yield call(postAcknowledgeNotifications, tokens, ids); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + } +}); + +function* oldBackendSagaOne(): SagaIterator { yield takeEvery( DELETE_SOURCECAST_ENTRY, function* (action: ReturnType): any { @@ -959,72 +847,66 @@ function* BackendSaga(): SagaIterator { yield call(showSuccessMessage, 'Updated successfully!', 1000); } ); +} - yield takeEvery( - UPDATE_LATEST_VIEWED_COURSE, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { courseId } = action.payload; +const newBackendSagaThree = combineSagaHandlers(SessionActions, { + updateLatestViewedCourse: function* (action) { + const tokens: Tokens = yield selectTokens(); + const { courseId } = action.payload; - const resp: Response | null = yield call(putLatestViewedCourse, tokens, courseId); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + const resp: Response | null = yield call(putLatestViewedCourse, tokens, courseId); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } - const { - courseRegistration, - courseConfiguration, - assessmentConfigurations - }: { - courseRegistration: CourseRegistration | null; - courseConfiguration: CourseConfiguration | null; - assessmentConfigurations: AssessmentConfiguration[] | null; - } = yield call(getLatestCourseRegistrationAndConfiguration, tokens); - - if (!courseRegistration || !courseConfiguration || !assessmentConfigurations) { - yield call(showWarningMessage, `Failed to load course!`); - return yield routerNavigate('/welcome'); - } + const { + courseRegistration, + courseConfiguration, + assessmentConfigurations + }: { + courseRegistration: CourseRegistration | null; + courseConfiguration: CourseConfiguration | null; + assessmentConfigurations: AssessmentConfiguration[] | null; + } = yield call(getLatestCourseRegistrationAndConfiguration, tokens); - yield put(actions.setCourseConfiguration(courseConfiguration)); - yield put(actions.setAssessmentConfigurations(assessmentConfigurations)); - yield put(actions.setCourseRegistration(courseRegistration)); + if (!courseRegistration || !courseConfiguration || !assessmentConfigurations) { + yield call(showWarningMessage, `Failed to load course!`); + return yield routerNavigate('/welcome'); + } - if (courseConfiguration.enableStories) { - yield put(actions.getStoriesUser()); - // TODO: Fetch associated stories group ID - } else { - yield put(actions.clearStoriesUserAndGroup()); - } + yield put(actions.setCourseConfiguration(courseConfiguration)); + yield put(actions.setAssessmentConfigurations(assessmentConfigurations)); + yield put(actions.setCourseRegistration(courseRegistration)); - yield call(showSuccessMessage, `Switched to ${courseConfiguration.courseName}!`, 5000); + if (courseConfiguration.enableStories) { + yield put(actions.getStoriesUser()); + // TODO: Fetch associated stories group ID + } else { + yield put(actions.clearStoriesUserAndGroup()); } - ); - yield takeEvery( - UPDATE_COURSE_CONFIG, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const courseConfig: UpdateCourseConfiguration = action.payload; - - const resp: Response | null = yield call(putCourseConfig, tokens, courseConfig); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + yield call(showSuccessMessage, `Switched to ${courseConfiguration.courseName}!`, 5000); + }, + updateCourseConfig: function* (action) { + const tokens: Tokens = yield selectTokens(); + const courseConfig: UpdateCourseConfiguration = action.payload; - if (courseConfig.enableStories) { - yield put(actions.getStoriesUser()); - // TODO: Fetch associated stories group ID - } else { - yield put(actions.clearStoriesUserAndGroup()); - } + const resp: Response | null = yield call(putCourseConfig, tokens, courseConfig); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } - yield put(actions.setCourseConfiguration(courseConfig)); - yield call(showSuccessMessage, 'Updated successfully!', 1000); + if (courseConfig.enableStories) { + yield put(actions.getStoriesUser()); + // TODO: Fetch associated stories group ID + } else { + yield put(actions.clearStoriesUserAndGroup()); } - ); - yield takeEvery(FETCH_ASSESSMENT_CONFIGS, function* (): any { + yield put(actions.setCourseConfiguration(courseConfig)); + yield call(showSuccessMessage, 'Updated successfully!', 1000); + }, + fetchAssessmentConfigs: function* () { const tokens: Tokens = yield selectTokens(); const assessmentConfigs: AssessmentConfiguration[] | null = yield call( @@ -1034,27 +916,22 @@ function* BackendSaga(): SagaIterator { if (assessmentConfigs) { yield put(actions.setAssessmentConfigurations(assessmentConfigs)); } - }); - - yield takeEvery( - FETCH_CONFIGURABLE_NOTIFICATION_CONFIGS, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { courseRegId }: { courseRegId: number } = action.payload; + }, + fetchConfigurableNotificationConfigs: function* (action) { + const tokens: Tokens = yield selectTokens(); + const { courseRegId }: { courseRegId: number } = action.payload; - const notificationConfigs: NotificationConfiguration[] | null = yield call( - getConfigurableNotificationConfigs, - tokens, - courseRegId - ); + const notificationConfigs: NotificationConfiguration[] | null = yield call( + getConfigurableNotificationConfigs, + tokens, + courseRegId + ); - if (notificationConfigs) { - yield put(actions.setConfigurableNotificationConfigs(notificationConfigs)); - } + if (notificationConfigs) { + yield put(actions.setConfigurableNotificationConfigs(notificationConfigs)); } - ); - - yield takeEvery(FETCH_NOTIFICATION_CONFIGS, function* (): any { + }, + fetchNotificationConfigs: function* () { const tokens: Tokens = yield selectTokens(); const notificationConfigs: NotificationConfiguration[] | null = yield call( @@ -1065,128 +942,102 @@ function* BackendSaga(): SagaIterator { if (notificationConfigs) { yield put(actions.setNotificationConfigs(notificationConfigs)); } - }); - - yield takeEvery( - UPDATE_ASSESSMENT_CONFIGS, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const assessmentConfigs: AssessmentConfiguration[] = action.payload; - - const resp: Response | null = yield call(putAssessmentConfigs, tokens, assessmentConfigs); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } - - const updatedAssessmentConfigs: AssessmentConfiguration[] | null = yield call( - getAssessmentConfigs, - tokens - ); + }, + updateAssessmentConfigs: function* (action) { + const tokens: Tokens = yield selectTokens(); + const assessmentConfigs: AssessmentConfiguration[] = action.payload; - if (updatedAssessmentConfigs) { - yield put(actions.setAssessmentConfigurations(updatedAssessmentConfigs)); - } - yield call(showSuccessMessage, 'Updated successfully!', 1000); + const resp: Response | null = yield call(putAssessmentConfigs, tokens, assessmentConfigs); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); - - yield takeEvery( - UPDATE_NOTIFICATION_CONFIG, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const notificationConfigs: NotificationConfiguration[] = action.payload; - - const resp: Response | null = yield call(putNotificationConfigs, tokens, notificationConfigs); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } - const updatedNotificationConfigs: NotificationConfiguration[] | null = yield call( - getNotificationConfigs, - tokens - ); + const updatedAssessmentConfigs: AssessmentConfiguration[] | null = yield call( + getAssessmentConfigs, + tokens + ); - if (updatedNotificationConfigs) { - yield put(actions.setNotificationConfigs(updatedNotificationConfigs)); - } + if (updatedAssessmentConfigs) { + yield put(actions.setAssessmentConfigurations(updatedAssessmentConfigs)); + } + yield call(showSuccessMessage, 'Updated successfully!', 1000); + }, + updateNotificationConfigs: function* (action) { + const tokens: Tokens = yield selectTokens(); + const notificationConfigs: NotificationConfiguration[] = action.payload; - yield call(showSuccessMessage, 'Updated successfully!', 1000); + const resp: Response | null = yield call(putNotificationConfigs, tokens, notificationConfigs); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); - yield takeEvery( - UPDATE_NOTIFICATION_PREFERENCES, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { notificationPreferences, courseRegId } = action.payload; - const resp: Response | null = yield call( - putNotificationPreferences, - tokens, - notificationPreferences, - courseRegId - ); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + const updatedNotificationConfigs: NotificationConfiguration[] | null = yield call( + getNotificationConfigs, + tokens + ); - yield call(showSuccessMessage, 'Updated successfully!', 1000); + if (updatedNotificationConfigs) { + yield put(actions.setNotificationConfigs(updatedNotificationConfigs)); } - ); - yield takeEvery( - DELETE_ASSESSMENT_CONFIG, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const assessmentConfig: AssessmentConfiguration = action.payload; - - const resp: Response | null = yield call(removeAssessmentConfig, tokens, assessmentConfig); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + yield call(showSuccessMessage, 'Updated successfully!', 1000); + }, + updateNotificationPreferences: function* (action) { + const tokens: Tokens = yield selectTokens(); + const { notificationPreferences, courseRegId } = action.payload; + const resp: Response | null = yield call( + putNotificationPreferences, + tokens, + notificationPreferences, + courseRegId + ); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); - yield takeEvery( - UPDATE_TIME_OPTIONS, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const timeOptions: TimeOption[] = action.payload; + yield call(showSuccessMessage, 'Updated successfully!', 1000); + }, + deleteAssessmentConfig: function* (action) { + const tokens: Tokens = yield selectTokens(); + const assessmentConfig: AssessmentConfiguration = action.payload; - const resp: Response | null = yield call(putTimeOptions, tokens, timeOptions); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + const resp: Response | null = yield call(removeAssessmentConfig, tokens, assessmentConfig); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); - - yield takeEvery( - DELETE_TIME_OPTIONS, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const timeOptionIds: number[] = action.payload; + }, + updateTimeOptions: function* (action) { + const tokens: Tokens = yield selectTokens(); + const timeOptions: TimeOption[] = action.payload; - const resp: Response | null = yield call(removeTimeOptions, tokens, timeOptionIds); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + const resp: Response | null = yield call(putTimeOptions, tokens, timeOptions); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); + }, + deleteTimeOptions: function* (action) { + const tokens: Tokens = yield selectTokens(); + const timeOptionIds: number[] = action.payload; - yield takeEvery( - FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, - function* (action: ReturnType) { - const tokens: Tokens = yield selectTokens(); + const resp: Response | null = yield call(removeTimeOptions, tokens, timeOptionIds); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + }, + fetchAdminPanelCourseRegistrations: function* (action) { + const tokens: Tokens = yield selectTokens(); - const courseRegistrations: AdminPanelCourseRegistration[] | null = yield call( - getUserCourseRegistrations, - tokens - ); - if (courseRegistrations) { - yield put(actions.setAdminPanelCourseRegistrations(courseRegistrations)); - } + const courseRegistrations: AdminPanelCourseRegistration[] | null = yield call( + getUserCourseRegistrations, + tokens + ); + if (courseRegistrations) { + yield put(actions.setAdminPanelCourseRegistrations(courseRegistrations)); } - ); + } +}); +function* oldBackendSagaTwo(): SagaIterator { yield takeEvery(CREATE_COURSE, function* (action: ReturnType): any { const tokens: Tokens = yield selectTokens(); const courseConfig: UpdateCourseConfiguration = action.payload; @@ -1273,18 +1124,15 @@ function* BackendSaga(): SagaIterator { ); yield takeEvery( - UPDATE_USER_ROLE, - function* (action: ReturnType): any { + ADD_NEW_STORIES_USERS_TO_COURSE, + function* (action: ReturnType): any { const tokens: Tokens = yield selectTokens(); - const { courseRegId, role }: { courseRegId: number; role: Role } = action.payload; + const { users, provider } = action.payload; - const resp: Response | null = yield call(putUserRole, tokens, courseRegId, role); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + yield call(postNewStoriesUsers, tokens, users, provider); - yield put(actions.fetchAdminPanelCourseRegistrations()); - yield call(showSuccessMessage, 'Role updated!'); + // TODO: Refresh the list of story users + // once that page is implemented } ); @@ -1307,23 +1155,36 @@ function* BackendSaga(): SagaIterator { yield call(showSuccessMessage, 'Research preference saved!'); } ); +} - yield takeEvery( - DELETE_USER_COURSE_REGISTRATION, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { courseRegId }: { courseRegId: number } = action.payload; +const newBackendSagaFour = combineSagaHandlers(SessionActions, { + updateUserRole: function* (action): any { + const tokens: Tokens = yield selectTokens(); + const { courseRegId, role }: { courseRegId: number; role: Role } = action.payload; - const resp: Response | null = yield call(removeUserCourseRegistration, tokens, courseRegId); - if (!resp || !resp.ok) { - return yield handleResponseError(resp); - } + const resp: Response | null = yield call(putUserRole, tokens, courseRegId, role); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } - yield put(actions.fetchAdminPanelCourseRegistrations()); - yield call(showSuccessMessage, 'User deleted!'); + yield put(actions.fetchAdminPanelCourseRegistrations()); + yield call(showSuccessMessage, 'Role updated!'); + }, + deleteUserCourseRegistration: function* (action): any { + const tokens: Tokens = yield selectTokens(); + const { courseRegId }: { courseRegId: number } = action.payload; + + const resp: Response | null = yield call(removeUserCourseRegistration, tokens, courseRegId); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); } - ); + yield put(actions.fetchAdminPanelCourseRegistrations()); + yield call(showSuccessMessage, 'User deleted!'); + } +}); + +function* oldBackendSagaThree(): SagaIterator { yield takeEvery( FETCH_GROUP_GRADING_SUMMARY, function* (action: ReturnType) { @@ -1529,4 +1390,16 @@ function* handleReautogradeResponse(resp: Response | null): any { return yield call(showWarningMessage, 'Failed to queue autograde job.'); } +function* BackendSaga(): SagaIterator { + yield all([ + fork(newBackendSagaOne), + fork(newBackendSagaTwo), + fork(newBackendSagaThree), + fork(newBackendSagaFour), + fork(oldBackendSagaOne), + fork(oldBackendSagaTwo), + fork(oldBackendSagaThree) + ]); +} + export default BackendSaga; diff --git a/src/commons/sagas/GitHubPersistenceSaga.ts b/src/commons/sagas/GitHubPersistenceSaga.ts index a11896757f..f03523e495 100644 --- a/src/commons/sagas/GitHubPersistenceSaga.ts +++ b/src/commons/sagas/GitHubPersistenceSaga.ts @@ -13,8 +13,8 @@ import { import * as GitHubUtils from '../../features/github/GitHubUtils'; import { getGitHubOctokitInstance } from '../../features/github/GitHubUtils'; import { store } from '../../pages/createStore'; +import { loginGitHub, logoutGitHub } from '../application/actions/SessionActions'; import { OverallState } from '../application/ApplicationTypes'; -import { LOGIN_GITHUB, LOGOUT_GITHUB } from '../application/types/SessionTypes'; import FileExplorerDialog, { FileExplorerDialogProps } from '../gitHubOverlay/FileExplorerDialog'; import RepositoryDialog, { RepositoryDialogProps } from '../gitHubOverlay/RepositoryDialog'; import { actions } from '../utils/ActionsHelper'; @@ -24,8 +24,8 @@ import { showSuccessMessage } from '../utils/notifications/NotificationsHelper'; import { EditorTabState } from '../workspace/WorkspaceTypes'; export function* GitHubPersistenceSaga(): SagaIterator { - yield takeLatest(LOGIN_GITHUB, githubLoginSaga); - yield takeLatest(LOGOUT_GITHUB, githubLogoutSaga); + yield takeLatest(loginGitHub.type, githubLoginSaga); + yield takeLatest(logoutGitHub.type, githubLogoutSaga); yield takeLatest(GITHUB_OPEN_FILE, githubOpenFile); yield takeLatest(GITHUB_SAVE_FILE, githubSaveFile); diff --git a/src/commons/sagas/LoginSaga.ts b/src/commons/sagas/LoginSaga.ts index aa68a1a174..d665b703a2 100644 --- a/src/commons/sagas/LoginSaga.ts +++ b/src/commons/sagas/LoginSaga.ts @@ -2,17 +2,17 @@ import * as Sentry from '@sentry/browser'; import { SagaIterator } from 'redux-saga'; import { call } from 'redux-saga/effects'; +import { login, setUser } from '../application/actions/SessionActions'; import { LOG_OUT } from '../application/types/CommonsTypes'; -import { LOGIN, SET_USER } from '../application/types/SessionTypes'; import { actions } from '../utils/ActionsHelper'; import { computeEndpointUrl } from '../utils/AuthHelper'; import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; import { safeTakeEvery as takeEvery } from './SafeEffects'; export default function* LoginSaga(): SagaIterator { - yield takeEvery(LOGIN, updateLoginHref); + yield takeEvery(login.type, updateLoginHref); - yield takeEvery(SET_USER, (action: ReturnType) => { + yield takeEvery(setUser.type, (action: ReturnType) => { Sentry.setUser({ id: action.payload.userId.toString() }); }); diff --git a/src/commons/sagas/PersistenceSaga.tsx b/src/commons/sagas/PersistenceSaga.tsx index e87e2f923d..8bf139ab08 100644 --- a/src/commons/sagas/PersistenceSaga.tsx +++ b/src/commons/sagas/PersistenceSaga.tsx @@ -11,9 +11,9 @@ import { PersistenceFile } from '../../features/persistence/PersistenceTypes'; import { store } from '../../pages/createStore'; +import { logoutGoogle } from '../application/actions/SessionActions'; import { OverallState } from '../application/ApplicationTypes'; import { ExternalLibraryName } from '../application/types/ExternalTypes'; -import { LOGOUT_GOOGLE } from '../application/types/SessionTypes'; import { actions } from '../utils/ActionsHelper'; import Constants from '../utils/Constants'; import { showSimpleConfirmDialog, showSimplePromptDialog } from '../utils/DialogHelper'; @@ -37,7 +37,7 @@ const MIME_SOURCE = 'text/plain'; // const MIME_FOLDER = 'application/vnd.google-apps.folder'; export function* persistenceSaga(): SagaIterator { - yield takeLatest(LOGOUT_GOOGLE, function* () { + yield takeLatest(logoutGoogle.type, function* () { yield put(actions.playgroundUpdatePersistenceFile(undefined)); yield call(ensureInitialised); yield call([gapi.auth2.getAuthInstance(), 'signOut']); diff --git a/src/commons/sagas/SideContentSaga.ts b/src/commons/sagas/SideContentSaga.ts index 4c8e075803..b6d75e2219 100644 --- a/src/commons/sagas/SideContentSaga.ts +++ b/src/commons/sagas/SideContentSaga.ts @@ -2,7 +2,6 @@ import { Action } from 'redux'; import type { SagaIterator } from 'redux-saga'; import { put, take } from 'redux-saga/effects'; import { notifyStoriesEvaluated } from 'src/features/stories/StoriesActions'; -import { NOTIFY_STORIES_EVALUATED } from 'src/features/stories/StoriesTypes'; import * as actions from '../sideContent/SideContentActions'; import { @@ -52,7 +51,7 @@ export default function* SideContentSaga(): SagaIterator { ); yield takeEvery( - NOTIFY_STORIES_EVALUATED, + notifyStoriesEvaluated.type, function* (action: ReturnType) { yield put(actions.spawnSideContent(`stories.${action.payload.env}`, action.payload)); } diff --git a/src/commons/sagas/StoriesSaga.ts b/src/commons/sagas/StoriesSaga.ts index 67696b7e53..02e2ab483a 100644 --- a/src/commons/sagas/StoriesSaga.ts +++ b/src/commons/sagas/StoriesSaga.ts @@ -1,37 +1,29 @@ -import { SagaIterator } from 'redux-saga'; -import { call, put, select, takeLatest } from 'redux-saga/effects'; -import { ADD_NEW_STORIES_USERS_TO_COURSE } from 'src/features/academy/AcademyTypes'; +import { Context } from 'js-slang'; +import { call, put, select } from 'redux-saga/effects'; +import StoriesActions from 'src/features/stories/StoriesActions'; import { deleteStory, getStories, getStoriesUser, getStory, - postNewStoriesUsers, postStory, updateStory } from 'src/features/stories/storiesComponents/BackendAccess'; -import { - CREATE_STORY, - DELETE_STORY, - GET_STORIES_LIST, - GET_STORIES_USER, - SAVE_STORY, - SET_CURRENT_STORY_ID, - StoryData, - StoryListView, - StoryView -} from 'src/features/stories/StoriesTypes'; +import { StoryData, StoryListView, StoryView } from 'src/features/stories/StoriesTypes'; import { OverallState, StoriesRole } from '../application/ApplicationTypes'; import { Tokens } from '../application/types/SessionTypes'; +import { combineSagaHandlers } from '../redux/utils'; +import { resetSideContent } from '../sideContent/SideContentActions'; import { actions } from '../utils/ActionsHelper'; import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; import { defaultStoryContent } from '../utils/StoriesHelper'; import { selectTokens } from './BackendSaga'; -import { safeTakeEvery as takeEvery } from './SafeEffects'; +import { evalCode } from './WorkspaceSaga/helpers/evalCode'; -export function* storiesSaga(): SagaIterator { - yield takeLatest(GET_STORIES_LIST, function* () { +const StoriesSaga = combineSagaHandlers(StoriesActions, { + // TODO: This should be using `takeLatest`, not `takeEvery` + getStoriesList: function* () { const tokens: Tokens = yield selectTokens(); const allStories: StoryListView[] = yield call(async () => { const resp = await getStories(tokens); @@ -39,36 +31,29 @@ export function* storiesSaga(): SagaIterator { }); yield put(actions.updateStoriesList(allStories)); - }); - - // takeEvery used to ensure that setting to null (clearing the story) is always - // handled even if a refresh is triggered later. - yield takeEvery( - SET_CURRENT_STORY_ID, - function* (action: ReturnType) { - const tokens: Tokens = yield selectTokens(); - const storyId = action.payload; - if (storyId) { - const story: StoryView = yield call(getStory, tokens, storyId); - yield put(actions.setCurrentStory(story)); - } else { - const defaultStory: StoryData = { - title: '', - content: defaultStoryContent, - pinOrder: null - }; - yield put(actions.setCurrentStory(defaultStory)); - } + }, + setCurrentStoryId: function* (action) { + const tokens: Tokens = yield selectTokens(); + const storyId = action.payload; + if (storyId) { + const story: StoryView = yield call(getStory, tokens, storyId); + yield put(actions.setCurrentStory(story)); + } else { + const defaultStory: StoryData = { + title: '', + content: defaultStoryContent, + pinOrder: null + }; + yield put(actions.setCurrentStory(defaultStory)); } - ); - - yield takeEvery(CREATE_STORY, function* (action: ReturnType) { + }, + createStory: function* (action) { const tokens: Tokens = yield selectTokens(); const story = action.payload; const userId: number | undefined = yield select((state: OverallState) => state.stories.userId); if (userId === undefined) { - showWarningMessage('Failed to create story: Invalid user'); + yield call(showWarningMessage, 'Failed to create story: Invalid user'); return; } @@ -87,9 +72,8 @@ export function* storiesSaga(): SagaIterator { } yield put(actions.getStoriesList()); - }); - - yield takeEvery(SAVE_STORY, function* (action: ReturnType) { + }, + saveStory: function* (action) { const tokens: Tokens = yield selectTokens(); const { story, id } = action.payload; const updatedStory: StoryView | null = yield call( @@ -107,17 +91,17 @@ export function* storiesSaga(): SagaIterator { } yield put(actions.getStoriesList()); - }); + }, - yield takeEvery(DELETE_STORY, function* (action: ReturnType) { + deleteStory: function* (action) { const tokens: Tokens = yield selectTokens(); const storyId = action.payload; yield call(deleteStory, tokens, storyId); yield put(actions.getStoriesList()); - }); + }, - yield takeEvery(GET_STORIES_USER, function* () { + getStoriesUser: function* () { const tokens: Tokens = yield selectTokens(); const me: { id: number; @@ -134,20 +118,21 @@ export function* storiesSaga(): SagaIterator { } yield put(actions.setCurrentStoriesUser(me.id, me.name)); yield put(actions.setCurrentStoriesGroup(me.groupId, me.groupName, me.role)); - }); - - yield takeEvery( - ADD_NEW_STORIES_USERS_TO_COURSE, - function* (action: ReturnType): any { - const tokens: Tokens = yield selectTokens(); - const { users, provider } = action.payload; - - yield call(postNewStoriesUsers, tokens, users, provider); - - // TODO: Refresh the list of story users - // once that page is implemented - } - ); -} - -export default storiesSaga; + }, + evalStory: function* (action) { + const env = action.payload.env; + const code = action.payload.code; + const execTime: number = yield select( + (state: OverallState) => state.stories.envs[env].execTime + ); + const context: Context = yield select((state: OverallState) => state.stories.envs[env].context); + const codeFilePath = '/code.js'; + const codeFiles = { + [codeFilePath]: code + }; + yield put(resetSideContent(`stories.${env}`)); + yield call(evalCode, codeFiles, codeFilePath, context, execTime, 'stories', action.type, env); + } +}); + +export default StoriesSaga; diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts index 29426be942..61b5fa0a8c 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts @@ -11,7 +11,6 @@ import * as Sourceror from 'sourceror'; import { makeCCompilerConfig, specialCReturnObject } from 'src/commons/utils/CToWasmHelper'; import { javaRun } from 'src/commons/utils/JavaHelper'; import { notifyStoriesEvaluated } from 'src/features/stories/StoriesActions'; -import { EVAL_STORY } from 'src/features/stories/StoriesTypes'; import { EventType } from '../../../../features/achievement/AchievementTypes'; import { isSchemeLanguage, OverallState } from '../../../application/ApplicationTypes'; @@ -48,7 +47,7 @@ export function* evalCode( ): SagaIterator { context.runtime.debuggerOn = (actionType === EVAL_EDITOR || actionType === DEBUG_RESUME) && context.chapter > 2; - const isStoriesBlock = actionType === EVAL_STORY || workspaceLocation === 'stories'; + const isStoriesBlock = actionType === actions.evalStory.type || workspaceLocation === 'stories'; // Logic for execution of substitution model visualizer const correctWorkspace = workspaceLocation === 'playground' || workspaceLocation === 'sicp'; diff --git a/src/commons/sagas/WorkspaceSaga/index.ts b/src/commons/sagas/WorkspaceSaga/index.ts index 57ccf7f0fe..14436c9eff 100644 --- a/src/commons/sagas/WorkspaceSaga/index.ts +++ b/src/commons/sagas/WorkspaceSaga/index.ts @@ -5,7 +5,6 @@ import Phaser from 'phaser'; import { SagaIterator } from 'redux-saga'; import { call, put, select } from 'redux-saga/effects'; import CseMachine from 'src/features/cseMachine/CseMachine'; -import { EVAL_STORY } from 'src/features/stories/StoriesTypes'; import { EventType } from '../../../features/achievement/AchievementTypes'; import DataVisualizer from '../../../features/dataVisualizer/dataVisualizer'; @@ -25,7 +24,6 @@ import { import { Library, Testcase } from '../../assessment/AssessmentTypes'; import { Documentation } from '../../documentation/Documentation'; import { writeFileRecursively } from '../../fileSystem/utils'; -import { resetSideContent } from '../../sideContent/SideContentActions'; import { actions } from '../../utils/ActionsHelper'; import { highlightClean, @@ -282,21 +280,6 @@ export default function* WorkspaceSaga(): SagaIterator { yield call(evalCode, codeFiles, codeFilePath, context, execTime, workspaceLocation, EVAL_REPL); }); - yield takeEvery(EVAL_STORY, function* (action: ReturnType) { - const env = action.payload.env; - const code = action.payload.code; - const execTime: number = yield select( - (state: OverallState) => state.stories.envs[env].execTime - ); - context = yield select((state: OverallState) => state.stories.envs[env].context); - const codeFilePath = '/code.js'; - const codeFiles = { - [codeFilePath]: code - }; - yield put(resetSideContent(`stories.${env}`)); - yield call(evalCode, codeFiles, codeFilePath, context, execTime, 'stories', EVAL_STORY, env); - }); - yield takeEvery(DEBUG_RESUME, function* (action: ReturnType) { const workspaceLocation = action.payload.workspaceLocation; const code: string = yield select( diff --git a/src/commons/sagas/__tests__/BackendSaga.ts b/src/commons/sagas/__tests__/BackendSaga.ts index fbaaaf7ce9..712ddf071e 100644 --- a/src/commons/sagas/__tests__/BackendSaga.ts +++ b/src/commons/sagas/__tests__/BackendSaga.ts @@ -12,19 +12,37 @@ import { UPDATE_GROUP_GRADING_SUMMARY } from '../../../features/dashboard/DashboardTypes'; import { + acknowledgeNotifications, + deleteUserCourseRegistration, fetchAdminPanelCourseRegistrations, + fetchAssessment, + fetchAssessmentConfigs, + fetchAssessmentOverviews, + fetchAuth, + fetchCourseConfig, + fetchNotifications, + fetchStudents, + fetchTeamFormationOverviews, + fetchUserAndCourse, + reautogradeAnswer, + reautogradeSubmission, setAdminPanelCourseRegistrations, setAssessmentConfigurations, setCourseConfiguration, setCourseRegistration, setTokens, setUser, + submitAnswer, + submitAssessment, updateAssessment, + updateAssessmentConfigs, updateAssessmentOverviews, + updateCourseConfig, updateLatestViewedCourse, updateNotifications, updateStudents, - updateTeamFormationOverviews + updateTeamFormationOverviews, + updateUserRole } from '../../application/actions/SessionActions'; import { GameState, @@ -34,38 +52,11 @@ import { SupportedLanguage } from '../../application/ApplicationTypes'; import { - ACKNOWLEDGE_NOTIFICATIONS, AdminPanelCourseRegistration, CourseConfiguration, CourseRegistration, - DELETE_USER_COURSE_REGISTRATION, - FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS, - FETCH_ASSESSMENT, - FETCH_ASSESSMENT_CONFIGS, - FETCH_AUTH, - FETCH_COURSE_CONFIG, - FETCH_NOTIFICATIONS, - FETCH_STUDENTS, - FETCH_TEAM_FORMATION_OVERVIEWS, - FETCH_USER_AND_COURSE, - REAUTOGRADE_ANSWER, - REAUTOGRADE_SUBMISSION, - SET_ADMIN_PANEL_COURSE_REGISTRATIONS, - SET_ASSESSMENT_CONFIGURATIONS, - SET_COURSE_CONFIGURATION, - SET_COURSE_REGISTRATION, - SET_TOKENS, - SET_USER, - SUBMIT_ANSWER, UPDATE_ASSESSMENT, - UPDATE_ASSESSMENT_CONFIGS, - UPDATE_ASSESSMENT_OVERVIEWS, - UPDATE_COURSE_CONFIG, UPDATE_COURSE_RESEARCH_AGREEMENT, - UPDATE_LATEST_VIEWED_COURSE, - UPDATE_STUDENTS, - UPDATE_TEAM_FORMATION_OVERVIEWS, - UPDATE_USER_ROLE, UpdateCourseConfiguration, User } from '../../application/types/SessionTypes'; @@ -73,9 +64,7 @@ import { Assessment, AssessmentConfiguration, AssessmentStatuses, - FETCH_ASSESSMENT_OVERVIEWS, - Question, - SUBMIT_ASSESSMENT + Question } from '../../assessment/AssessmentTypes'; import { mockAssessmentOverviews, @@ -337,7 +326,7 @@ describe('Test FETCH_AUTH action', () => { .put(setTokens(mockTokens)) .call(getUser, mockTokens) .put(setUser(user)) - .not.put.actionType(UPDATE_LATEST_VIEWED_COURSE) + .not.put.actionType(updateLatestViewedCourse.type) .put(setCourseRegistration(courseRegistration)) .put(setCourseConfiguration(courseConfiguration)) .put(setAssessmentConfigurations(assessmentConfigurations)) @@ -348,7 +337,7 @@ describe('Test FETCH_AUTH action', () => { { user, courseRegistration, courseConfiguration, assessmentConfigurations } ] ]) - .dispatch({ type: FETCH_AUTH, payload: { code, providerId } }) + .dispatch({ type: fetchAuth.type, payload: { code, providerId } }) .silentRun(); }); @@ -357,14 +346,14 @@ describe('Test FETCH_AUTH action', () => { .withState(mockStates) .provide([[call(postAuth, code, providerId, clientId, redirectUrl), null]]) .call(postAuth, code, providerId, clientId, redirectUrl) - .not.put.actionType(SET_TOKENS) + .not.put.actionType(setTokens.type) .not.call.fn(getUser) - .not.put.actionType(SET_USER) - .not.put.actionType(UPDATE_LATEST_VIEWED_COURSE) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) - .dispatch({ type: FETCH_AUTH, payload: { code, providerId } }) + .not.put.actionType(setUser.type) + .not.put.actionType(updateLatestViewedCourse.type) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .not.put.actionType(setAssessmentConfigurations.type) + .dispatch({ type: fetchAuth.type, payload: { code, providerId } }) .silentRun(); }); @@ -388,11 +377,11 @@ describe('Test FETCH_AUTH action', () => { .put(setTokens(mockTokens)) .call(getUser, mockTokens) .put(setUser(userWithNoCourse)) - .not.put.actionType(UPDATE_LATEST_VIEWED_COURSE) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) - .dispatch({ type: FETCH_AUTH, payload: { code, providerId } }) + .not.put.actionType(updateLatestViewedCourse.type) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .not.put.actionType(setAssessmentConfigurations.type) + .dispatch({ type: fetchAuth.type, payload: { code, providerId } }) .silentRun(); }); @@ -416,10 +405,10 @@ describe('Test FETCH_AUTH action', () => { .call(getUser, mockTokens) .put(setUser(user)) .put(updateLatestViewedCourse(user.courses[0].courseId)) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) - .dispatch({ type: FETCH_AUTH, payload: { code, providerId } }) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .not.put.actionType(setAssessmentConfigurations.type) + .dispatch({ type: fetchAuth.type, payload: { code, providerId } }) .silentRun(); }); @@ -440,12 +429,12 @@ describe('Test FETCH_AUTH action', () => { .call(postAuth, code, providerId, clientId, redirectUrl) .put(setTokens(mockTokens)) .call(getUser, mockTokens) - .not.put.actionType(SET_USER) - .not.put.actionType(UPDATE_LATEST_VIEWED_COURSE) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) - .dispatch({ type: FETCH_AUTH, payload: { code, providerId } }) + .not.put.actionType(setUser.type) + .not.put.actionType(updateLatestViewedCourse.type) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .not.put.actionType(setAssessmentConfigurations.type) + .dispatch({ type: fetchAuth.type, payload: { code, providerId } }) .silentRun(); }); }); @@ -461,7 +450,7 @@ describe('Test FETCH_USER_AND_COURSE action', () => { .withState({ session: mockTokens }) .call(getUser, mockTokens) .put(setUser(user)) - .not.put.actionType(UPDATE_LATEST_VIEWED_COURSE) + .not.put.actionType(updateLatestViewedCourse.type) .put(setCourseRegistration(courseRegistration)) .put(setCourseConfiguration(courseConfiguration)) .put(setAssessmentConfigurations(assessmentConfigurations)) @@ -471,7 +460,7 @@ describe('Test FETCH_USER_AND_COURSE action', () => { { user, courseRegistration, courseConfiguration, assessmentConfigurations } ] ]) - .dispatch({ type: FETCH_USER_AND_COURSE, payload: true }) + .dispatch({ type: fetchUserAndCourse.type, payload: true }) .silentRun(); }); @@ -492,10 +481,10 @@ describe('Test FETCH_USER_AND_COURSE action', () => { .call(getUser, mockTokens) .put(setUser(user)) .put(updateLatestViewedCourse(user.courses[0].courseId)) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) - .dispatch({ type: FETCH_USER_AND_COURSE, payload: true }) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .not.put.actionType(setAssessmentConfigurations.type) + .dispatch({ type: fetchUserAndCourse.type, payload: true }) .silentRun(); }); @@ -516,11 +505,11 @@ describe('Test FETCH_USER_AND_COURSE action', () => { ]) .call(getUser, mockTokens) .put(setUser(userWithNoCourse)) - .not.put.actionType(UPDATE_LATEST_VIEWED_COURSE) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) - .dispatch({ type: FETCH_USER_AND_COURSE, payload: true }) + .not.put.actionType(updateLatestViewedCourse.type) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .not.put.actionType(setAssessmentConfigurations.type) + .dispatch({ type: fetchUserAndCourse.type, payload: true }) .silentRun(); }); @@ -539,12 +528,12 @@ describe('Test FETCH_USER_AND_COURSE action', () => { ] ]) .call(getUser, mockTokens) - .not.put.actionType(SET_USER) - .not.put.actionType(UPDATE_LATEST_VIEWED_COURSE) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) - .dispatch({ type: FETCH_USER_AND_COURSE, payload: true }) + .not.put.actionType(setUser.type) + .not.put.actionType(updateLatestViewedCourse.type) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .not.put.actionType(setAssessmentConfigurations.type) + .dispatch({ type: fetchUserAndCourse.type, payload: true }) .silentRun(); }); }); @@ -555,7 +544,7 @@ describe('Test FETCH_COURSE_CONFIG action', () => { .withState(mockStates) .provide([[call(getCourseConfig, mockTokens), { config: mockCourseConfiguration1 }]]) .put(setCourseConfiguration(mockCourseConfiguration1)) - .dispatch({ type: FETCH_COURSE_CONFIG }) + .dispatch({ type: fetchCourseConfig.type }) .silentRun(); }); @@ -563,8 +552,8 @@ describe('Test FETCH_COURSE_CONFIG action', () => { return expectSaga(BackendSaga) .withState(mockStates) .provide([[call(getCourseConfig, mockTokens), { config: null }]]) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .dispatch({ type: FETCH_COURSE_CONFIG }) + .not.put.actionType(setCourseConfiguration.type) + .dispatch({ type: fetchCourseConfig.type }) .silentRun(); }); }); @@ -576,7 +565,7 @@ describe('Test FETCH_ASSESSMENT_OVERVIEWS action', () => { .provide([[call(getAssessmentOverviews, mockTokens), mockAssessmentOverviews]]) .put(updateAssessmentOverviews(mockAssessmentOverviews)) .hasFinalState({ session: mockTokens }) - .dispatch({ type: FETCH_ASSESSMENT_OVERVIEWS }) + .dispatch({ type: fetchAssessmentOverviews.type }) .silentRun(); }); @@ -586,9 +575,9 @@ describe('Test FETCH_ASSESSMENT_OVERVIEWS action', () => { .withState({ session: mockTokens }) .provide([[call(getAssessmentOverviews, mockTokens), ret]]) .call(getAssessmentOverviews, mockTokens) - .not.put.actionType(UPDATE_ASSESSMENT_OVERVIEWS) + .not.put.actionType(updateAssessmentOverviews.type) .hasFinalState({ session: mockTokens }) - .dispatch({ type: FETCH_ASSESSMENT_OVERVIEWS }) + .dispatch({ type: fetchAssessmentOverviews.type }) .silentRun(); }); }); @@ -600,7 +589,7 @@ describe('Test FETCH_TEAM_FORMATION_OVERVIEWS action', () => { .provide([[call(getTeamFormationOverviews, mockTokens), mockTeamFormationOverviews]]) .put(updateTeamFormationOverviews(mockTeamFormationOverviews)) .hasFinalState({ session: mockTokens }) - .dispatch({ type: FETCH_TEAM_FORMATION_OVERVIEWS }) + .dispatch({ type: fetchTeamFormationOverviews.type }) .silentRun(); }); @@ -609,9 +598,9 @@ describe('Test FETCH_TEAM_FORMATION_OVERVIEWS action', () => { .withState({ session: mockTokens }) .provide([[call(getTeamFormationOverviews, mockTokens), null]]) .call(getTeamFormationOverviews, mockTokens) - .not.put.actionType(UPDATE_TEAM_FORMATION_OVERVIEWS) + .not.put.actionType(updateTeamFormationOverviews.type) .hasFinalState({ session: mockTokens }) - .dispatch({ type: FETCH_TEAM_FORMATION_OVERVIEWS }) + .dispatch({ type: fetchTeamFormationOverviews.type }) .silentRun(); }); }); @@ -623,7 +612,7 @@ describe('Test FETCH_STUDENTS action', () => { .provide([[call(getStudents, mockTokens), mockStudents]]) .put(updateStudents(mockStudents)) .hasFinalState({ session: mockTokens }) - .dispatch({ type: FETCH_STUDENTS }) + .dispatch({ type: fetchStudents.type }) .silentRun(); }); @@ -632,9 +621,9 @@ describe('Test FETCH_STUDENTS action', () => { .withState({ session: mockTokens }) .provide([[call(getStudents, mockTokens), null]]) .call(getStudents, mockTokens) - .not.put.actionType(UPDATE_STUDENTS) + .not.put.actionType(updateStudents.type) .hasFinalState({ session: mockTokens }) - .dispatch({ type: FETCH_STUDENTS }) + .dispatch({ type: fetchStudents.type }) .silentRun(); }); }); @@ -647,7 +636,7 @@ describe('Test FETCH_ASSESSMENT action', () => { .provide([[call(getAssessment, mockId, mockTokens, undefined, undefined), mockAssessment]]) .put(updateAssessment(mockAssessment)) .hasFinalState({ session: mockTokens }) - .dispatch({ type: FETCH_ASSESSMENT, payload: { assessmentId: mockId } }) + .dispatch({ type: fetchAssessment.type, payload: { assessmentId: mockId } }) .silentRun(); }); @@ -659,7 +648,7 @@ describe('Test FETCH_ASSESSMENT action', () => { .call(getAssessment, mockId, mockTokens, undefined, undefined) .not.put.actionType(UPDATE_ASSESSMENT) .hasFinalState({ session: mockTokens }) - .dispatch({ type: FETCH_ASSESSMENT, payload: { assessmentId: mockId } }) + .dispatch({ type: fetchAssessment.type, payload: { assessmentId: mockId } }) .silentRun(); }); }); @@ -699,7 +688,7 @@ describe('Test SUBMIT_ANSWER action', () => { .call(showSuccessMessage, 'Saved!', 1000) .put(updateAssessment(mockNewAssessment)) .put(updateHasUnsavedChanges('assessment' as WorkspaceLocation, false)) - .dispatch({ type: SUBMIT_ANSWER, payload: mockAnsweredAssessmentQuestion }) + .dispatch({ type: submitAnswer.type, payload: mockAnsweredAssessmentQuestion }) .silentRun(); // To make sure no changes in state return expect(mockStates.session.assessments[mockNewAssessment.id].questions[0].answer).toEqual( @@ -741,7 +730,7 @@ describe('Test SUBMIT_ANSWER action', () => { .call(showSuccessMessage, 'Saved!', 1000) .put(updateAssessment(mockNewAssessment)) .put(updateHasUnsavedChanges('assessment' as WorkspaceLocation, false)) - .dispatch({ type: SUBMIT_ANSWER, payload: mockAnsweredAssessmentQuestion }) + .dispatch({ type: submitAnswer.type, payload: mockAnsweredAssessmentQuestion }) .silentRun(); // To make sure no changes in state return expect(mockStates.session.assessments[mockNewAssessment.id].questions[0].answer).toEqual( @@ -775,7 +764,7 @@ describe('Test SUBMIT_ANSWER action', () => { .not.put.actionType(UPDATE_ASSESSMENT) .not.put.actionType(UPDATE_HAS_UNSAVED_CHANGES) .hasFinalState({ session: { ...mockTokens, role: Role.Student } }) - .dispatch({ type: SUBMIT_ANSWER, payload: mockAnsweredAssessmentQuestion }) + .dispatch({ type: submitAnswer.type, payload: mockAnsweredAssessmentQuestion }) .silentRun(); }); }); @@ -795,7 +784,7 @@ describe('Test SUBMIT_ASSESSMENT action', () => { .not.call(showWarningMessage) .call(showSuccessMessage, 'Submitted!', 2000) .put(updateAssessmentOverviews(mockNewOverviews)) - .dispatch({ type: SUBMIT_ASSESSMENT, payload: mockAssessmentId }) + .dispatch({ type: submitAssessment.type, payload: mockAssessmentId }) .silentRun(); expect(mockStates.session.assessmentOverviews[0].id).toEqual(mockAssessmentId); return expect(mockStates.session.assessmentOverviews[0].status).not.toEqual( @@ -809,9 +798,9 @@ describe('Test SUBMIT_ASSESSMENT action', () => { .provide([[call(postAssessment, 0, mockTokens), null]]) .call(postAssessment, 0, mockTokens) .call(showWarningMessage, "Couldn't reach our servers. Are you online?") - .not.put.actionType(UPDATE_ASSESSMENT_OVERVIEWS) + .not.put.actionType(updateAssessmentOverviews.type) .hasFinalState({ session: { ...mockTokens, role: Role.Student } }) - .dispatch({ type: SUBMIT_ASSESSMENT, payload: 0 }) + .dispatch({ type: submitAssessment.type, payload: 0 }) .silentRun(); }); @@ -829,7 +818,7 @@ describe('Test SUBMIT_ASSESSMENT action', () => { .not.call(showWarningMessage) .call(showSuccessMessage, 'Submitted!', 2000) .put(updateAssessmentOverviews(mockNewOverviews)) - .dispatch({ type: SUBMIT_ASSESSMENT, payload: mockAssessmentId }) + .dispatch({ type: submitAssessment.type, payload: mockAssessmentId }) .silentRun(); expect(mockStates.session.assessmentOverviews[0].id).toEqual(mockAssessmentId); return expect(mockStates.session.assessmentOverviews[0].status).not.toEqual( @@ -844,7 +833,7 @@ describe('Test FETCH_NOTIFICATIONS action', () => { .withState(mockStates) .provide([[call(getNotifications, mockTokens), mockNotifications]]) .put(updateNotifications(mockNotifications)) - .dispatch({ type: FETCH_NOTIFICATIONS }) + .dispatch({ type: fetchNotifications.type }) .silentRun(); }); }); @@ -859,7 +848,7 @@ describe('Test ACKNOWLEDGE_NOTIFICATIONS action', () => { .not.call(showWarningMessage) .put(updateNotifications(mockNewNotifications)) .dispatch({ - type: ACKNOWLEDGE_NOTIFICATIONS, + type: acknowledgeNotifications.type, payload: { withFilter: (notifications: Notification[]) => notifications.filter(notification => ids.includes(notification.id)) @@ -924,7 +913,7 @@ describe('Test UPDATE_LATEST_VIEWED_COURSE action', () => { } ] ]) - .dispatch({ type: UPDATE_LATEST_VIEWED_COURSE, payload: { courseId } }) + .dispatch({ type: updateLatestViewedCourse.type, payload: { courseId } }) .silentRun(); }); @@ -934,9 +923,9 @@ describe('Test UPDATE_LATEST_VIEWED_COURSE action', () => { .provide([[call(putLatestViewedCourse, mockTokens, courseId), errorResp]]) .call(putLatestViewedCourse, mockTokens, courseId) .not.call.fn(getLatestCourseRegistrationAndConfiguration) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .dispatch({ type: UPDATE_LATEST_VIEWED_COURSE, payload: { courseId } }) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .dispatch({ type: updateLatestViewedCourse.type, payload: { courseId } }) .silentRun(); }); @@ -952,9 +941,9 @@ describe('Test UPDATE_LATEST_VIEWED_COURSE action', () => { ]) .call(putLatestViewedCourse, mockTokens, courseId) .call(getLatestCourseRegistrationAndConfiguration, mockTokens) - .not.put.actionType(SET_COURSE_REGISTRATION) - .not.put.actionType(SET_COURSE_CONFIGURATION) - .dispatch({ type: UPDATE_LATEST_VIEWED_COURSE, payload: { courseId } }) + .not.put.actionType(setCourseRegistration.type) + .not.put.actionType(setCourseConfiguration.type) + .dispatch({ type: updateLatestViewedCourse.type, payload: { courseId } }) .silentRun(); }); }); @@ -981,7 +970,7 @@ describe('Test UPDATE_COURSE_CONFIG action', () => { .put(setCourseConfiguration(courseConfiguration)) .call.fn(showSuccessMessage) .provide([[call(putCourseConfig, mockTokens, courseConfiguration), okResp]]) - .dispatch({ type: UPDATE_COURSE_CONFIG, payload: courseConfiguration }) + .dispatch({ type: updateCourseConfig.type, payload: courseConfiguration }) .silentRun(); }); @@ -990,9 +979,9 @@ describe('Test UPDATE_COURSE_CONFIG action', () => { .provide([[call(putCourseConfig, mockTokens, courseConfiguration), errorResp]]) .withState(mockStates) .call(putCourseConfig, mockTokens, courseConfiguration) - .not.put.actionType(SET_COURSE_CONFIGURATION) + .not.put.actionType(setCourseConfiguration.type) .not.call.fn(showSuccessMessage) - .dispatch({ type: UPDATE_COURSE_CONFIG, payload: courseConfiguration }) + .dispatch({ type: updateCourseConfig.type, payload: courseConfiguration }) .silentRun(); }); }); @@ -1004,7 +993,7 @@ describe('Test FETCH_ASSESSMENT_CONFIG action', () => { .call(getAssessmentConfigs, mockTokens) .put(setAssessmentConfigurations(mockAssessmentConfigurations)) .provide([[call(getAssessmentConfigs, mockTokens), mockAssessmentConfigurations]]) - .dispatch({ type: FETCH_ASSESSMENT_CONFIGS }) + .dispatch({ type: fetchAssessmentConfigs.type }) .silentRun(); }); @@ -1013,8 +1002,8 @@ describe('Test FETCH_ASSESSMENT_CONFIG action', () => { .withState(mockStates) .provide([[call(getAssessmentConfigs, mockTokens), null]]) .call(getAssessmentConfigs, mockTokens) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) - .dispatch({ type: FETCH_ASSESSMENT_CONFIGS }) + .not.put.actionType(setAssessmentConfigurations.type) + .dispatch({ type: fetchAssessmentConfigs.type }) .silentRun(); }); }); @@ -1042,7 +1031,7 @@ describe('Test FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS action', () => { .call(getUserCourseRegistrations, mockTokens) .put(setAdminPanelCourseRegistrations(userCourseRegistrations)) .provide([[call(getUserCourseRegistrations, mockTokens), userCourseRegistrations]]) - .dispatch({ type: FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS }) + .dispatch({ type: fetchAdminPanelCourseRegistrations.type }) .silentRun(); }); @@ -1051,8 +1040,8 @@ describe('Test FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS action', () => { .withState(mockStates) .provide([[call(getUserCourseRegistrations, mockTokens), null]]) .call(getUserCourseRegistrations, mockTokens) - .not.put.actionType(SET_ADMIN_PANEL_COURSE_REGISTRATIONS) - .dispatch({ type: FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS }) + .not.put.actionType(setAdminPanelCourseRegistrations.type) + .dispatch({ type: fetchAdminPanelCourseRegistrations.type }) .silentRun(); }); }); @@ -1123,8 +1112,8 @@ describe('Test CREATE_COURSE action', () => { .withState(mockStates) .call(postCreateCourse, mockTokens, courseConfig) .not.call.fn(getUser) - .not.put.actionType(SET_USER) - .not.put.actionType(SET_COURSE_REGISTRATION) + .not.put.actionType(setUser.type) + .not.put.actionType(setCourseRegistration.type) .not.call.fn(putAssessmentConfigs) .not.call.fn(showSuccessMessage) .provide([[call(postCreateCourse, mockTokens, courseConfig), errorResp]]) @@ -1175,7 +1164,7 @@ describe('Test ADD_NEW_USERS_TO_COURSE action', () => { return expectSaga(BackendSaga) .withState(mockStates) .call(putNewUsers, mockTokens, users, provider) - .not.put.actionType(FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS) + .not.put.actionType(fetchAdminPanelCourseRegistrations.type) .not.call.fn(showSuccessMessage) .provide([[call(putNewUsers, mockTokens, users, provider), errorResp]]) .dispatch({ type: ADD_NEW_USERS_TO_COURSE, payload: { users, provider } }) @@ -1214,7 +1203,7 @@ describe('Test UPDATE_USER_ROLE action', () => { [call(putUserRole, mockTokens, courseRegId, role), okResp], [call(getUserCourseRegistrations, mockTokens), userCourseRegistrations] ]) - .dispatch({ type: UPDATE_USER_ROLE, payload: { courseRegId, role } }) + .dispatch({ type: updateUserRole.type, payload: { courseRegId, role } }) .silentRun(); }); @@ -1222,10 +1211,10 @@ describe('Test UPDATE_USER_ROLE action', () => { return expectSaga(BackendSaga) .withState(mockStates) .call(putUserRole, mockTokens, courseRegId, role) - .not.put.actionType(FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS) + .not.put.actionType(fetchAdminPanelCourseRegistrations.type) .not.call.fn(showSuccessMessage) .provide([[call(putUserRole, mockTokens, courseRegId, role), errorResp]]) - .dispatch({ type: UPDATE_USER_ROLE, payload: { courseRegId, role } }) + .dispatch({ type: updateUserRole.type, payload: { courseRegId, role } }) .silentRun(); }); }); @@ -1248,7 +1237,7 @@ describe('Test UPDATE_COURSE_RESEARCH_AGREEMENT', () => { return expectSaga(BackendSaga) .withState(mockStates) .call(putCourseResearchAgreement, mockTokens, agreedToResearch) - .not.put.actionType(SET_COURSE_REGISTRATION) + .not.put.actionType(setCourseRegistration.type) .not.call.fn(showSuccessMessage) .provide([[call(putCourseResearchAgreement, mockTokens, agreedToResearch), errorResp]]) .dispatch({ type: UPDATE_COURSE_RESEARCH_AGREEMENT, payload: { agreedToResearch } }) @@ -1279,7 +1268,7 @@ describe('Test DELETE_USER_COURSE_REGISTRATION action', () => { [call(removeUserCourseRegistration, mockTokens, courseRegId), okResp], [call(getUserCourseRegistrations, mockTokens), userCourseRegistrations] ]) - .dispatch({ type: DELETE_USER_COURSE_REGISTRATION, payload: { courseRegId } }) + .dispatch({ type: deleteUserCourseRegistration.type, payload: { courseRegId } }) .silentRun(); }); @@ -1287,10 +1276,10 @@ describe('Test DELETE_USER_COURSE_REGISTRATION action', () => { return expectSaga(BackendSaga) .withState(mockStates) .call(removeUserCourseRegistration, mockTokens, courseRegId) - .not.put.actionType(FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS) + .not.put.actionType(fetchAdminPanelCourseRegistrations.type) .not.call.fn(showSuccessMessage) .provide([[call(removeUserCourseRegistration, mockTokens, courseRegId), errorResp]]) - .dispatch({ type: DELETE_USER_COURSE_REGISTRATION, payload: { courseRegId } }) + .dispatch({ type: deleteUserCourseRegistration.type, payload: { courseRegId } }) .silentRun(); }); }); @@ -1307,7 +1296,7 @@ describe('Test UPDATE_ASSESSMENT_CONFIGS action', () => { [call(putAssessmentConfigs, mockTokens, mockAssessmentConfigurations), okResp], [call(getAssessmentConfigs, mockTokens), mockAssessmentConfigurations] ]) - .dispatch({ type: UPDATE_ASSESSMENT_CONFIGS, payload: mockAssessmentConfigurations }) + .dispatch({ type: updateAssessmentConfigs.type, payload: mockAssessmentConfigurations }) .silentRun(); }); @@ -1317,9 +1306,9 @@ describe('Test UPDATE_ASSESSMENT_CONFIGS action', () => { .withState(mockStates) .call(putAssessmentConfigs, mockTokens, mockAssessmentConfigurations) .not.call(getAssessmentConfigs) - .not.put.actionType(SET_ASSESSMENT_CONFIGURATIONS) + .not.put.actionType(setAssessmentConfigurations.type) .not.call.fn(showSuccessMessage) - .dispatch({ type: UPDATE_ASSESSMENT_CONFIGS, payload: mockAssessmentConfigurations }) + .dispatch({ type: updateAssessmentConfigs.type, payload: mockAssessmentConfigurations }) .silentRun(); }); }); @@ -1356,7 +1345,7 @@ describe('Test REAUTOGRADE_SUBMISSION Action', () => { .call(postReautogradeSubmission, submissionId, mockTokens) .call.fn(showSuccessMessage) .not.call.fn(showWarningMessage) - .dispatch({ type: REAUTOGRADE_SUBMISSION, payload: submissionId }) + .dispatch({ type: reautogradeSubmission.type, payload: submissionId }) .silentRun(); }); @@ -1367,7 +1356,7 @@ describe('Test REAUTOGRADE_SUBMISSION Action', () => { .call(postReautogradeSubmission, submissionId, mockTokens) .not.call.fn(showSuccessMessage) .call.fn(showWarningMessage) - .dispatch({ type: REAUTOGRADE_SUBMISSION, payload: submissionId }) + .dispatch({ type: reautogradeSubmission.type, payload: submissionId }) .silentRun(); }); }); @@ -1383,7 +1372,7 @@ describe('Test REAUTOGRADE_ANSWER Action', () => { .call(postReautogradeAnswer, submissionId, questionId, mockTokens) .call.fn(showSuccessMessage) .not.call.fn(showWarningMessage) - .dispatch({ type: REAUTOGRADE_ANSWER, payload: { submissionId, questionId } }) + .dispatch({ type: reautogradeAnswer.type, payload: { submissionId, questionId } }) .silentRun(); }); @@ -1395,7 +1384,7 @@ describe('Test REAUTOGRADE_ANSWER Action', () => { .call(postReautogradeAnswer, submissionId, questionId, mockTokens) .not.call.fn(showSuccessMessage) .call.fn(showWarningMessage) - .dispatch({ type: REAUTOGRADE_ANSWER, payload: { submissionId, questionId } }) + .dispatch({ type: reautogradeAnswer.type, payload: { submissionId, questionId } }) .silentRun(); }); }); diff --git a/src/commons/sagas/__tests__/GitHubPersistenceSaga.ts b/src/commons/sagas/__tests__/GitHubPersistenceSaga.ts index ed5d253c77..ae23aa0466 100644 --- a/src/commons/sagas/__tests__/GitHubPersistenceSaga.ts +++ b/src/commons/sagas/__tests__/GitHubPersistenceSaga.ts @@ -1,6 +1,6 @@ import { expectSaga } from 'redux-saga-test-plan'; +import { removeGitHubOctokitObjectAndAccessToken } from 'src/commons/application/actions/SessionActions'; -import { REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN } from '../../application/types/SessionTypes'; import { actions } from '../../utils/ActionsHelper'; // mock away the store @@ -12,7 +12,7 @@ const GitHubPersistenceSaga = require('../GitHubPersistenceSaga').default; test('logoutGitHub results in REMOVE_GITHUB_OCTOKIT_OBJECT being dispatched', async () => { await expectSaga(GitHubPersistenceSaga) .put({ - type: REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, + type: removeGitHubOctokitObjectAndAccessToken.type, payload: {} }) .dispatch(actions.logoutGitHub()) diff --git a/src/commons/utils/ActionsHelper.ts b/src/commons/utils/ActionsHelper.ts index eaa7e95592..dd3066471b 100644 --- a/src/commons/utils/ActionsHelper.ts +++ b/src/commons/utils/ActionsHelper.ts @@ -1,6 +1,6 @@ import * as CommonsActions from '../../commons/application/actions/CommonsActions'; import * as InterpreterActions from '../../commons/application/actions/InterpreterActions'; -import * as SessionActions from '../../commons/application/actions/SessionActions'; +import SessionActions from '../../commons/application/actions/SessionActions'; import * as CollabEditingActions from '../../commons/collabEditing/CollabEditingActions'; import * as FileSystemActions from '../../commons/fileSystem/FileSystemActions'; import * as SideContentActions from '../../commons/sideContent/SideContentActions'; @@ -16,7 +16,7 @@ import * as RemoteExecutionActions from '../../features/remoteExecution/RemoteEx import * as SourcecastActions from '../../features/sourceRecorder/sourcecast/SourcecastActions'; import * as SourceRecorderActions from '../../features/sourceRecorder/SourceRecorderActions'; import * as SourcereelActions from '../../features/sourceRecorder/sourcereel/SourcereelActions'; -import * as StoriesActions from '../../features/stories/StoriesActions'; +import StoriesActions from '../../features/stories/StoriesActions'; import { ActionType } from './TypeHelper'; export const actions = { diff --git a/src/commons/utils/TestUtils.ts b/src/commons/utils/TestUtils.ts index 9684eaa2f2..820580164e 100644 --- a/src/commons/utils/TestUtils.ts +++ b/src/commons/utils/TestUtils.ts @@ -47,3 +47,19 @@ export function deepFilter( helper(nestedObject); return matches; } + +// TODO: Fix type inference for this function, +// then use it in tests. We no longer need to +// check the action type as everything gets migrated +// to RTK and explicit action types are no longer needed. +// /** +// * The `expectActionPayload` function is used to test the payload +// * of an action. +// * @param action The action to test +// */ +// export function expectActionPayload>( +// action: ReturnType +// ) { +// const payload: ReturnType['payload'] = action.payload; +// return expect(payload); +// } diff --git a/src/features/stories/StoriesActions.ts b/src/features/stories/StoriesActions.ts index c2c7a61907..2b8faed6c1 100644 --- a/src/features/stories/StoriesActions.ts +++ b/src/features/stories/StoriesActions.ts @@ -1,88 +1,44 @@ import { createAction } from '@reduxjs/toolkit'; import { Chapter, Context, SourceError, Value, Variant } from 'js-slang/dist/types'; import { StoriesRole } from 'src/commons/application/ApplicationTypes'; +import { createActions } from 'src/commons/redux/utils'; import { - ADD_STORY_ENV, CLEAR_STORIES_USER_AND_GROUP, - CLEAR_STORY_ENV, - CREATE_STORY, - DELETE_STORY, - EVAL_STORY, - EVAL_STORY_ERROR, - EVAL_STORY_SUCCESS, - GET_STORIES_LIST, GET_STORIES_USER, - HANDLE_STORIES_CONSOLE_LOG, - NOTIFY_STORIES_EVALUATED, - SAVE_STORY, SET_CURRENT_STORIES_GROUP, SET_CURRENT_STORIES_USER, - SET_CURRENT_STORY, - SET_CURRENT_STORY_ID, StoryData, StoryListView, - StoryParams, - TOGGLE_STORIES_USING_SUBST, - UPDATE_STORIES_LIST + StoryParams } from './StoriesTypes'; -export const addStoryEnv = createAction( - ADD_STORY_ENV, - (env: string, chapter: Chapter, variant: Variant) => ({ payload: { env, chapter, variant } }) -); - -export const clearStoryEnv = createAction(CLEAR_STORY_ENV, (env?: string) => ({ - payload: { env } -})); - -export const evalStory = createAction(EVAL_STORY, (env: string, code: string) => ({ - payload: { env, code } -})); - -export const evalStoryError = createAction( - EVAL_STORY_ERROR, - (errors: SourceError[], env: string) => ({ payload: { type: 'errors', errors, env } }) -); - -export const evalStorySuccess = createAction(EVAL_STORY_SUCCESS, (value: Value, env: string) => ({ - payload: { type: 'result', value, env } -})); - -export const handleStoriesConsoleLog = createAction( - HANDLE_STORIES_CONSOLE_LOG, - (env: string, ...logString: string[]) => ({ payload: { logString, env } }) -); +const newActions = createActions('stories', { + addStoryEnv: (env: string, chapter: Chapter, variant: Variant) => ({ env, chapter, variant }), + clearStoryEnv: (env?: string) => ({ env }), + evalStory: (env: string, code: string) => ({ env, code }), + evalStoryError: (errors: SourceError[], env: string) => ({ type: 'errors', errors, env }), + evalStorySuccess: (value: Value, env: string) => ({ type: 'result', value, env }), + handleStoriesConsoleLog: (env: string, ...logString: string[]) => ({ logString, env }), + notifyStoriesEvaluated: ( + result: any, + lastDebuggerResult: any, + code: string, + context: Context, + env: string + ) => ({ result, lastDebuggerResult, code, context, env }), + toggleStoriesUsingSubst: (usingSubst: boolean, env: String) => ({ usingSubst, env }), -export const notifyStoriesEvaluated = createAction( - NOTIFY_STORIES_EVALUATED, - (result: any, lastDebuggerResult: any, code: string, context: Context, env: string) => ({ - payload: { result, lastDebuggerResult, code, context, env } - }) -); + // New action creators post-refactor + getStoriesList: () => ({}), + updateStoriesList: (storyList: StoryListView[]) => storyList, + setCurrentStory: (story: StoryData | null) => story, + setCurrentStoryId: (id: number | null) => id, + createStory: (story: StoryParams) => story, + saveStory: (story: StoryParams, id: number) => ({ story, id }), + deleteStory: (id: number) => id +}); -export const toggleStoriesUsingSubst = createAction( - TOGGLE_STORIES_USING_SUBST, - (usingSubst: boolean, env: String) => ({ payload: { usingSubst, env } }) -); - -// New action creators post-refactor -export const getStoriesList = createAction(GET_STORIES_LIST, () => ({ payload: {} })); -export const updateStoriesList = createAction( - UPDATE_STORIES_LIST, - (storyList: StoryListView[]) => ({ payload: storyList }) -); -export const setCurrentStory = createAction(SET_CURRENT_STORY, (story: StoryData | null) => ({ - payload: story -})); -export const setCurrentStoryId = createAction(SET_CURRENT_STORY_ID, (id: number | null) => ({ - payload: id -})); -export const createStory = createAction(CREATE_STORY, (story: StoryParams) => ({ payload: story })); -export const saveStory = createAction(SAVE_STORY, (story: StoryParams, id: number) => ({ - payload: { story, id } -})); -export const deleteStory = createAction(DELETE_STORY, (id: number) => ({ payload: id })); // Auth-related actions export const getStoriesUser = createAction(GET_STORIES_USER, () => ({ payload: {} })); export const setCurrentStoriesUser = createAction( @@ -97,3 +53,32 @@ export const setCurrentStoriesGroup = createAction( export const clearStoriesUserAndGroup = createAction(CLEAR_STORIES_USER_AND_GROUP, () => ({ payload: {} })); + +// For compatibility with existing code (reducer) +export const { + addStoryEnv, + clearStoryEnv, + evalStory, + evalStoryError, + evalStorySuccess, + handleStoriesConsoleLog, + notifyStoriesEvaluated, + toggleStoriesUsingSubst, + // New action creators post-refactor + getStoriesList, + updateStoriesList, + setCurrentStory, + setCurrentStoryId, + createStory, + saveStory, + deleteStory +} = newActions; + +// For compatibility with existing code (actions helper) +export default { + ...newActions, + getStoriesUser, + setCurrentStoriesUser, + setCurrentStoriesGroup, + clearStoriesUserAndGroup +}; diff --git a/src/features/stories/StoriesTypes.ts b/src/features/stories/StoriesTypes.ts index 524210e5b2..b281b70ed3 100644 --- a/src/features/stories/StoriesTypes.ts +++ b/src/features/stories/StoriesTypes.ts @@ -3,22 +3,6 @@ import { DebuggerContext } from 'src/commons/workspace/WorkspaceTypes'; import { InterpreterOutput, StoriesRole } from '../../commons/application/ApplicationTypes'; -export const ADD_STORY_ENV = 'ADD_STORY_ENV'; -export const CLEAR_STORY_ENV = 'CLEAR_STORY_ENV'; -export const EVAL_STORY = 'EVAL_STORY'; -export const EVAL_STORY_ERROR = 'EVAL_STORY_ERROR'; -export const EVAL_STORY_SUCCESS = 'EVAL_STORY_SUCCESS'; -export const HANDLE_STORIES_CONSOLE_LOG = 'HANDLE_STORIES_CONSOLE_LOG'; -export const NOTIFY_STORIES_EVALUATED = 'NOTIFY_STORIES_EVALUATED'; -export const TOGGLE_STORIES_USING_SUBST = 'TOGGLE_STORIES_USING_SUBST'; -// New actions post-refactor -export const GET_STORIES_LIST = 'GET_STORIES_LIST'; -export const UPDATE_STORIES_LIST = 'UPDATE_STORIES_LIST'; -export const SET_CURRENT_STORY_ID = 'SET_CURRENT_STORY_ID'; -export const SET_CURRENT_STORY = 'SET_CURRENT_STORY'; -export const CREATE_STORY = 'CREATE_STORY'; -export const SAVE_STORY = 'SAVE_STORY'; -export const DELETE_STORY = 'DELETE_STORY'; // Auth-related actions export const GET_STORIES_USER = 'GET_STORIES_USER'; export const CLEAR_STORIES_USER_AND_GROUP = 'CLEAR_STORIES_USER_AND_GROUP'; diff --git a/src/features/stories/__tests__/StoriesActions.ts b/src/features/stories/__tests__/StoriesActions.ts index a0610f41f4..99a2d03091 100644 --- a/src/features/stories/__tests__/StoriesActions.ts +++ b/src/features/stories/__tests__/StoriesActions.ts @@ -1,12 +1,11 @@ import { Chapter, Variant } from 'js-slang/dist/types'; import { addStoryEnv, clearStoryEnv, evalStory } from '../StoriesActions'; -import { ADD_STORY_ENV, CLEAR_STORY_ENV, EVAL_STORY } from '../StoriesTypes'; test('addStoryEnv generates correct action object', () => { const action = addStoryEnv('testEnv', Chapter.SOURCE_4, Variant.DEFAULT); expect(action).toEqual({ - type: ADD_STORY_ENV, + type: addStoryEnv.type, payload: { env: 'testEnv', chapter: Chapter.SOURCE_4, @@ -18,7 +17,7 @@ test('addStoryEnv generates correct action object', () => { test('clearStoryEnv generates correct action object', () => { const action = clearStoryEnv(); expect(action).toEqual({ - type: CLEAR_STORY_ENV, + type: clearStoryEnv.type, payload: { env: undefined } @@ -28,7 +27,7 @@ test('clearStoryEnv generates correct action object', () => { test('clearStoryEnv with environment generates correct action object', () => { const action = clearStoryEnv('default'); expect(action).toEqual({ - type: CLEAR_STORY_ENV, + type: clearStoryEnv.type, payload: { env: 'default' } @@ -38,7 +37,7 @@ test('clearStoryEnv with environment generates correct action object', () => { test('evalStory generates correct action object', () => { const action = evalStory('testEnv', '1;'); expect(action).toEqual({ - type: EVAL_STORY, + type: evalStory.type, payload: { env: 'testEnv', code: '1;' diff --git a/src/pages/createStore.ts b/src/pages/createStore.ts index 04389b95bb..861cdd543e 100644 --- a/src/pages/createStore.ts +++ b/src/pages/createStore.ts @@ -5,7 +5,7 @@ import createSagaMiddleware from 'redux-saga'; import { SourceActionType } from 'src/commons/utils/ActionsHelper'; import { defaultState, OverallState } from '../commons/application/ApplicationTypes'; -import createRootReducer from '../commons/application/reducers/RootReducer'; +import rootReducer from '../commons/application/reducers/RootReducer'; import MainSaga from '../commons/sagas/MainSaga'; import { generateOctokitInstance } from '../commons/utils/GitHubPersistenceHelper'; import { loadStoredState, SavedState, saveState } from './localStorage'; @@ -21,7 +21,7 @@ export function createStore() { const initialStore = loadStore(loadStoredState()) || defaultState; const createdStore = configureStore({ - reducer: createRootReducer(), + reducer: rootReducer, // Fix for redux-saga type incompatibility // See: https://github.com/reduxjs/redux-toolkit/issues/3950 middleware: middleware as any,