From 421cf61e8400d519655b46e8636188fd4e5cdbed Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Wed, 17 Aug 2022 17:38:57 +0800 Subject: [PATCH 01/37] feat(assessmentform): categorise settings in sticky sections --- .../containers/AssessmentForm/index.jsx | 799 +++++++++--------- .../AssessmentForm/translations.intl.js | 32 + client/app/lib/components/layouts/Section.tsx | 50 ++ 3 files changed, 497 insertions(+), 384 deletions(-) create mode 100644 client/app/lib/components/layouts/Section.tsx diff --git a/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx index 0b7f97fb0c1..ff2dd230f82 100644 --- a/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx @@ -19,6 +19,7 @@ import ReactTooltip from 'react-tooltip'; import translations from './translations.intl'; import MaterialUploader from '../MaterialUploader'; import { fetchTabs } from './actions'; +import Section from 'lib/components/layouts/Section'; const styles = { flexGroup: { @@ -211,126 +212,6 @@ const AssessmentForm = (props) => { ); - const renderExtraOptions = () => { - if (autograded) { - return ( -
- ( - } - renderIf={autograded} - style={styles.toggle} - /> - )} - /> - ( - - } - renderIf={autograded} - style={styles.toggle} - /> - )} - /> - ( - } - renderIf={autograded} - style={styles.toggle} - /> - )} - /> -
- -
-
- ); - } - const options = [ - { - value: false, - label: , - }, - { - value: true, - label: , - }, - ]; - return ( - <> - ( - } - options={options} - renderIf={!autograded} - type="boolean" - /> - )} - /> - ( - - } - renderIf={!autograded} - style={styles.toggle} - /> - )} - /> -
- -
- - ( - } - renderIf={!autograded} - style={styles.toggle} - /> - )} - /> - {passwordProtected && renderPasswordFields()} - - ); - }; - const renderTabs = () => { if (!loadedTabs) { return null; @@ -365,376 +246,526 @@ const AssessmentForm = (props) => { onSubmit={handleSubmit((data) => onSubmit(data, setError))} > -
+ +
+
+ ( + } + fullWidth + InputLabelProps={{ + shrink: true, + }} + required + style={styles.flexChild} + variant="standard" + /> + )} + /> +
+ +
+ ( + } + style={styles.flexChild} + /> + )} + /> + ( + } + style={styles.flexChild} + /> + )} + /> + {gamified && ( + ( + } + style={styles.flexChild} + /> + )} + /> + )} +
+ ( - } + label={} fullWidth InputLabelProps={{ shrink: true, }} - required - style={styles.flexChild} variant="standard" /> )} /> - {editing && renderTabs(loadedTabs, disabled)} -
- ( - } - fullWidth - InputLabelProps={{ - shrink: true, - }} - variant="standard" + + {editing && ( + ( + } + style={styles.toggle} + /> + )} /> )} - /> -
- ( - } - style={styles.flexChild} - /> - )} - /> - ( - } - style={styles.flexChild} + + {folderAttributes && ( + <> +
+ - )} - /> + + )} + + +
{gamified && ( +
+ ( + } + InputLabelProps={{ + shrink: true, + }} + onWheel={(event) => event.currentTarget.blur()} + style={styles.flexChild} + type="number" + variant="standard" + /> + )} + /> + ( + } + InputLabelProps={{ + shrink: true, + }} + onWheel={(event) => event.currentTarget.blur()} + style={styles.flexChild} + type="number" + variant="standard" + /> + )} + /> +
+ )} + + {editing && conditionAttributes && ( +
+ +
+ )} +
+ + + {autogradedToggleTooltip} + + +
+
( - } + style={styles.toggle} + /> + )} + /> +
+ + {modeSwitching && !containsCodaveri && ( +
+ +
+ )} + +
+ +
+ +
+ ( + } + label={} style={styles.flexChild} /> )} /> - )} -
- {gamified && ( -
( - } - InputLabelProps={{ - shrink: true, - }} - onWheel={(event) => event.currentTarget.blur()} + label={} style={styles.flexChild} - type="number" - variant="standard" /> )} /> ( - } - InputLabelProps={{ - shrink: true, - }} - onWheel={(event) => event.currentTarget.blur()} + label={} style={styles.flexChild} - type="number" - variant="standard" /> )} />
- )} - {editing && ( + {autograded ? ( + <> + ( + + } + style={styles.toggle} + /> + )} + /> +
+ +
+ + ) : null} +
+ +
+ {autograded ? ( +
+ ( + } + renderIf={autograded} + style={styles.toggle} + /> + )} + /> + ( + + } + renderIf={autograded} + style={styles.toggle} + /> + )} + /> +
+ ) : null} + ( } + label={} style={styles.toggle} /> )} /> - )} - - - {autogradedToggleTooltip} - - -
+
+ +
( } + disabled={disabled} + label={} style={styles.toggle} /> )} /> -
- - {modeSwitching && !containsCodaveri && (
- +
- )} - - ( - - } - style={styles.toggle} - /> - )} - /> - - {renderExtraOptions()} - - ( - } - style={styles.toggle} - /> - )} - /> -
- -
- -
- -
-
( } - style={styles.flexChild} + label={} + style={styles.toggle} /> )} /> +
+ +
+
+ +
+ {editing && renderTabs(loadedTabs, disabled)} + ( - } - style={styles.flexChild} + label={} + options={[ + { + value: false, + label: , + }, + { + value: true, + label: , + }, + ]} + renderIf={!autograded} + type="boolean" /> )} /> +
+ +
( } - style={styles.flexChild} + label={ + + } + style={styles.toggle} /> )} /> -
- ( - } - style={styles.toggle} - /> - )} - /> -
- -
- ( - } - style={styles.toggle} - /> + {randomizationAllowed && ( + <> + ( + + } + style={styles.toggle} + /> + )} + /> +
+ +
+ )} - /> -
- -
- - {randomizationAllowed && ( - <> - ( - - } - style={styles.toggle} - /> - )} - /> -
- -
- - )} - - {showPersonalizedTimelineFeatures && ( - <> - ( - } - style={styles.toggle} - /> - )} - /> -
- -
- ( - - } - style={styles.toggle} - /> - )} - /> -
- -
- - )} - - {folderAttributes && ( - <> -
- - - )} - {editing && conditionAttributes && ( -
- -
- )} + {autograded ? ( + <> + ( + } + renderIf={autograded} + style={styles.toggle} + /> + )} + /> +
+ +
+ + ( + + } + renderIf={!autograded} + style={styles.toggle} + /> + )} + /> + {passwordProtected && renderPasswordFields()} + + ) : null} + + +
+ {showPersonalizedTimelineFeatures && ( + <> + ( + + } + style={styles.toggle} + /> + )} + /> +
+ +
+ + ( + + } + style={styles.toggle} + /> + )} + /> +
+ +
+ + )} +
); }; diff --git a/client/app/bundles/course/assessment/containers/AssessmentForm/translations.intl.js b/client/app/bundles/course/assessment/containers/AssessmentForm/translations.intl.js index 69cb6281dee..5b082a4557d 100644 --- a/client/app/bundles/course/assessment/containers/AssessmentForm/translations.intl.js +++ b/client/app/bundles/course/assessment/containers/AssessmentForm/translations.intl.js @@ -219,6 +219,38 @@ const translations = defineMessages({ defaultMessage: 'Enables randomized assignment of question bundles to students (per question group)', }, + assessmentDetails: { + id: 'course.assessment.form.assessmentDetails', + defaultMessage: 'Assessment details', + }, + gamification: { + id: 'course.assessment.form.gamification', + defaultMessage: 'Gamification', + }, + grading: { + id: 'course.assessment.form.grading', + defaultMessage: 'Grading', + }, + answersAndTestCases: { + id: 'course.assessment.form.answersAndTestCases', + defaultMessage: 'Answers and test cases', + }, + organisation: { + id: 'course.assessment.form.organisation', + defaultMessage: 'Organisation', + }, + organisationSubtitle: { + id: 'course.assessment.form.organisationSubtitle', + defaultMessage: 'Change how the asssessment looks and feels.', + }, + examsAndAccessControl: { + id: 'course.assessment.form.examsAndAccessControl', + defaultMessage: 'Exams and access control', + }, + personalisedTimelines: { + id: 'course.assessment.form.personalisedTimelines', + defaultMessage: 'Personalised timelines', + }, }); export default translations; diff --git a/client/app/lib/components/layouts/Section.tsx b/client/app/lib/components/layouts/Section.tsx new file mode 100644 index 00000000000..aad4c3805bc --- /dev/null +++ b/client/app/lib/components/layouts/Section.tsx @@ -0,0 +1,50 @@ +import { ReactNode } from 'react'; +import { Grid, Typography, Divider, Container } from '@mui/material'; +import { SxProps, Theme } from '@mui/material/styles'; + +interface SectionProps { + title: string; + subtitle?: string; + children?: ReactNode; +} + +const styles: { [key: string]: SxProps } = { + marginBottom: { marginBottom: 1 }, + stickyHeadersLgOnly: (theme) => ({ + [theme.breakpoints.up('lg')]: { + position: 'sticky', + top: '5rem', + alignSelf: 'flex-start', + }, + }), +}; + +const Section = (props: SectionProps): JSX.Element => { + return ( + + + + {props.title ? ( + + {props.title} + + ) : null} + + {props.subtitle ? ( + + {props.subtitle} + + ) : null} + + + + {props.children} + + + + + + ); +}; + +export default Section; From 9cdabbe27d2b751313499010240263986ee48254 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Fri, 19 Aug 2022 18:21:53 +0800 Subject: [PATCH 02/37] style(assessmentform): FormattedMessage -> intl.formatMessage --- .../containers/AssessmentForm/index.jsx | 159 +++++++----------- 1 file changed, 63 insertions(+), 96 deletions(-) diff --git a/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx index ff2dd230f82..ff3de91d843 100644 --- a/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage, injectIntl } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { yupResolver } from '@hookform/resolvers/yup'; import { Controller, useForm } from 'react-hook-form'; @@ -16,7 +16,7 @@ import ConditionList from 'lib/components/course/ConditionList'; import formTranslations from 'lib/translations/form'; import { achievementTypesConditionAttributes, typeMaterial } from 'lib/types'; import ReactTooltip from 'react-tooltip'; -import translations from './translations.intl'; +import t from './translations.intl'; import MaterialUploader from '../MaterialUploader'; import { fetchTabs } from './actions'; import Section from 'lib/components/layouts/Section'; @@ -53,12 +53,12 @@ const validationSchema = yup.object({ .date() .nullable() .typeError(formTranslations.invalidDate) - .min(yup.ref('start_at'), translations.startEndValidationError), + .min(yup.ref('start_at'), t.startEndValidationError), bonus_end_at: yup .date() .nullable() .typeError(formTranslations.invalidDate) - .min(yup.ref('start_at'), translations.startEndValidationError), + .min(yup.ref('start_at'), t.startEndValidationError), base_exp: yup .number() .typeError(formTranslations.required) @@ -88,7 +88,7 @@ const validationSchema = yup.object({ // Check if there is at least 1 password type when password_protectd // is enabled. password_protected ? session_password || view_password : true, - message: translations.passwordRequired, + message: t.passwordRequired, }), ), view_password: yup.string().nullable(), @@ -102,7 +102,7 @@ const validationSchema = yup.object({ schema.test({ // Check if there is at least 1 selected test case. test: (use_evaluation) => use_public || use_private || use_evaluation, - message: translations.noTestCaseChosenError, + message: t.noTestCaseChosenError, }), ), show_private: yup.bool(), @@ -147,18 +147,14 @@ const AssessmentForm = (props) => { useEffect(() => { if (editing) { - const failureMessage = ( - - ); + const failureMessage = intl.formatMessage(t.fetchTabFailure); dispatch(fetchTabs(failureMessage)); } }, [dispatch]); - const autogradedToggleTooltip = containsCodaveri ? ( - - ) : ( - - ); + const autogradedToggleTooltip = containsCodaveri + ? intl.formatMessage(t.containsCodaveriQuestion) + : intl.formatMessage(t.modeSwitchingDisabled); const renderPasswordFields = () => (
@@ -170,7 +166,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - placeholder={intl.formatMessage(translations.viewPassword)} + placeholder={intl.formatMessage(t.viewPassword)} fullWidth InputLabelProps={{ shrink: true, @@ -182,9 +178,7 @@ const AssessmentForm = (props) => { /> )} /> -
- -
+
{intl.formatMessage(t.viewPasswordHint)}
{ field={field} fieldState={fieldState} disabled={disabled} - placeholder={intl.formatMessage(translations.sessionPassword)} + placeholder={intl.formatMessage(t.sessionPassword)} fullWidth InputLabelProps={{ shrink: true, @@ -206,9 +200,7 @@ const AssessmentForm = (props) => { /> )} /> -
- -
+
{intl.formatMessage(t.sessionPasswordHint)}
); @@ -230,7 +222,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.tab)} options={options} /> )} @@ -247,7 +239,7 @@ const AssessmentForm = (props) => { > -
+
{ field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.title)} fullWidth InputLabelProps={{ shrink: true, @@ -279,7 +271,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.startAt)} style={styles.flexChild} /> )} @@ -292,7 +284,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.endAt)} style={styles.flexChild} /> )} @@ -306,7 +298,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.bonusEndAt)} style={styles.flexChild} /> )} @@ -322,7 +314,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.description)} fullWidth InputLabelProps={{ shrink: true, @@ -341,7 +333,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.published)} style={styles.toggle} /> )} @@ -360,7 +352,7 @@ const AssessmentForm = (props) => { )}
-
+
{gamified && (
{ fieldState={fieldState} disabled={disabled} fullWidth - label={} + label={intl.formatMessage(t.baseExp)} InputLabelProps={{ shrink: true, }} @@ -392,7 +384,7 @@ const AssessmentForm = (props) => { fieldState={fieldState} disabled={disabled} fullWidth - label={} + label={intl.formatMessage(t.timeBonusExp)} InputLabelProps={{ shrink: true, }} @@ -416,11 +408,10 @@ const AssessmentForm = (props) => { )}
- - {autogradedToggleTooltip} - - -
+
+ + {autogradedToggleTooltip} +
{ field={field} fieldState={fieldState} disabled={containsCodaveri || !modeSwitching || disabled} - label={} + label={intl.formatMessage(t.autograded)} style={styles.toggle} /> )} @@ -442,13 +433,11 @@ const AssessmentForm = (props) => {
{modeSwitching && !containsCodaveri && ( -
- -
+
{intl.formatMessage(t.autogradedHint)}
)}
- + {intl.formatMessage(t.autogradeTestCasesHint)}
@@ -460,7 +449,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.usePublic)} style={styles.flexChild} /> )} @@ -473,7 +462,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.usePrivate)} style={styles.flexChild} /> )} @@ -486,7 +475,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.useEvaluation)} style={styles.flexChild} /> )} @@ -503,23 +492,19 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={ - - } + label={intl.formatMessage(t.delayedGradePublication)} style={styles.toggle} /> )} />
- + {intl.formatMessage(t.delayedGradePublicationHint)}
) : null}
-
+
{autograded ? (
{ field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.skippable)} renderIf={autograded} style={styles.toggle} /> @@ -544,11 +529,7 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={ - - } + label={intl.formatMessage(t.allowPartialSubmission)} renderIf={autograded} style={styles.toggle} /> @@ -565,14 +546,12 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.showPrivate)} style={styles.toggle} /> )} /> -
- -
+
{intl.formatMessage(t.showPrivateHint)}
{ field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.showEvaluation)} style={styles.toggle} /> )} />
- + {intl.formatMessage(t.showEvaluationHint)}
{ field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.showMcqMrqSolution)} style={styles.toggle} /> )} />
- + {intl.formatMessage(t.showMcqMrqSolutionHint)}
{editing && renderTabs(loadedTabs, disabled)} @@ -622,15 +601,15 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.tab)} options={[ { value: false, - label: , + label: intl.formatMessage(t.singlePage), }, { value: true, - label: , + label: intl.formatMessage(t.tabbedView), }, ]} renderIf={!autograded} @@ -640,7 +619,7 @@ const AssessmentForm = (props) => { />
-
+
{ field={field} fieldState={fieldState} disabled={disabled} - label={ - - } + label={intl.formatMessage(t.blockStudentViewingAfterSubmitted)} style={styles.toggle} /> )} @@ -669,15 +644,13 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={ - - } + label={intl.formatMessage(t.enableRandomization)} style={styles.toggle} /> )} />
- + {intl.formatMessage(t.enableRandomizationHint)}
)} @@ -692,14 +665,14 @@ const AssessmentForm = (props) => { field={field} fieldState={fieldState} disabled={disabled} - label={} + label={intl.formatMessage(t.showMcqAnswer)} renderIf={autograded} style={styles.toggle} /> )} />
- + {intl.formatMessage(t.showMcqAnswerHint)}
{ field={field} fieldState={fieldState} disabled={disabled} - label={ - - } + label={intl.formatMessage(t.passwordProtection)} renderIf={!autograded} style={styles.toggle} /> @@ -723,7 +694,7 @@ const AssessmentForm = (props) => { ) : null}
-
+
{showPersonalizedTimelineFeatures && ( <> { field={field} fieldState={fieldState} disabled={disabled} - label={ - - } + label={intl.formatMessage(t.hasPersonalTimes)} style={styles.toggle} /> )} />
- + {intl.formatMessage(t.hasPersonalTimesHint)}
{ field={field} fieldState={fieldState} disabled={disabled} - label={ - - } + label={intl.formatMessage(t.affectsPersonalTimes)} style={styles.toggle} /> )} />
- + {intl.formatMessage(t.affectsPersonalTimesHint)}
)} From 7882c708f2961be1b419d74d31bdff0f14aafbff Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Fri, 19 Aug 2022 18:24:58 +0800 Subject: [PATCH 03/37] style(assessmentform): remove unused proptypes --- .../containers/AssessmentForm/index.jsx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx index ff3de91d843..274e61ae846 100644 --- a/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import { yupResolver } from '@hookform/resolvers/yup'; import { Controller, useForm } from 'react-hook-form'; import * as yup from 'yup'; + import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerField'; import FormRichTextField from 'lib/components/form/fields/RichTextField'; import FormSelectField from 'lib/components/form/fields/SelectField'; @@ -14,12 +15,12 @@ import FormToggleField from 'lib/components/form/fields/ToggleField'; import ErrorText from 'lib/components/ErrorText'; import ConditionList from 'lib/components/course/ConditionList'; import formTranslations from 'lib/translations/form'; +import Section from 'lib/components/layouts/Section'; import { achievementTypesConditionAttributes, typeMaterial } from 'lib/types'; import ReactTooltip from 'react-tooltip'; import t from './translations.intl'; import MaterialUploader from '../MaterialUploader'; import { fetchTabs } from './actions'; -import Section from 'lib/components/layouts/Section'; const styles = { flexGroup: { @@ -744,14 +745,6 @@ AssessmentForm.defaultProps = { AssessmentForm.propTypes = { disabled: PropTypes.bool, dispatch: PropTypes.func.isRequired, - start_at: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), - end_at: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), - bonus_end_at: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(Date), - ]), - autograded: PropTypes.bool, - password_protected: PropTypes.bool, showPersonalizedTimelineFeatures: PropTypes.bool, tabs: PropTypes.arrayOf( PropTypes.shape({ @@ -759,8 +752,6 @@ AssessmentForm.propTypes = { title: PropTypes.string, }), ), - // If randomization is enabled for the assessment - randomization: PropTypes.bool, onSubmit: PropTypes.func.isRequired, // If the Form is in editing mode, `published` button will be displayed. @@ -768,8 +759,6 @@ AssessmentForm.propTypes = { // if the EXP fields should be displayed gamified: PropTypes.bool, // If the personalized timeline fields should be displayed - show_personalized_timeline_features: PropTypes.bool, - // If randomization is allowed for assessments in the current course randomizationAllowed: PropTypes.bool, // If allow to switch between autoraded and manually graded mode. modeSwitching: PropTypes.bool, @@ -782,7 +771,7 @@ AssessmentForm.propTypes = { // See MaterialFormContainer for detailed PropTypes. materials: typeMaterial, }), - // Condtions will be displayed if the attributes are present. + // Conditions will be displayed if the attributes are present. conditionAttributes: achievementTypesConditionAttributes, initialValues: PropTypes.object, intl: PropTypes.object, From 1c85795d4858aa0398c1a6ac32e3e1b7e7edbf96 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Fri, 19 Aug 2022 19:03:16 +0800 Subject: [PATCH 04/37] style(assessment): containers -> components --- .../{containers => components}/AssessmentForm/actions.js | 0 .../{containers => components}/AssessmentForm/index.jsx | 0 .../AssessmentForm/translations.intl.js | 0 .../{containers => components}/MaterialUploader/Material.jsx | 0 .../MaterialUploader/MaterialList.jsx | 0 .../MaterialUploader/__test__/__snapshots__/index.test.js.snap | 0 .../MaterialUploader/__test__/index.test.js | 0 .../{containers => components}/MaterialUploader/index.jsx | 0 .../MaterialUploader/translations.intl.js | 0 .../bundles/course/assessment/pages/AssessmentEdit/index.jsx | 2 +- .../bundles/course/assessment/pages/AssessmentIndex/index.jsx | 2 +- 11 files changed, 2 insertions(+), 2 deletions(-) rename client/app/bundles/course/assessment/{containers => components}/AssessmentForm/actions.js (100%) rename client/app/bundles/course/assessment/{containers => components}/AssessmentForm/index.jsx (100%) rename client/app/bundles/course/assessment/{containers => components}/AssessmentForm/translations.intl.js (100%) rename client/app/bundles/course/assessment/{containers => components}/MaterialUploader/Material.jsx (100%) rename client/app/bundles/course/assessment/{containers => components}/MaterialUploader/MaterialList.jsx (100%) rename client/app/bundles/course/assessment/{containers => components}/MaterialUploader/__test__/__snapshots__/index.test.js.snap (100%) rename client/app/bundles/course/assessment/{containers => components}/MaterialUploader/__test__/index.test.js (100%) rename client/app/bundles/course/assessment/{containers => components}/MaterialUploader/index.jsx (100%) rename client/app/bundles/course/assessment/{containers => components}/MaterialUploader/translations.intl.js (100%) diff --git a/client/app/bundles/course/assessment/containers/AssessmentForm/actions.js b/client/app/bundles/course/assessment/components/AssessmentForm/actions.js similarity index 100% rename from client/app/bundles/course/assessment/containers/AssessmentForm/actions.js rename to client/app/bundles/course/assessment/components/AssessmentForm/actions.js diff --git a/client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx similarity index 100% rename from client/app/bundles/course/assessment/containers/AssessmentForm/index.jsx rename to client/app/bundles/course/assessment/components/AssessmentForm/index.jsx diff --git a/client/app/bundles/course/assessment/containers/AssessmentForm/translations.intl.js b/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js similarity index 100% rename from client/app/bundles/course/assessment/containers/AssessmentForm/translations.intl.js rename to client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js diff --git a/client/app/bundles/course/assessment/containers/MaterialUploader/Material.jsx b/client/app/bundles/course/assessment/components/MaterialUploader/Material.jsx similarity index 100% rename from client/app/bundles/course/assessment/containers/MaterialUploader/Material.jsx rename to client/app/bundles/course/assessment/components/MaterialUploader/Material.jsx diff --git a/client/app/bundles/course/assessment/containers/MaterialUploader/MaterialList.jsx b/client/app/bundles/course/assessment/components/MaterialUploader/MaterialList.jsx similarity index 100% rename from client/app/bundles/course/assessment/containers/MaterialUploader/MaterialList.jsx rename to client/app/bundles/course/assessment/components/MaterialUploader/MaterialList.jsx diff --git a/client/app/bundles/course/assessment/containers/MaterialUploader/__test__/__snapshots__/index.test.js.snap b/client/app/bundles/course/assessment/components/MaterialUploader/__test__/__snapshots__/index.test.js.snap similarity index 100% rename from client/app/bundles/course/assessment/containers/MaterialUploader/__test__/__snapshots__/index.test.js.snap rename to client/app/bundles/course/assessment/components/MaterialUploader/__test__/__snapshots__/index.test.js.snap diff --git a/client/app/bundles/course/assessment/containers/MaterialUploader/__test__/index.test.js b/client/app/bundles/course/assessment/components/MaterialUploader/__test__/index.test.js similarity index 100% rename from client/app/bundles/course/assessment/containers/MaterialUploader/__test__/index.test.js rename to client/app/bundles/course/assessment/components/MaterialUploader/__test__/index.test.js diff --git a/client/app/bundles/course/assessment/containers/MaterialUploader/index.jsx b/client/app/bundles/course/assessment/components/MaterialUploader/index.jsx similarity index 100% rename from client/app/bundles/course/assessment/containers/MaterialUploader/index.jsx rename to client/app/bundles/course/assessment/components/MaterialUploader/index.jsx diff --git a/client/app/bundles/course/assessment/containers/MaterialUploader/translations.intl.js b/client/app/bundles/course/assessment/components/MaterialUploader/translations.intl.js similarity index 100% rename from client/app/bundles/course/assessment/containers/MaterialUploader/translations.intl.js rename to client/app/bundles/course/assessment/components/MaterialUploader/translations.intl.js diff --git a/client/app/bundles/course/assessment/pages/AssessmentEdit/index.jsx b/client/app/bundles/course/assessment/pages/AssessmentEdit/index.jsx index 36dfe330d58..1145b9c3447 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentEdit/index.jsx +++ b/client/app/bundles/course/assessment/pages/AssessmentEdit/index.jsx @@ -7,7 +7,7 @@ import NotificationBar, { notificationShape, } from 'lib/components/NotificationBar'; import { achievementTypesConditionAttributes } from 'lib/types'; -import AssessmentForm from '../../containers/AssessmentForm'; +import AssessmentForm from '../../components/AssessmentForm'; import * as actions from '../../actions'; import translations from './translations.intl'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentIndex/index.jsx b/client/app/bundles/course/assessment/pages/AssessmentIndex/index.jsx index cdf31ae296e..f6baee46d85 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentIndex/index.jsx +++ b/client/app/bundles/course/assessment/pages/AssessmentIndex/index.jsx @@ -14,7 +14,7 @@ import NotificationBar, { } from 'lib/components/NotificationBar'; import ConfirmationDialog from 'lib/components/ConfirmationDialog'; import formTranslations from 'lib/translations/form'; -import AssessmentForm from '../../containers/AssessmentForm'; +import AssessmentForm from '../../components/AssessmentForm'; import * as actions from '../../actions'; import translations from './translations.intl'; import actionTypes from '../../constants'; From c45e3625103833e48c64d38645752599bb8e6961 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Tue, 23 Aug 2022 23:38:39 +0800 Subject: [PATCH 05/37] feat(section): add margin between children --- client/app/lib/components/layouts/Section.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/app/lib/components/layouts/Section.tsx b/client/app/lib/components/layouts/Section.tsx index aad4c3805bc..e7a25610f1f 100644 --- a/client/app/lib/components/layouts/Section.tsx +++ b/client/app/lib/components/layouts/Section.tsx @@ -9,7 +9,9 @@ interface SectionProps { } const styles: { [key: string]: SxProps } = { - marginBottom: { marginBottom: 1 }, + marginBottom: { + marginBottom: 1, + }, stickyHeadersLgOnly: (theme) => ({ [theme.breakpoints.up('lg')]: { position: 'sticky', @@ -17,6 +19,9 @@ const styles: { [key: string]: SxProps } = { alignSelf: 'flex-start', }, }), + content: { + '> *:not(:last-child)': { marginBottom: 2 }, + }, }; const Section = (props: SectionProps): JSX.Element => { @@ -37,7 +42,7 @@ const Section = (props: SectionProps): JSX.Element => { ) : null} - + {props.children} From e75f07f355b89e7fb18945d81259481d775f54ea Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Tue, 23 Aug 2022 23:39:36 +0800 Subject: [PATCH 06/37] feat(selectfield): support for mui variants --- client/app/lib/components/form/fields/SelectField.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/app/lib/components/form/fields/SelectField.jsx b/client/app/lib/components/form/fields/SelectField.jsx index 4b3b7cc2093..839f5439eab 100644 --- a/client/app/lib/components/form/fields/SelectField.jsx +++ b/client/app/lib/components/form/fields/SelectField.jsx @@ -32,6 +32,7 @@ const FormSelectField = (props) => { shrink, displayEmpty, className, + variant = 'standard', ...custom } = props; const isError = !!fieldState.error; @@ -51,7 +52,7 @@ const FormSelectField = (props) => { error={isError} fullWidth style={{ margin: margin ?? styles.selectFieldStyle.margin }} - variant="standard" + variant={variant} > {label} + + + + {props.selectedRows && ( + <> + + + + + )} + + ); +}; + +export default injectIntl(Toolbar); diff --git a/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx b/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx new file mode 100644 index 00000000000..914dd0d0475 --- /dev/null +++ b/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx @@ -0,0 +1,81 @@ +import MockAdapter from 'axios-mock-adapter'; + +import { + act, + fireEvent, + render, + RenderResult, + waitFor, +} from 'utilities/test-utils'; +import FileManager from '..'; +import CourseAPI from 'api/course'; + +const FOLDER_ID = 1; + +const MATERIALS = [ + { + id: 1, + name: `Material 1`, + updated_at: `2017-01-01T01:00:00.0000000Z`, + deleting: false, + }, + { + id: 2, + name: `Material 2`, + updated_at: `2017-01-01T02:00:00.0000000Z`, + deleting: false, + }, +]; + +const NEW_MATERIAL = { + id: 10, + name: 'Material 3', + updated_at: '2017-01-01T08:00:00.0000000Z', + deleting: false, +}; + +const client = CourseAPI.materialFolders.getClient(); +const mock = new MockAdapter(client); + +let fileManager: RenderResult; +beforeEach(() => { + fileManager = render( + , + ); +}); + +beforeEach(mock.reset); + +describe('', () => { + it('shows existing files', () => { + expect(fileManager.getByText('Material 1')).toBeVisible(); + expect(fileManager.getByText('Material 2')).toBeVisible(); + }); + + it('uploads a new file and shows it', async () => { + mock + .onPut( + `/courses/${global.courseId}/materials/folders/${FOLDER_ID}/upload_materials`, + ) + .reply(200, { + materials: [NEW_MATERIAL], + }); + + const uploadApi = jest.spyOn(CourseAPI.materialFolders, 'upload'); + const addFilesButton = fileManager.getByText('Add Files'); + expect(addFilesButton).toBeVisible(); + + const fileInput = fileManager.getByTestId('FileInput'); + + act(() => { + fireEvent.change(fileInput, { + target: { files: [{ name: NEW_MATERIAL.name }] }, + }); + }); + + await waitFor(() => expect(uploadApi).toHaveBeenCalled()); + + const newMaterialRow = await fileManager.findByText(NEW_MATERIAL.name); + expect(newMaterialRow).toBeVisible(); + }); +}); diff --git a/client/app/bundles/course/assessment/components/FileManager/index.tsx b/client/app/bundles/course/assessment/components/FileManager/index.tsx new file mode 100644 index 00000000000..8116895de97 --- /dev/null +++ b/client/app/bundles/course/assessment/components/FileManager/index.tsx @@ -0,0 +1,184 @@ +import { useState, CSSProperties } from 'react'; +import { injectIntl, WrappedComponentProps } from 'react-intl'; +import { Checkbox, CircularProgress } from '@mui/material'; +import { toast } from 'react-toastify'; +import { AxiosError } from 'axios'; + +import DataTable from 'lib/components/DataTable'; +import Toolbar from './Toolbar'; +import InfoLabel from '../InfoLabel'; +import t from './translations.intl'; + +import CourseAPI from 'api/course'; +import { formatLongDateTime } from 'lib/moment'; + +interface Material { + id?: number; + name?: string; + updated_at?: string; + deleting?: boolean; +} + +interface FileManagerProps extends WrappedComponentProps { + folderId: number; + disabled?: boolean; + materials?: Material[]; +} + +const styles: { [key: string]: CSSProperties } = { + uploadingIndicator: { + margin: '9px', + }, +}; + +const FileManager = (props: FileManagerProps): JSX.Element => { + const { intl } = props; + + const [materials, setMaterials] = useState(props.materials ?? []); + const [uploadingMaterials, setUploadingMaterials] = useState([]); + + const loadData = () => { + const materialsData = materials?.map((file) => [ + file.name, + formatLongDateTime(file.updated_at), + ]); + + const uploadingMaterialsData = uploadingMaterials?.map((file) => [ + file.name, + intl.formatMessage(t.uploadingFile), + ]); + + return [...materialsData, ...uploadingMaterialsData]; + }; + + /** + * Remove materials from uploading list and add new materials from server response to existing + * materials list. + */ + const updateMaterials = (mat: Material[], response) => { + setUploadingMaterials((uploadingMaterials) => + uploadingMaterials.filter((m) => mat.indexOf(m) === -1), + ); + + const newMaterials = response?.data?.materials; + if (!newMaterials) return; + setMaterials((materials) => materials.concat(newMaterials)); + }; + + /** + * Remove given materials from uploading list and display error message. + */ + const removeUploads = (mat: Material[], response) => { + const messageFromServer = response?.data?.errors; + const failureMessage = intl.formatMessage(t.uploadFail); + + setUploadingMaterials((uploadingMaterials) => + uploadingMaterials.filter((m) => mat.indexOf(m) === -1), + ); + + toast.error(messageFromServer || failureMessage); + }; + + /** + * Uploads the given files to the corresponding `folderId`. + * @param files array of `File`s mapped from the file input in `Toolbar.tsx` + */ + const uploadFiles = async (files: File[]) => { + const { folderId } = props; + + const materials = files.map((file) => ({ name: file.name })); + setUploadingMaterials((uploadingMaterials) => + uploadingMaterials.concat(materials), + ); + + try { + const response = await CourseAPI.materialFolders.upload(folderId, files); + updateMaterials(materials, response); + } catch (error) { + if (error instanceof AxiosError) removeUploads(materials, error.response); + } + }; + + /** + * Deletes a file on the `DataTable` asynchronously. + * @param index row index of the file selected for deletion in the `DataTable` + */ + const deleteFileWithRowIndex = async (index: number) => { + const { id, name } = materials[index]; + + setMaterials((materials) => + materials?.map((m) => (m.id === id ? { ...m, deleting: true } : m)), + ); + + try { + await CourseAPI.materials.destroy(props.folderId, id); + setMaterials((materials) => materials?.filter((m) => m.id !== id)); + toast.success(intl.formatMessage(t.deleteSuccess, { name })); + } catch (error) { + setMaterials((materials) => + materials?.map((m) => (m.id === id ? { ...m, deleting: false } : m)), + ); + toast.error(intl.formatMessage(t.deleteFail, { name })); + } + }; + + const ToolbarComponent = (props) => ( + + ); + + const DisabledMessage = () => ( + {intl.formatMessage(t.disableNewFile)} + ); + + const renderTopTableComponent = () => + !props.disabled ? ToolbarComponent : DisabledMessage; + + const RowStartComponent = (props) => { + const type = props['data-description']; + const index = props['data-index']; + + const isBodyRow = type === 'row-select'; + const isUploadingMaterial = index >= materials.length; + const isDeletingMaterial = + index < materials.length && materials[index]?.deleting; + + if (isBodyRow && (isUploadingMaterial || isDeletingMaterial)) { + return ; + } else { + return ; + } + }; + + return ( + ({ size: 'small', sx: { overflow: 'hidden' } }), + }} + components={{ + Checkbox: RowStartComponent, + TableToolbar: renderTopTableComponent(), + TableToolbarSelect: renderTopTableComponent(), + ...(materials.length <= 0 + ? { + TableBody: () => null, + TableHead: () => null, + } + : null), + }} + /> + ); +}; + +export default injectIntl(FileManager); diff --git a/client/app/bundles/course/assessment/components/FileManager/translations.intl.js b/client/app/bundles/course/assessment/components/FileManager/translations.intl.js new file mode 100644 index 00000000000..e11688284f0 --- /dev/null +++ b/client/app/bundles/course/assessment/components/FileManager/translations.intl.js @@ -0,0 +1,43 @@ +import { defineMessages } from 'react-intl'; + +const translations = defineMessages({ + deleteSuccess: { + id: 'course.assessment.fileManager.deleteSuccess', + defaultMessage: '"{name}" was deleted.', + }, + deleteFail: { + id: 'course.assessment.fileManager.deleteFail', + defaultMessage: 'Failed to delete "{name}", please try again.', + }, + uploadFail: { + id: 'course.assessment.fileManager.uploadFail', + defaultMessage: 'Failed to upload materials.', + }, + addFiles: { + id: 'course.assessment.fileManager.addFiles', + defaultMessage: 'Add Files', + }, + deleteSelected: { + id: 'course.assessment.fileManager.deleteSelected', + defaultMessage: 'Delete Selected', + }, + fileName: { + id: 'course.assessment.fileManager.fileName', + defaultMessage: 'File name', + }, + dateAdded: { + id: 'course.assessment.fileManager.dateAdded', + defaultMessage: 'Date added', + }, + uploadingFile: { + id: 'course.assessment.fileManager.uploadingFile', + defaultMessage: 'Uploading file...', + }, + disableNewFile: { + id: 'course.assessment.fileManager.disableNewFile', + defaultMessage: + 'You cannot add new files because the Materials component is disabled in Course Settings.', + }, +}); + +export default translations; diff --git a/client/app/bundles/course/assessment/components/MaterialUploader/Material.jsx b/client/app/bundles/course/assessment/components/MaterialUploader/Material.jsx deleted file mode 100644 index 0de3abe8972..00000000000 --- a/client/app/bundles/course/assessment/components/MaterialUploader/Material.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage, defineMessages } from 'react-intl'; -import { formatLongDateTime } from 'lib/moment'; -import { - Avatar, - CircularProgress, - IconButton, - ListItem, - ListItemAvatar, - ListItemText, - ListItemSecondaryAction, -} from '@mui/material'; -import Assignment from '@mui/icons-material/Assignment'; -import Delete from '@mui/icons-material/Delete'; -import ReactTooltip from 'react-tooltip'; - -const styles = { - avatar: { - height: '32px', - width: '32px', - }, - iconButton: { - color: 'black', - width: 24, - height: 24, - padding: 4, - marginRight: 16, - }, - secondaryText: { - fontSize: 12, - }, -}; - -const translations = defineMessages({ - uploading: { - id: 'course.material.uploading', - defaultMessage: 'Uploading', - }, - disableDelete: { - id: 'course.material.disableDelete', - defaultMessage: - 'This action is unavailable as the Materials Component is disabled in the Admin Settings', - }, -}); - -const propTypes = { - id: PropTypes.number, - name: PropTypes.string.isRequired, - updatedAt: PropTypes.string, - onMaterialDelete: PropTypes.func, - deleting: PropTypes.bool, - uploading: PropTypes.bool, - disabled: PropTypes.bool, -}; - -class Material extends PureComponent { - onDelete = (e) => { - e.preventDefault(); - const { id, name, onMaterialDelete } = this.props; - if (onMaterialDelete) onMaterialDelete(id, name); - }; - - renderIcon() { - const { disabled } = this.props; - if (this.props.deleting || this.props.uploading) { - return ; - } - - return ( - - - - - - - ); - } - - renderSecondaryText() { - if (this.props.uploading) { - return ; - } - const { updatedAt } = this.props; - return ( -
{formatLongDateTime(updatedAt)}
- ); - } - - render() { - const { name } = this.props; - - return ( - - - - - - - - {this.renderIcon()} - - ); - } -} - -Material.propTypes = propTypes; - -export default Material; diff --git a/client/app/bundles/course/assessment/components/MaterialUploader/MaterialList.jsx b/client/app/bundles/course/assessment/components/MaterialUploader/MaterialList.jsx deleted file mode 100644 index 73864ecedc9..00000000000 --- a/client/app/bundles/course/assessment/components/MaterialUploader/MaterialList.jsx +++ /dev/null @@ -1,155 +0,0 @@ -import PropTypes from 'prop-types'; -import { FormattedMessage, defineMessages } from 'react-intl'; -import { Button, List, ListSubheader, Divider } from '@mui/material'; -import Add from '@mui/icons-material/Add'; -import NotificationBar, { - notificationShape, -} from 'lib/components/NotificationBar'; -import ReactTooltip from 'react-tooltip'; -import Material from './Material'; - -const translations = defineMessages({ - addFiles: { - id: 'course.material.addFiles', - defaultMessage: 'Add Files', - }, - disableNewFile: { - id: 'course.material.disableNewFile', - defaultMessage: - 'This action is unavailable as the Materials Component is disabled in the Admin Settings', - }, -}); - -const propTypes = { - materials: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.number, - name: PropTypes.string, - updated_at: PropTypes.string, - deleting: PropTypes.bool, - }), - ), - // The popup notification message - notification: notificationShape, - onMaterialDelete: PropTypes.func.isRequired, - // The materials that are being uploading. - uploadingMaterials: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string, - uploading: PropTypes.bool, - }), - ), - onFileInputChange: PropTypes.func, - enableMaterialsAction: PropTypes.bool, -}; - -const defaultProps = { - materials: [], - uploadingMaterials: [], -}; - -const styles = { - newFileButton: { - verticalAlign: 'middle', - }, - uploadInput: { - cursor: 'pointer', - position: 'absolute', - top: 0, - bottom: 0, - right: 0, - left: 0, - opacity: 0, - }, -}; - -const MaterialList = (props) => { - const { - materials, - uploadingMaterials, - onMaterialDelete, - onFileInputChange, - enableMaterialsAction, - } = props; - const header = ( - - ); - - const materialNodes = materials.map((material) => ( - - )); - - const uploadingMaterialNodes = uploadingMaterials.map((material) => ( - - )); - - const newFileButton = ( - <> -
- -
- - - - - ); - - return ( - <> - - - {(materials.length > 0 || uploadingMaterials.length > 0) && ( - {header} - )} - {materialNodes} - {uploadingMaterialNodes} - {newFileButton} - - - - - ); -}; - -MaterialList.propTypes = propTypes; -MaterialList.defaultProps = defaultProps; - -export default MaterialList; diff --git a/client/app/bundles/course/assessment/components/MaterialUploader/__test__/__snapshots__/index.test.js.snap b/client/app/bundles/course/assessment/components/MaterialUploader/__test__/__snapshots__/index.test.js.snap deleted file mode 100644 index 812d7ae7162..00000000000 --- a/client/app/bundles/course/assessment/components/MaterialUploader/__test__/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,181 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders the material 1`] = ` - - - - - - - - 01 Jan 2017, 4:00pm - - } - /> - - - - - - - - - -`; - -exports[` renders the component with materials 1`] = ` - - - - - - - - -
- } - style={ - Object { - "verticalAlign": "middle", - } - } - > - - - -
- - - -
- - -
-`; diff --git a/client/app/bundles/course/assessment/components/MaterialUploader/__test__/index.test.js b/client/app/bundles/course/assessment/components/MaterialUploader/__test__/index.test.js deleted file mode 100644 index 885effef222..00000000000 --- a/client/app/bundles/course/assessment/components/MaterialUploader/__test__/index.test.js +++ /dev/null @@ -1,88 +0,0 @@ -import { mount, shallow } from 'enzyme'; -import MockAdapter from 'axios-mock-adapter'; -import ProviderWrapper from 'lib/components/ProviderWrapper'; -import CourseAPI from 'api/course'; -import MaterialUploader from '../index'; -import MaterialList from '../MaterialList'; -import Material from '../Material'; - -const folderId = 1; -const uploadedMaterial = { - id: 10, - name: 'Uploaded Material', - updated_at: '2017-01-01T08:00:00.0000000Z', - deleting: false, -}; -const materials = [1, 2].map((id) => ({ - id, - name: `Material ${id}`, - updated_at: `2017-01-01T0${id}:00:00.0000000Z`, - deleting: false, -})); - -// Mock axios -const client = CourseAPI.materialFolders.getClient(); -const mock = new MockAdapter(client); - -beforeEach(() => { - mock.reset(); -}); - -describe('', () => { - it('renders the component with materials', () => { - const materialList = shallow( - , - ); - - expect(materialList).toMatchSnapshot(); - }); -}); - -describe('', () => { - it('renders the material', () => { - const material = shallow( - , - ); - - expect(material).toMatchSnapshot(); - }); -}); - -describe('', () => { - it('uploads the material', async () => { - mock - .onPut( - `/courses/${courseId}/materials/folders/${folderId}/upload_materials`, - ) - .reply(200, { - materials: [uploadedMaterial], - }); - - const materialUploader = mount( - - - , - ); - - expect(materialUploader.find('Material')).toHaveLength(2); - - const spyUpload = jest.spyOn(CourseAPI.materialFolders, 'upload'); - // Upload a file - materialUploader.find('input[type="file"]').simulate('change', { - target: { - files: [{ name: 'Uploading file' }], - }, - }); - - await sleep(1); - expect(spyUpload).toHaveBeenCalled(); - expect(materialUploader.find('Material')).toHaveLength(3); - }); -}); diff --git a/client/app/bundles/course/assessment/components/MaterialUploader/index.jsx b/client/app/bundles/course/assessment/components/MaterialUploader/index.jsx deleted file mode 100644 index 2e5e32a3fcd..00000000000 --- a/client/app/bundles/course/assessment/components/MaterialUploader/index.jsx +++ /dev/null @@ -1,143 +0,0 @@ -import { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import CourseAPI from 'api/course'; -import { typeMaterial } from 'lib/types'; -import MaterialList from './MaterialList'; -import translations from './translations.intl'; - -const propTypes = { - folderId: PropTypes.number.isRequired, - materials: typeMaterial, - enableMaterialsAction: PropTypes.bool.isRequired, -}; - -class MaterialUploader extends PureComponent { - constructor(props) { - super(props); - this.state = { - materials: props.materials, - uploadingMaterials: [], - }; - } - - onFileInputChange = (e) => { - e.preventDefault(); - const fileInput = e.target; - const { folderId } = this.props; - const files = fileInput.files; - - const materials = []; - for (let i = 0; i < files.length; i += 1) { - materials.push({ name: files[i].name }); - } - this.setState((state) => ({ - uploadingMaterials: state.uploadingMaterials.concat(materials), - })); - - CourseAPI.materialFolders - .upload(folderId, files) - .then((response) => { - this.updateMaterials(materials, response); - }) - .catch((error) => { - this.removeUploads(materials, error.response); - // Set the value to null so that the files can be selected again. - fileInput.value = null; - }); - }; - - onMaterialDelete = (id, name) => { - this.setState((state) => { - // Update UI to show the loader. - const updatedMaterials = state.materials.map((m) => { - if (m.id === id) { - return { ...m, deleting: true }; - } - return m; - }); - - return { materials: updatedMaterials }; - }); - - CourseAPI.materials - .destroy(this.props.folderId, id) - .then(() => { - this.setState((state) => { - // Remove material from the list - const materials = state.materials.filter((m) => m.id !== id); - const successMessage = ( - - ); - - return { materials, notification: { message: successMessage } }; - }); - }) - .catch(() => { - this.setState((state) => { - // Display failure message and restore the material to not deleting state - const materials = state.materials.map((m) => { - if (m.id === id) { - return { ...m, deleting: false }; - } - return m; - }); - const failureMessage = ( - - ); - - return { materials, notification: { message: failureMessage } }; - }); - }); - }; - - // Remove given materials from uploading list and display error message. - removeUploads(materials, response) { - const messageFromServer = response && response.data && response.data.errors; - const failureMessage = ; - this.setState((state) => ({ - uploadingMaterials: state.uploadingMaterials.filter( - (m) => materials.indexOf(m) === -1, - ), - notification: { message: messageFromServer || failureMessage }, - })); - } - - // Remove materials from uploading list and add new materials from server response to existing - // materials list. - updateMaterials(materials, response) { - const uploadingMaterials = this.state.uploadingMaterials.filter( - (m) => materials.indexOf(m) === -1, - ); - const newState = { - uploadingMaterials, - }; - - const newMaterials = response && response.data && response.data.materials; - if (newMaterials) { - newState.materials = this.state.materials.concat(newMaterials); - } - this.setState(newState); - } - - render() { - return ( - - ); - } -} - -MaterialUploader.propTypes = propTypes; - -export default MaterialUploader; diff --git a/client/app/bundles/course/assessment/components/MaterialUploader/translations.intl.js b/client/app/bundles/course/assessment/components/MaterialUploader/translations.intl.js deleted file mode 100644 index 165ca7f19b8..00000000000 --- a/client/app/bundles/course/assessment/components/MaterialUploader/translations.intl.js +++ /dev/null @@ -1,18 +0,0 @@ -import { defineMessages } from 'react-intl'; - -const translations = defineMessages({ - deleteSuccess: { - id: 'course.assessment.materialList.deleteSuccess', - defaultMessage: '"{name}" was deleted.', - }, - deleteFail: { - id: 'course.assessment.materialList.deleteFail', - defaultMessage: 'Failed to delete "{name}", please try again.', - }, - uploadFail: { - id: 'course.assessment.materialList.uploadFail', - defaultMessage: 'Failed to upload materials.', - }, -}); - -export default translations; diff --git a/client/package.json b/client/package.json index 84728b9cf7a..6ecfea69af1 100644 --- a/client/package.json +++ b/client/package.json @@ -155,7 +155,8 @@ "@babel/preset-typescript": "^7.18.6", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", - "@types/jest": "^27.5.2", + "@types/jest": "^28.1.7", + "@types/enzyme": "^3.10.12", "@types/jquery": "^3.5.14", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", diff --git a/client/yarn.lock b/client/yarn.lock index 2ed96a92c82..64bc466e910 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1416,6 +1416,13 @@ "@types/node" "*" jest-mock "^27.5.1" +"@jest/expect-utils@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.3.tgz#58561ce5db7cd253a7edddbc051fb39dda50f525" + integrity sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA== + dependencies: + jest-get-type "^28.0.2" + "@jest/fake-timers@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" @@ -1981,7 +1988,7 @@ dependencies: "@types/node" "*" -"@types/cheerio@^0.22.22": +"@types/cheerio@*", "@types/cheerio@^0.22.22": version "0.22.31" resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.31.tgz#b8538100653d6bb1b08a1e46dec75b4f2a5d5eb6" integrity sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw== @@ -2003,6 +2010,14 @@ dependencies: "@types/node" "*" +"@types/enzyme@^3.10.12": + version "3.10.12" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.12.tgz#ac4494801b38188935580642f772ad18f72c132f" + integrity sha512-xryQlOEIe1TduDWAOphR0ihfebKFSWOXpIsk+70JskCfRfW+xALdnJ0r1ZOTo85F9Qsjk6vtlU7edTYHbls9tA== + dependencies: + "@types/cheerio" "*" + "@types/react" "*" + "@types/eslint-scope@^3.7.3": version "3.7.3" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" @@ -2084,13 +2099,13 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^27.5.2": - version "27.5.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.2.tgz#ec49d29d926500ffb9fd22b84262e862049c026c" - integrity sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA== +"@types/jest@*", "@types/jest@^28.1.7": + version "28.1.7" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-28.1.7.tgz#a680c5d05b69634c2d54a63cb106d7fb1adaba16" + integrity sha512-acDN4VHD40V24tgu0iC44jchXavRNVFXQ/E6Z5XNsswgoSO/4NgsXoEYmPUGookKldlZQyIpmrEXsHI9cA3ZTA== dependencies: - jest-matcher-utils "^27.0.0" - pretty-format "^27.0.0" + expect "^28.0.0" + pretty-format "^28.0.0" "@types/jquery@^3.5.14": version "3.5.14" @@ -3721,6 +3736,11 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== +diff-sequences@^28.1.1: + version "28.1.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" + integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -4387,6 +4407,17 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" +expect@^28.0.0: + version "28.1.3" + resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.3.tgz#90a7c1a124f1824133dd4533cce2d2bdcb6603ec" + integrity sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g== + dependencies: + "@jest/expect-utils" "^28.1.3" + jest-get-type "^28.0.2" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + expose-loader@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-4.0.0.tgz#aa6f06f57cbc904175de4fe4eaff6211337e0231" @@ -5476,6 +5507,16 @@ jest-diff@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" +jest-diff@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.3.tgz#948a192d86f4e7a64c5264ad4da4877133d8792f" + integrity sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw== + dependencies: + chalk "^4.0.0" + diff-sequences "^28.1.1" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + jest-docblock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" @@ -5524,6 +5565,11 @@ jest-get-type@^27.5.1: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== +jest-get-type@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" + integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== + jest-haste-map@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" @@ -5599,7 +5645,7 @@ jest-localstorage-mock@^2.4.22: resolved "https://registry.yarnpkg.com/jest-localstorage-mock/-/jest-localstorage-mock-2.4.22.tgz#9d70be92bfc591c0be289ee2f71de1b4b2a5ca9b" integrity sha512-60PWSDFQOS5v7JzSmYLM3dPLg0JLl+2Vc4lIEz/rj2yrXJzegsFLn7anwc5IL0WzJbBa/Las064CHbFg491/DQ== -jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: +jest-matcher-utils@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== @@ -5609,6 +5655,16 @@ jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" +jest-matcher-utils@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz#5a77f1c129dd5ba3b4d7fc20728806c78893146e" + integrity sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw== + dependencies: + chalk "^4.0.0" + jest-diff "^28.1.3" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + jest-message-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" @@ -5624,6 +5680,21 @@ jest-message-util@^27.5.1: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.3.tgz#232def7f2e333f1eecc90649b5b94b0055e7c43d" + integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^28.1.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^28.1.3" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" @@ -7031,7 +7102,7 @@ prettier@^2.7.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== -pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: +pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== @@ -7040,6 +7111,16 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^28.0.0, pretty-format@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" + integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== + dependencies: + "@jest/schemas" "^28.1.3" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -7352,7 +7433,7 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.2.0: +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== From aad076e70b2d3a7d44ab544b45524019db568771 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Wed, 24 Aug 2022 00:25:10 +0800 Subject: [PATCH 14/37] feat(assessmentform): style exp fields --- .../components/AssessmentForm/index.jsx | 111 +++++++++--------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx index 5c129dd23de..6cbacb77b57 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx @@ -395,61 +395,64 @@ const AssessmentForm = (props) => { )}
-
- {gamified && ( -
- ( - event.currentTarget.blur()} - style={styles.flexChild} - type="number" - variant="standard" - /> - )} - /> - ( - event.currentTarget.blur()} - style={styles.flexChild} - type="number" - variant="standard" - /> - )} - /> -
- )} + {gamified && ( +
+ + + ( + event.currentTarget.blur()} + style={styles.flexChild} + type="number" + variant="filled" + /> + )} + /> + - {editing && conditionAttributes && ( -
- -
- )} -
+ + ( + event.currentTarget.blur()} + style={styles.flexChild} + type="number" + variant="filled" + /> + )} + /> + + + + {editing && conditionAttributes && ( +
+ +
+ )} +
+ )}
From 1efb5abab52faa5dacd97fb271573a19164759fe Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Wed, 24 Aug 2022 00:30:48 +0800 Subject: [PATCH 15/37] feat(assessmentform): replace autograded toggle with radio --- .../components/AssessmentForm/index.jsx | 75 ++++++++++++------- .../AssessmentForm/translations.intl.js | 14 ++-- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx index 6cbacb77b57..240987bc890 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx @@ -10,6 +10,9 @@ import { RadioGroup, Typography, Grid } from '@mui/material'; import { Public as PublishedIcon, Block as DraftIcon, + Create as ManualIcon, + CheckCircle as AutogradedIcon, + InfoOutlined as InfoOutlinedIcon, } from '@mui/icons-material'; import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerField'; @@ -26,6 +29,7 @@ import ReactTooltip from 'react-tooltip'; import t from './translations.intl'; import FileManager from '../FileManager'; import IconRadio from '../IconRadio'; +import InfoLabel from '../InfoLabel'; import { fetchTabs } from './actions'; const styles = { @@ -159,10 +163,6 @@ const AssessmentForm = (props) => { } }, [dispatch]); - const autogradedToggleTooltip = containsCodaveri - ? intl.formatMessage(t.containsCodaveriQuestion) - : intl.formatMessage(t.modeSwitchingDisabled); - const renderPasswordFields = () => (
{ )}
- - {autogradedToggleTooltip} - -
- ( - - )} - /> -
+ + {intl.formatMessage(t.gradingMode)} + + + {!modeSwitching ? ( + {intl.formatMessage(t.modeSwitchingDisabled)} + ) : null} + + {containsCodaveri ? ( + + {intl.formatMessage(t.containsCodaveriQuestion)} + + ) : null} + + ( + <> + { + const isAutograded = e.target.value === 'autograded'; + field.onChange(isAutograded); + }} + > + + + + + + )} + /> {modeSwitching && !containsCodaveri && (
{intl.formatMessage(t.autogradedHint)}
diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js b/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js index 6f5a88f15fc..67ff90a9c11 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js +++ b/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js @@ -123,20 +123,24 @@ const translations = defineMessages({ id: 'course.assessment.form.draftHint', defaultMessage: 'Only you and staff can see this assessment.', }, + gradingMode: { + id: 'course.assessment.form.gradingMode', + defaultMessage: 'Grading mode', + }, autogradedHint: { id: 'course.assessment.form.autogradedHint', defaultMessage: - 'Automatically assign grade and experience points after assessment is \ - submitted. Answers that are not auto-gradable will always receive the maximum grade.', + 'Automatically assign grade and EXP upon submission. \ + Non-autogradeable questions will always receive the maximum grade.', }, modeSwitchingDisabled: { id: 'course.assessment.form.modeSwitchingHint', defaultMessage: - 'Switch to autograded mode is not allowed as there are submissions \ - for the assessment.', + 'You can no longer change the grading mode because there are already submissions \ + for this assessment.', }, containsCodaveriQuestion: { - id: 'course.assessment.form.modeSwitchingHint', + id: 'course.assessment.form.containsCodaveriQuestion', defaultMessage: "Switch to autograded mode is not allowed as there's \ codaveri programming question type. This question type is only supported \ From 0c546de04a4a4d8a584b2f633117fc39f7e3e882 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Wed, 24 Aug 2022 00:33:42 +0800 Subject: [PATCH 16/37] feat(assessmentform): replace grading options with checkboxes --- .../components/AssessmentForm/index.jsx | 125 +++++++++--------- .../AssessmentForm/translations.intl.js | 29 +++- 2 files changed, 81 insertions(+), 73 deletions(-) diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx index 240987bc890..8b22a341042 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx @@ -20,6 +20,7 @@ import FormRichTextField from 'lib/components/form/fields/RichTextField'; import FormSelectField from 'lib/components/form/fields/SelectField'; import FormTextField from 'lib/components/form/fields/TextField'; import FormToggleField from 'lib/components/form/fields/ToggleField'; +import FormCheckboxField from 'lib/components/form/fields/CheckboxField'; import ErrorText from 'lib/components/ErrorText'; import ConditionList from 'lib/components/course/ConditionList'; import formTranslations from 'lib/translations/form'; @@ -501,76 +502,68 @@ const AssessmentForm = (props) => { )} /> - {modeSwitching && !containsCodaveri && ( -
{intl.formatMessage(t.autogradedHint)}
- )} - -
- {intl.formatMessage(t.autogradeTestCasesHint)} -
+ + {intl.formatMessage(t.calculateGradeWith)} + -
- ( - - )} - /> - ( - - )} - /> - ( - - )} - /> -
+ ( + + )} + /> + ( + + )} + /> + ( + + )} + /> - {autograded ? ( - <> - ( - ( + - )} + } /> -
- {intl.formatMessage(t.delayedGradePublicationHint)} -
- - ) : null} + )} + />
diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js b/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js index 67ff90a9c11..864dae7b1e9 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js +++ b/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js @@ -43,15 +43,15 @@ const translations = defineMessages({ }, usePublic: { id: 'course.assessment.form.usePublic', - defaultMessage: 'Public', + defaultMessage: 'Public test cases', }, usePrivate: { id: 'course.assessment.form.usePrivate', - defaultMessage: 'Private', + defaultMessage: 'Private test cases', }, useEvaluation: { id: 'course.assessment.form.useEvaluation', - defaultMessage: 'Evaluation', + defaultMessage: 'Evaluation test cases', }, allowPartialSubmission: { id: 'course.assessment.form.allowPartialSubmission', @@ -146,6 +146,10 @@ const translations = defineMessages({ codaveri programming question type. This question type is only supported \ in non-autograded assessment.", }, + calculateGradeWith: { + id: 'course.assessment.form.calculateGradeWith', + defaultMessage: 'Calculate grade and EXP with', + }, skippable: { id: 'course.assessment.form.skippable', defaultMessage: 'Allow to skip steps', @@ -164,14 +168,13 @@ const translations = defineMessages({ }, delayedGradePublication: { id: 'course.assessment.form.delayedGradePublication', - defaultMessage: 'Delayed Grade Publication', + defaultMessage: 'Enable delayed grade publication', }, delayedGradePublicationHint: { id: 'course.assessment.form.delayedGradePublicationHint', defaultMessage: - "When delayed grade publication is enabled, gradings done by course staff will \ - not be immediately shown to the student. To publish all gradings for this assessment, click \ - on the 'Publish Grades' button on the top right of the submissions listing for this assessment.", + 'When delayed grade publication is on, gradings will not be immediately shown to students. \ + To publish all gradings, you may click Publish Grades in the Submissions page.', }, showMcqMrqSolution: { id: 'course.assessment.form.showMcqMrqSolution', @@ -271,6 +274,18 @@ const translations = defineMessages({ id: 'course.assessment.form.personalisedTimelines', defaultMessage: 'Personalised timelines', }, + unavailableInAutograded: { + id: 'course.assessment.form.unavailableInAutograded', + defaultMessage: 'Unavailable in autograded assessments.', + }, + unavailableInManuallyGraded: { + id: 'course.assessment.form.unavailableInManuallyGraded', + defaultMessage: 'Unavailable in manually graded assessments.', + }, + afterSubmissionGraded: { + id: 'course.assessment.form.afterSubmissionGraded', + defaultMessage: 'After submission is graded and published', + }, files: { id: 'course.assessment.form.files', defaultMessage: 'Files', From 6950cec1313d24e6cf3477690b49abf8d63e8c2a Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Wed, 24 Aug 2022 00:36:49 +0800 Subject: [PATCH 17/37] feat(assessmentform): replace answers options with checkboxes --- .../components/AssessmentForm/index.jsx | 76 +++++++++---------- .../AssessmentForm/translations.intl.js | 21 +---- 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx index 8b22a341042..2d97bf39541 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx @@ -567,44 +567,50 @@ const AssessmentForm = (props) => {
- {autograded ? ( -
- ( - ( + - )} + } /> - ( - + ( + - )} + } /> -
- ) : null} + )} + /> + + + {intl.formatMessage(t.afterSubmissionGraded)} + ( - { /> )} /> -
{intl.formatMessage(t.showPrivateHint)}
( - { /> )} /> -
- {intl.formatMessage(t.showEvaluationHint)} -
( - )} /> -
- {intl.formatMessage(t.showMcqMrqSolutionHint)} -
Date: Wed, 24 Aug 2022 00:43:00 +0800 Subject: [PATCH 18/37] feat(assessmentform): style tabs, pers. timeline, mcq elements --- .../components/AssessmentForm/index.jsx | 167 ++++++++---------- .../AssessmentForm/translations.intl.js | 10 +- 2 files changed, 77 insertions(+), 100 deletions(-) diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx index 2d97bf39541..fc352537c31 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx @@ -34,22 +34,7 @@ import InfoLabel from '../InfoLabel'; import { fetchTabs } from './actions'; const styles = { - flexGroup: { - display: 'flex', - }, - flexChild: { - flex: 1, - }, - toggle: { - marginTop: 16, - }, - hint: { - fontSize: 14, - marginBottom: 12, - }, - conditions: { - marginTop: 24, - }, + conditions: { marginTop: 24 }, }; const validationSchema = yup.object({ @@ -213,14 +198,13 @@ const AssessmentForm = (props) => { ); const renderTabs = () => { - if (!loadedTabs) { - return null; - } + if (!loadedTabs) return null; const options = loadedTabs.map((tab) => ({ value: tab.tab_id, label: tab.title, })); + return ( { disabled={disabled} label={intl.formatMessage(t.tab)} options={options} + variant="filled" /> )} /> @@ -660,8 +645,8 @@ const AssessmentForm = (props) => { { label: intl.formatMessage(t.tabbedView), }, ]} - renderIf={!autograded} type="boolean" + variant="filled" /> )} /> @@ -684,56 +669,49 @@ const AssessmentForm = (props) => { name="block_student_viewing_after_submitted" control={control} render={({ field, fieldState }) => ( - )} /> {randomizationAllowed && ( - <> - ( - - )} - /> -
- {intl.formatMessage(t.enableRandomizationHint)} -
- + ( + + )} + /> )} - {autograded ? ( - <> - ( - ( + - )} + } /> -
- {intl.formatMessage(t.showMcqAnswerHint)} -
+ )} + /> { ) : null}
-
- {showPersonalizedTimelineFeatures && ( - <> - ( - - )} - /> -
- {intl.formatMessage(t.hasPersonalTimesHint)} -
+ {showPersonalizedTimelineFeatures && ( +
+ ( + + )} + /> - ( - - )} - /> -
- {intl.formatMessage(t.affectsPersonalTimesHint)} -
- - )} -
+ ( + + )} + /> +
+ )} ); }; diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js b/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js index 203dbaf95e1..8ddd6f3d59d 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js +++ b/client/app/bundles/course/assessment/components/AssessmentForm/translations.intl.js @@ -35,7 +35,7 @@ const translations = defineMessages({ }, blockStudentViewingAfterSubmitted: { id: 'course.assessment.form.blockStudentViewingAfterSubmitted', - defaultMessage: 'Block Students from Viewing Finalized Submissions', + defaultMessage: 'Block students from viewing finalized submissions', }, autogradeTestCasesHint: { id: 'course.assessment.form.autogradeTestCasesHint', @@ -59,12 +59,12 @@ const translations = defineMessages({ }, showMcqAnswer: { id: 'course.assessment.form.showMcqAnswer', - defaultMessage: 'Show MCQ Submit Result', + defaultMessage: 'Show MCQ submit result', }, showMcqAnswerHint: { id: 'course.assessment.form.showMcqAnswerHint', defaultMessage: - 'Students can try to submit answer to MCQ and get feedback until they get the right answer', + 'When enabled, students can try to submit MCQ answers and get feedback until they get it right.', }, showPrivate: { id: 'course.assessment.form.showPrivate', @@ -148,6 +148,10 @@ const translations = defineMessages({ id: 'course.assessment.form.layout', defaultMessage: 'Layout', }, + displayAssessmentAs: { + id: 'course.assessment.form.displayAssessmentAs', + defaultMessage: 'Display assessment as', + }, tabbedView: { id: 'course.assessment.form.tabbedView', defaultMessage: 'Tabbed View', From d28dc087730e84e9ecf4b959e0e30016d39fcc6a Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Wed, 24 Aug 2022 15:37:25 +0800 Subject: [PATCH 19/37] fix(section): header stuck too low on new assessment page --- .../components/AssessmentForm/index.jsx | 24 ++++++++++++---- client/app/lib/components/layouts/Section.tsx | 28 ++++++++++++------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx index fc352537c31..6c59df98f02 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx @@ -232,7 +232,10 @@ const AssessmentForm = (props) => { > -
+
{
{gamified && ( -
+
{
)} -
+
{intl.formatMessage(t.gradingMode)} @@ -551,7 +557,10 @@ const AssessmentForm = (props) => { />
-
+
{
{editing && renderTabs(loadedTabs, disabled)} @@ -664,7 +673,10 @@ const AssessmentForm = (props) => { />
-
+
} = { marginBottom: { - marginBottom: 1, + marginBottom: 2, }, - stickyHeadersLgOnly: (theme) => ({ - [theme.breakpoints.up('lg')]: { - position: 'sticky', - top: '5rem', - alignSelf: 'flex-start', - }, - }), content: { '> *:not(:last-child)': { marginBottom: 2 }, }, + container: { + '& + &': { marginTop: 2 }, + }, }; const Section = (props: SectionProps): JSX.Element => { return ( - + - + ({ + [theme.breakpoints.up('lg')]: { + position: 'sticky', + top: props.sticksToNavbar ? '5rem' : '-1em', + alignSelf: 'flex-start', + }, + })} + > {props.title ? ( {props.title} From 8b0725f003c29e5bfcadff69abb7072e9cd548a2 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Sun, 28 Aug 2022 16:52:59 +0800 Subject: [PATCH 20/37] style(assessmentform): abstract yup validation out --- .../components/AssessmentForm/index.jsx | 84 +---------------- .../AssessmentForm/useFormValidation.tsx | 93 +++++++++++++++++++ 2 files changed, 97 insertions(+), 80 deletions(-) create mode 100644 client/app/bundles/course/assessment/components/AssessmentForm/useFormValidation.tsx diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx index 6c59df98f02..d2c66116bd1 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx @@ -3,9 +3,7 @@ import { useEffect } from 'react'; import PropTypes from 'prop-types'; import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { Controller, useForm } from 'react-hook-form'; -import * as yup from 'yup'; +import { Controller } from 'react-hook-form'; import { RadioGroup, Typography, Grid } from '@mui/material'; import { Public as PublishedIcon, @@ -23,7 +21,6 @@ import FormToggleField from 'lib/components/form/fields/ToggleField'; import FormCheckboxField from 'lib/components/form/fields/CheckboxField'; import ErrorText from 'lib/components/ErrorText'; import ConditionList from 'lib/components/course/ConditionList'; -import formTranslations from 'lib/translations/form'; import Section from 'lib/components/layouts/Section'; import { achievementTypesConditionAttributes, typeMaterial } from 'lib/types'; import ReactTooltip from 'react-tooltip'; @@ -32,83 +29,12 @@ import FileManager from '../FileManager'; import IconRadio from '../IconRadio'; import InfoLabel from '../InfoLabel'; import { fetchTabs } from './actions'; +import useFormValidation from './useFormValidation'; const styles = { conditions: { marginTop: 24 }, }; -const validationSchema = yup.object({ - title: yup.string().required(formTranslations.required), - tab_id: yup.number(), - description: yup.string(), - start_at: yup - .date() - .nullable() - .typeError(formTranslations.invalidDate) - .required(formTranslations.required), - end_at: yup - .date() - .nullable() - .typeError(formTranslations.invalidDate) - .min(yup.ref('start_at'), t.startEndValidationError), - bonus_end_at: yup - .date() - .nullable() - .typeError(formTranslations.invalidDate) - .min(yup.ref('start_at'), t.startEndValidationError), - base_exp: yup - .number() - .typeError(formTranslations.required) - .required(formTranslations.required), - time_bonus_exp: yup - .number() - .nullable(true) - .transform((_, val) => (val === Number(val) ? val : null)), - published: yup.bool(), - autograded: yup.bool(), - block_student_viewing_after_submitted: yup.bool(), - skippable: yup.bool(), - allow_partial_submission: yup.bool(), - show_mcq_answer: yup.bool(), - tabbed_view: yup.bool().when('autograded', { - is: false, - then: yup.bool().required(formTranslations.required), - }), - delayed_grade_publication: yup.bool(), - password_protected: yup - .bool() - .when( - ['view_password', 'session_password'], - (view_password, session_password, schema) => - schema.test({ - test: (password_protected) => - // Check if there is at least 1 password type when password_protectd - // is enabled. - password_protected ? session_password || view_password : true, - message: t.passwordRequired, - }), - ), - view_password: yup.string().nullable(), - session_password: yup.string().nullable(), - show_mcq_mrq_solution: yup.bool(), - use_public: yup.bool(), - use_private: yup.bool(), - use_evaluation: yup - .bool() - .when(['use_public', 'use_private'], (use_public, use_private, schema) => - schema.test({ - // Check if there is at least 1 selected test case. - test: (use_evaluation) => use_public || use_private || use_evaluation, - message: t.noTestCaseChosenError, - }), - ), - show_private: yup.bool(), - show_evaluation: yup.bool(), - randomization: yup.bool(), - has_personal_times: yup.bool(), - affects_personal_times: yup.bool(), -}); - const AssessmentForm = (props) => { const { conditionAttributes, @@ -132,10 +58,8 @@ const AssessmentForm = (props) => { setError, watch, formState: { errors }, - } = useForm({ - defaultValues: initialValues, - resolver: yupResolver(validationSchema), - }); + } = useFormValidation(initialValues); + const autograded = watch('autograded'); const passwordProtected = watch('password_protected'); diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/useFormValidation.tsx b/client/app/bundles/course/assessment/components/AssessmentForm/useFormValidation.tsx new file mode 100644 index 00000000000..2dddac63986 --- /dev/null +++ b/client/app/bundles/course/assessment/components/AssessmentForm/useFormValidation.tsx @@ -0,0 +1,93 @@ +// @ts-nocheck +// Disable type-checking because as of yup 0.32.11, arguments types +// for yup.when(['a', 'b'], (a, b, schema) => ...) cannot be resolved. +// This is a known issue: https://github.com/jquense/yup/issues/1529 +// Probably fixed in yup 1.0+ with a new function signature with array destructuring +// https://github.com/jquense/yup#:~:text=isBig%27%2C%20(-,%5BisBig%5D,-%2C%20schema) + +import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useForm, UseFormReturn } from 'react-hook-form'; + +import ft from 'lib/translations/form'; +import t from './translations.intl'; + +const validationSchema = yup.object({ + title: yup.string().required(ft.required), + tab_id: yup.number(), + description: yup.string(), + start_at: yup + .date() + .nullable() + .typeError(ft.invalidDate) + .required(ft.required), + end_at: yup + .date() + .nullable() + .typeError(ft.invalidDate) + .min(yup.ref('start_at'), t.startEndValidationError), + bonus_end_at: yup + .date() + .nullable() + .typeError(ft.invalidDate) + .min(yup.ref('start_at'), t.startEndValidationError), + base_exp: yup.number().typeError(ft.required).required(ft.required), + time_bonus_exp: yup + .number() + .nullable(true) + .transform((_, val) => (val === Number(val) ? val : null)), + published: yup.bool(), + autograded: yup.bool(), + block_student_viewing_after_submitted: yup.bool(), + skippable: yup.bool(), + allow_partial_submission: yup.bool(), + show_mcq_answer: yup.bool(), + tabbed_view: yup.bool().when('autograded', { + is: false, + then: yup.bool().required(ft.required), + }), + delayed_grade_publication: yup.bool(), + password_protected: yup + .bool() + .when( + ['view_password', 'session_password'], + (view_password, session_password, schema: yup.BooleanSchema) => + schema.test({ + test: (password_protected) => + // Check if there is at least 1 password type when password_protectd + // is enabled. + password_protected ? session_password || view_password : true, + message: t.passwordRequired, + }), + ), + view_password: yup.string().nullable(), + session_password: yup.string().nullable(), + show_mcq_mrq_solution: yup.bool(), + use_public: yup.bool(), + use_private: yup.bool(), + use_evaluation: yup + .bool() + .when( + ['use_public', 'use_private'], + (use_public, use_private, schema: yup.BooleanSchema) => + schema.test({ + // Check if there is at least 1 selected test case. + test: (use_evaluation) => use_public || use_private || use_evaluation, + message: t.noTestCaseChosenError, + }), + ), + show_private: yup.bool(), + show_evaluation: yup.bool(), + randomization: yup.bool(), + has_personal_times: yup.bool(), + affects_personal_times: yup.bool(), +}); + +const useFormValidation = (initialValues): UseFormReturn => { + return useForm({ + defaultValues: initialValues, + resolver: yupResolver(validationSchema), + }); +}; + +export default useFormValidation; From e15e9a8bf34b014811e858834b78d26402c37156 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Sun, 28 Aug 2022 18:12:33 +0800 Subject: [PATCH 21/37] style(assessmentform): move to a TypeScript component --- .../AssessmentForm/{index.jsx => index.tsx} | 81 ++++--------------- .../components/AssessmentForm/types.ts | 68 ++++++++++++++++ .../components/FileManager/index.tsx | 2 +- .../components/form/fields/RichTextField.jsx | 3 + .../components/form/fields/SelectField.jsx | 1 + .../lib/components/form/fields/TextField.jsx | 10 +++ 6 files changed, 97 insertions(+), 68 deletions(-) rename client/app/bundles/course/assessment/components/AssessmentForm/{index.jsx => index.tsx} (89%) create mode 100644 client/app/bundles/course/assessment/components/AssessmentForm/types.ts diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx similarity index 89% rename from client/app/bundles/course/assessment/components/AssessmentForm/index.jsx rename to client/app/bundles/course/assessment/components/AssessmentForm/index.tsx index d2c66116bd1..839a01a9d77 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.jsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx @@ -1,8 +1,6 @@ /* eslint-disable camelcase */ import { useEffect } from 'react'; -import PropTypes from 'prop-types'; import { injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; import { Controller } from 'react-hook-form'; import { RadioGroup, Typography, Grid } from '@mui/material'; import { @@ -10,32 +8,29 @@ import { Block as DraftIcon, Create as ManualIcon, CheckCircle as AutogradedIcon, - InfoOutlined as InfoOutlinedIcon, } from '@mui/icons-material'; import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerField'; import FormRichTextField from 'lib/components/form/fields/RichTextField'; import FormSelectField from 'lib/components/form/fields/SelectField'; import FormTextField from 'lib/components/form/fields/TextField'; -import FormToggleField from 'lib/components/form/fields/ToggleField'; import FormCheckboxField from 'lib/components/form/fields/CheckboxField'; import ErrorText from 'lib/components/ErrorText'; import ConditionList from 'lib/components/course/ConditionList'; import Section from 'lib/components/layouts/Section'; -import { achievementTypesConditionAttributes, typeMaterial } from 'lib/types'; -import ReactTooltip from 'react-tooltip'; import t from './translations.intl'; +import IconRadio from 'lib/components/IconRadio'; +import InfoLabel from 'lib/components/InfoLabel'; import FileManager from '../FileManager'; -import IconRadio from '../IconRadio'; -import InfoLabel from '../InfoLabel'; import { fetchTabs } from './actions'; import useFormValidation from './useFormValidation'; +import { connector, AssessmentFormProps } from './types'; const styles = { conditions: { marginTop: 24 }, }; -const AssessmentForm = (props) => { +const AssessmentForm = (props: AssessmentFormProps) => { const { conditionAttributes, containsCodaveri, @@ -69,6 +64,9 @@ const AssessmentForm = (props) => { useEffect(() => { if (editing) { const failureMessage = intl.formatMessage(t.fetchTabFailure); + + // @ts-ignore until Assessment store and a custom dispatch for thunk is fully typed + // https://redux.js.org/tutorials/typescript-quick-start#define-typed-hooks dispatch(fetchTabs(failureMessage)); } }, [dispatch]); @@ -90,7 +88,6 @@ const AssessmentForm = (props) => { }} renderIf={passwordProtected} required - style={styles.flexChild} variant="standard" /> )} @@ -112,7 +109,6 @@ const AssessmentForm = (props) => { }} renderIf={passwordProtected} required - style={styles.flexChild} variant="standard" /> )} @@ -174,7 +170,6 @@ const AssessmentForm = (props) => { shrink: true, }} required - style={styles.flexChild} variant="filled" /> )} @@ -265,7 +260,7 @@ const AssessmentForm = (props) => { ( + render={({ field }) => ( { label={intl.formatMessage(t.baseExp)} InputLabelProps={{ shrink: true }} onWheel={(event) => event.currentTarget.blur()} - style={styles.flexChild} type="number" variant="filled" /> @@ -350,7 +344,6 @@ const AssessmentForm = (props) => { shrink: true, }} onWheel={(event) => event.currentTarget.blur()} - style={styles.flexChild} type="number" variant="filled" /> @@ -375,9 +368,9 @@ const AssessmentForm = (props) => { {intl.formatMessage(t.gradingMode)} - {!modeSwitching ? ( - {intl.formatMessage(t.modeSwitchingDisabled)} - ) : null} + {!modeSwitching && ( + + )} {containsCodaveri ? ( @@ -388,7 +381,7 @@ const AssessmentForm = (props) => { ( + render={({ field }) => ( <> { fieldState={fieldState} disabled={disabled} label={intl.formatMessage(t.usePublic)} - style={styles.flexChild} /> )} /> @@ -443,7 +435,6 @@ const AssessmentForm = (props) => { fieldState={fieldState} disabled={disabled} label={intl.formatMessage(t.usePrivate)} - style={styles.flexChild} /> )} /> @@ -456,7 +447,6 @@ const AssessmentForm = (props) => { fieldState={fieldState} disabled={disabled} label={intl.formatMessage(t.useEvaluation)} - style={styles.flexChild} /> )} /> @@ -533,7 +523,6 @@ const AssessmentForm = (props) => { fieldState={fieldState} disabled={disabled} label={intl.formatMessage(t.showPrivate)} - style={styles.toggle} /> )} /> @@ -546,7 +535,6 @@ const AssessmentForm = (props) => { fieldState={fieldState} disabled={disabled} label={intl.formatMessage(t.showEvaluation)} - style={styles.toggle} /> )} /> @@ -569,7 +557,7 @@ const AssessmentForm = (props) => { title={intl.formatMessage(t.organisation)} sticksToNavbar={editing} > - {editing && renderTabs(loadedTabs, disabled)} + {editing && renderTabs()} ({ tabs: state.editPage.tabs })); + +export interface AssessmentFormProps + extends WrappedComponentProps, + ConnectedProps { + tabs: Tab[]; + onSubmit: (data: FieldValues, setError: UseFormSetError) => void; + + initialValues?; + disabled?: boolean; + showPersonalizedTimelineFeatures?: boolean; + randomizationAllowed?: boolean; + folderAttributes?: FolderAttributes; + conditionAttributes?: AchievementTypesConditionAttributes; + + /** + * If `true`, this component is displayed on Edit Assessment page + */ + editing?: boolean; + + /** + * If `true`, this course is gamified + */ + gamified?: boolean; + + /** + * If `true`, Autograded and Manual grading modes can be changed + */ + modeSwitching?: boolean; + + containsCodaveri?: boolean; +} diff --git a/client/app/bundles/course/assessment/components/FileManager/index.tsx b/client/app/bundles/course/assessment/components/FileManager/index.tsx index 8116895de97..1b834197eb4 100644 --- a/client/app/bundles/course/assessment/components/FileManager/index.tsx +++ b/client/app/bundles/course/assessment/components/FileManager/index.tsx @@ -12,7 +12,7 @@ import t from './translations.intl'; import CourseAPI from 'api/course'; import { formatLongDateTime } from 'lib/moment'; -interface Material { +export interface Material { id?: number; name?: string; updated_at?: string; diff --git a/client/app/lib/components/form/fields/RichTextField.jsx b/client/app/lib/components/form/fields/RichTextField.jsx index f5d3aaf0e75..84afcd4ff20 100644 --- a/client/app/lib/components/form/fields/RichTextField.jsx +++ b/client/app/lib/components/form/fields/RichTextField.jsx @@ -21,6 +21,9 @@ FormRichTextField.propTypes = { fieldState: PropTypes.object.isRequired, disabled: PropTypes.bool, label: PropTypes.node, + fullWidth: PropTypes.bool, + InputLabelProps: PropTypes.object, + variant: PropTypes.string, }; export default memo(FormRichTextField, propsAreEqual); diff --git a/client/app/lib/components/form/fields/SelectField.jsx b/client/app/lib/components/form/fields/SelectField.jsx index 839f5439eab..5660fc8765d 100644 --- a/client/app/lib/components/form/fields/SelectField.jsx +++ b/client/app/lib/components/form/fields/SelectField.jsx @@ -115,6 +115,7 @@ FormSelectField.propTypes = { displayEmpty: PropTypes.bool, className: PropTypes.string, variant: PropTypes.string, + type: PropTypes.string, }; export default memo(FormSelectField, propsAreEqual); diff --git a/client/app/lib/components/form/fields/TextField.jsx b/client/app/lib/components/form/fields/TextField.jsx index 386900805ae..666412afc9f 100644 --- a/client/app/lib/components/form/fields/TextField.jsx +++ b/client/app/lib/components/form/fields/TextField.jsx @@ -105,6 +105,16 @@ FormTextField.propTypes = { renderIf: PropTypes.bool, margins: PropTypes.bool, enableDebouncing: PropTypes.bool, + fullWidth: PropTypes.bool, + InputLabelProps: PropTypes.object, + onWheel: PropTypes.func, + type: PropTypes.string, + variant: PropTypes.string, + required: PropTypes.bool, + placeholder: PropTypes.string, + id: PropTypes.string, + sx: PropTypes.object, + className: PropTypes.string, }; export default FormTextField; From 08423549e5881eddb06aaf4e7aa7e59f4e395377 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Sun, 28 Aug 2022 20:14:37 +0800 Subject: [PATCH 22/37] fix(selectfield): unoverridable margin --- client/app/lib/components/form/fields/SelectField.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/lib/components/form/fields/SelectField.jsx b/client/app/lib/components/form/fields/SelectField.jsx index 5660fc8765d..725710df654 100644 --- a/client/app/lib/components/form/fields/SelectField.jsx +++ b/client/app/lib/components/form/fields/SelectField.jsx @@ -51,7 +51,7 @@ const FormSelectField = (props) => { disabled={disabled} error={isError} fullWidth - style={{ margin: margin ?? styles.selectFieldStyle.margin }} + sx={{ margin: margin ?? styles.selectFieldStyle.margin }} variant={variant} > {label} From 8cae6a4fbedbbb807286837ae22c7d6d87a47841 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Sun, 28 Aug 2022 20:15:55 +0800 Subject: [PATCH 23/37] feat(textfield): support for password visibility --- client/app/lib/components/TextField.tsx | 53 +++++++++++++++++++ .../lib/components/form/fields/TextField.jsx | 28 +++++----- 2 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 client/app/lib/components/TextField.tsx diff --git a/client/app/lib/components/TextField.tsx b/client/app/lib/components/TextField.tsx new file mode 100644 index 00000000000..9f8ef858cb6 --- /dev/null +++ b/client/app/lib/components/TextField.tsx @@ -0,0 +1,53 @@ +import { useState, forwardRef, ComponentProps } from 'react'; +import { + InputAdornment, + TextField as MuiTextField, + Typography, + IconButton, +} from '@mui/material'; +import { VisibilityOff, Visibility } from '@mui/icons-material'; + +type TextFieldProps = ComponentProps & { + description?: string; +}; + +const TextField = forwardRef( + (props, ref): JSX.Element => { + const { description, ...rest } = props; + + const [showPassword, setShowPassword] = useState(false); + + return ( + <> + + setShowPassword((state) => !state)} + onMouseDown={(e) => e.preventDefault()} + edge="end" + > + {showPassword ? : } + + + ), + }, + })} + /> + + {description && ( + + {description} + + )} + + ); + }, +); + +export default TextField; diff --git a/client/app/lib/components/form/fields/TextField.jsx b/client/app/lib/components/form/fields/TextField.jsx index 666412afc9f..24191644f91 100644 --- a/client/app/lib/components/form/fields/TextField.jsx +++ b/client/app/lib/components/form/fields/TextField.jsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react'; import PropTypes from 'prop-types'; import { debounce } from 'lodash'; -import { TextField } from '@mui/material'; +import TextField from 'lib/components/TextField'; import { formatErrorMessage } from 'lib/components/form/fields/utils/mapError'; import { FIELD_DEBOUNCE_DELAY } from 'lib/constants/sharedConstants'; @@ -10,13 +10,9 @@ const styles = { empty: { margin: '0px 0px 0px 0px' }, }; -const onlyNumberInput = (evt) => { - if (evt.which === 8) { - return; - } - if (evt.which < 48 || evt.which > 57) { - evt.preventDefault(); - } +const onlyNumberInput = (e) => { + if (e.which === 8) return; + if (e.which < 48 || e.which > 57) e.preventDefault(); }; const FormTextField = (props) => { @@ -30,10 +26,10 @@ const FormTextField = (props) => { enableDebouncing = false, ...custom } = props; + + if (!renderIf) return null; + const [ownValue, setOwnValue] = useState(field.value); - if (!renderIf) { - return null; - } // Debounced function to sync the value of this component with the form. This helps to reduce the cost of re-rendering // the entire form when the form state changes, especially in large forms. @@ -64,9 +60,8 @@ const FormTextField = (props) => { }; const handleKeyPress = (e) => { - if (custom.type === 'number') { - onlyNumberInput(e); - } + if (custom.type === 'number') onlyNumberInput(e); + // To remove trailing whitespace when clicking enter within the field. if (e.charCode === 13) { setOwnValue(e.target.value.trim()); // Update internal field value @@ -77,7 +72,7 @@ const FormTextField = (props) => { return ( { helperText={ fieldState.error && formatErrorMessage(fieldState.error.message) } + {...(margins ? { style: styles.textFieldStyle } : null)} {...custom} - style={margins ? styles.textFieldStyle : styles.empty} /> ); }; @@ -115,6 +110,7 @@ FormTextField.propTypes = { id: PropTypes.string, sx: PropTypes.object, className: PropTypes.string, + description: PropTypes.string, }; export default FormTextField; From fd593c56a68dbae19cdb42d41d16f766d6f3ec60 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar Date: Sun, 28 Aug 2022 20:23:48 +0800 Subject: [PATCH 24/37] fix(assessmentform): clean up margins, shrinking labels --- .../components/AssessmentForm/index.tsx | 34 ++++++--------- .../app/lib/components/CKEditorRichText.tsx | 43 ++++++++++++------- .../components/form/fields/CheckboxField.tsx | 15 ++----- .../form/fields/DateTimePickerField.jsx | 7 +-- .../components/form/fields/RichTextField.jsx | 1 + 5 files changed, 51 insertions(+), 49 deletions(-) diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx index 839a01a9d77..57063b946fe 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx @@ -26,10 +26,6 @@ import { fetchTabs } from './actions'; import useFormValidation from './useFormValidation'; import { connector, AssessmentFormProps } from './types'; -const styles = { - conditions: { marginTop: 24 }, -}; - const AssessmentForm = (props: AssessmentFormProps) => { const { conditionAttributes, @@ -47,6 +43,7 @@ const AssessmentForm = (props: AssessmentFormProps) => { tabs, intl, } = props; + const { control, handleSubmit, @@ -137,6 +134,7 @@ const AssessmentForm = (props: AssessmentFormProps) => { label={intl.formatMessage(t.tab)} options={options} variant="filled" + margin="0" /> )} /> @@ -166,11 +164,9 @@ const AssessmentForm = (props: AssessmentFormProps) => { disabled={disabled} label={intl.formatMessage(t.title)} fullWidth - InputLabelProps={{ - shrink: true, - }} required variant="filled" + margins={false} /> )} /> @@ -188,6 +184,7 @@ const AssessmentForm = (props: AssessmentFormProps) => { label={intl.formatMessage(t.startAt)} variant="filled" disableMargins + disableShrinkingLabel /> )} /> @@ -205,6 +202,7 @@ const AssessmentForm = (props: AssessmentFormProps) => { label={intl.formatMessage(t.endAt)} variant="filled" disableMargins + disableShrinkingLabel /> )} /> @@ -223,6 +221,7 @@ const AssessmentForm = (props: AssessmentFormProps) => { label={intl.formatMessage(t.bonusEndAt)} variant="filled" disableMargins + disableShrinkingLabel /> )} /> @@ -243,10 +242,8 @@ const AssessmentForm = (props: AssessmentFormProps) => { fieldState={fieldState} disabled={disabled} fullWidth - InputLabelProps={{ - shrink: true, - }} variant="standard" + disableMargins /> )} /> @@ -320,10 +317,10 @@ const AssessmentForm = (props: AssessmentFormProps) => { disabled={disabled} fullWidth label={intl.formatMessage(t.baseExp)} - InputLabelProps={{ shrink: true }} onWheel={(event) => event.currentTarget.blur()} type="number" variant="filled" + margins={false} /> )} /> @@ -340,12 +337,10 @@ const AssessmentForm = (props: AssessmentFormProps) => { disabled={disabled} fullWidth label={intl.formatMessage(t.timeBonusExp)} - InputLabelProps={{ - shrink: true, - }} onWheel={(event) => event.currentTarget.blur()} type="number" variant="filled" + margins={false} /> )} /> @@ -353,12 +348,10 @@ const AssessmentForm = (props: AssessmentFormProps) => { {editing && conditionAttributes && ( -
- -
+ )}
)} @@ -580,6 +573,7 @@ const AssessmentForm = (props: AssessmentFormProps) => { ]} type="boolean" variant="filled" + margin="0" /> )} /> diff --git a/client/app/lib/components/CKEditorRichText.tsx b/client/app/lib/components/CKEditorRichText.tsx index 9051b239771..2ef2479b4db 100644 --- a/client/app/lib/components/CKEditorRichText.tsx +++ b/client/app/lib/components/CKEditorRichText.tsx @@ -15,11 +15,21 @@ interface Props { required?: boolean | undefined; name: string; inputId: string; + disableMargins?: boolean; } const CKEditorRichText: FC = forwardRef((props: Props, ref) => { - const { label, value, onChange, disabled, field, required, name, inputId } = - props; + const { + label, + value, + onChange, + disabled, + field, + required, + name, + inputId, + disableMargins, + } = props; const [isFocused, setIsFocused] = useState(false); const testFieldLabelColor = isFocused ? cyan[500] : undefined; @@ -73,21 +83,24 @@ const CKEditorRichText: FC = forwardRef((props: Props, ref) => { backgroundColor: 'transparent', fontFamily: 'Roboto, sans-serif', paddingTop: label ? '1em' : 0, - paddingBottom: '1em', + paddingBottom: !disableMargins ? '1em' : 0, }} > - - {label} - + {label && ( + + {label} + + )} +