Skip to content

Commit 0ef74f6

Browse files
committed
overridable: Start implementing for custom fields
* This commit demonstrates a potential method of implementing `Overridable` support for custom fields. * When creating a custom field [UI definition](https://inveniordm.docs.cern.ch/customize/metadata/custom_fields/records/#upload-deposit-form), the user simply has to specify an additional `id` prop with their desired ID value for the Overridable. Then, they can specify a `parametrize` in the `overridableRegistry/mapping.js` as usual. * The aim is to have a set of common props that can be applied to all the field widgets, similarly to inveniosoftware/invenio-app-rdm#3103. * This commit makes the necessary changes to two of the UI widgets as a demonstration. When parameterizing, the user has to reference e.g. `AutocompleteDropdownComponent` instead of `AutocompleteDropdown`. These are both exported.
1 parent 8b7e578 commit 0ef74f6

File tree

4 files changed

+156
-44
lines changed

4 files changed

+156
-44
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// This file is part of React-Invenio-Forms
2+
// Copyright (C) 2022-2025 CERN.
3+
// Copyright (C) 2022 Northwestern University.
4+
//
5+
// React-Invenio-Forms is free software; you can redistribute it and/or modify it
6+
// under the terms of the MIT License; see LICENSE file for more details.
7+
8+
import PropTypes from "prop-types";
9+
import React from "react";
10+
import Overridable from "react-overridable";
11+
12+
// Props used for fields that are mandatory for all records
13+
export const mandatoryFieldCommonProps = {
14+
fieldPath: PropTypes.string.isRequired,
15+
label: PropTypes.string,
16+
labelIcon: PropTypes.string,
17+
helpText: PropTypes.string,
18+
placeholder: PropTypes.string,
19+
};
20+
21+
// Also includes props that allow not including a field in the form, which are not applicable
22+
// to mandatory fields.
23+
export const fieldCommonProps = {
24+
...mandatoryFieldCommonProps,
25+
hidden: PropTypes.bool,
26+
disabled: PropTypes.bool,
27+
required: PropTypes.bool,
28+
};
29+
30+
export const createCommonDepositFieldComponent = (id, Child) => {
31+
const Component = ({ hidden, ...props }) => {
32+
if (props.disabled && props.required) {
33+
throw new Error(`Cannot make field component ${id} both required and disabled`);
34+
}
35+
36+
if (props.hidden) return null;
37+
return <Child {...props} />;
38+
};
39+
40+
Component.propTypes = Child.propTypes;
41+
return Overridable.component(id, Component);
42+
};
43+
44+
export const createShowHideComponent = (Component) => {
45+
const ShowHideComponent = ({ hidden, ...props }) => {
46+
if (hidden) return null;
47+
return <Component {...props} />;
48+
};
49+
50+
ShowHideComponent.displayName = `ShowHide(${
51+
Component.displayName || Component.name
52+
})`;
53+
ShowHideComponent.propTypes = { ...Component.propTypes };
54+
ShowHideComponent.defaultProps = { ...Component.defaultProps };
55+
return ShowHideComponent;
56+
};
57+
58+
export const createDynamicOverridableWidget = (Widget) => {
59+
const Component = ({ id, ...props }) => {
60+
if (id === undefined) return <Widget {...props} />;
61+
62+
return (
63+
<Overridable id={id} {...props}>
64+
<Widget {...props} />
65+
</Overridable>
66+
);
67+
};
68+
69+
Component.propTypes = { ...Widget.propTypes, id: PropTypes.string };
70+
Component.defaultProps = { ...Widget.defaultProps, id: undefined };
71+
Component.displayName = `DynamicOverridable(${Widget.displayName || Widget.name})`;
72+
return Component;
73+
};

src/lib/forms/widgets/select/AutocompleteDropdown.js

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ import _isArray from "lodash/isArray";
66
import { Field } from "formik";
77
import { FieldLabel } from "../../FieldLabel";
88
import { RemoteSelectField } from "../../RemoteSelectField";
9+
import {
10+
createDynamicOverridableWidget,
11+
createShowHideComponent,
12+
fieldCommonProps,
13+
} from "../../common/fieldComponents";
914

10-
export default class AutocompleteDropdown extends Component {
15+
class _AutocompleteDropdownComponent extends Component {
1116
render() {
1217
const {
1318
description,
@@ -20,10 +25,16 @@ export default class AutocompleteDropdown extends Component {
2025
multiple,
2126
autocompleteFrom,
2227
autocompleteFromAcceptHeader,
28+
helpText: helpTextProp,
29+
labelIcon: labelIconProp,
2330
} = this.props;
31+
32+
const helpText = helpTextProp ?? description;
33+
const labelIcon = labelIconProp ?? icon;
34+
2435
return (
2536
<>
26-
<FieldLabel htmlFor={fieldPath} icon={icon} label={label} />
37+
<FieldLabel htmlFor={fieldPath} icon={labelIcon} label={label} />
2738
<Field name={fieldPath}>
2839
{({ form: { values } }) => {
2940
return (
@@ -58,29 +69,40 @@ export default class AutocompleteDropdown extends Component {
5869
);
5970
}}
6071
</Field>
61-
{description && <label className="helptext">{description}</label>}
72+
{helpText && <label className="helptext">{helpText}</label>}
6273
</>
6374
);
6475
}
6576
}
6677

67-
AutocompleteDropdown.propTypes = {
68-
fieldPath: PropTypes.string.isRequired,
69-
label: PropTypes.string.isRequired,
78+
_AutocompleteDropdownComponent.propTypes = {
7079
placeholder: PropTypes.string.isRequired,
71-
description: PropTypes.string.isRequired,
7280
autocompleteFrom: PropTypes.string.isRequired,
7381
autocompleteFromAcceptHeader: PropTypes.string,
74-
icon: PropTypes.string,
7582
clearable: PropTypes.bool,
7683
multiple: PropTypes.bool,
77-
required: PropTypes.bool,
84+
/**
85+
* @deprecated Use `helpText` instead
86+
*/
87+
description: PropTypes.string,
88+
/**
89+
* @deprecated Use `labelIcon` instead
90+
*/
91+
icon: PropTypes.string,
92+
...fieldCommonProps,
7893
};
7994

80-
AutocompleteDropdown.defaultProps = {
81-
icon: undefined,
95+
_AutocompleteDropdownComponent.defaultProps = {
8296
autocompleteFromAcceptHeader: "application/vnd.inveniordm.v1+json",
8397
clearable: false,
8498
multiple: false,
85-
required: false,
99+
icon: undefined,
100+
description: undefined,
86101
};
102+
103+
export const AutocompleteDropdownComponent = createShowHideComponent(
104+
_AutocompleteDropdownComponent
105+
);
106+
export const AutocompleteDropdown = createDynamicOverridableWidget(
107+
AutocompleteDropdownComponent
108+
);
Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import React, { Component } from "react";
22
import PropTypes from "prop-types";
3-
43
import { FieldLabel } from "../../FieldLabel";
54
import { SelectField } from "../../SelectField";
5+
import {
6+
createDynamicOverridableWidget,
7+
createShowHideComponent,
8+
fieldCommonProps,
9+
} from "../../common/fieldComponents";
610

7-
export default class Dropdown extends Component {
11+
class _DropdownComponent extends Component {
812
serializeOptions = (options) =>
913
options?.map((option) => ({
1014
text: option.title_l10n,
@@ -15,62 +19,72 @@ export default class Dropdown extends Component {
1519
render() {
1620
const {
1721
description,
22+
helpText: helpTextProp,
1823
placeholder,
1924
fieldPath,
2025
label,
2126
icon,
27+
labelIcon: labelIconProp,
2228
options,
2329
search,
2430
multiple,
2531
clearable,
2632
required,
2733
} = this.props;
34+
35+
const helpText = helpTextProp ?? description;
36+
const labelIcon = labelIconProp ?? icon;
37+
2838
return (
29-
<>
30-
<SelectField
31-
fieldPath={fieldPath}
32-
label={<FieldLabel htmlFor={fieldPath} icon={icon} label={label} />}
33-
options={this.serializeOptions(options)}
34-
search={search}
35-
aria-label={label}
36-
multiple={multiple}
37-
placeholder={{
38-
role: "option",
39-
content: placeholder,
40-
}}
41-
clearable={clearable}
42-
required={required}
43-
optimized
44-
defaultValue={multiple ? [] : ""}
45-
/>
46-
{description && <label className="helptext">{description}</label>}
47-
</>
39+
<SelectField
40+
fieldPath={fieldPath}
41+
label={<FieldLabel htmlFor={fieldPath} icon={labelIcon} label={label} />}
42+
options={this.serializeOptions(options)}
43+
search={search}
44+
aria-label={label}
45+
multiple={multiple}
46+
placeholder={{
47+
role: "option",
48+
content: placeholder,
49+
}}
50+
clearable={clearable}
51+
required={required}
52+
optimized
53+
defaultValue={multiple ? [] : ""}
54+
helpText={helpText}
55+
/>
4856
);
4957
}
5058
}
5159

52-
Dropdown.propTypes = {
53-
fieldPath: PropTypes.string.isRequired,
54-
label: PropTypes.string.isRequired,
55-
placeholder: PropTypes.string.isRequired,
56-
description: PropTypes.string.isRequired,
60+
_DropdownComponent.propTypes = {
5761
options: PropTypes.arrayOf(
5862
PropTypes.shape({
5963
id: PropTypes.string.isRequired,
6064
title_l10n: PropTypes.string.isRequired,
6165
})
6266
).isRequired,
63-
icon: PropTypes.string,
6467
search: PropTypes.bool,
6568
multiple: PropTypes.bool,
6669
clearable: PropTypes.bool,
67-
required: PropTypes.bool,
70+
/**
71+
* @deprecated Use `helpText` instead
72+
*/
73+
description: PropTypes.string,
74+
/**
75+
* @deprecated Use `labelIcon` instead
76+
*/
77+
icon: PropTypes.string,
78+
...fieldCommonProps,
6879
};
6980

70-
Dropdown.defaultProps = {
81+
_DropdownComponent.defaultProps = {
7182
icon: undefined,
7283
search: false,
7384
multiple: false,
7485
clearable: true,
75-
required: false,
86+
description: undefined,
7687
};
88+
89+
export const DropdownComponent = createShowHideComponent(_DropdownComponent);
90+
export const Dropdown = createDynamicOverridableWidget(_DropdownComponent);

src/lib/forms/widgets/select/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
export { default as AutocompleteDropdown } from "./AutocompleteDropdown";
2-
export { default as Dropdown } from "./Dropdown";
1+
export {
2+
AutocompleteDropdown,
3+
AutocompleteDropdownComponent,
4+
} from "./AutocompleteDropdown";
5+
export { Dropdown, DropdownComponent } from "./Dropdown";

0 commit comments

Comments
 (0)