Support fetching sections and students
Fetch sections and students and display them as options when choosing Custom access. closes LF-641, LF-642 flag= differentiated_modules Test plan - In a course with several sections and students, go to the modules page. - Open the module menu. - Click on the “Assign to” option. - Click on the “Custom Access” option. - Click on the “Assign to” multi-select. - Expect to see course sections and students as available options. - Try to filter options that are not listed. - Expect the results to be searched in both endpoints (sections and students) - Select some options. - Expect multiple selection to be allowed without duplicate values. - Click on “Clear All”. - Expect the selection to reset. Change-Id: I285d2b9159878544764da783aed803c633122328 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/327852 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Product-Review: Jonathan Guardado <jonathan.guardado@instructure.com> Reviewed-by: Sarah Gerard <sarah.gerard@instructure.com> QA-Review: Sarah Gerard <sarah.gerard@instructure.com> QA-Review: Robin Kuss <rkuss@instructure.com>
This commit is contained in:
parent
d57e3d3101
commit
fe82042d5e
|
@ -2531,6 +2531,7 @@ $(document).ready(function () {
|
|||
initialTab='assign-to'
|
||||
assignOnly={false}
|
||||
moduleElement={moduleElement}
|
||||
courseId={ENV.COURSE_ID}
|
||||
{...settingsProps}
|
||||
/>,
|
||||
document.getElementById('differentiated-modules-mount-point')
|
||||
|
|
|
@ -32,6 +32,7 @@ const I18n = useI18nScope('differentiated_modules')
|
|||
const {Item: FlexItem} = Flex as any
|
||||
|
||||
export interface AssignToPanelProps {
|
||||
courseId: string
|
||||
height: string
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ const OPTIONS: Option[] = [
|
|||
},
|
||||
]
|
||||
|
||||
export default function AssignToPanel({height, onDismiss}: AssignToPanelProps) {
|
||||
export default function AssignToPanel({courseId, height, onDismiss}: AssignToPanelProps) {
|
||||
const [selectedOption, setSelectedOption] = useState<string>(OPTIONS[0].value)
|
||||
|
||||
return (
|
||||
|
@ -92,7 +93,7 @@ export default function AssignToPanel({height, onDismiss}: AssignToPanelProps) {
|
|||
</View>
|
||||
{option.value === OPTIONS[1].value && selectedOption === OPTIONS[1].value && (
|
||||
<View as="div" margin="small large none none">
|
||||
<AssigneeSelector />
|
||||
<AssigneeSelector courseId={courseId} />
|
||||
</View>
|
||||
)}
|
||||
</FlexItem>
|
||||
|
|
|
@ -17,37 +17,100 @@
|
|||
*/
|
||||
|
||||
import CanvasMultiSelect from '@canvas/multi-select/react'
|
||||
import React, {ReactElement, useState} from 'react'
|
||||
import React, {ReactElement, useEffect, useState} from 'react'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import {Link} from '@instructure/ui-link'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import doFetchApi from '@canvas/do-fetch-api-effect'
|
||||
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||
import {debounce, uniqBy} from 'lodash'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
|
||||
const {Option: CanvasMultiSelectOption} = CanvasMultiSelect as any
|
||||
|
||||
const I18n = useI18nScope('differentiated_modules')
|
||||
|
||||
interface Props {
|
||||
courseId: string
|
||||
}
|
||||
|
||||
interface Option {
|
||||
id: string
|
||||
value: string
|
||||
group: string
|
||||
}
|
||||
export const OPTIONS = [
|
||||
{id: '1', value: 'Section A'},
|
||||
{id: '2', value: 'Section B'},
|
||||
{id: '3', value: 'Section C'},
|
||||
{id: '4', value: 'Section D'},
|
||||
{id: '5', value: 'Section E'},
|
||||
{id: '6', value: 'Section F'},
|
||||
]
|
||||
|
||||
const AssigneeSelector = () => {
|
||||
const AssigneeSelector = ({courseId}: Props) => {
|
||||
const [selectedAssignees, setSelectedAssignees] = useState<Option[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [options, setOptions] = useState<Option[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleChange = (newSelected: string[]) => {
|
||||
const newSelectedSet = new Set(newSelected)
|
||||
const selected = OPTIONS.filter(option => newSelectedSet.has(option.id))
|
||||
const selected = options.filter(option => newSelectedSet.has(option.id))
|
||||
setSelectedAssignees(selected)
|
||||
}
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
debounce(() => setSearchTerm(value), 300)()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const params: Record<string, string> = {}
|
||||
const shouldSearchTerm = searchTerm.length > 2
|
||||
if (shouldSearchTerm || searchTerm === '') {
|
||||
setIsLoading(true)
|
||||
if (shouldSearchTerm) {
|
||||
params.search_term = searchTerm
|
||||
}
|
||||
const fetchSections = doFetchApi({
|
||||
path: `/api/v1/courses/${courseId}/sections`,
|
||||
params,
|
||||
})
|
||||
const fetchStudents = doFetchApi({
|
||||
path: `/api/v1/courses/${courseId}/users`,
|
||||
params: {...params, enrollment_type: 'student'},
|
||||
})
|
||||
|
||||
Promise.allSettled([fetchSections, fetchStudents])
|
||||
.then(results => {
|
||||
const sectionsResult = results[0]
|
||||
const studentsResult = results[1]
|
||||
let sectionsParsedResult = []
|
||||
let studentsParsedResult = []
|
||||
if (sectionsResult.status === 'fulfilled') {
|
||||
sectionsParsedResult = sectionsResult.value.json.map(({id, name}: any) => ({
|
||||
id: `section-${id}`,
|
||||
value: name,
|
||||
group: I18n.t('Sections'),
|
||||
}))
|
||||
} else {
|
||||
showFlashError(I18n.t('Failed to load sections data'))(sectionsResult.reason)
|
||||
}
|
||||
|
||||
if (studentsResult.status === 'fulfilled') {
|
||||
studentsParsedResult = studentsResult.value.json.map(({id, name}: any) => ({
|
||||
id: `student-${id}`,
|
||||
value: name,
|
||||
group: I18n.t('Students'),
|
||||
}))
|
||||
} else {
|
||||
showFlashError(I18n.t('Failed to load students data'))(studentsResult.reason)
|
||||
}
|
||||
|
||||
const newOptions = uniqBy(
|
||||
[...options, ...sectionsParsedResult, ...studentsParsedResult],
|
||||
'id'
|
||||
)
|
||||
setOptions(newOptions)
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch(e => showFlashError(I18n.t('Something went wrong while fetching data'))(e))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [courseId, searchTerm])
|
||||
|
||||
return (
|
||||
<>
|
||||
<CanvasMultiSelect
|
||||
|
@ -57,6 +120,9 @@ const AssigneeSelector = () => {
|
|||
selectedOptionIds={selectedAssignees.map(val => val.id)}
|
||||
onChange={handleChange}
|
||||
renderAfterInput={<></>}
|
||||
customOnInputChange={handleInputChange}
|
||||
visibleOptionsCount={14}
|
||||
isLoading={isLoading}
|
||||
customRenderBeforeInput={tags =>
|
||||
tags?.map((tag: ReactElement) => (
|
||||
<View
|
||||
|
@ -71,10 +137,15 @@ const AssigneeSelector = () => {
|
|||
))
|
||||
}
|
||||
>
|
||||
{OPTIONS.map(role => {
|
||||
{options.map(option => {
|
||||
return (
|
||||
<CanvasMultiSelectOption id={role.id} value={role.id} key={role.id}>
|
||||
{role.value}
|
||||
<CanvasMultiSelectOption
|
||||
id={option.id}
|
||||
value={option.id}
|
||||
key={option.id}
|
||||
group={option.group}
|
||||
>
|
||||
{option.value}
|
||||
</CanvasMultiSelectOption>
|
||||
)
|
||||
})}
|
||||
|
@ -85,7 +156,8 @@ const AssigneeSelector = () => {
|
|||
onClick={() => setSelectedAssignees([])}
|
||||
isWithinText={false}
|
||||
>
|
||||
{I18n.t('Clear All')}
|
||||
<span aria-hidden={true}>{I18n.t('Clear All')}</span>
|
||||
<ScreenReaderContent>{I18n.t('Clear Assign To')}</ScreenReaderContent>
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
|
|
|
@ -42,6 +42,7 @@ export interface DifferentiatedModulesTrayProps {
|
|||
unlockAt?: string
|
||||
prerequisites?: Module[]
|
||||
moduleList?: Module[]
|
||||
courseId: string
|
||||
}
|
||||
|
||||
const SettingsPanel = React.lazy(() => import('./SettingsPanel'))
|
||||
|
@ -54,6 +55,7 @@ export default function DifferentiatedModulesTray({
|
|||
moduleId = '',
|
||||
initialTab = 'assign-to',
|
||||
assignOnly = true,
|
||||
courseId,
|
||||
...settingsProps
|
||||
}: DifferentiatedModulesTrayProps) {
|
||||
const [selectedTab, setSelectedTab] = useState<string | undefined>(initialTab)
|
||||
|
@ -95,7 +97,7 @@ export default function DifferentiatedModulesTray({
|
|||
return (
|
||||
<React.Suspense fallback={<Fallback />}>
|
||||
{assignOnly ? (
|
||||
<AssignToPanel height={panelHeight} onDismiss={onDismiss} />
|
||||
<AssignToPanel courseId={courseId} height={panelHeight} onDismiss={onDismiss} />
|
||||
) : (
|
||||
<Tabs onRequestTabChange={(_e: any, {id}: {id?: string}) => setSelectedTab(id)}>
|
||||
<Tabs.Panel
|
||||
|
@ -120,7 +122,7 @@ export default function DifferentiatedModulesTray({
|
|||
isSelected={selectedTab === 'assign-to'}
|
||||
padding="none"
|
||||
>
|
||||
<AssignToPanel height={panelHeight} onDismiss={onDismiss} />
|
||||
<AssignToPanel courseId={courseId} height={panelHeight} onDismiss={onDismiss} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
)}
|
||||
|
|
|
@ -22,6 +22,7 @@ import AssignToPanel, {AssignToPanelProps} from '../AssignToPanel'
|
|||
|
||||
describe('AssignToPanel', () => {
|
||||
const props: AssignToPanelProps = {
|
||||
courseId: '1',
|
||||
height: '500px',
|
||||
onDismiss: () => {},
|
||||
}
|
||||
|
|
|
@ -17,8 +17,19 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {act, render} from '@testing-library/react'
|
||||
import AssigneeSelector, {OPTIONS} from '../AssigneeSelector'
|
||||
import {act, fireEvent, render} from '@testing-library/react'
|
||||
import AssigneeSelector from '../AssigneeSelector'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {FILTERED_SECTIONS_DATA, FILTERED_STUDENTS_DATA, SECTIONS_DATA, STUDENTS_DATA} from './mocks'
|
||||
|
||||
const props = {
|
||||
courseId: '1',
|
||||
}
|
||||
|
||||
const SECTIONS_URL = `/api/v1/courses/${props.courseId}/sections`
|
||||
const STUDENTS_URL = `api/v1/courses/${props.courseId}/users?enrollment_type=student`
|
||||
const FILTERED_SECTIONS_URL = `/api/v1/courses/${props.courseId}/sections?search_term=sec`
|
||||
const FILTERED_STUDENTS_URL = `api/v1/courses/${props.courseId}/users?search_term=sec&enrollment_type=student`
|
||||
|
||||
describe('AssigneeSelector', () => {
|
||||
beforeAll(() => {
|
||||
|
@ -30,26 +41,60 @@ describe('AssigneeSelector', () => {
|
|||
}
|
||||
})
|
||||
|
||||
const renderComponent = () => render(<AssigneeSelector />)
|
||||
|
||||
it('renders', () => {
|
||||
const {getByTestId} = renderComponent()
|
||||
expect(getByTestId('assignee_selector')).toBeInTheDocument()
|
||||
beforeEach(() => {
|
||||
fetchMock.getOnce(SECTIONS_URL, SECTIONS_DATA)
|
||||
fetchMock.getOnce(STUDENTS_URL, STUDENTS_DATA)
|
||||
fetchMock.getOnce(FILTERED_SECTIONS_URL, FILTERED_SECTIONS_DATA)
|
||||
fetchMock.getOnce(FILTERED_STUDENTS_URL, FILTERED_STUDENTS_DATA)
|
||||
})
|
||||
afterEach(() => {
|
||||
fetchMock.restore()
|
||||
})
|
||||
|
||||
it('selects multiple options', () => {
|
||||
const {getByTestId, getByText, getAllByTestId} = renderComponent()
|
||||
const renderComponent = () => render(<AssigneeSelector {...props} />)
|
||||
|
||||
it('displays sections and students as options', async () => {
|
||||
const {getByTestId, findByText, getByText} = renderComponent()
|
||||
act(() => getByTestId('assignee_selector').click())
|
||||
act(() => getByText(OPTIONS[0].value).click())
|
||||
await findByText(SECTIONS_DATA[0].name)
|
||||
SECTIONS_DATA.forEach(section => {
|
||||
expect(getByText(section.name)).toBeInTheDocument()
|
||||
})
|
||||
STUDENTS_DATA.forEach(student => {
|
||||
expect(getByText(student.name)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('fetches filtered results from both APIs', async () => {
|
||||
const {getByTestId, findByText, getByText} = renderComponent()
|
||||
const assigneeSelector = getByTestId('assignee_selector')
|
||||
act(() => assigneeSelector.click())
|
||||
fireEvent.change(assigneeSelector, {target: {value: 'sec'}})
|
||||
await findByText(FILTERED_SECTIONS_DATA[0].name)
|
||||
FILTERED_SECTIONS_DATA.forEach(section => {
|
||||
expect(getByText(section.name)).toBeInTheDocument()
|
||||
})
|
||||
FILTERED_STUDENTS_DATA.forEach(student => {
|
||||
expect(getByText(student.name)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('selects multiple options', async () => {
|
||||
const {getByTestId, findByText, getAllByTestId} = renderComponent()
|
||||
act(() => getByTestId('assignee_selector').click())
|
||||
act(() => getByText(OPTIONS[2].value).click())
|
||||
const option1 = await findByText(SECTIONS_DATA[0].name)
|
||||
act(() => option1.click())
|
||||
act(() => getByTestId('assignee_selector').click())
|
||||
const option2 = await findByText(SECTIONS_DATA[2].name)
|
||||
act(() => option2.click())
|
||||
expect(getAllByTestId('assignee_selector_option').length).toBe(2)
|
||||
})
|
||||
|
||||
it('clears selection', () => {
|
||||
const {getByTestId, queryAllByTestId, getByText} = renderComponent()
|
||||
it('clears selection', async () => {
|
||||
const {getByTestId, queryAllByTestId, findByText} = renderComponent()
|
||||
act(() => getByTestId('assignee_selector').click())
|
||||
act(() => getByText(OPTIONS[0].value).click())
|
||||
const option = await findByText(STUDENTS_DATA[0].name)
|
||||
act(() => option.click())
|
||||
expect(queryAllByTestId('assignee_selector_option').length).toBe(1)
|
||||
act(() => getByTestId('clear_selection_button').click())
|
||||
expect(queryAllByTestId('assignee_selector_option').length).toBe(0)
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('DifferentiatedModulesTray', () => {
|
|||
moduleElement: document.createElement('div'),
|
||||
initialTab: 'assign-to',
|
||||
assignOnly: true,
|
||||
courseId: '1',
|
||||
}
|
||||
|
||||
const renderComponent = (overrides = {}) =>
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2023 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const SECTIONS_DATA = [
|
||||
{id: '1', course_id: '1', name: 'Course 1', start_at: null, end_at: null},
|
||||
{id: '2', course_id: '1', name: 'Section A', start_at: null, end_at: null},
|
||||
{id: '3', course_id: '1', name: 'Section B', start_at: null, end_at: null},
|
||||
{id: '4', course_id: '1', name: 'Section C', start_at: null, end_at: null},
|
||||
]
|
||||
|
||||
export const FILTERED_SECTIONS_DATA = [
|
||||
{id: '2', course_id: '1', name: 'Section A', start_at: null, end_at: null},
|
||||
{id: '3', course_id: '1', name: 'Section B', start_at: null, end_at: null},
|
||||
{id: '4', course_id: '1', name: 'Section C', start_at: null, end_at: null},
|
||||
]
|
||||
|
||||
export const STUDENTS_DATA = [
|
||||
{id: '1', name: 'Ben', created_at: '2023-01-01', sortable_name: 'Ben'},
|
||||
{id: '2', name: 'Peter', created_at: '2023-01-01', sortable_name: 'Peter'},
|
||||
{id: '3', name: 'Grace', created_at: '2023-01-01', sortable_name: 'Grace'},
|
||||
{id: '4', name: 'Secilia', created_at: '2023-01-01', sortable_name: 'Secilia'},
|
||||
]
|
||||
|
||||
export const FILTERED_STUDENTS_DATA = [
|
||||
{id: '4', name: 'Secilia', created_at: '2023-01-01', sortable_name: 'Secilia'},
|
||||
]
|
|
@ -31,7 +31,7 @@ describe('CanvasMultiSelect', () => {
|
|||
return renderFn(
|
||||
<CanvasMultiSelect {...props}>
|
||||
{options.map(o => (
|
||||
<CanvasMultiSelect.Option id={o.id} key={o.id} value={o.id}>
|
||||
<CanvasMultiSelect.Option id={o.id} key={o.id} value={o.id} group={o.group}>
|
||||
{o.text}
|
||||
</CanvasMultiSelect.Option>
|
||||
))}
|
||||
|
@ -71,6 +71,31 @@ describe('CanvasMultiSelect', () => {
|
|||
expect(getByRole('option', {name: 'Broccoli'})).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('categorizes by groups if set in the options', () => {
|
||||
options = [
|
||||
{id: '1', text: 'Cucumber', group: 'Vegetable'},
|
||||
{id: '2', text: 'Broccoli', group: 'Vegetable'},
|
||||
{id: '3', text: 'Apple', group: 'Fruit'},
|
||||
]
|
||||
const {getByRole} = renderComponent()
|
||||
|
||||
const combobox = getByRole('combobox', {name: 'Vegetables'})
|
||||
fireEvent.click(combobox)
|
||||
expect(getByRole('group', {name: 'Vegetable'})).toBeInTheDocument()
|
||||
expect(getByRole('group', {name: 'Fruit'})).toBeInTheDocument()
|
||||
expect(getByRole('option', {name: 'Apple'})).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not categorize by groups if they are not set in the options', () => {
|
||||
const {queryByRole} = renderComponent()
|
||||
|
||||
const combobox = queryByRole('combobox', {name: 'Vegetables'})
|
||||
fireEvent.click(combobox)
|
||||
expect(queryByRole('group', {name: 'Vegetable'})).not.toBeInTheDocument()
|
||||
expect(queryByRole('group', {name: 'Fruit'})).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
it('filters available options when text is input', () => {
|
||||
const {getByRole, queryByRole} = renderComponent()
|
||||
const combobox = getByRole('combobox', {name: 'Vegetables'})
|
||||
|
|
|
@ -16,15 +16,16 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useState, useRef, useMemo} from 'react'
|
||||
import React, {useState, useEffect, useRef, useMemo} from 'react'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import keycode from 'keycode'
|
||||
import {Select} from '@instructure/ui-select'
|
||||
import {Tag} from '@instructure/ui-tag'
|
||||
import {func, string, node, arrayOf, oneOfType, bool} from 'prop-types'
|
||||
import {func, string, node, arrayOf, oneOfType, bool, number} from 'prop-types'
|
||||
import {matchComponentTypes} from '@instructure/ui-react-utils'
|
||||
import {compact, uniqueId} from 'lodash'
|
||||
import {Alert} from '@instructure/ui-alerts'
|
||||
import {Spinner} from '@instructure/ui-spinner'
|
||||
|
||||
const I18n = useI18nScope('app_shared_components')
|
||||
|
||||
|
@ -33,6 +34,7 @@ const CanvasMultiSelectOption = () => <div />
|
|||
CanvasMultiSelectOption.propTypes = {
|
||||
id: string.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
value: string.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
group: string, // eslint-disable-line react/no-unused-prop-types
|
||||
}
|
||||
|
||||
function alwaysArray(scalarOrArray) {
|
||||
|
@ -57,6 +59,8 @@ function CanvasMultiSelect(props) {
|
|||
disabled,
|
||||
customRenderBeforeInput,
|
||||
customMatcher,
|
||||
customOnInputChange,
|
||||
isLoading,
|
||||
...otherProps
|
||||
} = props
|
||||
|
||||
|
@ -84,6 +88,8 @@ function CanvasMultiSelect(props) {
|
|||
}
|
||||
|
||||
function renderChildren() {
|
||||
const groups = [...new Set(children.map(child => child.props.group))].filter(group => group);
|
||||
|
||||
function renderOption(child) {
|
||||
const {id, children, ...optionProps} = child.props
|
||||
return (
|
||||
|
@ -101,7 +107,8 @@ function CanvasMultiSelect(props) {
|
|||
function renderNoOptionsOption() {
|
||||
return (
|
||||
<Select.Option id={noOptionId.current} isHighlighted={false} isSelected={false}>
|
||||
{noOptionsLabel}
|
||||
{ isLoading ? <Spinner renderTitle="Loading" size="x-small" /> :
|
||||
noOptionsLabel}
|
||||
</Select.Option>
|
||||
)
|
||||
}
|
||||
|
@ -119,7 +126,16 @@ function CanvasMultiSelect(props) {
|
|||
})
|
||||
)
|
||||
|
||||
return filteredChildren.length === 0 ? renderNoOptionsOption() : filteredChildren
|
||||
function renderGroups() {
|
||||
const groupsToRender = groups.filter(group => filteredChildren.some(child => child.props.group === group))
|
||||
return groupsToRender.map((group) => <Select.Group key={group} renderLabel={group}>
|
||||
{filteredChildren.filter(({props}) => props.group === group).map(option => renderOption(option))}
|
||||
</Select.Group>)
|
||||
}
|
||||
|
||||
if(filteredChildren.length === 0) return renderNoOptionsOption();
|
||||
|
||||
return groups.length === 0 ? filteredChildren : renderGroups()
|
||||
}
|
||||
|
||||
function dismissTag(e, id) {
|
||||
|
@ -156,8 +172,20 @@ function CanvasMultiSelect(props) {
|
|||
return customRenderBeforeInput ? customRenderBeforeInput(tags) : tags
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(inputValue !== ''){
|
||||
filterOptions(inputValue)
|
||||
}
|
||||
}, [childProps])
|
||||
|
||||
function onInputChange(e) {
|
||||
const {value} = e.target
|
||||
filterOptions(value)
|
||||
setInputValue(value)
|
||||
customOnInputChange(value)
|
||||
}
|
||||
|
||||
function filterOptions(value) {
|
||||
const defaultMatcher = (option, term) => option.label.match(new RegExp(`^${term}`, 'i'))
|
||||
const matcher = customMatcher || defaultMatcher
|
||||
const filtered = childProps.filter(child => matcher(child, value.trim()))
|
||||
|
@ -175,7 +203,6 @@ function CanvasMultiSelect(props) {
|
|||
if (message && filtered.length > 0 && highlightedOptionId !== filtered[0].id) {
|
||||
message = getChildById(filtered[0].id).label + '. ' + message
|
||||
}
|
||||
setInputValue(value)
|
||||
setFilteredOptionIds(filtered.map(f => f.id))
|
||||
if (filtered.length > 0) setHighlightedOptionId(filtered[0].id)
|
||||
setIsShowingOptions(true)
|
||||
|
@ -282,6 +309,9 @@ CanvasMultiSelect.propTypes = {
|
|||
selectedOptionIds: arrayOf(string).isRequired,
|
||||
noOptionsLabel: string,
|
||||
size: string,
|
||||
customOnInputChange: func,
|
||||
visibleOptionsCount: number,
|
||||
isLoading: bool,
|
||||
}
|
||||
|
||||
CanvasMultiSelect.defaultProps = {
|
||||
|
@ -290,6 +320,8 @@ CanvasMultiSelect.defaultProps = {
|
|||
noOptionsLabel: '---',
|
||||
selectedOptionIds: [],
|
||||
disabled: false,
|
||||
isLoading: false,
|
||||
customOnInputChange: () => {}
|
||||
}
|
||||
|
||||
CanvasMultiSelect.Option = CanvasMultiSelectOption
|
||||
|
|
Loading…
Reference in New Issue