Loading frontend/src/email/EmailContactSelector/index.tsx +0 −1 Original line number Diff line number Diff line Loading @@ -74,7 +74,6 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ onItemSelect={onItemSelect} onRemove={onRemove} popoverProps={{ matchTargetWidth: true, minimal: true, }} resetOnSelect Loading frontend/src/email/EmailForm/InstructorEmailForm.tsx +1 −8 Original line number Diff line number Diff line Loading @@ -56,14 +56,7 @@ const InstructorEmailForm: FC<EmailFormProps> = ({ emailThread, threadId, teamId, submitFunction({ threadId, senderAddress, content, fileName, activateMilestone, deactivateMilestone, }) { submitFunction: (threadId: string) => { instructorMutate({ variables: { threadId, Loading frontend/src/email/EmailForm/TraineeEmailForm.tsx +4 −5 Original line number Diff line number Diff line Loading @@ -28,7 +28,6 @@ const TraineeEmailForm: FC<EmailFormProps> = ({ setContent, discard, storeDraft, senderAddress, selectedContacts, setSelectedContacts, subject, Loading @@ -44,15 +43,15 @@ const TraineeEmailForm: FC<EmailFormProps> = ({ file, fileName, subject: subject || '', senderAddress, senderAddress: teamAddress!, selectedContacts, threadId, teamId, submitFunction({ threadId, senderAddress, content, fileName }) { submitFunction: threadId => { traineeMutate({ variables: { threadId, senderAddress, senderAddress: teamAddress!, content, fileName, }, Loading Loading @@ -98,7 +97,7 @@ const TraineeEmailForm: FC<EmailFormProps> = ({ subject, setSubject, })} senderAddress={senderAddress} senderAddress={teamAddress!} /> <Divider style={{ margin: '0.5rem 0' }} /> Loading frontend/src/email/EmailForm/useThreadSubmission.ts +2 −20 Original line number Diff line number Diff line Loading @@ -13,14 +13,7 @@ type ThreadSubmissionProps = { subject: string emailThread?: EmailThread threadId?: string submitFunction: ({ threadId, senderAddress, content, fileName, activateMilestone, deactivateMilestone, }: SendEmailInput) => void submitFunction: (threadId: string) => void } & Omit<SendEmailInput, 'threadId'> const useThreadSubmission = ({ Loading @@ -33,8 +26,6 @@ const useThreadSubmission = ({ selectedContacts, subject, emailThread, activateMilestone, deactivateMilestone, submitFunction, }: ThreadSubmissionProps) => { const [createThread] = useCreateThread() Loading Loading @@ -102,19 +93,10 @@ const useThreadSubmission = ({ }) } return submitFunction({ threadId: threadIdPtr as string, senderAddress, content, fileName, activateMilestone, deactivateMilestone, }) return submitFunction(emailThread?.id || threadIdPtr!) }, [ activateMilestone, content, createThread, deactivateMilestone, emailThread, file, fileName, Loading frontend/src/instructor/MilestoneSelector/MilestoneSelector.tsx +86 −156 Original line number Diff line number Diff line /* eslint-disable @typescript-eslint/no-unused-vars */ import type { ItemRenderer, ItemRendererProps } from '@blueprintjs/select' import { MultiSelect } from '@blueprintjs/select' import { MenuItem } from '@blueprintjs/core' import type { SetStateAction } from 'react' import type React from 'react' import { memo, useCallback, useEffect, useRef, useState } from 'react' import { memo, useEffect, useState } from 'react' import notEmpty from '@inject/shared/utils/notEmpty' import { useGetTeamMilestones } from '@inject/graphql/queries/GetTeamMilestones.generated' import { MilestoneState } from '@inject/graphql/fragments/MilestoneState.generated' // TODO: refactor this into an object, there is an O(n) search in the code, it can be "O(1)" export interface MilestoneOption { name: string milestoneId: string selected?: boolean not: boolean milestoneState: MilestoneState selected: boolean } const MilestoneItemRenderer: ItemRenderer<MilestoneOption> = ( milestone, const getMilestoneOptionText = (milestoneOption: MilestoneOption) => milestoneOption.milestoneState.reached ? `not ${milestoneOption.milestoneState.milestone.name}` : milestoneOption.milestoneState.milestone.name const MilestoneOptionRenderer: ItemRenderer<MilestoneOption> = ( milestoneOption, { handleClick, handleFocus, modifiers, ref }: ItemRendererProps ) => { if (!modifiers.matchesPredicate) { Loading @@ -24,9 +28,9 @@ const MilestoneItemRenderer: ItemRenderer<MilestoneOption> = ( } return ( <MenuItem key={milestone?.name} text={milestone?.name} selected={milestone?.selected} key={milestoneOption.milestoneState.id} text={getMilestoneOptionText(milestoneOption)} selected={milestoneOption.selected} roleStructure='listoption' shouldDismissPopover={false} onClick={handleClick} Loading @@ -40,8 +44,6 @@ const MilestoneItemRenderer: ItemRenderer<MilestoneOption> = ( interface MilestoneSelectorProps { teamId: string /** Initialization string, contains milestone strings separated with " and " as specified in the docs * when string is changed with setState, selector is initialized again */ activateMilestone: string deactivateMilestone: string setActivateMilestone: React.Dispatch<SetStateAction<string>> Loading @@ -62,171 +64,101 @@ const MilestoneSelector = ({ }, }) const [milestones, setMilestonesList] = useState<MilestoneOption[]>( (data?.teamMilestones || []).filter(notEmpty).flatMap( // TODO remove this condition object creation, if invisible milestones causes problems ({ milestone, reached }) => !reached ? { name: milestone.name || '', milestoneId: milestone.name || '', not: false, } : { name: `not ${milestone.name || ''}`, milestoneId: milestone.name || '', not: true, } ) const [milestoneOptions, setMilestoneOptions] = useState<MilestoneOption[]>( (data?.teamMilestones || []) .filter(notEmpty) .map(milestoneState => ({ milestoneState, selected: false })) ) const [selectedMilestones, setSelectedMilestones] = useState< MilestoneOption[] >(() => { // this is duplicate compared to prev version, it is necessary to prevent state changes, when initializing // instead of using handleItemChange const preselected: MilestoneOption[] = [] const milestoneloop = (set: boolean) => (milestone: string) => { const milestoneOption = milestones.find(m => m.name === milestone) if (milestoneOption) { setMilestonesList(prevState => { milestoneOption.selected = set return [...prevState] }) preselected.push(milestoneOption) } } activateMilestone.split(',').forEach(milestoneloop(true)) deactivateMilestone.split(',').forEach(milestoneloop(true)) return preselected }) useEffect(() => { setActivateMilestone( milestones .filter(x => x.selected) .filter(x => !x.not) .map(milestone => milestone.name) .join(',') ) setDeactivateMilestone( milestones .filter(x => x.selected) .filter(x => x.not) .map(milestone => milestone.name) .map(x => x.slice(4)) .join(',') // TODO: update when loading a template // const setSelectedMilestones = () => { // const milestoneNames = [ // ...activateMilestone.split(','), // ...deactivateMilestone.split(','), // ] // setMilestoneOptions(prev => // prev.map(milestoneOption => ({ // ...milestoneOption, // selected: milestoneNames.includes( // milestoneOption.milestoneState.milestone.name // ), // })) // ) // } // useEffect( // () => setSelectedMilestones(), // [activateMilestone, deactivateMilestone] // ) const milestoneOptionsToString = (reached: boolean) => milestoneOptions .filter(milestoneOption => milestoneOption.selected) .filter(milestoneOption => reached ? milestoneOption.milestoneState.reached : !milestoneOption.milestoneState.reached ) }, [ selectedMilestones, milestones, setActivateMilestone, setDeactivateMilestone, ]) .map(milestoneOption => milestoneOption.milestoneState.milestone.name) .join(' ') const dropdownRef = useRef(null) useEffect(() => { setActivateMilestone(milestoneOptionsToString(false)) setDeactivateMilestone(milestoneOptionsToString(true)) }, [milestoneOptions]) const handleItemChange = ( milestone: MilestoneOption, selected: boolean | undefined milestoneOption: MilestoneOption, selected: boolean ) => { setMilestonesList(prevState => { prevState.forEach(prevMilestone => { if (prevMilestone.milestoneId === milestone.milestoneId) { prevMilestone.selected = selected === undefined ? undefined : !selected } }) milestone.selected = selected return [...prevState] }) const index = milestoneOptions.indexOf(milestoneOption) setSelectedMilestones(prevState => { if (selected !== undefined) { return [...prevState, milestone] } return [ ...prevState.filter( selectedMilestone => selectedMilestone.name !== milestone.name ), ] }) } useEffect(() => { // TODO: i don't what this code is, but I haven't tested what it will do if I remove it // TODO: test if behaviour is not broken, when email is based on a template const activatedNames = activateMilestone.split(',') const deactivatedNames = deactivateMilestone.split(',') selectedMilestones.forEach(milestone => { if (!activatedNames.includes(milestone.name)) { handleItemChange(milestone, undefined) } if (!deactivatedNames.includes(milestone.name)) { handleItemChange(milestone, undefined) } }) const arr = [activatedNames, deactivatedNames] arr.forEach(x => x.forEach(milestoneName => { const milestone = milestones.find(m => m.name === milestoneName) if (milestone && !milestone?.selected) { handleItemChange(milestone, true) } }) ) }, []) const selectionFilter = useCallback( (query: string, milestone: MilestoneOption) => { if (milestone.selected !== undefined) { return false setMilestoneOptions(prev => [ ...prev.slice(0, index), { ...prev[index], selected }, ...prev.slice(index + 1), ]) } return milestone.name.toLowerCase().includes(query.toLowerCase()) }, [] ) const disabled = (milestone: MilestoneOption) => milestone.selected !== undefined return ( <MultiSelect<MilestoneOption> placeholder='Reach milestones...' items={milestones} itemRenderer={MilestoneItemRenderer} onItemSelect={milestone => { handleItemChange(milestone, true) placeholder='Reach milestones' items={milestoneOptions} itemRenderer={MilestoneOptionRenderer} onItemSelect={milestoneOption => { handleItemChange(milestoneOption, true) }} tagInputProps={{ onRemove: (tag, index) => { onRemove: tag => { const name = tag?.valueOf() if (selectedMilestones[index].name === name) { handleItemChange(selectedMilestones[index], undefined) } const index = milestoneOptions.indexOf( milestoneOptions.find( milestoneOption => milestoneOption.milestoneState.milestone.name === name )! ) // TODO: not ideal, searches for the index twice handleItemChange(milestoneOptions[index], false) }, tagProps: { minimal: true, }, }} tagRenderer={milestone => milestone.selected ? milestone.name : `not ${milestone.name}` tagRenderer={getMilestoneOptionText} selectedItems={milestoneOptions.filter( milestoneOption => milestoneOption.selected )} itemPredicate={(query, milestoneOption) => milestoneOption.milestoneState.milestone.name .toLowerCase() .includes(query.toLowerCase()) } selectedItems={selectedMilestones} itemPredicate={selectionFilter} resetOnSelect onClear={() => { setSelectedMilestones([]) setMilestonesList(prevState => { prevState.forEach(milestone => { milestone.selected = undefined }) return [...milestones] }) setMilestoneOptions(prev => prev.map(milestoneOption => ({ ...milestoneOption, selected: false })) ) }} popoverRef={dropdownRef} menuProps={{ 'aria-label': 'milestones', }} Loading @@ -237,8 +169,6 @@ const MilestoneSelector = ({ roleStructure='listoption' /> } openOnKeyDown itemDisabled={disabled} popoverProps={{ minimal: true, }} Loading Loading
frontend/src/email/EmailContactSelector/index.tsx +0 −1 Original line number Diff line number Diff line Loading @@ -74,7 +74,6 @@ const EmailContactSelector: FC<EmailContactSelectorProps> = ({ onItemSelect={onItemSelect} onRemove={onRemove} popoverProps={{ matchTargetWidth: true, minimal: true, }} resetOnSelect Loading
frontend/src/email/EmailForm/InstructorEmailForm.tsx +1 −8 Original line number Diff line number Diff line Loading @@ -56,14 +56,7 @@ const InstructorEmailForm: FC<EmailFormProps> = ({ emailThread, threadId, teamId, submitFunction({ threadId, senderAddress, content, fileName, activateMilestone, deactivateMilestone, }) { submitFunction: (threadId: string) => { instructorMutate({ variables: { threadId, Loading
frontend/src/email/EmailForm/TraineeEmailForm.tsx +4 −5 Original line number Diff line number Diff line Loading @@ -28,7 +28,6 @@ const TraineeEmailForm: FC<EmailFormProps> = ({ setContent, discard, storeDraft, senderAddress, selectedContacts, setSelectedContacts, subject, Loading @@ -44,15 +43,15 @@ const TraineeEmailForm: FC<EmailFormProps> = ({ file, fileName, subject: subject || '', senderAddress, senderAddress: teamAddress!, selectedContacts, threadId, teamId, submitFunction({ threadId, senderAddress, content, fileName }) { submitFunction: threadId => { traineeMutate({ variables: { threadId, senderAddress, senderAddress: teamAddress!, content, fileName, }, Loading Loading @@ -98,7 +97,7 @@ const TraineeEmailForm: FC<EmailFormProps> = ({ subject, setSubject, })} senderAddress={senderAddress} senderAddress={teamAddress!} /> <Divider style={{ margin: '0.5rem 0' }} /> Loading
frontend/src/email/EmailForm/useThreadSubmission.ts +2 −20 Original line number Diff line number Diff line Loading @@ -13,14 +13,7 @@ type ThreadSubmissionProps = { subject: string emailThread?: EmailThread threadId?: string submitFunction: ({ threadId, senderAddress, content, fileName, activateMilestone, deactivateMilestone, }: SendEmailInput) => void submitFunction: (threadId: string) => void } & Omit<SendEmailInput, 'threadId'> const useThreadSubmission = ({ Loading @@ -33,8 +26,6 @@ const useThreadSubmission = ({ selectedContacts, subject, emailThread, activateMilestone, deactivateMilestone, submitFunction, }: ThreadSubmissionProps) => { const [createThread] = useCreateThread() Loading Loading @@ -102,19 +93,10 @@ const useThreadSubmission = ({ }) } return submitFunction({ threadId: threadIdPtr as string, senderAddress, content, fileName, activateMilestone, deactivateMilestone, }) return submitFunction(emailThread?.id || threadIdPtr!) }, [ activateMilestone, content, createThread, deactivateMilestone, emailThread, file, fileName, Loading
frontend/src/instructor/MilestoneSelector/MilestoneSelector.tsx +86 −156 Original line number Diff line number Diff line /* eslint-disable @typescript-eslint/no-unused-vars */ import type { ItemRenderer, ItemRendererProps } from '@blueprintjs/select' import { MultiSelect } from '@blueprintjs/select' import { MenuItem } from '@blueprintjs/core' import type { SetStateAction } from 'react' import type React from 'react' import { memo, useCallback, useEffect, useRef, useState } from 'react' import { memo, useEffect, useState } from 'react' import notEmpty from '@inject/shared/utils/notEmpty' import { useGetTeamMilestones } from '@inject/graphql/queries/GetTeamMilestones.generated' import { MilestoneState } from '@inject/graphql/fragments/MilestoneState.generated' // TODO: refactor this into an object, there is an O(n) search in the code, it can be "O(1)" export interface MilestoneOption { name: string milestoneId: string selected?: boolean not: boolean milestoneState: MilestoneState selected: boolean } const MilestoneItemRenderer: ItemRenderer<MilestoneOption> = ( milestone, const getMilestoneOptionText = (milestoneOption: MilestoneOption) => milestoneOption.milestoneState.reached ? `not ${milestoneOption.milestoneState.milestone.name}` : milestoneOption.milestoneState.milestone.name const MilestoneOptionRenderer: ItemRenderer<MilestoneOption> = ( milestoneOption, { handleClick, handleFocus, modifiers, ref }: ItemRendererProps ) => { if (!modifiers.matchesPredicate) { Loading @@ -24,9 +28,9 @@ const MilestoneItemRenderer: ItemRenderer<MilestoneOption> = ( } return ( <MenuItem key={milestone?.name} text={milestone?.name} selected={milestone?.selected} key={milestoneOption.milestoneState.id} text={getMilestoneOptionText(milestoneOption)} selected={milestoneOption.selected} roleStructure='listoption' shouldDismissPopover={false} onClick={handleClick} Loading @@ -40,8 +44,6 @@ const MilestoneItemRenderer: ItemRenderer<MilestoneOption> = ( interface MilestoneSelectorProps { teamId: string /** Initialization string, contains milestone strings separated with " and " as specified in the docs * when string is changed with setState, selector is initialized again */ activateMilestone: string deactivateMilestone: string setActivateMilestone: React.Dispatch<SetStateAction<string>> Loading @@ -62,171 +64,101 @@ const MilestoneSelector = ({ }, }) const [milestones, setMilestonesList] = useState<MilestoneOption[]>( (data?.teamMilestones || []).filter(notEmpty).flatMap( // TODO remove this condition object creation, if invisible milestones causes problems ({ milestone, reached }) => !reached ? { name: milestone.name || '', milestoneId: milestone.name || '', not: false, } : { name: `not ${milestone.name || ''}`, milestoneId: milestone.name || '', not: true, } ) const [milestoneOptions, setMilestoneOptions] = useState<MilestoneOption[]>( (data?.teamMilestones || []) .filter(notEmpty) .map(milestoneState => ({ milestoneState, selected: false })) ) const [selectedMilestones, setSelectedMilestones] = useState< MilestoneOption[] >(() => { // this is duplicate compared to prev version, it is necessary to prevent state changes, when initializing // instead of using handleItemChange const preselected: MilestoneOption[] = [] const milestoneloop = (set: boolean) => (milestone: string) => { const milestoneOption = milestones.find(m => m.name === milestone) if (milestoneOption) { setMilestonesList(prevState => { milestoneOption.selected = set return [...prevState] }) preselected.push(milestoneOption) } } activateMilestone.split(',').forEach(milestoneloop(true)) deactivateMilestone.split(',').forEach(milestoneloop(true)) return preselected }) useEffect(() => { setActivateMilestone( milestones .filter(x => x.selected) .filter(x => !x.not) .map(milestone => milestone.name) .join(',') ) setDeactivateMilestone( milestones .filter(x => x.selected) .filter(x => x.not) .map(milestone => milestone.name) .map(x => x.slice(4)) .join(',') // TODO: update when loading a template // const setSelectedMilestones = () => { // const milestoneNames = [ // ...activateMilestone.split(','), // ...deactivateMilestone.split(','), // ] // setMilestoneOptions(prev => // prev.map(milestoneOption => ({ // ...milestoneOption, // selected: milestoneNames.includes( // milestoneOption.milestoneState.milestone.name // ), // })) // ) // } // useEffect( // () => setSelectedMilestones(), // [activateMilestone, deactivateMilestone] // ) const milestoneOptionsToString = (reached: boolean) => milestoneOptions .filter(milestoneOption => milestoneOption.selected) .filter(milestoneOption => reached ? milestoneOption.milestoneState.reached : !milestoneOption.milestoneState.reached ) }, [ selectedMilestones, milestones, setActivateMilestone, setDeactivateMilestone, ]) .map(milestoneOption => milestoneOption.milestoneState.milestone.name) .join(' ') const dropdownRef = useRef(null) useEffect(() => { setActivateMilestone(milestoneOptionsToString(false)) setDeactivateMilestone(milestoneOptionsToString(true)) }, [milestoneOptions]) const handleItemChange = ( milestone: MilestoneOption, selected: boolean | undefined milestoneOption: MilestoneOption, selected: boolean ) => { setMilestonesList(prevState => { prevState.forEach(prevMilestone => { if (prevMilestone.milestoneId === milestone.milestoneId) { prevMilestone.selected = selected === undefined ? undefined : !selected } }) milestone.selected = selected return [...prevState] }) const index = milestoneOptions.indexOf(milestoneOption) setSelectedMilestones(prevState => { if (selected !== undefined) { return [...prevState, milestone] } return [ ...prevState.filter( selectedMilestone => selectedMilestone.name !== milestone.name ), ] }) } useEffect(() => { // TODO: i don't what this code is, but I haven't tested what it will do if I remove it // TODO: test if behaviour is not broken, when email is based on a template const activatedNames = activateMilestone.split(',') const deactivatedNames = deactivateMilestone.split(',') selectedMilestones.forEach(milestone => { if (!activatedNames.includes(milestone.name)) { handleItemChange(milestone, undefined) } if (!deactivatedNames.includes(milestone.name)) { handleItemChange(milestone, undefined) } }) const arr = [activatedNames, deactivatedNames] arr.forEach(x => x.forEach(milestoneName => { const milestone = milestones.find(m => m.name === milestoneName) if (milestone && !milestone?.selected) { handleItemChange(milestone, true) } }) ) }, []) const selectionFilter = useCallback( (query: string, milestone: MilestoneOption) => { if (milestone.selected !== undefined) { return false setMilestoneOptions(prev => [ ...prev.slice(0, index), { ...prev[index], selected }, ...prev.slice(index + 1), ]) } return milestone.name.toLowerCase().includes(query.toLowerCase()) }, [] ) const disabled = (milestone: MilestoneOption) => milestone.selected !== undefined return ( <MultiSelect<MilestoneOption> placeholder='Reach milestones...' items={milestones} itemRenderer={MilestoneItemRenderer} onItemSelect={milestone => { handleItemChange(milestone, true) placeholder='Reach milestones' items={milestoneOptions} itemRenderer={MilestoneOptionRenderer} onItemSelect={milestoneOption => { handleItemChange(milestoneOption, true) }} tagInputProps={{ onRemove: (tag, index) => { onRemove: tag => { const name = tag?.valueOf() if (selectedMilestones[index].name === name) { handleItemChange(selectedMilestones[index], undefined) } const index = milestoneOptions.indexOf( milestoneOptions.find( milestoneOption => milestoneOption.milestoneState.milestone.name === name )! ) // TODO: not ideal, searches for the index twice handleItemChange(milestoneOptions[index], false) }, tagProps: { minimal: true, }, }} tagRenderer={milestone => milestone.selected ? milestone.name : `not ${milestone.name}` tagRenderer={getMilestoneOptionText} selectedItems={milestoneOptions.filter( milestoneOption => milestoneOption.selected )} itemPredicate={(query, milestoneOption) => milestoneOption.milestoneState.milestone.name .toLowerCase() .includes(query.toLowerCase()) } selectedItems={selectedMilestones} itemPredicate={selectionFilter} resetOnSelect onClear={() => { setSelectedMilestones([]) setMilestonesList(prevState => { prevState.forEach(milestone => { milestone.selected = undefined }) return [...milestones] }) setMilestoneOptions(prev => prev.map(milestoneOption => ({ ...milestoneOption, selected: false })) ) }} popoverRef={dropdownRef} menuProps={{ 'aria-label': 'milestones', }} Loading @@ -237,8 +169,6 @@ const MilestoneSelector = ({ roleStructure='listoption' /> } openOnKeyDown itemDisabled={disabled} popoverProps={{ minimal: true, }} Loading