Loading frontend/src/instructor/InstructorMilestones/AllMilestoneIndicator.tsx +25 −50 Original line number Original line Diff line number Diff line Loading @@ -43,41 +43,22 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ const filteredStates = states.filter( const filteredStates = states.filter( state => milestone.id === state.milestone.id state => milestone.id === state.milestone.id ) ) const subbedTeams = filteredStates.filter(state => const subbedTeams = teamIds.length !== 0 ? filteredStates.filter(state => state.teamIds.some(id => teamIds.includes(id)) state.teamIds.some(id => teamIds.includes(id)) ) ) const reached = filteredStates.every(({ reached }) => reached) : filteredStates const unreachedAll = filteredStates.every(({ reached }) => !reached) const reached = subbedTeams.every(({ reached }) => reached) const areEqualSize = filteredStates.length === subbedTeams.length const unreachedAll = subbedTeams.every(({ reached }) => !reached) const contentTag = areEqualSize const contentTag = `Partially reached ${subbedTeams.filter(state => state.reached).length}/${subbedTeams.length}` ? `Partially reached ${subbedTeams.filter(state => state.reached).length}/${subbedTeams.length}` : `Partially reached ${subbedTeams.filter(state => state.reached).length}/${subbedTeams.length} (for all ${filteredStates.filter(state => state.reached).length}/${filteredStates.length})` const reachedTag = areEqualSize ? 'Reached' : `Reached for given selection (for all ${filteredStates.filter(state => state.reached).length}/${filteredStates.length})` /* /* * the switch will change its state even if the alert is cancelled; * the switch will change its state even if the alert is cancelled; * we track of the state separately to revert it if the alert is cancelled * we track of the state separately to revert it if the alert is cancelled */ */ const [open, setOpen] = useState(false) const [open, setOpen] = useState(false) const confirmAll = async () => { const confirm = async () => { filteredStates subbedTeams .filter(state => state.reached === reached) .forEach(state => { state.teamIds.forEach(teamId => { client .mutation<unknown, VariablesOf<typeof SetMilestone>>(SetMilestone, { milestone: milestone.name, activate: !reached, teamId, }) .toPromise() }) }) setOpen(false) } const confirmSome = async () => { filteredStates .filter(state => state.reached === reached) .filter(state => state.reached === reached) .forEach(state => { .forEach(state => { state.teamIds.forEach(teamId => { state.teamIds.forEach(teamId => { Loading Loading @@ -114,7 +95,7 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ reached ? ( reached ? ( <StyledTag <StyledTag isAchieved isAchieved content={reachedTag} content='Reached' className={timestamp} className={timestamp} /> /> ) : unreachedAll ? ( ) : unreachedAll ? ( Loading @@ -125,7 +106,7 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ /> /> ) : ( ) : ( <StyledTag <StyledTag isAchieved={false} isRead={false} content={contentTag} content={contentTag} className={timestamp} className={timestamp} /> /> Loading Loading @@ -161,10 +142,11 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ className={Classes.ALERT} className={Classes.ALERT} > > <div className={Classes.ALERT_BODY}> <div className={Classes.ALERT_BODY}> <Icon icon='warning-sign' size={40} intent='warning' /> <Icon icon='warning-sign' size={40} intent={teamIds.length === 0 ? 'danger' : 'warning'} /> <div className={Classes.ALERT_CONTENTS}> <div className={Classes.ALERT_CONTENTS}> Are you sure you want to mark this milestone as{' '} Are you sure you want to mark this milestone as{' '} {reached ? 'not reached' : 'reached'} for selected or all teams? {reached ? 'not reached' : 'reached'} for {teamIds.length === 0 ? ' ALL teams' : ' selected teams'} </div> </div> </div> </div> <div <div Loading @@ -173,26 +155,19 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ [css` [css` flex-wrap: wrap; flex-wrap: wrap; gap: 0.5rem; gap: 0.5rem; `]: !areEqualSize, `]: true, })} })} > > {!areEqualSize && ( <Button <Button intent='primary' intent={teamIds.length === 0 ? 'danger' : 'warning'} title='Sets milestone for instructed teams' title={ onClick={confirmSome} teamIds.length === 0 fill={!areEqualSize} ? 'Sets milestone for all teams' > : 'Sets milestone for selected teams' Confirm (for {teamIds.length}) } </Button> onClick={confirm} )} <Button intent='danger' title='Sets milestone for all teams present in the exercise' onClick={confirmAll} fill={!areEqualSize} > > Confirm{!areEqualSize && ' (for all)'} Confirm </Button> </Button> <Button onClick={() => setOpen(false)}>Cancel</Button> <Button onClick={() => setOpen(false)}>Cancel</Button> </div> </div> Loading frontend/src/instructor/InstructorMilestones/index.tsx +6 −8 Original line number Original line Diff line number Diff line Loading @@ -6,17 +6,16 @@ import { GetMilestones, GetMilestones, } from '@inject/graphql/queries' } from '@inject/graphql/queries' import { type FC } from 'react' import { type FC } from 'react' import useGetSelectedTeams from '../InstructorTeamSelector/useGetSelectedTeams' import AllMilestoneIndicator from './AllMilestoneIndicator' import AllMilestoneIndicator from './AllMilestoneIndicator' import MilestoneIndicator from './MilestoneIndicator' import MilestoneIndicator from './MilestoneIndicator' interface InstructorMilestonesProps { interface InstructorMilestonesProps { exerciseId: string exerciseId: string teamId?: string teamIds: string[] } } const InstructorMilestones: FC<InstructorMilestonesProps> = ({ const InstructorMilestones: FC<InstructorMilestonesProps> = ({ teamId, teamIds, exerciseId, exerciseId, }) => { }) => { const [{ data: states }] = useTypedQuery({ const [{ data: states }] = useTypedQuery({ Loading @@ -27,7 +26,6 @@ const InstructorMilestones: FC<InstructorMilestonesProps> = ({ query: GetMilestones, query: GetMilestones, variables: { exerciseId }, variables: { exerciseId }, }) }) const teamIds = useGetSelectedTeams(exerciseId).map(team => team.id) return ( return ( <div <div Loading @@ -41,17 +39,17 @@ const InstructorMilestones: FC<InstructorMilestonesProps> = ({ > > <h3 style={{ order: 0, marginLeft: '0.5rem' }}>Reached</h3> <h3 style={{ order: 0, marginLeft: '0.5rem' }}>Reached</h3> <h3 style={{ order: 2, marginLeft: '0.5rem' }}>Not reached</h3> <h3 style={{ order: 2, marginLeft: '0.5rem' }}>Not reached</h3> {teamId && {teamIds.length === 1 && states?.analyticsMilestones states?.analyticsMilestones .filter(milestone => milestone.teamIds.includes(teamId)) .filter(milestone => milestone.teamIds.includes(teamIds[0])) .map(milestone => ( .map(milestone => ( <MilestoneIndicator <MilestoneIndicator milestone={milestone} milestone={milestone} key={milestone.milestone.id} key={milestone.milestone.id} teamId={teamId} teamId={teamIds[0]} /> /> ))} ))} {!teamId && {teamIds.length !== 1 && milestones?.milestones.map(milestone => ( milestones?.milestones.map(milestone => ( <AllMilestoneIndicator <AllMilestoneIndicator teamIds={teamIds} teamIds={teamIds} Loading frontend/src/views/InstructorView/MilestoneButton.tsx +54 −4 Original line number Original line Diff line number Diff line Loading @@ -4,9 +4,10 @@ import { } from '@/clientsettings/vars/milestones' } from '@/clientsettings/vars/milestones' import Sidebar from '@/components/Sidebar' import Sidebar from '@/components/Sidebar' import InstructorMilestones from '@/instructor/InstructorMilestones' import InstructorMilestones from '@/instructor/InstructorMilestones' import { Button } from '@blueprintjs/core' import useGetSelectedTeams from '@/instructor/InstructorTeamSelector/useGetSelectedTeams' import { Button, ButtonGroup } from '@blueprintjs/core' import { css } from '@emotion/css' import { css } from '@emotion/css' import type { FC } from 'react' import { useLayoutEffect, useState, type FC } from 'react' import { createPortal } from 'react-dom' import { createPortal } from 'react-dom' const MilestonesButton: FC<{ const MilestonesButton: FC<{ Loading @@ -18,6 +19,28 @@ const MilestonesButton: FC<{ const open = useMilestonesOpen() const open = useMilestonesOpen() const portalRef = document.getElementById('rightPanel') const portalRef = document.getElementById('rightPanel') const teamIds = useGetSelectedTeams(exerciseId).map(team => team.id) const [select, setSelect] = useState<null | 'team' | 'selection' | 'all'>( null ) const defaultSelect: typeof select = !select ? teamId ? 'team' : 'selection' : select const selectTeamIds = defaultSelect === 'all' ? [] : defaultSelect === 'team' && teamId ? [teamId] : teamIds useLayoutEffect(() => { if (select !== null) { setSelect(null) } }, [teamId]) return ( return ( <> <> Loading Loading @@ -45,14 +68,41 @@ const MilestonesButton: FC<{ className={css` className={css` width: 22rem; width: 22rem; `} `} title={teamId ? 'Milestones for team' : 'Milestones for all'} title='Milestones' sections={[ sections={[ { id: 'selectors', node: ( <ButtonGroup minimal fill> <Button disabled={defaultSelect === 'all'} onClick={() => setSelect('all')} > All </Button> {teamId && ( <Button disabled={defaultSelect === 'team'} onClick={() => setSelect('team')} > Team </Button> )} <Button disabled={defaultSelect === 'selection'} onClick={() => setSelect('selection')} > Selected </Button> </ButtonGroup> ), }, { { id: 'reached', id: 'reached', node: ( node: ( <InstructorMilestones <InstructorMilestones exerciseId={exerciseId} exerciseId={exerciseId} teamId={teamId} teamIds={selectTeamIds} /> /> ), ), }, }, Loading Loading
frontend/src/instructor/InstructorMilestones/AllMilestoneIndicator.tsx +25 −50 Original line number Original line Diff line number Diff line Loading @@ -43,41 +43,22 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ const filteredStates = states.filter( const filteredStates = states.filter( state => milestone.id === state.milestone.id state => milestone.id === state.milestone.id ) ) const subbedTeams = filteredStates.filter(state => const subbedTeams = teamIds.length !== 0 ? filteredStates.filter(state => state.teamIds.some(id => teamIds.includes(id)) state.teamIds.some(id => teamIds.includes(id)) ) ) const reached = filteredStates.every(({ reached }) => reached) : filteredStates const unreachedAll = filteredStates.every(({ reached }) => !reached) const reached = subbedTeams.every(({ reached }) => reached) const areEqualSize = filteredStates.length === subbedTeams.length const unreachedAll = subbedTeams.every(({ reached }) => !reached) const contentTag = areEqualSize const contentTag = `Partially reached ${subbedTeams.filter(state => state.reached).length}/${subbedTeams.length}` ? `Partially reached ${subbedTeams.filter(state => state.reached).length}/${subbedTeams.length}` : `Partially reached ${subbedTeams.filter(state => state.reached).length}/${subbedTeams.length} (for all ${filteredStates.filter(state => state.reached).length}/${filteredStates.length})` const reachedTag = areEqualSize ? 'Reached' : `Reached for given selection (for all ${filteredStates.filter(state => state.reached).length}/${filteredStates.length})` /* /* * the switch will change its state even if the alert is cancelled; * the switch will change its state even if the alert is cancelled; * we track of the state separately to revert it if the alert is cancelled * we track of the state separately to revert it if the alert is cancelled */ */ const [open, setOpen] = useState(false) const [open, setOpen] = useState(false) const confirmAll = async () => { const confirm = async () => { filteredStates subbedTeams .filter(state => state.reached === reached) .forEach(state => { state.teamIds.forEach(teamId => { client .mutation<unknown, VariablesOf<typeof SetMilestone>>(SetMilestone, { milestone: milestone.name, activate: !reached, teamId, }) .toPromise() }) }) setOpen(false) } const confirmSome = async () => { filteredStates .filter(state => state.reached === reached) .filter(state => state.reached === reached) .forEach(state => { .forEach(state => { state.teamIds.forEach(teamId => { state.teamIds.forEach(teamId => { Loading Loading @@ -114,7 +95,7 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ reached ? ( reached ? ( <StyledTag <StyledTag isAchieved isAchieved content={reachedTag} content='Reached' className={timestamp} className={timestamp} /> /> ) : unreachedAll ? ( ) : unreachedAll ? ( Loading @@ -125,7 +106,7 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ /> /> ) : ( ) : ( <StyledTag <StyledTag isAchieved={false} isRead={false} content={contentTag} content={contentTag} className={timestamp} className={timestamp} /> /> Loading Loading @@ -161,10 +142,11 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ className={Classes.ALERT} className={Classes.ALERT} > > <div className={Classes.ALERT_BODY}> <div className={Classes.ALERT_BODY}> <Icon icon='warning-sign' size={40} intent='warning' /> <Icon icon='warning-sign' size={40} intent={teamIds.length === 0 ? 'danger' : 'warning'} /> <div className={Classes.ALERT_CONTENTS}> <div className={Classes.ALERT_CONTENTS}> Are you sure you want to mark this milestone as{' '} Are you sure you want to mark this milestone as{' '} {reached ? 'not reached' : 'reached'} for selected or all teams? {reached ? 'not reached' : 'reached'} for {teamIds.length === 0 ? ' ALL teams' : ' selected teams'} </div> </div> </div> </div> <div <div Loading @@ -173,26 +155,19 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({ [css` [css` flex-wrap: wrap; flex-wrap: wrap; gap: 0.5rem; gap: 0.5rem; `]: !areEqualSize, `]: true, })} })} > > {!areEqualSize && ( <Button <Button intent='primary' intent={teamIds.length === 0 ? 'danger' : 'warning'} title='Sets milestone for instructed teams' title={ onClick={confirmSome} teamIds.length === 0 fill={!areEqualSize} ? 'Sets milestone for all teams' > : 'Sets milestone for selected teams' Confirm (for {teamIds.length}) } </Button> onClick={confirm} )} <Button intent='danger' title='Sets milestone for all teams present in the exercise' onClick={confirmAll} fill={!areEqualSize} > > Confirm{!areEqualSize && ' (for all)'} Confirm </Button> </Button> <Button onClick={() => setOpen(false)}>Cancel</Button> <Button onClick={() => setOpen(false)}>Cancel</Button> </div> </div> Loading
frontend/src/instructor/InstructorMilestones/index.tsx +6 −8 Original line number Original line Diff line number Diff line Loading @@ -6,17 +6,16 @@ import { GetMilestones, GetMilestones, } from '@inject/graphql/queries' } from '@inject/graphql/queries' import { type FC } from 'react' import { type FC } from 'react' import useGetSelectedTeams from '../InstructorTeamSelector/useGetSelectedTeams' import AllMilestoneIndicator from './AllMilestoneIndicator' import AllMilestoneIndicator from './AllMilestoneIndicator' import MilestoneIndicator from './MilestoneIndicator' import MilestoneIndicator from './MilestoneIndicator' interface InstructorMilestonesProps { interface InstructorMilestonesProps { exerciseId: string exerciseId: string teamId?: string teamIds: string[] } } const InstructorMilestones: FC<InstructorMilestonesProps> = ({ const InstructorMilestones: FC<InstructorMilestonesProps> = ({ teamId, teamIds, exerciseId, exerciseId, }) => { }) => { const [{ data: states }] = useTypedQuery({ const [{ data: states }] = useTypedQuery({ Loading @@ -27,7 +26,6 @@ const InstructorMilestones: FC<InstructorMilestonesProps> = ({ query: GetMilestones, query: GetMilestones, variables: { exerciseId }, variables: { exerciseId }, }) }) const teamIds = useGetSelectedTeams(exerciseId).map(team => team.id) return ( return ( <div <div Loading @@ -41,17 +39,17 @@ const InstructorMilestones: FC<InstructorMilestonesProps> = ({ > > <h3 style={{ order: 0, marginLeft: '0.5rem' }}>Reached</h3> <h3 style={{ order: 0, marginLeft: '0.5rem' }}>Reached</h3> <h3 style={{ order: 2, marginLeft: '0.5rem' }}>Not reached</h3> <h3 style={{ order: 2, marginLeft: '0.5rem' }}>Not reached</h3> {teamId && {teamIds.length === 1 && states?.analyticsMilestones states?.analyticsMilestones .filter(milestone => milestone.teamIds.includes(teamId)) .filter(milestone => milestone.teamIds.includes(teamIds[0])) .map(milestone => ( .map(milestone => ( <MilestoneIndicator <MilestoneIndicator milestone={milestone} milestone={milestone} key={milestone.milestone.id} key={milestone.milestone.id} teamId={teamId} teamId={teamIds[0]} /> /> ))} ))} {!teamId && {teamIds.length !== 1 && milestones?.milestones.map(milestone => ( milestones?.milestones.map(milestone => ( <AllMilestoneIndicator <AllMilestoneIndicator teamIds={teamIds} teamIds={teamIds} Loading
frontend/src/views/InstructorView/MilestoneButton.tsx +54 −4 Original line number Original line Diff line number Diff line Loading @@ -4,9 +4,10 @@ import { } from '@/clientsettings/vars/milestones' } from '@/clientsettings/vars/milestones' import Sidebar from '@/components/Sidebar' import Sidebar from '@/components/Sidebar' import InstructorMilestones from '@/instructor/InstructorMilestones' import InstructorMilestones from '@/instructor/InstructorMilestones' import { Button } from '@blueprintjs/core' import useGetSelectedTeams from '@/instructor/InstructorTeamSelector/useGetSelectedTeams' import { Button, ButtonGroup } from '@blueprintjs/core' import { css } from '@emotion/css' import { css } from '@emotion/css' import type { FC } from 'react' import { useLayoutEffect, useState, type FC } from 'react' import { createPortal } from 'react-dom' import { createPortal } from 'react-dom' const MilestonesButton: FC<{ const MilestonesButton: FC<{ Loading @@ -18,6 +19,28 @@ const MilestonesButton: FC<{ const open = useMilestonesOpen() const open = useMilestonesOpen() const portalRef = document.getElementById('rightPanel') const portalRef = document.getElementById('rightPanel') const teamIds = useGetSelectedTeams(exerciseId).map(team => team.id) const [select, setSelect] = useState<null | 'team' | 'selection' | 'all'>( null ) const defaultSelect: typeof select = !select ? teamId ? 'team' : 'selection' : select const selectTeamIds = defaultSelect === 'all' ? [] : defaultSelect === 'team' && teamId ? [teamId] : teamIds useLayoutEffect(() => { if (select !== null) { setSelect(null) } }, [teamId]) return ( return ( <> <> Loading Loading @@ -45,14 +68,41 @@ const MilestonesButton: FC<{ className={css` className={css` width: 22rem; width: 22rem; `} `} title={teamId ? 'Milestones for team' : 'Milestones for all'} title='Milestones' sections={[ sections={[ { id: 'selectors', node: ( <ButtonGroup minimal fill> <Button disabled={defaultSelect === 'all'} onClick={() => setSelect('all')} > All </Button> {teamId && ( <Button disabled={defaultSelect === 'team'} onClick={() => setSelect('team')} > Team </Button> )} <Button disabled={defaultSelect === 'selection'} onClick={() => setSelect('selection')} > Selected </Button> </ButtonGroup> ), }, { { id: 'reached', id: 'reached', node: ( node: ( <InstructorMilestones <InstructorMilestones exerciseId={exerciseId} exerciseId={exerciseId} teamId={teamId} teamIds={selectTeamIds} /> /> ), ), }, }, Loading