Commit 3638c67d authored by Patrik Kotucek's avatar Patrik Kotucek
Browse files

feat: instructor team overview clustering

parent 4410d0f8
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ export { InstructorTeamSelector } from './instructor/InstructorTeamSelector'
export { SelectorPage } from './instructor/InstructorTeamSelector/SelectorPage'
export { useSubscribedTeams } from './instructor/InstructorTeamSelector/useSubscribedTeams'
export { LearningObjective } from './instructor/LearningObjectives/LearningObjective'
export { default as TeamsScores } from './instructor/TeamsScores'

export { synchronousExerciseState, useStaffBoundary } from './utils'

+16 −11
Original line number Diff line number Diff line
import { Button, Card, Classes, Colors, Tooltip } from '@blueprintjs/core'
import { Card, Classes, Colors, Tooltip } from '@blueprintjs/core'
import { css, cx } from '@emotion/css'
import { TeamLabel } from '@inject/frontend'
import type { Team, TeamLearningObjective } from '@inject/graphql'
@@ -97,6 +97,16 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({
  return (
    <Card
      key={team.id}
      onClick={onClick}
      onKeyDown={event => {
        if (!onClick) return
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault()
          onClick()
        }
      }}
      role={onClick ? 'button' : undefined}
      tabIndex={onClick ? 0 : undefined}
      className={css`
        display: flex;
        flex-direction: ${row ? 'row' : 'column'};
@@ -116,6 +126,11 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({
        border-color: ${Colors.BLUE5} !important;
        `
          : ''}
        ${onClick
          ? `
        cursor: pointer;
        `
          : ''}
      `}
    >
      <div
@@ -139,16 +154,6 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({
            indexName={team.openSearchAccess?.indexName}
          />
        </div>
        {onClick && (
          <Button
            minimal
            icon={isExpanded ? 'chevron-up' : 'chevron-down'}
            aria-label={
              isExpanded ? 'Collapse team details' : 'Expand team details'
            }
            onClick={onClick}
          />
        )}
      </div>
      <div className={progressRow}>
        <div className={progressItem}>
+171 −158
Original line number Diff line number Diff line
@@ -6,39 +6,37 @@ import {
  SectionCard,
  Tab,
  Tabs,
  Tag,
} from '@blueprintjs/core'
import { css } from '@emotion/css'
import {
  ExerciseInstructorComments,
  type ResultOf,
  type Team,
  TeamLearningObjectivesQuery,
  type TodoLogActionLogsQuery,
  TodoLogActionLogsQuery,
  useTypedQuery,
} from '@inject/graphql'
import { useTranslationFrontend } from '@inject/locale'
import { useEffect, useMemo, useRef, useState } from 'react'
// import { InstructorComments } from '../../components'
import { getColorScheme } from '@inject/shared'
import { useMemo, useState } from 'react'
import { InstructorCommentList } from '../../components'
import { iTodoLogActionLogCheck } from '../../utils'
// import { InstructorTodoLog } from '../InstructorTodoLog'
import { InstructorTodoLog } from '../InstructorTodoLog'
import { TeamScoreCard } from './TeamScoreCard'

interface TodoTabsProps {
  exerciseId: string
  teams: Team[]
  actionLogData?: ResultOf<typeof TodoLogActionLogsQuery>
}

const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
const TeamsScores = ({ exerciseId, teams }: TodoTabsProps) => {
  const { t } = useTranslationFrontend()
  const [activeTab, setActiveTab] = useState<'todos' | 'instructor-comments'>(
    'todos'
  )
  const [expandedTeamId, setExpandedTeamId] = useState<Team['id'] | null>(null)
  const [overlayTop, setOverlayTop] = useState(0)
  const [overlayMaxHeight, setOverlayMaxHeight] = useState(0)
  const [isSectionCollapsed, setIsSectionCollapsed] = useState(false)
  const [done, setDone] = useState(false)
  const cardRefs = useRef<Record<string, HTMLDivElement | null>>({})
  const teamIds = teams.map(team => team.id)
  const [{ data }] = useTypedQuery({
    query: TeamLearningObjectivesQuery,
@@ -46,51 +44,39 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
    pause: !teamIds.length,
    context: useMemo(() => ({ suspense: true }), []),
  })
  const [{ data: instructorCommentsData }] = useTypedQuery({
  const [{ data: instructorCommentsData, fetching: loading }] = useTypedQuery({
    query: ExerciseInstructorComments,
    variables: {
      teamIds,
    },
  })

  const [{ data: actionLogData }] = useTypedQuery({
    query: TodoLogActionLogsQuery,
    variables: {
      teamIds: teams.map(team => team.id),
    },
    context: useMemo(
      () => ({
        suspense: true,
      }),
      []
    ),
  })

  const todoActionLogs =
    actionLogData?.teamActionLogs
      .filter(iTodoLogActionLogCheck)
      ?.filter(iTodoLogActionLogCheck)
      .filter(log => log.done === done) || []

  // const selectedActionLogs =
  //   expandedTeamId === null
  //     ? []
  //     : todoActionLogs.filter(log => log.teamId === expandedTeamId)
  // const overlayContentMaxHeight = Math.max(0, overlayMaxHeight - 56)

  useEffect(() => {
    setActiveTab('todos')
  }, [expandedTeamId])

  useEffect(() => {
    const updateOverlayPosition = () => {
      if (expandedTeamId === null) return

      const selectedCard = cardRefs.current[String(expandedTeamId)]
      if (!selectedCard) return

      setOverlayTop(selectedCard.offsetTop + selectedCard.offsetHeight + 4)
      const selectedCardRect = selectedCard.getBoundingClientRect()
      const availableViewportSpace =
        window.innerHeight - selectedCardRect.bottom - 16
      setOverlayMaxHeight(Math.max(0, availableViewportSpace))
    }

    updateOverlayPosition()
    window.addEventListener('resize', updateOverlayPosition)
    window.addEventListener('scroll', updateOverlayPosition, { passive: true })

    return () => {
      window.removeEventListener('resize', updateOverlayPosition)
      window.removeEventListener('scroll', updateOverlayPosition)
    }
  }, [expandedTeamId, teams.length])
  const selectedActionLogs =
    expandedTeamId === null
      ? todoActionLogs
      : todoActionLogs.filter(log => log.teamId === expandedTeamId)
  const selectedTeamIds =
    expandedTeamId === null ? teams.map(team => team.id) : [expandedTeamId]
  const selectedTeamName =
    teams.find(team => team.id === expandedTeamId)?.name || 'All'

  if (teams.length === 0) {
    return (
@@ -111,6 +97,9 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
      className={css`
        display: flex;
        flex-direction: column;
        flex: 1;
        min-height: 0;
        overflow: hidden;
        gap: 0.35rem;
      `}
    >
@@ -155,11 +144,6 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
          </Tabs>
        </div>
      </div>
      <div
        className={css`
          position: relative;
        `}
      >
      <div
        className={css`
          display: grid;
@@ -197,12 +181,7 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
          const isExpanded = expandedTeamId === team.id

          return (
              <div
                key={team.id}
                ref={element => {
                  cardRefs.current[String(team.id)] = element
                }}
              >
            <div key={team.id}>
              <TeamScoreCard
                team={team}
                teamLearningObjectives={teamLearningObjectives}
@@ -220,16 +199,19 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
        })}
      </div>

        {expandedTeamId && (
      <div
        className={css`
          flex: ${isSectionCollapsed ? '0 0 auto' : '1'};
          min-height: 0;
          overflow: hidden;
        `}
      >
        <SectionCard
          className={css`
              position: absolute;
              top: ${overlayTop}px;
              left: 0;
              right: 0;
              z-index: 20;
              max-height: ${overlayMaxHeight}px;
            height: ${isSectionCollapsed ? 'auto' : '100%'};
            max-height: ${isSectionCollapsed ? 'none' : '100%'};
            overflow: hidden;
            padding: 0.5rem !important;
            background-color: ${Colors.DARK_GRAY5} !important;
            opacity: 1 !important;
            font-size: 0.9rem;
@@ -241,18 +223,37 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
            className={css`
              display: flex;
              flex-direction: column;
                gap: 0.5rem;
                max-height: ${overlayMaxHeight}px;
              gap: 0.25rem;
              height: 100%;
              min-height: 0;
            `}
          >
            <div
              className={css`
                display: flex;
                align-items: center;
                  justify-content: space-between;
                  gap: 0.5rem;
                justify-content: center;
                gap: 0.25rem;
                position: relative;
              `}
            >
              <Tag
                className={css`
                  position: absolute;
                  left: 0;
                  max-width: 35%;
                  overflow: hidden;
                  text-overflow: ellipsis;
                  white-space: nowrap;
                `}
                title={expandedTeamId || 'All'}
                style={{
                  backgroundColor: getColorScheme(Number(expandedTeamId)),
                }}
                round
              >
                {selectedTeamName}
              </Tag>
              <ButtonGroup
                className={css`
                  gap: 0.25rem;
@@ -273,15 +274,26 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
              </ButtonGroup>
              <Button
                minimal
                  icon='cross'
                  aria-label='Close'
                  onClick={() => setExpandedTeamId(null)}
                icon={isSectionCollapsed ? 'chevron-down' : 'chevron-up'}
                aria-label={
                  isSectionCollapsed
                    ? 'Expand section card'
                    : 'Collapse section card'
                }
                onClick={() => setIsSectionCollapsed(current => !current)}
                className={css`
                  position: absolute;
                  right: 0;
                  top: 50%;
                  transform: translateY(-50%);
                `}
              />
            </div>
              {/* <div
            {!isSectionCollapsed && (
              <div
                className={css`
                  min-height: 0;
                  max-height: ${overlayContentMaxHeight}px;
                  flex: 1;
                  overflow-y: auto;
                  overflow-x: hidden;
                  overscroll-behavior: contain;
@@ -292,24 +304,25 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => {
                    actionLogs={selectedActionLogs}
                    contextType='team'
                    done={done}
                    teamIds={[expandedTeamId]}
                    teamIds={teams.map(team => team.id)}
                  />
                ) : (
                  <InstructorComments
                  <InstructorCommentList
                    exerciseId={exerciseId}
                    instructorComments={
                    loading={loading}
                    teamInstructorComments={
                      instructorCommentsData?.exerciseInstructorComments?.filter(
                        comment => comment.teamId === expandedTeamId
                        comment => selectedTeamIds.includes(comment.teamId)
                      ) || []
                    }
                    loading={loading}
                    allowDelete
                  />
                )}
              </div> */}
              </div>
          </SectionCard>
            )}
          </div>
        </SectionCard>
      </div>
    </div>
  )
}
+4 −20
Original line number Diff line number Diff line
import { css } from '@emotion/css'
import { TodoLogActionLogsQuery, useTypedQuery } from '@inject/graphql'
import { createFileRoute } from '@tanstack/react-router'
import { useMemo } from 'react'
import { useSubscribedTeams } from '../../../../instructor/InstructorTeamSelector/useSubscribedTeams'
import TeamsScores from '../../../../instructor/TeamsScores'
// TODO: improve this layout, use components from analyst overview
@@ -14,32 +12,18 @@ const RouteComponent = () => {
    context: 'instructor',
  })

  const [{ data: actionLogData }] = useTypedQuery({
    query: TodoLogActionLogsQuery,
    variables: {
      teamIds: selectedTeamStates.map(team => team.id),
    },
    context: useMemo(
      () => ({
        suspense: true,
      }),
      []
    ),
  })

  return (
    <main
      className={css`
        display: flex;
        flex-direction: column;
        flex: 1;
        min-height: 0;
        overflow: hidden;
        padding: 0.5rem;
      `}
    >
      <TeamsScores
        exerciseId={exerciseId}
        teams={selectedTeamStates}
        actionLogData={actionLogData}
      />
      <TeamsScores exerciseId={exerciseId} teams={selectedTeamStates} />
    </main>
  )
}