Skip to Content
CustomizeModel UIQuick start tutorial

Quick Start: Customizing Your Model UI

After generating your repository with nrp-model-copier, you’ll want to customize the starter pages to display your actual record model. This tutorial walks you through the most common customization tasks.

This tutorial assumes you’ve already generated a model using nrp-model-copier and have a working repository. For detailed architecture and configuration topics, see the in-depth documentation for Record landing page, Deposit form, and Search page.

Overview

The nrp-model-copier template provides starter files that you edit directly to customize your model’s UI:

ComponentPurposeStarter FileDocumentation
Record detailDisplays individual recordui/{{model_name}}/templates/semantic-ui/{{model_name}}/record_detail/main.htmlRecord landing page
Deposit formForm for creating/editing recordsui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js (plus section files you add yourself)Deposit form
Search resultHow records appear in searchui/{{model_name}}/semantic-ui/js/{{model_name}}/search/ResultsListItem.jsxSearch page

No FormFieldsContainer.jsx is generated. The current nrp-model-copier template (rdm-14) generates only forms/index.js, wiring preset section arrays (CCMMSections, RDMMinimalSections/RDMBasicSections, or ExampleSection for empty base models) into DepositFormApp with useWizardForm. Depending on the answers you gave during model creation, your generated index.js may even reference a ready-made section set from @js/ccmm_invenio/forms or @js/oarepo_rdm/form. Deposit forms are customized by defining section objects and, where needed, componentOverrides — see Deposit form. FormFieldsContainer-centric snippets elsewhere on this page predate the sections-based template.

For JavaScript file changes (deposit form, search result), run the assets watcher in a separate terminal to automatically rebuild on file changes:

./run.sh cli assets watch

For more information, see Run frontend assets builder.


1. Customize Record Detail Page

The record detail page starter template contains predefined JinjaX blocks that you modify to display your metadata.

Step 1: Find the metadata field you want to display

Looking at your metadata schema:

{{model_name}}/metadata.yaml
# Definition of metadata for {{model_name}} Metadata: properties: title: type: fulltext+keyword label: en: Title description: type: fulltext label: en: Description authors: type: array items: type: keyword label: en: Authors

Step 2: Edit the starter template

Edit ui/{{model_name}}/templates/semantic-ui/{{model_name}}/record_detail/main.html:

ui/{{model_name}}/templates/semantic-ui/{{model_name}}/record_detail/main.html
{% extends "oarepo_ui/record_detail/main.html" %} {%- block record_content -%} {%- if record.metadata.description -%} <div class="ui segment"> <p>{{ record.metadata.description }}</p> </div> {%- endif -%} {%- if record.metadata.authors -%} <div class="ui segment"> <strong>Authors:</strong> {{ record.metadata.authors | join(', ') }} </div> {%- endif -%} {%- endblock record_content -%}

The starter template includes commented-out blocks - uncomment the blocks you want to customize.

For available override blocks, see also Template structure.

Step 3: Visit the page

Visit a record detail page at https://127.0.0.1:5000/mymodel/your-record-id

2. Customize Deposit Form

Adapt the generated deposit form to your model’s metadata schema.

Step 1: Review the generated entry point

The copier template generated ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js for you. Its content depends on the base_model you chose — e.g. an rdm_minimal model starts from:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
import { DepositFormApp, parseFormAppConfig } from "@js/oarepo_ui/forms"; import { RDMMinimalSections, RDMMinimalRecordSerializer } from "@js/oarepo_rdm/form"; import React from "react"; import ReactDOM from "react-dom"; const { rootEl, config, ...rest } = parseFormAppConfig(); const recordSerializer = new RDMMinimalRecordSerializer( config.default_locale, config.custom_fields.vocabularies, ); ReactDOM.render( <DepositFormApp config={config} {...rest} sections={RDMMinimalSections} recordSerializer={recordSerializer} useWizardForm />, rootEl, );

Step 2: Add a section with your fields

First declare the fields in model/{{model_name}}/metadata.yaml (labels, hints and help texts live there — form components pick them up automatically via useFieldData). Then create a section file using Deposit form components:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/BasicInformationSection.js
import React from "react"; import { i18next } from "@translations/i18next"; import { TextField } from "@js/oarepo_ui/forms"; export const BasicInformationSection = { key: "basic-information", label: i18next.t("Basic information"), component: () => ( <React.Fragment> <TextField fieldPath="metadata.title" required /> <TextField fieldPath="metadata.description" /> </React.Fragment> ), includesPaths: ["metadata.title", "metadata.description"], };

For array fields use StringArrayField / ArrayField, for vocabularies VocabularyField, for EDTF dates the EDTFSingleDatePicker / EDTFDaterangePicker components.

Step 3: Compose your sections and pass them to the form

Collect the sections in one place and swap them into index.js:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/sections.js
import { BasicInformationSection } from "./BasicInformationSection"; // Compose with or replace preset sections, e.g. // import { RDMCommunityAndAccess, RDMFiles } from "@js/oarepo_rdm/form"; export const mySections = [ // RDMCommunityAndAccess, // RDMFiles, BasicInformationSection, ];
ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js
import { mySections } from "./sections"; // ... ReactDOM.render( <DepositFormApp config={config} {...rest} sections={mySections} recordSerializer={recordSerializer} useWizardForm />, rootEl, );

For more on the section object format (auto-save on tab switch, completion indicators, locking), see Defining Sections and Building Your Deposit Form.

Step 2: View changes

The assets watcher will automatically rebuild your changes. Visit the deposit page to see the updated form.

Step 3: Test the form

Visit the deposit page at https://127.0.0.1:5000/mymodel/uploads/new

Available Form Fields

ComponentBest ForDocumentation
TextFieldSimple text inputTextField
RichInputFieldRich text/descriptionRichInputField
ArrayFieldRepeating fieldsArrayField
SelectFieldDropdown selectionSelectField
DateFieldDate/time pickersDateField
BooleanFieldYes/no togglesBooleanField

For a complete list, see Deposit form components.

3. Customize Search Result Item

Edit the search result starter component to display how your records appear in search.

Step 1: Edit the starter component

Edit ui/{{model_name}}/semantic-ui/js/{{model_name}}/search/ResultsListItem.jsx:

ui/{{model_name}}/semantic-ui/js/{{model_name}}/search/ResultsListItem.jsx
import React from "react"; import PropTypes from "prop-types"; import _get from "lodash/get"; import { Item } from "semantic-ui-react"; import { i18next } from "@translations/i18next"; export const ResultsListItem = ({ result }) => { const title = _get(result, "metadata.title", i18next.t("No title")); const description = _get(result, "metadata.description", ""); const authors = _get(result, "metadata.authors", []); return ( <Item key={result.id}> <Item.Content> <Item.Header as="h2"> <a href={result.links.self_html}>{title}</a> </Item.Header> {description && ( <Item.Description> {description} </Item.Description> )} {authors.length > 0 && ( <Item.Meta> Authors: {authors.join(", ")} </Item.Meta> )} </Item.Content> </Item> ); }; ResultsListItem.propTypes = { result: PropTypes.object.isRequired, }; export default ResultsListItem;

For more on building your search result item, see Building Your Search Result Item.

Step 2: View search results

Visit the search page at https://127.0.0.1:5000/mymodel


Putting It All Together

Here’s a complete example for a “Book” record model:

Metadata Schema

{{model_name}}/metadata.yaml
# Definition of metadata for {{model_name}} Metadata: properties: title: type: fulltext+keyword label: en: Title subtitle: type: fulltext label: en: Subtitle isbn: type: keyword label: en: ISBN authors: type: array items: type: keyword label: en: Authors publisher: type: keyword label: en: Publisher published_date: type: datetime label: en: Published date abstract: type: fulltext label: en: Abstract

For more on defining metadata schema, see Metadata.

Record Detail Template

ui/{{model_name}}/templates/semantic-ui/{{model_name}}/record_detail/main.html
{% extends "oarepo_ui/record_detail/main.html" %} {%- block record_content -%} <div class="ui segment"> <div class="ui divided items"> {%- if record.metadata.authors -%} <div class="item"> <div class="content"> <strong>Authors:</strong> <ul> {%- for author in record.metadata.authors -%} <li>{{ author }}</li> {%- endfor -%} </ul> </div> </div> {%- endif -%} {%- if record.metadata.publisher -%} <div class="item"> <div class="content"> <strong>Publisher:</strong> {{ record.metadata.publisher }} </div> </div> {%- endif -%} {%- if record.metadata.published_date -%} <div class="item"> <div class="content"> <strong>Published:</strong> {{ record.metadata.published_date }} </div> </div> {%- endif -%} {%- if record.metadata.isbn -%} <div class="item"> <div class="content"> <strong>ISBN:</strong> {{ record.metadata.isbn }} </div> </div> {%- endif -%} </div> </div> {%- if record.metadata.abstract -%} <div class="ui segment"> <strong>Abstract:</strong> <p>{{ record.metadata.abstract }}</p> </div> {%- endif -%} {%- endblock record_content -%}

For more detail page customization options, see Record landing page.

Deposit Form

ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/CustomFieldsSection.js
import React from "react"; import { i18next } from "@translations/i18next"; import { TextField, StringArrayField, EDTFSingleDatePicker } from "@js/oarepo_ui/forms"; export const CustomFieldsSection = { key: "custom-fields", label: i18next.t("Publication"), component: () => ( <React.Fragment> <TextField fieldPath="metadata.title" required /> <TextField fieldPath="metadata.subtitle" /> <TextField fieldPath="metadata.isbn" /> <TextField fieldPath="metadata.publisher" /> <EDTFSingleDatePicker fieldPath="metadata.published_date" /> <StringArrayField fieldPath="metadata.authors" addButtonLabel="Add author" /> </React.Fragment> ), includesPaths: [ "metadata.title", "metadata.subtitle", "metadata.isbn", "metadata.publisher", "metadata.published_date", "metadata.authors", ], };

Register the section in forms/sections.js and pass the composed array as sections={mySections} to DepositFormApp in forms/index.js (see Step 3 above).

Search Result Item

ui/{{model_name}}/semantic-ui/js/{{model_name}}/search/ResultsListItem.jsx
import React from "react"; import PropTypes from "prop-types"; import _get from "lodash/get"; import { Item, Label } from "semantic-ui-react"; import { i18next } from "@translations/i18next"; export const ResultsListItem = ({ result }) => { const title = _get(result, "metadata.title", i18next.t("No title")); const subtitle = _get(result, "metadata.subtitle", ""); const authors = _get(result, "metadata.authors", []); const isbn = _get(result, "metadata.isbn", ""); return ( <Item key={result.id}> <Item.Content> <Item.Header as="h2"> <a href={result.links.self_html}>{title}</a> </Item.Header> {subtitle && ( <Item.Meta>{subtitle}</Item.Meta> )} {authors.length > 0 && ( <Item.Description> By {authors.join(", ")} </Item.Description> )} {isbn && ( <Item.Extra> <Label>ISBN: {isbn}</Label> </Item.Extra> )} </Item.Content> </Item> ); }; ResultsListItem.propTypes = { result: PropTypes.object.isRequired, }; export default ResultsListItem;

For more search result customization and configuration, see Search page.

Further Reading

Last updated on