From f85bd773bf01d8b32fb3256289426b77edc03b57 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 20 Nov 2023 17:13:11 +0000 Subject: [PATCH 01/66] fix: progressbar opens modal in xblock view --- src/components/ProgressBar/ProgressStep.jsx | 3 ++- src/components/ProgressBar/hooks.js | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/ProgressBar/ProgressStep.jsx b/src/components/ProgressBar/ProgressStep.jsx index 2370dd81..2ea16374 100644 --- a/src/components/ProgressBar/ProgressStep.jsx +++ b/src/components/ProgressBar/ProgressStep.jsx @@ -29,6 +29,7 @@ const ProgressStep = ({ label, }) => { const { + onClick, href, isActive, isEnabled, @@ -51,7 +52,7 @@ const ProgressStep = ({ } return ( { @@ -12,16 +15,19 @@ export const useProgressStepData = ({ step, canRevisit = false }) => { const isEmbedded = useIsEmbedded(); const viewStep = useViewStep(); const { effectiveGrade, stepState } = useGlobalState({ step }); + const openModal = useOpenModal(); const href = `/${stepRoutes[step]}${isEmbedded ? '/embedded' : ''}/${courseId}/${xblockId}`; + const onClick = () => openModal({ view: step, title: step }); const isActive = viewStep === step; const isEnabled = ( isActive || (stepState === stepStates.inProgress) || (canRevisit && stepState === stepStates.done) ); + return { - href, + ...(viewStep === stepNames.xblock ? { onClick } : { href }), isEnabled, isActive, isComplete: stepState === stepStates.done, From 262f82044dd333553f05f7e224d936a50879dcfe Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 20 Nov 2023 17:13:32 +0000 Subject: [PATCH 02/66] chore: better pageData logging, including xblockId --- src/data/services/lms/hooks/data.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts index d112beec..5ab08db3 100644 --- a/src/data/services/lms/hooks/data.ts +++ b/src/data/services/lms/hooks/data.ts @@ -2,7 +2,7 @@ import React from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth' +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { camelCaseObject } from '@edx/frontend-platform'; import { useHasSubmitted } from 'data/redux/hooks'; // for test data @@ -78,24 +78,23 @@ export const usePageData = () => { const viewKey = stepRoutes[viewStep]; const progressKey = testProgressKey || params.progressKey || defaultViewProgressKeys[viewKey]; - const queryFn = React.useCallback(() => { + const queryFn = () => { + console.log("page data query function"); if (testDataPath) { - console.log("page data fake data"); return Promise.resolve(camelCaseObject(loadState({ view, progressKey }))); } const url = (hasSubmitted || view === stepNames.xblock) ? pageDataUrl() : pageDataUrl(viewStep); - console.log({ url, hasSubmitted, view }); - console.log("page data real data"); console.log({ pageDataUrl: url }); + console.log({ params }); return getAuthenticatedHttpClient().post(url, {}) .then(({ data }) => camelCaseObject(data)) .then(data => { console.log({ pageData: data }); return data; }); - }, [testDataPath, view, progressKey, testProgressKey, hasSubmitted]); + }; return useQuery({ queryKey: [queryKeys.pageData, testDataPath], From fbdf846a5c080868cc54abd8014f21a6bf8d0415 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 20 Nov 2023 17:13:46 +0000 Subject: [PATCH 03/66] fix: start step button should now load new response --- src/hooks/actions/useStartStepAction.js | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/hooks/actions/useStartStepAction.js b/src/hooks/actions/useStartStepAction.js index 63cf5f1b..fe6ce02d 100644 --- a/src/hooks/actions/useStartStepAction.js +++ b/src/hooks/actions/useStartStepAction.js @@ -1,20 +1,16 @@ -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { stepNames, stepRoutes } from 'constants'; -import { useRefreshPageData, useActiveStepName, useSetResponse } from 'hooks/app'; -import { useSetHasSubmitted, useSetShowValidation } from 'hooks/assessment'; +import { + useActiveStepName, +} from 'hooks/app'; import messages from './messages'; const useStartStepAction = (viewStep) => { const { formatMessage } = useIntl(); - const navigate = useNavigate(); const { courseId, xblockId } = useParams(); - const refreshPageData = useRefreshPageData(); - const setHasSubmitted = useSetHasSubmitted(); - const setShowValidation = useSetShowValidation(); - const setResponse = useSetResponse(); const stepName = useActiveStepName(); @@ -22,15 +18,7 @@ const useStartStepAction = (viewStep) => { || [stepNames.submission, stepNames.staff].includes(stepName)) { return null; } - - const onClick = () => { - console.log("Load next page"); - setHasSubmitted(false); - setShowValidation(false); - setResponse(null); - navigate(`/${stepRoutes[stepName]}/${courseId}/${xblockId}`); - refreshPageData(); - }; + const url = `/${stepRoutes[stepName]}/${courseId}/${xblockId}`; const startMessages = { [stepNames.studentTraining]: messages.startTraining, @@ -38,7 +26,7 @@ const useStartStepAction = (viewStep) => { [stepNames.peer]: messages.startPeer, [stepNames.done]: messages.viewGrades, }; - return { children: formatMessage(startMessages[stepName]), onClick }; + return { children: formatMessage(startMessages[stepName]), href: url }; }; export default useStartStepAction; From 1621b4d88a3403b7303850c091160614011ec2c8 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 20 Nov 2023 18:56:08 +0000 Subject: [PATCH 04/66] fix: feedback requirement field --- .../Assessment/ReadonlyAssessment/AssessmentCriteria.jsx | 4 +--- src/components/Assessment/ReadonlyAssessment/Feedback.jsx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/Assessment/ReadonlyAssessment/AssessmentCriteria.jsx b/src/components/Assessment/ReadonlyAssessment/AssessmentCriteria.jsx index 60c091fd..ed59f09f 100644 --- a/src/components/Assessment/ReadonlyAssessment/AssessmentCriteria.jsx +++ b/src/components/Assessment/ReadonlyAssessment/AssessmentCriteria.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useCriteriaConfig } from 'hooks/assessment'; -import { feedbackRequirement } from 'constants'; import Feedback from './Feedback'; import messages from './messages'; @@ -22,7 +21,6 @@ const AssessmentCriteria = ({ criteria, overallFeedback, stepLabel }) => { return ( { /> ); })} +
{selectedOption} -- {selectedPoints} points

)} - {feedbackRequired !== feedbackRequirement.disabled && ( + {commentBody && (
From 1f4551aa57f6bec7dfebc47bd5020f2b378599a6 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Mon, 20 Nov 2023 14:01:10 -0500 Subject: [PATCH 05/66] chore: update student training finish state --- .../hooks/useFinishedStateActions.js | 6 ++++- .../services/lms/hooks/selectors/pageData.ts | 3 +++ src/data/services/lms/types/blockInfo.ts | 7 +----- src/data/services/lms/types/pageData.ts | 6 ++--- src/hooks/app.js | 1 + .../StudentTrainingView/index.jsx | 24 ++++++++++--------- 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/components/ModalActions/hooks/useFinishedStateActions.js b/src/components/ModalActions/hooks/useFinishedStateActions.js index d9e17e77..56bff37c 100644 --- a/src/components/ModalActions/hooks/useFinishedStateActions.js +++ b/src/components/ModalActions/hooks/useFinishedStateActions.js @@ -1,4 +1,4 @@ -import { useGlobalState } from 'hooks/app'; +import { useGlobalState, useTrainingStepIsCompleted } from 'hooks/app'; import { useHasSubmitted, useSubmittedAssessment, @@ -19,6 +19,7 @@ const useFinishedStateActions = () => { const startStepAction = useStartStepAction(step); const submittedAssessment = useSubmittedAssessment(); const loadNextAction = useLoadNextAction(); + const trainingStepIsCompleted = useTrainingStepIsCompleted(); const stepState = globalState.activeStepState; @@ -26,6 +27,9 @@ const useFinishedStateActions = () => { const exitAction = useExitAction(); if (!hasSubmitted) { + if (step === stepNames.studentTraining && trainingStepIsCompleted) { + return { primary: startStepAction, secondary: finishLaterAction }; + } return null; } diff --git a/src/data/services/lms/hooks/selectors/pageData.ts b/src/data/services/lms/hooks/selectors/pageData.ts index 7f3959cc..91e1e6a0 100644 --- a/src/data/services/lms/hooks/selectors/pageData.ts +++ b/src/data/services/lms/hooks/selectors/pageData.ts @@ -4,6 +4,7 @@ import { } from 'constants'; import * as data from 'data/services/lms/hooks/data'; import * as types from 'data/services/lms/types'; +import { useAssessmentStepConfig } from './oraConfig'; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Page Data @@ -87,3 +88,5 @@ export const useEffectiveGrade = () => { const assessment = useAssessmentData(); return assessment ? assessment[assessment.effectiveAssessmentType] : null; }; + +export const useTrainingStepIsCompleted = () => useStepInfo().studentTraining?.numberOfAssessmentsCompleted === useAssessmentStepConfig().settings.studentTraining.numberOfExamples; \ No newline at end of file diff --git a/src/data/services/lms/types/blockInfo.ts b/src/data/services/lms/types/blockInfo.ts index 0ee93851..2b8010b5 100644 --- a/src/data/services/lms/types/blockInfo.ts +++ b/src/data/services/lms/types/blockInfo.ts @@ -61,12 +61,7 @@ export interface SelfStepSettings { export interface TrainingStepSettings { required: boolean, - data: { - examples: { - response: string, - criteria: { name: string, selection: string }[], - }[], - }, + numberOfExamples: number, } export interface AssessmentStepConfig { diff --git a/src/data/services/lms/types/pageData.ts b/src/data/services/lms/types/pageData.ts index d81a82d1..0116825f 100644 --- a/src/data/services/lms/types/pageData.ts +++ b/src/data/services/lms/types/pageData.ts @@ -15,7 +15,7 @@ export interface RubricSelection { } export interface StepClosedInfo { - isClosed: boolean, + closed: boolean, closedReason?: 'notAvailable' | 'pastDue', } @@ -34,7 +34,7 @@ export interface SubmissionStepInfo extends StepClosedInfo { teamInfo: SubmissionTeamInfo | null, } -export interface LearnerTrainingStepInfo extends StepClosedInfo { +export interface StudentTrainingStepInfo extends StepClosedInfo { numberOfAssessmentsCompleted: number, expectedRubricSelctions: RubricSelection[], } @@ -48,7 +48,7 @@ export interface PeerStepInfo extends StepClosedInfo { export interface StepInfo { submission: SubmissionStepInfo, peer: PeerStepInfo | null, - learnerTraining: LearnerTrainingStepInfo | null, + studentTraining: StudentTrainingStepInfo | null, self: StepClosedInfo | null, } diff --git a/src/hooks/app.js b/src/hooks/app.js index 3c41c58f..57b49a0c 100644 --- a/src/hooks/app.js +++ b/src/hooks/app.js @@ -30,6 +30,7 @@ export const { useSubmissionConfig, useTextResponses, useFileUploadEnabled, + useTrainingStepIsCompleted, } = lmsSelectors; export const { diff --git a/src/views/AssessmentView/StudentTrainingView/index.jsx b/src/views/AssessmentView/StudentTrainingView/index.jsx index 6a2b230f..a00fefd0 100644 --- a/src/views/AssessmentView/StudentTrainingView/index.jsx +++ b/src/views/AssessmentView/StudentTrainingView/index.jsx @@ -22,17 +22,19 @@ export const StudentTrainingView = () => { } return ( {}}> -
- {React.Children.toArray( - prompts.map((prompt, index) => ( -
- - -
- )), - )} - -
+ {response && Object.keys(response).length > 0 && ( +
+ {React.Children.toArray( + prompts.map((prompt, index) => ( +
+ + +
+ )) + )} + +
+ )}
); }; From 4e12fbc1ad648c8e107238e395cc040eba667513 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Mon, 20 Nov 2023 14:33:22 -0500 Subject: [PATCH 06/66] fix: student training progress bar state bug --- src/data/services/lms/hooks/selectors/index.ts | 5 +++++ src/data/services/lms/hooks/selectors/pageData.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/data/services/lms/hooks/selectors/index.ts b/src/data/services/lms/hooks/selectors/index.ts index 5add47fc..18ab17d8 100644 --- a/src/data/services/lms/hooks/selectors/index.ts +++ b/src/data/services/lms/hooks/selectors/index.ts @@ -29,6 +29,7 @@ export const useStepState = ({ step = null } = {}) => { const activeStepIndex = selectors.useStepIndex({ step: activeStepName }); const stepIndex = selectors.useStepIndex({ step: stepName }); const subState = selectors.useSubmissionState(); + const trainingStepIsCompleted = selectors.useTrainingStepIsCompleted(); if (hasReceivedFinalGrade) { return stepStates.done; } @@ -44,6 +45,10 @@ export const useStepState = ({ step = null } = {}) => { return hasReceivedFinalGrade ? stepStates.done : stepStates.notAvailable; } + if (stepName === stepNames.studentTraining && trainingStepIsCompleted) { + return stepStates.done; + } + if (activeStepName === stepNames.peer && stepInfo?.peer?.isWaitingForSubmissions) { return stepStates.waiting; } diff --git a/src/data/services/lms/hooks/selectors/pageData.ts b/src/data/services/lms/hooks/selectors/pageData.ts index 91e1e6a0..36fd3020 100644 --- a/src/data/services/lms/hooks/selectors/pageData.ts +++ b/src/data/services/lms/hooks/selectors/pageData.ts @@ -31,6 +31,7 @@ export const useIsPageDataLoaded = (): boolean => { export const usePageData = (): types.PageData => { const pageData = data.usePageData()?.data; if (process.env.NODE_ENV === 'development') { + // @ts-ignore window.pageData = pageData; } return data.usePageData()?.data; @@ -69,7 +70,7 @@ export const useSubmissionState = () => { if (subStatus.hasSubmitted) { return stepStates.done; } - if (subStatus.isClosed) { + if (subStatus.closed) { if (subStatus.closedReason === closedReasons.pastDue) { return stepStates.closed; } From d09757749e94dc17aac25de89b0a67583cfdcc59 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Mon, 20 Nov 2023 14:54:06 -0500 Subject: [PATCH 07/66] chore: update peer grade waiting state --- src/components/ProgressBar/hooks.js | 25 ++++++++++--------- .../StudentTrainingView/index.jsx | 8 +++--- src/views/AssessmentView/index.jsx | 8 +++--- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/components/ProgressBar/hooks.js b/src/components/ProgressBar/hooks.js index 8caaaeb8..6a6cf6b0 100644 --- a/src/components/ProgressBar/hooks.js +++ b/src/components/ProgressBar/hooks.js @@ -2,29 +2,30 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { useIsEmbedded, useViewStep } from 'hooks/routing'; -import { useGlobalState } from 'hooks/app'; +import { useGlobalState, useStepInfo } from 'hooks/app'; import { useOpenModal } from 'hooks/modal'; -import { - stepRoutes, - stepStates, - stepNames, -} from 'constants'; +import { stepRoutes, stepStates, stepNames } from 'constants'; export const useProgressStepData = ({ step, canRevisit = false }) => { const { xblockId, courseId } = useParams(); const isEmbedded = useIsEmbedded(); const viewStep = useViewStep(); const { effectiveGrade, stepState } = useGlobalState({ step }); + const stepInfo = useStepInfo(); const openModal = useOpenModal(); - const href = `/${stepRoutes[step]}${isEmbedded ? '/embedded' : ''}/${courseId}/${xblockId}`; + const href = `/${stepRoutes[step]}${ + isEmbedded ? '/embedded' : '' + }/${courseId}/${xblockId}`; const onClick = () => openModal({ view: step, title: step }); const isActive = viewStep === step; - const isEnabled = ( - isActive - || (stepState === stepStates.inProgress) - || (canRevisit && stepState === stepStates.done) - ); + const isEnabled = + isActive || + stepState === stepStates.inProgress || + (canRevisit && + (stepState === stepStates.done || + (step === stepNames.peer && + stepInfo.peer?.numberOfReceivedAssessments > 0))); return { ...(viewStep === stepNames.xblock ? { onClick } : { href }), diff --git a/src/views/AssessmentView/StudentTrainingView/index.jsx b/src/views/AssessmentView/StudentTrainingView/index.jsx index a00fefd0..09a4fce6 100644 --- a/src/views/AssessmentView/StudentTrainingView/index.jsx +++ b/src/views/AssessmentView/StudentTrainingView/index.jsx @@ -20,21 +20,21 @@ export const StudentTrainingView = () => { if (!useIsORAConfigLoaded()) { return null; } + const responseIsEmpty = !!response?.textResponses?.length; + return ( {}}> - {response && Object.keys(response).length > 0 && (
{React.Children.toArray( prompts.map((prompt, index) => (
- + {responseIsEmpty && }
)) )} - + {responseIsEmpty && }
- )}
); }; diff --git a/src/views/AssessmentView/index.jsx b/src/views/AssessmentView/index.jsx index ff40ee38..0c82665e 100644 --- a/src/views/AssessmentView/index.jsx +++ b/src/views/AssessmentView/index.jsx @@ -9,10 +9,12 @@ import useAssessmentData from './useAssessmentData'; export const AssessmentView = () => { const { prompts, response, isLoaded } = useAssessmentData(); - if (!isLoaded || !response) { + if (!isLoaded) { return null; } + const responseIsEmpty = !!response?.textResponses?.length; + return ( {}}>
@@ -20,11 +22,11 @@ export const AssessmentView = () => { prompts.map((prompt, index) => (
- + {responseIsEmpty && }
)), )} - + {responseIsEmpty && }
); From d121a442bd0d1f3ea21498a3f94b507cb3e8565d Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 20 Nov 2023 19:03:45 +0000 Subject: [PATCH 08/66] chore: remove dead file --- src/data/services/lms/dataLoaders.js | 127 --------------------------- 1 file changed, 127 deletions(-) delete mode 100644 src/data/services/lms/dataLoaders.js diff --git a/src/data/services/lms/dataLoaders.js b/src/data/services/lms/dataLoaders.js deleted file mode 100644 index 1d623ecc..00000000 --- a/src/data/services/lms/dataLoaders.js +++ /dev/null @@ -1,127 +0,0 @@ -// ORA Config loaders -export const loadAssessmentConfig = ({ - assessmentSteps: { - order, - settings: { - peer, - self, - studentTraining, - staff, - }, - }, -}) => ({ - order, - peer: peer && { - startTime: peer.startTime, - endTime: peer.endTime, - required: peer.required, - data: { - minNumberToGrade: peer.data.minNumberToGrade, - minNumberToBeGradedBy: peer.data.minNumberToBeGradedBy, - enableFlexibleGrading: peer.data.enableFlexibleGrading, - }, - }, - self: self && { - startTime: self.startTime, - endTime: self.endTime, - required: self.required, - }, - studentTraining: studentTraining && { - required: studentTraining.required, - data: { examples: studentTraining.data.examples }, - }, - staff: staff && { required: staff.required }, -}); - -export const loadSubmissionConfig = ({ - submissionConfig: { - textResponseConfig: text, - fileResponseConfig: file, - teamsConfig, - ...config - }, -}) => ({ - startDatetime: config.startDatetime, - endDatetime: config.endDatetime, - textResponseConfig: text && { - enabled: text.enabled, - optional: text.optional, - editorType: text.editorType, - allowLatexPreview: text.allowLatexPreview, - }, - fileResponseConfig: file && { - enabled: file.enabled, - optional: file.optional, - fileUploadType: file.fileUploadType, - allowedExtensions: file.allowedExtensions, - blockedExtensions: file.blockedExtensions, - fileTypeDescription: file.fileTypeDescription, - }, - teamsConfig: teamsConfig && { - enabled: teamsConfig.enabled, - teamsetName: teamsConfig.teamsetName, - }, -}); - -export const loadRubricConfig = ({ rubric }) => ({ - showDuringResponse: rubric.showDuringResponse, - feedbackConfig: { - description: rubric.feedbackConfig.description, - defaultText: rubric.feedbackConfig.defaultText, - }, - criteria: rubric.criteria.map(criterion => ({ - name: criterion.name, - description: criterion.description, - feedbackEnabled: criterion.feedbackEnabled, - feedbackRequired: criterion.feedbackRequired, - options: criterion.options.map(option => ({ - name: option.name, - points: option.points, - description: option.description, - })), - })), -}); - -export const loadORAConfigData = (data) => ({ - title: data.title, - prompts: data.prompts, - baseAssetUrl: data.baseAssetUrl, - submissionConfig: loadSubmissionConfig(data), - assessmentSteps: loadAssessmentConfig(data), - rubric: loadRubricConfig(data), - leaderboardConfig: { - enabled: data.leaderboardConfig.enabled, - numberOfEntries: data.leaderboardConfig.numberOfEntries, - }, -}); - -// Submission loaders -export const loadFile = (file) => { - console.log({ loadFile: file }); - return { - url: file.fileUrl, - description: file.fileDescription, - name: file.fileName, - size: file.fileSize, - uploadedBy: file.uploadedBy, - }; -}; - -export const loadSubmissionData = ({ teamInfo, submissionStatus, submission }) => ({ - teamInfo: { - teamName: teamInfo.teamName, - teamUsernames: teamInfo.teamUsernames, - previousTeamName: teamInfo.previousTeamName, - hasSubmitted: teamInfo.hasSubmitted, - uploadedFiles: teamInfo.teamUploadedFiles.map(loadFile), - }, - submissionStatus: { - hasSubmitted: submissionStatus.hasSubmitted, - hasCancelled: submissionStatus.hasCancelled, - hasReceivedGrade: submissionStatus.hasReceivedGrade, - }, - submission: { - textResponses: submission.textResponses, - uploadedFiles: submission.uploadedFiles.map(loadFile), - }, -}); From 14e93a26436945d84fb4ba3abc596008819c4b85 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 20 Nov 2023 20:16:36 +0000 Subject: [PATCH 09/66] chore: types for api data --- src/data/redux/app/reducer.js | 88 ----------------------------- src/data/redux/app/reducer.ts | 101 ++++++++++++++++++++++++++++++++++ src/data/redux/app/types.ts | 37 +++++++++++++ src/data/services/lms/api.ts | 63 +++++++-------------- tsconfig.json | 10 +++- 5 files changed, 165 insertions(+), 134 deletions(-) delete mode 100644 src/data/redux/app/reducer.js create mode 100644 src/data/redux/app/reducer.ts create mode 100644 src/data/redux/app/types.ts diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js deleted file mode 100644 index 3bbec275..00000000 --- a/src/data/redux/app/reducer.js +++ /dev/null @@ -1,88 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; - -import { StrictDict } from 'utils'; - -const initialState = { - assessment: { - submittedAssessment: null, - showTrainingError: false, - }, - response: null, - formFields: { - criteria: [], - overallFeedback: '', - }, - hasSubmitted: false, - showValidation: false, - - testDirty: false, - testProgressKey: null, - testDataPath: undefined, -}; - -// eslint-disable-next-line no-unused-vars -const app = createSlice({ - name: 'app', - initialState, - reducers: { - loadAssessment: (state, { payload }) => ({ - ...state, - assessment: { ...initialState.assessment, submittedAssessment: payload.data }, - }), - loadResponse: (state, { payload }) => ({ ...state, response: payload }), - setHasSubmitted: (state, { payload }) => ({ - ...state, - hasSubmitted: payload, - testDirty: payload, // test - }), - setShowValidation: (state, { payload }) => ({ ...state, showValidation: payload }), - setShowTrainingError: (state, { payload }) => ({ - ...state, - assessment: { ...state.assessment, showTrainingError: payload }, - }), - resetAssessment: (state) => ({ - ...state, - formFields: initialState.formFields, - assessment: initialState.assessment, - hasSubmitted: false, - showValidation: false, - }), - setFormFields: (state, { payload }) => ({ - ...state, - formFields: { ...state.formFields, ...payload }, - }), - setCriterionOption: (state, { payload }) => { - const { criterionIndex, option } = payload; - // eslint-disable-next-line - state.formFields.criteria[criterionIndex].selectedOption = option; - return state; - }, - setCriterionFeedback: (state, { payload }) => { - const { criterionIndex, feedback } = payload; - // eslint-disable-next-line - state.formFields.criteria[criterionIndex].feedback = feedback; - return state; - }, - setOverallFeedback: (state, { payload }) => { - // eslint-disable-next-line - state.formFields.overallFeedback = payload; - return state; - }, - setTestProgressKey: (state, { payload }) => ({ - ...state, - testProgressKey: payload, - testDirty: false, - }), - setTestDataPath: (state, { payload }) => ({ ...state, testDataPath: payload }), - }, -}); - -const actions = StrictDict(app.actions); - -const { reducer } = app; - -export { - actions, - initialState, - reducer, -}; diff --git a/src/data/redux/app/reducer.ts b/src/data/redux/app/reducer.ts new file mode 100644 index 00000000..78d5da29 --- /dev/null +++ b/src/data/redux/app/reducer.ts @@ -0,0 +1,101 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { StrictDict } from 'utils'; +import * as types from './types'; + +const initialState = { + assessment: { + submittedAssessment: null, + showTrainingError: false, + }, + response: null, + formFields: { + criteria: [], + overallFeedback: '', + }, + hasSubmitted: false, + showValidation: false, + + testDirty: false, + testProgressKey: null, + testDataPath: undefined, +}; + +// eslint-disable-next-line no-unused-vars +const app = createSlice({ + name: 'app', + initialState: initialState as types.AppState, + reducers: { + loadAssessment: (state: types.AppState, action: PayloadAction) => ({ + ...state, + assessment: { ...initialState.assessment, submittedAssessment: action.payload.data }, + }), + loadResponse: (state: types.AppState, action: PayloadAction) => ({ + ...state, + response: action.payload, + }), + setHasSubmitted: (state: types.AppState, action: PayloadAction) => ({ + ...state, + hasSubmitted: action.payload, + testDirty: action.payload, // test + }), + setShowValidation: (state: types.AppState, action: PayloadAction) => ({ + ...state, + showValidation: action.payload, + }), + setShowTrainingError: (state: types.AppState, action: PayloadAction) => ({ + ...state, + assessment: { ...state.assessment, showTrainingError: action.payload }, + }), + resetAssessment: (state: types.AppState) => ({ + ...state, + formFields: initialState.formFields, + assessment: initialState.assessment, + hasSubmitted: false, + showValidation: false, + }), + setFormFields: (state: types.AppState, action: PayloadAction) => ({ + ...state, + formFields: { ...state.formFields, ...action.payload }, + }), + setCriterionOption: (state: types.AppState, action: PayloadAction) => { + const { criterionIndex, option } = action.payload; + // eslint-disable-next-line + state.formFields.criteria[criterionIndex].selectedOption = option; + return state; + }, + setCriterionFeedback: ( + state: types.AppState, + action: PayloadAction, + ) => { + const { criterionIndex, feedback } = action.payload; + // eslint-disable-next-line + state.formFields.criteria[criterionIndex].feedback = feedback; + return state; + }, + setOverallFeedback: (state: types.AppState, action: PayloadAction) => { + // eslint-disable-next-line + state.formFields.overallFeedback = action.payload; + return state; + }, + setTestProgressKey: (state: types.AppState, action: PayloadAction) => ({ + ...state, + testProgressKey: action.payload, + testDirty: false, + }), + setTestDataPath: (state: types.AppState, action: PayloadAction) => ({ + ...state, + testDataPath: action.payload, + }), + }, +}); + +const actions = StrictDict(app.actions); + +const { reducer } = app; + +export { + actions, + initialState, + reducer, +}; diff --git a/src/data/redux/app/types.ts b/src/data/redux/app/types.ts new file mode 100644 index 00000000..1951a861 --- /dev/null +++ b/src/data/redux/app/types.ts @@ -0,0 +1,37 @@ +import * as apiTypes from 'data/services/lms/types'; + +export interface Assessment { + submittedAssessment: apiTypes.AssessmentData | null, + showTrainingError: boolean, +} + +export interface Criterion { + feedback?: string, + selectedOption?: number, +} + +export interface FormFields { + criteria: Criterion[], + overallFeedback: string, +} + +export interface AssessmentAction { data: { Assessment } } + +export type Response = string[] | null; + +export interface CriterionAction { + criterionIndex: number, + option?: number, + feedback?: string, +} + +export interface AppState { + assessment: Assessment, + response: Response, + formFields: FormFields, + hasSubmitted: boolean, + showValidation: boolean, + testDirty: boolean, + testProgressKey?: string | null, + testDataPath?: string | null, +} diff --git a/src/data/services/lms/api.ts b/src/data/services/lms/api.ts index 860e8dec..a284e87e 100644 --- a/src/data/services/lms/api.ts +++ b/src/data/services/lms/api.ts @@ -1,82 +1,59 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; // import { queryKeys } from './constants'; -import { AssessmentData } from './types'; +import * as types from './types'; import * as urls from './urls'; export const useSubmitAssessment = () => { const url = urls.useSubmitAssessmentUrl(); - return (data: AssessmentData) => { + const client = getAuthenticatedHttpClient(); + return (data: types.AssessmentData) => { console.log({ submitAssessment: data }); - return getAuthenticatedHttpClient().post(url, data); + return client.post(url, data); }; }; export const useSubmitResponse = () => { const url = urls.useSubmitUrl(); - return (data: any) => { + const client = getAuthenticatedHttpClient(); + return (data: types.ResponseData) => { console.log({ submitResponse: data }); - return getAuthenticatedHttpClient().post(url, { submission: data }); + return client.post(url, { submission: data }); }; }; export const useSaveDraft = () => { const url = urls.useSaveDraftUrl(); - return (data: any) => { - console.log({ save: data }); - return getAuthenticatedHttpClient().post(url, { response: data }); - }; + const client = getAuthenticatedHttpClient(); + return (data: string[]) => client.post(url, { response: data }); }; export const useAddFile = () => { const url = urls.useAddFileUrl(); + const client = getAuthenticatedHttpClient(); const responseUrl = urls.useUploadResponseUrl(); - return (data: any, description: string) => { - const { post } = getAuthenticatedHttpClient(); + return (data: { name: string, size: number, type: string }, description: string) => { const file = { fileDescription: description, fileName: data.name, fileSize: data.size, contentType: data.type, }; - console.log({ addFile: { data, description, file } }); - return post(url, file) - .then(response => post(responseUrl, { fileIndex: response.data.fileIndex, success: true })) + return client.post(url, file).then( + response => client.post( + responseUrl, + { fileIndex: response.data.fileIndex, success: true }, + ), + ); }; }; export const useDeleteFile = () => { const url = urls.useDeleteFileUrl(); - return (fileIndex) => { - return getAuthenticatedHttpClient().post(url, { fileIndex }); - }; -}; - -export const fakeProgress = async (requestConfig) => { - for (let i = 0; i <= 50; i++) { - // eslint-disable-next-line no-await-in-loop, no-promise-executor-return - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - requestConfig.onUploadProgress({ loaded: i, total: 50 }); - } -}; - -export const uploadFiles = (data: any) => { - const { fileData, requestConfig } = data; - console.log({ uploadFiles: { fileData, requestConfig } }); - // TODO: upload files - /* - * const files = fileData.getAll('file'); - * const addFileResponse = await post(`{xblock_id}/handler/file/add`, file); - * const uploadResponse = await(post(response.fileUrl, file)); - * post(`${xblock_id}/handler/download_url', (response)); - */ - return fakeProgress(data.requestConfig).then(() => { - Promise.resolve(); - }); + const client = getAuthenticatedHttpClient(); + return (fileIndex: number) => client.post(url, { fileIndex }); }; -export const deleteFile = (fileIndex) => { +export const deleteFile = (fileIndex: number) => { console.log({ deleteFile: fileIndex }); return new Promise((resolve) => { setTimeout(() => { diff --git a/tsconfig.json b/tsconfig.json index 173f94f6..4c65ac2f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,11 +3,15 @@ "compilerOptions": { "rootDir": "./src", "outDir": "dist", - "baseUrl": "./src", + "baseUrl": "src", "paths": { - "*": ["*"] + "data": ["data/*"], + "components": ["components/*"], + "constants": ["constants/*"], + "views": ["views/*"], + "utils": ["utils/*"] } }, - "include": ["src/**/*"], + "include": ["src"], "exclude": ["dist", "node_modules"] } From 60783f89af3d3c816a18387ce07bd971c40a04be Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 20 Nov 2023 20:16:42 +0000 Subject: [PATCH 10/66] chore: lint:fix --- .../ReadonlyAssessment/CollapsibleAssessment.jsx | 8 ++++---- .../FileRenderer/BaseRenderers/textHooks.js | 14 ++++++-------- src/components/FileUpload/FileDownload.jsx | 2 +- src/components/FileUpload/hooks.js | 9 ++++----- src/components/FileUpload/index.jsx | 2 +- .../ModalActions/hooks/useFinishedStateActions.js | 10 +++++----- src/components/ProgressBar/index.jsx | 4 ++-- .../services/lms/fakeData/pageData/assessments.js | 2 +- src/hooks/actions/index.js | 2 +- src/hooks/assessment.js | 6 +++--- src/hooks/modal.js | 6 +++--- src/hooks/test.js | 2 +- .../AssessmentView/StudentTrainingView/index.jsx | 4 ++-- src/views/AssessmentView/useAssessmentData.js | 2 +- src/views/SubmissionView/hooks/index.js | 1 + .../SubmissionView/hooks/useSubmissionViewData.js | 2 +- src/views/XBlockView/index.jsx | 2 +- 17 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.jsx b/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.jsx index 75e5b09a..4a48c915 100644 --- a/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.jsx +++ b/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.jsx @@ -19,11 +19,11 @@ const CollapsibleAssessment = ({ - {stepLabel + {stepLabel ? formatMessage( - stepScore ? messages.grade : messages.unweightedGrade, - { stepLabel }, - ) + stepScore ? messages.grade : messages.unweightedGrade, + { stepLabel }, + ) : formatMessage(messages.submittedAssessment)} {stepScore && formatMessage(messages.gradePoints, stepScore)} diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.js b/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.js index 5e9477e2..82a1395a 100644 --- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.js +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.js @@ -21,14 +21,12 @@ export const fetchFile = async ({ export const useTextRendererData = ({ url, onError, onSuccess }) => { const [content, setContent] = useKeyedState(stateKeys.content, ''); - useEffect(() => { - return fetchFile({ - setContent, - url, - onError, - onSuccess, - }); - }, [onError, onSuccess, setContent, url]); + useEffect(() => fetchFile({ + setContent, + url, + onError, + onSuccess, + }), [onError, onSuccess, setContent, url]); return { content }; }; diff --git a/src/components/FileUpload/FileDownload.jsx b/src/components/FileUpload/FileDownload.jsx index a719531c..fe568076 100644 --- a/src/components/FileUpload/FileDownload.jsx +++ b/src/components/FileUpload/FileDownload.jsx @@ -51,7 +51,7 @@ FileDownload.propTypes = { fileUrl: PropTypes.string.isRequired, fileName: PropTypes.string.isRequired, fileDescription: PropTypes.string, - }) + }), ).isRequired, }; diff --git a/src/components/FileUpload/hooks.js b/src/components/FileUpload/hooks.js index 9c06adb4..be2df109 100644 --- a/src/components/FileUpload/hooks.js +++ b/src/components/FileUpload/hooks.js @@ -89,11 +89,10 @@ export const useFileUploadHooks = ({ onFileUploaded }) => { export const useFileDownloadHooks = ({ files, zipFileName }) => { const downloadFileMation = useDownloadFiles(); - const downloadFiles = () => - downloadFileMation.mutate({ - files, - zipFileName, - }); + const downloadFiles = () => downloadFileMation.mutate({ + files, + zipFileName, + }); return { downloadFiles, status: downloadFileMation.status, diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx index 5113dda9..853a348b 100644 --- a/src/components/FileUpload/index.jsx +++ b/src/components/FileUpload/index.jsx @@ -47,7 +47,7 @@ const FileUpload = ({ return (

File Upload

- {isReadOnly && } + {isReadOnly && } {uploadedFiles.length > 0 && ( <> Uploaded Files diff --git a/src/components/ModalActions/hooks/useFinishedStateActions.js b/src/components/ModalActions/hooks/useFinishedStateActions.js index 56bff37c..e9bfe8b6 100644 --- a/src/components/ModalActions/hooks/useFinishedStateActions.js +++ b/src/components/ModalActions/hooks/useFinishedStateActions.js @@ -43,17 +43,17 @@ const useFinishedStateActions = () => { } // finished and moved to next step if ([stepNames.submission || stepNames.self].includes(step)) { - console.log("self or submission"); + console.log('self or submission'); return { primary: startStepAction, secondary: finishLaterAction }; } if (step !== activeStepName) { // next step is available - console.log("next step"); + console.log('next step'); if (stepState === stepStates.inProgress) { console.log({ startStepAction }); return { primary: startStepAction, secondary: finishLaterAction }; } - console.log("next step not available"); + console.log('next step not available'); // next step is not available return null; } @@ -61,14 +61,14 @@ const useFinishedStateActions = () => { console.log({ step, activeStepName }); // finished current assessment but not current step if (stepState === stepStates.inProgress) { - console.log("finished intermediate"); + console.log('finished intermediate'); return { primary: loadNextAction, secondary: finishLaterAction }; } // finished current assessment, but not step // and there are no more assessments available for the current step return { primary: exitAction }; } - console.log("?"); + console.log('?'); // submission finished state console.log({ startStepAction }); return { primary: startStepAction, secondary: finishLaterAction }; diff --git a/src/components/ProgressBar/index.jsx b/src/components/ProgressBar/index.jsx index eaed274b..6b6a6c00 100644 --- a/src/components/ProgressBar/index.jsx +++ b/src/components/ProgressBar/index.jsx @@ -50,7 +50,7 @@ export const ProgressBar = ({ className }) => { return null; } - const stepEl = (curStep) => stepLabels[curStep] + const stepEl = (curStep) => (stepLabels[curStep] ? ( { label={formatMessage(stepLabels[curStep])} canRevisit={(curStep === 'done' && hasReceivedFinalGrade) || stepCanRevisit[curStep]} /> - ) : null; + ) : null); return ( diff --git a/src/data/services/lms/fakeData/pageData/assessments.js b/src/data/services/lms/fakeData/pageData/assessments.js index c2e03e7b..0f1d30dc 100644 --- a/src/data/services/lms/fakeData/pageData/assessments.js +++ b/src/data/services/lms/fakeData/pageData/assessments.js @@ -13,7 +13,7 @@ const gradedState = createAssessmentState({ criteria: new Array(4).fill(0).map((_, i) => ({ feedback: `feedback ${i + 1}`, // random 0-3 - selectedOption: Math.floor(Math.random() * 4) + selectedOption: Math.floor(Math.random() * 4), })), overall_feedback: 'nice job', }); diff --git a/src/hooks/actions/index.js b/src/hooks/actions/index.js index 41421bfe..44404c50 100644 --- a/src/hooks/actions/index.js +++ b/src/hooks/actions/index.js @@ -14,4 +14,4 @@ export default { useExitAction, useLoadNextAction, useStartStepAction, -} +}; diff --git a/src/hooks/assessment.js b/src/hooks/assessment.js index bacac0ce..823d1927 100644 --- a/src/hooks/assessment.js +++ b/src/hooks/assessment.js @@ -101,14 +101,14 @@ export const useOnSubmit = () => { onSubmit: React.useCallback(() => { console.log({ onSubmit: { isInvalid, activeStepName, checkTrainingSelection } }); if (isInvalid) { - console.log("is invalid"); + console.log('is invalid'); return setShowValidation(true); } if (activeStepName === stepNames.studentTraining && !checkTrainingSelection) { - console.log("training validation"); + console.log('training validation'); return setShowTrainingError(true); } - console.log("is valid"); + console.log('is valid'); return submitAssessmentMutation.mutateAsync(formFields).then((data) => { setAssessment(data); setHasSubmitted(true); diff --git a/src/hooks/modal.js b/src/hooks/modal.js index 7fb35148..29c9ef05 100644 --- a/src/hooks/modal.js +++ b/src/hooks/modal.js @@ -6,12 +6,12 @@ export const useRefreshUpstream = () => { if (document.referrer !== '') { const postMessage = (data) => window.parent.postMessage(data, process.env.BASE_URL); return () => { - console.log("Send refresh upstream"); + console.log('Send refresh upstream'); postMessage({ type: 'ora-refresh' }); }; } return () => { - console.log("refresh upstream"); + console.log('refresh upstream'); }; }; @@ -24,7 +24,7 @@ export const useCloseModal = () => { }; } return () => { - console.log("CLose Modal"); + console.log('CLose Modal'); }; }; diff --git a/src/hooks/test.js b/src/hooks/test.js index 379eebe6..42ff4609 100644 --- a/src/hooks/test.js +++ b/src/hooks/test.js @@ -72,7 +72,7 @@ export const useUpdateTestProgressKey = () => { if (testDataPath && !testDirty) { console.log({ testDirty, testProgressKey }); queryClient.invalidateQueries({ queryKey: [queryKeys.pageData] }); - console.log("invalidated"); + console.log('invalidated'); } }, [testDirty]); }; diff --git a/src/views/AssessmentView/StudentTrainingView/index.jsx b/src/views/AssessmentView/StudentTrainingView/index.jsx index 09a4fce6..6a4314f8 100644 --- a/src/views/AssessmentView/StudentTrainingView/index.jsx +++ b/src/views/AssessmentView/StudentTrainingView/index.jsx @@ -16,7 +16,7 @@ import BaseAssessmentView from '../BaseAssessmentView'; export const StudentTrainingView = () => { const prompts = usePrompts(); const response = useResponse(); - console.log("StudentTrainingView"); + console.log('StudentTrainingView'); if (!useIsORAConfigLoaded()) { return null; } @@ -31,7 +31,7 @@ export const StudentTrainingView = () => { {responseIsEmpty && }
- )) + )), )} {responseIsEmpty && }
diff --git a/src/views/AssessmentView/useAssessmentData.js b/src/views/AssessmentView/useAssessmentData.js index 17cf1822..9886ecbe 100644 --- a/src/views/AssessmentView/useAssessmentData.js +++ b/src/views/AssessmentView/useAssessmentData.js @@ -24,7 +24,7 @@ const useAssessmentData = () => { const isPageDataLoaded = useIsPageDataLoaded(); console.log({ pageDataStatus: usePageDataStatus() }); React.useEffect(() => { - console.log("useAssessmentView useEffect"); + console.log('useAssessmentView useEffect'); if (!initialized && isLoaded && isPageDataLoaded) { setResponse(responseData); setInitialized(true); diff --git a/src/views/SubmissionView/hooks/index.js b/src/views/SubmissionView/hooks/index.js index 2b42cdd8..d7b9af06 100644 --- a/src/views/SubmissionView/hooks/index.js +++ b/src/views/SubmissionView/hooks/index.js @@ -1,2 +1,3 @@ import useSubmissionViewData from './useSubmissionViewData'; + export default useSubmissionViewData; diff --git a/src/views/SubmissionView/hooks/useSubmissionViewData.js b/src/views/SubmissionView/hooks/useSubmissionViewData.js index b1248a1e..4cfd78a4 100644 --- a/src/views/SubmissionView/hooks/useSubmissionViewData.js +++ b/src/views/SubmissionView/hooks/useSubmissionViewData.js @@ -56,7 +56,7 @@ const useSubmissionViewData = () => { textResponses, uploadedFiles, }).then(() => { - console.log("submitResponseMutation.then"); + console.log('submitResponseMutation.then'); setResponse({ textResponses, uploadedFiles }); setHasSubmitted(true); refreshPageData(); diff --git a/src/views/XBlockView/index.jsx b/src/views/XBlockView/index.jsx index 8bab2ae0..7e822b7e 100644 --- a/src/views/XBlockView/index.jsx +++ b/src/views/XBlockView/index.jsx @@ -24,7 +24,7 @@ export const XBlockView = () => { {prompts.map(prompt => )} - + ); From 95080a1d373d483b2e21c9812ce233f235a86ef2 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Tue, 21 Nov 2023 16:27:52 +0000 Subject: [PATCH 11/66] feat: less-chatty draft saving --- .../hooks/useSubmissionViewData.js | 12 ------- .../hooks/useTextResponsesData.js | 32 +++++++++++++++++-- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/views/SubmissionView/hooks/useSubmissionViewData.js b/src/views/SubmissionView/hooks/useSubmissionViewData.js index 4cfd78a4..58aa2a9c 100644 --- a/src/views/SubmissionView/hooks/useSubmissionViewData.js +++ b/src/views/SubmissionView/hooks/useSubmissionViewData.js @@ -64,18 +64,6 @@ const useSubmissionViewData = () => { }); }, [setHasSubmitted, submitResponseMutation, textResponses, uploadedFiles]); - useEffect(() => { - if (!hasSubmitted) { - const timer = setTimeout(() => { - saveResponse(); - if (!hasSavedDraft) { - setHasSavedDraft(true); - } - }, 5000); - return () => clearTimeout(timer); - } - }, [saveResponse, hasSubmitted]); - return { actionOptions: { finishLater, diff --git a/src/views/SubmissionView/hooks/useTextResponsesData.js b/src/views/SubmissionView/hooks/useTextResponsesData.js index 6b85046d..c49036b5 100644 --- a/src/views/SubmissionView/hooks/useTextResponsesData.js +++ b/src/views/SubmissionView/hooks/useTextResponsesData.js @@ -1,17 +1,26 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; -import { useFinishLater, useSaveDraftResponse, useTextResponses } from 'hooks/app'; +import { + useFinishLater, + useHasSubmitted, + useSaveDraftResponse, + useTextResponses, +} from 'hooks/app'; import { MutationStatus } from 'constants'; export const stateKeys = StrictDict({ textResponses: 'textResponses', isDirty: 'isDirty', + hasSaved: 'hasSaved', + lastChanged: 'lastChanged', }); const useTextResponsesData = () => { const textResponses = useTextResponses(); - + const hasSubmitted = useHasSubmitted(); + const [hasSaved, setHasSaved] = useKeyedState(stateKeys.hasSaved, false); + const [lastChanged, setLastChanged] = useKeyedState(stateKeys.lastChanged, null); const [isDirty, setIsDirty] = useKeyedState(stateKeys.isDirty, false); const [value, setValue] = useKeyedState(stateKeys.textResponses, textResponses); @@ -35,8 +44,25 @@ const useTextResponsesData = () => { return out; }); setIsDirty(true); + setLastChanged(Date.now()); }, [setValue, setIsDirty]); + useEffect(() => { + const interval = setInterval(() => { + if (!lastChanged) { + return; + } + const timeSinceChange = Date.now() - lastChanged; + if (isDirty && timeSinceChange > 2000) { + saveResponse(); + if (!hasSaved) { + setHasSaved(true); + } + } + }, 2000); + return () => clearInterval(interval); + }, [isDirty, hasSubmitted, hasSaved, lastChanged]); + return { textResponses: value, onUpdateTextResponse: onChange, From eb904cfcc737d31ca9027f66a0489828c7498c32 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Tue, 21 Nov 2023 16:31:46 +0000 Subject: [PATCH 12/66] feat: first pass files fix --- src/data/services/lms/api.ts | 36 ++++++++++++++++---- src/data/services/lms/hooks/actions/files.ts | 27 ++++++++++++++- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/data/services/lms/api.ts b/src/data/services/lms/api.ts index a284e87e..976ce338 100644 --- a/src/data/services/lms/api.ts +++ b/src/data/services/lms/api.ts @@ -1,3 +1,4 @@ +import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; // import { queryKeys } from './constants'; import * as types from './types'; @@ -31,6 +32,14 @@ export const useAddFile = () => { const url = urls.useAddFileUrl(); const client = getAuthenticatedHttpClient(); const responseUrl = urls.useUploadResponseUrl(); + const encode = (str) => encodeURIComponent(str) + // Note that although RFC3986 reserves "!", RFC5987 does not, + // so we do not need to escape it + .replace(/['()]/g, escape) // i.e., %27 %28 %29 + .replace(/\*/g, '%2A') + // The following are not required for percent-encoding per RFC5987, + // so we can allow for a little better readability over the wire: |`^ + .replace(/%(?:7C|60|5E)/g, unescape); return (data: { name: string, size: number, type: string }, description: string) => { const file = { fileDescription: description, @@ -38,13 +47,26 @@ export const useAddFile = () => { fileSize: data.size, contentType: data.type, }; - return client.post(url, file).then( - response => client.post( - responseUrl, - { fileIndex: response.data.fileIndex, success: true }, - ), - ); - }; + console.log({ upload: { data, description, file, url } }); + return client.post(url, file) + .then(response => { + console.log({ uploadResponse: response }); + return fetch( + `${getConfig().LMS_BASE_URL}${response.data.fileUrl}`, + { + method: 'PUT', + body: data, + headers: { 'Content-Disposition': `attachment; filename*=UTF-8''${encode(data.name)}` }, + }, + ).then(response2 => { + console.log({ uploadResponseResponse: response2 }); + return client.post( + responseUrl, + { fileIndex: response.data.fileIndex, success: true }, + ); + }); + }); + }; }; export const useDeleteFile = () => { diff --git a/src/data/services/lms/hooks/actions/files.ts b/src/data/services/lms/hooks/actions/files.ts index 307194eb..42b8326d 100644 --- a/src/data/services/lms/hooks/actions/files.ts +++ b/src/data/services/lms/hooks/actions/files.ts @@ -112,7 +112,32 @@ export const useUploadFiles = () => { const { fileData, requestConfig, description } = data; const file = fileData.getAll('file')[0]; console.log({ file }); - return addFile(file, description); + const encode = (str) => encodeURIComponent(str) + // Note that although RFC3986 reserves "!", RFC5987 does not, + // so we do not need to escape it + .replace(/['()]/g, escape) // i.e., %27 %28 %29 + .replace(/\*/g, '%2A') + // The following are not required for percent-encoding per RFC5987, + // so we can allow for a little better readability over the wire: |`^ + .replace(/%(?:7C|60|5E)/g, unescape); + return addFile(file, description).then(response => fetch( + response.fileUrl, + { + method: 'PUT', + body: file, + headers: { 'Content-Disposition': `attachment; filename*=UTF-8''${encode(file.name)}` }, + }, + )).then(() => { + // Log an analytics event + console.log( + 'openassessment.upload_file', + { + fileName: file.name, + fileSize: file.size, + fileType: file.type, + }, + ); + }); }; const mockFn = (data, description) => { const { fileData, requestConfig } = data; From bd5113043d92196914a3c80e8663b3e4623bd49e Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Tue, 21 Nov 2023 16:31:53 +0000 Subject: [PATCH 13/66] chore: lint fixes --- src/App.jsx | 4 +- src/components/AppContainer.jsx | 2 - .../ReadonlyAssessment/Feedback.jsx | 1 - .../Assessment/useAssessmentData.js | 5 -- .../FileRenderer/Banners/ErrorBanner.jsx | 2 +- .../FileRenderer/Banners/LoadingBanner.jsx | 2 +- .../BaseRenderers/PDFRenderer.jsx | 6 +- .../FileRenderer/BaseRenderers/pdfHooks.js | 2 +- .../FileRenderer/FileRenderer.test.jsx | 63 ------------------- .../FilePreview/components/utils.js | 6 -- src/components/FilePreview/index.jsx | 6 +- src/components/FileUpload/ActionCell.jsx | 2 +- src/components/ProgressBar/hooks.js | 1 - src/components/ProgressBar/index.jsx | 1 - .../CriterionContainer/RadioCriterion.jsx | 2 +- src/components/Rubric/index.jsx | 6 +- src/data/services/lms/fakeData/dataStates.js | 6 +- 17 files changed, 19 insertions(+), 98 deletions(-) delete mode 100644 src/components/FilePreview/components/FileRenderer/FileRenderer.test.jsx diff --git a/src/App.jsx b/src/App.jsx index 9a3d2f00..faead772 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,8 +14,8 @@ import GradeView from 'views/GradeView'; import AppContainer from 'components/AppContainer'; import ModalContainer from 'components/ModalContainer'; -import { useRefreshPageData } from 'hooks/app'; -import { useRefreshUpstream } from 'hooks/modal'; +// import { useRefreshPageData } from 'hooks/app'; +// import { useRefreshUpstream } from 'hooks/modal'; import { useUpdateTestProgressKey } from 'hooks/test'; import messages from './messages'; diff --git a/src/components/AppContainer.jsx b/src/components/AppContainer.jsx index 9d48e003..70990bf1 100644 --- a/src/components/AppContainer.jsx +++ b/src/components/AppContainer.jsx @@ -1,8 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ProgressBar from 'components/ProgressBar'; - import { useIsPageDataLoaded, useIsORAConfigLoaded } from 'hooks/app'; /* The purpose of this component is to wrap views with a header/footer for situations diff --git a/src/components/Assessment/ReadonlyAssessment/Feedback.jsx b/src/components/Assessment/ReadonlyAssessment/Feedback.jsx index 380c7969..b95bf4cf 100644 --- a/src/components/Assessment/ReadonlyAssessment/Feedback.jsx +++ b/src/components/Assessment/ReadonlyAssessment/Feedback.jsx @@ -83,7 +83,6 @@ Feedback.propTypes = { selectedPoints: PropTypes.number, commentHeader: PropTypes.string, commentBody: PropTypes.string.isRequired, - feedbackRequired: PropTypes.string.isRequired, }; export default Feedback; diff --git a/src/components/Assessment/useAssessmentData.js b/src/components/Assessment/useAssessmentData.js index 92029f35..1a5a7d76 100644 --- a/src/components/Assessment/useAssessmentData.js +++ b/src/components/Assessment/useAssessmentData.js @@ -4,10 +4,7 @@ import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; import { useHasSubmitted, useInitializeAssessment, - useSetHasSubmitted, } from 'hooks/assessment'; -import { useViewStep } from 'hooks/routing'; -import { stepNames } from 'constants'; export const stateKeys = StrictDict({ initialized: 'initialized', @@ -16,9 +13,7 @@ export const stateKeys = StrictDict({ const useAssessmentData = () => { const [initialized, setInitialized] = useKeyedState(stateKeys.initialized, false); const hasSubmitted = useHasSubmitted(); - const setHasSubmitted = useSetHasSubmitted(); const initialize = useInitializeAssessment(); - const viewStep = useViewStep(); React.useEffect(() => { initialize(); setInitialized(true); diff --git a/src/components/FilePreview/components/FileRenderer/Banners/ErrorBanner.jsx b/src/components/FilePreview/components/FileRenderer/Banners/ErrorBanner.jsx index 22473bef..a525ed7a 100644 --- a/src/components/FilePreview/components/FileRenderer/Banners/ErrorBanner.jsx +++ b/src/components/FilePreview/components/FileRenderer/Banners/ErrorBanner.jsx @@ -10,7 +10,7 @@ const messageShape = PropTypes.shape({ defaultMessage: PropTypes.string, }); -export const ErrorBanner = ({ actions, headerMessage, children }) => { +const ErrorBanner = ({ actions, headerMessage, children }) => { const { formatMessage } = useIntl(); const actionButtons = actions.map(action => ( , - , - ] - } - icon={[Function]} - variant="danger" -> - - Unknown errors - -

- Abitary Child -

- -`; diff --git a/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/LoadingBanner.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/LoadingBanner.test.jsx.snap deleted file mode 100644 index d6a2a516..00000000 --- a/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/LoadingBanner.test.jsx.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Loading Banner component snapshot 1`] = ` - - - -`; diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/ImageRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/BaseRenderers/ImageRenderer.test.jsx deleted file mode 100644 index 9b919fda..00000000 --- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/ImageRenderer.test.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - -import ImageRenderer from './ImageRenderer'; - -describe('Image Renderer Component', () => { - const props = { - url: 'some_url.jpg', - fileName: 'some_file_name.jpg', - onError: jest.fn().mockName('onError'), - onSuccess: jest.fn().mockName('onSuccess'), - }; - - let el; - beforeEach(() => { - el = shallow(); - }); - test('snapshot', () => { - expect(el.snapshot).toMatchSnapshot(); - }); -}); diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx deleted file mode 100644 index d4c996c3..00000000 --- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - -import PDFRenderer from './PDFRenderer'; - -import * as hooks from './pdfHooks'; - -jest.mock('react-pdf', () => ({ - pdfjs: { GlobalWorkerOptions: {} }, - Document: () => 'Document', - Page: () => 'Page', -})); - -jest.mock('./pdfHooks', () => ({ - rendererHooks: jest.fn(), -})); - -describe('PDF Renderer Component', () => { - const props = { - url: 'some_url.pdf', - onError: jest.fn().mockName('this.props.onError'), - onSuccess: jest.fn().mockName('this.props.onSuccess'), - }; - const hookProps = { - pageNumber: 1, - numPages: 10, - relativeHeight: 200, - wrapperRef: { current: 'hooks.wrapperRef' }, - onDocumentLoadSuccess: jest.fn().mockName('hooks.onDocumentLoadSuccess'), - onLoadPageSuccess: jest.fn().mockName('hooks.onLoadPageSuccess'), - onDocumentLoadError: jest.fn().mockName('hooks.onDocumentLoadError'), - onInputPageChange: jest.fn().mockName('hooks.onInputPageChange'), - onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'), - onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'), - hasNext: true, - hasPref: false, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('snapshots', () => { - test('first page, prev is disabled', () => { - hooks.rendererHooks.mockReturnValue(hookProps); - expect(shallow().snapshot).toMatchSnapshot(); - }); - test('on last page, next is disabled', () => { - hooks.rendererHooks.mockReturnValue({ - ...hookProps, - pageNumber: hookProps.numPages, - hasNext: false, - hasPrev: true, - }); - expect(shallow().snapshot).toMatchSnapshot(); - }); - }); -}); diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx deleted file mode 100644 index 43f82247..00000000 --- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - -import TXTRenderer from './TXTRenderer'; - -jest.mock('./textHooks', () => { - const content = 'test-content'; - return { - content, - rendererHooks: (args) => ({ content, rendererHooks: args }), - }; -}); - -describe('TXT Renderer Component', () => { - const props = { - url: 'some_url.txt', - onError: jest.fn().mockName('this.props.onError'), - onSuccess: jest.fn().mockName('this.props.onSuccess'), - }; - test('snapshot', () => { - expect(shallow().snapshot).toMatchSnapshot(); - }); -}); diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap deleted file mode 100644 index c9503872..00000000 --- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Image Renderer Component snapshot 1`] = ` -some_file_name.jpg -`; diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap deleted file mode 100644 index 5d7b3536..00000000 --- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap +++ /dev/null @@ -1,139 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = ` -
- -
- -
-
- - - - - Page - - - - of - 10 - - - - -
-`; - -exports[`PDF Renderer Component snapshots on last page, next is disabled 1`] = ` -
- -
- -
-
- - - - - Page - - - - of - 10 - - - - -
-`; diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap deleted file mode 100644 index 7675f907..00000000 --- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TXT Renderer Component snapshot 1`] = ` -
-  test-content
-
-`; diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js b/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js deleted file mode 100644 index 74272be0..00000000 --- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js +++ /dev/null @@ -1,151 +0,0 @@ -import React from 'react'; - -import { mockUseKeyedState } from '@edx/react-unit-test-utils'; - -import { - stateKeys, - initialState, -} from './pdfHooks'; - -jest.mock('react-pdf', () => ({ - pdfjs: { GlobalWorkerOptions: {} }, - Document: () => 'Document', - Page: () => 'Page', -})); - -jest.mock('./pdfHooks', () => ({ - ...jest.requireActual('./pdfHooks'), - safeSetPageNumber: jest.fn(), - rendererHooks: jest.fn(), -})); - -const state = mockUseKeyedState(stateKeys); - -const testValue = 'my-test-value'; - -describe('PDF Renderer hooks', () => { - const props = { - onError: jest.fn().mockName('this.props.onError'), - onSuccess: jest.fn().mockName('this.props.onSuccess'), - }; - - const actualHooks = jest.requireActual('./pdfHooks'); - - beforeEach(() => state.mock()); - afterEach(() => state.resetVals()); - - describe('state hooks', () => { - test('initialization', () => { - actualHooks.rendererHooks(props); - state.expectInitializedWith( - stateKeys.pageNumber, - initialState.pageNumber, - ); - state.expectInitializedWith(stateKeys.numPages, initialState.numPages); - state.expectInitializedWith( - stateKeys.relativeHeight, - initialState.relativeHeight, - ); - }); - }); - - test('safeSetPageNumber returns value handler that sets page number if valid', () => { - const { safeSetPageNumber } = actualHooks; - const rawSetPageNumber = jest.fn(); - const numPages = 10; - safeSetPageNumber({ numPages, rawSetPageNumber })(0); - expect(rawSetPageNumber).not.toHaveBeenCalled(); - safeSetPageNumber({ numPages, rawSetPageNumber })(numPages + 1); - expect(rawSetPageNumber).not.toHaveBeenCalled(); - safeSetPageNumber({ numPages, rawSetPageNumber })(numPages - 1); - expect(rawSetPageNumber).toHaveBeenCalledWith(numPages - 1); - }); - - describe('rendererHooks', () => { - const { rendererHooks } = actualHooks; - - test('wrapperRef passed as react ref', () => { - const hook = rendererHooks(props); - expect(hook.wrapperRef.useRef).toEqual(true); - }); - describe('onDocumentLoadSuccess', () => { - it('calls onSuccess and sets numPages based on args', () => { - const hook = rendererHooks(props); - hook.onDocumentLoadSuccess({ numPages: testValue }); - expect(props.onSuccess).toHaveBeenCalled(); - expect(state.setState.numPages).toHaveBeenCalledWith(testValue); - }); - }); - describe('onLoadPageSuccess', () => { - it('sets relative height based on page size', () => { - const width = 23; - React.useRef.mockReturnValueOnce({ - current: { - getBoundingClientRect: () => ({ width }), - }, - }); - const [pageWidth, pageHeight] = [20, 30]; - const page = { view: [0, 0, pageWidth, pageHeight] }; - const hook = rendererHooks(props); - const height = (width * pageHeight) / pageWidth; - hook.onLoadPageSuccess(page); - expect(state.setState.relativeHeight).toHaveBeenCalledWith(height); - }); - }); - test('onDocumentLoadError will call onError', () => { - const error = new Error('missingPDF'); - const hook = rendererHooks(props); - hook.onDocumentLoadError(error); - expect(props.onError).toHaveBeenCalledWith(error); - }); - - describe('pages hook', () => { - let oldNumPages; - let oldPageNumber; - let hook; - beforeEach(() => { - state.mock(); - // TODO: update state test instead of hacking initial state - oldNumPages = initialState.numPages; - oldPageNumber = initialState.pageNumber; - initialState.numPages = 10; - initialState.pageNumber = 5; - hook = rendererHooks(props); - }); - afterEach(() => { - initialState.numPages = oldNumPages; - initialState.pageNumber = oldPageNumber; - state.resetVals(); - }); - test('onInputPageChange will call setPageNumber with int event target value', () => { - hook.onInputPageChange({ target: { value: '3.3' } }); - expect(state.setState.pageNumber).toHaveBeenCalledWith(3); - }); - test('onPrevPageButtonClick will call setPageNumber with current page number - 1', () => { - hook.onPrevPageButtonClick(); - expect(state.setState.pageNumber).toHaveBeenCalledWith( - initialState.pageNumber - 1, - ); - }); - test('onNextPageButtonClick will call setPageNumber with current page number + 1', () => { - hook.onNextPageButtonClick(); - expect(state.setState.pageNumber).toHaveBeenCalledWith( - initialState.pageNumber + 1, - ); - }); - - test('hasNext returns true iff pageNumber is less than total number of pages', () => { - expect(hook.hasNext).toEqual(true); - initialState.pageNumber = initialState.numPages; - hook = rendererHooks(props); - expect(hook.hasNext).toEqual(false); - }); - test('hasPrev returns true iff pageNumber is greater than 1', () => { - expect(hook.hasPrev).toEqual(true); - initialState.pageNumber = 1; - hook = rendererHooks(props); - expect(hook.hasPrev).toEqual(false); - }); - }); - }); -}); diff --git a/src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx b/src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx deleted file mode 100644 index ea680a23..00000000 --- a/src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - -import { Collapsible } from '@edx/paragon'; - -import FileCard from '.'; - -describe('File Preview Card component', () => { - const props = { - file: { - name: 'test-file-name.pdf', - description: 'test-file description', - downloadUrl: 'destination/test-file-name.pdf', - }, - }; - const children = (

some children

); - let el; - beforeEach(() => { - el = shallow({children}); - }); - test('snapshot', () => { - expect(el.snapshot).toMatchSnapshot(); - }); - describe('Component', () => { - test('collapsible title is name header', () => { - const { title } = el.instance.findByType(Collapsible)[0].props; - expect(title).toEqual(

{props.file.name}

); - }); - // test('forwards children into preview-panel', () => { - // const previewPanelChildren = el.find('.preview-panel').children(); - // expect(previewPanelChildren.at(1).equals(children)).toEqual(true); - // }); - }); -}); diff --git a/src/components/FileUpload/ActionCell.test.jsx b/src/components/FileUpload/ActionCell.test.jsx deleted file mode 100644 index 6ffbd72e..00000000 --- a/src/components/FileUpload/ActionCell.test.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { shallow } from '@edx/react-unit-test-utils'; -import ActionCell from './ActionCell'; - -describe('', () => { - const props = { - onDeletedFile: jest.fn(), - disabled: false, - row: { - index: 0, - }, - }; - it('renders', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - }); -}); diff --git a/src/components/FileUpload/UploadConfirmModal.test.jsx b/src/components/FileUpload/UploadConfirmModal.test.jsx deleted file mode 100644 index 1fc43a64..00000000 --- a/src/components/FileUpload/UploadConfirmModal.test.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import { shallow } from '@edx/react-unit-test-utils'; -import UploadConfirmModal from './UploadConfirmModal'; - -import { useUploadConfirmModalHooks } from './hooks'; - -jest.mock('./hooks', () => ({ - useUploadConfirmModalHooks: jest.fn(), -})); - -describe('', () => { - const props = { - open: true, - file: { name: 'file1' }, - closeHandler: jest.fn().mockName('closeHandler'), - uploadHandler: jest.fn().mockName('uploadHandler'), - }; - - const mockHooks = (overrides) => { - useUploadConfirmModalHooks.mockReturnValueOnce({ - shouldShowError: false, - exitHandler: jest.fn().mockName('exitHandler'), - confirmUploadClickHandler: jest.fn().mockName('confirmUploadClickHandler'), - onFileDescriptionChange: jest.fn().mockName('onFileDescriptionChange'), - ...overrides, - }); - }; - describe('renders', () => { - test('without error', () => { - mockHooks( - { errors: new Array(2) }, - ); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - - expect(wrapper.instance.findByType('Form.Group').length).toBe(1); - expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0); - }); - - test('with errors', () => { - mockHooks({ shouldShowError: true }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - - expect(wrapper.instance.findByType('Form.Group').length).toBe(1); - expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1); - }); - }); -}); diff --git a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap deleted file mode 100644 index dd66fe44..00000000 --- a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders 1`] = ` - - - - -`; diff --git a/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap b/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap deleted file mode 100644 index dccdbae1..00000000 --- a/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap +++ /dev/null @@ -1,112 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders with errors 1`] = ` - - - - Add a text description to your file - - - -
- - - - Description for: - - - file1 - - - - - formatMessage(messages.fileDescriptionMissingError) - - -
-
- - - - Cancel upload - - - - -
-`; - -exports[` renders without error 1`] = ` - - - - Add a text description to your file - - - -
- - - - Description for: - - - file1 - - - - -
-
- - - - Cancel upload - - - - -
-`; diff --git a/src/components/FileUpload/__snapshots__/index.test.jsx.snap b/src/components/FileUpload/__snapshots__/index.test.jsx.snap deleted file mode 100644 index cecf480a..00000000 --- a/src/components/FileUpload/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,139 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render default 1`] = ` -
-

- File Upload -

- - - Uploaded Files - - - - - - - -
-`; - -exports[` render no uploaded files 1`] = ` -
-

- File Upload -

- - - - -
-`; - -exports[` render read only 1`] = ` -
-

- File Upload -

- - - Uploaded Files - - - -
-`; diff --git a/src/components/FileUpload/index.test.jsx b/src/components/FileUpload/index.test.jsx deleted file mode 100644 index 81076b3a..00000000 --- a/src/components/FileUpload/index.test.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import { shallow } from '@edx/react-unit-test-utils'; -import FileUpload from '.'; - -import { useFileUploadHooks } from './hooks'; - -jest.mock('./hooks', () => ({ - useFileUploadHooks: jest.fn(), -})); - -jest.mock('./UploadConfirmModal', () => 'UploadConfirmModal'); -jest.mock('./ActionCell', () => 'ActionCell'); - -describe('', () => { - const props = { - isReadOnly: false, - uploadedFiles: [ - { - fileName: 'file1', - fileDescription: 'file1 desc', - fileSize: 100, - }, - { - fileName: 'file2', - fileDescription: 'file2 desc', - fileSize: 200, - }, - ], - onFileUploaded: jest.fn().mockName('props.onFileUploaded'), - }; - - const mockHooks = (overrides) => { - useFileUploadHooks.mockReturnValueOnce({ - isModalOpen: false, - uploadArgs: {}, - confirmUpload: jest.fn().mockName('confirmUpload'), - closeUploadModal: jest.fn().mockName('closeUploadModal'), - onProcessUpload: jest.fn().mockName('onProcessUpload'), - ...overrides, - }); - }; - describe('behavior', () => { - it('initializes data from hook', () => { - mockHooks(); - shallow(); - expect(useFileUploadHooks).toHaveBeenCalledWith({ onFileUploaded: props.onFileUploaded }); - }); - }); - describe('render', () => { - // TODO: examine files in the table - // TODO: examine dropzone args - test('default', () => { - mockHooks(); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('Dropzone')).toHaveLength(1); - expect(wrapper.instance.findByType('DataTable')).toHaveLength(1); - }); - - test('read only', () => { - mockHooks({ isReadOnly: true }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - - expect(wrapper.instance.findByType('Dropzone')).toHaveLength(0); - }); - - test('no uploaded files', () => { - mockHooks(); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - - expect(wrapper.instance.findByType('DataTable')).toHaveLength(0); - }); - }); -}); diff --git a/src/data/services/lms/hooks/actions/files.ts b/src/data/services/lms/hooks/actions/files.ts index 97c5a476..c8c93083 100644 --- a/src/data/services/lms/hooks/actions/files.ts +++ b/src/data/services/lms/hooks/actions/files.ts @@ -5,7 +5,7 @@ import { useQueryClient, useMutation } from '@tanstack/react-query'; import { queryKeys } from 'constants'; import * as api from 'data/services/lms/api'; -import { useTestDataPath } from 'hooks/test'; +import { useTestDataPath } from 'hooks/testHooks'; import fakeData from '../../fakeData'; import { diff --git a/src/data/services/lms/hooks/actions/index.ts b/src/data/services/lms/hooks/actions/index.ts index 4ab01fe9..86fb1dd0 100644 --- a/src/data/services/lms/hooks/actions/index.ts +++ b/src/data/services/lms/hooks/actions/index.ts @@ -9,7 +9,7 @@ import { progressKeys } from 'constants/mockData'; import * as api from 'data/services/lms/api'; // import { AssessmentData } from 'data/services/lms/types'; import { loadState } from 'data/services/lms/fakeData/dataStates'; -import { useTestDataPath } from 'hooks/test'; +import { useTestDataPath } from 'hooks/testHooks'; import { useViewStep } from 'hooks/routing'; diff --git a/src/data/services/lms/hooks/data.test.ts b/src/data/services/lms/hooks/data.test.ts deleted file mode 100644 index c6bed049..00000000 --- a/src/data/services/lms/hooks/data.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useMatch } from 'react-router-dom'; -import { camelCaseObject } from '@edx/frontend-platform'; -import { when } from 'jest-when'; - -import routes from 'routes'; -import * as types from '../types'; -import { queryKeys } from '../constants'; -import fakeData from '../fakeData'; - -import { useORAConfig, usePageData } from './data'; - -jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn() })); - -jest.mock('react-router-dom', () => ({ useMatch: jest.fn() })); - -interface QueryFn { (): string } -interface QueryArgs { queryKey: string, queryFn: QueryFn } - -interface MockORAQuery extends QueryArgs { data: types.ORAConfig } -interface MockUseORAQuery { (QueryArgs): MockORAQuery } -interface MockORAUseConfigHook { (): MockORAQuery } - -interface MockPageDataQuery extends QueryArgs { data: types.PageData } -interface MockUsePageDataQuery { (QueryArgs): MockPageDataQuery } -interface MockPageDataUseConfigHook { (): MockPageDataQuery } - -let out; -describe.skip('lms data hooks', () => { - describe('useORAConfig', () => { - const mockUseQuery = (hasData: boolean): MockUseORAQuery => ({ queryKey, queryFn }) => ({ - data: hasData ? camelCaseObject(fakeData.oraConfig.assessmentText) : undefined, - queryKey, - queryFn, - }); - - const mockUseQueryForORA = (hasData) => { - when(useQuery) - .calledWith(expect.objectContaining({ queryKey: [queryKeys.oraConfig] })) - .mockImplementationOnce(mockUseQuery(hasData)); - }; - - const testUseORAConfig = useORAConfig as unknown as MockORAUseConfigHook; - - beforeEach(() => { - mockUseQueryForORA(true); - out = testUseORAConfig(); - }); - it('initializes query with oraConfig queryKey', () => { - expect(out.queryKey).toEqual([queryKeys.oraConfig]); - }); - it('initializes query with promise pointing to assessment text', async () => { - const old = window.location; - Object.defineProperty(window, 'location', { - value: new URL('http://dummy.com/text'), - writable: true, - }); - const response = await out.queryFn(); - expect(response).toEqual(fakeData.oraConfig.assessmentText); - window.location = old; - }); - it('initializes query with promise pointing to assessment tinyMCE', async () => { - const response = await out.queryFn(); - expect(response).toEqual(fakeData.oraConfig.assessmentTinyMCE); - }); - it('returns camelCase object from data if data has been returned', () => { - expect(out.data).toEqual(camelCaseObject(fakeData.oraConfig.assessmentText)); - }); - it('returns empty object from data if data has not been returned', () => { - mockUseQueryForORA(false); - out = testUseORAConfig(); - expect(out.data).toEqual({}); - }); - }); - describe('usePageData', () => { - const pageDataCamelCase = (data: any) => ({ - ...camelCaseObject(data), - rubric: { - optionsSelected: {...data.rubric.options_selected}, - criterionFeedback: {...data.rubric.criterion_feedback}, - overallFeedback: data.rubric.overall_feedback, - }, - }); - const mockUseQuery = (data?: types.PageData): MockUsePageDataQuery => ({ queryKey, queryFn }) => ({ - data: data ? pageDataCamelCase(data) : {}, - queryKey, - queryFn, - }); - - const mockUseQueryForPageData = (data, isAssessment) => { - when(useQuery) - .calledWith(expect.objectContaining({ queryKey: [queryKeys.pageData, isAssessment] })) - .mockImplementationOnce(mockUseQuery(data)); - }; - - const mockUseMatch = (path) => { - when(useMatch) - .calledWith(path) - .mockReturnValueOnce({ pattern: { path } }); - }; - - const testUsePageData = usePageData as unknown as MockPageDataUseConfigHook; - describe('submission', () => { - beforeEach(() => { - mockUseMatch(routes.submission); - mockUseQueryForPageData(fakeData.pageData.shapes.emptySubmission, false); - out = testUsePageData(); - }); - it('initializes query with pageData queryKey and isAssessment: false', () => { - expect(out.queryKey).toEqual([queryKeys.pageData, false]); - }); - it('initializes query with promise pointing to empty submission page data', async () => { - const response = await out.queryFn(); - expect(response).toEqual(pageDataCamelCase(fakeData.pageData.shapes.emptySubmission)); - }); - it('returns camelCase object from data if data has been returned', () => { - expect(out.data).toEqual(pageDataCamelCase(fakeData.pageData.shapes.emptySubmission)); - }); - }); - describe('assessment', () => { - beforeEach(() => { - mockUseMatch(routes.peerAssessment); - mockUseQueryForPageData(fakeData.pageData.shapes.peerAssessment, true); - out = testUsePageData(); - }); - it('initializes query with pageData queryKey and isAssessment: true', () => { - expect(out.queryKey).toEqual([queryKeys.pageData, true]); - }); - it('initializes query with promise pointing to peer assessment page data', async () => { - const response = await out.queryFn(); - expect(response).toEqual(pageDataCamelCase(fakeData.pageData.shapes.peerAssessment)); - }); - it('returns camelCase object from data if data has been returned', () => { - expect(out.data).toEqual(pageDataCamelCase(fakeData.pageData.shapes.peerAssessment)); - }); - }); - it('returns empty object from data if data has not been returned', () => { - mockUseMatch(routes.submission); - mockUseQueryForPageData(undefined, false); - out = testUsePageData(); - expect(out.data).toEqual({}); - }); - }); -}); diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts index 5ab08db3..1e46726c 100644 --- a/src/data/services/lms/hooks/data.ts +++ b/src/data/services/lms/hooks/data.ts @@ -9,7 +9,7 @@ import { useHasSubmitted } from 'data/redux/hooks'; // for test data import { useTestProgressKey, useTestDataPath, -} from 'hooks/test'; +} from 'hooks/testHooks'; import { routeSteps, diff --git a/src/hooks/test.js b/src/hooks/testHooks.js similarity index 100% rename from src/hooks/test.js rename to src/hooks/testHooks.js diff --git a/src/views/SubmissionView/TextResponseEditor/index.test.jsx b/src/views/SubmissionView/TextResponseEditor/index.test.jsx deleted file mode 100644 index 8f278295..00000000 --- a/src/views/SubmissionView/TextResponseEditor/index.test.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { shallow } from '@edx/react-unit-test-utils'; -import TextResponseEditor from '.'; - -jest.mock('./TextEditor', () => 'TextEditor'); -jest.mock('./RichTextEditor', () => 'RichTextEditor'); - -describe('', () => { - const props = { - submissionConfig: { - textResponseConfig: { - optional: false, - enabled: true, - editorType: 'text', - }, - }, - value: 'value', - onChange: jest.fn().mockName('onChange'), - }; - - it('render Text Editor ', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - - expect(wrapper.instance.findByType('TextEditor').length).toEqual(1); - expect(wrapper.instance.findByType('RichTextEditor').length).toEqual(0); - }); - - it('render Rich Text Editor ', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - - expect(wrapper.instance.findByType('TextEditor').length).toEqual(0); - expect(wrapper.instance.findByType('RichTextEditor').length).toEqual(1); - }); -}); diff --git a/src/views/SubmissionView/index.test.jsx b/src/views/SubmissionView/index.test.jsx deleted file mode 100644 index 8311e215..00000000 --- a/src/views/SubmissionView/index.test.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import { shallow } from '@edx/react-unit-test-utils'; -import { SubmissionView } from '.'; - -jest.mock('components/Rubric', () => 'Rubric'); -jest.mock('components/ProgressBar', () => 'ProgressBar'); -jest.mock('./SubmissionContent', () => 'SubmissionContent'); -jest.mock('./SubmissionActions', () => 'SubmissionActions'); - -jest.mock('./hooks', () => jest.fn().mockReturnValue({ - submission: 'submission', - oraConfigData: 'oraConfigData', - onFileUploaded: jest.fn().mockName('onFileUploaded'), - onTextResponseChange: jest.fn().mockName('onTextResponseChange'), - submitResponseHandler: jest.fn().mockName('submitResponseHandler'), - submitResponseStatus: 'submitResponseStatus', - saveResponseHandler: jest.fn().mockName('saveResponseHandler'), - saveResponseStatus: 'saveResponseStatus', - draftSaved: true, -})); -jest.mock('data/services/lms/hooks/selectors', () => ({ - useIsPageDataLoaded: jest.fn(() => true), -})); - -describe('', () => { - it('renders', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - }); -}); From 9a6f6c27b5d515dee0d178859431a7c31c2222e3 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 03:53:20 +0000 Subject: [PATCH 21/66] chore: lms api tests --- src/data/services/lms/api.test.ts | 190 ++++++++++++++++++++++++++++++ src/data/services/lms/api.ts | 77 ++++++------ 2 files changed, 223 insertions(+), 44 deletions(-) create mode 100644 src/data/services/lms/api.test.ts diff --git a/src/data/services/lms/api.test.ts b/src/data/services/lms/api.test.ts new file mode 100644 index 00000000..0fd77928 --- /dev/null +++ b/src/data/services/lms/api.test.ts @@ -0,0 +1,190 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { when } from 'jest-when'; + +import * as api from './api'; +import * as urls from './urls'; + +jest.mock('@edx/frontend-platform/auth', () => ({ + getAuthenticatedHttpClient: jest.fn(), +})); +jest.mock('./urls', () => ({ + useSubmitAssessmentUrl: jest.fn(), + useSubmitUrl: jest.fn(), + useSaveDraftUrl: jest.fn(), + useAddFileUrl: jest.fn(), + useUploadResponseUrl: jest.fn(), + useDeleteFileUrl: jest.fn(), +})); + +const testUrls = { + submitAssessment: 'test-submit-assessment-url', + submit: 'test-submit-url', + saveDraft: 'test-save-draft-url', + addFile: 'test-add-file-url', + uploadResponse: 'test-upload-response-url', + deleteFile: 'test-delete-file-url', +}; +when(urls.useSubmitAssessmentUrl).calledWith().mockReturnValue(testUrls.submitAssessment); +when(urls.useSubmitUrl).calledWith().mockReturnValue(testUrls.submit); +when(urls.useSaveDraftUrl).calledWith().mockReturnValue(testUrls.saveDraft); +when(urls.useAddFileUrl).calledWith().mockReturnValue(testUrls.addFile); +when(urls.useUploadResponseUrl).calledWith().mockReturnValue(testUrls.uploadResponse); +when(urls.useDeleteFileUrl).calledWith().mockReturnValue(testUrls.deleteFile); + +const authClient = { post: jest.fn((...args) => ({ post: args })) }; +when(getAuthenticatedHttpClient).calledWith().mockReturnValue(authClient); + +const testData = 'test-data'; +global.fetch = jest.fn(); + +let hook; +describe('lms api', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const testLoadsClient = () => { + it('loads authenticated http client', () => { + expect(getAuthenticatedHttpClient).toHaveBeenCalledWith(); + }); + }; + describe('useSubmitAssessment', () => { + beforeEach(() => { + hook = api.useSubmitAssessment(); + }); + describe('behavior', () => { + it('loads url from hook', () => { + expect(urls.useSubmitAssessmentUrl).toHaveBeenCalledWith(); + }); + testLoadsClient(); + }); + describe('output', () => { + it('returns a method that posts data to submitAssessment url', () => { + expect(hook(testData)).toEqual( + authClient.post(testUrls.submitAssessment, testData), + ); + }); + }); + }); + describe('useSubmitResponse', () => { + beforeEach(() => { + hook = api.useSubmitResponse(); + }); + describe('behavior', () => { + it('loads url from hook', () => { + expect(urls.useSubmitUrl).toHaveBeenCalledWith(); + }); + testLoadsClient(); + }); + describe('output', () => { + it('returns a method that posts data as submission to submit url', () => { + expect(hook(testData)).toEqual( + authClient.post(testUrls.submit, { submission: testData }), + ); + }); + }); + }); + describe('useSaveDraft', () => { + beforeEach(() => { + hook = api.useSaveDraft(); + }); + describe('behavior', () => { + it('loads url from hook', () => { + expect(urls.useSaveDraftUrl).toHaveBeenCalledWith(); + }); + testLoadsClient(); + }); + describe('output', () => { + it('returns a method that posts data as submission to submit url', () => { + expect(hook(testData)).toEqual( + authClient.post(testUrls.saveDraft, { response: testData }), + ); + }); + }); + }); + describe('encode', () => { + it('escapes invalid characters', () => { + const testString = '()*^`' + "'"; // eslint-disable-line no-useless-concat + expect(api.encode(testString)).toEqual('%28%29%2A^`%27'); + }); + }); + describe('fileHeader', () => { + it('returns object with content disposition', () => { + const encodedName = 'encoded-name'; + const encode = jest.spyOn(api, 'encode'); + when(encode).calledWith(testData).mockReturnValue(encodedName); + const keys = api.uploadKeys; + expect(api.fileHeader(testData)).toEqual({ + [keys.contentDisposition]: `${keys.attachmentPrefix}${encodedName}`, + }); + }); + }); + describe('uploadFile', () => { + it('PUTS data to fileUrl with fileHeaders for file name', () => { + }); + }); + describe('useAddFile', () => { + const fileData = { + name: 'test-name', + size: 'test-size', + type: 'test-type', + }; + const description = 'test-description'; + const file = { + fileDescription: description, + fileName: fileData.name, + fileSize: fileData.size, + contentType: fileData.type, + }; + const fileIndex = 23; + const fileUrl = 'test-file-url'; + const addFileResponse = { data: { fileIndex, fileUrl } }; + const uploadFile = jest.spyOn(api, 'uploadFile'); + beforeEach(() => { + when(authClient.post) + .calledWith(testUrls.addFile, expect.anything()) + .mockResolvedValue(addFileResponse); + when(authClient.post) + .calledWith(fileUrl, expect.anything()) + .mockResolvedValue(); + when(uploadFile) + .calledWith(fileData, fileUrl) + .mockResolvedValue(); + hook = api.useAddFile(); + }); + describe('behavior', () => { + it('loads url from hook', () => { + expect(urls.useAddFileUrl).toHaveBeenCalledWith(); + expect(urls.useUploadResponseUrl).toHaveBeenCalledWith(); + }); + testLoadsClient(); + }); + describe('output', () => { + it('returns callback that takes file and description, adds and uploads file', async () => { + await expect(hook(fileData, description)).resolves.toStrictEqual({ + ...file, + fileIndex, + fileUrl, + }); + }); + }); + }); + describe('useDeleteFile', () => { + beforeEach(() => { + hook = api.useDeleteFile(); + }); + describe('behavior', () => { + it('loads url from hook', () => { + expect(urls.useDeleteFileUrl).toHaveBeenCalledWith(); + }); + testLoadsClient(); + }); + describe('output', () => { + it('returns a method that posts data as submission to submit url', () => { + const fileIndex = 3; + expect(hook(fileIndex)).toEqual( + authClient.post(testUrls.deleteFile, { fileIndex }), + ); + }); + }); + }); +}); diff --git a/src/data/services/lms/api.ts b/src/data/services/lms/api.ts index d637d749..25f82d46 100644 --- a/src/data/services/lms/api.ts +++ b/src/data/services/lms/api.ts @@ -1,6 +1,4 @@ -import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -// import { queryKeys } from './constants'; import * as types from './types'; import * as urls from './urls'; @@ -28,48 +26,49 @@ export const useSaveDraft = () => { return (data: string[]) => client.post(url, { response: data }); }; +export const encode = (str) => encodeURIComponent(str) + // Note that although RFC3986 reserves "!", RFC5987 does not, + // so we do not need to escape it + .replace(/['()]/g, escape) // i.e., %27 %28 %29 + .replace(/\*/g, '%2A') + // The following are not required for percent-encoding per RFC5987, + // so we can allow for a little better readability over the wire: |`^ + .replace(/%(?:7C|60|5E)/g, unescape); + +export const uploadKeys = { + contentDisposition: 'Content-Disposition', + attachmentPrefix: 'attachment; filename*=UTF-8\'\'', +}; +export const fileHeader = (name) => ({ + [uploadKeys.contentDisposition]: `${uploadKeys.attachmentPrefix}${encode(name)}`, +}); +export const uploadFile = (data, fileUrl) => fetch( + fileUrl, + { + method: 'PUT', + body: data, + headers: fileHeader(data.name), + }, +); + export const useAddFile = () => { const url = urls.useAddFileUrl(); const client = getAuthenticatedHttpClient(); const responseUrl = urls.useUploadResponseUrl(); - const encode = (str) => encodeURIComponent(str) - // Note that although RFC3986 reserves "!", RFC5987 does not, - // so we do not need to escape it - .replace(/['()]/g, escape) // i.e., %27 %28 %29 - .replace(/\*/g, '%2A') - // The following are not required for percent-encoding per RFC5987, - // so we can allow for a little better readability over the wire: |`^ - .replace(/%(?:7C|60|5E)/g, unescape); - return (data: { name: string, size: number, type: string }, description: string) => { + return (data: Blob, description: string) => { const file = { fileDescription: description, fileName: data.name, fileSize: data.size, contentType: data.type, }; - console.log({ upload: { data, description, file, url } }); - return client.post(url, file) - .then(response => { - console.log({ uploadResponse: response }); - return fetch( - response.data.fileUrl, - { - method: 'PUT', - body: data, - headers: { 'Content-Disposition': `attachment; filename*=UTF-8''${encode(data.name)}` }, - }, - ).then(response2 => { - console.log({ uploadResponseResponse: response2 }); - return client.post( - responseUrl, - { fileIndex: response.data.fileIndex, success: true }, - ); - }).then(() => (({ - ...file, - fileIndex: response.data.fileIndex, - fileUrl: response.data.fileUrl, - }))); - }); + + return client.post(url, file).then(response => { + const { fileIndex, fileUrl } = response.data; + return uploadFile(data, fileUrl) + .then(() => client.post(responseUrl, { fileIndex, success: true })) + .then(() => ({ ...file, fileIndex, fileUrl })); + }); }; }; @@ -78,13 +77,3 @@ export const useDeleteFile = () => { const client = getAuthenticatedHttpClient(); return (fileIndex: number) => client.post(url, { fileIndex }); }; - -export const deleteFile = (fileIndex: number) => { - console.log({ deleteFile: fileIndex }); - return new Promise((resolve) => { - setTimeout(() => { - console.log('deleted file'); - resolve(null); - }, 1000); - }); -}; From f6ac4ba0d190585ff8b81f631b0f071ef2237107 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 04:09:51 +0000 Subject: [PATCH 22/66] chore: redux tests pt 1 --- src/data/redux/app/reducer.test.ts | 69 ++++++++++++++++++++++++++++++ src/data/redux/app/reducer.ts | 3 +- src/data/redux/app/types.ts | 2 +- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/data/redux/app/reducer.test.ts diff --git a/src/data/redux/app/reducer.test.ts b/src/data/redux/app/reducer.test.ts new file mode 100644 index 00000000..3e318009 --- /dev/null +++ b/src/data/redux/app/reducer.test.ts @@ -0,0 +1,69 @@ +import { initialState, actions, reducer } from './reducer'; + +const testValue = 'test-value'; +describe('app reducer', () => { + it('returns initial state', () => { + expect(reducer(undefined, { type: undefined })).toEqual(initialState); + }); + describe('actions', () => { + const testAction = (action, expected) => { + expect(reducer(initialState, action)).toEqual({ ...initialState, ...expected }); + }; + describe('loadAssessment', () => { + it('overrides assessment.submittedAssessment with action data', () => { + testAction( + actions.loadAssessment({ data: testValue }), + { assessment: { ...initialState.assessment, submittedAssessment: testValue } }, + ); + }); + }); + describe('loadResponse', () => { + it('overrides response', () => { + testAction(actions.loadResponse(testValue), { response: testValue }); + }); + }); + describe('setHasSubmitted', () => { + it('overrides hasSubmitted and testDirty', () => { + testAction( + actions.setHasSubmitted(testValue), + { hasSubmitted: testValue, testDirty: testValue }, + ); + }); + }); + describe('setShowValidation', () => { + it('overrides showValidation', () => { + testAction(actions.setShowValidation(testValue), { showValidation: testValue }); + }); + }); + describe('setShowTrainingError', () => { + it('overrides assessment.showTrainingError', () => { + testAction( + actions.setShowTrainingError(testValue), + { assessment: { ...initialState.assessment, showTrainingError: testValue } }, + ); + }); + }); + describe('resetAssessment', () => { + }); + describe('setFormFields', () => { + }); + describe('setCriterionOption', () => { + }); + describe('setCriterionFeedback', () => { + }); + describe('resetAssessment', () => { + }); + describe('setFormFields', () => { + }); + describe('setCriterionOption', () => { + }); + describe('setCriterionFeedback', () => { + }); + describe('setOverallFeedback', () => { + }); + describe('setTestProgressKey', () => { + }); + describe('setTestDataPath', () => { + }); + }); +}); diff --git a/src/data/redux/app/reducer.ts b/src/data/redux/app/reducer.ts index 78d5da29..d8908d1a 100644 --- a/src/data/redux/app/reducer.ts +++ b/src/data/redux/app/reducer.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { StrictDict } from 'utils'; +import { StrictDict } from '@edx/react-unit-test-utils'; + import * as types from './types'; const initialState = { diff --git a/src/data/redux/app/types.ts b/src/data/redux/app/types.ts index 1951a861..b519a5a5 100644 --- a/src/data/redux/app/types.ts +++ b/src/data/redux/app/types.ts @@ -15,7 +15,7 @@ export interface FormFields { overallFeedback: string, } -export interface AssessmentAction { data: { Assessment } } +export interface AssessmentAction { data: apiTypes.AssessmentData } export type Response = string[] | null; From 92cfc51001604e61c781965cf39ad82b9b1db890 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 04:46:02 +0000 Subject: [PATCH 23/66] chore: redux app reducer tests --- src/data/redux/app/reducer.test.ts | 104 ++++++++++++++++++++++++++--- src/data/redux/app/reducer.ts | 4 +- 2 files changed, 96 insertions(+), 12 deletions(-) diff --git a/src/data/redux/app/reducer.test.ts b/src/data/redux/app/reducer.test.ts index 3e318009..a6afc0ea 100644 --- a/src/data/redux/app/reducer.test.ts +++ b/src/data/redux/app/reducer.test.ts @@ -1,5 +1,31 @@ import { initialState, actions, reducer } from './reducer'; +const testState = { + assessment: { + submittedAssessment: { + criteria: [ + { selectedOption: 1, feedback: 'test-criterion-feedback1' }, + { selectedOption: 2, feedback: 'test-criterion-feedback2' }, + ], + overallFeedback: 'test-overall-feedback', + }, + showTrainingError: true, + }, + response: ['test-response'], + formFields: { + criteria: [ + { selectedOption: 1, feedback: 'test-formFields-criterion-feedback1' }, + { selectedOption: 3, feedback: 'test-formFields-criterion-feedback2' }, + ], + overallFeedback: 'formFields-overall-feedback', + }, + hasSubmitted: true, + showValidation: true, + testDirty: true, + testProgressKey: 'test-progress-key', + testDataPath: 'test-data-path', +}; + const testValue = 'test-value'; describe('app reducer', () => { it('returns initial state', () => { @@ -7,7 +33,7 @@ describe('app reducer', () => { }); describe('actions', () => { const testAction = (action, expected) => { - expect(reducer(initialState, action)).toEqual({ ...initialState, ...expected }); + expect(reducer(testState, action)).toEqual({ ...testState, ...expected }); }; describe('loadAssessment', () => { it('overrides assessment.submittedAssessment with action data', () => { @@ -39,31 +65,89 @@ describe('app reducer', () => { it('overrides assessment.showTrainingError', () => { testAction( actions.setShowTrainingError(testValue), - { assessment: { ...initialState.assessment, showTrainingError: testValue } }, + { assessment: { ...testState.assessment, showTrainingError: testValue } }, ); }); }); describe('resetAssessment', () => { + it('resets formFields, assessment, showValidation, and hasSubmitted', () => { + testAction( + actions.resetAssessment(), + { + assessment: initialState.assessment, + formFields: initialState.formFields, + hasSubmitted: initialState.hasSubmitted, + showValidation: initialState.showValidation, + }, + ); + }); }); describe('setFormFields', () => { + it('partially overrides formFields', () => { + testAction( + actions.setFormFields({ overallFeedback: testValue }), + { formFields: { ...testState.formFields, overallFeedback: testValue } }, + ); + const testCriteria = [{ selectedOption: 1, feedback: 'test-feedback' }]; + testAction( + actions.setFormFields({ criteria: testCriteria }), + { formFields: { ...testState.formFields, criteria: testCriteria } }, + ); + }); }); describe('setCriterionOption', () => { + it('overrides the selectedOption for the criterion with the given index', () => { + const criterionIndex = 1; + const option = 23; + const expectedCriteria = [...testState.formFields.criteria]; + expectedCriteria[criterionIndex] = { + ...expectedCriteria[criterionIndex], + selectedOption: option, + }; + testAction( + actions.setCriterionOption({ criterionIndex, option }), + { formFields: { ...testState.formFields, criteria: expectedCriteria } }, + ); + }); }); describe('setCriterionFeedback', () => { - }); - describe('resetAssessment', () => { - }); - describe('setFormFields', () => { - }); - describe('setCriterionOption', () => { - }); - describe('setCriterionFeedback', () => { + it('overrides the feedback for the criterion with the given index', () => { + const criterionIndex = 1; + const feedback = 'expected-feedback'; + const expectedCriteria = [...testState.formFields.criteria]; + expectedCriteria[criterionIndex] = { + ...expectedCriteria[criterionIndex], + feedback, + }; + testAction( + actions.setCriterionFeedback({ criterionIndex, feedback }), + { formFields: { ...testState.formFields, criteria: expectedCriteria } }, + ); + }); }); describe('setOverallFeedback', () => { + it('overrides formFields.overallFeedback', () => { + testAction( + actions.setOverallFeedback(testValue), + { formFields: { ...testState.formFields, overallFeedback: testValue } }, + ); + }); }); describe('setTestProgressKey', () => { + it('overrides formFields.overallFeedback', () => { + testAction( + actions.setTestProgressKey(testValue), + { testProgressKey: testValue, testDirty: false }, + ); + }); }); describe('setTestDataPath', () => { + it('overrides testDataPath', () => { + testAction( + actions.setTestDataPath(testValue), + { testDataPath: testValue }, + ); + }); }); }); }); diff --git a/src/data/redux/app/reducer.ts b/src/data/redux/app/reducer.ts index d8908d1a..51c12efb 100644 --- a/src/data/redux/app/reducer.ts +++ b/src/data/redux/app/reducer.ts @@ -52,8 +52,8 @@ const app = createSlice({ ...state, formFields: initialState.formFields, assessment: initialState.assessment, - hasSubmitted: false, - showValidation: false, + hasSubmitted: initialState.hasSubmitted, + showValidation: initialState.showValidation, }), setFormFields: (state: types.AppState, action: PayloadAction) => ({ ...state, From ba9dd9cee16d382e08668d6fa7aaac0fe671f4e0 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 05:16:44 +0000 Subject: [PATCH 24/66] chore: redux app selector types --- src/data/redux/app/selectors/index.js | 38 -------------- src/data/redux/app/selectors/index.ts | 71 +++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 38 deletions(-) delete mode 100644 src/data/redux/app/selectors/index.js create mode 100644 src/data/redux/app/selectors/index.ts diff --git a/src/data/redux/app/selectors/index.js b/src/data/redux/app/selectors/index.js deleted file mode 100644 index 69c4493c..00000000 --- a/src/data/redux/app/selectors/index.js +++ /dev/null @@ -1,38 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; - -const rootSelector = ({ app }) => app; -const assessmentData = createSelector(rootSelector, ({ assessment }) => assessment); -const formFieldsData = createSelector(rootSelector, ({ formFields }) => formFields); -const selectors = { - criterionFeedback: (state, criterionIndex) => ( - formFieldsData(state).criteria[criterionIndex]?.feedback - ), - - criterionOption: (state, criterionIndex) => ( - formFieldsData(state).criteria[criterionIndex]?.selectedOption - ), - - response: createSelector(rootSelector, ({ response }) => response), - - hasSubmitted: createSelector(rootSelector, ({ hasSubmitted }) => hasSubmitted), - - overallFeedback: (state) => formFieldsData(state).overallFeedback, - - showTrainingError: createSelector(assessmentData, (assessment) => assessment?.showTrainingError), - - showValidation: createSelector(rootSelector, ({ showValidation }) => showValidation), - - submittedAssessment: - createSelector(assessmentData, ({ submittedAssessment }) => submittedAssessment), - - // test - testProgressKey: createSelector(rootSelector, ({ testProgressKey }) => testProgressKey), - testDirty: createSelector(rootSelector, ({ testDirty }) => testDirty), - testDataPath: createSelector(rootSelector, ({ testDataPath }) => testDataPath), -}; - -export default { - assessment: assessmentData, - formFields: formFieldsData, - ...selectors, -}; diff --git a/src/data/redux/app/selectors/index.ts b/src/data/redux/app/selectors/index.ts new file mode 100644 index 00000000..1d4d753a --- /dev/null +++ b/src/data/redux/app/selectors/index.ts @@ -0,0 +1,71 @@ +import { createSelector } from '@reduxjs/toolkit'; +import * as types from '../types'; + +type RootState = { app: types.AppState }; + +const rootSelector = (state: RootState): types.AppState => state.app; + +const assessmentData = createSelector( + rootSelector, + (app: types.AppState): types.Assessment => app.assessment, +); +const formFieldsData = createSelector( + rootSelector, + (app: types.AppState): types.FormFields => app.formFields, +); +const selectors = { + criterionFeedback: (state: RootState, criterionIndex: number): string | undefined => ( + formFieldsData(state).criteria[criterionIndex]?.feedback + ), + + criterionOption: (state: RootState, criterionIndex: number): number | undefined => ( + formFieldsData(state).criteria[criterionIndex]?.selectedOption + ), + + response: createSelector( + rootSelector, + (app: types.AppState): types.Response => app.response, + ), + + hasSubmitted: createSelector( + rootSelector, + (app: types.AppState) => app.hasSubmitted, + ), + + overallFeedback: (state: RootState): string => formFieldsData(state).overallFeedback, + + showTrainingError: createSelector( + assessmentData, + (assessment: types.Assessment): boolean => assessment?.showTrainingError, + ), + + showValidation: createSelector( + rootSelector, + (app: types.AppState) => app.showValidation, + ), + + submittedAssessment: createSelector( + assessmentData, + (assessment: types.Assessment) => assessment.submittedAssessment, + ), + + // test + testProgressKey: createSelector( + rootSelector, + ({ testProgressKey }) => testProgressKey, + ), + testDirty: createSelector( + rootSelector, + (app: types.AppState): boolean => app.testDirty, + ), + testDataPath: createSelector( + rootSelector, + (app: types.AppState): string | null | undefined => app.testDataPath, + ), +}; + +export default { + assessment: assessmentData, + formFields: formFieldsData, + ...selectors, +}; From 08823e63d0100f2445c13a5e19ef4a164d8d3dfa Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 15:08:44 +0000 Subject: [PATCH 25/66] chore: update snapshots --- .../__snapshots__/index.test.jsx.snap | 94 ---------------- .../__snapshots__/FileCard.test.jsx.snap | 28 ----- .../__snapshots__/FileRenderer.test.jsx.snap | 34 ------ .../__snapshots__/index.test.jsx.snap | 26 ----- .../SubmissionActions.test.jsx.snap | 39 ------- .../SubmissionContent.test.jsx.snap | 101 ------------------ .../SubmissionContentLayout.test.jsx.snap | 66 ------------ .../__snapshots__/index.test.jsx.snap | 25 ----- 8 files changed, 413 deletions(-) delete mode 100644 src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap delete mode 100644 src/components/FilePreview/components/__snapshots__/FileCard.test.jsx.snap delete mode 100644 src/components/FilePreview/components/__snapshots__/FileRenderer.test.jsx.snap delete mode 100644 src/views/SubmissionView/TextResponseEditor/__snapshots__/index.test.jsx.snap delete mode 100644 src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap delete mode 100644 src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap delete mode 100644 src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap delete mode 100644 src/views/SubmissionView/__snapshots__/index.test.jsx.snap diff --git a/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap b/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap deleted file mode 100644 index d67b202e..00000000 --- a/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,94 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders overall feedback is disabled 1`] = ` - - - - Overall comments - - -
- props.overallFeedbackPrompt -
-
-
- -
-`; - -exports[` renders overall feedback is enabled 1`] = ` - - - - Overall comments - - -
- props.overallFeedbackPrompt -
-
-
- -
-`; - -exports[` renders overall feedback is invalid 1`] = ` - - - - Overall comments - - -
- props.overallFeedbackPrompt -
-
-
- - - The overall feedback is required - -
-`; diff --git a/src/components/FilePreview/components/__snapshots__/FileCard.test.jsx.snap b/src/components/FilePreview/components/__snapshots__/FileCard.test.jsx.snap deleted file mode 100644 index 2cf08071..00000000 --- a/src/components/FilePreview/components/__snapshots__/FileCard.test.jsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`File Preview Card component snapshot 1`] = ` - - - test-file-name.pdf - - } - > -
-

- some children -

-
-
-
-`; diff --git a/src/components/FilePreview/components/__snapshots__/FileRenderer.test.jsx.snap b/src/components/FilePreview/components/__snapshots__/FileRenderer.test.jsx.snap deleted file mode 100644 index d8af97d8..00000000 --- a/src/components/FilePreview/components/__snapshots__/FileRenderer.test.jsx.snap +++ /dev/null @@ -1,34 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FileRenderer component snapshot is not loading, with error 1`] = ` - - - -`; - -exports[`FileRenderer component snapshot isLoading, no Error 1`] = ` - - - - -`; diff --git a/src/views/SubmissionView/TextResponseEditor/__snapshots__/index.test.jsx.snap b/src/views/SubmissionView/TextResponseEditor/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 6ea02cf8..00000000 --- a/src/views/SubmissionView/TextResponseEditor/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render Rich Text Editor 1`] = ` -
- -
-`; - -exports[` render Text Editor 1`] = ` -
- -
-`; diff --git a/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap deleted file mode 100644 index e3cb598b..00000000 --- a/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders 1`] = ` - - - - -`; diff --git a/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap deleted file mode 100644 index 7b07e027..00000000 --- a/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap +++ /dev/null @@ -1,101 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render default 1`] = ` -
-
-

- Your response -

-

- - Draft saved -

-
-

- - Instructions - : - - Create a response to the prompt below. - Progress will be saved automatically and you can return to complete your - progress at any time. After you submit your response, you cannot edit - it. -

-
- - -
- -
-`; - -exports[` render no prompts 1`] = ` -
-
-

- Your response -

-

- - Draft saved -

-
-

- - Instructions - : - - Create a response to the prompt below. - Progress will be saved automatically and you can return to complete your - progress at any time. After you submit your response, you cannot edit - it. -

- -
-`; diff --git a/src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap deleted file mode 100644 index 86180b15..00000000 --- a/src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap +++ /dev/null @@ -1,66 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` hide rubric 1`] = ` -
-
- - - - - -
-
-`; - -exports[` show rubric 1`] = ` -
-
- - - - - - -
-
-`; diff --git a/src/views/SubmissionView/__snapshots__/index.test.jsx.snap b/src/views/SubmissionView/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 8eaaa21d..00000000 --- a/src/views/SubmissionView/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders 1`] = ` - - -
-
- - - - - -
-
- -
-`; From 67d2ea38445c514b59f646e585e4fd0bd589d760 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 15:13:56 +0000 Subject: [PATCH 26/66] fix: message id --- src/views/XBlockView/StatusRow/DueDateMessage/messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/XBlockView/StatusRow/DueDateMessage/messages.js b/src/views/XBlockView/StatusRow/DueDateMessage/messages.js index 372598c7..8bafbdeb 100644 --- a/src/views/XBlockView/StatusRow/DueDateMessage/messages.js +++ b/src/views/XBlockView/StatusRow/DueDateMessage/messages.js @@ -19,7 +19,7 @@ const messages = defineMessages({ yourSelfAssessment: { defaultMessage: 'Your self assessment', description: 'Self assessment label string for sentence noun context', - id: 'frontend-app-ora.XBlockView.DueDateMessage.selfAssessment', + id: 'frontend-app-ora.XBlockView.DueDateMessage.yourSelfAssessment', }, peerGrading: { defaultMessage: 'Peer grading', From 8c59663d6be3aa302d0b3bfe0762bd246c2527b7 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 15:25:11 +0000 Subject: [PATCH 27/66] fix: messages --- src/components/Assessment/ReadonlyAssessment/messages.js | 2 +- src/components/ModalActions/messages.js | 2 +- src/components/StatusAlert/messages.js | 2 +- src/hooks/actions/messages.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Assessment/ReadonlyAssessment/messages.js b/src/components/Assessment/ReadonlyAssessment/messages.js index d126c322..35f8a2f7 100644 --- a/src/components/Assessment/ReadonlyAssessment/messages.js +++ b/src/components/Assessment/ReadonlyAssessment/messages.js @@ -52,7 +52,7 @@ const messages = defineMessages({ description: 'Hedaer for submitted grade display', }, submittedAssessment: { - id: 'ora-collapsible-comment.submittedGrade', + id: 'ora-collapsible-comment.submittedAssessment', defaultMessage: 'Submitted assessment', description: 'Header for submitted assessment display', }, diff --git a/src/components/ModalActions/messages.js b/src/components/ModalActions/messages.js index 3c30aba4..4259ace4 100644 --- a/src/components/ModalActions/messages.js +++ b/src/components/ModalActions/messages.js @@ -33,7 +33,7 @@ const messages = defineMessages({ description: 'Action button to begin studentTraining step', }, startSelf: { - id: 'ora-mfe.ModalActions.startTraining', + id: 'ora-mfe.ModalActions.startSelf', defaultMessage: 'Begin self grading', description: 'Action button to begin self assessment step', }, diff --git a/src/components/StatusAlert/messages.js b/src/components/StatusAlert/messages.js index 24b0327d..ff60a12e 100644 --- a/src/components/StatusAlert/messages.js +++ b/src/components/StatusAlert/messages.js @@ -210,7 +210,7 @@ const peerHeadings = defineMessages({ description: 'Peer Assessment not avilable status alert heading', }, [stepStates.submitted]: { - id: 'frontend-app-ora.StatusAlert.Heading.studentTraining.submitted', + id: 'frontend-app-ora.StatusAlert.Heading.peer.submitted', defaultMessage: 'Peer Assessment Submitted: TODO', description: 'Peer Assessment submitted status alert', }, diff --git a/src/hooks/actions/messages.js b/src/hooks/actions/messages.js index 92340d8f..9481151b 100644 --- a/src/hooks/actions/messages.js +++ b/src/hooks/actions/messages.js @@ -13,7 +13,7 @@ const messages = defineMessages({ description: 'Action button to begin studentTraining step', }, startSelf: { - id: 'ora-mfe.ModalActions.startTraining', + id: 'ora-mfe.ModalActions.startSelf', defaultMessage: 'Begin self grading', description: 'Action button to begin self assessment step', }, From 06b4ff0f8e4565d33eb0f59071824ccae88c887c Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 18:00:57 +0000 Subject: [PATCH 28/66] chore: redux selector tests --- src/data/redux/app/selectors.test.ts | 138 ++++++++++++++++++++++++++ src/data/redux/app/selectors.ts | 68 +++++++++++++ src/data/redux/app/selectors/index.ts | 71 ------------- 3 files changed, 206 insertions(+), 71 deletions(-) create mode 100644 src/data/redux/app/selectors.test.ts create mode 100644 src/data/redux/app/selectors.ts delete mode 100644 src/data/redux/app/selectors/index.ts diff --git a/src/data/redux/app/selectors.test.ts b/src/data/redux/app/selectors.test.ts new file mode 100644 index 00000000..400cd3fe --- /dev/null +++ b/src/data/redux/app/selectors.test.ts @@ -0,0 +1,138 @@ +import { keyStore } from '@edx/react-unit-test-utils'; + +import selectors from './selectors'; + +const testState = { + app: { + assessment: { + submittedAssessment: { + criteria: [ + { selectedOption: 1, feedback: 'test-criterion-feedback1' }, + { selectedOption: 2, feedback: 'test-criterion-feedback2' }, + ], + overallFeedback: 'test-overall-feedback', + }, + showTrainingError: true, + }, + response: ['test-response'], + formFields: { + criteria: [ + { selectedOption: 1, feedback: 'test-formFields-criterion-feedback1' }, + { selectedOption: 3, feedback: 'test-formFields-criterion-feedback2' }, + ], + overallFeedback: 'formFields-overall-feedback', + }, + hasSubmitted: true, + showValidation: true, + testDirty: true, + testProgressKey: 'test-progress-key', + testDataPath: 'test-data-path', + }, +}; + +let selector; +const appKeys = keyStore(testState.app); +describe('redux app selectors', () => { + const testSimpleAppSelector = (key) => { + selector = selectors[key]; + expect(selector.dependencies).toEqual([selectors.root]); + expect(selector.resultFunc(testState.app)).toEqual(testState.app[key]); + }; + describe('root', () => { + it('returns app object from top-level state', () => { + expect(selectors.root(testState)).toEqual(testState.app); + }); + }); + describe('assessment', () => { + it('returns assessment object from root selector', () => { + testSimpleAppSelector(appKeys.assessment); + }); + }); + describe('formFields', () => { + it('returns formFields object from root selector', () => { + testSimpleAppSelector(appKeys.formFields); + }); + }); + describe('criterionFeedback', () => { + beforeEach(() => { + selector = selectors.criterionFeedback; + }); + it('returns criterion feedback if it exists', () => { + expect(selector(testState, 0)) + .toEqual(testState.app.formFields.criteria[0].feedback); + }); + it('returns undefined if criterion does not exist', () => { + expect(selector(testState, 4)).toEqual(undefined); + }); + }); + describe('criterionOption', () => { + beforeEach(() => { + selector = selectors.criterionOption; + }); + it('returns criterion option if it exists', () => { + expect(selector(testState, 0)) + .toEqual(testState.app.formFields.criteria[0].selectedOption); + }); + it('returns undefined if criterion does not exist', () => { + expect(selector(testState, 4)).toEqual(undefined); + }); + }); + describe('hasSubmitted', () => { + it('returns hasSubmitted from root selector', () => { + testSimpleAppSelector(appKeys.hasSubmitted); + }); + }); + describe('overallFeedback', () => { + it('returns overallFeedback from formFields', () => { + selector = selectors.overallFeedback; + expect(selector(testState)).toEqual(testState.app.formFields.overallFeedback); + }); + }); + describe('response', () => { + it('returns the response from the root selector', () => { + testSimpleAppSelector(appKeys.response); + }); + }); + describe('showTrainingError', () => { + beforeEach(() => { + selector = selectors.showTrainingError; + }); + it('is memoized based on the assessment', () => { + expect(selector.dependencies).toEqual([selectors.assessment]); + }); + it('returns showTrainingError from the assessment if there is one', () => { + expect(selector.resultFunc(testState.app.assessment)).toEqual(testState.app.assessment.showTrainingError); + }); + it('returns undefined if there is no assessment', () => { + expect(selector.resultFunc(null)).toEqual(undefined); + }); + }); + describe('showValidation', () => { + it('returns showValidation from the root selector', () => { + testSimpleAppSelector(appKeys.showValidation); + }); + }); + describe('submittedAssessment', () => { + it('returns submittedAssessment from assessment selector', () => { + selector = selectors.submittedAssessment; + expect(selector.dependencies).toEqual([selectors.assessment]); + expect(selector.resultFunc(testState.app.assessment)) + .toEqual(testState.app.assessment.submittedAssessment); + }); + }); + describe('testDataPath', () => { + it('returns testDataPath from root selector', () => { + testSimpleAppSelector(appKeys.testDataPath); + }); + }); + describe('testDirty', () => { + it('returns testDirty from root selector', () => { + testSimpleAppSelector(appKeys.testDirty); + }); + }); + describe('testProgressKey', () => { + it('returns testProgressKey from root selector', () => { + testSimpleAppSelector(appKeys.testProgressKey); + }); + }); +}); diff --git a/src/data/redux/app/selectors.ts b/src/data/redux/app/selectors.ts new file mode 100644 index 00000000..1528e67f --- /dev/null +++ b/src/data/redux/app/selectors.ts @@ -0,0 +1,68 @@ +import { createSelector } from '@reduxjs/toolkit'; +import * as types from './types'; + +type RootState = { app: types.AppState }; + +const selectors: { [k: string]: any } = { + root: (state: RootState): types.AppState => state.app, +}; +selectors.assessment = createSelector( + selectors.root, + (app: types.AppState): types.Assessment => app.assessment, +); +selectors.formFields = createSelector( + selectors.root, + (app: types.AppState): types.FormFields => app.formFields, +); +selectors.criterionFeedback = (state: RootState, criterionIndex: number): string | undefined => ( + selectors.formFields(state).criteria[criterionIndex]?.feedback +); + +selectors.criterionOption = (state: RootState, criterionIndex: number): number | undefined => ( + selectors.formFields(state).criteria[criterionIndex]?.selectedOption +); + +selectors.overallFeedback = (state: RootState): string => ( + selectors.formFields(state).overallFeedback +); + +selectors.hasSubmitted = createSelector( + selectors.root, + (app: types.AppState) => app.hasSubmitted, +); + +selectors.response = createSelector( + selectors.root, + (app: types.AppState): types.Response => app.response, +); + +selectors.showTrainingError = createSelector( + selectors.assessment, + (assessment: types.Assessment): boolean => assessment?.showTrainingError, +); + +selectors.showValidation = createSelector( + selectors.root, + (app: types.AppState) => app.showValidation, +); + +selectors.submittedAssessment = createSelector( + selectors.assessment, + (assessment: types.Assessment) => assessment.submittedAssessment, +); + +// test +selectors.testDataPath = createSelector( + selectors.root, + (app: types.AppState): string | null | undefined => app.testDataPath, +); +selectors.testDirty = createSelector( + selectors.root, + (app: types.AppState): boolean => app.testDirty, +); +selectors.testProgressKey = createSelector( + selectors.root, + (app: types.AppState): string | null | undefined => app.testProgressKey, +); + +export default selectors; diff --git a/src/data/redux/app/selectors/index.ts b/src/data/redux/app/selectors/index.ts deleted file mode 100644 index 1d4d753a..00000000 --- a/src/data/redux/app/selectors/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import * as types from '../types'; - -type RootState = { app: types.AppState }; - -const rootSelector = (state: RootState): types.AppState => state.app; - -const assessmentData = createSelector( - rootSelector, - (app: types.AppState): types.Assessment => app.assessment, -); -const formFieldsData = createSelector( - rootSelector, - (app: types.AppState): types.FormFields => app.formFields, -); -const selectors = { - criterionFeedback: (state: RootState, criterionIndex: number): string | undefined => ( - formFieldsData(state).criteria[criterionIndex]?.feedback - ), - - criterionOption: (state: RootState, criterionIndex: number): number | undefined => ( - formFieldsData(state).criteria[criterionIndex]?.selectedOption - ), - - response: createSelector( - rootSelector, - (app: types.AppState): types.Response => app.response, - ), - - hasSubmitted: createSelector( - rootSelector, - (app: types.AppState) => app.hasSubmitted, - ), - - overallFeedback: (state: RootState): string => formFieldsData(state).overallFeedback, - - showTrainingError: createSelector( - assessmentData, - (assessment: types.Assessment): boolean => assessment?.showTrainingError, - ), - - showValidation: createSelector( - rootSelector, - (app: types.AppState) => app.showValidation, - ), - - submittedAssessment: createSelector( - assessmentData, - (assessment: types.Assessment) => assessment.submittedAssessment, - ), - - // test - testProgressKey: createSelector( - rootSelector, - ({ testProgressKey }) => testProgressKey, - ), - testDirty: createSelector( - rootSelector, - (app: types.AppState): boolean => app.testDirty, - ), - testDataPath: createSelector( - rootSelector, - (app: types.AppState): string | null | undefined => app.testDataPath, - ), -}; - -export default { - assessment: assessmentData, - formFields: formFieldsData, - ...selectors, -}; From dfeab9df35b6b832eeef974c044ed27bb0d4f580 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 22 Nov 2023 18:19:52 +0000 Subject: [PATCH 29/66] chore: linting --- src/data/services/lms/hooks/data.ts | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts index 1e46726c..d5ecc7ed 100644 --- a/src/data/services/lms/hooks/data.ts +++ b/src/data/services/lms/hooks/data.ts @@ -16,7 +16,7 @@ import { queryKeys, stepNames, stepRoutes, -} from 'constants'; +} from 'constants/index'; import { defaultViewProgressKeys, progressKeys } from 'constants/mockData'; import * as types from '../types'; @@ -26,7 +26,7 @@ import fakeData from '../fakeData'; import { loadState } from '../fakeData/dataStates'; -export const useORAConfig = (): types.QueryData => { +export const useORAConfig = (): types.QueryData => { const oraConfigUrl = useORAConfigUrl(); const testDataPath = useTestDataPath(); const { progressKey } = useParams(); @@ -35,24 +35,22 @@ export const useORAConfig = (): types.QueryData => { queryKey: [queryKeys.oraConfig], queryFn: () => { if (testDataPath) { - console.log("ora config fake data"); if (progressKey === progressKeys.staffAfterSubmission) { return Promise.resolve( - camelCaseObject(fakeData.oraConfig.assessmentStaffAfterSubmission) + camelCaseObject(fakeData.oraConfig.assessmentStaffAfterSubmission), ); } if (progressKey === progressKeys.staffAfterSelf) { return Promise.resolve( - camelCaseObject(fakeData.oraConfig.assessmentStaffAfterSelf) + camelCaseObject(fakeData.oraConfig.assessmentStaffAfterSelf), ); } return Promise.resolve( - camelCaseObject(fakeData.oraConfig.assessmentTinyMCE) + camelCaseObject(fakeData.oraConfig.assessmentTinyMCE), ); } - console.log("ora config real data"); return getAuthenticatedHttpClient().post(oraConfigUrl, {}).then( - ({ data }) => camelCaseObject(data) + ({ data }) => camelCaseObject(data), ); }, staleTime: Infinity, @@ -68,9 +66,6 @@ export const usePageData = () => { const testDataPath = useTestDataPath(); const pageDataUrl = usePageDataUrl(); - const loadMockData = (key) => Promise.resolve( - camelCaseObject(loadState({ view, progressKey: key })), - ); // test const testProgressKey = useTestProgressKey(); @@ -79,7 +74,6 @@ export const usePageData = () => { const progressKey = testProgressKey || params.progressKey || defaultViewProgressKeys[viewKey]; const queryFn = () => { - console.log("page data query function"); if (testDataPath) { return Promise.resolve(camelCaseObject(loadState({ view, progressKey }))); } @@ -102,11 +96,3 @@ export const usePageData = () => { staleTime: Infinity, }); }; - -export const useSubmitResponse = () => - useMutation({ - mutationFn: (response) => { - // console.log({ submit: response }); - return Promise.resolve(); - }, - }); From 17f85c76f1947b19859f3efe0baafd4e352d18ff Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Thu, 23 Nov 2023 04:38:33 +0000 Subject: [PATCH 30/66] chore: lms data hook tests pt 1 --- src/data/services/lms/fakeData/dataStates.js | 2 +- .../lms/fakeData/pageData/progress.js | 2 +- src/data/services/lms/hooks/data.test.ts | 158 ++++++++++++++++++ src/data/services/lms/hooks/data.ts | 91 +++------- src/data/services/lms/hooks/mockData.ts | 43 +++++ src/data/services/lms/hooks/utils.js | 23 +++ src/data/services/lms/urls.js | 12 +- 7 files changed, 254 insertions(+), 77 deletions(-) create mode 100644 src/data/services/lms/hooks/data.test.ts create mode 100644 src/data/services/lms/hooks/mockData.ts create mode 100644 src/data/services/lms/hooks/utils.js diff --git a/src/data/services/lms/fakeData/dataStates.js b/src/data/services/lms/fakeData/dataStates.js index 24bd928b..59cf8e6d 100644 --- a/src/data/services/lms/fakeData/dataStates.js +++ b/src/data/services/lms/fakeData/dataStates.js @@ -1,4 +1,4 @@ -import { routeSteps } from 'constants'; +import { routeSteps } from 'constants/index'; import { defaultViewProgressKeys, stepConfigs, diff --git a/src/data/services/lms/fakeData/pageData/progress.js b/src/data/services/lms/fakeData/pageData/progress.js index 054a7334..d7d11bda 100644 --- a/src/data/services/lms/fakeData/pageData/progress.js +++ b/src/data/services/lms/fakeData/pageData/progress.js @@ -1,6 +1,6 @@ import { StrictDict } from '@edx/react-unit-test-utils'; -import { stepNames } from 'constants'; +import { stepNames } from 'constants/index'; import { closedStates, progressKeys } from 'constants/mockData'; import { assessmentSteps } from '../oraConfig'; /* eslint-disable camelcase */ diff --git a/src/data/services/lms/hooks/data.test.ts b/src/data/services/lms/hooks/data.test.ts new file mode 100644 index 00000000..1655af6e --- /dev/null +++ b/src/data/services/lms/hooks/data.test.ts @@ -0,0 +1,158 @@ +import { when } from 'jest-when'; +import { useQuery } from '@tanstack/react-query'; +import { useViewStep } from 'hooks/routing'; +import { useTestDataPath } from 'hooks/testHooks'; + +import { queryKeys } from 'constants/index'; +import fakeData from '../fakeData'; +import { loadState } from '../fakeData/dataStates'; +import { useORAConfigUrl, usePageDataUrl } from '../urls'; +import { loadData, logPageData, post } from './utils'; +import { useMockORAConfig, useMockPageData } from './mockData'; + +import { useORAConfig, usePageData } from './data'; + +jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn() })); +jest.mock('hooks/routing', () => ({ useViewStep: jest.fn() })); +jest.mock('hooks/testHooks', () => ({ + useTestDataPath: jest.fn(() => null), +})); +jest.mock('./utils', () => ({ + loadData: jest.fn(), + logPageData: jest.fn(), + post: jest.fn(), +})); +jest.mock('../urls', () => ({ + useORAConfigUrl: jest.fn(), + usePageDataUrl: jest.fn(), +})); +jest.mock('./mockData', () => ({ + useMockORAConfig: jest.fn(), + useMockPageData: jest.fn(), +})); + +when(useQuery) + .calledWith(expect.anything()) + .mockImplementation(args => ({ useQuery: args })); + +const viewStep = 'view-step'; +when(useViewStep).calledWith().mockReturnValue(viewStep); +const oraConfigUrl = 'test-url'; +when(useORAConfigUrl).calledWith().mockReturnValue(oraConfigUrl); +const pageDataUrl = (step) => ({ pageDataUrl: step }); +when(usePageDataUrl).calledWith().mockReturnValue(pageDataUrl); +const mockORAConfig = fakeData.oraConfig.assessmentTinyMCE; +when(useMockORAConfig).calledWith().mockReturnValue(mockORAConfig); +when(loadData).calledWith(expect.anything()).mockImplementation( + data => ({ loadData: data }), +); +when(logPageData).calledWith(expect.anything()).mockImplementation(data => data); +const postObj = (url, data) => ({ data: { post: { url, data } } }); +when(post).calledWith(expect.anything(), expect.anything()) + .mockImplementation((url, data) => Promise.resolve(postObj(url, data))); +const mockPageData = loadState({ view: 'submission', progressKey: 'submission_saved' }); +when(useMockPageData).calledWith().mockReturnValue(mockPageData); + +const testDataPath = 'test-data-path'; + +let hook; +describe('lms service top-level data hooks', () => { + describe('useORAConfig', () => { + describe('behavior', () => { + beforeEach(() => { + hook = useORAConfig(); + }); + it('loads url from hook', () => { + expect(useORAConfigUrl).toHaveBeenCalledWith(); + }); + it('loads testDataPath and mockORAConfig from hooks', () => { + expect(useTestDataPath).toHaveBeenCalledWith(); + expect(useMockORAConfig).toHaveBeenCalledWith(); + }); + }); + describe('output', () => { + describe('if testDataPath is set', () => { + beforeEach(() => { + when(useTestDataPath).calledWith().mockReturnValueOnce('test-data-path'); + hook = useORAConfig(); + }); + it('returns a useQuery call with inifite staleTime and oraConfig queryKey', () => { + expect(hook.useQuery.queryKey).toEqual([queryKeys.oraConfig]); + expect(hook.useQuery.staleTime).toEqual(Infinity); + }); + it('returns mockORAConfig for queryFn', () => { + expect(hook.useQuery.queryFn).toEqual(mockORAConfig); + }); + }); + describe('if testDataPath is not set', () => { + beforeEach(() => { + hook = useORAConfig(); + }); + it('returns a useQuery call with inifite staleTime and oraConfig queryKey', () => { + expect(hook.useQuery.queryKey).toEqual([queryKeys.oraConfig]); + expect(hook.useQuery.staleTime).toEqual(Infinity); + }); + describe('queryFn', () => { + it('returns a callback based on oraConfigUrl', () => { + expect(hook.useQuery.queryFn.useCallback.prereqs).toEqual([oraConfigUrl]); + }); + it('posts empty object to oraConfigUrl, then calls loadData with result', async () => { + await expect(hook.useQuery.queryFn.useCallback.cb()) + .resolves.toStrictEqual(loadData(postObj(oraConfigUrl, {}))); + }); + }); + }); + }); + }); + describe('usePageData', () => { + describe('behavior', () => { + beforeEach(() => { + hook = usePageData(); + }); + it('loads url from hook', () => { + expect(usePageDataUrl).toHaveBeenCalledWith(); + }); + it('loads testDataPath and mockORAConfig from hooks', () => { + expect(useTestDataPath).toHaveBeenCalledWith(); + expect(useMockPageData).toHaveBeenCalledWith(); + }); + }); + describe('output', () => { + describe('if testDataPath is set', () => { + beforeEach(() => { + when(useTestDataPath).calledWith().mockReturnValueOnce(testDataPath); + hook = usePageData(); + }); + it('returns a useQuery call with inifite staleTime and oraConfig queryKey', () => { + expect(hook.useQuery.queryKey).toEqual([queryKeys.pageData, testDataPath]); + expect(hook.useQuery.staleTime).toEqual(Infinity); + }); + it('returns mockORAConfig for queryFn', () => { + expect(hook.useQuery.queryFn).toEqual(mockPageData); + }); + }); + describe('if testDataPath is not set', () => { + let url; + let callback; + beforeEach(() => { + hook = usePageData(); + url = pageDataUrl(viewStep); + callback = hook.useQuery.queryFn.useCallback; + }); + it('returns a useQuery call with inifite staleTime and pageData queryKey', () => { + expect(hook.useQuery.queryKey).toEqual([queryKeys.pageData, null]); + expect(hook.useQuery.staleTime).toEqual(Infinity); + }); + describe('queryFn', () => { + it('returns a callback based on pageDataUrl', () => { + expect(callback.prereqs).toEqual([pageDataUrl, viewStep]); + }); + it('posts empty object to pageDataUrl, then calls loadData with result', async () => { + await expect(callback.cb()) + .resolves.toStrictEqual(loadData(postObj(url, {}))); + }); + }); + }); + }); + }); +}); diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts index d5ecc7ed..73a62a52 100644 --- a/src/data/services/lms/hooks/data.ts +++ b/src/data/services/lms/hooks/data.ts @@ -1,98 +1,47 @@ import React from 'react'; -import { useParams, useLocation } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { camelCaseObject } from '@edx/frontend-platform'; +import { queryKeys } from 'constants/index'; -import { useHasSubmitted } from 'data/redux/hooks'; // for test data -import { - useTestProgressKey, - useTestDataPath, -} from 'hooks/testHooks'; - -import { - routeSteps, - queryKeys, - stepNames, - stepRoutes, -} from 'constants/index'; -import { defaultViewProgressKeys, progressKeys } from 'constants/mockData'; +import { useViewStep } from 'hooks/routing'; +import { useTestDataPath } from 'hooks/testHooks'; import * as types from '../types'; import { useORAConfigUrl, usePageDataUrl } from '../urls'; -import fakeData from '../fakeData'; - -import { loadState } from '../fakeData/dataStates'; +import { loadData, logPageData, post } from './utils'; +import { useMockORAConfig, useMockPageData } from './mockData'; export const useORAConfig = (): types.QueryData => { const oraConfigUrl = useORAConfigUrl(); const testDataPath = useTestDataPath(); - const { progressKey } = useParams(); - + const mockORAConfig = useMockORAConfig(); + const apiMethod = React.useCallback( + () => post(oraConfigUrl, {}).then(loadData), + [oraConfigUrl], + ); return useQuery({ queryKey: [queryKeys.oraConfig], - queryFn: () => { - if (testDataPath) { - if (progressKey === progressKeys.staffAfterSubmission) { - return Promise.resolve( - camelCaseObject(fakeData.oraConfig.assessmentStaffAfterSubmission), - ); - } - if (progressKey === progressKeys.staffAfterSelf) { - return Promise.resolve( - camelCaseObject(fakeData.oraConfig.assessmentStaffAfterSelf), - ); - } - return Promise.resolve( - camelCaseObject(fakeData.oraConfig.assessmentTinyMCE), - ); - } - return getAuthenticatedHttpClient().post(oraConfigUrl, {}).then( - ({ data }) => camelCaseObject(data), - ); - }, + queryFn: testDataPath ? mockORAConfig : apiMethod, staleTime: Infinity, }); }; -export const usePageData = () => { - const location = useLocation(); - const view = location.pathname.split('/')[1]; - const hasSubmitted = useHasSubmitted(); - const viewStep = routeSteps[view]; - - const testDataPath = useTestDataPath(); - +export const usePageData = (): types.QueryData => { + const viewStep = useViewStep(); const pageDataUrl = usePageDataUrl(); - // test - const testProgressKey = useTestProgressKey(); - const params = useParams(); - const viewKey = stepRoutes[viewStep]; - const progressKey = testProgressKey || params.progressKey || defaultViewProgressKeys[viewKey]; + const testDataPath = useTestDataPath(); + const mockPageData = useMockPageData(); - const queryFn = () => { - if (testDataPath) { - return Promise.resolve(camelCaseObject(loadState({ view, progressKey }))); - } - const url = (hasSubmitted || view === stepNames.xblock) - ? pageDataUrl() - : pageDataUrl(viewStep); - console.log({ pageDataUrl: url }); - console.log({ params }); - return getAuthenticatedHttpClient().post(url, {}) - .then(({ data }) => camelCaseObject(data)) - .then(data => { - console.log({ pageData: data }); - return data; - }); - }; + const apiMethod = React.useCallback( + () => post(pageDataUrl(viewStep), {}).then(loadData).then(logPageData), + [pageDataUrl, viewStep], + ); return useQuery({ queryKey: [queryKeys.pageData, testDataPath], - queryFn, + queryFn: testDataPath ? mockPageData : apiMethod, staleTime: Infinity, }); }; diff --git a/src/data/services/lms/hooks/mockData.ts b/src/data/services/lms/hooks/mockData.ts new file mode 100644 index 00000000..9464de1d --- /dev/null +++ b/src/data/services/lms/hooks/mockData.ts @@ -0,0 +1,43 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { useActiveView, useViewStep } from 'hooks/routing'; +import { useTestProgressKey } from 'hooks/testHooks'; +import { defaultViewProgressKeys, progressKeys } from 'constants/mockData'; +import { stepRoutes } from 'constants/index'; + +import * as types from '../types'; +import fakeData from '../fakeData'; +import { loadState } from '../fakeData/dataStates'; +import { fakeResponse } from './utils'; + +export const useProgressKey = (): string => { + const params = useParams(); + const viewStep = useViewStep(); + const testProgressKey = useTestProgressKey(); + const viewKey = stepRoutes[viewStep]; + return testProgressKey || params.progressKey || defaultViewProgressKeys[viewKey]; +}; + +export const oraConfigs = { + [progressKeys.staffAfterSubmission]: fakeData.oraConfig.assessmentStaffAfterSubmission, + [progressKeys.staffAfterSelf]: fakeData.oraConfig.assessmentStaffAfterSelf, + default: fakeData.oraConfig.assessmentTinyMCE, +}; + +type ORAConfigEvent = () => Promise; +export const useMockORAConfig = (): ORAConfigEvent => { + const progressKey = useProgressKey(); + const config = progressKey in oraConfigs ? oraConfigs[progressKey] : oraConfigs.default; + return React.useCallback(() => fakeResponse(config), [config]); +}; + +type PageDataEvent = () => Promise; +export const useMockPageData = (): PageDataEvent => { + const view = useActiveView(); + const progressKey = useProgressKey(); + return React.useCallback( + () => fakeResponse(loadState({ view, progressKey })), + [view, progressKey], + ); +}; diff --git a/src/data/services/lms/hooks/utils.js b/src/data/services/lms/hooks/utils.js new file mode 100644 index 00000000..c97f19fa --- /dev/null +++ b/src/data/services/lms/hooks/utils.js @@ -0,0 +1,23 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { camelCaseObject } from '@edx/frontend-platform'; + +import { progressKeys } from 'constants/mockData'; + +import fakeData from '../fakeData'; + +export const loadData = ({ data }) => camelCaseObject(data); + +export const post = (...args) => getAuthenticatedHttpClient().post(...args); + +export const fakeResponse = (data) => Promise.resolve(camelCaseObject(data)); + +export const oraConfigs = { + [progressKeys.staffAfterSubmission]: fakeData.oraConfig.assessmentStaffAfterSubmission, + [progressKeys.staffAfterSelf]: fakeData.oraConfig.assessmentStaffAfterSelf, + default: fakeData.oraConfig.assessmentStaffAfterSelf, +}; + +export const logPageData = (data => { + console.log({ pageData: data }); + return data; +}); diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js index f48230c5..d4793dd4 100644 --- a/src/data/services/lms/urls.js +++ b/src/data/services/lms/urls.js @@ -3,7 +3,9 @@ import { useParams } from 'react-router-dom'; import { StrictDict } from '@edx/react-unit-test-utils'; import { getConfig } from '@edx/frontend-platform'; -import { stepRoutes } from 'constants'; +import { useHasSubmitted } from 'data/redux/hooks'; + +import { stepNames, stepRoutes } from 'constants'; const useBaseUrl = () => { const { xblockId, courseId } = useParams(); @@ -25,10 +27,12 @@ export const useViewUrl = () => { }; export const usePageDataUrl = () => { + const hasSubmitted = useHasSubmitted(); const baseUrl = useBaseUrl(); - return (step) => (step - ? `${baseUrl}/get_learner_data/${step}` - : `${baseUrl}/get_learner_data/`); + const url = `${baseUrl}/get_learner_data/`; + return (step) => ( + ((step === stepNames.xblock) || hasSubmitted) ? `${url}${step}` : `${url}` + ); }; export default StrictDict({ From 6e80f127e196083871206585382974d26067e9c2 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Sat, 25 Nov 2023 03:09:21 +0000 Subject: [PATCH 31/66] chore: lms hook utils and mockData tests --- src/data/services/lms/hooks/mockData.test.ts | 129 +++++++++++++++++++ src/data/services/lms/hooks/utils.js | 14 +- src/data/services/lms/hooks/utils.test.js | 44 +++++++ 3 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 src/data/services/lms/hooks/mockData.test.ts create mode 100644 src/data/services/lms/hooks/utils.test.js diff --git a/src/data/services/lms/hooks/mockData.test.ts b/src/data/services/lms/hooks/mockData.test.ts new file mode 100644 index 00000000..449ed1e0 --- /dev/null +++ b/src/data/services/lms/hooks/mockData.test.ts @@ -0,0 +1,129 @@ +import { when } from 'jest-when'; +import { useParams } from 'react-router-dom'; +import { useActiveView, useViewStep } from 'hooks/routing'; +import { useTestProgressKey } from 'hooks/testHooks'; +import { + defaultViewProgressKeys, + progressKeys, + viewKeys, +} from 'constants/mockData'; +import { stepNames } from 'constants/index'; +import { loadState } from '../fakeData/dataStates'; +import { fakeResponse } from './utils'; + +import * as mockData from './mockData'; + +jest.mock('react-router-dom', () => ({ + useParams: jest.fn(), +})); +jest.mock('hooks/routing', () => ({ + useActiveView: jest.fn(), + useViewStep: jest.fn(), +})); +jest.mock('hooks/testHooks', () => ({ + useTestProgressKey: jest.fn(), +})); +jest.mock('../fakeData', () => ({ + oraConfig: { + assessmentStaffAfterSubmission: 'staff-after-submission', + assessmentStaffAfterSelf: 'staff-after-self', + assessmentTinyMCE: 'tiny-mce', + }, +})); +jest.mock('../fakeData/dataStates', () => ({ + loadState: jest.fn(), +})); +jest.mock('./utils', () => ({ + fakeResponse: jest.fn(), +})); + +when(useViewStep).calledWith().mockReturnValue(stepNames.self); +when(fakeResponse).calledWith(expect.anything()) + .mockImplementation(data => ({ fakeResponse: data })); + +let testValue; +let out; +describe('lms mock data hooks', () => { + describe('useProgressKey', () => { + it('returns testProgressKey if there is one', () => { + testValue = progressKeys.submissionFinished; + when(useTestProgressKey).calledWith().mockReturnValueOnce(testValue); + expect(mockData.useProgressKey()).toEqual(testValue); + }); + describe('if testProgressKey is not truthy', () => { + it('returns params.progressKey if truthy', () => { + testValue = progressKeys.peerAssessment; + when(useParams).calledWith().mockReturnValueOnce({ progressKey: testValue }); + expect(mockData.useProgressKey()).toEqual(testValue); + }); + describe('if params.progressKey is not set', () => { + it('returns the defaultViewProgressKey for the view', () => { + when(useParams).calledWith().mockReturnValueOnce({}); + testValue = progressKeys.peerAssessment; + expect(mockData.useProgressKey()) + .toEqual(defaultViewProgressKeys[viewKeys.self]); + }); + }); + }); + }); + describe('mock configs', () => { + let progressKey; + let progressKeySpy; + beforeEach(() => { + progressKeySpy = jest.spyOn(mockData, 'useProgressKey'); + }); + describe('useMockORAConfig', () => { + describe('behavior', () => { + it('loads progressKey from hook', () => { + progressKey = progressKeys.selfAssessment; + when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey); + mockData.useMockORAConfig(); + expect(progressKeySpy).toHaveBeenCalled(); + }); + }); + describe('output - useCallback hook', () => { + it('returns fakeResponse of oraConfig for progressKey, based on config', () => { + progressKey = progressKeys.selfAssessment; + when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey); + out = mockData.useMockORAConfig(); + const config = mockData.oraConfigs.default; + expect(out.useCallback.prereqs).toEqual([config]); + expect(out.useCallback.cb()).toEqual(fakeResponse(config)); + }); + it('returns fakeResponse of default oraConfig based on config if not config', () => { + progressKey = progressKeys.staffAfterSubmission; + when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey); + out = mockData.useMockORAConfig(); + const config = mockData.oraConfigs[progressKey]; + expect(out.useCallback.prereqs).toEqual([config]); + expect(out.useCallback.cb()).toEqual(fakeResponse(config)); + }); + }); + }); + describe('useMockPageData', () => { + describe('behavior', () => { + it('loads progressKey from hook', () => { + progressKey = progressKeys.selfAssessment; + when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey); + const testView = 'testView'; + when(useActiveView).calledWith().mockReturnValue(testView); + mockData.useMockPageData(); + expect(progressKeySpy).toHaveBeenCalled(); + expect(useActiveView).toHaveBeenCalled(); + }); + }); + describe('output - useCallback hook', () => { + it('returns fakeResponse of loadState({ view, progressKey })', () => { + const testView = 'testView'; + when(useActiveView).calledWith().mockReturnValue(testView); + progressKey = progressKeys.selfAssessment; + when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey); + out = mockData.useMockPageData(); + expect(out.useCallback.prereqs).toEqual([testView, progressKey]); + expect(out.useCallback.cb()) + .toEqual(fakeResponse(loadState({ view: testView, progressKey }))); + }); + }); + }); + }); +}); diff --git a/src/data/services/lms/hooks/utils.js b/src/data/services/lms/hooks/utils.js index c97f19fa..f2758e29 100644 --- a/src/data/services/lms/hooks/utils.js +++ b/src/data/services/lms/hooks/utils.js @@ -1,23 +1,13 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { camelCaseObject } from '@edx/frontend-platform'; -import { progressKeys } from 'constants/mockData'; - -import fakeData from '../fakeData'; - export const loadData = ({ data }) => camelCaseObject(data); export const post = (...args) => getAuthenticatedHttpClient().post(...args); export const fakeResponse = (data) => Promise.resolve(camelCaseObject(data)); -export const oraConfigs = { - [progressKeys.staffAfterSubmission]: fakeData.oraConfig.assessmentStaffAfterSubmission, - [progressKeys.staffAfterSelf]: fakeData.oraConfig.assessmentStaffAfterSelf, - default: fakeData.oraConfig.assessmentStaffAfterSelf, -}; - -export const logPageData = (data => { +export const logPageData = (data) => { console.log({ pageData: data }); return data; -}); +}; diff --git a/src/data/services/lms/hooks/utils.test.js b/src/data/services/lms/hooks/utils.test.js new file mode 100644 index 00000000..f0848874 --- /dev/null +++ b/src/data/services/lms/hooks/utils.test.js @@ -0,0 +1,44 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { camelCaseObject } from '@edx/frontend-platform'; +import * as utils from './utils'; + +jest.mock('@edx/frontend-platform/auth', () => ({ + getAuthenticatedHttpClient: jest.fn(), +})); +jest.mock('@edx/frontend-platform', () => ({ + camelCaseObject: jest.fn(obj => ({ camelCaseObject: obj })), +})); +const post = jest.fn((...args) => ({ post: args })); +getAuthenticatedHttpClient.mockReturnValue({ post }); + +const testValue = 'test-value'; +const testValue2 = 'test-value-2'; +const testObject = { + testKey: 'test-value', + testKey1: 'test-value-1', +}; +describe('lms service hook utils', () => { + describe('loadData', () => { + it('returns camel-cased data object from input arg', () => { + expect(utils.loadData({ data: testObject })) + .toEqual(camelCaseObject(testObject)); + }); + }); + describe('post', () => { + it('forwards the arguments to authenticated post request', () => { + expect(utils.post(testValue, testValue2)) + .toEqual(getAuthenticatedHttpClient().post(testValue, testValue2)); + }); + }); + describe('fakeResponse', () => { + it('returns a promise that resolves to camel-cased input', async () => { + await expect(utils.fakeResponse(testObject)) + .resolves.toStrictEqual(camelCaseObject(testObject)); + }); + }); + describe('logPageData', () => { + it('returns input data', () => { + expect(utils.logPageData(testObject)).toEqual(testObject); + }); + }); +}); From 4f8d0928dba64f59df1ee664f7879dfd735512d7 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Mon, 27 Nov 2023 10:55:17 -0500 Subject: [PATCH 32/66] chore: send resize message on load for xblock view --- src/views/XBlockView/index.jsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/views/XBlockView/index.jsx b/src/views/XBlockView/index.jsx index 7e822b7e..f1788007 100644 --- a/src/views/XBlockView/index.jsx +++ b/src/views/XBlockView/index.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { usePrompts } from 'hooks/app'; @@ -15,6 +15,13 @@ import './index.scss'; export const XBlockView = () => { const prompts = usePrompts(); + + useEffect(() => { + if (window.parent.length > 0) { + window.parent.postMessage({ type: 'plugin.resize', payload: { height: document.body.scrollHeight } }, document.referrer); + } + }, []); + return (

Open Response Assessment

From afffeb5c238637b831c86b5c8d72863405905705 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 27 Nov 2023 09:50:33 -0500 Subject: [PATCH 33/66] build: Updating workflow `commitlint.yml`. The .github/workflows/commitlint.yml workflow is missing or needs an update to stay in sync with the current standard for this workflow as defined in the `.github` repo of the `openedx` GitHub org. --- .github/workflows/commitlint.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 36e15a54..fec11d6c 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -7,14 +7,4 @@ on: jobs: commitlint: - runs-on: ubuntu-20.04 - steps: - - name: Check out repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: remove tsconfig.json # see issue https://github.com/conventional-changelog/commitlint/issues/3256 - run: | - rm tsconfig.json - - name: Check commits - uses: wagoid/commitlint-github-action@v5 + uses: openedx/.github/.github/workflows/commitlint.yml@master From 84ac141f66588aeb6ae42ebc68905c443d03df91 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 27 Nov 2023 16:49:17 +0000 Subject: [PATCH 34/66] fix: page data url conditional --- src/data/services/lms/urls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js index d4793dd4..6cecd91e 100644 --- a/src/data/services/lms/urls.js +++ b/src/data/services/lms/urls.js @@ -31,7 +31,7 @@ export const usePageDataUrl = () => { const baseUrl = useBaseUrl(); const url = `${baseUrl}/get_learner_data/`; return (step) => ( - ((step === stepNames.xblock) || hasSubmitted) ? `${url}${step}` : `${url}` + ((step === stepNames.xblock) || hasSubmitted) ? `${url}` : `${url}${step}` ); }; From f89b6af77c1a47c53f27a33b63bf02d510d96c0f Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 27 Nov 2023 16:59:01 +0000 Subject: [PATCH 35/66] chore: cleanup --- src/data/services/lms/urls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js index 6cecd91e..cf80d308 100644 --- a/src/data/services/lms/urls.js +++ b/src/data/services/lms/urls.js @@ -31,7 +31,7 @@ export const usePageDataUrl = () => { const baseUrl = useBaseUrl(); const url = `${baseUrl}/get_learner_data/`; return (step) => ( - ((step === stepNames.xblock) || hasSubmitted) ? `${url}` : `${url}${step}` + ((step === stepNames.xblock) || hasSubmitted) ? url : `${url}${step}` ); }; From e93dddcf2aadd4b46c6743501fa4319edf7d1d8e Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 27 Nov 2023 18:30:30 +0000 Subject: [PATCH 36/66] fix: step progress action conditional --- src/components/StepProgressIndicator/index.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/StepProgressIndicator/index.jsx b/src/components/StepProgressIndicator/index.jsx index 2cfe4335..fe305293 100644 --- a/src/components/StepProgressIndicator/index.jsx +++ b/src/components/StepProgressIndicator/index.jsx @@ -36,10 +36,9 @@ const StepProgressIndicator = ({ step }) => { const done = activeStepName === step ? stepInfo[step].numberOfAssessmentsCompleted : needed; - const showAction = hasSubmitted && ( - (step === stepNames.peer && !stepInfo[step].isWaitingForSubmissions) - || (needed !== done) - ); + const showAction = hasSubmitted + && !(step === stepNames.peer && stepInfo[step].isWaitingForSubmissions) + && (needed !== done); return (
{formatMessage(messages.progress, { needed, done })} From c4da7148896adac49c1cf4e51c4b8e481d7e0ead Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 27 Nov 2023 21:18:32 +0000 Subject: [PATCH 37/66] fix: assessment workflow --- src/App.jsx | 5 +++- .../Assessment/ReadonlyAssessment/index.jsx | 9 +++++- src/components/ModalActions/index.jsx | 18 +++++++++++- src/components/StatusAlert/index.jsx | 14 +++++++++- .../StepProgressIndicator/index.jsx | 20 +++++++++++-- src/data/redux/app/reducer.test.ts | 14 ++++++++-- src/data/redux/app/reducer.ts | 28 +++++++++++++------ src/data/redux/app/types.ts | 1 + .../services/lms/hooks/selectors/pageData.ts | 14 ++++++---- src/data/services/lms/urls.js | 11 ++++---- src/hooks/app.js | 1 + src/hooks/assessment.js | 2 +- 12 files changed, 109 insertions(+), 28 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index dac7ce94..4435f757 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,7 @@ import { ErrorPage, } from '@edx/frontend-platform/react'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { SkeletonTheme } from '@edx/paragon'; import AssessmentView from 'views/AssessmentView'; import SubmissionView from 'views/SubmissionView'; @@ -58,7 +59,9 @@ const App = () => { const pageWrapper = (children) => ( - {children} + + {children} + ); diff --git a/src/components/Assessment/ReadonlyAssessment/index.jsx b/src/components/Assessment/ReadonlyAssessment/index.jsx index e60dd8d2..64256b6b 100644 --- a/src/components/Assessment/ReadonlyAssessment/index.jsx +++ b/src/components/Assessment/ReadonlyAssessment/index.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useHasSubmitted } from 'hooks/app'; +import { useHasSubmitted, useRefreshPageData } from 'hooks/app'; import { useSubmittedAssessment } from 'hooks/assessment'; import ReadOnlyAssessment from './ReadOnlyAssessment'; @@ -12,6 +12,13 @@ import ReadOnlyAssessment from './ReadOnlyAssessment'; const ReadOnlyAssessmentContainer = (props) => { const hasSubmitted = useHasSubmitted(); const submittedAssessment = useSubmittedAssessment(); + const refreshPageData = useRefreshPageData(hasSubmitted); + // refresh page data on transfer to Read-only mode from editable + React.useEffect(() => { + if (hasSubmitted) { + refreshPageData(); + } + }, [hasSubmitted]); // eslint-disable-line react-hooks/exhaustive-deps return ( { const actions = useModalActionConfig({ options }); + const isPageDataLoading = useIsPageDataLoading(); const { primary, secondary } = actions || {}; const actionButton = (variant, btnProps) => (btnProps.state ? : ); From aea51b94fb7a8cd83f9069f7704ee75fc7ac9991 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Fri, 1 Dec 2023 05:49:09 +0000 Subject: [PATCH 57/66] chore: clean up files actions --- src/data/services/lms/hooks/actions/files.ts | 160 +++++++------------ 1 file changed, 62 insertions(+), 98 deletions(-) diff --git a/src/data/services/lms/hooks/actions/files.ts b/src/data/services/lms/hooks/actions/files.ts index c8c93083..27bc6311 100644 --- a/src/data/services/lms/hooks/actions/files.ts +++ b/src/data/services/lms/hooks/actions/files.ts @@ -1,32 +1,14 @@ import * as zip from '@zip.js/zip.js'; import FileSaver from 'file-saver'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform'; import { useQueryClient, useMutation } from '@tanstack/react-query'; -import { queryKeys } from 'constants'; +import { queryKeys } from 'constants/index'; import * as api from 'data/services/lms/api'; -import { useTestDataPath } from 'hooks/testHooks'; +import { PageData, UploadedFile } from '../../types'; -import fakeData from '../../fakeData'; -import { - PageData, - UploadedFile, -} from '../../types'; - -import { useCreateMutationAction } from './utils'; - -export const fakeProgress = async (requestConfig) => { - for (let i = 0; i <= 50; i++) { - // eslint-disable-next-line no-await-in-loop, no-promise-executor-return - await new Promise((resolve) => setTimeout(resolve, 100)); - requestConfig.onUploadProgress({ loaded: i, total: 50 }); - } -}; - -export const DownloadException = (errors: string[]) => ({ - errors, - name: 'DownloadException', -}); +export const DownloadException = (errors: string[]) => new Error( + `DownloadException: ${errors.join(', ')}` +); export const FetchSubmissionFilesException = () => ({ name: 'FetchSubmissionFilesException', @@ -35,38 +17,35 @@ export const FetchSubmissionFilesException = () => ({ /** * Generate a manifest file content based on files object */ -export const genManifest = (files: UploadedFile[]) => - files - .map( - (file, i) => - `Filename: ${i}-${file.fileName}\nDescription: ${file.fileDescription}\nSize: ${file.fileSize}` - ) - .join('\n\n'); +export const manifestString = ({ fileName, fileDescription, fileSize }, index) => ( + `Filename: ${index}-${fileName}\nDescription: ${fileDescription}\nSize: ${fileSize}` +); +export const genManifest = (files: UploadedFile[]) => files.map(manifestString).join('\n\n'); /** * Zip the blob output of a set of files with a manifest file. */ +export const zipSubFileName = ({ fileName }, index) => `${index}-${fileName}`; export const zipFiles = async ( files: UploadedFile[], blobs: Blob[], - zipFileName: string + zipFileName: string, ) => { const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip')); await zipWriter.add('manifest.txt', new zip.TextReader(genManifest(files))); // forEach or map will create additional thread. It is less readable if we create more // promise or async function just to circumvent that. + const promises: Promise[] = []; for (let i = 0; i < blobs.length; i++) { - // eslint-disable-next-line no-await-in-loop - await zipWriter.add( - `${i}-${files[i].fileName}`, - new zip.BlobReader(blobs[i]), - { - bufferedWrite: true, - } - ); + const blob = new zip.BlobReader(blobs[i]); + promises.push(zipWriter.add( + zipSubFileName(files[i], i), + blob, + { bufferedWrite: true }, + )); } - + await Promise.all(promises); const zipFile = await zipWriter.close(); const zipName = `${zipFileName}.zip`; FileSaver.saveAs(zipFile, zipName); @@ -75,15 +54,14 @@ export const zipFiles = async ( /** * Download a file and return its blob is successful, or null if not. */ -export const downloadFile = (file: UploadedFile) => - fetch(file.fileUrl).then((response) => { - if (!response.ok) { - // This is necessary because some of the error such as 404 does not throw. - // Due to that inconsistency, I have decide to share catch statement like this. - throw new Error(response.statusText); - } - return response.blob(); - }); +export const downloadFile = (file: UploadedFile) => fetch(file.fileUrl).then((response) => { + if (!response.ok) { + // This is necessary because some of the error such as 404 does not throw. + // Due to that inconsistency, I have decide to share catch statement like this. + throw new Error(response.statusText); + } + return response.blob(); +}); /** * Download blobs given file objects. Returns a promise map. @@ -92,23 +70,43 @@ export const downloadBlobs = async (files: UploadedFile[]) => { const blobs: Blob[] = []; const errors: string[] = []; + const promises: Promise[] = []; // eslint-disable-next-line no-restricted-syntax for (const file of files) { try { - // eslint-disable-next-line no-await-in-loop - blobs.push(await downloadFile(file)); + promises.push(downloadFile(file).then(blobs.push)); } catch (error) { errors.push(file.fileName); } } + await Promise.all(promises); if (errors.length) { throw DownloadException(errors); } return { blobs, files }; }; +export const transforms: ({ [k: string]: any }) = { + loadResponse: (oldData, response) => ({ ...oldData, response }), +}; +transforms.loadFiles = (oldData, uploadedFiles) => transforms.loadResponse( + oldData, + { ...oldData.response, uploadedFiles }, +); +transforms.deleteFile = (oldData, index) => { + const { uploadedFiles } = oldData.response; + return uploadedFiles + ? transforms.loadFiles(oldData, uploadedFiles.filter(f => f.fileIndex !== index)) + : oldData; +}; +transforms.addFile = (oldData, addedFile) => { + const { uploadedFiles } = oldData.response; + return uploadedFiles + ? transforms.loadFiles(oldData, [...uploadedFiles, addedFile]) + : oldData; +}; + export const useUploadFiles = () => { - const testDataPath = useTestDataPath(); const addFile = api.useAddFile(); const queryClient = useQueryClient(); const apiFn = (data) => { @@ -116,66 +114,32 @@ export const useUploadFiles = () => { const file = fileData.getAll('file')[0]; return addFile(file, description).then(addedFile => { queryClient.setQueryData( - [queryKeys.pageData, testDataPath], - (oldData: PageData) => ({ - ...oldData, - response: { - ...oldData.response, - uploadedFiles: oldData.response.uploadedFiles - ? [...oldData.response.uploadedFiles, addedFile] - : oldData.response.uploadedFiles, - }, - }), + [queryKeys.pageData], + (oldData: PageData) => transforms.addFile(oldData, addedFile), ); }); - ; - }; - const mockFn = (data, description) => { - const { fileData, requestConfig } = data; - return fakeProgress(requestConfig); }; - return useMutation({ - mutationFn: testDataPath ? mockFn : apiFn, - }); + return useMutation({ mutationFn: apiFn }); }; export const useDeleteFile = () => { - const testDataPath = useTestDataPath(); const deleteFile = api.useDeleteFile(); const queryClient = useQueryClient(); const apiFn = (index) => { console.log({ deleteFile: index }); return deleteFile(index).then(() => { queryClient.setQueryData( - [queryKeys.pageData, testDataPath], - (oldData: PageData) => ({ - ...oldData, - response: { - ...oldData.response, - uploadedFiles: oldData.response.uploadedFiles - ? oldData.response.uploadedFiles.filter(f => f.fileIndex !== index) - : oldData.response.uploadedFiles, - }, - }), + [queryKeys.pageData], + (oldData: PageData) => transforms.deleteFile(oldData, index), ); }); }; - const mockFn = (data) => Promise.resolve(data); - return useMutation({ - mutationFn: testDataPath ? mockFn : apiFn, - }); + return useMutation({ mutationFn: apiFn }); }; -export const useDownloadFiles = () => - useCreateMutationAction( - async ({ - files, - zipFileName, - }: { - files: UploadedFile[]; - zipFileName: string; - }) => { - const { blobs } = await downloadBlobs(files); - return zipFiles(files, blobs, zipFileName); - } - ); +export const useDownloadFiles = () => useMutation({ + mutationFn: async ({ files, zipFileName }: { files: UploadedFile[], zipFileName: string }) => { + const { blobs } = await downloadBlobs(files); + return zipFiles(files, blobs, zipFileName); + }, +}); From 8e2ca673d66813c65258907c41d50b46e31ee3a8 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Fri, 1 Dec 2023 19:01:24 +0000 Subject: [PATCH 58/66] fix: remove in-progress status alert --- src/components/StatusAlert/messages.js | 40 ------------------- .../StatusAlert/useStatusAlertData.jsx | 4 +- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/src/components/StatusAlert/messages.js b/src/components/StatusAlert/messages.js index a4c33341..07cbca4f 100644 --- a/src/components/StatusAlert/messages.js +++ b/src/components/StatusAlert/messages.js @@ -2,11 +2,6 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; import { stepNames, stepStates } from 'constants'; const submissionAlerts = defineMessages({ - [stepStates.inProgress]: { - id: 'frontend-app-ora.StatusAlert.submission.inProgress', - defaultMessage: "This assignment has several steps. In the first step you'll provide a response to the prompt.", - description: 'Submission in-progress status alert', - }, [stepStates.submitted]: { id: 'frontend-app-ora.StatusAlert.submission.submitted', defaultMessage: 'Your response has been submitted. You will receive your grade after all steps are complete and your response is fully assessed.', @@ -44,11 +39,6 @@ const submissionAlerts = defineMessages({ }, }); const submissionHeadings = defineMessages({ - [stepStates.inProgress]: { - id: 'frontend-app-ora.StatusAlert.Heading.submission.inProgress', - defaultMessage: 'Submission In Progress', - description: 'Submission in-progress status alert heading', - }, [stepStates.submitted]: { id: 'frontend-app-ora.StatusAlert.Heading.submission.submitted', defaultMessage: 'Submission Completed', @@ -87,11 +77,6 @@ const submissionHeadings = defineMessages({ }); const studentTrainingAlerts = defineMessages({ - [stepStates.inProgress]: { - id: 'frontend-app-ora.StatusAlert.studentTraining.inProgress', - defaultMessage: 'This assignment is in progress. Complete the learner training step to move on.', - description: 'Student Training in progress status alert', - }, [stepStates.submitted]: { id: 'frontend-app-ora.StatusAlert.studentTraining.submitted', defaultMessage: 'You have completed this practice grading example. Continue to the next example, or if you have completed all examples, continue to the next step.', @@ -104,11 +89,6 @@ const studentTrainingAlerts = defineMessages({ }, }); const studentTrainingHeadings = defineMessages({ - [stepStates.inProgress]: { - id: 'frontend-app-ora.StatusAlert.Heading.studentTraining.inProgress', - defaultMessage: 'Student Training: In Progress', - description: 'Student Training in progress status alert heading', - }, [stepStates.submitted]: { id: 'frontend-app-ora.StatusAlert.Heading.studentTraining.submitted', defaultMessage: 'Student Training: Submitted', @@ -117,11 +97,6 @@ const studentTrainingHeadings = defineMessages({ }); const selfAlerts = defineMessages({ - [stepStates.inProgress]: { - id: 'frontend-app-ora.StatusAlert.self.inProgress', - defaultMessage: 'This assignment is in progress. You still need to complete the self assessment step.', - description: 'Student Training in progress status alert', - }, [stepStates.closed]: { id: 'frontend-app-ora.StatusAlert.self.closed', defaultMessage: 'The due date for this step has passed. This step is now closed. You can no longer complete a self assessment or continue with this asseignment, and you will receive a grade of inccomplete', @@ -139,11 +114,6 @@ const selfHeadings = defineMessages({ defaultMessage: 'Self Assessment Completed', description: 'Self Assessment submitted status alert heading', }, - [stepStates.inProgress]: { - id: 'frontend-app-ora.StatusAlert.Heading.self.inProgress', - defaultMessage: 'Self Assessment In Progress', - description: 'Student Training in progress status alert heading', - }, [stepStates.closed]: { id: 'frontend-app-ora.StatusAlert.Heading.self.closed', defaultMessage: 'Self Assessment: Closed', @@ -152,11 +122,6 @@ const selfHeadings = defineMessages({ }); const peerAlerts = defineMessages({ - [stepStates.inProgress]: { - id: 'frontend-app-ora.StatusAlert.peer.inProgress', - defaultMessage: 'This assignment is in progress. You still need to complete the peer assessment step.', - description: 'Peer Assessment closed status alert', - }, [stepStates.waiting]: { id: 'frontend-app-ora.StatusAlert.peer.waiting', defaultMessage: 'All submitted responses have been assessed. Check back later to see if more learners have submitted responses.', @@ -184,11 +149,6 @@ const peerAlerts = defineMessages({ }, }); const peerHeadings = defineMessages({ - [stepStates.inProgress]: { - id: 'frontend-app-ora.StatusAlert.Heading.peer.inProgress', - defaultMessage: 'Peer Assessment In Progress', - description: 'Peer Assessment closed status alert heading', - }, [stepStates.waiting]: { id: 'frontend-app-ora.StatusAlert.Heading.peer.waiting', defaultMessage: 'Waiting for peers to submit', diff --git a/src/components/StatusAlert/useStatusAlertData.jsx b/src/components/StatusAlert/useStatusAlertData.jsx index 36e726ae..dc0237e1 100644 --- a/src/components/StatusAlert/useStatusAlertData.jsx +++ b/src/components/StatusAlert/useStatusAlertData.jsx @@ -41,7 +41,6 @@ export const alertMap = { [stepStates.needTeam]: alertTypes.warning, [stepStates.waiting]: alertTypes.warning, [stepStates.cancelled]: alertTypes.warning, - [stepStates.inProgress]: alertTypes.dark, [stepStates.notAvailable]: alertTypes.light, }; @@ -64,6 +63,9 @@ const useStatusAlertData = ({ const stepName = step || activeStepName; const isRevisit = stepName !== activeStepName; + if (stepState === stepStates.inProgress) { + return []; + } const { variant, icon } = alertMap[stepState]; const alertConfig = ({ From fcdd9683d6ba924ecb265e6c20c08856c6ea95e7 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Fri, 1 Dec 2023 19:35:19 +0000 Subject: [PATCH 59/66] chore: tinymce background color --- .../SubmissionView/TextResponseEditor/RichTextEditor.jsx | 3 ++- src/views/SubmissionView/index.scss | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx index beecfc4b..328cdaf7 100644 --- a/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx +++ b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx @@ -32,11 +32,12 @@ const RichTextEditor = ({ }; return ( -
+
Date: Mon, 4 Dec 2023 15:43:57 +0000 Subject: [PATCH 60/66] fix: integration fixes --- src/components/ActionButton.jsx | 22 ++ src/components/AppContainer.jsx | 2 +- src/components/ModalActions/index.jsx | 15 +- src/components/ModalContainer.jsx | 4 +- .../Prompt/__snapshots__/index.test.jsx.snap | 33 --- src/components/Prompt/index.test.jsx | 34 --- src/components/Prompt/messages.js | 2 +- src/components/StatusAlert/hooks/constants.js | 23 ++ .../StatusAlert/hooks/simpleAlerts.js | 47 ++++ .../StatusAlert/hooks/useCancelledAlerts.js | 28 ++ .../StatusAlert/hooks/useCreateAlert.js | 28 ++ .../StatusAlert/hooks/useModalAlerts.js | 47 ++++ .../StatusAlert/hooks/useModalAlerts.jsx | 249 ++++++++++++++++++ .../StatusAlert/hooks/useRevisitAlerts.js | 25 ++ .../StatusAlert/hooks/useStatusAlertData.jsx | 58 ++++ .../StatusAlert/hooks/useSuccessAlerts.js | 44 ++++ src/components/StatusAlert/index.jsx | 7 +- src/components/StatusAlert/messages.js | 4 +- .../StatusAlert/useStatusAlertData.jsx | 168 ------------ .../services/lms/hooks/selectors/index.ts | 4 +- .../services/lms/hooks/selectors/oraConfig.ts | 2 +- src/hooks/actions/messages.js | 2 +- .../BaseAssessmentView/index.jsx | 2 + .../RichTextEditor.test.jsx.snap | 9 +- 24 files changed, 598 insertions(+), 261 deletions(-) create mode 100644 src/components/ActionButton.jsx delete mode 100644 src/components/Prompt/__snapshots__/index.test.jsx.snap delete mode 100644 src/components/Prompt/index.test.jsx create mode 100644 src/components/StatusAlert/hooks/constants.js create mode 100644 src/components/StatusAlert/hooks/simpleAlerts.js create mode 100644 src/components/StatusAlert/hooks/useCancelledAlerts.js create mode 100644 src/components/StatusAlert/hooks/useCreateAlert.js create mode 100644 src/components/StatusAlert/hooks/useModalAlerts.js create mode 100644 src/components/StatusAlert/hooks/useModalAlerts.jsx create mode 100644 src/components/StatusAlert/hooks/useRevisitAlerts.js create mode 100644 src/components/StatusAlert/hooks/useStatusAlertData.jsx create mode 100644 src/components/StatusAlert/hooks/useSuccessAlerts.js delete mode 100644 src/components/StatusAlert/useStatusAlertData.jsx diff --git a/src/components/ActionButton.jsx b/src/components/ActionButton.jsx new file mode 100644 index 00000000..4d6f63b7 --- /dev/null +++ b/src/components/ActionButton.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Button, StatefulButton } from '@edx/paragon'; + +import { MutationStatus } from 'constants'; + +export const disabledStates = [MutationStatus.loading]; + +const ActionButton = (props) => ( + props.state + ? + : , - ], - })); - } - if (activeStepName === stepNames.staff) { - out.push(alertConfig({ - message: messages.alerts[activeStepName].staffAssessment, - heading: messages.headings[activeStepName].staffAssessment, - actions: [ - , - ], - })); - } - return out; - } - } - if (cancellationInfo.hasCancelled) { - const { cancelledBy, cancelledAt } = cancellationInfo; - if (cancelledBy) { - return [alertConfig({ - message: messages.alerts.submission.cancelledBy, - messageVals: { cancelledBy, cancelledAt }, - heading: messages.headings.submission.cancelledBy, - })]; - } - return [alertConfig({ - message: messages.alerts.submission.cancelledAt, - messageVals: { cancelledAt }, - heading: messages.headings.submission.cancelledAt, - })]; - } - if (stepName === stepNames.submission && isRevisit) { - return [alertConfig({ - message: messages.alerts.submission.finished, - heading: messages.headings.submission.finished, - })]; - } - if (stepName === stepNames.peer && isRevisit && stepState !== stepStates.waiting) { - return [alertConfig({ - message: messages.alerts.peer.finished, - heading: messages.headings.peer.finished, - })]; - } - if (stepName === stepNames.staff) { - return [alertConfig({ - message: messages.alerts[activeStepName].staffAssessment, - heading: messages.headings[activeStepName].staffAssessment, - })]; - } - return [alertConfig({ - message: messages.alerts[stepName][stepState], - heading: messages.headings[stepName][stepState], - })]; -}; - -export default useStatusAlertData; diff --git a/src/data/services/lms/hooks/selectors/index.ts b/src/data/services/lms/hooks/selectors/index.ts index 18ab17d8..61b2f667 100644 --- a/src/data/services/lms/hooks/selectors/index.ts +++ b/src/data/services/lms/hooks/selectors/index.ts @@ -4,9 +4,7 @@ import { stepNames, closedReasons, stepStates, - globalStates, -} from 'constants'; -import { useViewStep } from 'hooks/routing'; +} from 'constants/index'; import * as oraConfigSelectors from './oraConfig'; import * as pageDataSelectors from './pageData'; diff --git a/src/data/services/lms/hooks/selectors/oraConfig.ts b/src/data/services/lms/hooks/selectors/oraConfig.ts index 81142209..2a000fec 100644 --- a/src/data/services/lms/hooks/selectors/oraConfig.ts +++ b/src/data/services/lms/hooks/selectors/oraConfig.ts @@ -1,7 +1,7 @@ import React from 'react'; import * as data from 'data/services/lms/hooks/data'; import * as types from 'data/services/lms/types'; -import { stepNames } from 'constants'; +import { stepNames } from 'constants/index'; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ORA Config Data diff --git a/src/hooks/actions/messages.js b/src/hooks/actions/messages.js index 9481151b..cab576d4 100644 --- a/src/hooks/actions/messages.js +++ b/src/hooks/actions/messages.js @@ -34,7 +34,7 @@ const messages = defineMessages({ }, loadNext: { id: 'ora-mfe.ModalActions.loadNext', - defaultMessage: 'Load next', + defaultMessage: 'Grade next', description: 'Action button to load next peer response', }, loadingNext: { diff --git a/src/views/AssessmentView/BaseAssessmentView/index.jsx b/src/views/AssessmentView/BaseAssessmentView/index.jsx index ac671166..64ee27ed 100644 --- a/src/views/AssessmentView/BaseAssessmentView/index.jsx +++ b/src/views/AssessmentView/BaseAssessmentView/index.jsx @@ -8,6 +8,7 @@ import { useShowTrainingError } from 'hooks/assessment'; import { useViewStep } from 'hooks/routing'; import Assessment from 'components/Assessment'; +import Instructions from 'components/Instructions'; import ModalActions from 'components/ModalActions'; import StatusAlert from 'components/StatusAlert'; import StepProgressIndicator from 'components/StepProgressIndicator'; @@ -30,6 +31,7 @@ const BaseAssessmentView = ({ + {children} diff --git a/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap b/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap index 1048ca16..a7c71025 100644 --- a/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap +++ b/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap @@ -2,7 +2,7 @@ exports[` render disabled 1`] = `
render disabled 1`] = ` exports[` render optional 1`] = `
render optional 1`] = ` exports[` render required 1`] = `
Date: Mon, 4 Dec 2023 16:22:11 -0500 Subject: [PATCH 61/66] Responsive ui for ora (#97) * chore: fix assessment width out of proportion for long feedback * chore: fix typo * chore: update progress bar responsiveness * chore: fix active step for xblock view * chore: make base assessment responsive * chore: update linting --- src/App.jsx | 2 +- src/components/Assessment/Assessment.scss | 1 + .../EditableAssessment/AssessmentActions.jsx | 2 +- .../Assessment/ReadonlyAssessment/messages.js | 2 +- src/components/Assessment/messages.js | 2 +- src/components/CriterionContainer/messages.js | 2 +- src/components/Instructions/messages.js | 2 +- src/components/ProgressBar/hooks.js | 4 +- src/components/ProgressBar/index.jsx | 41 ++- src/components/ProgressBar/index.scss | 21 ++ src/components/Rubric/messages.js | 2 +- .../StatusAlert/hooks/useModalAlerts.jsx | 249 ------------------ .../StatusAlert/hooks/useStatusAlertData.jsx | 2 +- src/components/StatusAlert/messages.js | 12 +- .../lms/fakeData/pageData/progress.js | 2 +- .../services/lms/hooks/actions/utils.test.ts | 6 +- .../BaseAssessmentView.scss | 13 +- .../BaseAssessmentView/index.jsx | 8 +- src/views/GradeView/index.jsx | 38 +-- src/views/SubmissionView/index.scss | 2 +- src/views/XBlockView/index.scss | 4 +- 21 files changed, 114 insertions(+), 303 deletions(-) delete mode 100644 src/components/StatusAlert/hooks/useModalAlerts.jsx diff --git a/src/App.jsx b/src/App.jsx index 4c27c18c..40f069e6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -71,7 +71,7 @@ const App = () => { modalRoute(routes.selfAssessmentEmbed, SelfAssessmentView, 'ORA Self Assessment'), modalRoute(routes.studentTrainingEmbed, StudentTrainingView, 'ORA Student Training'), modalRoute(routes.submissionEmbed, SubmissionView, 'ORA Submission'), - modaleoute(routes.gradedEmbed, GradeView, 'My Grade'), + modalRoute(routes.gradedEmbed, GradeView, 'My Grade'), - { const { xblockId, courseId } = useParams(); const isEmbedded = useIsEmbedded(); const viewStep = useViewStep(); - const { effectiveGrade, stepState } = useGlobalState({ step }); + const { effectiveGrade, stepState, activeStepName } = useGlobalState({ step }); const stepInfo = useStepInfo(); const openModal = useOpenModal(); @@ -17,7 +17,7 @@ export const useProgressStepData = ({ step, canRevisit = false }) => { isEmbedded ? '/embedded' : '' }/${courseId}/${xblockId}`; const onClick = () => openModal({ view: step, title: step }); - const isActive = viewStep === step; + const isActive = viewStep === stepNames.xblock ? activeStepName === step : viewStep === step; let isEnabled = isActive || stepState === stepStates.inProgress || (canRevisit && stepState === stepStates.done); if (step === stepNames.peer) { diff --git a/src/components/ProgressBar/index.jsx b/src/components/ProgressBar/index.jsx index 8207a32d..e17bb89c 100644 --- a/src/components/ProgressBar/index.jsx +++ b/src/components/ProgressBar/index.jsx @@ -3,15 +3,18 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { Navbar } from '@edx/paragon'; +import { Navbar, Icon } from '@edx/paragon'; +import { ArrowDropUpDown, ArrowForwardIos } from '@edx/paragon/icons'; import { useAssessmentStepOrder, + useGlobalState, useHasReceivedFinalGrade, useIsPageDataLoaded, } from 'hooks/app'; import { stepNames } from 'constants'; +import { useViewStep } from 'hooks/routing'; import ProgressStep from './ProgressStep'; import messages from './messages'; @@ -37,6 +40,9 @@ export const ProgressBar = ({ className }) => { const isLoaded = useIsPageDataLoaded(); const hasReceivedFinalGrade = useHasReceivedFinalGrade(); + const activeStep = useViewStep(); + const { activeStepName } = useGlobalState(); + const stepOrders = [ stepNames.submission, ...useAssessmentStepOrder(), @@ -48,18 +54,31 @@ export const ProgressBar = ({ className }) => { return null; } - const stepEl = (curStep) => (stepLabels[curStep] - ? ( - - ) : null); + const stepEl = (curStep) => (stepLabels[curStep] ? ( + + ) : null); + + const activeStepTitle = activeStep === stepNames.xblock ? activeStepName : activeStep; return ( - + + +
+ + + {formatMessage(stepLabels[activeStepTitle])} + + +
+

{stepOrders.map(stepEl)} diff --git a/src/components/ProgressBar/index.scss b/src/components/ProgressBar/index.scss index c527f87b..90e4493a 100644 --- a/src/components/ProgressBar/index.scss +++ b/src/components/ProgressBar/index.scss @@ -1,6 +1,9 @@ +@import '@edx/paragon/scss/core/core'; + .ora-progress-nav-group { width: 100%; display: flex; + flex-direction: row; justify-content: space-between; .ora-progress-divider { position: absolute; @@ -31,3 +34,21 @@ border-bottom: 2px solid black; } } + +@include media-breakpoint-down(sm) { + .ora-progress-nav-group { + flex-direction: column; + align-items: start; + border-top: 1px solid black; + padding: 0 map-get($spacers, 3); + + .ora-progress-divider { + display: none; + } + + .ora-progress-nav { + width: 100%; + margin: 0 0.25rem; + } + } +} diff --git a/src/components/Rubric/messages.js b/src/components/Rubric/messages.js index 24fa43fc..ce64aef7 100644 --- a/src/components/Rubric/messages.js +++ b/src/components/Rubric/messages.js @@ -24,7 +24,7 @@ const messages = defineMessages({ overallComments: { id: 'frontend-app-ora.Rubric.overallComments', defaultMessage: 'Overall comments', - description: 'Rubric overall commnents label', + description: 'Rubric overall comments label', }, addComments: { id: 'frontend-app-ora.Rubric.addComments', diff --git a/src/components/StatusAlert/hooks/useModalAlerts.jsx b/src/components/StatusAlert/hooks/useModalAlerts.jsx deleted file mode 100644 index 0a00a31f..00000000 --- a/src/components/StatusAlert/hooks/useModalAlerts.jsx +++ /dev/null @@ -1,249 +0,0 @@ -import { useIntl } from '@edx/frontend-platform/i18n'; -import { CheckCircle, Info, WarningFilled } from '@edx/paragon/icons'; - -import { useViewStep } from 'hooks/routing'; -import { - useActiveStepName, - useGlobalState, - useHasReceivedFinalGrade, - useStepState, -} from 'hooks/app'; -import { useHasSubmitted } from 'hooks/assessment'; -import { useExitAction, useStartStepAction } from 'hooks/actions'; - -import { stepNames, stepStates } from 'constants'; - -import messages from './messages'; - -const alertTypes = { - success: { variant: 'success', icon: CheckCircle }, - danger: { variant: 'danger', icon: Info }, - warning: { variant: 'warning', icon: WarningFilled }, - light: { variant: 'light', icon: null }, - dark: { variant: 'dark', icon: null }, -}; - -export const alertMap = { - [stepStates.done]: alertTypes.success, - [stepStates.submitted]: alertTypes.success, - [stepStates.closed]: alertTypes.danger, - [stepStates.teamAlreadySubmitted]: alertTypes.warning, - [stepStates.needTeam]: alertTypes.warning, - [stepStates.waiting]: alertTypes.warning, - [stepStates.cancelled]: alertTypes.warning, - [stepStates.notAvailable]: alertTypes.light, - [stepStates.inProgress]: alertTypes.dark, -}; - -export const useAlertConfig = ({ step }) => alertMap[useStepState({ step })]; - -export const useCreateAlert = ({ step }) => { - const { formatMessage } = useIntl(); - const alertConfig = useAlertConfig({ step }); - return ({ - heading, - message, - actions, - headingVals = {}, - messageVals = {}, - ...overrides - }) => ({ - ...alertConfig, - message: formatMessage(message, messageVals), - heading: heading && formatMessage(heading, headingVals), - actions, - ...overrides, - }); -}; - -export const useGradedAlerts = ({ step }) => ([ - useAlertConfig({ step })({ - message: messages.alerts.done.status, - heading: messages.headings.done.status, - }), -]); - -export const useTrainingErrorAlerts = ({ step }) => ([ - useAlertConfig({ step })({ - message: messages.alerts.studentTraining[stepStates.trainingValidation], - variant: 'warning', - }), -]); - -export const useStaffAlerts = ({ step }) => { - const activeStepName = useActiveStepName(); - return [ - useAlertConfig({ step })({ - message: messages.alerts[activeStepName].staffAssessment, - heading: messages.headings[activeStepName].staffAssessment, - }), - ]; -}; - -export const useCreateFinishedAlert = ({ step }) => { - const createAlert = useCreateAlert({ step }); - return (target) => createAlert({ - message: messages.alerts[target].finished, - heading: messages.headings[target].finished, - }); -}; - -export const useCreateExitAlert = ({ step }) => { - const createAlert = useCreateAlert({ step }); - const exitAction = useExitAction(); - const activeStepName = useActiveStepName(); - return (target) => createAlert({ - message: messages.alerts[activeStepName][target], - heading: messages.headings[activeStepName][target], - actions: [exitAction], - }); -}; - -export const useRevisitAlerts = ({ step }) => { - const { activeStepName, stepState } = useGlobalState({ step }); - const viewStep = useViewStep(); - const stepName = step || activeStepName; - const finishedAlert = useCreateFinishedAlert({ step }); - const isRevisit = viewStep !== stepNames.xblock && stepName !== activeStepName; - let out = []; - if (isRevisit) { - if (stepName === stepNames.submission) { - out = [finishedAlert(stepNames.submission)]; - } else if (stepName === stepNames.peer && stepState !== stepStates.waiting) { - out = [finishedAlert(stepNames.peer)]; - } - } - return { revisitAlerts: out, isRevisit }; -}; - -export const useSuccessAlerts = ({ step }) => { - const { activeStepName, activeStepState } = useGlobalState({ step }); - const viewStep = useViewStep(); - const hasSubmitted = useHasSubmitted(); - const startStepAction = useStartStepAction(); - const exitAlert = useCreateExitAlert({ step }); - - const createAlert = useCreateAlert({ step }); - - const out = []; - if (hasSubmitted) { - const successAlert = { - message: messages.alerts[viewStep].submitted, - heading: messages.headings[viewStep].submitted, - ...alertTypes.success, - }; - if (activeStepState === stepStates.inProgress && activeStepName !== viewStep) { - successAlert.actions = [startStepAction]; - } - out.push(createAlert(successAlert)); - - if (activeStepState !== stepStates.inProgress) { - out.push(exitAlert(activeStepState)); - } - if (activeStepName === stepNames.staff) { - out.push(exitAlert(stepNames.staff)); - } - return out; - } - return null; -}; - -export const useCancelledAlerts = ({ step }) => { - const alertConfig = useAlertConfig({ step }); - return (cancellationInfo) => { - let out = null; - const { - hasCancelled, - cancelledBy, - cancelledAt, - } = cancellationInfo; - const alertMessages = messages.alerts.submission; - const headingMessages = messages.headings.submission; - if (hasCancelled) { - out = [ - alertConfig({ - message: cancelledBy ? alertMessages.cancelledBy : alertMessages.cancelledAt, - heading: cancelledBy ? headingMeadings.cancelledBy : headingMessages.cancelledAt, - messageVals: { cancelledAt, cancelledBy }, - }), - ]; - } - return { cancelledAlerts: out, hasCancelled }; -}; - -export const useModalAlerts = ({ step, showTrainingError }) => { - const { stepState } = useGlobalState({ step }); - const isDone = useHasReceivedFinalGrade(); - const viewStep = useViewStep(); - const hasSubmitted = useHasSubmitted(); - const { revisitAlerts, isRevisit } = useRevisitAlerts({ step }); - const trainingErrorAlerts = useTrainingErrorAlerts({ step }); - const successAlerts = useSuccessAlerts({ step }); - - // Do nothing if in xblock view - if (viewStep === stepNames.xblock) { - return null; - } - - // No in-progress messages unless for submitted step - if (stepState === stepStates.inProgress && !hasSubmitted) { - return null; - } - // No modal alerts for graded state - if (isDone) { - return null; - } - - if (isRevisit) { - return revisitAlerts; - } - if (showTrainingError) { - return trainingErrorAlerts; - } - if (hasSubmitted) { - return successAlerts; - } - return null; -}; - - -const useStatusAlertData = ({ - step = null, - showTrainingError, -}) => { - const { - activeStepName, - stepState, - } = useGlobalState({ step }); - const isDone = useHasReceivedFinalGrade(); - const viewStep = useViewStep(); - - const createAlert = useCreateAlert({ step }); - const modalAlerts = useModalAlerts({ step, showTrainingError }); - const gradedAlerts = useGradedAlerts({ step }); - const { hasCancelled, cancelledAlerts } = useCancelledAlerts({ step }); - const staffAlerts = useStaffAlerts({ step }); - - const stepName = step || activeStepName; - - if (isDone) { - return gradedAlerts; - } - if (viewStep !== stepNames.xblock) { - return modalAlerts; - } - if (hasCancelled) { - return cancelledAlerts; - } - - if (stepName === stepNames.staff) { - return staffAlerts; - } - - return [createAlert({ - message: messages.alerts[stepName][stepState], - heading: messages.headings[stepName][stepState], - })]; -}; - -export default useStatusAlertData; diff --git a/src/components/StatusAlert/hooks/useStatusAlertData.jsx b/src/components/StatusAlert/hooks/useStatusAlertData.jsx index d42674de..2d204964 100644 --- a/src/components/StatusAlert/hooks/useStatusAlertData.jsx +++ b/src/components/StatusAlert/hooks/useStatusAlertData.jsx @@ -48,7 +48,7 @@ const useStatusAlertData = ({ if (stepState === stepStates.inProgress) { return []; } - + return [createAlert({ message: messages.alerts[stepName][stepState], heading: messages.headings[stepName][stepState], diff --git a/src/components/StatusAlert/messages.js b/src/components/StatusAlert/messages.js index 32be1db3..5cf1eefe 100644 --- a/src/components/StatusAlert/messages.js +++ b/src/components/StatusAlert/messages.js @@ -10,7 +10,7 @@ const submissionAlerts = defineMessages({ [stepStates.notAvailable]: { id: 'frontend-app-ora.StatusAlert.submission.notAvailable', defaultMessage: 'This task is not available yet. Check back to complete the assignment once this section has opened', - description: 'Submission not avilable status alert', + description: 'Submission not available status alert', }, [stepStates.cancelled]: { id: 'frontend-app-ora.StatusAlert.submission.cancelled', @@ -47,7 +47,7 @@ const submissionHeadings = defineMessages({ [stepStates.notAvailable]: { id: 'frontend-app-ora.StatusAlert.Heading.submission.notAvailable', defaultMessage: 'Submission Not Available', - description: 'Submission not avilable status alert heading', + description: 'Submission not available status alert heading', }, [stepStates.cancelled]: { id: 'frontend-app-ora.StatusAlert.Heading.submission.cancelled', @@ -99,7 +99,7 @@ const studentTrainingHeadings = defineMessages({ const selfAlerts = defineMessages({ [stepStates.closed]: { id: 'frontend-app-ora.StatusAlert.self.closed', - defaultMessage: 'The due date for this step has passed. This step is now closed. You can no longer complete a self assessment or continue with this asseignment, and you will receive a grade of inccomplete', + defaultMessage: 'The due date for this step has passed. This step is now closed. You can no longer complete a self assessment or continue with this assignment, and you will receive a grade of incomplete', description: 'Student Training closed status alert', }, [stepStates.submitted]: { @@ -144,7 +144,7 @@ const peerAlerts = defineMessages({ }, [stepStates.submitted]: { id: 'frontend-app-ora.StatusAlert.peer.submitted', - defaultMessage: 'Continue to submite peer assessments until you have completed the required number.', + defaultMessage: 'Continue to submit peer assessments until you have completed the required number.', description: 'Peer Assessment submitted status alert', }, }); @@ -167,7 +167,7 @@ const peerHeadings = defineMessages({ [stepStates.notAvailable]: { id: 'frontend-app-ora.StatusAlert.Heading.peer.notAvailable', defaultMessage: 'Peer Assessment not available', - description: 'Peer Assessment not avilable status alert heading', + description: 'Peer Assessment not available status alert heading', }, [stepStates.submitted]: { id: 'frontend-app-ora.StatusAlert.Heading.peer.submitted', @@ -186,7 +186,7 @@ const doneAlerts = defineMessages({ const doneHeadings = defineMessages({ status: { id: 'frontend-app-ora.StatusAlert.Heading.done', - defaultMessage: 'Assignment Ccomplete and Graded', + defaultMessage: 'Assignment Complete and Graded', description: 'Done status alert heading', }, }); diff --git a/src/data/services/lms/fakeData/pageData/progress.js b/src/data/services/lms/fakeData/pageData/progress.js index d7d11bda..11992e55 100644 --- a/src/data/services/lms/fakeData/pageData/progress.js +++ b/src/data/services/lms/fakeData/pageData/progress.js @@ -227,7 +227,7 @@ export const getProgressState = ({ viewStep, progressKey, stepConfig }) => { [progressKeys.graded]: createProgressData(stepConfig[stepConfig.length - 1], { isGraded: true }), [progressKeys.gradedSubmittedOnPreviousTeam]: - createProgressData(stepConfig[stepConfig.length - 1], { isGrdaed: true }), + createProgressData(stepConfig[stepConfig.length - 1], { isGraded: true }), }); return mapping[progressKey]; }; diff --git a/src/data/services/lms/hooks/actions/utils.test.ts b/src/data/services/lms/hooks/actions/utils.test.ts index f7909423..c61ca755 100644 --- a/src/data/services/lms/hooks/actions/utils.test.ts +++ b/src/data/services/lms/hooks/actions/utils.test.ts @@ -19,11 +19,11 @@ describe.skip('actions', () => { describe('createMutationAction', () => { it('returns a mutation function', () => { - const aribtraryMutationFn = jest.fn(); - const mutation = useCreateMutationAction(aribtraryMutationFn) as any; + const arbitraryMutationFn = jest.fn(); + const mutation = useCreateMutationAction(arbitraryMutationFn) as any; mutation.mutate('foo', 'bar'); - expect(aribtraryMutationFn).toHaveBeenCalledWith('foo', 'bar', queryClient); + expect(arbitraryMutationFn).toHaveBeenCalledWith('foo', 'bar', queryClient); }); }); }); diff --git a/src/views/AssessmentView/BaseAssessmentView/BaseAssessmentView.scss b/src/views/AssessmentView/BaseAssessmentView/BaseAssessmentView.scss index 01920d70..4481dae6 100644 --- a/src/views/AssessmentView/BaseAssessmentView/BaseAssessmentView.scss +++ b/src/views/AssessmentView/BaseAssessmentView/BaseAssessmentView.scss @@ -12,9 +12,12 @@ height: 100%; .content-wrapper { - min-width: min-content; + max-width: $max-width-lg; } + .assessment-col { + min-width: 300px; + } } @include media-breakpoint-down(sm) { @@ -22,6 +25,14 @@ .content-wrapper { width: 100%; } + + .content-body { + flex-direction: column; + + .assessment-col { + max-width: 100%; + } + } } } diff --git a/src/views/AssessmentView/BaseAssessmentView/index.jsx b/src/views/AssessmentView/BaseAssessmentView/index.jsx index 64ee27ed..b80b49e1 100644 --- a/src/views/AssessmentView/BaseAssessmentView/index.jsx +++ b/src/views/AssessmentView/BaseAssessmentView/index.jsx @@ -15,6 +15,8 @@ import StepProgressIndicator from 'components/StepProgressIndicator'; import messages from '../messages'; +import './BaseAssessmentView.scss'; + const BaseAssessmentView = ({ children, }) => { @@ -29,13 +31,15 @@ const BaseAssessmentView = ({

{formatMessage(messages[step])}

- + {children} - + + +
diff --git a/src/views/GradeView/index.jsx b/src/views/GradeView/index.jsx index fd705bea..8e63e7c6 100644 --- a/src/views/GradeView/index.jsx +++ b/src/views/GradeView/index.jsx @@ -12,24 +12,26 @@ import Content from './Content'; import './index.scss'; const GradeView = () => ( -
- - - - - - - - - +
+
+ + + + + + + + + +
); GradeView.defaultProps = {}; diff --git a/src/views/SubmissionView/index.scss b/src/views/SubmissionView/index.scss index 93387344..106fe5ee 100644 --- a/src/views/SubmissionView/index.scss +++ b/src/views/SubmissionView/index.scss @@ -12,7 +12,7 @@ height: 100%; .content-wrapper { - min-width: min-content; + max-width: $max-width-lg; } } diff --git a/src/views/XBlockView/index.scss b/src/views/XBlockView/index.scss index 8fe83a09..fedcb49f 100644 --- a/src/views/XBlockView/index.scss +++ b/src/views/XBlockView/index.scss @@ -1,6 +1,8 @@ +@import "@edx/paragon/scss/core/core"; + #ora-xblock-view { max-width: 1024px; - @media (min-width: 830px) { + @include media-breakpoint-down(md) { padding-left: 40px; padding-right: 40px; } From 3b493af41b745609949a5a24c90d1b8bfd1e0a8c Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Wed, 6 Dec 2023 11:43:00 -0500 Subject: [PATCH 62/66] chore: fix tinymce modal transparent problem --- .../TextResponseEditor/RichTextEditor.jsx | 9 ++++++--- .../__snapshots__/RichTextEditor.test.jsx.snap | 15 +++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx index 328cdaf7..9ea3db27 100644 --- a/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx +++ b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx @@ -9,7 +9,6 @@ import 'tinymce/plugins/lists'; import 'tinymce/plugins/code'; import 'tinymce/plugins/image'; import 'tinymce/themes/silver'; -import 'tinymce/skins/ui/oxide/skin.min.css'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; @@ -43,8 +42,12 @@ const RichTextEditor = ({ init={{ menubar: false, statusbar: false, - skin: false, - content_css: false, + // TODO: rewrite this to use skin=false and content_css=false when we figure + // which part of css-loader to change to stop tinymce from changing the + // oxide skin css. + base_url: `${process.env.LMS_BASE_URL}/static/js/vendor/tinymce/js/tinymce`, + skin: 'studio-tmce5', + content_css: 'studio-tmce5', height: '300', schema: 'html5', plugins: 'code image link lists', diff --git a/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap b/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap index a7c71025..fb23e1ef 100644 --- a/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap +++ b/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap @@ -17,13 +17,14 @@ exports[` render disabled 1`] = ` disabled={true} init={ Object { - "content_css": false, + "base_url": "http://localhost:18000/static/js/vendor/tinymce/js/tinymce", + "content_css": "studio-tmce5", "height": "300", "menubar": false, "plugins": "code image link lists", "readonly": 1, "schema": "html5", - "skin": false, + "skin": "studio-tmce5", "statusbar": false, "toolbar": false, } @@ -52,12 +53,13 @@ exports[` render optional 1`] = ` disabled={false} init={ Object { - "content_css": false, + "base_url": "http://localhost:18000/static/js/vendor/tinymce/js/tinymce", + "content_css": "studio-tmce5", "height": "300", "menubar": false, "plugins": "code image link lists", "schema": "html5", - "skin": false, + "skin": "studio-tmce5", "statusbar": false, "toolbar": "formatselect | bold italic underline | link blockquote image | numlist bullist outdent indent | strikethrough | code | undo redo", } @@ -86,12 +88,13 @@ exports[` render required 1`] = ` disabled={false} init={ Object { - "content_css": false, + "base_url": "http://localhost:18000/static/js/vendor/tinymce/js/tinymce", + "content_css": "studio-tmce5", "height": "300", "menubar": false, "plugins": "code image link lists", "schema": "html5", - "skin": false, + "skin": "studio-tmce5", "statusbar": false, "toolbar": "formatselect | bold italic underline | link blockquote image | numlist bullist outdent indent | strikethrough | code | undo redo", } From de12d963c9849d9cea3214df75aa35f6da5279b4 Mon Sep 17 00:00:00 2001 From: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:29:22 -0500 Subject: [PATCH 63/66] chore: update messages (#99) * chore: update messages * chore: add space --- .../EditableAssessment/AssessmentActions.jsx | 8 +++++-- src/components/Assessment/messages.js | 23 +++++++++++++++++-- src/components/Assessment/types.ts | 4 +--- src/components/ModalActions/hooks/messages.js | 4 ++-- src/components/ModalActions/messages.js | 4 ++-- src/components/ProgressBar/messages.js | 4 ++-- src/components/Rubric/types.ts | 4 +--- src/hooks/actions/messages.js | 4 ++-- src/views/AssessmentView/index.jsx | 5 ++-- src/views/AssessmentView/messages.js | 2 +- src/views/SubmissionView/messages.js | 2 +- src/views/XBlockView/Actions/messages.js | 6 ++--- src/views/XBlockView/index.jsx | 6 ++--- 13 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/components/Assessment/EditableAssessment/AssessmentActions.jsx b/src/components/Assessment/EditableAssessment/AssessmentActions.jsx index 4a823e07..d8618b3b 100644 --- a/src/components/Assessment/EditableAssessment/AssessmentActions.jsx +++ b/src/components/Assessment/EditableAssessment/AssessmentActions.jsx @@ -7,7 +7,8 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useCloseModal } from 'hooks/modal'; import { MutationStatus } from 'constants'; -import messages from '../messages'; +import { useViewStep } from 'hooks/routing'; +import messages, { viewStepMessages } from '../messages'; /** * @@ -18,6 +19,9 @@ const AssessmentActions = ({ }) => { const closeModal = useCloseModal(); const { formatMessage } = useIntl(); + const step = useViewStep(); + const viewStep = viewStepMessages[step] ? `${formatMessage(viewStepMessages[step])} ` : ''; + return (
); }; From 7dcd9b2428fd837a12f3fec0e900f0e2be3a6406 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Tue, 5 Dec 2023 15:34:21 +0000 Subject: [PATCH 64/66] fix: ora title as modal header --- src/App.jsx | 14 ++++++------- src/components/ModalContainer.jsx | 33 +++++++++++++++++-------------- src/hooks/app.js | 1 + 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 40f069e6..a68ef3c0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -52,12 +52,12 @@ const App = () => { element={pageWrapper()} /> ); - const modalRoute = (route, Component, title) => ( + const modalRoute = (route, Component) => ( + , )} @@ -81,11 +81,11 @@ const App = () => { */ const baseRoutes = [ appRoute(routes.xblock, XBlockView), - modalRoute(routes.peerAssessment, AssessmentView, 'Assess your peers'), - modalRoute(routes.selfAssessment, AssessmentView, 'Assess yourself'), - modalRoute(routes.studentTraining, AssessmentView, 'Practice grading'), - modalRoute(routes.submission, SubmissionView, 'Your response'), - modalRoute(routes.graded, GradeView, 'My Grade'), + modalRoute(routes.peerAssessment, AssessmentView), + modalRoute(routes.selfAssessment, AssessmentView), + modalRoute(routes.studentTraining, AssessmentView), + modalRoute(routes.submission, SubmissionView), + modalRoute(routes.graded, GradeView), } />, ]; diff --git a/src/components/ModalContainer.jsx b/src/components/ModalContainer.jsx index 4d548973..7c7f5fdc 100644 --- a/src/components/ModalContainer.jsx +++ b/src/components/ModalContainer.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { FullscreenModal } from '@edx/paragon'; import { nullMethod } from 'utils'; +import { useORAConfigData } from 'hooks/app'; import ProgressBar from 'components/ProgressBar'; @@ -10,23 +11,25 @@ import ProgressBar from 'components/ProgressBar'; * where we need to run non-embedded */ -const ModalContainer = ({ title, children }) => ( - } - > -
- {children} -
-
-); +const ModalContainer = ({ children }) => { + const { title } = useORAConfigData(); + return ( + } + > +
+ {children} +
+
+ ); +}; ModalContainer.propTypes = { children: PropTypes.node.isRequired, - title: PropTypes.string.isRequired, }; export default ModalContainer; diff --git a/src/hooks/app.js b/src/hooks/app.js index e094a4ce..c5d45aff 100644 --- a/src/hooks/app.js +++ b/src/hooks/app.js @@ -22,6 +22,7 @@ export const { useIsORAConfigLoaded, useIsPageDataLoaded, useIsPageDataLoading, + useORAConfigData, usePageDataStatus, usePrompts, useResponseData, From 8c448a775f84999999818551445d727365b98b09 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Wed, 6 Dec 2023 15:59:14 +0000 Subject: [PATCH 65/66] chore: jsx hook cleanup --- .../hooks/{useStatusAlertData.jsx => useStatusAlertData.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/StatusAlert/hooks/{useStatusAlertData.jsx => useStatusAlertData.js} (100%) diff --git a/src/components/StatusAlert/hooks/useStatusAlertData.jsx b/src/components/StatusAlert/hooks/useStatusAlertData.js similarity index 100% rename from src/components/StatusAlert/hooks/useStatusAlertData.jsx rename to src/components/StatusAlert/hooks/useStatusAlertData.js From b47e2c28cbca12adc5205a9894de980e5e093552 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Wed, 6 Dec 2023 13:22:02 -0500 Subject: [PATCH 66/66] chore: rewrite modal container --- src/components/ModalContainer.jsx | 26 ++++++++++++-------------- src/index.jsx | 7 ------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/components/ModalContainer.jsx b/src/components/ModalContainer.jsx index 7c7f5fdc..2934e3cc 100644 --- a/src/components/ModalContainer.jsx +++ b/src/components/ModalContainer.jsx @@ -1,31 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FullscreenModal } from '@edx/paragon'; - -import { nullMethod } from 'utils'; import { useORAConfigData } from 'hooks/app'; import ProgressBar from 'components/ProgressBar'; /* The purpose of this component is to wrap views with a header/footer for situations - * where we need to run non-embedded + * where we need to run non-embedded. It is a replicated style of FullScreenModal from + * paragon. The reason we use this instead of FullScreenModal is because FullScreenModal + * is very opinionated on other components that it uses on top of it. Since we are not + * using the modality of the component, it would be better to just replicate the style + * instead of using the component. */ const ModalContainer = ({ children }) => { const { title } = useORAConfigData(); return ( - } - > -
+
+
+

{title}

+ +
+
{children}
- +
); }; ModalContainer.propTypes = { diff --git a/src/index.jsx b/src/index.jsx index 58ac232b..313bdbb5 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -25,13 +25,6 @@ const queryClient = new QueryClient({ subscribe(APP_READY, () => { const isDev = process.env.NODE_ENV === 'development'; const rootEl = document.getElementById('root'); - if (isDev) { - setTimeout(() => { - // This is a hack to prevent the Paragon Modal overlay stop query devtools from clickable - rootEl.removeAttribute('data-focus-on-hidden'); - rootEl.removeAttribute('aria-hidden'); - }, 3000); - } ReactDOM.render(