Loading analyst/package.json +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", Loading analyst/src/components/ActionLog/ActionLogItem.tsx +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 { Loading @@ -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}; Loading @@ -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 Loading @@ -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} Loading @@ -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> ) } analyst/src/components/ActionLogTitle/ActionLogContent.tsx 0 → 100644 +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> </> ) } } } analyst/src/components/ActionLogTitle/ActionLogDescription.tsx 0 → 100644 +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> </> ) } } } } analyst/src/components/ActionLogTitle/ActionLogIcon.tsx 0 → 100644 +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
analyst/package.json +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", Loading
analyst/src/components/ActionLog/ActionLogItem.tsx +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 { Loading @@ -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}; Loading @@ -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 Loading @@ -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} Loading @@ -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> ) }
analyst/src/components/ActionLogTitle/ActionLogContent.tsx 0 → 100644 +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> </> ) } } }
analyst/src/components/ActionLogTitle/ActionLogDescription.tsx 0 → 100644 +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> </> ) } } } }
analyst/src/components/ActionLogTitle/ActionLogIcon.tsx 0 → 100644 +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> )