Skip to content

Commit 1af4c2e

Browse files
committed
Merge branch 'dropdown-with-variablesizelist' of Arnei/opencast-admin-interface into r/17.x
Pull request #1343 Make DropDown more performant for many options
2 parents 55193d8 + 5249565 commit 1af4c2e

8 files changed

Lines changed: 124 additions & 13 deletions

File tree

package-lock.json

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"react-router": "^7.6.0",
3737
"react-select": "^5.10.1",
3838
"react-textarea-autosize": "^8.5.9",
39+
"react-window": "^1.8.11",
3940
"redux": "^5.0.1",
4041
"redux-persist": "^6.0.0",
4142
"redux-thunk": "^3.1.0",
@@ -66,6 +67,7 @@
6667
"@types/lodash": "^4.17.16",
6768
"@types/node": "^22.15.18",
6869
"@types/react-dom": "^19.1.5",
70+
"@types/react-window": "^1.8.8",
6971
"@types/uuid": "^10.0.0",
7072
"@vitejs/plugin-react-swc": "^3.9.0",
7173
"eslint": "^9.26.0",

src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,14 @@ const NewAccessPage = <T extends RequiredFormProps>({
241241
formik.values.acls
242242
)
243243
)}
244+
fetchOptions={() =>
245+
formatAclRolesForDropdown(
246+
filterRoles(
247+
roles,
248+
formik.values.acls
249+
)
250+
)
251+
}
244252
required={true}
245253
handleChange={(element) => {
246254
if (element) {
@@ -259,6 +267,8 @@ const NewAccessPage = <T extends RequiredFormProps>({
259267
user
260268
)
261269
}
270+
skipTranslate
271+
optionHeight={35}
262272
customCSS={{ width: 360, optionPaddingTop: 5 }}
263273
/>
264274
</td>

src/components/shared/DropDown.tsx

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import {
44
dropDownSpacingTheme,
55
dropDownStyle,
66
} from "../../utils/componentStyles";
7-
import Select, { GroupBase, Props, SelectInstance } from "react-select";
8-
import CreatableSelect from "react-select/creatable";
7+
import { GroupBase, MenuListProps, Props, SelectInstance, createFilter } from "react-select";
98
import { isJson } from "../../utils/utils";
109
import { ParseKeys } from "i18next";
10+
import { FixedSizeList, ListChildComponentProps } from "react-window";
11+
import AsyncSelect from 'react-select/async';
12+
import AsyncCreatableSelect from 'react-select/async-creatable';
1113

1214
export type DropDownOption = {
1315
label: string,
@@ -34,7 +36,10 @@ const DropDown = <T, >({
3436
disabled = false,
3537
menuIsOpen = undefined,
3638
handleMenuIsOpen = undefined,
39+
skipTranslate = false,
40+
optionHeight = 25,
3741
customCSS,
42+
fetchOptions,
3843
}: {
3944
ref?: React.RefObject<SelectInstance<any, boolean, GroupBase<any>> | null>
4045
value: T
@@ -51,12 +56,15 @@ const DropDown = <T, >({
5156
disabled?: boolean,
5257
menuIsOpen?: boolean,
5358
handleMenuIsOpen?: (open: boolean) => void,
59+
skipTranslate?: boolean,
60+
optionHeight?: number,
5461
customCSS?: {
5562
isMetadataStyle?: boolean,
5663
width?: number | string,
5764
optionPaddingTop?: number,
5865
optionLineHeight?: string
59-
}
66+
},
67+
fetchOptions?: () => { label: string, value: string}[]
6068
}) => {
6169
const { t } = useTranslation();
6270

@@ -84,8 +92,11 @@ const DropDown = <T, >({
8492
filterText: string,
8593
required: boolean,
8694
) => {
87-
// Translate?
88-
unformattedOptions = unformattedOptions.map(option => ({...option, label: t(option.label as ParseKeys)}))
95+
// Translate
96+
// Translating is expensive, skip it if it is not required
97+
if (!skipTranslate) {
98+
unformattedOptions = unformattedOptions.map(option => ({...option, label: t(option.label as ParseKeys)}))
99+
}
89100

90101
// Filter
91102
filterText = filterText.toLowerCase();
@@ -119,6 +130,41 @@ const DropDown = <T, >({
119130
return unformattedOptions;
120131
};
121132

133+
const itemHeight = optionHeight;
134+
/**
135+
* Custom component for list virtualization
136+
*/
137+
const MenuList = (props: MenuListProps<DropDownOption, false>) => {
138+
const { options, children, maxHeight, getValue } = props
139+
140+
console.log("Menu List render")
141+
142+
return Array.isArray(children) ? (
143+
<div style={{ paddingTop: 4 }}>
144+
<FixedSizeList
145+
height={maxHeight < (children.length * itemHeight) ? maxHeight : children.length * itemHeight}
146+
itemCount={children.length}
147+
itemSize={itemHeight}
148+
overscanCount={4}
149+
width="100%"
150+
>
151+
{({ index, style }: ListChildComponentProps) => <div style={{ ...style }}>{children[index]}</div>}
152+
</FixedSizeList>
153+
</div>
154+
) : null
155+
}
156+
157+
const loadOptions = (
158+
inputValue: string,
159+
callback: (options: DropDownOption[]) => void
160+
) => {
161+
callback(formatOptions(
162+
fetchOptions ? fetchOptions() : options,
163+
searchText,
164+
required,
165+
));
166+
};
167+
122168

123169
let commonProps: Props = {
124170
tabIndex: tabIndex,
@@ -129,11 +175,6 @@ const DropDown = <T, >({
129175
isSearchable: true,
130176
value: { value: value, label: text === "" ? placeholder : text },
131177
inputValue: searchText,
132-
options: formatOptions(
133-
options,
134-
searchText,
135-
required,
136-
),
137178
placeholder: placeholder,
138179
onInputChange: (value: string) => setSearch(value),
139180
onChange: (element) => handleChange(element as {value: T, label: string}),
@@ -142,18 +183,36 @@ const DropDown = <T, >({
142183
onMenuClose: () => openMenu(false),
143184
isDisabled: disabled,
144185
openMenuOnFocus: openMenuOnFocus,
186+
187+
//@ts-expect-error: React-Select typing does not account for the typing of option it itself requires
188+
components: { MenuList },
189+
filterOption: createFilter({ ignoreAccents: false }), // To improve performance on filtering
145190
};
146191

147192
return creatable ? (
148-
<CreatableSelect
193+
<AsyncCreatableSelect
149194
ref={selectRef}
150195
{...commonProps}
196+
cacheOptions
197+
defaultOptions={formatOptions(
198+
options,
199+
searchText,
200+
required,
201+
)}
202+
loadOptions={loadOptions}
151203
/>
152204
) : (
153-
<Select
205+
<AsyncSelect
154206
ref={selectRef}
155207
{...commonProps}
156208
noOptionsMessage={() => t("SELECT_NO_MATCHING_RESULTS")}
209+
cacheOptions
210+
defaultOptions={formatOptions(
211+
options,
212+
searchText,
213+
required,
214+
)}
215+
loadOptions={loadOptions}
157216
/>
158217
);
159218
};

src/components/shared/TableFilters.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ const FilterSwitch = ({
457457
openMenuOnFocus
458458
menuIsOpen={openSecondFilterMenu}
459459
handleMenuIsOpen={setOpenSecondFilterMenu}
460+
skipTranslate={!filter.translatable}
460461
customCSS={{ width: 200, optionPaddingTop: 5 }}
461462
/>
462463
</div>

src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,8 @@ const ResourceDetailsAccessPolicyTab = ({
495495
user
496496
)
497497
}
498+
skipTranslate
499+
optionHeight={35}
498500
customCSS={{ width: 360, optionPaddingTop: 5 }}
499501
/>
500502
) : (

src/components/users/partials/wizard/AclAccessPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ const AclAccessPage = <T extends RequiredFormProps>({
227227
"USERS.ACLS.NEW.ACCESS.ROLES.LABEL"
228228
)}
229229
disabled={!isAccess}
230+
skipTranslate
231+
optionHeight={35}
230232
customCSS={{ width: 360, optionPaddingTop: 5 }}
231233
/>
232234
</td>

src/selectors/tableFilterSelectors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSelector } from "reselect";
1+
import { createSelector } from "@reduxjs/toolkit";
22
import { RootState } from "../store";
33
import { Resource } from "../slices/tableSlice";
44

0 commit comments

Comments
 (0)