Commit 78f69335 authored by Marek Veselý's avatar Marek Veselý
Browse files

Merge branch '939-room-view-action-log-children' into 'main'

Room view action log children, New version of Backend - v4.4.0

Closes #939, #944, #942, inject-issues#223, and inject-issues#244

See merge request inject/frontend!810
parents 41f5a99d f7eff6d8
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
{
  "name": "@inject/analyst",
  "version": "4.2.0",
  "version": "4.4.0",
  "description": "Analyst module for Inject Exercise Platform",
  "main": "index.js",
  "license": "MIT",
+4 −25
Original line number Diff line number Diff line
import { Icon } from '@blueprintjs/core'
import { css, cx } from '@emotion/css'
import type { IAnalystActionLogSimple } from '@inject/graphql'
import {
@@ -9,8 +8,8 @@ import {
  useTimeFormatChars,
} from '@inject/shared'
import type { FC } from 'react'
import { ActionLogTitle } from '../ActionLogTitle'
import { actionTypeColor } from '../utilities'
import { ActionLogContent } from '../ActionLogTitle/ActionLogContent'
import { ActionLogIcon } from '../ActionLogTitle/ActionLogIcon'

const td = css`
  ${ellipsized};
@@ -18,26 +17,6 @@ const td = css`
  cursor: pointer;
`

const getIcon = (logType: IAnalystActionLogSimple['logType']): JSX.Element => {
  switch (logType) {
    case 'INJECT':
    case 'CUSTOM_INJECT':
    case 'TOOL':
    case 'FORM_SUBMISSION':
    case 'FORM':
    case 'FORM_REVIEW':
    case 'CONFIRMATION':
    case 'FILE_DOWNLOAD':
      return (
        <Icon icon='full-circle' color={actionTypeColor(logType.toString())} />
      )
    case 'EMAIL':
      return <Icon icon='envelope' />
    case 'MILESTONE_MODIFICATION':
      return <Icon icon='flag' />
  }
}

interface ActionLogItemProps {
  actionLog: IAnalystActionLogSimple
  onClick: () => void
@@ -61,7 +40,7 @@ export const ActionLogItem: FC<ActionLogItemProps> = ({
      onClick={onClick}
    >
      <td className={td} style={{ width: 32 /* icon size + padding */ }}>
        {getIcon(actionLog.logType)}
        <ActionLogIcon actionLog={actionLog} />
      </td>
      <td
        className={td}
@@ -77,7 +56,7 @@ export const ActionLogItem: FC<ActionLogItemProps> = ({
          }}
        />
      </td>
      <td className={td}>{<ActionLogTitle actionLog={actionLog} />}</td>
      <td className={td}>{<ActionLogContent actionLog={actionLog} />}</td>
    </tr>
  )
}
+70 −0
Original line number Diff line number Diff line
import { Classes, Colors } from '@blueprintjs/core'
import { css } from '@emotion/css'
import type { IAnalystActionLogSimple } from '@inject/graphql'
import type { FC } from 'react'

const activated = css`
  color: ${Colors.GREEN2};
  .${Classes.DARK} & {
    color: ${Colors.GREEN4};
  }
`

const deactivated = css`
  color: ${Colors.RED2};
  .${Classes.DARK} & {
    color: ${Colors.RED4};
  }
`

interface ActionLogContentProps {
  actionLog: IAnalystActionLogSimple
}

export const ActionLogContent: FC<ActionLogContentProps> = ({ actionLog }) => {
  switch (actionLog.details.__typename) {
    case 'IToolDetailsType':
      return <>{actionLog.details.tool.displayName}</>
    case 'IInjectDetailsType':
      return <>{actionLog.details.inject.displayName}</>
    case 'IConfirmationDetailsType':
      // TODO: improve (requires more data from BE)
      return <>confirmation</>
    case 'ICustomInjectDetailsType':
      return <>custom inject</>
    case 'IEmailType':
      return <>{actionLog.details.thread.subject}</>
    case 'IQuestionnaireReviewDetailsType':
      // TODO: improve (requires more data from BE)
      return <>questionnaire reviewed</>
    case 'IQuestionnaireSubmissionType':
      // TODO: improve (requires more data from BE)
      return <>questionnaire submitted</>
    case 'ITeamQuestionnaireStateType':
      return <>{actionLog.details.questionnaire.displayName}</>
    case 'IFileDownloadDetailsType':
      return <>{actionLog.details.fileInfo.fileName}</>
    case 'IMilestoneModificationDetailsType': {
      const { activatedMilestoneStates, deactivatedMilestoneStates } =
        actionLog.details
      const hasActivated = activatedMilestoneStates.length > 0
      const hasDeactivated = deactivatedMilestoneStates.length > 0

      return (
        <>
          <span className={activated}>
            {activatedMilestoneStates
              .map(state => state.milestone.displayName)
              .join(', ')}
          </span>
          {hasActivated && hasDeactivated ? ' | ' : ''}
          <span className={deactivated}>
            {deactivatedMilestoneStates
              .map(state => state.milestone.displayName)
              .join(', ')}
          </span>
        </>
      )
    }
  }
}
+148 −0
Original line number Diff line number Diff line
import type { IAnalystActionLogSimple } from '@inject/graphql'
import type { FC } from 'react'

interface ActionLogDescriptionProps {
  actionLog: IAnalystActionLogSimple
}

// TODO: add users? ask xvykopal

export const ActionLogDescription: FC<ActionLogDescriptionProps> = ({
  actionLog,
}) => {
  switch (actionLog.details.__typename) {
    case 'IToolDetailsType':
      return (
        <>
          The <b>{actionLog.details.tool.displayName}</b> tool was used by a
          trainee
        </>
      )
    case 'IInjectDetailsType':
      return (
        <>
          The <b>{actionLog.details.inject.displayName}</b> inject was sent to
          the team
        </>
      )
    case 'IConfirmationDetailsType':
      // TODO: improve (requires more data from BE)
      return (
        <>
          An inject was <b>confirmed</b> by a trainee
        </>
      )
    case 'ICustomInjectDetailsType':
      return (
        <>
          A <b>custom inject</b> was sent by an instructor
        </>
      )
    case 'IEmailType': {
      return (
        <>
          An email was sent to the <b>{actionLog.details.thread.subject}</b>{' '}
          thread by a{' '}
          <b>
            {actionLog.details.sender.address ===
            actionLog.team.emailAddress?.address
              ? 'trainee'
              : 'definition address'}
          </b>
        </>
      )
    }
    case 'IQuestionnaireReviewDetailsType':
      // TODO: improve (requires more data from BE)
      return (
        <>
          A questionnaire was <b>reviewed</b> by an instructor
        </>
      )
    case 'IQuestionnaireSubmissionType':
      // TODO: improve (requires more data from BE)
      return (
        <>
          A questionnaire was submitted by a trainee, the answers were{' '}
          <b>{actionLog.details.accepted ? '' : 'not '}accepted</b>
        </>
      )
    case 'ITeamQuestionnaireStateType':
      return (
        <>
          The <b>{actionLog.details.questionnaire.displayName}</b> questionnaire
          was sent to the team
        </>
      )
    case 'IFileDownloadDetailsType':
      return (
        <>
          The <b>{actionLog.details.fileInfo.fileName}</b> file was
          viewed/downloaded by a trainee
        </>
      )
    case 'IMilestoneModificationDetailsType': {
      const { activatedMilestoneStates, deactivatedMilestoneStates, cause } =
        actionLog.details
      const activatedString = activatedMilestoneStates
        .map(state => state.milestone.displayName)
        .join(', ')
      const deactivatedString = deactivatedMilestoneStates
        .map(state => state.milestone.displayName)
        .join(', ')
      const causeString: string = (() => {
        switch (cause) {
          case 'AUTOMATIC_ACTION':
            return 'automatically'
          case 'INSTRUCTOR_ACTION':
            return 'by an instructor'
          case 'TRAINEE_ACTION':
            return 'by a trainee'
        }
      })()

      if (
        activatedMilestoneStates.length &&
        deactivatedMilestoneStates.length
      ) {
        return (
          <>
            The <b>{activatedString}</b>{' '}
            {activatedMilestoneStates.length > 0
              ? 'milestones were'
              : 'milestone was'}{' '}
            activated and the <b>{deactivatedString}</b>{' '}
            {deactivatedMilestoneStates.length > 0
              ? 'milestones were'
              : 'milestone was'}{' '}
            deactivated <b>{causeString}</b>
          </>
        )
      }

      if (activatedMilestoneStates.length) {
        return (
          <>
            The <b>{activatedString}</b>{' '}
            {activatedMilestoneStates.length > 0
              ? 'milestones were'
              : 'milestone was'}{' '}
            activated <b>{causeString}</b>
          </>
        )
      }

      if (deactivatedMilestoneStates.length) {
        return (
          <>
            The <b>{deactivatedString}</b>{' '}
            {deactivatedMilestoneStates.length > 0
              ? 'milestones were'
              : 'milestone was'}{' '}
            deactivated <b>{causeString}</b>
          </>
        )
      }
    }
  }
}
+128 −0
Original line number Diff line number Diff line
import { Classes, Colors } from '@blueprintjs/core'
import {
  Briefcase,
  Confirm,
  CrossCircle,
  DocumentOpen,
  Envelope,
  EyeOpen,
  Flag,
  Form,
  InfoSign,
  IssueNew,
  Notifications,
  SendMessage,
} from '@blueprintjs/icons'
import { css, cx } from '@emotion/css'
import type { IAnalystActionLogSimple } from '@inject/graphql'
import type { FC } from 'react'

const iconClass = cx(
  Classes.TEXT_MUTED,
  css`
    & svg {
      overflow: visible;
    }
  `
)

const additionalIcon = css`
  position: absolute;
  right: 0;
  bottom: 0;

  &,
  & svg,
  & svg * {
    stroke: ${Colors.GRAY5};
    stroke-width: 5rem;
    paint-order: stroke fill !important;
    overflow: visible;
  }
  .${Classes.DARK} &,
  .${Classes.DARK} & svg,
  .${Classes.DARK} & svg * {
    stroke: ${Colors.GRAY1};
  }
`
const ADDITIONAL_ICON_SIZE = 12

interface ActionLogIconProps {
  actionLog: IAnalystActionLogSimple
}

const ActionLogIconInternal: FC<ActionLogIconProps> = ({ actionLog }) => {
  const { team } = actionLog

  switch (actionLog.details.__typename) {
    case 'IToolDetailsType':
      return <Briefcase className={iconClass} />
    case 'IInjectDetailsType':
      return <InfoSign className={iconClass} />
    case 'IConfirmationDetailsType':
      return <Confirm className={iconClass} />
    case 'ICustomInjectDetailsType':
      return <IssueNew className={iconClass} />
    case 'IEmailType': {
      if (actionLog.details.sender.address === team.emailAddress?.address) {
        return <SendMessage className={iconClass} />
      }
      return <Envelope className={iconClass} />
    }
    case 'IQuestionnaireReviewDetailsType':
      return (
        <>
          <Form className={iconClass} />

          <EyeOpen
            size={ADDITIONAL_ICON_SIZE}
            className={cx(additionalIcon, iconClass)}
          />
        </>
      )
    case 'IQuestionnaireSubmissionType':
      return (
        <>
          <Form className={iconClass} />

          {!actionLog.details.accepted && (
            <CrossCircle
              size={ADDITIONAL_ICON_SIZE}
              className={cx(additionalIcon, iconClass)}
            />
          )}
        </>
      )

    case 'ITeamQuestionnaireStateType':
      return (
        <>
          <Form className={iconClass} />

          <Notifications
            size={ADDITIONAL_ICON_SIZE}
            className={cx(additionalIcon, iconClass)}
          />
        </>
      )
    case 'IFileDownloadDetailsType':
      return <DocumentOpen className={iconClass} />
    case 'IMilestoneModificationDetailsType':
      return <Flag className={iconClass} />
  }
}

export const ActionLogIcon: FC<ActionLogIconProps> = ({ actionLog }) => (
  <div
    className={css`
      width: fit-content;
      position: relative;
      padding: 0.25rem;
      display: flex;
      align-items: center;
      justify-content: center;
    `}
  >
    <ActionLogIconInternal actionLog={actionLog} />
  </div>
)
Loading