Commit e6fdf0e4 authored by Marek Veselý's avatar Marek Veselý
Browse files

Merge branch '890-analyst-overview-make-instructor-comments-collapsible' into 'main'

Added resizable score in analyst + can hide instructor comments/la

Closes #890

See merge request inject/frontend!784
parents 01d0bee9 d496d3cc
Loading
Loading
Loading
Loading
+53 −13
Original line number Diff line number Diff line
import { Divider } from '@blueprintjs/core'
import { Classes, Divider, Icon } from '@blueprintjs/core'
import { css } from '@emotion/css'
import { ExerciseContext } from '@inject/analyst'
import {
@@ -7,9 +7,27 @@ import {
  TeamsDisplay,
  useSubscribedTeams,
} from '@inject/frontend'
import { useResize } from '@inject/shared'
import { createFileRoute } from '@tanstack/react-router'
import { useContext } from 'react'

const dividerContainer = css`
  cursor: ew-resize;
  height: 100%;
  padding: 0.4rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  row-gap: 0.5rem;
`

const mainContainer = css`
  display: flex;
  min-height: 100%;
  gap: 0.5rem;
  position: relative;
`

// TODO: add progress bar of exercise with auto-inject lines/points

const RouteComponent = () => {
@@ -21,21 +39,30 @@ const RouteComponent = () => {

  const teams = useSubscribedTeams({ exerciseId, context: 'analyst' })

  const {
    containerRef,
    handleMouseDown,
    handleMouseMove,
    handleMouseUp,
    leftWidth,
  } = useResize()

  return (
    <div
      className={css`
        display: flex;
        min-height: 100%;
        gap: 0.5rem;
      `}
    <main
      ref={containerRef}
      className={mainContainer}
      onMouseUp={handleMouseUp}
      onMouseLeave={handleMouseUp}
      onMouseMove={handleMouseMove}
    >
      <div
      <section
        className={css`
          flex: 1;
          width: ${leftWidth}%;
          display: flex;
          flex-direction: column;
          gap: 0.5rem;
          overflow: auto;
          min-width: 35rem;
        `}
      >
        <h1
@@ -53,15 +80,28 @@ const RouteComponent = () => {
        <Divider />

        <InstructorComments exerciseId={exerciseId} />
      </div>
      </section>

      <Divider />
      <div onMouseDown={handleMouseDown} className={dividerContainer}>
        <Divider
          className={css`
            margin: 0 !important;
            height: 100%;
          `}
        />
        <Icon icon='arrows-horizontal' className={Classes.TEXT_MUTED} />
        <Divider
          className={css`
            margin: 0 !important;
            height: 100%;
          `}
        />
      </div>

      <TeamsDisplay teams={teams} exercise={exercise} showExport />
    </div>
    </main>
  )
}

export const Route = createFileRoute('/_layout/$exerciseId/')({
  component: RouteComponent,
})
+103 −15
Original line number Diff line number Diff line
import { Classes, Icon } from '@blueprintjs/core'
import { css, cx } from '@emotion/css'
import { InstructorCommentList } from '@inject/frontend'
import { ExerciseInstructorComments, useTypedQuery } from '@inject/graphql'
import { useTranslationFrontend } from '@inject/locale'
import type { FC } from 'react'
import { instructorCommentsContainer } from './utils'
import { useState, type FC } from 'react'
import { useDescription } from './utils'

// TODO: deduplicate with LearningObjectives
const headerContainer = css`
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
`

// TODO: make collapsible
const container = css`
  display: flex;
  flex-direction: column;
  overflow: auto;
  gap: 0.5rem;
`

const containerOpened = css`
  flex: 2;
  gap: 1rem;
`

const titleDescription = cx(
  Classes.TEXT_MUTED,
  css`
    display: flex;
    align-items: center;
    column-gap: 0.5rem;
    margin-top: 0.25rem;
  `
)

interface InstructorCommentsProps {
  exerciseId: string
@@ -21,14 +48,73 @@ export const InstructorComments: FC<InstructorCommentsProps> = ({
      exerciseId,
    },
  })

  const [isOpen, setIsOpen] = useState(
    (data?.exerciseInstructorComments?.length ?? 0) > 0
  )

  const { t } = useTranslationFrontend()

  const getDescriptionText = useDescription()

  return (
    <div className={instructorCommentsContainer}>
      <b style={{ fontSize: '1.2em' }}>
        {t('overview.instructorComments.title')}:
      </b>
    <div
      className={cx(container, {
        [containerOpened]: isOpen,
      })}
    >
      <div
        onClick={() => {
          setIsOpen(!isOpen)
        }}
        className={headerContainer}
      >
        <div>
          <h2
            className={css`
              margin: 0;
            `}
          >
            {t('overview.instructorComments.title')}
          </h2>
          {!isOpen && (
            <div className={titleDescription}>
              <Icon
                icon={
                  data?.exerciseInstructorComments?.length
                    ? 'comment'
                    : 'low-voltage-pole'
                }
              />
              <p
                className={css`
                  margin-bottom: 0;
                `}
              >
                {getDescriptionText(data?.exerciseInstructorComments?.length)}
              </p>
            </div>
          )}
        </div>

        <Icon
          icon='chevron-right'
          className={css`
            margin-right: 1rem;
            transition: transform 0.3s ease;
            transform: ${isOpen ? 'rotate(-90deg)' : 'rotate(90deg)'};
          `}
        />
      </div>

      {isOpen && (
        <div
          className={css`
            flex: 1;
            display: flex;
            overflow: auto;
          `}
        >
          <InstructorCommentList
            exerciseId={exerciseId}
            loading={loading}
@@ -36,5 +122,7 @@ export const InstructorComments: FC<InstructorCommentsProps> = ({
            allowDelete
          />
        </div>
      )}
    </div>
  )
}
+0 −9
Original line number Diff line number Diff line
import { css } from '@emotion/css'

export const instructorCommentsContainer = css`
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  overflow: auto;
`
+34 −0
Original line number Diff line number Diff line
import { css } from '@emotion/css'
import { useTranslationFrontend } from '@inject/locale'

export const instructorCommentsContainer = css`
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  overflow: auto;
`

export const useDescription = () => {
  const { t } = useTranslationFrontend()

  const getDescriptionText = (length?: number) => {
    if (!length) {
      return t('overview.instructorComments.headerDescription.notFound')
    }
    if (length == 1) {
      return t('overview.instructorComments.headerDescription.found')
    }
    if (length >= 5) {
      return t('overview.instructorComments.headerDescription.foundPlural2', {
        count: length,
      })
    }

    return t('overview.instructorComments.headerDescription.foundPlural', {
      count: length,
    })
  }

  return getDescriptionText
}
+70 −42
Original line number Diff line number Diff line
import { NonIdealState } from '@blueprintjs/core'
import { css } from '@emotion/css'
import { Icon, NonIdealState } from '@blueprintjs/core'
import { css, cx } from '@emotion/css'
import { TeamLearningObjectivesQuery, useTypedQuery } from '@inject/graphql'
import type { FC } from 'react'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { LearningObjective } from './LearningObjective'
import type { ObjectiveWithTeamStates } from './types'

const headerContainer = css`
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
`

const container = css`
  display: flex;
  flex-direction: column;
  overflow: auto;
  gap: 0.5rem;
`

const containerOpened = css`
  flex: 3;
  gap: 1rem;
`

interface LearningObjectivesProps {
  teamIds: string[]
  showTeams?: boolean
@@ -15,6 +34,8 @@ export const LearningObjectives: FC<LearningObjectivesProps> = ({
  teamIds,
  showTeams,
}) => {
  const [isOpen, setIsOpen] = useState(true)

  const [{ data }] = useTypedQuery({
    query: TeamLearningObjectivesQuery,
    variables: { teamIds: teamIds },
@@ -42,14 +63,11 @@ export const LearningObjectives: FC<LearningObjectivesProps> = ({

  return (
    <div
      className={css`
        flex: 3;
        display: flex;
        flex-direction: column;
        gap: 1rem;
        overflow: auto;
      `}
      className={cx(container, {
        [containerOpened]: isOpen,
      })}
    >
      <div onClick={() => setIsOpen(!isOpen)} className={headerContainer}>
        <h2
          className={css`
            margin: 0;
@@ -57,15 +75,25 @@ export const LearningObjectives: FC<LearningObjectivesProps> = ({
        >
          Learning objectives
        </h2>
        <Icon
          icon='chevron-right'
          className={css`
            margin-right: 1rem;
            transition: transform 0.3s ease;
            transform: ${isOpen ? 'rotate(-90deg)' : 'rotate(90deg)'};
          `}
        />
      </div>

      {isOpen && (
        <div
        className={css`
          className={cx(css`
            display: flex;
            flex-direction: column;
            overflow: auto;
            gap: 1rem;
            padding: 0.25rem 1rem;
        `}
          `)}
        >
          {objectives.length === 0 && (
            <NonIdealState
@@ -74,7 +102,6 @@ export const LearningObjectives: FC<LearningObjectivesProps> = ({
              description='This exercise has no learning objectives'
            />
          )}

          {objectives.map(objective => (
            <LearningObjective
              key={objective.id}
@@ -83,6 +110,7 @@ export const LearningObjectives: FC<LearningObjectivesProps> = ({
            />
          ))}
        </div>
      )}
    </div>
  )
}
Loading