Skip to content

Commit

Permalink
Merge pull request #23 from isaaccomputerscience/Hannah/context-recon…
Browse files Browse the repository at this point in the history
…firmation

Hannah/context reconfirmation
  • Loading branch information
hannah-whelan authored Jun 21, 2023
2 parents f49fb04 + fac82dd commit 828e9c6
Show file tree
Hide file tree
Showing 18 changed files with 13,008 additions and 9,122 deletions.
4 changes: 2 additions & 2 deletions src/app/components/elements/inputs/GenderInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ interface GenderInputProps {
required: boolean;
}
export const GenderInput = ({userToUpdate, setUserToUpdate, submissionAttempted, idPrefix="account", required}: GenderInputProps) => {
return <RS.FormGroup className="my-1">
<RS.Label htmlFor={`${idPrefix}-gender-select`} className={classNames({"form-required": required})}>
return <RS.FormGroup>
<RS.Label htmlFor={`${idPrefix}-gender-select`}>
Gender
</RS.Label>
<Input
Expand Down
16 changes: 6 additions & 10 deletions src/app/components/elements/inputs/SchoolInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as RS from "reactstrap";
import {School, ValidationUser} from "../../../../IsaacAppTypes";
import {api, schoolNameWithPostcode, validateUserSchool} from "../../../services";
import {throttle} from "lodash";
import classNames from "classnames";
import {Immutable} from "immer";

interface SchoolInputProps {
Expand All @@ -29,7 +28,7 @@ const schoolSearch = (schoolSearchText: string, setAsyncSelectOptionsCallback: (
const throttledSchoolSearch = throttle(schoolSearch, 450, {trailing: true, leading: true});

export const SchoolInput = ({userToUpdate, setUserToUpdate, submissionAttempted, className, idPrefix="school", disableInput, required}: SchoolInputProps) => {
let [selectedSchoolObject, setSelectedSchoolObject] = useState<School | null>();
const [selectedSchoolObject, setSelectedSchoolObject] = useState<School | null>();

// Get school associated with urn
function fetchSchool(urn: string) {
Expand Down Expand Up @@ -79,11 +78,11 @@ export const SchoolInput = ({userToUpdate, setUserToUpdate, submissionAttempted,
undefined))
);

let randomNumber = Math.random();
const randomNumber = Math.random();

const isInvalid = submissionAttempted && required && !validateUserSchool(userToUpdate);
return <RS.FormGroup className={`school ${className}`}>
<RS.Label htmlFor={`school-input-${randomNumber}`} className={classNames({"form-required": required})}>School</RS.Label>
<RS.Label htmlFor={`school-input-${randomNumber}`} >My current school</RS.Label>
{userToUpdate.schoolOther !== NOT_APPLICABLE && <React.Fragment>
<AsyncCreatableSelect
isClearable
Expand All @@ -100,7 +99,7 @@ export const SchoolInput = ({userToUpdate, setUserToUpdate, submissionAttempted,
/>
</React.Fragment>}

{((userToUpdate.schoolOther == undefined && !(selectedSchoolObject && selectedSchoolObject.name)) || userToUpdate.schoolOther == NOT_APPLICABLE) && <div className="d-flex mt-2">
{((userToUpdate.schoolOther == undefined && !(selectedSchoolObject && selectedSchoolObject.name)) || userToUpdate.schoolOther == NOT_APPLICABLE) && <div className={(userToUpdate.schoolOther === NOT_APPLICABLE ? "mt-1": "mt-3") + " d-flex"}>
<RS.CustomInput
type="checkbox" id={`${idPrefix}-not-associated-with-school`}
checked={userToUpdate.schoolOther === NOT_APPLICABLE}
Expand All @@ -114,12 +113,9 @@ export const SchoolInput = ({userToUpdate, setUserToUpdate, submissionAttempted,
setUserToUpdate?.(userWithoutSchoolInfo);
}
})}
label="Not associated with a school"
label="I am not associated with a school"
className="larger-checkbox"
/>
</div>}

<div className="invalid-school">
{submissionAttempted && required && !validateUserSchool(userToUpdate) ? "Please specify your school association" : null}
</div>
</RS.FormGroup>
};
75 changes: 39 additions & 36 deletions src/app/components/elements/inputs/UserContextAccountInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useRef} from "react";
import {BooleanNotation, DisplaySettings, ValidationUser} from "../../../../IsaacAppTypes";
import {BooleanNotation, DisplaySettings} from "../../../../IsaacAppTypes";
import {
EMPTY_BOOLEAN_NOTATION_RECORD,
EXAM_BOARD,
Expand All @@ -8,17 +8,16 @@ import {
getFilteredStageOptions,
isDefined,
isTutorOrAbove,
STAGE, TEACHER_REQUEST_ROUTE
STAGE
} from "../../../services";
import * as RS from "reactstrap";
import {CustomInput, Input} from "reactstrap";
import {UserContext} from "../../../../IsaacApiTypes";
import {v4 as uuid_v4} from "uuid";
import {Link} from "react-router-dom";
import classNames from "classnames";
import {Immutable} from "immer";
import { selectors, useAppSelector } from "../../../state";

interface UserContextRowProps {
isStudent?: boolean;
userContext: UserContext;
setUserContext: (ucs: UserContext) => void;
showNullStageOption: boolean;
Expand All @@ -29,13 +28,14 @@ interface UserContextRowProps {
}

function UserContextRow({
userContext, setUserContext, showNullStageOption, submissionAttempted, existingUserContexts, setBooleanNotation, setDisplaySettings
isStudent, userContext, setUserContext, showNullStageOption, submissionAttempted, existingUserContexts, setBooleanNotation, setDisplaySettings
}: UserContextRowProps) {
const onlyUCWithThisStage = existingUserContexts.filter(uc => uc.stage === userContext.stage).length === 1;
return <React.Fragment>
return <>
<RS.Col xs={5} md={5} lg={4} className="pr-1">
{/* Stage Selector */}
<Input
className="form-control w-auto d-inline-block pl-1 pr-0 mt-1 mt-sm-0" type="select"
className="form-control w-100 d-inline-block pl-1 pr-10" type="select"
aria-label="Stage"
value={userContext.stage || ""}
invalid={submissionAttempted && !Object.values(STAGE).includes(userContext.stage as STAGE)}
Expand All @@ -56,15 +56,17 @@ function UserContextRow({
<option value=""></option>
{getFilteredStageOptions({
byUserContexts: existingUserContexts.filter(uc => !(uc.stage === userContext.stage && uc.examBoard === userContext.examBoard)),
includeNullOptions: showNullStageOption, hideFurtherA: true
includeNullOptions: showNullStageOption, hideFurtherA: true,
byRole: isStudent ? true : undefined
}).map(item =>
<option key={item.value} value={item.value}>{item.label}</option>
)}
</Input>

</RS.Col>
<RS.Col xs={5} md={5} lg={4} className="pl-1">
{/* Exam Board Selector */}
<Input
className="form-control w-auto d-inline-block pl-1 pr-0 ml-sm-2 mt-1 mt-sm-0" type="select"
className="form-control w-100 d-inline-block pl-1 pr-10 ml-2" type="select"
aria-label="Exam Board"
value={userContext.examBoard || ""}
invalid={submissionAttempted && !Object.values(EXAM_BOARD).includes(userContext.examBoard as EXAM_BOARD)}
Expand All @@ -86,11 +88,11 @@ function UserContextRow({
)
}
</Input>
</React.Fragment>
</RS.Col>
</>
}

interface UserContextAccountInputProps {
user: Immutable<ValidationUser>;
userContexts: UserContext[];
setUserContexts: (ucs: UserContext[]) => void;
setBooleanNotation: (bn: BooleanNotation) => void;
Expand All @@ -99,14 +101,16 @@ interface UserContextAccountInputProps {
submissionAttempted: boolean;
}
export function UserContextAccountInput({
user, userContexts, setUserContexts, displaySettings, setDisplaySettings, setBooleanNotation, submissionAttempted,
userContexts, setUserContexts, displaySettings, setDisplaySettings, setBooleanNotation, submissionAttempted,
}: UserContextAccountInputProps) {
const user = useAppSelector(selectors.user.orNull);
const tutorOrAbove = isTutorOrAbove({...user, loggedIn: true});
const studyingOrTeaching = tutorOrAbove ? 'teaching' : 'studying';
const componentId = useRef(uuid_v4().slice(0, 4)).current;

return <div>
<RS.Label htmlFor="user-context-selector" className="form-required">
<span>Show me content for:</span>
return <>
<RS.Label htmlFor="user-context-selector">
<span>I am {studyingOrTeaching}</span>
</RS.Label>
<React.Fragment>
<span id={`show-me-content-${componentId}`} className="icon-help" />
Expand All @@ -117,60 +121,59 @@ export function UserContextAccountInput({
}
</RS.UncontrolledTooltip>
</React.Fragment>
<div id="user-context-selector" className={classNames({"d-flex flex-wrap": false})}>
<div id="user-context-selector">
{userContexts.map((userContext, index) => {
const showPlusOption = tutorOrAbove &&
index === userContexts.length - 1 &&
// at least one exam board for the potential stage
getFilteredStageOptions({byUserContexts: userContexts, hideFurtherA: true}).length > 0;

return <RS.FormGroup key={index}>
<UserContextRow
<RS.Row>
<UserContextRow isStudent={!tutorOrAbove}
userContext={userContext} showNullStageOption={userContexts.length <= 1} submissionAttempted={submissionAttempted}
setUserContext={newUc => setUserContexts(userContexts.map((uc, i) => i === index ? newUc : uc))}
existingUserContexts={userContexts} setBooleanNotation={setBooleanNotation} setDisplaySettings={setDisplaySettings}
/>

{tutorOrAbove && userContexts.length > 1 && <button
type="button" className="mx-2 close float-none align-middle" aria-label="clear stage row"
onClick={() => setUserContexts(userContexts.filter((uc, i) => i !== index))}
>
×
</button>}

{showPlusOption && <RS.Label>
</RS.Row>

{showPlusOption && <RS.Row className="mt-3 ml-0"><RS.Label className="vertical-center">
<button
type="button" aria-label="Add stage"
className={`${userContexts.length <= 1 ? "ml-2" : ""} align-middle close float-none pointer-cursor`}
className="align-middle close float-none pointer-cursor"
onClick={() => setUserContexts([...userContexts, {}])}
>
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</button>
<span className="ml-1">add stage</span>
</RS.Label>}
<span className="ml-2 mt-1 pointer-cursor">Add another stage</span>
</RS.Label></RS.Row>}

{index === userContexts.length - 1 && (userContexts.findIndex(p => p.stage === STAGE.ALL && p.examBoard === EXAM_BOARD.ALL) === -1) && <RS.Label className="mt-2">
{index === userContexts.length - 1 && (userContexts.findIndex(p => p.stage === STAGE.ALL && p.examBoard === EXAM_BOARD.ALL) === -1) && <RS.Label className="m-0 mt-3">
<CustomInput
type="checkbox" id={`hide-content-check-${componentId}`} className="d-inline-block"
type="checkbox" id={`hide-content-check-${componentId}`} className="d-inline-block larger-checkbox"
checked={isDefined(displaySettings?.HIDE_NON_AUDIENCE_CONTENT) ? !displaySettings?.HIDE_NON_AUDIENCE_CONTENT : true}
onChange={e => setDisplaySettings(oldDs => ({...oldDs, HIDE_NON_AUDIENCE_CONTENT: !e.target.checked}))}
/>{" "}
<span>Show other content that is not for my selected qualification(s). <span id={`show-other-content-${componentId}`} className="icon-help ml-1" /></span>
<span>Show other content that is not for my selected exam board. <span id={`show-other-content-${componentId}`} className="icon-help ml-1" /></span>
<RS.UncontrolledTooltip placement="bottom" target={`show-other-content-${componentId}`}>
{tutorOrAbove ?
"If you select this box, additional content that is not intended for your chosen stage and examination board will be shown (e.g. you will also see A level content in your GCSE view)." :
"If you select this box, additional content that is not intended for your chosen stage and examination board will be shown (e.g. you will also see A level content if you are studying GCSE)."
}
</RS.UncontrolledTooltip>
</RS.Label>}

{!tutorOrAbove && <><br/>
<small>
If you are a teacher or tutor, <Link to={TEACHER_REQUEST_ROUTE} target="_blank">upgrade your account</Link> to choose more than one exam board and stage.
</small>
</>}
</RS.FormGroup>
})}
</div>
</div>
</>
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@ const RequiredAccountInfoBody = () => {
const allUserFieldsAreValid = validateUserSchool(initialUserValue) && validateUserGender(initialUserValue) && validateUserContexts(initialUserContexts);

return <RS.Form onSubmit={formSubmission}>
{!allUserFieldsAreValid && <RS.CardBody className="py-0">
<div className="text-right text-muted required-before">
Required
</div>
{!allUserFieldsAreValid && <RS.CardBody className="p-0">
{!isTutorOrAbove(user) && <div className="text-left mb-4">
Account type: <b>{user?.loggedIn && user.role && UserFacingRole[user.role]}</b> <span>
<small>(Are you a teacher or tutor? {" "}
Expand All @@ -78,6 +75,13 @@ const RequiredAccountInfoBody = () => {
</div>}

<RS.Row className="d-flex flex-wrap my-2">
{!validateUserSchool(initialUserValue) && <RS.Col>
<SchoolInput
userToUpdate={userToUpdate} setUserToUpdate={setUserToUpdate}
submissionAttempted={submissionAttempted} idPrefix="modal"
required={!("role" in userToUpdate && isTutor(userToUpdate))}
/>
</RS.Col>}
{((!validateUserGender(initialUserValue)) || !validateUserContexts(initialUserContexts)) && <RS.Col lg={6}>
{!validateUserGender(initialUserValue) && <div className="mb-3">
<GenderInput
Expand All @@ -86,29 +90,21 @@ const RequiredAccountInfoBody = () => {
required
/>
</div>}
{!validateUserContexts(initialUserContexts) && <div>
</RS.Col>}
</RS.Row>
<RS.Row>
{!validateUserContexts(initialUserContexts) && <RS.Col>
<UserContextAccountInput
user={userToUpdate} userContexts={userContexts} setUserContexts={setUserContexts}
userContexts={userContexts} setUserContexts={setUserContexts}
displaySettings={displaySettings} setDisplaySettings={setDisplaySettings}
setBooleanNotation={setBooleanNotation} submissionAttempted={submissionAttempted}
/>
</div>}
</RS.Col>}
{!validateUserSchool(initialUserValue) && <RS.Col>
<SchoolInput
userToUpdate={userToUpdate} setUserToUpdate={setUserToUpdate}
submissionAttempted={submissionAttempted} idPrefix="modal"
required={!("role" in userToUpdate && isTutor(userToUpdate))}
/>
</RS.Col>}
</RS.Col>}

</RS.Row>
<div className="text-muted small pb-2">
Providing a few extra pieces of information helps us understand the usage of Isaac {SITE_SUBJECT_TITLE} across the UK and beyond.
Full details on how we use your personal information can be found in our <a target="_blank" href="/privacy">Privacy Policy</a>.
</div>
</RS.CardBody>}

{!allUserFieldsAreValid && !validateEmailPreferences(initialEmailPreferencesValue) && <RS.CardBody>
{!allUserFieldsAreValid && !validateEmailPreferences(initialEmailPreferencesValue) && <RS.CardBody className="p-0">
<hr className="text-center" />
</RS.CardBody>}

Expand All @@ -119,11 +115,10 @@ const RequiredAccountInfoBody = () => {
/>
</div>}

{submissionAttempted && !allRequiredInformationIsPresent(userToUpdate, userPreferencesToUpdate, userContexts) && <div>
<h4 role="alert" className="text-danger text-center mb-4">
Not all required fields have been correctly filled.
</h4>
</div>}
<div className="text-muted small pb-2">
Providing this information helps us understand the usage of Isaac {SITE_SUBJECT_TITLE}.
Full details on how we use your personal data can be found in our <a target="_blank" href="/privacy">Privacy Policy</a>.
</div>

<RS.CardBody className="py-0">
<RS.Row className="text-center pb-3">
Expand Down
Loading

0 comments on commit 828e9c6

Please sign in to comment.