diff --git a/package.json b/package.json index f2d1758ef1..0bf701eb52 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-debounce-render": "^8.0.2", "react-dom": "^18.2.0", + "react-drag-drop-files": "^2.3.10", "react-draggable": "^4.4.5", "react-dropzone": "^14.2.3", "react-hotkeys": "^2.0.0", @@ -77,6 +78,7 @@ "react-redux": "^8.1.1", "react-responsive": "^10.0.0", "react-router-dom": "^6.14.1", + "react-select": "^5.7.3", "react-simple-keyboard": "^3.6.27", "react-sortable-hoc": "^2.0.0", "react-syntax-highlighter": "^15.5.0", @@ -90,6 +92,7 @@ "typesafe-actions": "^5.1.0", "unified": "^11.0.0", "uuid": "^9.0.0", + "xlsx": "0.16.4", "xml2js": "^0.6.0", "yareco": "^0.1.5" }, diff --git a/src/commons/XMLParser/XMLParserHelper.ts b/src/commons/XMLParser/XMLParserHelper.ts index fbf9385575..eeca016af3 100644 --- a/src/commons/XMLParser/XMLParserHelper.ts +++ b/src/commons/XMLParser/XMLParserHelper.ts @@ -84,7 +84,8 @@ const makeAssessmentOverview = (result: any, maxXpVal: number): AssessmentOvervi status: AssessmentStatuses.attempting, story: rawOverview.story, xp: 0, - gradingStatus: 'none' as GradingStatuses + gradingStatus: 'none' as GradingStatuses, + maxTeamSize: 1 }; }; @@ -202,6 +203,7 @@ const makeProgramming = ( testcases: publicTestcases.map(testcase => makeTestcase(testcase)), testcasesPrivate: privateTestcases.map(testcase => makeTestcase(testcase)), answer: solution ? (solution[0] as string).trim() : '', + lastModifiedAt: new Date().toISOString(), type: 'programming' }; if (problem.SNIPPET[0].GRADER) { diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index fc0c6cfe94..9aad11f2db 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -405,6 +405,19 @@ export const defaultWorkspaceManager: WorkspaceManagerState = { currentQuestion: undefined, hasUnsavedChanges: false }, + teamFormation: { + ...createDefaultWorkspace('teamFormation'), + teamFormationTableFilters: { + columnFilters: [], + globalFilter: null + } + }, + groundControl: { + ...createDefaultWorkspace('groundControl'), + GroundControlTableFilters: { + columnFilters: [] + } + }, playground: { ...createDefaultWorkspace('playground'), usingSubst: false, @@ -500,6 +513,8 @@ export const defaultSession: SessionState = { sessionId: Date.now(), githubOctokitObject: { octokit: undefined }, gradingOverviews: undefined, + students: undefined, + teamFormationOverviews: undefined, gradings: new Map(), notifications: [] }; @@ -539,6 +554,8 @@ export const defaultSideContentManager: SideContentManagerState = { assessment: defaultSideContent, grading: defaultSideContent, playground: defaultSideContent, + groundControl: defaultSideContent, + teamFormation: defaultSideContent, sicp: defaultSideContent, sourcecast: defaultSideContent, sourcereel: defaultSideContent, diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 9f40acbb90..c8cbc7d758 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -3,8 +3,10 @@ import { paginationToBackendParams, ungradedToBackendParams } from 'src/features/grading/GradingUtils'; +import { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm'; import { GradingOverviews, GradingQuery } from '../../../features/grading/GradingTypes'; +import { TeamFormationOverview } from '../../../features/teamFormation/TeamFormationTypes'; import { Assessment, AssessmentConfiguration, @@ -20,8 +22,12 @@ 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, @@ -36,6 +42,9 @@ import { 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, @@ -77,6 +86,10 @@ import { 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, @@ -137,6 +150,13 @@ export const fetchGradingOverviews = createAction( ) => ({ payload: { filterToGroup, gradedFilter, 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: {} })); @@ -202,6 +222,13 @@ export const submitAnswer = createAction( (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( @@ -246,6 +273,46 @@ export const updateGradingOverviews = createAction( (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. diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index a2def9171e..6cf331c79f 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -1,10 +1,12 @@ import { Chapter, Variant } from 'js-slang/dist/types'; +import { mockStudents } from 'src/commons/mocks/UserMocks'; import { paginationToBackendParams, ungradedToBackendParams } from 'src/features/grading/GradingUtils'; import { GradingOverviews, GradingQuery } from '../../../../features/grading/GradingTypes'; +import { TeamFormationOverview } from '../../../../features/teamFormation/TeamFormationTypes'; import { Assessment, AssessmentOverview } from '../../../assessment/AssessmentTypes'; import { Notification } from '../../../notificationBadge/NotificationBadgeTypes'; import { GameState, Role, Story } from '../../ApplicationTypes'; @@ -21,6 +23,8 @@ import { FETCH_GRADING, FETCH_GRADING_OVERVIEWS, FETCH_NOTIFICATIONS, + FETCH_STUDENTS, + FETCH_TEAM_FORMATION_OVERVIEWS, FETCH_USER_AND_COURSE, LOGIN, REAUTOGRADE_ANSWER, @@ -47,7 +51,11 @@ import { UPDATE_GRADING_OVERVIEWS, UPDATE_LATEST_VIEWED_COURSE, UPDATE_NOTIFICATIONS, - UPDATE_USER_ROLE + UPDATE_STUDENTS, + UPDATE_TEAM_FORMATION_OVERVIEW, + UPDATE_TEAM_FORMATION_OVERVIEWS, + UPDATE_USER_ROLE, + User } from '../../types/SessionTypes'; import { acknowledgeNotifications, @@ -62,6 +70,8 @@ import { fetchGrading, fetchGradingOverviews, fetchNotifications, + fetchStudents, + fetchTeamFormationOverviews, fetchUserAndCourse, login, reautogradeAnswer, @@ -88,6 +98,9 @@ import { updateGradingOverviews, updateLatestViewedCourse, updateNotifications, + updateStudents, + updateTeamFormationOverview, + updateTeamFormationOverviews, updateUserRole } from '../SessionActions'; @@ -183,6 +196,31 @@ test('fetchGradingOverviews generates correct action object', () => { }); }); +test('fetchTeamFormationOverviews generates correct default action object', () => { + const action = fetchTeamFormationOverviews(); + expect(action).toEqual({ + type: FETCH_TEAM_FORMATION_OVERVIEWS, + payload: true + }); +}); + +test('fetchTeamFormationOverviews generates correct action object', () => { + const filterToGroup = false; + const action = fetchTeamFormationOverviews(filterToGroup); + expect(action).toEqual({ + type: FETCH_TEAM_FORMATION_OVERVIEWS, + payload: filterToGroup + }); +}); + +test('fetchStudents generates correct action object', () => { + const action = fetchStudents(); + expect(action).toEqual({ + type: FETCH_STUDENTS, + payload: {} + }); +}); + test('fetchNotifications generates correct action object', () => { const action = fetchNotifications(); @@ -217,6 +255,7 @@ test('setUser generates correct action object', () => { const user = { userId: 123, name: 'test student', + username: 'test student', courses: [ { courseId: 1, @@ -501,7 +540,8 @@ test('updateAssessmentOverviews generates correct action object', () => { status: 'not_attempted', story: null, xp: 0, - gradingStatus: 'none' + gradingStatus: 'none', + maxTeamSize: 1 } ]; const action = updateAssessmentOverviews(overviews); @@ -546,7 +586,9 @@ test('updateGradingOverviews generates correct action object', () => { maxXp: 500, studentId: 100, studentName: 'test student', + studentNames: [], studentUsername: 'E0123456', + studentUsernames: [], submissionId: 1, submissionStatus: 'attempting', groupName: 'group', @@ -564,6 +606,52 @@ test('updateGradingOverviews generates correct action object', () => { }); }); +test('updateStudents generates correct action object', () => { + const students: User[] = mockStudents; + + const action = updateStudents(students); + expect(action).toEqual({ + type: UPDATE_STUDENTS, + payload: students + }); +}); + +test('updateTeamFormationOverview generates correct action object', () => { + const overview: TeamFormationOverview = { + teamId: 0, + assessmentId: 1, + assessmentName: 'Mission 1', + assessmentType: 'Missions', + studentIds: [0], + studentNames: ['Mark Henry'] + }; + + const action = updateTeamFormationOverview(overview); + expect(action).toEqual({ + type: UPDATE_TEAM_FORMATION_OVERVIEW, + payload: overview + }); +}); + +test('updateTeamFormationOverviews generates correct action object', () => { + const overviews: TeamFormationOverview[] = [ + { + teamId: 0, + assessmentId: 0, + assessmentName: 'Mission 2', + assessmentType: 'Missions', + studentIds: [0], + studentNames: ['Mark Henry'] + } + ]; + + const action = updateTeamFormationOverviews(overviews); + expect(action).toEqual({ + type: UPDATE_TEAM_FORMATION_OVERVIEWS, + payload: overviews + }); +}); + test('updateGrading generates correct action object', () => { const submissionId = 3; const grading: GradingQuery = { diff --git a/src/commons/application/reducers/SessionsReducer.ts b/src/commons/application/reducers/SessionsReducer.ts index 208dd8f1e5..9f98f583e1 100644 --- a/src/commons/application/reducers/SessionsReducer.ts +++ b/src/commons/application/reducers/SessionsReducer.ts @@ -26,6 +26,9 @@ import { UPDATE_GRADING, UPDATE_GRADING_OVERVIEWS, UPDATE_NOTIFICATIONS, + UPDATE_STUDENTS, + UPDATE_TEAM_FORMATION_OVERVIEW, + UPDATE_TEAM_FORMATION_OVERVIEWS, UPDATE_TOTAL_XP } from '../types/SessionTypes'; @@ -122,6 +125,21 @@ export const SessionsReducer: Reducer = ( ...state, notifications: action.payload }; + case UPDATE_STUDENTS: + return { + ...state, + students: action.payload + }; + case UPDATE_TEAM_FORMATION_OVERVIEWS: + return { + ...state, + teamFormationOverviews: action.payload + }; + case UPDATE_TEAM_FORMATION_OVERVIEW: + return { + ...state, + teamFormationOverview: action.payload + }; case REMOTE_EXEC_UPDATE_DEVICES: return { ...state, diff --git a/src/commons/application/reducers/__tests__/SessionReducer.ts b/src/commons/application/reducers/__tests__/SessionReducer.ts index 63c3ff4b1b..ac4ddb4e81 100644 --- a/src/commons/application/reducers/__tests__/SessionReducer.ts +++ b/src/commons/application/reducers/__tests__/SessionReducer.ts @@ -59,6 +59,7 @@ test('SET_TOKEN sets accessToken and refreshToken correctly', () => { test('SET_USER works correctly', () => { const payload = { userId: 123, + username: 'E1234567', name: 'test student', courses: [ { @@ -337,7 +338,8 @@ const assessmentOverviewsTest1: AssessmentOverview[] = [ status: AssessmentStatuses.not_attempted, story: null, xp: 0, - gradingStatus: GradingStatuses.none + gradingStatus: GradingStatuses.none, + maxTeamSize: 5 } ]; @@ -356,7 +358,8 @@ const assessmentOverviewsTest2: AssessmentOverview[] = [ status: AssessmentStatuses.attempted, story: null, xp: 1, - gradingStatus: GradingStatuses.grading + gradingStatus: GradingStatuses.grading, + maxTeamSize: 1 } ]; @@ -523,7 +526,9 @@ const gradingOverviewTest1: GradingOverview[] = [ maxXp: 500, studentId: 100, studentName: 'test student', + studentNames: [], studentUsername: 'E0123456', + studentUsernames: [], submissionId: 1, submissionStatus: 'attempting', groupName: 'group', @@ -546,7 +551,9 @@ const gradingOverviewTest2: GradingOverview[] = [ maxXp: 1000, studentId: 20, studentName: 'another student', + studentNames: [], studentUsername: 'E0000000', + studentUsernames: [], submissionId: 2, submissionStatus: 'attempted', groupName: 'another group', diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index ac5640e725..39eaeae823 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -3,6 +3,7 @@ import { Chapter, Variant } from 'js-slang/dist/types'; import { GradingOverviews, GradingQuery } from '../../../features/grading/GradingTypes'; import { Device, DeviceSession } from '../../../features/remoteExecution/RemoteExecutionTypes'; +import { TeamFormationOverview } from '../../../features/teamFormation/TeamFormationTypes'; import { Assessment, AssessmentConfiguration, @@ -11,6 +12,11 @@ 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'; @@ -21,6 +27,9 @@ 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'; @@ -48,6 +57,9 @@ 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'; @@ -114,6 +126,9 @@ export type SessionState = { readonly assessmentOverviews?: AssessmentOverview[]; readonly assessments: Map; readonly gradingOverviews?: GradingOverviews; + readonly students?: User[]; + readonly teamFormationOverview?: TeamFormationOverview; + readonly teamFormationOverviews?: TeamFormationOverview[]; readonly gradings: Map; readonly notifications: Notification[]; readonly googleUser?: string; @@ -139,6 +154,7 @@ export type UserCourse = { export type User = { userId: number; name: string; + username: string; courses: UserCourse[]; }; diff --git a/src/commons/assessment/Assessment.tsx b/src/commons/assessment/Assessment.tsx index 42b4f80635..8401e62d90 100644 --- a/src/commons/assessment/Assessment.tsx +++ b/src/commons/assessment/Assessment.tsx @@ -190,6 +190,15 @@ const Assessment: React.FC = props => {
+ {overview.maxTeamSize > 1 ? ( +
+
This is a team assessment.
+
+ ) : ( +
+
This is an individual assessment.
+
+ )}
@@ -289,7 +298,6 @@ const Assessment: React.FC = props => { /** Upcoming assessments, that are not released yet. */ const isOverviewUpcoming = (overview: AssessmentOverview) => !beforeNow(overview.closeAt) && !beforeNow(overview.openAt); - const upcomingCards = sortAssessments(assessmentOverviews.filter(isOverviewUpcoming)).map( (overview, index) => makeOverviewCard(overview, index, role !== Role.Student, false) ); diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index 28aed1f6ca..49e8777338 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -72,6 +72,7 @@ export type AssessmentOverview = { story: string | null; title: string; xp: number; + maxTeamSize: number; // For team assessment }; /* @@ -101,6 +102,7 @@ export type AssessmentConfiguration = { export interface IProgrammingQuestion extends BaseQuestion { answer: string | null; + lastModifiedAt: string; autogradingResults: AutogradingResult[]; graderTemplate?: string; prepend: string; @@ -243,7 +245,8 @@ export const overviewTemplate = (): AssessmentOverview => { status: AssessmentStatuses.not_attempted, story: 'mission', xp: 0, - gradingStatus: 'none' + gradingStatus: 'none', + maxTeamSize: 1 }; }; @@ -251,6 +254,7 @@ export const programmingTemplate = (): IProgrammingQuestion => { return { autogradingResults: [], answer: '// [Marking Scheme]\n// 1 mark for correct answer', + lastModifiedAt: '2023-08-05T17:48:24.000000Z', content: 'Enter content here', id: 0, library: emptyLibrary(), diff --git a/src/commons/assessment/__tests__/__snapshots__/Assessment.tsx.snap b/src/commons/assessment/__tests__/__snapshots__/Assessment.tsx.snap index d766ce1df5..f01a13a176 100644 --- a/src/commons/assessment/__tests__/__snapshots__/Assessment.tsx.snap +++ b/src/commons/assessment/__tests__/__snapshots__/Assessment.tsx.snap @@ -227,6 +227,13 @@ exports[`Assessment page does not show attempt Button for upcoming assessments f } />
+
+
+ This is an individual assessment. +
+
@@ -407,6 +414,13 @@ exports[`Assessment page does not show attempt Button for upcoming assessments f } />
+
+
+ This is an individual assessment. +
+
@@ -611,6 +625,15 @@ exports[`Assessment page does not show attempt Button for upcoming assessments f } />
+
+
+ This is a team assessment. +
+
@@ -956,6 +979,13 @@ exports[`Assessment page with multiple loaded missions renders correctly 1`] = ` } />
+
+
+ This is an individual assessment. +
+
@@ -1134,6 +1164,13 @@ exports[`Assessment page with multiple loaded missions renders correctly 1`] = ` } />
+
+
+ This is an individual assessment. +
+
@@ -1290,6 +1327,15 @@ exports[`Assessment page with multiple loaded missions renders correctly 1`] = ` } />
+
+
+ This is a team assessment. +
+
diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 38921cc19b..aa8abec360 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -17,6 +17,7 @@ import { isEqual } from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router'; +import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper'; import { onClickProgress } from 'src/features/assessments/AssessmentUtils'; import { mobileOnlyTabIds } from 'src/pages/playground/PlaygroundTabs'; @@ -27,7 +28,12 @@ import { KeyboardCommand, SelectionRange } from '../../features/sourceRecorder/SourceRecorderTypes'; -import { fetchAssessment, submitAnswer } from '../application/actions/SessionActions'; +import { + checkAnswerLastModifiedAt, + fetchAssessment, + fetchTeamFormationOverview, + submitAnswer +} from '../application/actions/SessionActions'; import { defaultWorkspaceManager } from '../application/ApplicationTypes'; import { AssessmentConfiguration, @@ -111,11 +117,17 @@ const workspaceLocation: WorkspaceLocation = 'assessment'; const AssessmentWorkspace: React.FC = props => { const [showOverlay, setShowOverlay] = useState(false); + const [isSaving, setIsSaving] = useState(false); const [showResetTemplateOverlay, setShowResetTemplateOverlay] = useState(false); const [sessionId, setSessionId] = useState(''); const { isMobileBreakpoint } = useResponsive(); const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); + const assessmentOverviews = useTypedSelector(state => state.session.assessmentOverviews); + const teamFormationOverview = useTypedSelector(state => state.session.teamFormationOverview); + const assessmentOverview = assessmentOverviews?.find(assessmentOverview => { + return assessmentOverview.id === assessment?.id; + }); const { selectedTab, setSelectedTab } = useSideContent( workspaceLocation, assessment?.questions[props.questionId].grader !== undefined @@ -142,6 +154,7 @@ const AssessmentWorkspace: React.FC = props => { const dispatch = useDispatch(); const { + handleTeamOverviewFetch, handleTestcaseEval, handleClearContext, handleChangeExecTime, @@ -154,11 +167,14 @@ const AssessmentWorkspace: React.FC = props => { handleEditorUpdateBreakpoints, handleReplEval, handleSave, + handleCheckLastModifiedAt, handleUpdateHasUnsavedChanges, handleEnableTokenCounter, handleDisableTokenCounter } = useMemo(() => { return { + handleTeamOverviewFetch: (assessmentId: number) => + dispatch(fetchTeamFormationOverview(assessmentId)), handleTestcaseEval: (id: number) => dispatch(evalTestcase(workspaceLocation, id)), handleClearContext: (library: Library, shouldInitLibrary: boolean) => dispatch(beginClearContext(workspaceLocation, library, shouldInitLibrary)), @@ -177,6 +193,8 @@ const AssessmentWorkspace: React.FC = props => { handleEditorUpdateBreakpoints: (editorTabIndex: number, newBreakpoints: string[]) => dispatch(setEditorBreakpoint(workspaceLocation, editorTabIndex, newBreakpoints)), handleReplEval: () => dispatch(evalRepl(workspaceLocation)), + handleCheckLastModifiedAt: (id: number, lastModifiedAt: string, saveAnswer: Function) => + dispatch(checkAnswerLastModifiedAt(id, lastModifiedAt, saveAnswer)), handleSave: (id: number, answer: number | string | ContestEntry[]) => dispatch(submitAnswer(id, answer)), handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => @@ -192,6 +210,11 @@ const AssessmentWorkspace: React.FC = props => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + handleTeamOverviewFetch(props.assessmentId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + /** * After mounting (either an older copy of the assessment * or a loading screen), try to fetch a newer assessment, @@ -431,6 +454,8 @@ const AssessmentWorkspace: React.FC = props => { ) => { const question = assessment!.questions[questionId]; const isGraded = question.grader !== undefined; + const isTeamAssessment = + assessmentOverview !== undefined ? assessmentOverview.maxTeamSize > 1 : false; const isContestVoting = question?.type === QuestionTypes.voting; const handleContestEntryClick = (_submissionId: number, answer: string) => { // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. @@ -446,6 +471,30 @@ const AssessmentWorkspace: React.FC = props => { } ]; + if (isTeamAssessment) { + tabs.push({ + label: `Team`, + iconName: IconNames.PEOPLE, + body: ( +
+ {teamFormationOverview === undefined ? ( + 'You are not assigned to any team!' + ) : ( +
+ Your teammates for this assessment:{' '} + {teamFormationOverview.studentNames.map((name: string, index: number) => ( + + {index > 0 ? ', ' : ''} + {name} + + ))} +
+ )} +
+ ) + }); + } + if (isContestVoting) { tabs.push( { @@ -597,8 +646,48 @@ const AssessmentWorkspace: React.FC = props => { }; const onClickReturn = () => navigate(listingPath); - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - const onClickSave = () => handleSave(question.id, editorTabs[0].value); + const onClickSave = () => { + if (isSaving) return; + setIsSaving(true); + checkLastModified(); + setTimeout(() => { + setIsSaving(false); + }, 3000); + }; + + const checkLastModified = () => { + const isTeamAssessment: boolean = assessmentOverview?.maxTeamSize !== 0; + if (isTeamAssessment && question.type === QuestionTypes.programming) { + handleCheckLastModifiedAt(question.id, question.lastModifiedAt, saveClick); + } + }; + + const saveClick = async (modified: boolean) => { + const isTeamAssessment: boolean = assessmentOverview?.maxTeamSize !== 0; + if (isTeamAssessment && question.type === QuestionTypes.programming) { + if (modified) { + const confirm = await showSimpleConfirmDialog({ + contents: ( + <> +

Save answer?

+

Note: The changes made by your teammate will be lost.

+ + ), + positiveIntent: 'danger', + positiveLabel: 'Save' + }); + + if (!confirm) { + return; + } + } + } + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + handleSave(question.id, editorTabs[0].value); + setTimeout(() => { + handleAssessmentFetch(props.assessmentId); + }, 1000); + }; const onClickResetTemplate = () => { setShowResetTemplateOverlay(true); @@ -650,10 +739,20 @@ const AssessmentWorkspace: React.FC = props => { /> ); + // Define the function to check if the Save button should be disabled + const shouldDisableSaveButton = (): boolean | undefined => { + const isIndividualAssessment: boolean = assessmentOverview?.maxTeamSize === 1; + if (isIndividualAssessment) { + return false; + } + return !teamFormationOverview; + }; + const saveButton = props.canSave && question.type === QuestionTypes.programming ? ( diff --git a/src/commons/assessmentWorkspace/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap b/src/commons/assessmentWorkspace/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap index 8c418ef182..f8fce30095 100644 --- a/src/commons/assessmentWorkspace/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap +++ b/src/commons/assessmentWorkspace/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap @@ -2909,8 +2909,12 @@ exports[`AssessmentWorkspace AssessmentWorkspace page with programming question - + - +
+ + +
+ Team Formation +
+
- - -
- Admin Panel -
-
): 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 { @@ -415,6 +451,11 @@ function* BackendSaga(): SagaIterator { function* (action: ReturnType) { const tokens: Tokens = yield selectTokens(); + const role: Role = yield select((state: OverallState) => state.session.role!); + if (role === Role.Student) { + return; + } + const { filterToGroup, gradedFilter, pageParams, filterParams } = action.payload; const gradingOverviews: GradingOverviews | null = yield call( @@ -431,6 +472,138 @@ function* BackendSaga(): SagaIterator { } ); + yield takeEvery( + FETCH_TEAM_FORMATION_OVERVIEW, + function* (action: ReturnType) { + const tokens: Tokens = yield selectTokens(); + const { assessmentId } = action.payload; + + const teamFormationOverview: TeamFormationOverview | null = yield call( + getTeamFormationOverview, + assessmentId, + tokens + ); + if (teamFormationOverview) { + yield put(actions.updateTeamFormationOverview(teamFormationOverview)); + } + } + ); + + yield takeEvery(FETCH_TEAM_FORMATION_OVERVIEWS, function* () { + const tokens: Tokens = yield selectTokens(); + + const role: Role = yield select((state: OverallState) => state.session.role!); + if (role === Role.Student) { + return; + } + + const teamFormationOverviews: TeamFormationOverview[] | null = yield call( + getTeamFormationOverviews, + tokens + ); + if (teamFormationOverviews) { + yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); + } + }); + + yield takeEvery(FETCH_STUDENTS, function* (): any { + const tokens: Tokens = yield selectTokens(); + const role: Role = yield select((state: OverallState) => state.session.role!); + if (role === Role.Student) { + return; + } + const students: User[] | null = yield call(getStudents, tokens); + if (students) { + yield put(actions.updateStudents(students)); + } + }); + + yield takeEvery(CREATE_TEAM, function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const { assessment, teams } = action.payload; + + const resp: Response | null = yield call(postTeams, assessment.id, teams, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + const teamFormationOverviews: TeamFormationOverview[] | null = yield call( + getTeamFormationOverviews, + tokens + ); + if (teamFormationOverviews) { + yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); + } + yield call(showSuccessMessage, 'Team created successfully', 1000); + 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 + ); + + yield call(showSuccessMessage, 'Team created successfully', 1000); + if (teamFormationOverviews) { + yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); + } + } + ); + + yield takeEvery(UPDATE_TEAM, function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const { teamId, assessment, teams } = action.payload; + const resp: Response | null = yield call(putTeams, assessment.id, teamId, teams, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + const teamFormationOverviews: TeamFormationOverview[] | null = yield call( + getTeamFormationOverviews, + tokens + ); + + yield call(showSuccessMessage, 'Team updated successfully', 1000); + if (teamFormationOverviews) { + yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); + } + }); + + yield takeEvery(DELETE_TEAM, function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const { teamId } = action.payload; + + const resp: Response | null = yield call(deleteTeam, teamId, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + const teamFormationOverviews: TeamFormationOverview[] | null = yield call( + getTeamFormationOverviews, + tokens + ); + + yield call(showSuccessMessage, 'Team deleted successfully', 1000); + if (teamFormationOverviews) { + yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); + } + }); + yield takeEvery(FETCH_GRADING, function* (action: ReturnType) { const tokens: Tokens = yield selectTokens(); const id = action.payload; @@ -1103,6 +1276,23 @@ function* BackendSaga(): SagaIterator { } ); + yield takeEvery( + CHANGE_TEAM_SIZE_ASSESSMENT, + function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const id = action.payload.id; + const maxTeamSize = action.payload.maxTeamSize; + + const resp: Response | null = yield updateAssessment(id, { maxTeamSize }, tokens); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + + yield put(actions.fetchAssessmentOverviews()); + yield call(showSuccessMessage, 'Team size updated successfully!', 1000); + } + ); + yield takeEvery( DELETE_ASSESSMENT, function* (action: ReturnType): any { diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 784c1d859f..189ff86468 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -1,4 +1,5 @@ import { call } from 'redux-saga/effects'; +import { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm'; import { AchievementGoal, @@ -20,6 +21,7 @@ import { WebSocketEndpointInformation } from '../../features/remoteExecution/RemoteExecutionTypes'; import { PlaybackData, SourcecastData } from '../../features/sourceRecorder/SourceRecorderTypes'; +import { TeamFormationOverview } from '../../features/teamFormation/TeamFormationTypes'; import { UsernameRoleGroup } from '../../pages/academy/adminPanel/subcomponents/AddUserPanel'; import { store } from '../../pages/createStore'; import { @@ -58,6 +60,9 @@ import Constants from '../utils/Constants'; import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; import { request } from '../utils/RequestHelper'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const XLSX = require('xlsx'); + /** * GET / * (health check) @@ -422,6 +427,7 @@ export const getAssessmentOverviews = async ( return null; // invalid accessToken _and_ refreshToken } const assessmentOverviews = await resp.json(); + return assessmentOverviews.map((overview: any) => { overview.gradingStatus = computeGradingStatus( overview.isManuallyGraded, @@ -588,6 +594,32 @@ export const postAnswer = async ( return resp; }; +/** + * POST /courses/{courseId}/assessments/question/{questionId}/answer + */ +export const checkAnswerLastModifiedAt = async ( + id: number, + lastModifiedAt: string, + tokens: Tokens +): Promise => { + const resp = await request( + `${courseId()}/assessments/question/${id}/answerLastModified`, + 'POST', + { + ...tokens, + body: { + lastModifiedAt: lastModifiedAt + }, + noHeaderAccept: true + } + ); + if (!resp) { + return null; // invalid accessToken _and_ refreshToken + } + const answerIsModified = await resp.json(); + return answerIsModified.lastModified; +}; + /** * POST /courses/{courseId}/assessments/{assessmentId}/submit */ @@ -631,13 +663,19 @@ export const getGradingOverviews = async ( assessmentNumber: overview.assessment.assessmentNumber, assessmentName: overview.assessment.title, assessmentType: overview.assessment.type, - studentId: overview.student.id, - studentUsername: overview.student.username, - studentName: overview.student.name, + studentId: overview.student ? overview.student.id : -1, + studentName: overview.student ? overview.student.name : undefined, + studentNames: overview.team + ? overview.team.team_members.map((member: { name: any }) => member.name) + : undefined, + studentUsername: overview.student ? overview.student.name : undefined, + studentUsernames: overview.team + ? overview.team.team_members.map((member: { username: any }) => member.username) + : undefined, submissionId: overview.id, submissionStatus: overview.status, - groupName: overview.student.groupName, - groupLeaderId: overview.student.groupLeaderId, + groupName: overview.student ? overview.student.groupName : '-', + groupLeaderId: overview.student ? overview.student.groupLeaderId : undefined, // Grading Status gradingStatus: 'none', questionCount: overview.assessment.questionCount, @@ -665,6 +703,201 @@ export const getGradingOverviews = async ( }; }; +/* + * GET /courses/{courseId}/admin/teams + */ +export const getTeamFormationOverviews = async ( + tokens: Tokens +): Promise => { + const resp = await request(`${courseId()}/admin/teams`, 'GET', { + ...tokens + }); + if (!resp) { + return null; // invalid accessToken _and_ refreshToken + } + const teamFormationOverviews = await resp.json(); + return teamFormationOverviews + .map((overview: any) => { + const teamFormationOverview: TeamFormationOverview = { + teamId: overview.teamId, + assessmentId: overview.assessmentId, + assessmentName: overview.assessmentName, + assessmentType: overview.assessmentType, + studentIds: overview.studentIds, + studentNames: overview.studentNames + }; + return teamFormationOverview; + }) + .sort( + (subX: TeamFormationOverview, subY: TeamFormationOverview) => + subY.assessmentId - subX.assessmentId + ); +}; + +/* + * GET /courses/{courseId}/team/{assessmentId} + */ +export const getTeamFormationOverview = async ( + assessmentId: number, + tokens: Tokens +): Promise => { + const resp = await request(`${courseId()}/team/${assessmentId}`, 'GET', { + ...tokens + }); + if (!resp) { + return null; // invalid accessToken _and_ refreshToken + } + const team = await resp.json(); + const teamFormationOverview: TeamFormationOverview = { + teamId: team.teamId, + assessmentId: team.assessmentId, + assessmentName: team.assessmentName, + assessmentType: team.assessmentType, + studentIds: team.studentIds, + studentNames: team.studentNames + }; + return teamFormationOverview; +}; + +/* + * POST /courses/{courseId}/admin/teams + */ +export const postTeams = async ( + assessmentId: number, + teams: OptionType[][], + tokens: Tokens +): Promise => { + const data = { + team: { + assessment_id: assessmentId, + student_ids: teams.map(team => team.map(option => option?.value)) + } + }; + + const resp = await request(`${courseId()}/admin/teams`, 'POST', { + body: data, + ...tokens + }); + return resp; +}; + +type CsvData = string[][]; + +/* + * POST /courses/{courseId}/admin/teams + */ +export const postUploadTeams = async ( + assessmentId: number, + teams: File, + students: User[] | undefined, + tokens: Tokens +): Promise => { + const parsed_teams: OptionType[][] = []; + + const teamsArrayBuffer = await readFileAsArrayBuffer(teams); + const workbook = XLSX.read(teamsArrayBuffer, { type: 'array' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const csvData: CsvData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); + + for (let i = 0; i < csvData.length; i++) { + const studentNames = csvData[i]; + const team: OptionType[] = []; + studentNames.forEach((username: string) => { + const student = students?.find((s: any) => s.username.trim() === username.trim()); + if (student) { + team.push({ + label: student.name, + value: student + }); + } + }); + parsed_teams.push(team); + } + + const data = { + team: { + assessment_id: assessmentId, + student_ids: parsed_teams.map(team => team.map(option => option?.value)) + } + }; + + const resp = await request(`${courseId()}/admin/teams`, 'POST', { + body: data, + ...tokens + }); + return resp; +}; + +const readFileAsArrayBuffer = async (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = event => { + if (event.target) { + const result = event.target.result as ArrayBuffer; + resolve(result); + } else { + reject(new Error('Error reading file')); + } + }; + reader.onerror = event => { + reject(new Error('Error reading file')); + }; + reader.readAsArrayBuffer(file); + }); +}; + +/* + * PUT /courses/{courseId}/admin/teams/{teamId} + */ +export const putTeams = async ( + assessmentId: number, + teamId: number, + teams: OptionType[][], + tokens: Tokens +): Promise => { + const data = { + teamId: teamId, + assessmentId: assessmentId, + student_ids: teams.map(team => team.map(option => option?.value)) + }; + + const resp = await request(`${courseId()}/admin/teams/${teamId}`, 'PUT', { + body: data, + ...tokens + }); + return resp; +}; + +/* + * DELETE /courses/{courseId}/admin/teams/{teamId} + */ +export const deleteTeam = async (teamId: number, tokens: Tokens): Promise => { + const data = { + teamId: teamId + }; + + const resp = await request(`${courseId()}/admin/teams/${teamId}`, 'DELETE', { + body: data, + ...tokens + }); + return resp; +}; + +/* + * GET /courses/{courseId}/admin/users/teamformation + */ +export const getStudents = async (tokens: Tokens): Promise => { + const resp = await request(`${courseId()}/admin/users/teamformation`, 'GET', { + ...tokens + }); + if (!resp || !resp.ok) { + return null; + } + + return await resp.json(); +}; + /** * GET /courses/{courseId}/admin/grading/{submissionId} */ @@ -682,10 +915,11 @@ export const getGrading = async ( const gradingResult = await resp.json(); const grading: GradingAnswer = gradingResult.answers.map((gradingQuestion: any) => { - const { student, question, grade } = gradingQuestion; + const { student, question, grade, team } = gradingQuestion; const result = { question: { answer: question.answer, + lastModifiedAt: question.lastModifiedAt, autogradingResults: question.autogradingResults || [], choices: question.choices, content: question.content, @@ -700,6 +934,7 @@ export const getGrading = async ( maxXp: question.maxXp }, student, + team, grade: { xp: grade.xp, xpAdjustment: grade.xpAdjustment, @@ -898,7 +1133,7 @@ export const deleteSourcecastEntry = async ( */ export const updateAssessment = async ( id: number, - body: { openAt?: string; closeAt?: string; isPublished?: boolean }, + body: { openAt?: string; closeAt?: string; isPublished?: boolean; maxTeamSize?: number }, tokens: Tokens ): Promise => { const resp = await request(`${courseId()}/admin/assessments/${id}`, 'POST', { diff --git a/src/commons/sagas/__tests__/BackendSaga.ts b/src/commons/sagas/__tests__/BackendSaga.ts index 4bc740624f..145b4f87f5 100644 --- a/src/commons/sagas/__tests__/BackendSaga.ts +++ b/src/commons/sagas/__tests__/BackendSaga.ts @@ -2,6 +2,7 @@ import { Chapter, Variant } from 'js-slang/dist/types'; import { createMemoryRouter } from 'react-router'; import { call } from 'redux-saga/effects'; import { expectSaga } from 'redux-saga-test-plan'; +import { mockTeamFormationOverviews } from 'src/commons/mocks/TeamFormationMocks'; import { ADD_NEW_USERS_TO_COURSE, CREATE_COURSE } from 'src/features/academy/AcademyTypes'; import { UsernameRoleGroup } from 'src/pages/academy/adminPanel/subcomponents/AddUserPanel'; @@ -21,7 +22,9 @@ import { updateAssessment, updateAssessmentOverviews, updateLatestViewedCourse, - updateNotifications + updateNotifications, + updateStudents, + updateTeamFormationOverviews } from '../../application/actions/SessionActions'; import { GameState, @@ -42,6 +45,8 @@ import { FETCH_AUTH, FETCH_COURSE_CONFIG, FETCH_NOTIFICATIONS, + FETCH_STUDENTS, + FETCH_TEAM_FORMATION_OVERVIEWS, FETCH_USER_AND_COURSE, REAUTOGRADE_ANSWER, REAUTOGRADE_SUBMISSION, @@ -58,6 +63,8 @@ import { UPDATE_COURSE_CONFIG, UPDATE_COURSE_RESEARCH_AGREEMENT, UPDATE_LATEST_VIEWED_COURSE, + UPDATE_STUDENTS, + UPDATE_TEAM_FORMATION_OVERVIEWS, UPDATE_USER_ROLE, UpdateCourseConfiguration, User @@ -76,7 +83,7 @@ import { mockAssessments } from '../../mocks/AssessmentMocks'; import { mockGradingSummary } from '../../mocks/GradingMocks'; -import { mockNotifications } from '../../mocks/UserMocks'; +import { mockNotifications, mockStudents } from '../../mocks/UserMocks'; import { Notification } from '../../notificationBadge/NotificationBadgeTypes'; import { AuthProviderType, computeRedirectUri } from '../../utils/AuthHelper'; import Constants from '../../utils/Constants'; @@ -99,6 +106,8 @@ import { getGradingSummary, getLatestCourseRegistrationAndConfiguration, getNotifications, + getStudents, + getTeamFormationOverviews, getUser, getUserCourseRegistrations, postAcknowledgeNotifications, @@ -126,11 +135,14 @@ const mockMapAssessments = new Map(mockAssessments.map(a => const mockAssessmentQuestion = mockAssessmentQuestions[0]; +const mockTeamFormationOverview = mockTeamFormationOverviews[0]; + const mockTokens = { accessToken: 'access', refreshToken: 'refresherOrb' }; const mockUser: User = { userId: 123, name: 'user', + username: 'user', courses: [ { courseId: 1, @@ -271,6 +283,8 @@ const mockStates = { session: { assessmentOverviews: mockAssessmentOverviews, assessments: mockMapAssessments, + teamFormationOverviews: mockTeamFormationOverviews, + teamFormationOverview: mockTeamFormationOverview, notifications: mockNotifications, ...mockTokens, ...mockUser, @@ -566,6 +580,52 @@ describe('Test FETCH_ASSESSMENT_OVERVIEWS action', () => { }); }); +describe('Test FETCH_TEAM_FORMATION_OVERVIEWS action', () => { + test('when team formation overviews are obtained', () => { + return expectSaga(BackendSaga) + .withState({ session: mockTokens }) + .provide([[call(getTeamFormationOverviews, mockTokens), mockTeamFormationOverviews]]) + .put(updateTeamFormationOverviews(mockTeamFormationOverviews)) + .hasFinalState({ session: mockTokens }) + .dispatch({ type: FETCH_TEAM_FORMATION_OVERVIEWS }) + .silentRun(); + }); + + test('when team formation overviews is null', () => { + return expectSaga(BackendSaga) + .withState({ session: mockTokens }) + .provide([[call(getTeamFormationOverviews, mockTokens), null]]) + .call(getTeamFormationOverviews, mockTokens) + .not.put.actionType(UPDATE_TEAM_FORMATION_OVERVIEWS) + .hasFinalState({ session: mockTokens }) + .dispatch({ type: FETCH_TEAM_FORMATION_OVERVIEWS }) + .silentRun(); + }); +}); + +describe('Test FETCH_STUDENTS action', () => { + test('when students are obtained', () => { + return expectSaga(BackendSaga) + .withState({ session: mockTokens }) + .provide([[call(getStudents, mockTokens), mockStudents]]) + .put(updateStudents(mockStudents)) + .hasFinalState({ session: mockTokens }) + .dispatch({ type: FETCH_STUDENTS }) + .silentRun(); + }); + + test('when students is null', () => { + return expectSaga(BackendSaga) + .withState({ session: mockTokens }) + .provide([[call(getStudents, mockTokens), null]]) + .call(getStudents, mockTokens) + .not.put.actionType(UPDATE_STUDENTS) + .hasFinalState({ session: mockTokens }) + .dispatch({ type: FETCH_STUDENTS }) + .silentRun(); + }); +}); + describe('Test FETCH_ASSESSMENT action', () => { test('when assessment is obtained', () => { const mockId = mockAssessment.id; diff --git a/src/commons/utils/RequestHelper.tsx b/src/commons/utils/RequestHelper.tsx index c90b30187a..ea466f74b8 100644 --- a/src/commons/utils/RequestHelper.tsx +++ b/src/commons/utils/RequestHelper.tsx @@ -56,7 +56,7 @@ export const request = async ( try { const url = rawUrl ? rawUrl : `${Constants.backendUrl}/v2/${path}`; const resp = await fetch(url, fetchOptions); - if (resp.ok) { + if (resp.ok || resp.status === 409) { return resp; } diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index 0968bab4fd..00df1326f4 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -34,6 +34,7 @@ import { EVAL_EDITOR_AND_TESTCASES, EVAL_REPL, EVAL_TESTCASE, + GroundControlTableFilters, MOVE_CURSOR, NAV_DECLARATION, PLAYGROUND_EXTERNAL_SELECT, @@ -50,6 +51,7 @@ import { SET_TOKEN_COUNT, SHIFT_EDITOR_TAB, SubmissionsTableFilters, + TeamFormationsTableFilters, TOGGLE_EDITOR_AUTORUN, TOGGLE_FOLDER_MODE, TOGGLE_UPDATE_CSE, @@ -63,6 +65,7 @@ import { UPDATE_CURRENTSTEP, UPDATE_EDITOR_BREAKPOINTS, UPDATE_EDITOR_VALUE, + UPDATE_GROUND_CONTROL_TABLE_FILTERS, UPDATE_HAS_UNSAVED_CHANGES, UPDATE_LAST_DEBUGGER_RESULT, UPDATE_LAST_NON_DET_RESULT, @@ -70,6 +73,7 @@ import { UPDATE_STEPSTOTAL, UPDATE_SUBLANGUAGE, UPDATE_SUBMISSIONS_TABLE_FILTERS, + UPDATE_TEAM_FORMATIONS_TABLE_FILTERS, UPDATE_WORKSPACE, WorkspaceLocation, WorkspaceLocationsWithTools, @@ -400,6 +404,16 @@ export const updateSubmissionsTableFilters = createAction( (filters: SubmissionsTableFilters) => ({ payload: { filters } }) ); +export const updateTeamFormationsTableFilters = createAction( + UPDATE_TEAM_FORMATIONS_TABLE_FILTERS, + (filters: TeamFormationsTableFilters) => ({ payload: { filters } }) +); + +export const updateGroundControlTableFilters = createAction( + UPDATE_GROUND_CONTROL_TABLE_FILTERS, + (filters: GroundControlTableFilters) => ({ payload: { filters } }) +); + export const updateCurrentAssessmentId = createAction( UPDATE_CURRENT_ASSESSMENT_ID, (assessmentId: number, questionId: number) => ({ payload: { assessmentId, questionId } }) diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index 40e341b480..a9bd133761 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -89,6 +89,7 @@ import { UPDATE_STEPSTOTAL, UPDATE_SUBLANGUAGE, UPDATE_SUBMISSIONS_TABLE_FILTERS, + UPDATE_TEAM_FORMATIONS_TABLE_FILTERS, UPDATE_WORKSPACE, WorkspaceLocation, WorkspaceManagerState @@ -626,6 +627,14 @@ const oldWorkspaceReducer: Reducer = ( submissionsTableFilters: action.payload.filters } }; + case UPDATE_TEAM_FORMATIONS_TABLE_FILTERS: + return { + ...state, + teamFormation: { + ...state.teamFormation, + teamFormationTableFilters: action.payload.filters + } + }; case UPDATE_CURRENT_ASSESSMENT_ID: return { ...state, diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 53343053e5..7656f6fc67 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -39,6 +39,8 @@ export const TOGGLE_USING_SUBST = 'TOGGLE_USING_SUBST'; export const TOGGLE_USING_CSE = 'TOGGLE_USING_CSE'; export const TOGGLE_UPDATE_CSE = 'TOGGLE_UPDATE_CSE'; export const UPDATE_SUBMISSIONS_TABLE_FILTERS = 'UPDATE_SUBMISSIONS_TABLE_FILTERS'; +export const UPDATE_TEAM_FORMATIONS_TABLE_FILTERS = 'UPDATE_TEAM_FORMATIONS_TABLE_FILTERS'; +export const UPDATE_GROUND_CONTROL_TABLE_FILTERS = 'UPDATE_GROUND_CONTROL_TABLE_FILTERS'; export const UPDATE_CURRENT_ASSESSMENT_ID = 'UPDATE_CURRENT_ASSESSMENT_ID'; export const UPDATE_CURRENT_SUBMISSION_ID = 'UPDATE_CURRENT_SUBMISSION_ID'; export const TOGGLE_FOLDER_MODE = 'TOGGLE_FOLDER_MODE'; @@ -81,8 +83,20 @@ type GradingWorkspaceAttr = { readonly currentQuestion?: number; readonly hasUnsavedChanges: boolean; }; + +type TeamFormationWorkspaceAttr = { + readonly teamFormationTableFilters: TeamFormationsTableFilters; +}; + type GradingWorkspaceState = GradingWorkspaceAttr & WorkspaceState; +type TeamFormationWorkspaceState = TeamFormationWorkspaceAttr & WorkspaceState; + +type GroundControlWorkspaceAttr = { + readonly GroundControlTableFilters: GroundControlTableFilters; +}; +type GroundControlWorkspaceState = GroundControlWorkspaceAttr & WorkspaceState; + type PlaygroundWorkspaceAttr = { readonly usingSubst: boolean; readonly usingCse: boolean; @@ -98,6 +112,8 @@ export type SicpWorkspaceState = PlaygroundWorkspaceState; export type WorkspaceManagerState = { readonly assessment: AssessmentWorkspaceState; readonly grading: GradingWorkspaceState; + readonly teamFormation: TeamFormationWorkspaceState; + readonly groundControl: GroundControlWorkspaceState; readonly playground: PlaygroundWorkspaceState; readonly sourcecast: SourcecastWorkspaceState; readonly sourcereel: SourcereelWorkspaceState; @@ -167,3 +183,12 @@ export type DebuggerContext = { export type SubmissionsTableFilters = { columnFilters: { id: string; value: unknown }[]; }; + +export type TeamFormationsTableFilters = { + columnFilters: { id: string; value: unknown }[]; + globalFilter: string | null; +}; + +export type GroundControlTableFilters = { + columnFilters: { id: string; value: unknown }[]; +}; diff --git a/src/commons/workspace/__tests__/WorkspaceReducer.ts b/src/commons/workspace/__tests__/WorkspaceReducer.ts index 7620ec2a09..f8c2843a03 100644 --- a/src/commons/workspace/__tests__/WorkspaceReducer.ts +++ b/src/commons/workspace/__tests__/WorkspaceReducer.ts @@ -116,6 +116,14 @@ function generateDefaultWorkspace(payload: any = {}): WorkspaceManagerState { stories: { ...defaultWorkspaceManager.stories, ...cloneDeep(payload) + }, + teamFormation: { + ...defaultWorkspaceManager.teamFormation, + ...cloneDeep(payload) + }, + groundControl: { + ...defaultWorkspaceManager.groundControl, + ...cloneDeep(payload) } }; } diff --git a/src/features/academy/AcademyTypes.ts b/src/features/academy/AcademyTypes.ts index 8dcd3bbd3f..34891d1a95 100644 --- a/src/features/academy/AcademyTypes.ts +++ b/src/features/academy/AcademyTypes.ts @@ -5,6 +5,7 @@ export const assessmentFullPathRegex = /\/courses\/\d+\/[a-zA-Z]+\/\d+\/\d+/; export const assessmentRegExp = ':assessmentId?/:questionId?'; export const gradingRegExp = ':submissionId?/:questionId?'; +export const teamRegExp = ':teamId?'; export const CREATE_COURSE = 'CREATE_COURSE'; export const ADD_NEW_USERS_TO_COURSE = 'ADD_NEW_USERS_TO_COURSE'; diff --git a/src/features/grading/GradingTypes.ts b/src/features/grading/GradingTypes.ts index aa99ea8bc6..5e531b046e 100644 --- a/src/features/grading/GradingTypes.ts +++ b/src/features/grading/GradingTypes.ts @@ -23,8 +23,10 @@ export type GradingOverview = { currentXp: number; maxXp: number; studentId: number; - studentName: string; - studentUsername: string; + studentName: string | undefined; + studentNames: string[] | undefined; + studentUsername: string | undefined; + studentUsernames: string[] | undefined; submissionId: number; submissionStatus: string; groupName: string; @@ -71,6 +73,11 @@ export type GradingQuery = { */ export type GradingQuestion = { question: AnsweredQuestion; + team?: Array<{ + username: any; + name: string; + id: number; + }>; student: { name: string; username: string; @@ -101,6 +108,7 @@ export type AnsweredQuestion = Question & Answer; type Answer = { autogradingResults: AutogradingResult[]; + lastModifiedAt: string; prepend: string; postpend: string; testcases: Testcase[]; diff --git a/src/features/groundControl/GroundControlActions.ts b/src/features/groundControl/GroundControlActions.ts index ecdc77aab7..b4dd6076e7 100644 --- a/src/features/groundControl/GroundControlActions.ts +++ b/src/features/groundControl/GroundControlActions.ts @@ -2,6 +2,7 @@ import { createAction } from '@reduxjs/toolkit'; import { CHANGE_DATE_ASSESSMENT, + CHANGE_TEAM_SIZE_ASSESSMENT, DELETE_ASSESSMENT, PUBLISH_ASSESSMENT, UPLOAD_ASSESSMENT @@ -12,6 +13,11 @@ export const changeDateAssessment = createAction( (id: number, openAt: string, closeAt: string) => ({ payload: { id, openAt, closeAt } }) ); +export const changeTeamSizeAssessment = createAction( + CHANGE_TEAM_SIZE_ASSESSMENT, + (id: number, maxTeamSize: number) => ({ payload: { id, maxTeamSize } }) +); + export const deleteAssessment = createAction(DELETE_ASSESSMENT, (id: number) => ({ payload: id })); export const publishAssessment = createAction( diff --git a/src/features/groundControl/GroundControlTypes.ts b/src/features/groundControl/GroundControlTypes.ts index d248b2d7c8..6881b4e036 100644 --- a/src/features/groundControl/GroundControlTypes.ts +++ b/src/features/groundControl/GroundControlTypes.ts @@ -1,4 +1,5 @@ export const CHANGE_DATE_ASSESSMENT = 'CHANGE_DATE_ASSESSMENT'; +export const CHANGE_TEAM_SIZE_ASSESSMENT = 'CHANGE_TEAM_SIZE_ASSESSMENT'; export const DELETE_ASSESSMENT = 'DELETE_ASSESSMENT'; export const PUBLISH_ASSESSMENT = 'PUBLISH_ASSESSMENT'; export const UPLOAD_ASSESSMENT = 'UPLOAD_ASSESSMENT'; diff --git a/src/features/teamFormation/TeamFormationTypes.ts b/src/features/teamFormation/TeamFormationTypes.ts new file mode 100644 index 0000000000..5806cfeb9a --- /dev/null +++ b/src/features/teamFormation/TeamFormationTypes.ts @@ -0,0 +1,14 @@ +import { AssessmentType } from '../../commons/assessment/AssessmentTypes'; + +/** + * Information on a Team, for a particular student submission + * for a particular assessment. Used for display in the Team Formation Table. + */ +export type TeamFormationOverview = { + teamId: number; + assessmentId: number; + assessmentName: string; + assessmentType: AssessmentType; + studentIds: number[]; + studentNames: string[]; +}; diff --git a/src/pages/academy/Academy.tsx b/src/pages/academy/Academy.tsx index 8f9a3514fb..4dc179621b 100644 --- a/src/pages/academy/Academy.tsx +++ b/src/pages/academy/Academy.tsx @@ -11,11 +11,18 @@ import classes from 'src/styles/Academy.module.scss'; import { fetchNotifications, + fetchStudents, + fetchTeamFormationOverviews, updateLatestViewedCourse } from '../../commons/application/actions/SessionActions'; import Assessment from '../../commons/assessment/Assessment'; import { assessmentTypeLink } from '../../commons/utils/ParamParseHelper'; -import { assessmentRegExp, gradingRegExp, numberRegExp } from '../../features/academy/AcademyTypes'; +import { + assessmentRegExp, + gradingRegExp, + numberRegExp, + teamRegExp +} from '../../features/academy/AcademyTypes'; import Achievement from '../achievement/Achievement'; import NotFound from '../notFound/NotFound'; import Sourcecast from '../sourcecast/Sourcecast'; @@ -27,11 +34,16 @@ import Grading from './grading/Grading'; import GroundControl from './groundControl/GroundControlContainer'; import NotiPreference from './notiPreference/NotiPreference'; import Sourcereel from './sourcereel/Sourcereel'; +import TeamFormationForm from './teamFormation/subcomponents/TeamFormationForm'; +import TeamFormationImport from './teamFormation/subcomponents/TeamFormationImport'; +import TeamFormation from './teamFormation/TeamFormation'; const Academy: React.FC = () => { const dispatch = useDispatch(); React.useEffect(() => { + dispatch(fetchStudents()); dispatch(fetchNotifications()); + dispatch(fetchTeamFormationOverviews(false)); }, [dispatch]); const { agreedToResearch, assessmentConfigurations, enableGame, role } = useSession(); @@ -43,7 +55,15 @@ const Academy: React.FC = () => { } key={1} />, } key={2} />, } key={3} />, - } key={4} /> + } key={4} />, + } key={5} />, + } + key={6} + />, + } key={7} />, + } key={8} /> ] : null; return ( diff --git a/src/pages/academy/adminPanel/subcomponents/AddUserPanel.tsx b/src/pages/academy/adminPanel/subcomponents/AddUserPanel.tsx index 13637fbf66..b8345768bf 100644 --- a/src/pages/academy/adminPanel/subcomponents/AddUserPanel.tsx +++ b/src/pages/academy/adminPanel/subcomponents/AddUserPanel.tsx @@ -100,7 +100,6 @@ const AddUserPanel: React.FC = props => { * valid uploaded entries in the table */ const processed: UsernameRoleGroup[] = [...users]; - if (data.length + users.length > 1000) { setInvalidCsvMsg('Please limit each upload to 1000 entries!'); return; diff --git a/src/pages/academy/grading/Grading.tsx b/src/pages/academy/grading/Grading.tsx index 8f4ae43087..af28f984c3 100644 --- a/src/pages/academy/grading/Grading.tsx +++ b/src/pages/academy/grading/Grading.tsx @@ -92,7 +92,12 @@ const Grading: React.FC = () => { const submissions = gradingOverviews?.data?.map(e => - !e.studentName ? { ...e, studentName: '(user has yet to log in)' } : e + !e.studentName + ? { + ...e, + studentName: Array.isArray(e.studentNames) ? e.studentNames.join(', ') : e.studentNames + } + : e ) ?? []; return ( diff --git a/src/pages/academy/grading/subcomponents/GradingEditor.tsx b/src/pages/academy/grading/subcomponents/GradingEditor.tsx index 57dc6a69ca..20ed22af44 100644 --- a/src/pages/academy/grading/subcomponents/GradingEditor.tsx +++ b/src/pages/academy/grading/subcomponents/GradingEditor.tsx @@ -44,8 +44,8 @@ type Props = { initialXp: number; xpAdjustment: number; maxXp: number; - studentName: string; - studentUsername: string; + studentNames: string[]; + studentUsernames: string[]; comments: string; graderName?: string; gradedAt?: string; @@ -249,7 +249,16 @@ const GradingEditor: React.FC = props => {

- Currently Grading: {props.studentName} ({props.studentUsername}) + Currently Grading: +
+ {props.studentNames.map((name, index) => ( +
+ + {name} ({props.studentUsernames[index]}) + +
+
+ ))}

{props.solution !== null ? ( diff --git a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx index 21f8aef388..ab63b9cf79 100644 --- a/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx +++ b/src/pages/academy/grading/subcomponents/GradingSubmissionsTable.tsx @@ -55,12 +55,36 @@ const makeColumns = (handleClick: () => void) => [ ) }), columnHelper.accessor('studentName', { - header: 'Student', - cell: info => + header: 'Student(s)', + cell: info => { + const value = info.getValue(); + const fallbackValue = info.row.original.studentNames; + const finalValue = value || ''; + const finalFallbackValue = fallbackValue?.join(', ') || ''; + return ( + + ); + } }), columnHelper.accessor('studentUsername', { - header: 'Username', - cell: info => + header: 'Username(s)', + cell: info => { + const value = info.getValue(); + const fallbackValue = info.row.original.studentUsernames; + const finalValue = value || ''; + const finalFallbackValue = fallbackValue?.join(', ') || ''; + return ( + + ); + } }), columnHelper.accessor('groupName', { header: 'Group', diff --git a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx index 33c8d26361..ee5051e572 100644 --- a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx +++ b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx @@ -307,8 +307,16 @@ const GradingWorkspace: React.FC = props => { initialXp={grading!.answers[questionId].grade.xp} xpAdjustment={grading!.answers[questionId].grade.xpAdjustment} maxXp={grading!.answers[questionId].question.maxXp} - studentName={grading!.answers[questionId].student.name} - studentUsername={grading!.answers[questionId].student.username} + studentNames={ + grading![questionId].student.name + ? [grading!.answers[questionId].student.name] + : grading!.answers[questionId].team!.map(member => member.name) + } + studentUsernames={ + grading![questionId].student.username + ? [grading!.answers[questionId].student.username] + : grading!.answers[questionId].team!.map(member => member.username) + } comments={grading!.answers[questionId].grade.comments ?? ''} graderName={ grading!.answers[questionId].grade.grader diff --git a/src/pages/academy/groundControl/GroundControl.tsx b/src/pages/academy/groundControl/GroundControl.tsx index 375bbe6bc3..d4319429ba 100644 --- a/src/pages/academy/groundControl/GroundControl.tsx +++ b/src/pages/academy/groundControl/GroundControl.tsx @@ -1,21 +1,52 @@ -import 'ag-grid-community/styles/ag-grid.css'; -import 'ag-grid-community/styles/ag-theme-balham.css'; - -import { Button, Collapse, Divider, Intent } from '@blueprintjs/core'; +import { + Button, + Collapse, + Divider, + Icon as BpIcon, + Intent, + NonIdealState, + Spinner, + SpinnerSize +} from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { ColDef, ColumnApi, GridApi, GridReadyEvent } from 'ag-grid-community'; -import { AgGridReact } from 'ag-grid-react'; -import React from 'react'; +import { + Column, + ColumnFilter, + ColumnFiltersState, + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + useReactTable +} from '@tanstack/react-table'; +import { + Flex, + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + Text +} from '@tremor/react'; +import React, { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useSession, useTypedSelector } from 'src/commons/utils/Hooks'; +import { updateGroundControlTableFilters } from 'src/commons/workspace/WorkspaceActions'; import { AssessmentConfiguration, AssessmentOverview } from '../../../commons/assessment/AssessmentTypes'; import ContentDisplay from '../../../commons/ContentDisplay'; +import { AssessmentTypeBadge } from '../grading/subcomponents/GradingBadges'; +import GroundControlFilters from './GroundControlFilters'; import DefaultChapterSelect from './subcomponents/DefaultChapterSelect'; import DeleteCell from './subcomponents/GroundControlDeleteCell'; import Dropzone from './subcomponents/GroundControlDropzone'; import EditCell from './subcomponents/GroundControlEditCell'; +import EditTeamSizeCell from './subcomponents/GroundControlEditTeamSizeCell'; import PublishCell from './subcomponents/GroundControlPublishCell'; export type GroundControlProps = DispatchProps & StateProps; @@ -26,6 +57,7 @@ export type DispatchProps = { handleUploadAssessment: (file: File, forceUpdate: boolean, assessmentConfigId: number) => void; handlePublishAssessment: (togglePublishTo: boolean, id: number) => void; handleAssessmentChangeDate: (id: number, openAt: string, closeAt: string) => void; + handleAssessmentChangeTeamSize: (id: number, maxTeamSize: number) => void; handleFetchCourseConfigs: () => void; }; @@ -34,207 +66,242 @@ export type StateProps = { assessmentConfigurations?: AssessmentConfiguration[]; }; -type State = { - showDropzone: boolean; -}; +const columnHelper = createColumnHelper(); -class GroundControl extends React.Component { - private columnDefs: ColDef[]; - private defaultColumnDefs: ColDef; - private gridApi?: GridApi; - private columnApi?: ColumnApi; - - public constructor(props: GroundControlProps) { - super(props); - - this.state = { - showDropzone: false - }; - - this.columnDefs = [ - { - field: 'number', - headerName: 'ID', - width: 50 - }, - { - headerName: 'Title', - field: 'title' - }, - { - headerName: 'Category', - field: 'type', - width: 100 - }, - { - headerName: 'Open Date', - field: 'openAt', - filter: 'agDateColumnFilter', - filterParams: { - comparator: this.dateFilterComparator, - inRangeInclusive: true - }, - sortingOrder: ['desc', 'asc', null], - cellRenderer: EditCell, - cellRendererParams: { - handleAssessmentChangeDate: this.props.handleAssessmentChangeDate, - forOpenDate: true - }, - width: 150 - }, - { - headerName: 'Close Date', - field: 'closeAt', - filter: 'agDateColumnFilter', - filterParams: { - comparator: this.dateFilterComparator, - inRangeInclusive: true - }, - sortingOrder: ['desc', 'asc', null], - cellRenderer: EditCell, - cellRendererParams: { - handleAssessmentChangeDate: this.props.handleAssessmentChangeDate, - forOpenDate: false - }, - width: 150 - }, - { - headerName: 'Publish', - field: '', - cellRenderer: PublishCell, - cellRendererParams: { - handlePublishAssessment: this.props.handlePublishAssessment - }, - width: 100, - filter: false, - resizable: false, - sortable: false, - cellStyle: { - padding: 0 - } - }, - { - headerName: 'Delete', - field: '', - cellRenderer: DeleteCell, - cellRendererParams: { - handleDeleteAssessment: this.props.handleDeleteAssessment - }, - width: 100, - filter: false, - resizable: false, - sortable: false, - cellStyle: { - padding: 0 - } - } - ]; - - this.defaultColumnDefs = { - filter: true, - resizable: true, - sortable: true - }; - } +const GroundControl: React.FC = props => { + const [showDropzone, setShowDropzone] = useState(false); - public render() { - const controls = ( -
- - - -
- ); + const dispatch = useDispatch(); + + const tableFilters = useTypedSelector( + state => state.workspaces.groundControl.GroundControlTableFilters + ); - const dropzone = ( - - ([ + ...tableFilters.columnFilters + ]); + + const { assessmentOverviews } = useSession(); + + const toggleDropzone = () => { + setShowDropzone(!showDropzone); + }; + + useEffect(() => { + dispatch(updateGroundControlTableFilters({ columnFilters })); + }, [columnFilters, dispatch]); + + const columns = [ + columnHelper.accessor('id', { + header: 'ID', + cell: info => {info.getValue()} + }), + columnHelper.accessor('title', { + header: 'Title', + cell: info => {info.getValue()}, + enableSorting: true, + sortingFn: 'alphanumeric' + }), + columnHelper.accessor('type', { + header: 'Category', + cell: info => ( + + + + ), + enableSorting: true + }), + columnHelper.accessor('openAt', { + header: 'Open Date', + cell: info => ( + - - ); + ), + enableSorting: true + }), + columnHelper.accessor('closeAt', { + header: 'Close Date', + cell: info => ( + + ), + enableSorting: true + }), - const grid = ( -
- ( + { + props.handleAssessmentChangeTeamSize(id, newTeamSize); + }} + data={info.row.original} /> -
- ); + ) + }), + columnHelper.accessor('isPublished', { + header: 'Publish', + cell: info => ( + + ), + enableSorting: false + }), + columnHelper.display({ + header: 'Delete', + size: 100, + cell: info => ( + + ), + enableSorting: false + }) + ]; - const content = ( -
- {controls} - {dropzone} - - {grid} -
- ); + const controls = ( +
+ + + +
+ ); + + const dropzone = ( + + + + ); + + const table = useReactTable({ + data: assessmentOverviews ?? [], + columns, + state: { columnFilters }, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel() + }); + + const handleFilterRemove = ({ id, value }: ColumnFilter) => { + const newFilters = columnFilters.filter(filter => filter.id !== id && filter.value !== value); + setColumnFilters(newFilters); + }; + + const grid = ( +
+ + +
+ + + {columnFilters.length > 0 + ? 'Filters: ' + : 'No filters applied. Click on any cell to filter by its value.'}{' '} + +
+ +
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + {header.isPlaceholder ? null : ( +
{flexRender(header.column.columnDef.header, header.getContext())}
+ )} +
+ ))} +
+ ))} +
+ + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + {cell.getIsPlaceholder() + ? null + : flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+
+ ); + if (!assessmentOverviews) { return ( -
- -
+ } + /> ); } - private loadContent = () => { + const content = ( +
+ {controls} + {dropzone} + + {grid} +
+ ); + + const loadContent = () => { // Always load AssessmentOverviews and CourseConfigs to get the latest values (just in case) - this.props.handleAssessmentOverviewFetch(); - this.props.handleFetchCourseConfigs(); + props.handleAssessmentOverviewFetch(); + props.handleFetchCourseConfigs(); }; - /* - * Reference: https://www.ag-grid.com/javascript-grid-filter-date/#date-filter-comparator - */ - private dateFilterComparator = (filterDate: Date, cellValue: string) => { - const cellDate = new Date(cellValue); - - return cellDate < filterDate ? -1 : cellDate > filterDate ? 1 : 0; - }; + return ( +
+ +
+ ); +}; - private onGridReady = (params: GridReadyEvent) => { - this.gridApi = params.api; - this.columnApi = params.columnApi; - this.gridApi.sizeColumnsToFit(); - - // Sort assessments by opening date, breaking ties by later of closing dates - this.columnApi.applyColumnState({ - state: [ - { colId: 'openAt', sort: 'desc' }, - { colId: 'closeAt', sort: 'desc' } - ] - }); - }; +type FilterableProps = { + column: Column; + value: string; + children?: React.ReactNode; +}; - private resizeGrid = () => { - if (this.gridApi) { - this.gridApi.sizeColumnsToFit(); - } +const Filterable: React.FC = ({ column, value, children }) => { + const handleFilterChange = () => { + column.setFilterValue(value); }; - private toggleDropzone = () => { - this.setState({ showDropzone: !this.state.showDropzone }); - }; -} + return ( + + ); +}; export default GroundControl; diff --git a/src/pages/academy/groundControl/GroundControlContainer.ts b/src/pages/academy/groundControl/GroundControlContainer.ts index d050fca668..687120648e 100644 --- a/src/pages/academy/groundControl/GroundControlContainer.ts +++ b/src/pages/academy/groundControl/GroundControlContainer.ts @@ -8,6 +8,7 @@ import { import { OverallState } from '../../../commons/application/ApplicationTypes'; import { changeDateAssessment, + changeTeamSizeAssessment, deleteAssessment, publishAssessment, uploadAssessment @@ -23,6 +24,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis bindActionCreators( { handleAssessmentChangeDate: changeDateAssessment, + handleAssessmentChangeTeamSize: changeTeamSizeAssessment, handleAssessmentOverviewFetch: fetchAssessmentOverviews, handleDeleteAssessment: deleteAssessment, handleUploadAssessment: uploadAssessment, diff --git a/src/pages/academy/groundControl/GroundControlFilters.tsx b/src/pages/academy/groundControl/GroundControlFilters.tsx new file mode 100644 index 0000000000..ba831bad50 --- /dev/null +++ b/src/pages/academy/groundControl/GroundControlFilters.tsx @@ -0,0 +1,21 @@ +import { ColumnFilter, ColumnFiltersState } from '@tanstack/react-table'; +import { Flex } from '@tremor/react'; + +import { FilterBadge } from '../grading/subcomponents/GradingBadges'; + +type GroundControlFiltersProps = { + filters: ColumnFiltersState; + onFilterRemove: (filter: ColumnFilter) => void; +}; + +const GroundControlFilters: React.FC = ({ filters, onFilterRemove }) => { + return ( + + {filters.map(filter => ( + + ))} + + ); +}; + +export default GroundControlFilters; diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlEditTeamSizeCell.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlEditTeamSizeCell.tsx new file mode 100644 index 0000000000..f8294217aa --- /dev/null +++ b/src/pages/academy/groundControl/subcomponents/GroundControlEditTeamSizeCell.tsx @@ -0,0 +1,66 @@ +import '@tremor/react/dist/esm/tremor.css'; + +import { Icon as BpIcon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Button, Flex } from '@tremor/react'; +import React, { useCallback } from 'react'; + +import { AssessmentOverview } from '../../../../commons/assessment/AssessmentTypes'; + +type Props = DispatchProps & StateProps; + +type DispatchProps = { + onTeamSizeChange: (id: number, newTeamSize: number) => void; +}; + +type StateProps = { + data: AssessmentOverview; +}; + +const EditTeamSizeCell: React.FC = ({ data, onTeamSizeChange }) => { + const minTeamSize = 1; // Corresponds to an individual assessment + const teamSize = data.maxTeamSize; + + const changeTeamSize = useCallback( + (size: number) => { + if (teamSize === size) { + return; + } + onTeamSizeChange(data.id, size); + }, + [data.id, teamSize, onTeamSizeChange] + ); + + const handleIncrement = () => { + const updatedTeamSize = teamSize + 1; + changeTeamSize(updatedTeamSize); + }; + + const handleDecrement = () => { + if (teamSize > minTeamSize) { + const updatedTeamSize = teamSize - 1; + changeTeamSize(updatedTeamSize); + } + }; + + return ( + + + + ); +}; + +export default TeamFormationActions; diff --git a/src/pages/academy/teamFormation/subcomponents/TeamFormationBadges.tsx b/src/pages/academy/teamFormation/subcomponents/TeamFormationBadges.tsx new file mode 100644 index 0000000000..e5ab93dc01 --- /dev/null +++ b/src/pages/academy/teamFormation/subcomponents/TeamFormationBadges.tsx @@ -0,0 +1,51 @@ +import { Icon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { ColumnFilter } from '@tanstack/react-table'; +import { Badge } from '@tremor/react'; + +const BADGE_COLORS = { + // assessment types + missions: 'indigo', + quests: 'emerald', + paths: 'sky' +}; + +export function getBadgeColorFromLabel(label: string) { + return BADGE_COLORS[label.toLowerCase()] || 'gray'; +} + +type AssessmentTypeBadgeProps = { + type: string; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +}; + +const AssessmentTypeBadge: React.FC = ({ type, size = 'sm' }) => { + return ( + + ); +}; + +type FilterBadgeProps = { + filter: ColumnFilter; + onRemove: (filter: ColumnFilter) => void; +}; + +const FilterBadge: React.FC = ({ filter, onRemove }) => { + let filterValue = filter.value as string; + filterValue = filterValue.charAt(0).toUpperCase() + filterValue.slice(1); + return ( + + ); +}; + +export { AssessmentTypeBadge, FilterBadge }; diff --git a/src/pages/academy/teamFormation/subcomponents/TeamFormationDashboard.tsx b/src/pages/academy/teamFormation/subcomponents/TeamFormationDashboard.tsx new file mode 100644 index 0000000000..30ec3fd77a --- /dev/null +++ b/src/pages/academy/teamFormation/subcomponents/TeamFormationDashboard.tsx @@ -0,0 +1,47 @@ +import '@tremor/react/dist/esm/tremor.css'; + +import { Button, Card, Col, ColGrid, Flex, Title } from '@tremor/react'; +import { useNavigate } from 'react-router'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; +import { TeamFormationOverview } from 'src/features/teamFormation/TeamFormationTypes'; + +import TeamFormationTable from './TeamFormationTable'; + +type TeamFormationDashboardProps = { + teams: TeamFormationOverview[]; +}; + +const TeamFormationDashboard: React.FC = ({ teams }) => { + const group = useTypedSelector(state => state.session.group); + const { courseId } = useTypedSelector(state => state.session); + const navigate = useNavigate(); + + const createTeam = () => { + navigate(`/courses/${courseId}/teamformation/create`); + }; + + const importTeam = () => { + navigate(`/courses/${courseId}/teamformation/import`); + }; + + const teamData = teams; + return ( + + + + + + Teams + + +   + + + + + + + ); +}; + +export default TeamFormationDashboard; diff --git a/src/pages/academy/teamFormation/subcomponents/TeamFormationFilters.tsx b/src/pages/academy/teamFormation/subcomponents/TeamFormationFilters.tsx new file mode 100644 index 0000000000..553de31d7b --- /dev/null +++ b/src/pages/academy/teamFormation/subcomponents/TeamFormationFilters.tsx @@ -0,0 +1,21 @@ +import { ColumnFilter, ColumnFiltersState } from '@tanstack/react-table'; +import { Flex } from '@tremor/react'; + +import { FilterBadge } from './TeamFormationBadges'; + +type TeamFormationFiltersProps = { + filters: ColumnFiltersState; + onFilterRemove: (filter: ColumnFilter) => void; +}; + +const TeamFormationFilters: React.FC = ({ filters, onFilterRemove }) => { + return ( + + {filters.map(filter => ( + + ))} + + ); +}; + +export default TeamFormationFilters; diff --git a/src/pages/academy/teamFormation/subcomponents/TeamFormationForm.tsx b/src/pages/academy/teamFormation/subcomponents/TeamFormationForm.tsx new file mode 100644 index 0000000000..a1bda1eeaf --- /dev/null +++ b/src/pages/academy/teamFormation/subcomponents/TeamFormationForm.tsx @@ -0,0 +1,210 @@ +import '@tremor/react/dist/esm/tremor.css'; + +import React, { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router'; +import { Form, useParams } from 'react-router-dom'; +import Select, { ActionMeta, MultiValue } from 'react-select'; +import { createTeam, updateTeam } from 'src/commons/application/actions/SessionActions'; +import { User } from 'src/commons/application/types/SessionTypes'; +import { AssessmentOverview } from 'src/commons/assessment/AssessmentTypes'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; +import { TeamFormationOverview } from 'src/features/teamFormation/TeamFormationTypes'; +import classes from 'src/styles/TeamFormation.module.scss'; + +export type OptionType = { + label: string | null; + value: User | null; +} | null; + +const TeamFormationForm: React.FC = () => { + const { teamId } = useParams(); // Retrieve the team ID from the URL + const assessmentOverviews = useTypedSelector(state => state.session.assessmentOverviews); + const { courseId, students, teamFormationOverviews } = useTypedSelector(state => state.session); + const dispatch = useDispatch(); + const [selectedAssessment, setSelectedAssessment] = useState( + undefined + ); + const [teams, setTeams] = useState([[]]); + const navigate = useNavigate(); + + let maxNoOfStudents: number | undefined = selectedAssessment ? selectedAssessment.maxTeamSize : 0; + + useEffect(() => { + if (teamId) { + const existingTeam: TeamFormationOverview | undefined = teamFormationOverviews?.find( + team => team.teamId.toString() === teamId + ); + + if (existingTeam) { + const existingAssessment: AssessmentOverview | undefined = assessmentOverviews?.find( + assessment => assessment.id === existingTeam.assessmentId + ); + setSelectedAssessment(existingAssessment); + + const existingTeams: OptionType[][] = existingTeam.studentIds + .map( + _ => + students + ?.filter(student => existingTeam.studentIds.includes(student.userId)) + .map(student => ({ + label: student.name, + value: student + })) as OptionType[] + ) + .slice(0, 1); + setTeams(existingTeams); + } + } + }, [assessmentOverviews, students, teamFormationOverviews, teamId]); + + const handleTeamChange = ( + index: number, + selectedOption: MultiValue, + actionMeta: ActionMeta + ) => { + const updatedTeams = [...teams]; + updatedTeams[index] = selectedOption as unknown as OptionType[]; + setTeams(updatedTeams); + }; + + const addAnotherTeam = () => { + setTeams([...teams, []]); + }; + + const removeTeam = (index: number) => { + const updatedTeams = [...teams]; + updatedTeams.splice(index, 1); + setTeams(updatedTeams); + }; + + const backToTeamDashboard = () => { + navigate(`/courses/${courseId}/teamformation`); + }; + + const handleAssessmentChange = (assessment: AssessmentOverview | undefined) => { + setSelectedAssessment(assessment); + maxNoOfStudents = assessment?.maxTeamSize; + }; + + const submitForm = () => { + if (!selectedAssessment) { + alert('Please select an assessment.'); + return; + } + + const hasEmptyTeam = teams.some(team => team.length === 0); + if (hasEmptyTeam) { + alert('Each team must have at least one student.'); + return; + } + + const isTeamSizeExceeded = teams.some(team => team.length > selectedAssessment.maxTeamSize); + if (isTeamSizeExceeded) { + alert('The number of students in a team cannot exceed the max team size.'); + return; + } + + if (teamId) { + dispatch(updateTeam(parseInt(teamId, 10), selectedAssessment, teams)); + } else { + dispatch(createTeam(selectedAssessment, teams)); + } + navigate(`/courses/${courseId}/teamformation`); + }; + + return ( +
+
+

{teamId ? 'Edit' : 'Create New'} Team

+
+
+ + +
+ )} +
+ + {teams.map((t, index) => ( +
+ +
+ ({ + label: assessment.title, + value: assessment + }))} + value={ + selectedAssessment + ? { label: selectedAssessment.title, value: selectedAssessment } + : null + } + onChange={option => handleAssessmentChange(option?.value)} + isSearchable + className={classes['form-select']} + /> +
+ {selectedAssessment && ( +
+ + +
+ )} +
+ + +

{file ? `File name: ${file.name}` : 'No file uploaded'}

+ +
+ + +
+ +
+
+ +
+ ); +}; + +export default TeamFormationImport; diff --git a/src/pages/academy/teamFormation/subcomponents/TeamFormationTable.tsx b/src/pages/academy/teamFormation/subcomponents/TeamFormationTable.tsx new file mode 100644 index 0000000000..28b0131de3 --- /dev/null +++ b/src/pages/academy/teamFormation/subcomponents/TeamFormationTable.tsx @@ -0,0 +1,250 @@ +import '@tremor/react/dist/esm/tremor.css'; + +import { Icon as BpIcon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { + Column, + ColumnFilter, + ColumnFiltersState, + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + Row, + useReactTable +} from '@tanstack/react-table'; +import { + Bold, + Button, + Flex, + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + Text, + TextInput +} from '@tremor/react'; +import React from 'react'; +import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; +import { updateTeamFormationsTableFilters } from 'src/commons/workspace/WorkspaceActions'; +import { TeamFormationOverview } from 'src/features/teamFormation/TeamFormationTypes'; + +import { AssessmentTypeBadge } from '../../teamFormation/subcomponents/TeamFormationBadges'; +import TeamFormationFilters from '../../teamFormation/subcomponents/TeamFormationFilters'; +import TeamFormationActions from './TeamFormationActions'; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor('assessmentName', { + header: 'Assessment', + cell: info => + }), + columnHelper.accessor('assessmentType', { + header: 'Type', + cell: info => ( + + + + ) + }), + columnHelper.accessor('studentNames', { + header: 'Students', + cell: info => + info.getValue().map((name: string, index: number) => ( + + + {name} + + {', '} + + )), + filterFn: (row: Row, id: string | number, filterValue: any): boolean => { + const rowValue = row.original[id]; + return Array.isArray(rowValue) && rowValue.includes(filterValue); + } + }), + columnHelper.accessor(({ teamId }) => ({ teamId }), { + header: 'Actions', + enableColumnFilter: false, + cell: info => { + const { teamId } = info.getValue(); + return ; + } + }) +]; + +type TeamFormationTableProps = { + group: string | null; + teams: TeamFormationOverview[]; +}; + +const TeamFormationTable: React.FC = ({ group, teams }) => { + const dispatch = useDispatch(); + const tableFilters = useTypedSelector( + state => state.workspaces.teamFormation.teamFormationTableFilters + ); + + const defaultFilters = []; + if (group && !tableFilters.columnFilters.find(filter => filter.id === 'groupName')) { + defaultFilters.push({ + id: 'groupName', + value: group + }); + } + + const [columnFilters, setColumnFilters] = useState([ + ...tableFilters.columnFilters, + ...defaultFilters + ]); + + const [globalFilter, setGlobalFilter] = useState(tableFilters.globalFilter); + + const globalFilterFn = ( + row: Row, + columnId: string | number, + filterValue: any + ): boolean => { + for (const column of Object.keys(row.original)) { + const rowValue = row.original[column]; + + if (Array.isArray(rowValue)) { + for (const value of rowValue) { + if (typeof value === 'string' && value.includes(filterValue)) { + return true; + } + } + } else if (typeof rowValue === 'string' && rowValue.includes(filterValue)) { + return true; + } + } + + return false; + }; + + const table = useReactTable({ + data: teams, + columns, + state: { + columnFilters, + globalFilter + }, + onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: setGlobalFilter, + globalFilterFn: globalFilterFn, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel() + }); + + const handleFilterRemove = ({ id, value }: ColumnFilter) => { + const newFilters = columnFilters.filter(filter => filter.id !== id && filter.value !== value); + setColumnFilters(newFilters); + }; + + useEffect(() => { + dispatch( + updateTeamFormationsTableFilters({ + columnFilters, + globalFilter + }) + ); + }, [columnFilters, globalFilter, dispatch]); + + return ( + <> + + +
+ + + {columnFilters.length > 0 + ? 'Filters: ' + : 'No filters applied. Click on any cell to filter by its value.'}{' '} + +
+ +
+ + } + placeholder="Search for any value here..." + onChange={e => setGlobalFilter(e.target.value)} + /> +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+
+ +
+ + ); +}; + +type FilterableProps = { + column: Column; + value: string; + children?: React.ReactNode; +}; + +const Filterable: React.FC = ({ column, value, children }) => { + const handleFilterChange = () => { + column.setFilterValue(value); + }; + + return ( + + ); +}; + +export default TeamFormationTable; diff --git a/src/pages/fileSystem/createInBrowserFileSystem.ts b/src/pages/fileSystem/createInBrowserFileSystem.ts index ab175213bc..28bb2279f1 100644 --- a/src/pages/fileSystem/createInBrowserFileSystem.ts +++ b/src/pages/fileSystem/createInBrowserFileSystem.ts @@ -16,6 +16,8 @@ import { EditorTabState, WorkspaceManagerState } from '../../commons/workspace/W export const WORKSPACE_BASE_PATHS: Record = { assessment: '', grading: '', + teamFormation: '', + groundControl: '', playground: '/playground', sicp: '/sicp', sourcecast: '', diff --git a/src/styles/TeamFormation.module.scss b/src/styles/TeamFormation.module.scss new file mode 100644 index 0000000000..8de4aec72f --- /dev/null +++ b/src/styles/TeamFormation.module.scss @@ -0,0 +1,247 @@ +/* Form container */ +.form-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background-color: #ffffff; + margin-top: 20px; +} + +/* Form fields */ +.form-field { + width: 300px; + margin-bottom: 20px; + margin-right: 80px; +} + +.student-form-field { + margin-bottom: 25px; + width: 800px; +} + +/* Input container */ +.input-container { + display: flex; + align-items: center; +} + +/* Labels */ +.form-label { + font-size: 16px; + margin-bottom: 15px; +} + +/* Select inputs */ +.form-select { + flex: 1; /* Take all available space */ + width: 100%; + height: 36px; + border-radius: 4px; + font-size: 14px; + transition: height 0.3s; /* Add transition for smooth height change */ +} + +/* Form footer */ +.form-footer { + margin-top: 20px; + margin-bottom: 10px; + display: flex; + justify-content: space-between; + align-items: center; +} + +/* Submit button container */ +.submit-button-container { + margin-left: auto; /* Pushes the container to the right */ +} + +/* Submit button */ +.submit-button { + cursor: pointer; + position: relative; + padding: 6px 12px; + color: rgb(39, 223, 57); + border: 2px solid rgb(39, 223, 57); + border-radius: 4px; + background-color: transparent; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); + overflow: hidden; +} + +.submit-button::before { + content: ''; + position: absolute; + inset: 0; + margin: auto; + width: 50px; + height: 50px; + border-radius: inherit; + scale: 0; + z-index: -1; + background-color: rgb(39, 223, 57); + transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1); +} + +.submit-button:hover::before { + scale: 3; +} + +.submit-button:hover { + color: #ffffff; + scale: 1.1; + box-shadow: 0 0px 20px rgba(98, 193, 138, 0.4); +} + +.submit-button:active { + scale: 1; +} + +/* Back button */ +.back-button { + cursor: pointer; + position: relative; + padding: 6px 12px; + color: rgb(226, 0, 0); + border: 2px solid rgb(226, 0, 0); + border-radius: 4px; + background-color: transparent; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); + overflow: hidden; +} + +.back-button::before { + content: ''; + position: absolute; + inset: 0; + margin: auto; + width: 50px; + height: 50px; + border-radius: inherit; + scale: 0; + z-index: -1; + background-color: rgb(226, 0, 0); + transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1); +} + +.back-button:hover::before { + scale: 3; +} + +.back-button:hover { + color: #ffffff; + scale: 1.1; + box-shadow: 0 0px 20px rgba(98, 193, 138, 0.4); +} + +.back-button:active { + scale: 1; +} + +/* Add button */ +.add-button { + cursor: pointer; + position: relative; + padding: 6px 12px; + color: #3b82f6; + border: 2px solid #3b82f6; + border-radius: 4px; + background-color: transparent; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); + overflow: hidden; +} + +.add-button::before { + content: ''; + position: absolute; + inset: 0; + margin: auto; + width: 50px; + height: 50px; + border-radius: inherit; + scale: 0; + z-index: -1; + background-color: #3b82f6; + transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1); +} + +.add-button:hover::before { + scale: 3; +} + +.add-button:hover { + color: #ffffff; + scale: 1.1; + box-shadow: 0 0px 20px rgba(98, 193, 138, 0.4); +} + +.add-button:active { + scale: 1; +} + +/* Remove button */ +.remove-button { + cursor: pointer; + position: relative; + padding: 6px 12px; + color: rgb(255, 60, 60); + border: 2px solid rgb(255, 60, 60); + border-radius: 4px; + background-color: transparent; + font-weight: 600; + margin-left: 10px; + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); + overflow: hidden; +} + +.remove-button::before { + content: ''; + position: absolute; + inset: 0; + margin: auto; + width: 50px; + height: 50px; + border-radius: inherit; + scale: 0; + z-index: -1; + background-color: rgb(255, 60, 60); + transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1); +} + +.remove-button:hover::before { + scale: 3; +} + +.remove-button:hover { + color: #ffffff; + scale: 1.1; + box-shadow: 0 0px 20px rgba(98, 193, 138, 0.4); +} + +.remove-button:active { + scale: 1; +} + +/* Drag and Drop field*/ +#drag-drop-area { + border: 2px dashed #ccc; + padding: 40px; + text-align: center; +} + +#drag-drop-area p { + margin: 0; +} + +#drag-drop-area:hover { + outline: 2px solid lightblue; +} + +.form-field-row { + display: flex; + align-items: center; + gap: 16px; /* Adjust the spacing as needed */ +} diff --git a/yarn.lock b/yarn.lock index 996cf74327..1c0626a299 100644 --- a/yarn.lock +++ b/yarn.lock @@ -176,7 +176,7 @@ dependencies: "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.22.15": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== @@ -292,6 +292,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== +"@babel/parser@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac" + integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a" @@ -467,7 +472,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.23.3": +"@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.22.5", "@babel/plugin-syntax-jsx@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== @@ -1133,6 +1138,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.12.0", "@babel/runtime@^7.18.3": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" + integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.23.9", "@babel/template@^7.3.3": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" @@ -1158,6 +1170,22 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.4.5": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" + integrity sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.24.0" + "@babel/types" "^7.24.0" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.3", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" @@ -1167,6 +1195,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" + integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1388,6 +1425,111 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@emotion/babel-plugin@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" + integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/serialize" "^1.1.2" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" + integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/sheet" "^1.2.2" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + stylis "4.2.0" + +"@emotion/hash@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" + integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== + +"@emotion/is-prop-valid@^1.1.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== + dependencies: + "@emotion/memoize" "^0.8.1" + +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== + +"@emotion/react@^11.8.1": + version "11.11.4" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d" + integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/cache" "^11.11.0" + "@emotion/serialize" "^1.1.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.3.tgz#84b77bfcfe3b7bb47d326602f640ccfcacd5ffb0" + integrity sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA== + dependencies: + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/unitless" "^0.8.1" + "@emotion/utils" "^1.2.1" + csstype "^3.0.2" + +"@emotion/sheet@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" + integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== + +"@emotion/stylis@^0.8.4": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@^0.7.4": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/unitless@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" + integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== + +"@emotion/utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" + integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== + +"@emotion/weak-memoize@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" + integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== + "@eslint-community/eslint-utils@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518" @@ -1420,6 +1562,26 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== +"@floating-ui/core@^1.0.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" + integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g== + dependencies: + "@floating-ui/utils" "^0.2.1" + +"@floating-ui/dom@^1.0.1": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" + integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -2974,6 +3136,13 @@ dependencies: react-textarea-autosize "*" +"@types/react-transition-group@^4.4.0": + version "4.4.10" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" + integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^18.2.13": version "18.2.66" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.66.tgz#d2eafc8c4e70939c5432221adb23d32d76bfe451" @@ -3447,6 +3616,19 @@ adjust-sourcemap-loader@^4.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" +adler-32@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25" + integrity sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ== + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + +adler-32@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2" + integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== + ag-grid-community@31.1.1, ag-grid-community@^31.0.0: version "31.1.1" resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-31.1.1.tgz#212fc3e358d4be1865bc4618f6d0d865faaed385" @@ -3978,6 +4160,17 @@ babel-plugin-polyfill-regenerator@^0.5.5: dependencies: "@babel/helper-define-polyfill-provider" "^0.5.0" +"babel-plugin-styled-components@>= 1.12.0": + version "2.1.4" + resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz#9a1f37c7f32ef927b4b008b529feb4a2c82b1092" + integrity sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + lodash "^4.17.21" + picomatch "^2.3.1" + babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" @@ -4358,6 +4551,11 @@ camelcase@^6.2.0, camelcase@^6.2.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + caniuse-api@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" @@ -4401,6 +4599,14 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +cfb@^1.1.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" + integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== + dependencies: + adler-32 "~1.3.0" + crc-32 "~1.2.0" + chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -4584,6 +4790,14 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" +codepage@~1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99" + integrity sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw== + dependencies: + commander "~2.14.1" + exit-on-epipe "~1.0.1" + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -4665,6 +4879,16 @@ commander@^9.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== +commander@~2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + integrity sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw== + +commander@~2.17.1: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== + commist@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/commist/-/commist-1.1.0.tgz#17811ec6978f6c15ee4de80c45c9beb77cee35d5" @@ -4764,7 +4988,7 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^1.4.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== @@ -4874,6 +5098,11 @@ coveralls@^3.1.1: minimist "^1.2.5" request "^2.88.2" +crc-32@~1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -4907,6 +5136,11 @@ css-blank-pseudo@^3.0.3: dependencies: postcss-selector-parser "^6.0.9" +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + css-declaration-sorter@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec" @@ -4992,6 +5226,15 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" +css-to-react-native@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" @@ -6216,6 +6459,11 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +exit-on-epipe@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" + integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -6441,6 +6689,11 @@ find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -6551,6 +6804,11 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + fraction.js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" @@ -7048,7 +7306,7 @@ highlight.js@^10.4.1, highlight.js@~10.7.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -8844,6 +9102,11 @@ memfs@^3.1.2, memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -10432,7 +10695,7 @@ postcss-value-parser@^3.3.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -10528,6 +10791,11 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" +printj@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" + integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== + prismjs@^1.27.0: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" @@ -10581,7 +10849,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.7, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.7, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -10814,6 +11082,14 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-drag-drop-files@^2.3.10: + version "2.3.10" + resolved "https://registry.yarnpkg.com/react-drag-drop-files/-/react-drag-drop-files-2.3.10.tgz#3f6ea316f8ad66a6f76b312cc4108c7c14065b06" + integrity sha512-Fv614W9+OtXFB5O+gjompTxQZLYGO7wJeT4paETGiXtiADB9yPOMGYD4A3PMCTY9Be874/wcpl+2dm3MvCIRzg== + dependencies: + prop-types "^15.7.2" + styled-components "^5.3.0" + react-draggable@^4.4.5: version "4.4.6" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e" @@ -11032,6 +11308,21 @@ react-scripts@^5.0.1: optionalDependencies: fsevents "^2.3.2" +react-select@^5.7.3: + version "5.8.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.0.tgz#bd5c467a4df223f079dd720be9498076a3f085b5" + integrity sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@floating-ui/dom" "^1.0.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^6.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.1.2" + react-shallow-renderer@^16.15.0: version "16.15.0" resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" @@ -11101,7 +11392,7 @@ react-transition-group@2.9.0: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react-transition-group@^4.4.5: +react-transition-group@^4.3.0, react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== @@ -11779,6 +12070,11 @@ shallow-equal@^3.1.0: resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-3.1.0.tgz#e7a54bac629c7f248eff6c2f5b63122ba4320bec" integrity sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg== +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + sharedb@^1.4.1: version "1.9.2" resolved "https://registry.yarnpkg.com/sharedb/-/sharedb-1.9.2.tgz#136d06a4bbdd481a98f961cbeaf6fc367435fef8" @@ -12043,6 +12339,13 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + dependencies: + frac "~1.1.2" + sshpk@^1.7.0: version "1.17.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" @@ -12282,6 +12585,22 @@ style-to-object@^0.4.0: dependencies: inline-style-parser "0.1.1" +styled-components@^5.3.0: + version "5.3.11" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.11.tgz#9fda7bf1108e39bf3f3e612fcc18170dedcd57a8" + integrity sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/traverse" "^7.4.5" + "@emotion/is-prop-valid" "^1.1.0" + "@emotion/stylis" "^0.8.4" + "@emotion/unitless" "^0.7.4" + babel-plugin-styled-components ">= 1.12.0" + css-to-react-native "^3.0.0" + hoist-non-react-statics "^3.0.0" + shallowequal "^1.1.0" + supports-color "^5.5.0" + stylehacks@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" @@ -12290,12 +12609,17 @@ stylehacks@^5.1.1: browserslist "^4.21.4" postcss-selector-parser "^6.0.4" +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -12990,7 +13314,7 @@ use-composed-ref@^1.3.0: resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== -use-isomorphic-layout-effect@^1.1.1: +use-isomorphic-layout-effect@^1.1.1, use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== @@ -13435,11 +13759,21 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.4" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + workbox-background-sync@6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz#3141afba3cc8aa2ae14c24d0f6811374ba8ff6a9" @@ -13643,6 +13977,21 @@ ws@^8.4.2: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +xlsx@0.16.4: + version "0.16.4" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.16.4.tgz#6cc8913fb12846a7c76e090650d8bc4c4d3f02d1" + integrity sha512-l1xqTdXRK3DCxkxHGj3OxZM1ertzeqjWodi0jevBNSivoyYMPEJAHhVW7BAfM3gFXK35dCM0CacGUXbATdFvqQ== + dependencies: + adler-32 "~1.2.0" + cfb "^1.1.4" + codepage "~1.14.0" + commander "~2.17.1" + crc-32 "~1.2.0" + exit-on-epipe "~1.0.1" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"