Loading CHANGELOG.md +2 −1 Original line number Diff line number Diff line 2024-05-07 - improve new email highlighting 2024-05-14 - improve new email highlighting 2024-05-07 - add the option to add not team-visible addresses to the recipients list 2024-05-07 - add exercise name, rework exercise panel add dialogs 2024-05-07 - add learning objectives page to the instructor view 2024-05-07 - change the tab icon to the INJECT logo Loading backend @ b0d99e75 Compare caf6b1c1 to b0d99e75 Original line number Diff line number Diff line Subproject commit caf6b1c1038b43f45124e4a9220e63d822a8e900 Subproject commit b0d99e75be6486619ae5abddcfc347c79c72f54a codegen/gql/queries/ValidateEmailAddress.graphql 0 → 100644 +3 −0 Original line number Diff line number Diff line query ValidateEmailAddress($exerciseId: ID!, $address: String!) { validateEmailAddress(exerciseId: $exerciseId, address: $address) } frontend/src/email/EmailContactSelector/index.tsx +49 −4 Original line number Diff line number Diff line Loading @@ -2,7 +2,9 @@ import { MenuItem, Tooltip } from '@blueprintjs/core' import type { ItemRenderer } from '@blueprintjs/select' import { MultiSelect } from '@blueprintjs/select' import type { EmailParticipant } from '@inject/graphql/fragments/EmailParticipant.generated' import type { Dispatch, FC, SetStateAction } from 'react' import { useValidateEmailAddressLazyQuery } from '@inject/graphql/queries/ValidateEmailAddress.generated' import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' import type { Dispatch, FC, MouseEventHandler, SetStateAction } from 'react' import { memo, useCallback, useMemo } from 'react' import type { ExtendedItemRenderer } from '../typing' Loading @@ -12,6 +14,7 @@ interface EmailContactSelectorProps { selectedContacts: string[] setSelectedContacts: Dispatch<SetStateAction<string[]>> inInstructor: boolean exerciseId: string } const EmailContactSelector: FC<EmailContactSelectorProps> = ({ Loading @@ -20,12 +23,17 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ senderAddress, setSelectedContacts, inInstructor, exerciseId, }) => { const suggestedItems = useMemo( () => emailContacts.map(x => x?.address || '') || [], [emailContacts] ) const [validateEmailAddress] = useValidateEmailAddressLazyQuery() const { notify } = useNotifyContext() const itemRenderer: ExtendedItemRenderer<string> = useCallback( (i, { handleClick, handleFocus, modifiers, hasTooltip }) => ( <MenuItem Loading Loading @@ -66,20 +74,55 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ [selectedContacts, senderAddress] ) const onItemSelect = useCallback( (i: string) => async (i: string) => { const { data: validateData } = await validateEmailAddress({ variables: { exerciseId, address: i, }, }) if (!validateData?.validateEmailAddress) { notify('Invalid email address', { intent: 'danger' }) return } setSelectedContacts(prev => { if (prev.includes(i)) { return prev.filter(x => x !== i) } return [...prev, i] }), [setSelectedContacts] }) }, [exerciseId, notify, setSelectedContacts, validateEmailAddress] ) const onRemove = useCallback( (i: string) => setSelectedContacts(prev => prev.filter(x => x !== i)), [setSelectedContacts] ) const createNewItemFromQuery: (query: string) => string | string[] = useCallback((query: string) => query, []) const createNewItemRenderer: ( query: string, active: boolean, handleClick: MouseEventHandler<HTMLElement> ) => undefined | JSX.Element = useCallback( ( query: string, active: boolean, handleClick: MouseEventHandler<HTMLElement> ) => ( <MenuItem active={active} onClick={handleClick} roleStructure='listoption' shouldDismissPopover={false} text={query} /> ), [] ) return ( <MultiSelect<string> placeholder='Recipients' Loading @@ -98,6 +141,8 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ popoverContentProps={{ style: { maxHeight: '50vh', overflowY: 'auto', overflowX: 'hidden' }, }} createNewItemFromQuery={createNewItemFromQuery} createNewItemRenderer={createNewItemRenderer} /> ) } Loading frontend/src/email/EmailForm/TraineeEmailForm.tsx +1 −0 Original line number Diff line number Diff line Loading @@ -110,6 +110,7 @@ const TraineeEmailForm: FC<TraineeEmailFormProps> = ({ })} contacts={traineeList} senderAddress={teamAddress} exerciseId={exerciseId} /> <Divider style={{ margin: '0.5rem 0' }} /> Loading Loading
CHANGELOG.md +2 −1 Original line number Diff line number Diff line 2024-05-07 - improve new email highlighting 2024-05-14 - improve new email highlighting 2024-05-07 - add the option to add not team-visible addresses to the recipients list 2024-05-07 - add exercise name, rework exercise panel add dialogs 2024-05-07 - add learning objectives page to the instructor view 2024-05-07 - change the tab icon to the INJECT logo Loading
backend @ b0d99e75 Compare caf6b1c1 to b0d99e75 Original line number Diff line number Diff line Subproject commit caf6b1c1038b43f45124e4a9220e63d822a8e900 Subproject commit b0d99e75be6486619ae5abddcfc347c79c72f54a
codegen/gql/queries/ValidateEmailAddress.graphql 0 → 100644 +3 −0 Original line number Diff line number Diff line query ValidateEmailAddress($exerciseId: ID!, $address: String!) { validateEmailAddress(exerciseId: $exerciseId, address: $address) }
frontend/src/email/EmailContactSelector/index.tsx +49 −4 Original line number Diff line number Diff line Loading @@ -2,7 +2,9 @@ import { MenuItem, Tooltip } from '@blueprintjs/core' import type { ItemRenderer } from '@blueprintjs/select' import { MultiSelect } from '@blueprintjs/select' import type { EmailParticipant } from '@inject/graphql/fragments/EmailParticipant.generated' import type { Dispatch, FC, SetStateAction } from 'react' import { useValidateEmailAddressLazyQuery } from '@inject/graphql/queries/ValidateEmailAddress.generated' import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' import type { Dispatch, FC, MouseEventHandler, SetStateAction } from 'react' import { memo, useCallback, useMemo } from 'react' import type { ExtendedItemRenderer } from '../typing' Loading @@ -12,6 +14,7 @@ interface EmailContactSelectorProps { selectedContacts: string[] setSelectedContacts: Dispatch<SetStateAction<string[]>> inInstructor: boolean exerciseId: string } const EmailContactSelector: FC<EmailContactSelectorProps> = ({ Loading @@ -20,12 +23,17 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ senderAddress, setSelectedContacts, inInstructor, exerciseId, }) => { const suggestedItems = useMemo( () => emailContacts.map(x => x?.address || '') || [], [emailContacts] ) const [validateEmailAddress] = useValidateEmailAddressLazyQuery() const { notify } = useNotifyContext() const itemRenderer: ExtendedItemRenderer<string> = useCallback( (i, { handleClick, handleFocus, modifiers, hasTooltip }) => ( <MenuItem Loading Loading @@ -66,20 +74,55 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ [selectedContacts, senderAddress] ) const onItemSelect = useCallback( (i: string) => async (i: string) => { const { data: validateData } = await validateEmailAddress({ variables: { exerciseId, address: i, }, }) if (!validateData?.validateEmailAddress) { notify('Invalid email address', { intent: 'danger' }) return } setSelectedContacts(prev => { if (prev.includes(i)) { return prev.filter(x => x !== i) } return [...prev, i] }), [setSelectedContacts] }) }, [exerciseId, notify, setSelectedContacts, validateEmailAddress] ) const onRemove = useCallback( (i: string) => setSelectedContacts(prev => prev.filter(x => x !== i)), [setSelectedContacts] ) const createNewItemFromQuery: (query: string) => string | string[] = useCallback((query: string) => query, []) const createNewItemRenderer: ( query: string, active: boolean, handleClick: MouseEventHandler<HTMLElement> ) => undefined | JSX.Element = useCallback( ( query: string, active: boolean, handleClick: MouseEventHandler<HTMLElement> ) => ( <MenuItem active={active} onClick={handleClick} roleStructure='listoption' shouldDismissPopover={false} text={query} /> ), [] ) return ( <MultiSelect<string> placeholder='Recipients' Loading @@ -98,6 +141,8 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ popoverContentProps={{ style: { maxHeight: '50vh', overflowY: 'auto', overflowX: 'hidden' }, }} createNewItemFromQuery={createNewItemFromQuery} createNewItemRenderer={createNewItemRenderer} /> ) } Loading
frontend/src/email/EmailForm/TraineeEmailForm.tsx +1 −0 Original line number Diff line number Diff line Loading @@ -110,6 +110,7 @@ const TraineeEmailForm: FC<TraineeEmailFormProps> = ({ })} contacts={traineeList} senderAddress={teamAddress} exerciseId={exerciseId} /> <Divider style={{ margin: '0.5rem 0' }} /> Loading