Commit 7ef612d9 authored by Marek Veselý's avatar Marek Veselý
Browse files

Merge branch '624-milestones' into 'main'

feat: rework milestones to have selectors

Closes #624

See merge request inject/frontend!528
parents b65acec1 dba8d293
Loading
Loading
Loading
Loading
+25 −50
Original line number Original line Diff line number Diff line
@@ -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 => {
@@ -114,7 +95,7 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({
                reached ? (
                reached ? (
                  <StyledTag
                  <StyledTag
                    isAchieved
                    isAchieved
                    content={reachedTag}
                    content='Reached'
                    className={timestamp}
                    className={timestamp}
                  />
                  />
                ) : unreachedAll ? (
                ) : unreachedAll ? (
@@ -125,7 +106,7 @@ const AllMilestoneIndicator: FC<MilestoneIndicatorProps> = ({
                  />
                  />
                ) : (
                ) : (
                  <StyledTag
                  <StyledTag
                    isAchieved={false}
                    isRead={false}
                    content={contentTag}
                    content={contentTag}
                    className={timestamp}
                    className={timestamp}
                  />
                  />
@@ -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
@@ -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>
+6 −8
Original line number Original line Diff line number Diff line
@@ -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({
@@ -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
@@ -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}
+54 −4
Original line number Original line Diff line number Diff line
@@ -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<{
@@ -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 (
    <>
    <>
@@ -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}
                  />
                  />
                ),
                ),
              },
              },