Commit d657d2f8 authored by Adam Parák's avatar Adam Parák 💬
Browse files

Merge branch '568-todo-marking' into 'main'

Resolve "nice to have - TODO list and review sync"

Closes #568

See merge request inject/frontend!483
parents 1661a0c5 34347b92
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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
+4 −1
Original line number Diff line number Diff line
@@ -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'

@@ -51,7 +53,7 @@ const InstructorEmailForm: FC<EmailFormProps> = ({
  },
}) => {
  const [{ fetching: loading }, sendEmailMutate] = useTypedMutation(SendEmail)

  const client = useClient()
  const { onSend } = useThreadSubmission({
    ...(emailThread
      ? { existingThreadId: emailThread.id }
@@ -79,6 +81,7 @@ const InstructorEmailForm: FC<EmailFormProps> = ({
      })
        .then(() => {
          discardDraft()
          instructorMarkTodo(client, threadId)
          onSuccess()
        })
        .catch(err => {
+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
+19 −2
Original line number Diff line number Diff line
@@ -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'

@@ -46,6 +49,7 @@ const InstructorQuestionnaire: FC<InstructorQuestionnaireProps> = ({
  relatedMilestones,
  hideReview,
}) => {
  const client = useClient()
  const [{ fetching: loading }, mutate] = useTypedMutation(ReviewQuestionnaire)
  const questionsAndAnswers: QuestionAndAnswer[] = useMemo(
    () =>
@@ -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' })
            })
        }}
+147 −74
Original line number Diff line number Diff line
@@ -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
@@ -222,7 +228,7 @@ const RequiresAttention: React.FC = () => (
)

const LogItemActions: React.FC<{
  actionLog?: SimplifiedActionLog
  actionLog: SimplifiedActionLog
}> = ({ actionLog }) => {
  const navTo = useNavTo()
  const client = useClient()
@@ -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,
@@ -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>
@@ -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