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:
| Component | Purpose | Starter File | Documentation |
|---|---|---|---|
| Record detail | Displays individual record | ui/{{model_name}}/templates/semantic-ui/{{model_name}}/record_detail/main.html | Record landing page |
| Deposit form | Form for creating/editing records | ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js (plus section files you add yourself) | Deposit form |
| Search result | How records appear in search | ui/{{model_name}}/semantic-ui/js/{{model_name}}/search/ResultsListItem.jsx | Search 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 watchFor 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:
# 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: AuthorsStep 2: Edit the starter template
Edit 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:
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:
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:
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,
];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
| Component | Best For | Documentation |
|---|---|---|
TextField | Simple text input | TextField |
RichInputField | Rich text/description | RichInputField |
ArrayField | Repeating fields | ArrayField |
SelectField | Dropdown selection | SelectField |
DateField | Date/time pickers | DateField |
BooleanField | Yes/no toggles | BooleanField |
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:
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
# 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: AbstractFor more on defining metadata schema, see Metadata.
Record Detail Template
{% 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
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
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
Detailed record page architecture and available override blocks
Record landing pageDeposit form architecture and configuration
Deposit formAvailable form field components reference
Deposit form componentsSearch page architecture and configuration
Search pageBackend search customization (facets, mappings, analyzers)
Search configurationSearch UI component reference
Search UI components