Loading frontend/src/email/EmailContactSelector/index.tsx +14 −16 Original line number Diff line number Diff line Loading @@ -15,30 +15,28 @@ interface EmailContactSelectorProps { inInstructor: boolean exerciseId: string placeholder?: string teamAddress?: string senderAddress: string } // TODO: add custom tag renderer not to render the "x" button for team address const EmailContactSelector: FC<EmailContactSelectorProps> = ({ placeholder, emailContacts, selectedContacts: initialSelectedContacts, selectedContacts, setSelectedContacts, inInstructor, exerciseId, teamAddress, senderAddress, }) => { const selectedContacts = useMemo( const suggestedItems = useMemo( () => teamAddress ? [teamAddress, ...initialSelectedContacts] : initialSelectedContacts, [initialSelectedContacts, teamAddress] emailContacts .map(contact => contact.address) .filter(address => address !== senderAddress), [emailContacts, senderAddress] ) const suggestedItems = useMemo( () => emailContacts.map(x => x?.address || '') || [], [emailContacts] const selectedItems = useMemo( () => selectedContacts.filter(contact => contact !== senderAddress), [selectedContacts, senderAddress] ) const [validateEmailAddress] = useValidateEmailAddressLazyQuery() Loading Loading @@ -88,8 +86,8 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ ) const itemDisabled = useCallback( (i: string) => i === teamAddress, [teamAddress] (i: string) => i === senderAddress, [senderAddress] ) const onItemSelect = useCallback( async (i: string) => { Loading Loading @@ -154,7 +152,7 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ minimal: true, }} resetOnSelect selectedItems={selectedContacts} selectedItems={selectedItems} tagRenderer={i => i} popoverContentProps={{ style: { maxHeight: '50vh', overflowY: 'auto', overflowX: 'hidden' }, Loading frontend/src/email/EmailForm/InstructorEmailForm.tsx +4 −4 Original line number Diff line number Diff line Loading @@ -10,8 +10,8 @@ import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyCon import notEmpty from '@inject/shared/utils/notEmpty' import type { FC } from 'react' import { memo, useCallback, useMemo } from 'react' import { MainDrawer } from './css' import HeaderArea from './modules/HeaderArea' import InstructorHeaderArea from './InstructorHeaderArea' import { form } from './classes' import type { EmailFormProps, OnSendEmailInput } from './typing' import useThreadSubmission from './useThreadSubmission' Loading Loading @@ -153,8 +153,8 @@ const InstructorEmailForm: FC<EmailFormProps> = ({ }, [onSend]) return ( <div className={MainDrawer}> <HeaderArea <div className={form}> <InstructorHeaderArea {...(emailThread ? { emailThread, Loading frontend/src/email/EmailForm/InstructorFromElement.tsx 0 → 100644 +55 −0 Original line number Diff line number Diff line import { InputGroup } from '@blueprintjs/core' import type { EmailParticipant } from '@inject/graphql/fragments/EmailParticipant.generated' import { type Dispatch, type FC, type SetStateAction } from 'react' import SenderSelector from '../SenderSelector' interface InstructorFromElementProps { senderList: string[] setSenderAddress: Dispatch<SetStateAction<string>> senderAddress: string contacts: EmailParticipant[] selectedContacts?: string[] setSelectedContacts?: Dispatch<SetStateAction<string[]>> } const InstructorFromElement: FC<InstructorFromElementProps> = ({ senderList, setSenderAddress, senderAddress, contacts, selectedContacts, setSelectedContacts, }) => { if (selectedContacts === undefined) { // existing thread return senderList.length > 1 ? ( <SenderSelector emailContacts={contacts} senderList={senderList} senderAddress={senderAddress} setSenderAddress={setSenderAddress} /> ) : ( <InputGroup readOnly value={senderAddress} /> ) } // new thread return ( <SenderSelector emailContacts={contacts} senderList={contacts .filter(contacts => contacts.definitionAddress) .map(contact => contact.address)} senderAddress={senderAddress} onItemSelect={item => { setSelectedContacts?.(prev => prev.includes(item) ? prev : [...prev, item] ) setSenderAddress(item) }} /> ) } export default InstructorFromElement frontend/src/email/EmailForm/modules/HeaderArea.tsx→frontend/src/email/EmailForm/InstructorHeaderArea.tsx +37 −97 Original line number Diff line number Diff line import EmailContactSelector from '@/email/EmailContactSelector' import EmailTemplates from '@/email/EmailTemplates' import SenderSelector from '@/email/SenderSelector' import { Button, InputGroup } from '@blueprintjs/core' import { css } from '@emotion/css' import type { EmailParticipant } from '@inject/graphql/fragments/EmailParticipant.generated' import type { EmailTemplate } from '@inject/graphql/fragments/EmailTemplate.generated' import type { EmailThread } from '@inject/graphql/fragments/EmailThread.generated' import type { FileInfo } from '@inject/graphql/fragments/FileInfo.generated' import notEmpty from '@inject/shared/utils/notEmpty' import type { Dispatch, FC, SetStateAction } from 'react' import { useEffect, useMemo, useState } from 'react' import InstructorFromElement from './InstructorFromElement' import { contactSelector, contactSelectorWrapper, gridContainer, gridItem, } from './classes' import type { HeaderAreaProps } from './typing' const gridContainer = css` display: grid; grid-template-columns: auto 1fr; grid-column-gap: 0.5rem; ` const gridItem = css` display: flex; align-items: center; ` const contactSelectorWrapper = css` width: 100%; display: flex; ` const contactSelector = css` flex: 1; ` type HeaderAreaProps = ( | { // new thread emailThread?: undefined selectedContacts: string[] setSelectedContacts: Dispatch<SetStateAction<string[]>> bccSelectedContacts: string[] setBccSelectedContacts: Dispatch<SetStateAction<string[]>> subject: string | undefined setSubject: Dispatch<SetStateAction<string | undefined>> } | { // existing thread emailThread: EmailThread selectedContacts?: undefined setSelectedContacts?: undefined bccSelectedContacts?: undefined setBccSelectedContacts?: undefined subject?: undefined setSubject?: undefined } ) & ( | { // instructor type InstructorHeaderAreaProps = HeaderAreaProps & { senderList: string[] setSenderAddress: Dispatch<SetStateAction<string>> setContent: Dispatch<SetStateAction<string>> Loading @@ -66,28 +26,9 @@ type HeaderAreaProps = ( setTemplate: Dispatch<SetStateAction<EmailTemplate | undefined>> teamId: string senderAddress: string teamAddress?: never } | { // trainee senderList?: undefined setSenderAddress?: undefined setContent?: undefined setActivateMilestone?: undefined setDeactivateMilestone?: undefined setFileInfo?: undefined template?: undefined setTemplate?: undefined teamId?: undefined senderAddress?: never teamAddress: string } ) & { contacts: EmailParticipant[] exerciseId: string } const HeaderArea: FC<HeaderAreaProps> = ({ const InstructorHeaderArea: FC<InstructorHeaderAreaProps> = ({ senderAddress, emailThread, senderList, Loading @@ -107,15 +48,15 @@ const HeaderArea: FC<HeaderAreaProps> = ({ setTemplate, teamId, exerciseId, teamAddress, }) => { const emailThreadParticipants = useMemo( () => (emailThread?.participants || []) .filter(notEmpty) .map(p => p.address) .map(participant => participant.address) .filter(address => address !== senderAddress) .join(', '), [emailThread] [emailThread?.participants, senderAddress] ) useEffect(() => { Loading @@ -132,31 +73,29 @@ const HeaderArea: FC<HeaderAreaProps> = ({ return ( <div className={gridContainer}> <div className={gridItem}>From:</div> {senderList ? ( <SenderSelector emailContacts={contacts} <InstructorFromElement selectedContacts={selectedContacts} setSelectedContacts={setSelectedContacts} senderList={senderList} senderAddress={senderAddress} setSenderAddress={setSenderAddress} contacts={contacts} /> ) : ( <InputGroup readOnly value={teamAddress} /> )} <div className={gridItem}>Participants:</div> <div className={gridItem}>To:</div> {emailThread ? ( <InputGroup readOnly value={emailThreadParticipants} /> ) : ( <div className={contactSelectorWrapper}> <div className={contactSelector}> <EmailContactSelector placeholder='Add participants...' placeholder='Add recipients...' emailContacts={contacts} selectedContacts={selectedContacts} setSelectedContacts={setSelectedContacts} inInstructor={senderList !== undefined} exerciseId={exerciseId} teamAddress={teamAddress} senderAddress={senderAddress} /> </div> <Button Loading Loading @@ -185,6 +124,7 @@ const HeaderArea: FC<HeaderAreaProps> = ({ setSelectedContacts={setBccSelectedContacts} inInstructor={senderList !== undefined} exerciseId={exerciseId} senderAddress={senderAddress} /> </> )} Loading Loading @@ -223,4 +163,4 @@ const HeaderArea: FC<HeaderAreaProps> = ({ ) } export default HeaderArea export default InstructorHeaderArea frontend/src/email/EmailForm/TraineeEmailForm.tsx +4 −4 Original line number Diff line number Diff line Loading @@ -8,8 +8,8 @@ import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyCon import notEmpty from '@inject/shared/utils/notEmpty' import type { FC } from 'react' import { memo, useCallback } from 'react' import { MainDrawer } from './css' import HeaderArea from './modules/HeaderArea' import TraineeHeaderArea from './TraineeHeaderArea' import { form } from './classes' import type { EmailFormProps, OnSendEmailInput } from './typing' import useThreadSubmission from './useThreadSubmission' Loading Loading @@ -99,8 +99,8 @@ const TraineeEmailForm: FC<EmailFormProps> = ({ }, [onSuccess, storeDraft]) return ( <div className={MainDrawer}> <HeaderArea <div className={form}> <TraineeHeaderArea {...(emailThread ? { emailThread, Loading Loading
frontend/src/email/EmailContactSelector/index.tsx +14 −16 Original line number Diff line number Diff line Loading @@ -15,30 +15,28 @@ interface EmailContactSelectorProps { inInstructor: boolean exerciseId: string placeholder?: string teamAddress?: string senderAddress: string } // TODO: add custom tag renderer not to render the "x" button for team address const EmailContactSelector: FC<EmailContactSelectorProps> = ({ placeholder, emailContacts, selectedContacts: initialSelectedContacts, selectedContacts, setSelectedContacts, inInstructor, exerciseId, teamAddress, senderAddress, }) => { const selectedContacts = useMemo( const suggestedItems = useMemo( () => teamAddress ? [teamAddress, ...initialSelectedContacts] : initialSelectedContacts, [initialSelectedContacts, teamAddress] emailContacts .map(contact => contact.address) .filter(address => address !== senderAddress), [emailContacts, senderAddress] ) const suggestedItems = useMemo( () => emailContacts.map(x => x?.address || '') || [], [emailContacts] const selectedItems = useMemo( () => selectedContacts.filter(contact => contact !== senderAddress), [selectedContacts, senderAddress] ) const [validateEmailAddress] = useValidateEmailAddressLazyQuery() Loading Loading @@ -88,8 +86,8 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ ) const itemDisabled = useCallback( (i: string) => i === teamAddress, [teamAddress] (i: string) => i === senderAddress, [senderAddress] ) const onItemSelect = useCallback( async (i: string) => { Loading Loading @@ -154,7 +152,7 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ minimal: true, }} resetOnSelect selectedItems={selectedContacts} selectedItems={selectedItems} tagRenderer={i => i} popoverContentProps={{ style: { maxHeight: '50vh', overflowY: 'auto', overflowX: 'hidden' }, Loading
frontend/src/email/EmailForm/InstructorEmailForm.tsx +4 −4 Original line number Diff line number Diff line Loading @@ -10,8 +10,8 @@ import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyCon import notEmpty from '@inject/shared/utils/notEmpty' import type { FC } from 'react' import { memo, useCallback, useMemo } from 'react' import { MainDrawer } from './css' import HeaderArea from './modules/HeaderArea' import InstructorHeaderArea from './InstructorHeaderArea' import { form } from './classes' import type { EmailFormProps, OnSendEmailInput } from './typing' import useThreadSubmission from './useThreadSubmission' Loading Loading @@ -153,8 +153,8 @@ const InstructorEmailForm: FC<EmailFormProps> = ({ }, [onSend]) return ( <div className={MainDrawer}> <HeaderArea <div className={form}> <InstructorHeaderArea {...(emailThread ? { emailThread, Loading
frontend/src/email/EmailForm/InstructorFromElement.tsx 0 → 100644 +55 −0 Original line number Diff line number Diff line import { InputGroup } from '@blueprintjs/core' import type { EmailParticipant } from '@inject/graphql/fragments/EmailParticipant.generated' import { type Dispatch, type FC, type SetStateAction } from 'react' import SenderSelector from '../SenderSelector' interface InstructorFromElementProps { senderList: string[] setSenderAddress: Dispatch<SetStateAction<string>> senderAddress: string contacts: EmailParticipant[] selectedContacts?: string[] setSelectedContacts?: Dispatch<SetStateAction<string[]>> } const InstructorFromElement: FC<InstructorFromElementProps> = ({ senderList, setSenderAddress, senderAddress, contacts, selectedContacts, setSelectedContacts, }) => { if (selectedContacts === undefined) { // existing thread return senderList.length > 1 ? ( <SenderSelector emailContacts={contacts} senderList={senderList} senderAddress={senderAddress} setSenderAddress={setSenderAddress} /> ) : ( <InputGroup readOnly value={senderAddress} /> ) } // new thread return ( <SenderSelector emailContacts={contacts} senderList={contacts .filter(contacts => contacts.definitionAddress) .map(contact => contact.address)} senderAddress={senderAddress} onItemSelect={item => { setSelectedContacts?.(prev => prev.includes(item) ? prev : [...prev, item] ) setSenderAddress(item) }} /> ) } export default InstructorFromElement
frontend/src/email/EmailForm/modules/HeaderArea.tsx→frontend/src/email/EmailForm/InstructorHeaderArea.tsx +37 −97 Original line number Diff line number Diff line import EmailContactSelector from '@/email/EmailContactSelector' import EmailTemplates from '@/email/EmailTemplates' import SenderSelector from '@/email/SenderSelector' import { Button, InputGroup } from '@blueprintjs/core' import { css } from '@emotion/css' import type { EmailParticipant } from '@inject/graphql/fragments/EmailParticipant.generated' import type { EmailTemplate } from '@inject/graphql/fragments/EmailTemplate.generated' import type { EmailThread } from '@inject/graphql/fragments/EmailThread.generated' import type { FileInfo } from '@inject/graphql/fragments/FileInfo.generated' import notEmpty from '@inject/shared/utils/notEmpty' import type { Dispatch, FC, SetStateAction } from 'react' import { useEffect, useMemo, useState } from 'react' import InstructorFromElement from './InstructorFromElement' import { contactSelector, contactSelectorWrapper, gridContainer, gridItem, } from './classes' import type { HeaderAreaProps } from './typing' const gridContainer = css` display: grid; grid-template-columns: auto 1fr; grid-column-gap: 0.5rem; ` const gridItem = css` display: flex; align-items: center; ` const contactSelectorWrapper = css` width: 100%; display: flex; ` const contactSelector = css` flex: 1; ` type HeaderAreaProps = ( | { // new thread emailThread?: undefined selectedContacts: string[] setSelectedContacts: Dispatch<SetStateAction<string[]>> bccSelectedContacts: string[] setBccSelectedContacts: Dispatch<SetStateAction<string[]>> subject: string | undefined setSubject: Dispatch<SetStateAction<string | undefined>> } | { // existing thread emailThread: EmailThread selectedContacts?: undefined setSelectedContacts?: undefined bccSelectedContacts?: undefined setBccSelectedContacts?: undefined subject?: undefined setSubject?: undefined } ) & ( | { // instructor type InstructorHeaderAreaProps = HeaderAreaProps & { senderList: string[] setSenderAddress: Dispatch<SetStateAction<string>> setContent: Dispatch<SetStateAction<string>> Loading @@ -66,28 +26,9 @@ type HeaderAreaProps = ( setTemplate: Dispatch<SetStateAction<EmailTemplate | undefined>> teamId: string senderAddress: string teamAddress?: never } | { // trainee senderList?: undefined setSenderAddress?: undefined setContent?: undefined setActivateMilestone?: undefined setDeactivateMilestone?: undefined setFileInfo?: undefined template?: undefined setTemplate?: undefined teamId?: undefined senderAddress?: never teamAddress: string } ) & { contacts: EmailParticipant[] exerciseId: string } const HeaderArea: FC<HeaderAreaProps> = ({ const InstructorHeaderArea: FC<InstructorHeaderAreaProps> = ({ senderAddress, emailThread, senderList, Loading @@ -107,15 +48,15 @@ const HeaderArea: FC<HeaderAreaProps> = ({ setTemplate, teamId, exerciseId, teamAddress, }) => { const emailThreadParticipants = useMemo( () => (emailThread?.participants || []) .filter(notEmpty) .map(p => p.address) .map(participant => participant.address) .filter(address => address !== senderAddress) .join(', '), [emailThread] [emailThread?.participants, senderAddress] ) useEffect(() => { Loading @@ -132,31 +73,29 @@ const HeaderArea: FC<HeaderAreaProps> = ({ return ( <div className={gridContainer}> <div className={gridItem}>From:</div> {senderList ? ( <SenderSelector emailContacts={contacts} <InstructorFromElement selectedContacts={selectedContacts} setSelectedContacts={setSelectedContacts} senderList={senderList} senderAddress={senderAddress} setSenderAddress={setSenderAddress} contacts={contacts} /> ) : ( <InputGroup readOnly value={teamAddress} /> )} <div className={gridItem}>Participants:</div> <div className={gridItem}>To:</div> {emailThread ? ( <InputGroup readOnly value={emailThreadParticipants} /> ) : ( <div className={contactSelectorWrapper}> <div className={contactSelector}> <EmailContactSelector placeholder='Add participants...' placeholder='Add recipients...' emailContacts={contacts} selectedContacts={selectedContacts} setSelectedContacts={setSelectedContacts} inInstructor={senderList !== undefined} exerciseId={exerciseId} teamAddress={teamAddress} senderAddress={senderAddress} /> </div> <Button Loading Loading @@ -185,6 +124,7 @@ const HeaderArea: FC<HeaderAreaProps> = ({ setSelectedContacts={setBccSelectedContacts} inInstructor={senderList !== undefined} exerciseId={exerciseId} senderAddress={senderAddress} /> </> )} Loading Loading @@ -223,4 +163,4 @@ const HeaderArea: FC<HeaderAreaProps> = ({ ) } export default HeaderArea export default InstructorHeaderArea
frontend/src/email/EmailForm/TraineeEmailForm.tsx +4 −4 Original line number Diff line number Diff line Loading @@ -8,8 +8,8 @@ import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyCon import notEmpty from '@inject/shared/utils/notEmpty' import type { FC } from 'react' import { memo, useCallback } from 'react' import { MainDrawer } from './css' import HeaderArea from './modules/HeaderArea' import TraineeHeaderArea from './TraineeHeaderArea' import { form } from './classes' import type { EmailFormProps, OnSendEmailInput } from './typing' import useThreadSubmission from './useThreadSubmission' Loading Loading @@ -99,8 +99,8 @@ const TraineeEmailForm: FC<EmailFormProps> = ({ }, [onSuccess, storeDraft]) return ( <div className={MainDrawer}> <HeaderArea <div className={form}> <TraineeHeaderArea {...(emailThread ? { emailThread, Loading