Commit 412ff87a authored by Patrik Kotucek's avatar Patrik Kotucek
Browse files

Merge branch 'main' into 310-instructor-todo-list-improve-email-items

parents 01c8dcd0 57d81938
Loading
Loading
Loading
Loading
+7 −28
Original line number Diff line number Diff line
@@ -65,12 +65,9 @@ const generateFileContents = async () => {
  const fileMapping: Array<{ name: string; content: string | Blob }> = []

  for (const [tableName, fileName] of Object.entries(TABLE_TO_FILE)) {
    // TODO: maybe there is a better way to type this? maybe dexie has something?
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const table = (db as Record<string, any>)[tableName]
    const table = db.table(tableName)
    if (!table) {
      // TODO: can this happen? how to handle this case?
      continue
      throw new Error('Wrong TABLE_TO_FILE name.')
    }

    let records
@@ -90,7 +87,6 @@ const generateFileContents = async () => {
    }

    if (!records || (Array.isArray(records) && records.length === 0)) {
      // TODO: decide if we want to export empty files or not
      continue
    }

@@ -123,13 +119,6 @@ const generateFileContents = async () => {
    })
  })

  // TODO: Export database blob for backup?
  // const expDb = await exportDB(db)
  // fileMapping.push({
  //   name: '_database.db',
  //   content: expDb,
  // })

  return fileMapping
}

@@ -393,12 +382,11 @@ export const loadDbData = async (zip: JSZip) => {

  for (const [fileName, tableName] of Object.entries(FILE_TO_TABLE)) {
    const records = await loadYamlData(zip, fileName)

    if (tableName === 'config') {
      if (!records || typeof records !== 'object' || Array.isArray(records)) {
        throw new Error('Config must be an object')
      }
      console.log(records)

      await db.config.clear()
      applyContentPathsDeep(records, contentMap, llmMap, false)
      const normalized = normalizeConfig(records)
@@ -406,19 +394,15 @@ export const loadDbData = async (zip: JSZip) => {
      continue
    }

    if (!Array.isArray(records)) {
      // TODO: handle empty or invalid files?
      throw new Error('YAML file does not contain array')
    if (!Array.isArray(records) || records.length === 0) {
      continue
    }

    applyContentPathsDeep(records, contentMap, llmMap, false)

    // TODO: maybe there is a better way to type this? maybe dexie has something?
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const table = (db as Record<string, any>)[tableName]
    const table = db.table(tableName)
    if (!table || typeof table !== 'object') {
      // TODO: can this happen? how to handle this case?
      continue
      throw new Error('Wrong TABLE_TO_FILE name.')
    }

    if (tableName === 'inject') {
@@ -437,9 +421,4 @@ export const loadDbData = async (zip: JSZip) => {

  await importFiles(zip)
  await importDrive(zip)
  // TODO: optionally restore from database blob for full backup?
  // const dbFile = zip.file('_database.db')
  // if (dbFile) {
  //   await importDB(await dbFile.async('blob'))
  // }
}
+91 −4
Original line number Diff line number Diff line
import {
  Button,
  ButtonGroup,
  Classes,
  Colors,
  Divider,
  Icon,
  Section,
  SectionCard,
} from '@blueprintjs/core'
import { Shield } from '@blueprintjs/icons'
import { css, cx } from '@emotion/css'
import { SetTimestampRead, useTypedMutation } from '@inject/graphql'
import {
  LLMEnabledQuery,
  SetTimestampRead,
  useTypedMutation,
  useTypedQuery,
} from '@inject/graphql'
import { useTranslationFrontend } from '@inject/locale'
import { breakWord, Timestamp } from '@inject/shared'
import {
  breakWord,
  LinkButton,
  notifyNoncommmit,
  Timestamp,
} from '@inject/shared'
import type { NavigateOptions } from '@tanstack/react-router'
import type { FC } from 'react'
import { useEffect, useLayoutEffect, useState } from 'react'
@@ -20,6 +32,7 @@ import { Assessment } from '../../components/Assessment'
import type { AssessmentProps } from '../../components/Assessment/types'
import Description from '../../components/Description'
import { FileViewRedirectButton } from '../../components/FileViewRedirectButton'
import { useToggleDone } from '../../hooks/useToggleDone'
import { canBeCommented } from '../../utils'
import { OPEN_COMPOSE_EVENT_TYPE } from '../EmailFormOverlay/events'
import type { ExtendedEmail } from '../typing'
@@ -45,6 +58,17 @@ const rightElement = css`
  align-items: center;
`

const buttonGroupRow = css`
  display: flex;
  justify-content: flex-end;
`

const buttonTextWithHelp = css`
  display: inline-flex;
  align-items: center;
  gap: 1rem;
`

export const EmailCard: FC<{
  exerciseId: string
  teamId?: string
@@ -54,6 +78,8 @@ export const EmailCard: FC<{
  inInstructor?: boolean
  getFileLink: (fileId: string) => NavigateOptions
  assessmentProps?: AssessmentProps
  markDoneAvailable?: boolean
  overviewLink?: NavigateOptions
}> = ({
  exerciseId,
  teamId,
@@ -63,9 +89,25 @@ export const EmailCard: FC<{
  inInstructor,
  getFileLink,
  assessmentProps,
  overviewLink,
  markDoneAvailable,
}) => {
  const [, setTimestampRead] = useTypedMutation(SetTimestampRead)
  const [{ data: llmData, fetching: llmFetching }] = useTypedQuery({
    query: LLMEnabledQuery,
    variables: { exerciseId },
    pause: !assessmentProps,
  })
  const { t } = useTranslationFrontend()
  const { toggleDone } = useToggleDone(
    email.__typename === 'IActionLogType'
      ? {
          __typename: email.__typename,
          id: email.id,
          done: email.done,
        }
      : undefined
  )

  // this ensures the message is rendered as 'not read' the first time it's rendered
  const [initialTimestampRead, setInitialTimestampRead] = useState(false)
@@ -94,6 +136,22 @@ export const EmailCard: FC<{
  ])

  const definitionAddress = email.details.sender.definitionAddress
  const llmEnabled = llmData?.exerciseId.llm ?? false
  const showAssessment = !!assessmentProps && llmEnabled
  const showDivider = !assessmentProps || (!llmFetching && !llmEnabled)

  const MarkAsDoneAndNotify = () => {
    toggleDone()
    notifyNoncommmit(t('overview.todoList.notification.description'), {
      intent: 'success',
      timeout: 2000,
      action: {
        icon: 'undo',
        text: t('overview.todoList.notification.undo'),
        onClick: () => toggleDone(false),
      },
    })
  }

  return (
    <Section
@@ -105,7 +163,7 @@ export const EmailCard: FC<{
      icon={
        teamId && email.details.sender.team?.id === teamId ? (
          <Icon
            className={Classes.TEXT_MUTED}
            className={cx(Classes.TEXT_MUTED)}
            icon='send-message'
            title={t('emails.senderTeam')}
          />
@@ -222,7 +280,36 @@ export const EmailCard: FC<{
          />
        ))}

        {assessmentProps && <Assessment {...assessmentProps} />}
        {showAssessment && assessmentProps && (
          <Assessment {...assessmentProps} />
        )}
        {markDoneAvailable && teamId && !email.done && (
          <>
            {showDivider && <Divider />}
            <div className={buttonGroupRow}>
              <ButtonGroup>
                <Button icon='changes' onClick={MarkAsDoneAndNotify}>
                  {t('overview.todoList.markAsDone')}
                </Button>
                {overviewLink && (
                  <LinkButton
                    button={{
                      icon: 'changes',
                      onClick: MarkAsDoneAndNotify,
                      text: (
                        <span className={buttonTextWithHelp}>
                          {t('overview.todoList.markAsDoneAndReturn')}
                        </span>
                      ),
                      minimal: true,
                    }}
                    link={overviewLink}
                  />
                )}
              </ButtonGroup>
            </div>
          </>
        )}
      </SectionCard>
    </Section>
  )
+20 −0
Original line number Diff line number Diff line
import type { ITodoLogActionLog } from '@inject/graphql'
import { SetDone, useTypedMutation } from '@inject/graphql'

type ToggleDoneLog = Pick<ITodoLogActionLog, '__typename' | 'id' | 'done'>

export const useToggleDone = (actionLog?: ToggleDoneLog) => {
  const [, setDone] = useTypedMutation(SetDone)
  return {
    toggleDone: (nextState?: boolean) => {
      if (!actionLog) {
        return
      }
      setDone({
        actionLogId: actionLog.id,
        state: nextState ?? !actionLog.done,
        typename: actionLog.__typename,
      })
    },
  }
}
+6 −4
Original line number Diff line number Diff line
@@ -393,6 +393,7 @@ const LogItemActions: React.FC<{
  actionLog: ITodoLogActionLog
  contextType: 'exercise' | 'team'
}> = ({ actionLog, contextType }) => {
  const { t } = useTranslationFrontend()
  const threadId =
    actionLog.details.__typename === 'IEmailType'
      ? actionLog.details.thread.id
@@ -404,14 +405,15 @@ const LogItemActions: React.FC<{
  const { toggleDone } = useToggleDone(actionLog)
  return (
    <ButtonGroup vertical>
      <Button small icon='changes' onClick={toggleDone}>
        {actionLog.done ? 'Mark as undone' : 'Mark as done'}
      <Button icon='changes' onClick={() => toggleDone()}>
        {actionLog.done
          ? t('overview.todoList.markAsUndone')
          : t('overview.todoList.markAsDone')}
      </Button>
      <LinkButton
        button={{
          icon: 'zoom-in',
          text: 'Inspect',
          small: true,
          text: t('overview.todoList.inspect'),
        }}
        link={getNavigateOptions(actionLog)}
      />
+8 −0
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ import { useTranslationFrontend } from '@inject/locale'
import { EmailSelection } from '@inject/shared'
import { createFileRoute } from '@tanstack/react-router'
import { useMemo } from 'react'
import { InstructorLandingPageRoute } from '../../..'
import { OPEN_REPLY_EVENT_TYPE } from '../../../../../../../email/EmailFormOverlay/events'
import { EmailCard } from '../../../../../../../email/TeamEmails/EmailCard'
import { ThreadHeaderCard } from '../../../../../../../email/TeamEmails/ThreadHeaderCard'
@@ -151,6 +152,13 @@ const RouteComponent = () => {
                  }
                : undefined
            }
            markDoneAvailable
            overviewLink={{
              to: InstructorLandingPageRoute.to,
              params: {
                exerciseId,
              },
            }}
          />
        ))}
      </div>
Loading