Allow selecting sections and groups in BBB modal

This commit should allow all sections and groups to be selected

closes: VICE-3096
flag=bbb_modal_update

Note
Groups and sections prepopulate the address book when:
- All of their students have been invited
- If they have no users

This matches legacy behavior

The behavior of the section/group/user selection is not intuitive
This commit should match legacy behavior.
Please check legacy if you see behavior you think is broken

Test Plan:
1. Create course with groups and Sections
2. Create new Conference
3. Verify that groups and sections appear
4. invite a group or section
5. Create the conference
6. Edit the newly created conference and open the addressbook
7. Verify that all users in group or section were invited
8. Verify group/section tag populates the addressbook
8a. Verify that the groups and sections selected match legacy

Change-Id: I1881eee5f45b4261c0b8ccf344330a017cc5cf19
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/301675
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Reviewed-by: Jeffrey Johnson <jeffrey.johnson@instructure.com>
Product-Review: Jeffrey Johnson <jeffrey.johnson@instructure.com>
This commit is contained in:
Jason Gillett 2022-09-26 11:41:51 -06:00
parent 03b4c69b3b
commit 12dbc7e04e
7 changed files with 393 additions and 69 deletions

View File

@ -82,6 +82,52 @@ describe "BigBlueButton conferences" do
end
end
context "attendee selection" do
before do
@section = @course.course_sections.create!(name: "test section")
student_in_section(@section, user: @student)
@group_category = @course.group_categories.create!(name: "Group Category")
@group = @course.groups.create!(group_category: @group_category, name: "Group 1")
@group.add_user(@student, "accepted")
end
context "on create" do
it "successfully invites a section to the conference" do
get "/courses/#{@course.id}/conferences"
new_conference_button.click
wait_for_ajaximations
f("div#tab-attendees").click
fj("label:contains('Invite all course members')").click
f("[data-testid='address-input']").click
f("[data-testid='section-#{@section.id}']").click
expect(@section.participants.count).to eq ff("[data-testid='address-tag']").count
wait_for_new_page_load { f("button[data-testid='submit-button']").click }
new_conference = WebConference.last
expect(@section.participants.count).to eq new_conference.users.count
end
it "successfully invites a group to the conference" do
get "/courses/#{@course.id}/conferences"
# Since the teacher isn't a participating user, we have to add 1 to this count
group_participant_and_group_tag_count = @group.participating_users_in_context.count + 1
new_conference_button.click
wait_for_ajaximations
f("div#tab-attendees").click
fj("label:contains('Invite all course members')").click
f("[data-testid='address-input']").click
f("[data-testid='group-#{@group.id}']").click
expect(group_participant_and_group_tag_count).to eq ff("[data-testid='address-tag']").count
wait_for_new_page_load { f("button[data-testid='submit-button']").click }
new_conference = WebConference.last
expect(group_participant_and_group_tag_count).to eq new_conference.users.count
end
end
end
it "validates name length" do
initial_conference_count = WebConference.count
get conferences_index_page

View File

@ -117,14 +117,35 @@ const ConferencesRouter = Backbone.Router.extend({
return {
displayName: name,
id,
type: 'user',
assetCode: `user-${id}`,
}
})
const availableSectionsList = ENV.sections.map(({id, name}) => {
return {
displayName: name,
id,
type: 'section',
assetCode: `section-${id}`,
}
})
const availableGroupsList = ENV.groups.map(({id, name}) => {
return {
displayName: name,
id,
type: 'group',
assetCode: `group-${id}`,
}
})
const menuData = availableAttendeesList.concat(availableSectionsList, availableGroupsList)
ReactDOM.render(
<VideoConferenceModal
open={true}
isEditing={false}
availableAttendeesList={availableAttendeesList}
availableAttendeesList={menuData}
onDismiss={() => {
window.location.hash = ''
ReactDOM.render(<span />, document.getElementById('react-conference-modal-container'))
@ -174,8 +195,14 @@ const ConferencesRouter = Backbone.Router.extend({
payload[`user[${userId}]`] = 1
})
} else {
data.selectedAttendees.forEach(userId => {
payload[`user[${userId}]`] = 1
data.selectedAttendees.forEach(menuItem => {
if (menuItem.type === 'group') {
payload[`group[${menuItem.id}]`] = 1
} else if (menuItem.type === 'section') {
payload[`section[${menuItem.id}]`] = 1
} else {
payload[`user[${menuItem.id}]`] = 1
}
})
}
@ -242,9 +269,30 @@ const ConferencesRouter = Backbone.Router.extend({
return {
displayName: name,
id,
type: 'user',
assetCode: `user-${id}`,
}
})
const availableSectionsList = ENV.sections.map(({id, name}) => {
return {
displayName: name,
id,
type: 'section',
assetCode: `section-${id}`,
}
})
const availableGroupsList = ENV.groups.map(({id, name}) => {
return {
displayName: name,
id,
type: 'group',
assetCode: `group-${id}`,
}
})
const menuData = availableAttendeesList.concat(availableSectionsList, availableGroupsList)
if (attributes.long_running === 1) {
options.push('no_time_limit')
}
@ -285,9 +333,10 @@ const ConferencesRouter = Backbone.Router.extend({
description={attributes.description}
invitationOptions={invitationOptions}
attendeesOptions={attendeesOptions}
availableAttendeesList={availableAttendeesList}
selectedAttendees={attributes.user_ids}
savedAttendees={attributes.user_ids}
availableAttendeesList={menuData}
selectedAttendees={attributes.user_ids.map(u => {
return {assetCode: `user-${u}`, id: u}
})}
startCalendarDate={attributes.start_at}
endCalendarDate={attributes.end_at}
onDismiss={() => {
@ -340,8 +389,14 @@ const ConferencesRouter = Backbone.Router.extend({
payload[`user[${userId}]`] = 1
})
} else {
data.selectedAttendees.forEach(userId => {
payload[`user[${userId}]`] = 1
data.selectedAttendees.forEach(menuItem => {
if (menuItem.type === 'group') {
payload[`group[${menuItem.id}]`] = 1
} else if (menuItem.type === 'section') {
payload[`section[${menuItem.id}]`] = 1
} else {
payload[`user[${menuItem.id}]`] = 1
}
})
}

View File

@ -41,7 +41,7 @@ const BBBModalOptions = ({addToCalendar, setAddToCalendar, ...props}) => {
const [noTimeLimit, setNoTimeLimit] = useState(props.options.includes('no_time_limit')) // match options.no_time_limit default
const contextIsGroup = ENV.context_asset_string?.split('_')[0] === 'group'
const inviteAllMemberstext = contextIsGroup
const inviteAllMembersText = contextIsGroup
? I18n.t('Invite all group members')
: I18n.t('Invite all course members')
@ -249,7 +249,7 @@ const BBBModalOptions = ({addToCalendar, setAddToCalendar, ...props}) => {
</View>
}
>
<Checkbox label={inviteAllMemberstext} value="invite_all" disabled={addToCalendar} />
<Checkbox label={inviteAllMembersText} value="invite_all" disabled={addToCalendar} />
{!contextIsGroup && (
<Checkbox
label={I18n.t('Remove all course observer members')}
@ -263,11 +263,10 @@ const BBBModalOptions = ({addToCalendar, setAddToCalendar, ...props}) => {
<Flex.Item padding="small">
<ConferenceAddressBook
data-testId="conference-address-book"
selectedIds={props.selectedAttendees}
savedAttendees={props.savedAttendees}
selectedItems={props.selectedAttendees}
menuItemList={props.availableAttendeesList}
onChange={userList => {
props.onAttendeesChange(userList.map(u => u.id))
onChange={menuItemList => {
props.onAttendeesChange(menuItemList)
}}
isEditing={props.isEditing}
/>
@ -335,8 +334,7 @@ BBBModalOptions.propTypes = {
showAddressBook: PropTypes.bool,
onAttendeesChange: PropTypes.func,
availableAttendeesList: PropTypes.arrayOf(PropTypes.object),
selectedAttendees: PropTypes.arrayOf(PropTypes.string),
savedAttendees: PropTypes.arrayOf(PropTypes.string),
selectedAttendees: PropTypes.arrayOf(PropTypes.object),
showCalendar: PropTypes.bool,
setAddToCalendar: PropTypes.func,
addToCalendar: PropTypes.bool,

View File

@ -113,11 +113,10 @@ const BaseModalOptions = props => {
<Flex.Item padding="medium">
<ConferenceAddressBook
data-testid="conference-address-book"
selectedIds={props.selectedAttendees}
savedAttendees={props.savedAttendees}
selectedItems={props.selectedAttendees}
menuItemList={props.availableAttendeesList}
onChange={userList => {
props.onAttendeesChange(userList.map(u => u.id))
onChange={menuItemList => {
props.onAttendeesChange(menuItemList)
}}
isEditing={props.isEditing}
/>
@ -154,7 +153,6 @@ BaseModalOptions.propTypes = {
onAttendeesChange: PropTypes.func,
availableAttendeesList: PropTypes.arrayOf(PropTypes.object),
selectedAttendees: PropTypes.arrayOf(PropTypes.string),
savedAttendees: PropTypes.arrayOf(PropTypes.string),
nameValidationMessages: PropTypes.array,
descriptionValidationMessages: PropTypes.array,
hasBegun: PropTypes.bool,

View File

@ -24,27 +24,68 @@ import {Tag} from '@instructure/ui-tag'
const I18n = useI18nScope('video_conference')
export const ConferenceAddressBook = ({
menuItemList,
onChange,
selectedIds,
isEditing,
savedAttendees,
}) => {
export const ConferenceAddressBook = ({menuItemList, onChange, selectedItems, isEditing}) => {
const [isOpen, setIsOpen] = useState(false)
const [highlightMenuItem, setHighlightMenuItem] = useState(null)
const [inputValue, setInputValue] = useState('')
const [selectedMenuItems, setSelectedMenuItems] = useState([])
const [announcement, setAnnouncement] = useState('')
const [savedAttendees, setSavedAttendees] = useState([])
// Initial setup of selectd Ids
useEffect(() => {
const initialSelectedMenuItems = menuItemList.filter(u => selectedIds?.includes(u.id))
if (initialSelectedMenuItems !== selectedMenuItems) {
setSelectedMenuItems(initialSelectedMenuItems)
const groupUserMap = ENV?.group_user_ids_map || {}
const sectionUserMap = ENV?.section_user_ids_map || {}
// Create an array that contains the shared elements between 2 arrays
const intersection = (array1, array2) => {
let tempSwitchVariable
if (array2.length > array1.length) {
tempSwitchVariable = array2
array2 = array1
array1 = tempSwitchVariable
}
return array1.filter(e => {
return array2.indexOf(e) > -1
})
}
// Runs once on startup to set up initially selected items
useEffect(() => {
const selectedUserIDs = selectedItems?.map(u => u.id)
const selectedUserAssetCode = selectedItems?.map(u => u.assetCode)
// This should only get set once. Represents users who are already a part of the conference
const sectionIDs = ENV.sections?.map(u => u.id) || []
const groupIDs = ENV.groups?.map(u => u.id) || []
let selectedSections = []
let selectedGroups = []
// Any section or group that has all of its students selected will be auto selected
// Empty groups or sections will be set to selected automatically
sectionIDs?.forEach(id => {
const sectionUsers = sectionUserMap[id]
const intersectionArray = intersection(sectionUsers, selectedUserIDs)
if (intersectionArray.length === sectionUsers.length) {
selectedSections.push(id)
}
})
groupIDs?.forEach(id => {
const groupUsers = groupUserMap[id]
const intersectionArray = intersection(groupUsers, selectedUserIDs)
if (intersectionArray.length === groupUsers.length) {
selectedGroups.push(id)
}
})
selectedGroups = selectedGroups?.map(u => `group-${u}`)
selectedSections = selectedSections?.map(u => `section-${u}`)
const initialSelectedMenuItems = menuItemList.filter(u =>
selectedGroups.concat(selectedSections, selectedUserAssetCode)?.includes(u.assetCode)
)
setSavedAttendees(initialSelectedMenuItems.map(u => u.assetCode))
setSelectedMenuItems([...selectedMenuItems.concat(initialSelectedMenuItems)])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedIds, menuItemList])
}, [])
const handleBlur = () => {
setHighlightMenuItem(null)
@ -59,7 +100,7 @@ export const ConferenceAddressBook = ({
const handleHighlight = (e, {id}) => {
if (id) {
const menuItem = menuItemList.find(u => u.id === id)
const menuItem = menuItemList.find(u => u.assetCode === id)
setHighlightMenuItem(menuItem)
setAnnouncement(menuItem.displayName)
}
@ -93,24 +134,62 @@ export const ConferenceAddressBook = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputValue, menuItemList, selectedMenuItems.length])
const mapOfFilteredMenuItems = useMemo(() => {
const sectionArray = []
const groupArray = []
const userArray = []
filteredMenuItems.forEach(menuItem => {
if (menuItem.type === 'section') {
sectionArray.push(menuItem)
} else if (menuItem.type === 'group') {
groupArray.push(menuItem)
} else {
userArray.push(menuItem)
}
})
return new Map([
['sections', sectionArray],
['groups', groupArray],
['users', userArray],
])
}, [filteredMenuItems])
const removeSelectedItem = menuItem => {
if (isEditing) {
if (savedAttendees?.includes(menuItem.id))
// Change this to work with asset codes
if (savedAttendees?.includes(menuItem.assetCode)) {
// terminate if menu item has been saved
return
}
}
// Get users from group or section to Remove
let additionalUsersToRemove = []
if (menuItem.type === 'group') {
additionalUsersToRemove = groupUserMap[menuItem.id]?.map(u => `user-${u}`)
} else if (menuItem.type === 'section') {
additionalUsersToRemove = sectionUserMap[menuItem.id]?.map(u => `user-${u}`)
}
const unsavedUsersToRemove = additionalUsersToRemove.filter(x => !savedAttendees?.includes(x))
const menuItemsToRemove = menuItemList.filter(u => unsavedUsersToRemove?.includes(u.assetCode))
menuItemsToRemove.push(menuItem)
const newSelectedMenuItems = selectedMenuItems
const removalIndex = newSelectedMenuItems.indexOf(menuItem)
if (removalIndex > -1) {
newSelectedMenuItems.splice(removalIndex, 1)
}
menuItemsToRemove.forEach(currentMenuItem => {
const removalIndex = newSelectedMenuItems.indexOf(currentMenuItem)
if (removalIndex > -1) {
newSelectedMenuItems.splice(removalIndex, 1)
}
})
setSelectedMenuItems([...newSelectedMenuItems])
onChange([...newSelectedMenuItems])
}
const addSelectedItem = (event, {id}) => {
const menuItem = menuItemList.find(u => u.id === id)
const menuItem = menuItemList.find(u => u.assetCode === id)
// Exit if selected menu item is already selected
if (selectedMenuItems.includes(menuItem)) {
setIsOpen(false)
@ -118,8 +197,25 @@ export const ConferenceAddressBook = ({
return
}
const newSelectedItems = selectedMenuItems
newSelectedItems.push(menuItem)
// Get users from group or section to add
let additionalUsersToAdd = []
if (menuItem.type === 'group') {
additionalUsersToAdd = groupUserMap[menuItem.id] || []
} else if (menuItem.type === 'section') {
additionalUsersToAdd = sectionUserMap[menuItem.id] || []
}
additionalUsersToAdd = additionalUsersToAdd?.map(u => `user-${u}`)
// Remove users that have already been selected so duplicates do not occur
const selectedMenuItemsAssetCode = selectedMenuItems?.map(u => u.assetCode)
const unselectedUsers = additionalUsersToAdd.filter(
x => !selectedMenuItemsAssetCode.includes(x)
)
const additionalUsersToAddMenuItem = menuItemList.filter(u =>
unselectedUsers?.includes(u.assetCode)
)
additionalUsersToAddMenuItem.push(menuItem)
const newSelectedItems = selectedMenuItems.concat(additionalUsersToAddMenuItem)
setAnnouncement(`${menuItem.displayName} selected. List collapsed.`)
setSelectedMenuItems([...newSelectedItems])
onChange([...newSelectedItems])
@ -139,7 +235,7 @@ export const ConferenceAddressBook = ({
return (
<div>
<Select
data-testId="address-input"
data-testid="address-input"
renderLabel={I18n.t('Course Members')}
assistiveText={I18n.t(
'Type or use arrow keys to navigate options. Multiple selections allowed.'
@ -162,13 +258,57 @@ export const ConferenceAddressBook = ({
) : null
}
>
{filteredMenuItems?.map(u => {
return (
<Select.Option id={u.id} key={u.id} isHighlighted={u.id === highlightMenuItem?.id}>
{u.displayName}
</Select.Option>
)
})}
{!!mapOfFilteredMenuItems.get('sections')?.length && (
<Select.Group
key="sections"
renderLabel="Sections"
data-testid="section-conference-header"
>
{mapOfFilteredMenuItems.get('sections')?.map(u => {
return (
<Select.Option
id={u.assetCode}
key={u.assetCode}
isHighlighted={u.assetCode === highlightMenuItem?.assetCode}
data-testid={u.assetCode}
>
{u.displayName}
</Select.Option>
)
})}
</Select.Group>
)}
{!!mapOfFilteredMenuItems.get('groups')?.length && (
<Select.Group key="groups" renderLabel="Groups" data-testid="group-conference-header">
{mapOfFilteredMenuItems.get('groups')?.map(u => {
return (
<Select.Option
id={u.assetCode}
key={u.assetCode}
isHighlighted={u.assetCode === highlightMenuItem?.assetCode}
data-testid={u.assetCode}
>
{u.displayName}
</Select.Option>
)
})}
</Select.Group>
)}
<Select.Group key="users" renderLabel="Users" data-testid="user-conference-header">
{mapOfFilteredMenuItems.get('users')?.map(u => {
return (
<Select.Option
id={u.assetCode}
key={u.assetCode}
isHighlighted={u.assetCode === highlightMenuItem?.assetCode}
data-testid={u.assetCode}
>
{u.displayName}
</Select.Option>
)
})}
</Select.Group>
</Select>
<Alert
liveRegion={() => document.getElementById('flash-messages')}
@ -183,10 +323,9 @@ export const ConferenceAddressBook = ({
ConferenceAddressBook.propTypes = {
menuItemList: PropTypes.array,
selectedIds: PropTypes.arrayOf(PropTypes.string),
savedAttendees: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
isEditing: PropTypes.bool,
selectedItems: PropTypes.arrayOf(PropTypes.object),
}
ConferenceAddressBook.defaultProps = {
@ -195,9 +334,9 @@ ConferenceAddressBook.defaultProps = {
const ConferenceAddressBookTags = ({selectedMenuItems, onDismiss}) => {
return selectedMenuItems.map((menuItem, index) => (
<Tag
data-testId="address-tag"
data-testid="address-tag"
dismissable={true}
key={menuItem.id}
key={menuItem.assetCode}
title={`Remove ${menuItem.displayName}`}
text={menuItem.displayName}
margin={index > 0 ? 'xxx-small 0 xxx-small xx-small' : 'xxx-small 0'}

View File

@ -21,14 +21,23 @@ import {fireEvent, render} from '@testing-library/react'
import {ConferenceAddressBook} from '../ConferenceAddressBook'
describe('ConferenceAddressBook', () => {
afterEach(() => {
window.ENV.sections = []
window.ENV.section_user_ids_map = {}
window.ENV.groups = []
window.ENV.group_user_ids_map = {}
})
const menuItemList = [
{displayName: 'Allison', id: '7'},
{displayName: 'Caleb', id: '3'},
{displayName: 'Chawn', id: '2'},
{displayName: 'Allison', id: '7', type: 'user', assetCode: 'user-7'},
{displayName: 'Caleb', id: '3', type: 'user', assetCode: 'user-3'},
{displayName: 'Chawn', id: '2', type: 'user', assetCode: 'user-2'},
{displayName: 'Group1', id: '23', type: 'group', assetCode: 'group-23'},
{displayName: 'Section1', id: '24', type: 'section', assetCode: 'section-24'},
]
const setup = (props = {}) => {
return render(<ConferenceAddressBook menuItemList={menuItemList} {...props} />)
const setup = (props = {}, menuList = menuItemList) => {
return render(<ConferenceAddressBook menuItemList={menuList} {...props} />)
}
it('should render', () => {
@ -62,12 +71,94 @@ describe('ConferenceAddressBook', () => {
expect(tag).toBeTruthy()
})
it('should render initial tags when selectedIds is passed', () => {
const container = setup({selectedIds: ['2']})
it('should add tag when group is selected', () => {
const container = setup()
const input = container.getByTestId('address-input')
input.click()
const item = container.getByText('Group1')
item.click()
const tag = container.getByTestId('address-tag')
expect(tag).toBeTruthy()
})
it('should add tag when section is selected', () => {
const container = setup()
const input = container.getByTestId('address-input')
input.click()
const item = container.getByText('Section1')
item.click()
const tag = container.getByTestId('address-tag')
expect(tag).toBeTruthy()
})
it('should have section header when section is present', () => {
const container = setup()
const input = container.getByTestId('address-input')
input.click()
const item = container.getByTestId('section-conference-header')
expect(item).toBeTruthy()
})
it('should have group header when group is present', () => {
const container = setup()
const input = container.getByTestId('address-input')
input.click()
const item = container.getByTestId('group-conference-header')
expect(item).toBeTruthy()
})
it('should not have group header when no groups exist', () => {
const menuItemList = [
{displayName: 'Allison', id: '7', type: 'user', assetCode: 'user-7'},
{displayName: 'Caleb', id: '3', type: 'user', assetCode: 'user-3'},
{displayName: 'Chawn', id: '2', type: 'user', assetCode: 'user-2'},
{displayName: 'Section1', id: '24', type: 'section', assetCode: 'section-24'},
]
const container = setup({}, menuItemList)
const input = container.getByTestId('address-input')
input.click()
const item = container.queryByTestId('group-conference-header')
expect(item).toBeFalsy()
})
it('should have User header', () => {
const container = setup()
const input = container.getByTestId('address-input')
input.click()
const item = container.getByTestId('user-conference-header')
expect(item).toBeTruthy()
})
it('should render initial tags when selectedIds is passed', () => {
const container = setup({
selectedItems: [{displayName: 'Chawn', id: '2', type: 'user', assetCode: 'user-2'}],
})
const tag = container.getByTestId('address-tag')
expect(tag).toBeTruthy()
})
it('should initially render groups that have all users selected', () => {
window.ENV.groups = [{displayName: 'Group1', id: '23'}]
window.ENV.group_user_ids_map = {23: ['2']}
const container = setup({
selectedItems: [{displayName: 'Chawn', id: '2', type: 'user', assetCode: 'user-2'}],
})
const tag = container.getAllByTestId('address-tag')
expect(tag).toBeTruthy()
expect(tag.length).toBe(2)
})
it('should initially render sections that have all users selected', () => {
window.ENV.sections = [{displayName: 'Section1', id: '24'}]
window.ENV.section_user_ids_map = {24: ['2']}
const container = setup({
selectedItems: [{displayName: 'Chawn', id: '2', type: 'user', assetCode: 'user-2'}],
})
const tag = container.getAllByTestId('address-tag')
expect(tag).toBeTruthy()
expect(tag.length).toBe(2)
})
it('should remove selected user when backspace is pressed and input is empty', () => {
const container = setup()
const input = container.getByTestId('address-input')
@ -80,7 +171,7 @@ describe('ConferenceAddressBook', () => {
})
it('should not remove saved users when isEditing', () => {
const container = setup({isEditing: true, savedAttendees: ['7']})
const container = setup({isEditing: true, selectedItems: [menuItemList[0]]})
const input = container.getByTestId('address-input')
input.click()
const item = container.getByText('Allison')
@ -93,7 +184,7 @@ describe('ConferenceAddressBook', () => {
})
it('should remove unsaved users when isEditing', () => {
const container = setup({isEditing: true, savedAttendees: ['25']})
const container = setup({isEditing: true, selectedItems: []})
const input = container.getByTestId('address-input')
input.click()
const item = container.getByText('Allison')

View File

@ -189,7 +189,6 @@ export const VideoConferenceModal = ({
onAttendeesChange={setSelectedAttendees}
availableAttendeesList={availableAttendeesList}
selectedAttendees={selectedAttendees}
savedAttendees={props.savedAttendees}
showCalendar={showCalendarOptions}
setAddToCalendar={setAddToCalendar}
addToCalendar={addToCalendar}
@ -225,7 +224,6 @@ export const VideoConferenceModal = ({
onAttendeesChange={setSelectedAttendees}
availableAttendeesList={availableAttendeesList}
selectedAttendees={selectedAttendees}
savedAttendees={props.savedAttendees}
nameValidationMessages={nameValidationMessages}
descriptionValidationMessages={descriptionValidationMessages}
hasBegun={props.hasBegun}
@ -337,8 +335,7 @@ VideoConferenceModal.propTypes = {
attendeesOptions: PropTypes.arrayOf(PropTypes.string),
type: PropTypes.string,
availableAttendeesList: PropTypes.arrayOf(PropTypes.object),
selectedAttendees: PropTypes.arrayOf(PropTypes.string),
savedAttendees: PropTypes.arrayOf(PropTypes.string),
selectedAttendees: PropTypes.arrayOf(PropTypes.object),
startCalendarDate: PropTypes.string,
endCalendarDate: PropTypes.string,
}