Loading .prettierignore +1 −1 Original line number Diff line number Diff line Loading @@ -5,5 +5,5 @@ router.ts graphql/schema-urql.ts graphql/urql/cache-typing.ts graphql/graphql-cache.d.ts graphql/graphql-env.ts graphql/graphql-env.d.ts graphql/fragment-types.ts frontend/src/email/EmailForm/InstructorEmailForm.tsx +4 −1 Original line number Diff line number Diff line Loading @@ -10,8 +10,10 @@ import { notify } from '@inject/shared/notification/engine' import notEmpty from '@inject/shared/utils/notEmpty' import type { FC } from 'react' import { memo, useCallback, useMemo } from 'react' import { useClient } from 'urql' import InstructorHeaderArea from './InstructorHeaderArea' import { form } from './classes' import instructorMarkTodo from './instructorMarkTodo' import type { EmailFormProps, OnSendEmailInput } from './typing' import useThreadSubmission from './useThreadSubmission' Loading Loading @@ -51,7 +53,7 @@ const InstructorEmailForm: FC<EmailFormProps> = ({ }, }) => { const [{ fetching: loading }, sendEmailMutate] = useTypedMutation(SendEmail) const client = useClient() const { onSend } = useThreadSubmission({ ...(emailThread ? { existingThreadId: emailThread.id } Loading Loading @@ -79,6 +81,7 @@ const InstructorEmailForm: FC<EmailFormProps> = ({ }) .then(() => { discardDraft() instructorMarkTodo(client, threadId) onSuccess() }) .catch(err => { Loading frontend/src/email/EmailForm/instructorMarkTodo.ts 0 → 100644 +31 −0 Original line number Diff line number Diff line import type { ResultOf, VariablesOf } from '@inject/graphql/graphql' import { SetEmailTodo } from '@inject/graphql/mutations.client' import { GetEmailThread } from '@inject/graphql/queries' import type { Client } from 'urql' const instructorMarkTodo = async (client: Client, threadId: string) => { console.warn('marking todo!') const { data } = await client .query<ResultOf<typeof GetEmailThread>, VariablesOf<typeof GetEmailThread>>( GetEmailThread, { threadId, } ) .toPromise() await Promise.all( (data?.emailThread.emails || []) .filter(x => !x.todo) .map(email => client .mutation<unknown, VariablesOf<typeof SetEmailTodo>>(SetEmailTodo, { emailId: email.id, state: true, }) .toPromise() ) ) } export default instructorMarkTodo frontend/src/instructor/InstructorQuestionnaire/index.tsx +19 −2 Original line number Diff line number Diff line Loading @@ -15,11 +15,14 @@ import type { QuestionRelatedMilestones, TeamQuestionnaireState, } from '@inject/graphql/fragment-types' import type { VariablesOf } from '@inject/graphql/graphql' import { useTypedMutation } from '@inject/graphql/graphql' import { ReviewQuestionnaire } from '@inject/graphql/mutations' import { SetTeamQuestionnaireTodo } from '@inject/graphql/mutations.client' import { notify } from '@inject/shared/notification/engine' import type { FC, ReactNode } from 'react' import { useCallback, useMemo, useState } from 'react' import { useClient } from 'urql' import ReviewButton from './ReviewButton' import { canBeReviewed } from './utils' Loading @@ -46,6 +49,7 @@ const InstructorQuestionnaire: FC<InstructorQuestionnaireProps> = ({ relatedMilestones, hideReview, }) => { const client = useClient() const [{ fetching: loading }, mutate] = useTypedMutation(ReviewQuestionnaire) const questionsAndAnswers: QuestionAndAnswer[] = useMemo( () => Loading Loading @@ -101,7 +105,20 @@ const InstructorQuestionnaire: FC<InstructorQuestionnaireProps> = ({ ({ deactivateMilestones }) => deactivateMilestones ), }, }).catch(error => { }) .then(() => { client .mutation< unknown, VariablesOf<typeof SetTeamQuestionnaireTodo> >(SetTeamQuestionnaireTodo, { questionnaireId, teamId, state: true, }) .toPromise() }) .catch(error => { notify(error.message, { intent: 'danger' }) }) }} Loading frontend/src/instructor/InstructorTodoLog/LogItem.tsx +147 −74 Original line number Diff line number Diff line Loading @@ -5,14 +5,20 @@ import { Button, ButtonGroup, Classes, Icon, Tag } from '@blueprintjs/core' import type { IconName } from '@blueprintjs/icons' import { css, cx } from '@emotion/css' import type { SimplifiedActionLog } from '@inject/graphql/fragment-types' import type { VariablesOf } from '@inject/graphql/graphql' import { useTypedQuery } from '@inject/graphql/graphql' import { SetTodoActionLog } from '@inject/graphql/mutations.client' import { GetSingleActionLog, GetThreadTemplates } from '@inject/graphql/queries' import { useClient } from '@inject/graphql/urql/client' import { getColorScheme } from '@inject/shared/components/ColorBox' import Timestamp from '@inject/shared/components/StyledTag/Timestamp' import { forwardRef, useCallback, type CSSProperties, type FC } from 'react' import { forwardRef, Suspense, useCallback, useMemo, type CSSProperties, type FC, } from 'react' import { execTodoWrite, selectTodoCat } from './utils' const GetTemplateCount: FC<{ threadId: string Loading Loading @@ -222,7 +228,7 @@ const RequiresAttention: React.FC = () => ( ) const LogItemActions: React.FC<{ actionLog?: SimplifiedActionLog actionLog: SimplifiedActionLog }> = ({ actionLog }) => { const navTo = useNavTo() const client = useClient() Loading @@ -230,59 +236,28 @@ const LogItemActions: React.FC<{ <ButtonGroup vertical> <Button icon='changes' disabled={!actionLog} onClick={() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions actionLog && client .mutation<unknown, VariablesOf<typeof SetTodoActionLog>>( SetTodoActionLog, { actionLogId: actionLog.id, state: !actionLog.todo, } ) .then() execTodoWrite(client, actionLog) }} > {actionLog ? actionLog.todo ? 'Mark as undone' : 'Mark as done' : 'Mark as ...'} {selectTodoCat(actionLog) ? 'Mark as undone' : 'Mark as done'} </Button> <Button icon='zoom-in' disabled={!actionLog} onClick={() => actionLog && navTo(actionLog)} > <Button icon='zoom-in' onClick={() => navTo(actionLog)}> Inspect </Button> </ButtonGroup> ) } const LogItem = forwardRef< const LogSkeleton = forwardRef< HTMLDivElement, { style?: CSSProperties actionLog: SimplifiedActionLog | null actionLogId: string } >(function LogItem({ actionLog: _actionLog, actionLogId, style }, ref) { const [{ data }] = useTypedQuery({ query: GetSingleActionLog, variables: { logId: actionLogId, }, pause: _actionLog !== null, }) const actionLog = _actionLog === null ? data?.actionLog : _actionLog >(function LogSkeleton({ style }, ref) { return ( <div ref={ref} key={_actionLog?.id || actionLogId} className={cx( Classes.CALLOUT, Classes.CALLOUT_ICON, Loading @@ -295,21 +270,14 @@ const LogItem = forwardRef< )} style={style} > <Icon icon={actionLog?.type ? getIcon(actionLog.type) : 'time'} /> <Icon icon={'time'} /> <div style={{ flexGrow: 0 }}> <h5 className={cx({ [Classes.HEADING]: true, [Classes.SKELETON]: !actionLog, })} className={cx(Classes.HEADING, Classes.SKELETON)} style={{ width: 'max-content' }} > {actionLog ? getTitle(actionLog) : 'Loading content'} Loading content </h5> {actionLog ? ( <OverviewPillNav actionLog={actionLog} /> ) : ( <> <Tag round minimal className={Classes.SKELETON}> Team Tag </Tag> Loading @@ -321,31 +289,136 @@ const LogItem = forwardRef< > Timestamp </Tag> </> <span className={Classes.SKELETON} style={{ display: 'inline-block', marginTop: '0.5rem' }} > Lorem ipsum dolor sit amet consectetur adipisicing elit. </span> </div> <div style={{ width: 'max-content' }}> <ButtonGroup vertical> <Button icon='changes' disabled> Mark as ... </Button> <Button icon='zoom-in' disabled> Inspect </Button> </ButtonGroup> </div> </div> ) }) const LogItemRender = forwardRef< HTMLDivElement, { style?: CSSProperties actionLog: SimplifiedActionLog } >(function LogItem({ actionLog, style }, ref) { return ( <div ref={ref} key={actionLog.id} className={cx( Classes.CALLOUT, Classes.CALLOUT_ICON, Classes.CALLOUT_HAS_BODY_CONTENT, css` display: flex; justify-content: space-between; width: 100%; ` )} style={style} > <Icon icon={getIcon(actionLog.type)} /> <div style={{ flexGrow: 0 }}> <h5 className={Classes.HEADING} style={{ width: 'max-content' }}> {getTitle(actionLog)} </h5> <OverviewPillNav actionLog={actionLog} /> {actionLog && ( <> {actionLog.requiresAttention && <RequiresAttention />} <Timestamp minimal datetime={new Date(actionLog.timestamp || 0)} /> </> )} {actionLog ? ( <GetContent actionLog={actionLog} /> ) : ( <span className={Classes.SKELETON} style={{ display: 'inline-block', marginTop: '0.5rem' }} > Lorem ipsum dolor sit amet consectetur adipisicing elit. </span> )} </div> <div style={{ width: 'max-content' }}> <LogItemActions actionLog={actionLog ?? undefined} /> <LogItemActions actionLog={actionLog} /> </div> </div> ) }) const LogItemSuspenseLoader = forwardRef< HTMLDivElement, { style?: CSSProperties actionLogId: string } >(function LogItem({ actionLogId, style }, ref) { const [{ data }] = useTypedQuery({ query: GetSingleActionLog, variables: { logId: actionLogId, }, context: useMemo( () => ({ suspense: true, }), [] ), }) if (!data) { return <></> } return ( <LogItemRender key={actionLogId} actionLog={data.actionLog} ref={ref} style={style} /> ) }) const LogItem = forwardRef< HTMLDivElement, { style?: CSSProperties actionLog: SimplifiedActionLog | null actionLogId: string } >(function LogItem({ actionLog, actionLogId, style }, ref) { if (actionLog === null) { return ( <Suspense fallback={<LogSkeleton style={style} />} key={actionLogId}> <LogItemSuspenseLoader ref={ref} style={style} actionLogId={actionLogId} /> </Suspense> ) } else { return ( <LogItemRender ref={ref} style={style} actionLog={actionLog} key={actionLog.id} /> ) } }) export default LogItem Loading
.prettierignore +1 −1 Original line number Diff line number Diff line Loading @@ -5,5 +5,5 @@ router.ts graphql/schema-urql.ts graphql/urql/cache-typing.ts graphql/graphql-cache.d.ts graphql/graphql-env.ts graphql/graphql-env.d.ts graphql/fragment-types.ts
frontend/src/email/EmailForm/InstructorEmailForm.tsx +4 −1 Original line number Diff line number Diff line Loading @@ -10,8 +10,10 @@ import { notify } from '@inject/shared/notification/engine' import notEmpty from '@inject/shared/utils/notEmpty' import type { FC } from 'react' import { memo, useCallback, useMemo } from 'react' import { useClient } from 'urql' import InstructorHeaderArea from './InstructorHeaderArea' import { form } from './classes' import instructorMarkTodo from './instructorMarkTodo' import type { EmailFormProps, OnSendEmailInput } from './typing' import useThreadSubmission from './useThreadSubmission' Loading Loading @@ -51,7 +53,7 @@ const InstructorEmailForm: FC<EmailFormProps> = ({ }, }) => { const [{ fetching: loading }, sendEmailMutate] = useTypedMutation(SendEmail) const client = useClient() const { onSend } = useThreadSubmission({ ...(emailThread ? { existingThreadId: emailThread.id } Loading Loading @@ -79,6 +81,7 @@ const InstructorEmailForm: FC<EmailFormProps> = ({ }) .then(() => { discardDraft() instructorMarkTodo(client, threadId) onSuccess() }) .catch(err => { Loading
frontend/src/email/EmailForm/instructorMarkTodo.ts 0 → 100644 +31 −0 Original line number Diff line number Diff line import type { ResultOf, VariablesOf } from '@inject/graphql/graphql' import { SetEmailTodo } from '@inject/graphql/mutations.client' import { GetEmailThread } from '@inject/graphql/queries' import type { Client } from 'urql' const instructorMarkTodo = async (client: Client, threadId: string) => { console.warn('marking todo!') const { data } = await client .query<ResultOf<typeof GetEmailThread>, VariablesOf<typeof GetEmailThread>>( GetEmailThread, { threadId, } ) .toPromise() await Promise.all( (data?.emailThread.emails || []) .filter(x => !x.todo) .map(email => client .mutation<unknown, VariablesOf<typeof SetEmailTodo>>(SetEmailTodo, { emailId: email.id, state: true, }) .toPromise() ) ) } export default instructorMarkTodo
frontend/src/instructor/InstructorQuestionnaire/index.tsx +19 −2 Original line number Diff line number Diff line Loading @@ -15,11 +15,14 @@ import type { QuestionRelatedMilestones, TeamQuestionnaireState, } from '@inject/graphql/fragment-types' import type { VariablesOf } from '@inject/graphql/graphql' import { useTypedMutation } from '@inject/graphql/graphql' import { ReviewQuestionnaire } from '@inject/graphql/mutations' import { SetTeamQuestionnaireTodo } from '@inject/graphql/mutations.client' import { notify } from '@inject/shared/notification/engine' import type { FC, ReactNode } from 'react' import { useCallback, useMemo, useState } from 'react' import { useClient } from 'urql' import ReviewButton from './ReviewButton' import { canBeReviewed } from './utils' Loading @@ -46,6 +49,7 @@ const InstructorQuestionnaire: FC<InstructorQuestionnaireProps> = ({ relatedMilestones, hideReview, }) => { const client = useClient() const [{ fetching: loading }, mutate] = useTypedMutation(ReviewQuestionnaire) const questionsAndAnswers: QuestionAndAnswer[] = useMemo( () => Loading Loading @@ -101,7 +105,20 @@ const InstructorQuestionnaire: FC<InstructorQuestionnaireProps> = ({ ({ deactivateMilestones }) => deactivateMilestones ), }, }).catch(error => { }) .then(() => { client .mutation< unknown, VariablesOf<typeof SetTeamQuestionnaireTodo> >(SetTeamQuestionnaireTodo, { questionnaireId, teamId, state: true, }) .toPromise() }) .catch(error => { notify(error.message, { intent: 'danger' }) }) }} Loading
frontend/src/instructor/InstructorTodoLog/LogItem.tsx +147 −74 Original line number Diff line number Diff line Loading @@ -5,14 +5,20 @@ import { Button, ButtonGroup, Classes, Icon, Tag } from '@blueprintjs/core' import type { IconName } from '@blueprintjs/icons' import { css, cx } from '@emotion/css' import type { SimplifiedActionLog } from '@inject/graphql/fragment-types' import type { VariablesOf } from '@inject/graphql/graphql' import { useTypedQuery } from '@inject/graphql/graphql' import { SetTodoActionLog } from '@inject/graphql/mutations.client' import { GetSingleActionLog, GetThreadTemplates } from '@inject/graphql/queries' import { useClient } from '@inject/graphql/urql/client' import { getColorScheme } from '@inject/shared/components/ColorBox' import Timestamp from '@inject/shared/components/StyledTag/Timestamp' import { forwardRef, useCallback, type CSSProperties, type FC } from 'react' import { forwardRef, Suspense, useCallback, useMemo, type CSSProperties, type FC, } from 'react' import { execTodoWrite, selectTodoCat } from './utils' const GetTemplateCount: FC<{ threadId: string Loading Loading @@ -222,7 +228,7 @@ const RequiresAttention: React.FC = () => ( ) const LogItemActions: React.FC<{ actionLog?: SimplifiedActionLog actionLog: SimplifiedActionLog }> = ({ actionLog }) => { const navTo = useNavTo() const client = useClient() Loading @@ -230,59 +236,28 @@ const LogItemActions: React.FC<{ <ButtonGroup vertical> <Button icon='changes' disabled={!actionLog} onClick={() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions actionLog && client .mutation<unknown, VariablesOf<typeof SetTodoActionLog>>( SetTodoActionLog, { actionLogId: actionLog.id, state: !actionLog.todo, } ) .then() execTodoWrite(client, actionLog) }} > {actionLog ? actionLog.todo ? 'Mark as undone' : 'Mark as done' : 'Mark as ...'} {selectTodoCat(actionLog) ? 'Mark as undone' : 'Mark as done'} </Button> <Button icon='zoom-in' disabled={!actionLog} onClick={() => actionLog && navTo(actionLog)} > <Button icon='zoom-in' onClick={() => navTo(actionLog)}> Inspect </Button> </ButtonGroup> ) } const LogItem = forwardRef< const LogSkeleton = forwardRef< HTMLDivElement, { style?: CSSProperties actionLog: SimplifiedActionLog | null actionLogId: string } >(function LogItem({ actionLog: _actionLog, actionLogId, style }, ref) { const [{ data }] = useTypedQuery({ query: GetSingleActionLog, variables: { logId: actionLogId, }, pause: _actionLog !== null, }) const actionLog = _actionLog === null ? data?.actionLog : _actionLog >(function LogSkeleton({ style }, ref) { return ( <div ref={ref} key={_actionLog?.id || actionLogId} className={cx( Classes.CALLOUT, Classes.CALLOUT_ICON, Loading @@ -295,21 +270,14 @@ const LogItem = forwardRef< )} style={style} > <Icon icon={actionLog?.type ? getIcon(actionLog.type) : 'time'} /> <Icon icon={'time'} /> <div style={{ flexGrow: 0 }}> <h5 className={cx({ [Classes.HEADING]: true, [Classes.SKELETON]: !actionLog, })} className={cx(Classes.HEADING, Classes.SKELETON)} style={{ width: 'max-content' }} > {actionLog ? getTitle(actionLog) : 'Loading content'} Loading content </h5> {actionLog ? ( <OverviewPillNav actionLog={actionLog} /> ) : ( <> <Tag round minimal className={Classes.SKELETON}> Team Tag </Tag> Loading @@ -321,31 +289,136 @@ const LogItem = forwardRef< > Timestamp </Tag> </> <span className={Classes.SKELETON} style={{ display: 'inline-block', marginTop: '0.5rem' }} > Lorem ipsum dolor sit amet consectetur adipisicing elit. </span> </div> <div style={{ width: 'max-content' }}> <ButtonGroup vertical> <Button icon='changes' disabled> Mark as ... </Button> <Button icon='zoom-in' disabled> Inspect </Button> </ButtonGroup> </div> </div> ) }) const LogItemRender = forwardRef< HTMLDivElement, { style?: CSSProperties actionLog: SimplifiedActionLog } >(function LogItem({ actionLog, style }, ref) { return ( <div ref={ref} key={actionLog.id} className={cx( Classes.CALLOUT, Classes.CALLOUT_ICON, Classes.CALLOUT_HAS_BODY_CONTENT, css` display: flex; justify-content: space-between; width: 100%; ` )} style={style} > <Icon icon={getIcon(actionLog.type)} /> <div style={{ flexGrow: 0 }}> <h5 className={Classes.HEADING} style={{ width: 'max-content' }}> {getTitle(actionLog)} </h5> <OverviewPillNav actionLog={actionLog} /> {actionLog && ( <> {actionLog.requiresAttention && <RequiresAttention />} <Timestamp minimal datetime={new Date(actionLog.timestamp || 0)} /> </> )} {actionLog ? ( <GetContent actionLog={actionLog} /> ) : ( <span className={Classes.SKELETON} style={{ display: 'inline-block', marginTop: '0.5rem' }} > Lorem ipsum dolor sit amet consectetur adipisicing elit. </span> )} </div> <div style={{ width: 'max-content' }}> <LogItemActions actionLog={actionLog ?? undefined} /> <LogItemActions actionLog={actionLog} /> </div> </div> ) }) const LogItemSuspenseLoader = forwardRef< HTMLDivElement, { style?: CSSProperties actionLogId: string } >(function LogItem({ actionLogId, style }, ref) { const [{ data }] = useTypedQuery({ query: GetSingleActionLog, variables: { logId: actionLogId, }, context: useMemo( () => ({ suspense: true, }), [] ), }) if (!data) { return <></> } return ( <LogItemRender key={actionLogId} actionLog={data.actionLog} ref={ref} style={style} /> ) }) const LogItem = forwardRef< HTMLDivElement, { style?: CSSProperties actionLog: SimplifiedActionLog | null actionLogId: string } >(function LogItem({ actionLog, actionLogId, style }, ref) { if (actionLog === null) { return ( <Suspense fallback={<LogSkeleton style={style} />} key={actionLogId}> <LogItemSuspenseLoader ref={ref} style={style} actionLogId={actionLogId} /> </Suspense> ) } else { return ( <LogItemRender ref={ref} style={style} actionLog={actionLog} key={actionLog.id} /> ) } }) export default LogItem