Loading frontend/src/index.ts +0 −1 Original line number Diff line number Diff line Loading @@ -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' Loading frontend/src/instructor/TeamsScores/TeamScoreCard.tsx +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' Loading Loading @@ -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'}; Loading @@ -116,6 +126,11 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({ border-color: ${Colors.BLUE5} !important; ` : ''} ${onClick ? ` cursor: pointer; ` : ''} `} > <div Loading @@ -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}> Loading frontend/src/instructor/TeamsScores/index.tsx +171 −158 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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 ( Loading @@ -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; `} > Loading Loading @@ -155,11 +144,6 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => { </Tabs> </div> </div> <div className={css` position: relative; `} > <div className={css` display: grid; Loading Loading @@ -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} Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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> ) } Loading frontend/src/routes/_protected/instructor/$exerciseId/index.tsx +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 Loading @@ -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> ) } Loading Loading
frontend/src/index.ts +0 −1 Original line number Diff line number Diff line Loading @@ -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' Loading
frontend/src/instructor/TeamsScores/TeamScoreCard.tsx +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' Loading Loading @@ -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'}; Loading @@ -116,6 +126,11 @@ export const TeamScoreCard: FC<TeamScoreCardProps> = ({ border-color: ${Colors.BLUE5} !important; ` : ''} ${onClick ? ` cursor: pointer; ` : ''} `} > <div Loading @@ -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}> Loading
frontend/src/instructor/TeamsScores/index.tsx +171 −158 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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 ( Loading @@ -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; `} > Loading Loading @@ -155,11 +144,6 @@ const TeamsScores = ({ teams, actionLogData }: TodoTabsProps) => { </Tabs> </div> </div> <div className={css` position: relative; `} > <div className={css` display: grid; Loading Loading @@ -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} Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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> ) } Loading
frontend/src/routes/_protected/instructor/$exerciseId/index.tsx +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 Loading @@ -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> ) } Loading