Skip to content

Commit 064329c

Browse files
committed
fix(ui): render relationship-type custom fields
* Displays relationship list custom fields on the record landing page and adds a UI widget for the deposit form. * Addresses #874.
1 parent 48be62c commit 064329c

File tree

4 files changed

+178
-15
lines changed

4 files changed

+178
-15
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// This file is part of Zenodo.
2+
// Copyright (C) 2025 CERN.
3+
//
4+
// Zenodo is free software; you can redistribute it and/or modify it
5+
// under the terms of the MIT License; see LICENSE file for more details.
6+
7+
import React, { Component } from "react";
8+
import PropTypes from "prop-types";
9+
import { Button, Form, Icon, Dropdown } from "semantic-ui-react";
10+
import {
11+
ArrayField,
12+
GroupField,
13+
showHideOverridableWithDynamicId,
14+
} from "react-invenio-forms";
15+
16+
const emptyRelationship = { subject: [], object: [] };
17+
18+
/**
19+
* A multi-value tag input using Semantic UI Dropdown.
20+
*/
21+
class TagInput extends Component {
22+
handleChange = (e, { value }) => {
23+
this.props.onChange(value);
24+
};
25+
26+
render() {
27+
const { value, placeholder } = this.props;
28+
const options = (value || []).map((v) => ({ key: v, text: v, value: v }));
29+
30+
return (
31+
<Dropdown
32+
fluid
33+
multiple
34+
search
35+
selection
36+
allowAdditions
37+
additionLabel="Add "
38+
placeholder={placeholder}
39+
value={value || []}
40+
options={options}
41+
onChange={this.handleChange}
42+
noResultsMessage="Type to add..."
43+
/>
44+
);
45+
}
46+
}
47+
48+
TagInput.propTypes = {
49+
value: PropTypes.array,
50+
placeholder: PropTypes.string,
51+
onChange: PropTypes.func.isRequired,
52+
};
53+
54+
TagInput.defaultProps = {
55+
value: [],
56+
placeholder: "",
57+
};
58+
59+
/**
60+
* Main component for editing a list of biotic relationships.
61+
* Used for the obo:RO_0002453 (Host of) custom field.
62+
*/
63+
class RelationshipListComponent extends Component {
64+
render() {
65+
const { fieldPath, label, labelIcon, helpText } = this.props;
66+
67+
return (
68+
<>
69+
{label && (
70+
<label className="field-label-class invenio-field-label" htmlFor={fieldPath}>
71+
<strong>{label}</strong>
72+
</label>
73+
)}
74+
75+
<ArrayField
76+
addButtonLabel="Add relationship"
77+
defaultNewValue={emptyRelationship}
78+
fieldPath={fieldPath}
79+
helpText={helpText}
80+
>
81+
{({ arrayHelpers, indexPath, array }) => {
82+
const relationship = array[indexPath] || emptyRelationship;
83+
84+
const handleSubjectChange = (newValue) => {
85+
arrayHelpers.replace(indexPath, { ...relationship, subject: newValue });
86+
};
87+
88+
const handleObjectChange = (newValue) => {
89+
arrayHelpers.replace(indexPath, { ...relationship, object: newValue });
90+
};
91+
92+
return (
93+
<GroupField>
94+
<Form.Field width={7}>
95+
<strong style={{ display: "block", marginBottom: "0.5em" }}>Subject</strong>
96+
<TagInput
97+
value={relationship.subject}
98+
placeholder="e.g. host species"
99+
onChange={handleSubjectChange}
100+
/>
101+
</Form.Field>
102+
103+
<Form.Field width={1} style={{ display: "flex", alignItems: "flex-end", justifyContent: "center", paddingBottom: "0.85em" }}>
104+
<Icon name="arrow right" />
105+
</Form.Field>
106+
107+
<Form.Field width={7}>
108+
<strong style={{ display: "block", marginBottom: "0.5em" }}>Object</strong>
109+
<TagInput
110+
value={relationship.object}
111+
placeholder="e.g. pathogen"
112+
onChange={handleObjectChange}
113+
/>
114+
</Form.Field>
115+
116+
<Form.Field width={1} style={{ display: "flex", alignItems: "flex-end", paddingBottom: "0.6em" }}>
117+
<Button
118+
aria-label="Remove field"
119+
className="close-btn"
120+
icon="close"
121+
onClick={() => arrayHelpers.remove(indexPath)}
122+
/>
123+
</Form.Field>
124+
</GroupField>
125+
);
126+
}}
127+
</ArrayField>
128+
</>
129+
);
130+
}
131+
}
132+
133+
RelationshipListComponent.propTypes = {
134+
fieldPath: PropTypes.string.isRequired,
135+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
136+
labelIcon: PropTypes.string,
137+
helpText: PropTypes.string,
138+
};
139+
140+
RelationshipListComponent.defaultProps = {
141+
label: undefined,
142+
labelIcon: undefined,
143+
helpText: undefined,
144+
};
145+
146+
export const RelationshipList = showHideOverridableWithDynamicId(RelationshipListComponent);

site/zenodo_rdm/custom_fields/domain_fields.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -844,16 +844,23 @@ def field(self):
844844
),
845845
),
846846
),
847-
# TODO - needs dedicated UI widget
848847
# obo
849-
# dict(
850-
# field="obo:RO_0002453",
851-
# template="zenodo_rdm/obo.html",
852-
# ui_widget="MultiInput",
853-
# props=dict(
854-
# label="Host of",
855-
# ),
856-
# ),
848+
dict(
849+
field="obo:RO_0002453",
850+
ui_widget="RelationshipList",
851+
template="zenodo_rdm/relationship_list.html",
852+
props=dict(
853+
label=_("Host of"),
854+
subject=dict(
855+
label=_("Subject"),
856+
placeholder=_("e.g., host species name"),
857+
),
858+
object=dict(
859+
label=_("Object"),
860+
placeholder=_("e.g., pathogen name"),
861+
),
862+
),
863+
),
857864
],
858865
}
859866

site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/obo.html

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{#
2+
Copyright (C) 2023-2025 CERN.
3+
4+
ZenodoRDM is free software; you can redistribute it and/or modify it
5+
under the terms of the MIT License; see LICENSE file for more details.
6+
#}
7+
8+
{# Template for rendering obo:RO_0002453 (Host of) custom field #}
9+
<dt class="ui tiny header">{{ _("Host of") }}</dt>
10+
<dd>
11+
{% for value in field_value %}
12+
<div>
13+
({{ value["subject"] | join(", ") }}) → ({{ value["object"] | join(", ") }})
14+
</div>
15+
{% endfor %}
16+
</dd>

0 commit comments

Comments
 (0)