Skip to content

Commit 3d3bd0b

Browse files
committed
Merge branch 'async-select-for-collections' of Arnei/opencast-admin-interface into r/18.x
Pull request #1375 Make event metadata load faster by fetching series options asynchronously
2 parents e813535 + 59f7ae0 commit 3d3bd0b

File tree

4 files changed

+136
-43
lines changed

4 files changed

+136
-43
lines changed

src/components/shared/DropDown.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const DropDown = <T, >({
4545
ref?: React.RefObject<SelectInstance<any, boolean, GroupBase<any>> | null>
4646
value: T
4747
text: string,
48-
options: DropDownOption[],
48+
options?: DropDownOption[],
4949
required: boolean,
5050
handleChange: (option: {value: T, label: string} | null) => void
5151
placeholder: string
@@ -66,7 +66,7 @@ const DropDown = <T, >({
6666
optionPaddingTop?: number,
6767
optionLineHeight?: string
6868
},
69-
fetchOptions?: () => { label: string, value: string}[]
69+
fetchOptions?: (inputValue: string) => Promise<{ label: string, value: string }[]>
7070
}) => {
7171
const { t } = useTranslation();
7272

@@ -157,14 +157,29 @@ const DropDown = <T, >({
157157
) : null;
158158
};
159159

160+
const filterOptions = (inputValue: string) => {
161+
if (options) {
162+
return options.filter(option =>
163+
option.label.toLowerCase().includes(inputValue.toLowerCase()),
164+
);
165+
}
166+
return [];
167+
};
168+
169+
const loadOptionsAsync = (inputValue: string, callback: (options: DropDownOption[]) => void) => {
170+
setTimeout(async () => {
171+
callback(formatOptions(
172+
fetchOptions ? await fetchOptions(inputValue) : filterOptions(inputValue),
173+
required,
174+
));
175+
}, 1000);
176+
};
177+
160178
const loadOptions = (
161179
inputValue: string,
162180
callback: (options: DropDownOption[]) => void,
163181
) => {
164-
callback(formatOptions(
165-
fetchOptions ? fetchOptions() : options,
166-
required,
167-
));
182+
callback(formatOptions(filterOptions(inputValue), required));
168183
};
169184

170185

@@ -176,10 +191,14 @@ const DropDown = <T, >({
176191
autoFocus: autoFocus,
177192
isSearchable: true,
178193
value: { value: value, label: text === "" ? placeholder : text },
179-
options: formatOptions(
180-
options,
181-
required,
182-
),
194+
defaultOptions: options
195+
? formatOptions(
196+
options,
197+
required,
198+
)
199+
: true,
200+
cacheOptions: true,
201+
loadOptions: fetchOptions ? loadOptionsAsync : loadOptions,
183202
placeholder: placeholder,
184203
onChange: element => handleChange(element as {value: T, label: string}),
185204
menuIsOpen: menuIsOpen,
@@ -191,31 +210,18 @@ const DropDown = <T, >({
191210

192211
// @ts-expect-error: React-Select typing does not account for the typing of option it itself requires
193212
components: { MenuList },
194-
filterOption: createFilter({ ignoreAccents: false }), // To improve performance on filtering
195213
};
196214

197215
return creatable ? (
198216
<AsyncCreatableSelect
199217
ref={selectRef}
200218
{...commonProps}
201-
cacheOptions
202-
defaultOptions={formatOptions(
203-
options,
204-
required,
205-
)}
206-
loadOptions={loadOptions}
207219
/>
208220
) : (
209221
<AsyncSelect
210222
ref={selectRef}
211223
{...commonProps}
212224
noOptionsMessage={() => t("SELECT_NO_MATCHING_RESULTS")}
213-
cacheOptions
214-
defaultOptions={formatOptions(
215-
options,
216-
required,
217-
)}
218-
loadOptions={loadOptions}
219225
/>
220226
);
221227
};

src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,11 +543,6 @@ export const AccessPolicyTable = <T extends AccessPolicyTabFormikProps>({
543543
? formatAclRolesForDropdown(rolesFilteredbyPolicies)
544544
: []
545545
}
546-
fetchOptions={() =>
547-
roles.length > 0
548-
? formatAclRolesForDropdown(rolesFilteredbyPolicies)
549-
: []
550-
}
551546
required={true}
552547
creatable={true}
553548
handleChange={element => {

src/components/shared/wizard/RenderField.tsx

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import React, { useRef, useState } from "react";
1+
import React, { useEffect, useRef, useState } from "react";
22
import { useTranslation } from "react-i18next";
33
import DatePicker from "react-datepicker";
44
import cn from "classnames";
5-
import { getMetadataCollectionFieldName } from "../../../utils/resourceUtils";
5+
import { getMetadataCollectionFieldName, transformListProvider } from "../../../utils/resourceUtils";
66
import { getCurrentLanguageInformation } from "../../../utils/utils";
77
import DropDown from "../DropDown";
88
import { parseISO } from "date-fns";
99
import { FieldProps } from "formik";
1010
import { MetadataField } from "../../../slices/eventSlice";
1111
import { GroupBase, SelectInstance } from "react-select";
1212
import TextareaAutosize from "react-textarea-autosize";
13+
import axios from "axios";
1314

1415
/**
1516
* This component renders an editable field for single values depending on the type of the corresponding metadata
@@ -65,7 +66,7 @@ const RenderField = ({
6566
)}
6667
{metadataField.type === "text" &&
6768
!!metadataField.collection &&
68-
metadataField.collection.length > 0 && (
69+
(
6970
<EditableSingleSelect
7071
metadataField={metadataField}
7172
field={field}
@@ -91,7 +92,7 @@ const RenderField = ({
9192
)}
9293
{metadataField.type === "text" &&
9394
!(
94-
!!metadataField.collection && metadataField.collection.length !== 0
95+
metadataField.collection
9596
) && (
9697
<EditableSingleValue
9798
field={field}
@@ -195,16 +196,7 @@ const EditableDateValue = ({
195196
};
196197

197198
// renders editable field for selecting value via dropdown
198-
const EditableSingleSelect = ({
199-
field,
200-
metadataField,
201-
text,
202-
form: { setFieldValue },
203-
isFirstField,
204-
focused,
205-
setFocused,
206-
ref,
207-
}: {
199+
type EditableSingleSelectProps = ({
208200
field: FieldProps["field"]
209201
metadataField: MetadataField
210202
text: string
@@ -213,9 +205,25 @@ const EditableSingleSelect = ({
213205
focused: boolean,
214206
setFocused: (open: boolean) => void
215207
ref: React.RefObject<SelectInstance<any, boolean, GroupBase<any>>>
216-
}) => {
208+
})
209+
const EditableSingleSelect = (props: EditableSingleSelectProps) => {
217210
const { t } = useTranslation();
218211

212+
const {
213+
field,
214+
metadataField,
215+
text,
216+
form: { setFieldValue },
217+
isFirstField,
218+
focused,
219+
setFocused,
220+
ref,
221+
} = props;
222+
223+
if (metadataField.id === "isPartOf") {
224+
return <EditableSingleSelectSeries {...props} />;
225+
}
226+
219227
return (
220228
<DropDown
221229
ref={ref}
@@ -321,4 +329,67 @@ const EditableSingleValueTime = ({
321329
);
322330
};
323331

332+
/**
333+
* Special case for series. Uses an async selector to fetch options.
334+
*
335+
* Ideally we could generalize this for all metadata fields with listproviders,
336+
* but other listproviders do not offer the required filtering capabilities.
337+
*/
338+
const EditableSingleSelectSeries = ({
339+
field,
340+
metadataField,
341+
text,
342+
form: { setFieldValue },
343+
isFirstField,
344+
focused,
345+
setFocused,
346+
ref,
347+
}: EditableSingleSelectProps) => {
348+
const { t } = useTranslation();
349+
350+
const [label, setLabel] = useState("");
351+
352+
useEffect(() => {
353+
// The metadata catalog only contains the field value, so we need to fetch the label ourselves
354+
const fetchLabelById = async () => {
355+
if (field.value) {
356+
const res = await axios.get<{ [key: string]: string }>(`/admin-ng/resources/SERIES.WRITE_ONLY.json?limit=1&filter=textFilter:${field.value}`);
357+
const data = res.data;
358+
const transformedData = transformListProvider(data);
359+
if (transformedData.length > 0) {
360+
setLabel(transformedData[0].label);
361+
}
362+
}
363+
};
364+
fetchLabelById();
365+
}, [field.value]);
366+
367+
// Fetch collection
368+
const fetchOptions = async (inputValue: string) => {
369+
const res = await axios.get<{ [key: string]: string }>(`/admin-ng/resources/SERIES.WRITE_ONLY.json?filter=textFilter:${inputValue}`);
370+
const data = res.data;
371+
return transformListProvider(data);
372+
};
373+
374+
return (
375+
<DropDown
376+
ref={ref}
377+
value={field.value as string}
378+
text={label}
379+
fetchOptions={fetchOptions}
380+
required={metadataField.required}
381+
handleChange={element => element && setFieldValue(field.name, element.value)}
382+
placeholder={focused
383+
? `-- ${t("SELECT_NO_OPTION_SELECTED")} --`
384+
: `${t("SELECT_NO_OPTION_SELECTED")}`
385+
}
386+
customCSS={{ isMetadataStyle: focused ? false : true }}
387+
handleMenuIsOpen={(open: boolean) => setFocused(open)}
388+
openMenuOnFocus
389+
autoFocus={isFirstField}
390+
skipTranslate={!metadataField.translatable}
391+
/>
392+
);
393+
};
394+
324395
export default RenderField;

src/utils/resourceUtils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,27 @@ export const transformMetadataFields = (metadata: MetadataField[]) => {
177177
return metadata;
178178
};
179179

180+
export const transformListProvider = (collection: { [key: string]: string }) => {
181+
return Object.entries(collection)
182+
.map(([key, value]) => {
183+
if (isJson(value)) {
184+
// TODO: Handle JSON parsing errors
185+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
186+
const collectionParsed: { [key: string]: string } = JSON.parse(value);
187+
return {
188+
label: collectionParsed.label || value,
189+
value: key,
190+
...collectionParsed,
191+
};
192+
} else {
193+
return {
194+
label: value,
195+
value: key,
196+
};
197+
}
198+
});
199+
};
200+
180201
// transform metadata catalog for update via post request
181202
export const transformMetadataForUpdate = (catalog: MetadataCatalog, values: { [key: string]: MetadataCatalog["fields"][0]["value"] }) => {
182203
const fields: MetadataCatalog["fields"] = [];

0 commit comments

Comments
 (0)