Skip to content

Commit 5161c5d

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 befe1ca commit 5161c5d

File tree

4 files changed

+225
-15
lines changed

4 files changed

+225
-15
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// This file is part of Zenodo.
2+
// Copyright (C) 2025 CERN.
3+
//
4+
// Zenodo is free software; you can redistribute it
5+
// and/or modify it under the terms of the GNU General Public License as
6+
// published by the Free Software Foundation; either version 2 of the
7+
// License, or (at your option) any later version.
8+
//
9+
// Zenodo is distributed in the hope that it will be
10+
// useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
// General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Zenodo; if not, write to the
16+
// Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
17+
// MA 02111-1307, USA.
18+
//
19+
// In applying this license, CERN does not
20+
// waive the privileges and immunities granted to it by virtue of its status
21+
// as an Intergovernmental Organization or submit itself to any jurisdiction.
22+
23+
import React, { Component } from "react";
24+
import PropTypes from "prop-types";
25+
import { Button, Form, Icon, Dropdown } from "semantic-ui-react";
26+
import {
27+
ArrayField,
28+
GroupField,
29+
showHideOverridableWithDynamicId,
30+
} from "react-invenio-forms";
31+
32+
const emptyRelationship = { subject: [], object: [] };
33+
34+
/**
35+
* A multi-value tag input using Semantic UI Dropdown.
36+
*/
37+
class TagInput extends Component {
38+
handleChange = (e, { value }) => {
39+
this.props.onChange(value);
40+
};
41+
42+
render() {
43+
const { value, placeholder } = this.props;
44+
const options = (value || []).map((v) => ({ key: v, text: v, value: v }));
45+
46+
return (
47+
<Dropdown
48+
fluid
49+
multiple
50+
search
51+
selection
52+
allowAdditions
53+
additionLabel="Add "
54+
placeholder={placeholder}
55+
value={value || []}
56+
options={options}
57+
onChange={this.handleChange}
58+
noResultsMessage="Type to add..."
59+
/>
60+
);
61+
}
62+
}
63+
64+
TagInput.propTypes = {
65+
value: PropTypes.array,
66+
placeholder: PropTypes.string,
67+
onChange: PropTypes.func.isRequired,
68+
};
69+
70+
TagInput.defaultProps = {
71+
value: [],
72+
placeholder: "",
73+
};
74+
75+
/**
76+
* Main component for editing a list of biotic relationships.
77+
* Used for the obo:RO_0002453 (Host of) custom field.
78+
*/
79+
class RelationshipListComponent extends Component {
80+
render() {
81+
const { fieldPath, label, labelIcon, helpText, subject, object } = this.props;
82+
83+
return (
84+
<>
85+
{label && (
86+
<label className="field-label-class invenio-field-label" htmlFor={fieldPath}>
87+
<strong>{label}</strong>
88+
</label>
89+
)}
90+
91+
<ArrayField
92+
addButtonLabel="Add relationship"
93+
defaultNewValue={emptyRelationship}
94+
fieldPath={fieldPath}
95+
helpText={helpText}
96+
>
97+
{({ arrayHelpers, indexPath, array }) => {
98+
const relationship = array[indexPath] || emptyRelationship;
99+
100+
const handleSubjectChange = (newValue) => {
101+
arrayHelpers.replace(indexPath, { ...relationship, subject: newValue });
102+
};
103+
104+
const handleObjectChange = (newValue) => {
105+
arrayHelpers.replace(indexPath, { ...relationship, object: newValue });
106+
};
107+
108+
return (
109+
<GroupField>
110+
<Form.Field width={7}>
111+
<strong style={{ display: "block", marginBottom: "0.5em" }}>
112+
{subject?.label || "Subject"}
113+
</strong>
114+
<TagInput
115+
value={relationship.subject}
116+
placeholder={subject?.placeholder || ""}
117+
onChange={handleSubjectChange}
118+
/>
119+
</Form.Field>
120+
121+
<Form.Field width={1} style={{ display: "flex", alignItems: "flex-end", justifyContent: "center", paddingBottom: "0.85em" }}>
122+
<Icon name="arrow right" />
123+
</Form.Field>
124+
125+
<Form.Field width={7}>
126+
<strong style={{ display: "block", marginBottom: "0.5em" }}>
127+
{object?.label || "Object"}
128+
</strong>
129+
<TagInput
130+
value={relationship.object}
131+
placeholder={object?.placeholder || ""}
132+
onChange={handleObjectChange}
133+
/>
134+
</Form.Field>
135+
136+
<Form.Field width={1} style={{ display: "flex", alignItems: "flex-end", paddingBottom: "0.6em" }}>
137+
<Button
138+
aria-label="Remove field"
139+
className="close-btn"
140+
icon="close"
141+
onClick={() => arrayHelpers.remove(indexPath)}
142+
/>
143+
</Form.Field>
144+
</GroupField>
145+
);
146+
}}
147+
</ArrayField>
148+
</>
149+
);
150+
}
151+
}
152+
153+
RelationshipListComponent.propTypes = {
154+
fieldPath: PropTypes.string.isRequired,
155+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
156+
labelIcon: PropTypes.string,
157+
helpText: PropTypes.string,
158+
subject: PropTypes.shape({
159+
label: PropTypes.string,
160+
placeholder: PropTypes.string,
161+
}),
162+
object: PropTypes.shape({
163+
label: PropTypes.string,
164+
placeholder: PropTypes.string,
165+
}),
166+
};
167+
168+
RelationshipListComponent.defaultProps = {
169+
label: undefined,
170+
labelIcon: undefined,
171+
helpText: undefined,
172+
subject: {},
173+
object: {},
174+
};
175+
176+
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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{#
2+
# This file is part of Zenodo.
3+
# Copyright (C) 2025 CERN.
4+
#
5+
# Zenodo is free software; you can redistribute it
6+
# and/or modify it under the terms of the GNU General Public License as
7+
# published by the Free Software Foundation; either version 2 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# Zenodo is distributed in the hope that it will be
11+
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with Zenodo; if not, write to the
17+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
18+
# MA 02111-1307, USA.
19+
#
20+
# In applying this license, CERN does not
21+
# waive the privileges and immunities granted to it by virtue of its status
22+
# as an Intergovernmental Organization or submit itself to any jurisdiction.
23+
-#}
24+
25+
{# Template for rendering relationship list custom fields (e.g. obo:RO_0002453) #}
26+
<dt class="ui tiny header">{{ field_cfg.props.label }}</dt>
27+
<dd>
28+
{% for value in field_value %}
29+
<div>
30+
({{ value["subject"] | join(", ") }}) → ({{ value["object"] | join(", ") }})
31+
</div>
32+
{% endfor %}
33+
</dd>

0 commit comments

Comments
 (0)