Loading frontend/src/email/TeamEmails/EmailCard.tsx +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' Loading @@ -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' Loading @@ -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 Loading @@ -54,6 +78,8 @@ export const EmailCard: FC<{ inInstructor?: boolean getFileLink: (fileId: string) => NavigateOptions assessmentProps?: AssessmentProps markDoneAvailable?: boolean overviewLink?: NavigateOptions }> = ({ exerciseId, teamId, Loading @@ -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) Loading Loading @@ -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 Loading @@ -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')} /> Loading Loading @@ -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> ) Loading frontend/src/hooks/useToggleDone.ts 0 → 100644 +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, }) }, } } frontend/src/instructor/InstructorTodoLog/LogItem.tsx +9 −21 Original line number Diff line number Diff line import { Button, ButtonGroup, Classes, Icon } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { ITodoLogActionLog } from '@inject/graphql' import { GetEmailThread, SetDone, useTypedMutation, useTypedQuery, } from '@inject/graphql' import { GetEmailThread, useTypedQuery } from '@inject/graphql' import { useTranslationFrontend } from '@inject/locale' import type { LinkButtonProps } from '@inject/shared' import { EmailSelection, LinkButton, Timestamp } from '@inject/shared' import type { NavigateOptions } from '@tanstack/react-router' import type { FC } from 'react' import { useToggleDone } from '../../hooks/useToggleDone' import { InstructorLandingPageRoute } from '../../routes/_protected/instructor/$exerciseId' import { InstructorTeamLandingPageRoute } from '../../routes/_protected/instructor/$exerciseId/$teamId' import { InstructorFormChannelActionLogRoute } from '../../routes/_protected/instructor/$exerciseId/$teamId/$channelId/form/$actionLogId' Loading Loading @@ -164,18 +161,6 @@ const useActionLogNavigate = } } const useToggleDone = (actionLog: ITodoLogActionLog) => { const [, setDone] = useTypedMutation(SetDone) return { toggleDone: () => setDone({ actionLogId: actionLog.id, state: !actionLog.done, typename: actionLog.__typename, }), } } const getContentInfo = (actionLog: ITodoLogActionLog): string => { switch (actionLog.details.__typename) { case 'IEmailType': { Loading Loading @@ -215,6 +200,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 Loading @@ -226,13 +212,15 @@ const LogItemActions: React.FC<{ const { toggleDone } = useToggleDone(actionLog) return ( <ButtonGroup vertical> <Button 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', text: t('overview.todoList.inspect'), }} link={getNavigateOptions(actionLog)} /> Loading frontend/src/routes/_protected/instructor/$exerciseId/$teamId/email/$tab/$threadId.tsx +8 −0 Original line number Diff line number Diff line Loading @@ -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' Loading Loading @@ -151,6 +152,13 @@ const RouteComponent = () => { } : undefined } markDoneAvailable overviewLink={{ to: InstructorLandingPageRoute.to, params: { exerciseId, }, }} /> ))} </div> Loading locale/resources/cs/frontend.json +9 −1 Original line number Diff line number Diff line Loading @@ -511,10 +511,18 @@ "overview": { "teamScores": "Skóre týmů", "todoList": { "markAsDone": "Označit jako dokončené", "markAsUndone": "Označit jako nedokončené", "markAsDoneAndReturn": "Označit jako dokončené a vrátit", "inspect": "Zkontrolovat", "done": "Dokončeno", "notDone": "Nedokončeno", "notFoundTitle": "Žádné injecty", "notFoundDescription": "Nebyly nalezeny žádné injecty, které by mohly být zobrazeny v téhle kategorii. Prosím počkejte na nové" "notFoundDescription": "Nebyly nalezeny žádné injecty, které by mohly být zobrazeny v téhle kategorii. Prosím počkejte na nové", "notification": { "description": "Email byl označen jako dokončený", "undo": "Vrátit zpět" } }, "instructorComments": { "title": "Komentáře od instruktorů", Loading Loading
frontend/src/email/TeamEmails/EmailCard.tsx +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' Loading @@ -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' Loading @@ -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 Loading @@ -54,6 +78,8 @@ export const EmailCard: FC<{ inInstructor?: boolean getFileLink: (fileId: string) => NavigateOptions assessmentProps?: AssessmentProps markDoneAvailable?: boolean overviewLink?: NavigateOptions }> = ({ exerciseId, teamId, Loading @@ -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) Loading Loading @@ -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 Loading @@ -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')} /> Loading Loading @@ -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> ) Loading
frontend/src/hooks/useToggleDone.ts 0 → 100644 +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, }) }, } }
frontend/src/instructor/InstructorTodoLog/LogItem.tsx +9 −21 Original line number Diff line number Diff line import { Button, ButtonGroup, Classes, Icon } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { ITodoLogActionLog } from '@inject/graphql' import { GetEmailThread, SetDone, useTypedMutation, useTypedQuery, } from '@inject/graphql' import { GetEmailThread, useTypedQuery } from '@inject/graphql' import { useTranslationFrontend } from '@inject/locale' import type { LinkButtonProps } from '@inject/shared' import { EmailSelection, LinkButton, Timestamp } from '@inject/shared' import type { NavigateOptions } from '@tanstack/react-router' import type { FC } from 'react' import { useToggleDone } from '../../hooks/useToggleDone' import { InstructorLandingPageRoute } from '../../routes/_protected/instructor/$exerciseId' import { InstructorTeamLandingPageRoute } from '../../routes/_protected/instructor/$exerciseId/$teamId' import { InstructorFormChannelActionLogRoute } from '../../routes/_protected/instructor/$exerciseId/$teamId/$channelId/form/$actionLogId' Loading Loading @@ -164,18 +161,6 @@ const useActionLogNavigate = } } const useToggleDone = (actionLog: ITodoLogActionLog) => { const [, setDone] = useTypedMutation(SetDone) return { toggleDone: () => setDone({ actionLogId: actionLog.id, state: !actionLog.done, typename: actionLog.__typename, }), } } const getContentInfo = (actionLog: ITodoLogActionLog): string => { switch (actionLog.details.__typename) { case 'IEmailType': { Loading Loading @@ -215,6 +200,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 Loading @@ -226,13 +212,15 @@ const LogItemActions: React.FC<{ const { toggleDone } = useToggleDone(actionLog) return ( <ButtonGroup vertical> <Button 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', text: t('overview.todoList.inspect'), }} link={getNavigateOptions(actionLog)} /> Loading
frontend/src/routes/_protected/instructor/$exerciseId/$teamId/email/$tab/$threadId.tsx +8 −0 Original line number Diff line number Diff line Loading @@ -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' Loading Loading @@ -151,6 +152,13 @@ const RouteComponent = () => { } : undefined } markDoneAvailable overviewLink={{ to: InstructorLandingPageRoute.to, params: { exerciseId, }, }} /> ))} </div> Loading
locale/resources/cs/frontend.json +9 −1 Original line number Diff line number Diff line Loading @@ -511,10 +511,18 @@ "overview": { "teamScores": "Skóre týmů", "todoList": { "markAsDone": "Označit jako dokončené", "markAsUndone": "Označit jako nedokončené", "markAsDoneAndReturn": "Označit jako dokončené a vrátit", "inspect": "Zkontrolovat", "done": "Dokončeno", "notDone": "Nedokončeno", "notFoundTitle": "Žádné injecty", "notFoundDescription": "Nebyly nalezeny žádné injecty, které by mohly být zobrazeny v téhle kategorii. Prosím počkejte na nové" "notFoundDescription": "Nebyly nalezeny žádné injecty, které by mohly být zobrazeny v téhle kategorii. Prosím počkejte na nové", "notification": { "description": "Email byl označen jako dokončený", "undo": "Vrátit zpět" } }, "instructorComments": { "title": "Komentáře od instruktorů", Loading