fix temporary enrollment modal data reset bug

This fixes a bug where the modal data was not being reset
when the modal “start over” button was clicked and/or modal
was closed. This caused the modal to display to not update
the data when restarting the temporary enrollment process.

closes FOO-3967
flag=temporary_enrollments

test plan:
- enable temporary_enrollments feature flag on root account
- enable permissions: User - Temporary Enrollments
- create three users on account (user1/user2/user3)
- create course add user1 as teacher
- add module/assignment and publish everything
- go to course/people page
- click the temporary enroll icon for user1 (the provider)
- search for user2 (recipient)
- expect user2 to be approved for temporary enrollments
- cancel/close modal and/or start over
- search for user3 (recipient)
- expect user3 to be approved for temporary enrollments
  - prior to this bug being fixed, user2 would show here
    instead of user3; so, expect user2 to not show at
    this step

Change-Id: I428456bffa984e866dc9d558b9f92abef305ae8c
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/330886
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Michael Hulse <michael.hulse@instructure.com>
Product-Review: Michael Hulse <michael.hulse@instructure.com>
Reviewed-by: August Thornton <august@instructure.com>
This commit is contained in:
Michael Hulse 2023-10-20 09:32:41 -07:00
parent 58b6bb2d38
commit 2890a936e7
7 changed files with 78 additions and 49 deletions

View File

@ -22,28 +22,28 @@ import {Spinner} from '@instructure/ui-spinner'
import {Course, Enrollment, Role, Section} from './types'
interface RoleChoice {
id: string
name: string
readonly id: string
readonly name: string
}
interface Props {
enrollmentsByCourse: Course[] | any
roles: Role[] | any
selectedRole: RoleChoice
createEnroll?: Function
readonly enrollmentsByCourse: Course[] | any
readonly roles: Role[] | any
readonly selectedRole: RoleChoice
readonly createEnroll?: Function
}
export interface NodeStructure {
children: NodeStructure[]
enrollId?: string
id: string
readonly children: NodeStructure[]
readonly enrollId?: string
readonly id: string
isCheck: boolean
isMismatch?: boolean
isMixed: boolean
isToggle?: boolean
label: string
parent?: NodeStructure
workState?: string
readonly label: string
readonly parent?: NodeStructure
readonly workState?: string
}
export function EnrollmentTree(props: Props) {

View File

@ -56,7 +56,7 @@ const I18n = useI18nScope('temporary_enrollment')
// initialize analytics props
const analyticProps = createAnalyticPropsGenerator(MODULE_NAME)
interface Role {
interface EnrollmentRole {
readonly id: string
readonly base_role_name: string
}
@ -144,15 +144,15 @@ function getStoredData(): StoredData {
}
}
interface EnrollmentPropsFunctionProps {
contextType: EnrollmentType
enrollment: User
user: User
interface EnrollmentAndUserProps {
readonly enrollmentProps: User
readonly userProps: User
}
interface EnrollmentAndUserProps {
enrollmentProps: User
userProps: User
interface EnrollmentAndUserContextProps {
readonly contextType: EnrollmentType
readonly enrollment: User
readonly user: User
}
/**
@ -166,7 +166,7 @@ interface EnrollmentAndUserProps {
* @returns {Object} Enrollment and user props
*/
export function getEnrollmentAndUserProps(
props: EnrollmentPropsFunctionProps
props: EnrollmentAndUserContextProps
): EnrollmentAndUserProps {
const {contextType, enrollment, user} = props
const enrollmentProps = contextType === RECIPIENT ? user : enrollment
@ -268,7 +268,9 @@ export function TempEnrollAssign(props: Props) {
}
const handleRoleSearchChange = (event: ChangeEvent, selectedOption: {id: string}) => {
const foundRole: Role | undefined = props.roles.find(role => role.id === selectedOption.id)
const foundRole: EnrollmentRole | undefined = props.roles.find(
role => role.id === selectedOption.id
)
const name = foundRole ? removeStringAffix(foundRole.base_role_name, 'Enrollment') : ''
setRoleChoice({

View File

@ -32,7 +32,7 @@ import {TempEnrollSearch} from './TempEnrollSearch'
import {TempEnrollEdit} from './TempEnrollEdit'
import {TempEnrollAssign} from './TempEnrollAssign'
import {Flex} from '@instructure/ui-flex'
import {Enrollment, EnrollmentType, MODULE_NAME, TempEnrollPermissions, User} from './types'
import {Enrollment, EnrollmentType, MODULE_NAME, Role, TempEnrollPermissions, User} from './types'
import {showFlashSuccess} from '@canvas/alerts/react/FlashAlert'
import {createAnalyticPropsGenerator} from './util/analytics'
@ -46,20 +46,20 @@ interface Props {
readonly enrollmentType: EnrollmentType
readonly children: ReactElement
readonly user: {
id: string
name: string
avatar_url?: string
readonly id: string
readonly name: string
readonly avatar_url?: string
}
readonly canReadSIS?: boolean
readonly permissions: {
teacher: boolean
ta: boolean
student: boolean
observer: boolean
designer: boolean
readonly teacher: boolean
readonly ta: boolean
readonly student: boolean
readonly observer: boolean
readonly designer: boolean
}
readonly accountId: string
readonly roles: {id: string; label: string; base_role_name: string}[]
readonly roles: Role[]
readonly onOpen?: () => void
readonly onClose?: () => void
readonly defaultOpen?: boolean
@ -90,8 +90,11 @@ export function TempEnrollModal(props: Props) {
}
}, [props.tempEnrollments])
function resetState(pg: number = 0) {
setPage(pg)
const resetState = () => {
setPage(0)
setEnrollment(null)
setIsViewingAssignFromEdit(false)
setEnrollmentData([])
if (props.isEditMode && props.onToggleEditMode) {
props.onToggleEditMode(false)
@ -154,7 +157,7 @@ export function TempEnrollModal(props: Props) {
const handleResetToBeginning = () => {
resetState()
setPage((p: number) => p - 1)
setPage(0)
}
const handlePageTransition = () => {
@ -172,7 +175,8 @@ export function TempEnrollModal(props: Props) {
const handleGoToAssignPageWithEnrollment = (chosenEnrollment: any) => {
setEnrollment(chosenEnrollment)
resetState(2)
setPage(2)
resetState()
setIsViewingAssignFromEdit(true)
}
@ -231,7 +235,7 @@ export function TempEnrollModal(props: Props) {
page={page}
searchFail={handleSearchFailure}
searchSuccess={handleSetEnrollmentFromSearch}
foundEnroll={enrollment !== null ? enrollment : undefined}
foundEnroll={enrollment}
/>
)
}

View File

@ -43,7 +43,7 @@ interface Props {
readonly searchSuccess: Function
readonly canReadSIS?: boolean
readonly accountId: string
readonly foundEnroll?: User
readonly foundEnroll?: User | null
}
export function TempEnrollSearch(props: Props) {
@ -135,6 +135,7 @@ export function TempEnrollSearch(props: Props) {
useEffect(() => {
if (props.page === 1 && !props.foundEnroll) {
setLoading(true)
const findUser = async () => {
try {
const {json} = await doFetchApi({
@ -142,14 +143,18 @@ export function TempEnrollSearch(props: Props) {
method: 'POST',
params: {user_list: search, v2: true, search_type: searchType},
})
processSearchApiResponse(json)
} catch (error: any) {
setMessage(error.message)
setEnrollment(EMPTY_USER)
props.searchFail()
setLoading(false)
}
}
findUser()
} else if (props.foundEnroll) {
setEnrollment({...props.foundEnroll})

View File

@ -24,36 +24,37 @@ import {NodeStructure} from '../EnrollmentTree'
const checkCallback = jest.fn()
const toggleCallback = jest.fn()
interface TestableNodeStructure extends NodeStructure {
parent?: NodeStructure
}
const emptyNode = {
id: '',
label: '',
// eslint-disable-next-line no-array-constructor
children: new Array<NodeStructure>(),
children: [],
isMixed: false,
isCheck: false,
}
const section2Node: NodeStructure = {
const section2Node: TestableNodeStructure = {
id: 's2',
label: 'Section 2',
// eslint-disable-next-line no-array-constructor
children: new Array<NodeStructure>(),
children: [],
parent: emptyNode,
isCheck: false,
isMixed: false,
}
const section1Node: NodeStructure = {
const section1Node: TestableNodeStructure = {
id: 's1',
label: 'Section 1',
// eslint-disable-next-line no-array-constructor
children: new Array<NodeStructure>(),
children: [],
parent: emptyNode,
isCheck: false,
isMixed: false,
}
const courseNode: NodeStructure = {
const courseNode: TestableNodeStructure = {
id: 'c1',
label: 'Course 1',
children: [section1Node],
@ -63,7 +64,7 @@ const courseNode: NodeStructure = {
isMixed: false,
}
const roleNode: NodeStructure = {
const roleNode: TestableNodeStructure = {
enrollId: '1',
id: 'r1',
label: 'Role 1',

View File

@ -59,6 +59,7 @@ const modalProps = {
{
base_role_name: 'TeacherEnrollment',
id: '234',
role: 'TeacherEnrollment',
label: 'Teacher',
},
],

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {fetchTemporaryEnrollments, deleteEnrollment} from '../../api/enrollment'
import {deleteEnrollment, fetchTemporaryEnrollments} from '../../api/enrollment'
import doFetchApi from '@canvas/do-fetch-api-effect'
// Mock the API call
@ -116,6 +116,22 @@ describe('enrollment api', () => {
expect(mockConsoleError).toHaveBeenCalled()
})
it.skip('handles deletion without onDelete gracefully', async () => {
;(doFetchApi as jest.Mock).mockResolvedValue({response: {status: 200}})
await expect(deleteEnrollment(1, 2)).resolves.not.toThrow()
})
it.skip('handles non-200 status code gracefully', async () => {
;(doFetchApi as jest.Mock).mockResolvedValue({response: {status: 404}})
try {
await deleteEnrollment(1, 2)
} catch (e: any) {
expect(e.message).toBe('Failed to delete enrollment: HTTP status code 404')
}
})
})
})
})