Deposit form
The deposit form enables users to create and edit records. It’s a React-based form application that provides field validation, dynamic sections, and integration with your record model.
Request and Application Flow
Request Flow
When a user accesses the deposit form URL:
Application Layers
The deposit form application is organized into server-side and client-side layers:
Server-Side Layers
| Layer | Component | Source | Purpose |
|---|---|---|---|
| View | RecordsUIResource.deposit_create/edit() | oarepo-ui | Handles HTTP request, authenticates user, fetches record/draft data |
| Components | run_components() | oarepo-ui | Executes component hooks to modify form_config and add context variables |
| Default Component | oarepo_ui.pages.DepositCreate/Edit | oarepo-ui | Pre-defined JinjaX page component with def block |
| Model Template | model_name/deposit_edit|create.html | nrp-model-copier | Model-specific template extending oarepo-ui base |
| Base Template | invenio_app_rdm/records/deposit.html | invenio-app-rdm | Provides page skeleton (CSS, JS, header, footer blocks) |
| Form Content | deposit/form.html | oarepo-ui | Renders hidden inputs with config and placeholder div for React app |
Client-Side Layers
| Layer | Component | Source | Purpose |
|---|---|---|---|
| Entry Point | {{model_name}}/semantic-ui/js/{{model_name}}/forms/index.js | Model-specific (nrp-model-copier ) | Webpack entry that mounts React app to #deposit-form div |
| Form App | DepositFormApp | oarepo-ui | Main React form component with providers, router, and context |
| Bootstrap | DepositBootstrap | invenio-rdm-records | Wraps form with notification providers and service setup |
| Form Layout | BaseFormLayout | oarepo-ui | Generic form layout component with overridable fields container |
| Fields Container | {{model_name}}/semantic-ui/js/{{model_name}}/forms/FormFieldsContainer.jsx | Model-specific (nrp-model-copier ) | Your custom component that defines form fields |
Configuration
UI Resource Routes
The deposit routes are defined in your model’s UI resource config:
from oarepo_ui.resources.records.config import RecordsUIResourceConfig
class MymodelUIResourceConfig(RecordsUIResourceConfig):
blueprint_name = "mymodel"
url_prefix = "/mymodel"
routes = {
"deposit_create": "/uploads/new",
"deposit_edit": "/uploads/<pid_value>",
# ... other routes
}Form Configuration
Form field definitions, labels, hints, and validation rules are derived from your record model’s YAML schema definition. See Model schema customization for details on defining field types, labels, hints, and help text.
Template Context
When no custom page template is configured, oarepo-ui uses the default page component (oarepo_ui.pages.DepositCreate or oarepo_ui.pages.DepositEdit). This component declares all context variables in a {# def #} block and extends your model’s base template.
For example, the default component for deposit_create page:
{# def
theme,
forms_config,
searchbar_config,
record,
community,
community_ui,
community_use_jinja_header,
files,
preselectedCommunity=None,
files_locked,
extra_context,
ui_links,
permissions,
webpack_entry,
#}
{% extends model_name ~ "/deposit_create.html" %}The default component extends your model’s base template (mymodel/deposit_create.html or mymodel/deposit_edit.html), which is provided by nrp-model-copier and extends oarepo_ui/base_deposit.html.
These context variables are available in your templates:
| Variable | Description |
|---|---|
theme | Theme configuration |
forms_config | Form configuration |
searchbar_config | Search bar configuration |
record | Current record data (for edit mode) |
community | Community data if applicable |
community_ui | Community UI data |
files | Record files entries |
extra_context | Additional context from resource components |
ui_links | UI links |
permissions | User permissions |
webpack_entry | Webpack entry point for the deposit form JavaScript |
Building Your Deposit Form
The nrp-model-copier template provides a starting point for your deposit form with only two example fields:
- Title - A text field in the “Basic information” section
- File uploads - A file uploader in the “Files upload” section
To create a form for your specific model, you typically need to build your own from this starting point.
Define Your Fields in the Model Schema
Add your fields to {{model_name}}/metadata.yaml. Field metadata (labels, hints, required status) is defined here:
# Definition of metadata for {{model_name}}
Metadata:
properties:
title:
type: fulltext+keyword
label:
en: Title
cs: Název
help:
en: The title of the record
cs: Název záznamu
# Add your custom fields here
description:
type: fulltext
label:
en: Description
cs: Popis
creators:
type: array
items:
type: vocabulary
vocabulary-type: creators
label:
en: Creators
cs: AutořiSee Model schema customization for details on defining field types, labels, hints, and help text.
Build Your Form in FormFieldsContainer
Edit ui/{{model_name}}/semantic-ui/js/{{model_name}}/forms/FormFieldsContainer.jsx to define your form fields organized into accordion sections:
import * as React from "react";
import { useFormConfig, TextField, ArrayField } from "@js/oarepo_ui/forms";
import { AccordionField } from "react-invenio-forms";
import { i18next } from "@translations/i18next";
import { UppyUploader } from "@js/invenio_rdm_records";
const FormFieldsContainerComponent = ({ record }) => {
const formConfig = useFormConfig();
const { filesLocked } = formConfig;
return (
<React.Fragment>
{/* Basic Information Section */}
<AccordionField
includesPaths={["metadata.title", "metadata.description"]}
active
label={i18next.t("Basic information")}
>
<TextField fieldPath="metadata.title" />
<TextField
fieldPath="metadata.description"
// Field metadata (label, hint) comes from model schema
/>
</AccordionField>
{/* Creators Section - Array Field */}
<AccordionField
includesPaths={["metadata.creators"]}
active
label={i18next.t("Creators")}
>
<ArrayField
fieldPath="metadata.creators"
addButtonLabel={i18next.t("Add creator")}
>
{/* Creator subfields */}
</ArrayField>
</AccordionField>
{/* Files Upload Section */}
<AccordionField
includesPaths={["files.enabled"]}
active
label={
<label htmlFor="files.enabled">{i18next.t("Files upload")}</label>
}
data-testid="filesupload-button"
>
<UppyUploader
isDraftRecord={!record.is_published}
config={formConfig}
quota={formConfig.quota}
filesLocked={filesLocked}
/>
</AccordionField>
</React.Fragment>
);
};Key concepts:
AccordionField- Groups related fields into collapsible sectionsincludesPaths- Array of field paths that trigger accordion expansion when modified, and ensures field errors display under the correct sectionactive- Whether section is expanded by defaultfieldPath- Dot-separated path to the field in the record data (e.g.,metadata.title)
Choose Field Components
Use the components documented in Deposit Form Components to add different field types:
| Component | Use for | Example |
|---|---|---|
TextField | Single-line/multi-line text input | metadata.title, metadata.name |
RichInputField | Rich text editor | metadata.description |
ArrayField | Arrays of items | metadata.creators, metadata.keywords |
VocabularyField | Vocabulary-selected items | metadata.resource_type |
EDTFDateRangePickerField | Date ranges | metadata.publication_date |
SelectField | Dropdown selections | Custom enumerations |
If no suitable field component exists, you can create custom form components.
Verify Entry Point Configuration
The webpack entry point (index.js) is automatically configured by the copier template. It registers your FormFieldsContainer as an override:
import { DepositFormApp, parseFormAppConfig } from "@js/oarepo_ui/forms";
import ReactDOM from "react-dom";
import { OARepoDepositSerializer } from "@js/oarepo_ui/api";
import FormFieldsContainer from "./FormFieldsContainer";
const overridableIdPrefix = "DepositForm";
const rootElem = document.getElementById("deposit-form");
const config = parseFormAppConfig(rootElem);
// Register your custom FormFieldsContainer
config.overridableComponents = {
[`${overridableIdPrefix}.FormFields.container`]: FormFieldsContainer,
};
ReactDOM.render(
<DepositFormApp config={config} serializer={OARepoDepositSerializer} />,
rootElem,
);You typically don’t need to modify the entry point - just update FormFieldsContainer.jsx with your form fields.
Use the Wizard Form Layout (Optional)
For complex forms with many fields, you can enable a wizard-style layout with vertical tabs. Enable wizard mode by adding the useWizardForm prop and providing sections:
ReactDOM.render(
<DepositFormApp
config={config}
{...rest}
sections={mySections}
recordSerializer={OARepoDepositSerializer}
componentOverrides={componentOverrides}
useWizardForm // Enable wizard layout
/>,
rootEl,
);How the Tab Form Works
- Free navigation — users can switch between tabs at any time without being required to complete or validate the current section first.
- Auto-save on tab switch — if the form has unsaved changes when switching tabs, it automatically saves the draft. This also triggers a fresh round of validation errors from the backend, so validation feedback is up to date.
- URL persistence — the active tab is reflected in the URL via the
?tab=keyquery parameter, so users can bookmark or share a link that opens directly on a specific section.
Defining Sections
Each section is a plain JavaScript object. The recommended approach is to define each section in its own file, then assemble them into an ordered array. This mirrors how ccmm-invenio organizes its built-in sections.
A single section file looks like this:
import React from "react";
import { i18next } from "@translations/i18next";
import { UppyUploader } from "@js/invenio_rdm_records";
export const FilesSection = {
key: "files",
label: i18next.t("Upload files"),
component: ({ record, formConfig }) => {
const { filesLocked } = formConfig.config;
return (
<UppyUploader
isDraftRecord={!record.is_published}
config={formConfig}
quota={formConfig.quota}
filesLocked={filesLocked}
/>
);
},
includesPaths: ["files.enabled"],
};Then collect all sections into an array in one place and pass it to DepositFormApp:
import { BasicInformationSection } from "./BasicInformationSection";
import { CreatorsSection } from "./CreatorsSection";
import { FilesSection } from "./FilesSection";
export const mySections = [
BasicInformationSection,
CreatorsSection,
FilesSection,
];import { mySections } from "./sections";
ReactDOM.render(
<DepositFormApp
config={config}
sections={mySections}
recordSerializer={OARepoDepositSerializer}
useWizardForm
/>,
rootEl,
);Section object properties:
| Property | Type | Description |
|---|---|---|
key | string | Unique identifier for the section (used in URL as ?tab=key) |
label | string | Display label shown in the tab menu |
component | function | Called as component({ record, formConfig, activeStep, next, back, initialRecord }), returns JSX |
includesPaths | string[] | Field paths for error tracking — errors in these fields show indicators on this tab |
Props available to component:
| Prop | Type | Description |
|---|---|---|
record | object | The current record/draft data. Use to read existing field values (e.g. record.ui.languages, record.is_published) |
formConfig | object | The form configuration object. Contains formConfig.config.vocabularies, formConfig.config.filesLocked, formConfig.quota, formConfig.overridableIdPrefix, etc. |
activeStep | number | Zero-based index of the currently active section tab |
next | function | Call next() to programmatically advance to the next tab |
back | function | Call back() to programmatically navigate to the previous tab |
initialRecord | object | The record data as it was when the form first loaded, before any user edits. Useful for comparing current state to the saved state |
Using CCMM sections
If during model creation you chose ccmm-invenio, your form will already be provided with prepared sections for this model.
Adding Custom Sections
import { CCMMSections } from "@js/ccmm_invenio/forms";
import { TextField } from "@js/oarepo_ui/forms";
import { i18next } from "@translations/i18next";
// Start with the shared sections and add your own at the end
export const mySections = [...CCMMSections, MyNewSection];Adding Content to Every Section via Override
To add inputs or content at the bottom of every section (or specific sections), use the Overridable pattern with the section key:
import { Overridable } from "react-overridable";
// Override for a specific section
const BasicInfoExtra = ({ section, activeStep }) => (
<div className="ui segment">
<p>Additional content for {section.label}</p>
</div>
);
const componentOverrides = {
[`${overridableIdPrefix}.FormFields.container`]: FormFieldsContainer,
// Add content below the "general-information" section
[`${overridableIdPrefix}.TabForm.TabContent.general-information`]:
BasicInfoExtra,
};The override component receives these props:
section- The current section objectactiveStep- Current step indexnext- Function to navigate to next stepback- Function to navigate to previous step
Reorganizing CCMM Sections
The CCMMSections export is a fixed ordered array. If you need a different order, or want to omit some sections, you cannot simply sort or filter the imported array — you need to import the individual section objects and assemble your own array:
import {
CCMMFiles,
CCMMGeneralInformation,
CCMMFunding,
CCMMAlternativeIdentifiers,
CCMMRelatedWorks,
CCMMCommunityAndAccess,
} from "@js/ccmm_invenio/forms";
import { MyCustomSection } from "./MyCustomSection";
// Re-order, omit, or insert sections as needed
export const mySections = [
CCMMGeneralInformation, // moved to first
CCMMFiles,
MyCustomSection, // inserted between built-in sections
CCMMFunding,
CCMMRelatedWorks,
// CCMMAlternativeIdentifiers omitted entirely
// CCMMCommunityAndAccess omitted entirely
];Overriding Individual Fields Within a Section
Each field inside a CCMM section is wrapped in an <Overridable> component, which means you can replace or adjust it via componentOverrides in your entry point without touching the section file itself.
The overridable ID for a field follows the pattern {overridableIdPrefix}.FieldName — for example, {overridableIdPrefix}.Files for the file uploader in CCMMFiles. The prefix comes from config.overridableIdPrefix returned by parseFormAppConfig().
Hiding a field — return null from the override to remove it from the UI entirely:
const { rootEl, config, ...rest } = parseFormAppConfig();
const overridablePrefix = config.overridableIdPrefix;
const componentOverrides = {
// Remove the file uploader section content completely
[`${overridablePrefix}.Files`]: () => null,
// Remove the version field from General Information
[`${overridablePrefix}.Version`]: () => null,
};Modifying field props — render the original component with adjusted props. The override receives the full tabConfig spread (i.e. record, formConfig, activeStep, next, back, initialRecord) as props:
import { UppyUploader } from "@js/invenio_rdm_records";
const { rootEl, config, ...rest } = parseFormAppConfig();
const overridablePrefix = config.overridableIdPrefix;
const componentOverrides = {
// Replace the file uploader with a version that locks files
[`${overridablePrefix}.Files`]: ({ record, formConfig }) => (
<UppyUploader
isDraftRecord={!record.is_published}
config={formConfig}
quota={formConfig.quota}
decimalSizeDisplay={formConfig.decimal_size_display}
allowEmptyFiles={formConfig.allow_empty_files}
fileUploadConcurrency={formConfig.file_upload_concurrency}
showMetadataOnlyToggle={false}
filesLocked={true} // override: always lock files
/>
),
};Pass componentOverrides to DepositFormApp:
ReactDOM.render(
<DepositFormApp
config={config}
{...rest}
sections={mySections}
recordSerializer={OARepoDepositSerializer}
componentOverrides={componentOverrides}
useWizardForm
/>,
rootEl,
);These are front-end only changes. If you are running a ccmm-invenio model, it
also has a backend part that expects certain data to be submitted from the
form. Removing a field from the UI that is required by ccmm-invenio means
there will be no way to fill in that value, and the record cannot be
published. Similarly, if you add extra sections with new fields, those fields
must also be declared in the backend model schema (e.g.
datasets/metadata.yaml).
useFieldData Helper
The useFieldData hook from oarepo_ui/forms provides access to field metadata (labels, help text, hints, required status, icon) defined in your model’s YAML schema. This metadata is extracted during model compilation and made available to React components.
Usage
import { useFieldData } from "@js/oarepo_ui/forms";
const MyField = ({ fieldPath }) => {
const { getFieldData } = useFieldData();
const { helpText, label, placeholder, required } = getFieldData({
fieldPath: fieldPath,
fieldRepresentation: "text", // "full", "compact", or "text"
icon: "pencil", // optional icon
});
return (
<div>
{/* label is a React node in "full"/"compact" modes or string in "text" mode */}
<label>{label}</label>
<input name={fieldPath} placeholder={placeholder} />
{helpText && <small>{helpText}</small>}
</div>
);
};getFieldData Options
| Option | Type | Default | Description |
|---|---|---|---|
fieldPath | string | (required) | The path to the field in the metadata |
fieldRepresentation | string | "full" | How to render the label: "full", "compact", or "text" |
icon | string | "pencil" | Icon to display with the field |
fullLabelClassName | string | - | CSS class for full mode label |
compactLabelClassName | string | - | CSS class for compact mode label |
fieldPathPrefix | string | (auto-set) | Prefix for nested fields, configured by provider |
ignorePrefix | boolean | false | Whether to ignore the prefix |
Return Values by Representation Mode
| Mode | label | helpText | Other properties |
|---|---|---|---|
| full | React <FieldLabel> component | helpText string | placeholder, required, detail |
| compact | React <CompactFieldLabel> with popup help | (in popup) | placeholder, required, detail |
| text | Plain string | helpText string | labelIcon, placeholder, required, detail |
All modes return an object with helpText, label, placeholder, required, detail (plus labelIcon in text mode).
Create Custom Form Components
If the available form components don’t meet your needs, you can create custom fields using these patterns:
- Wrapping
react-invenio-formscomponents - Use when extending standard form fields - Building from scratch with Formik - Use for completely custom UI not based on
react-invenio-forms
Pattern 1: Wrapping Base Components
Here’s an example of creating an UppercaseField by wrapping the standard TextField from react-invenio-forms and adding custom behavior:
import React from "react";
import { TextField as InvenioTextField } from "react-invenio-forms";
import { useFieldData } from "@js/oarepo_ui/forms";
import { getIn, useFormikContext } from "formik";
export const UppercaseField = ({
fieldPath,
fieldRepresentation = "full",
icon = "arrow up",
...rest
}) => {
const { setFieldTouched, setFieldValue, values } = useFormikContext();
const { getFieldData } = useFieldData();
return (
<InvenioTextField
optimized
fieldPath={fieldPath}
{...getFieldData({ fieldPath, fieldRepresentation, icon })}
onBlur={() => {
const currentValue = getIn(values, fieldPath);
// Convert to uppercase on field blur
if (typeof currentValue === "string") {
setFieldValue(fieldPath, currentValue.toUpperCase());
}
setFieldTouched(fieldPath, true);
}}
{...rest}
/>
);
};Key elements:
| Element | Purpose |
|---|---|
InvenioTextField | Base component from react-invenio-forms with Formik integration |
optimized | Uses FastField for better performance - only re-renders when this field changes |
getFieldData() | Gets label, helpText, placeholder, required, detail from model YAML |
useFormikContext() | Access setFieldValue, setFieldTouched, and form values for custom blur behavior |
getIn() | Safely access nested field values from form state |
Pattern 2: Building from Scratch with Formik
For custom UI not based on react-invenio-forms components, use Formik directly (same pattern as react-invenio-forms TextField ):
import React from "react";
import { FastField, Field } from "formik";
import PropTypes from "prop-types";
import { Form, Popup } from "semantic-ui-react";
import { useFieldData } from "@js/oarepo_ui/forms";
const CustomField = ({
fieldPath,
fieldRepresentation = "text",
icon = "pencil",
disabled = false,
optimized = true,
// Props from getFieldData can be overridden here
label,
helpText,
required,
placeholder,
...uiProps
}) => {
const { getFieldData } = useFieldData();
// Get field metadata from model YAML, with props as overrides
const fieldData = getFieldData({ fieldPath, fieldRepresentation, icon });
const computedLabel = label ?? fieldData.label;
const computedHelpText = helpText ?? fieldData.helpText;
const computedRequired = required ?? fieldData.required;
const computedPlaceholder = placeholder ?? fieldData.placeholder;
const FormikField = optimized ? FastField : Field;
return (
<>
<FormikField name={fieldPath}>
{({ field, meta }) => {
// Resolve error to display
const computedError =
meta.error || (!meta.touched && meta.initialError);
let formInputError = null;
if (typeof computedError === "string") {
formInputError = computedError;
} else if (
typeof computedError === "object" &&
computedError.message
) {
// Error object with severity/message - show as popup
formInputError = (
<Popup
trigger={<span>{computedError.message}</span>}
content={computedError.description}
position="top center"
/>
);
}
return (
<Form.Input
{...field}
error={formInputError}
disabled={disabled}
fluid
label={computedLabel}
required={computedRequired}
placeholder={computedPlaceholder}
icon={icon}
id={fieldPath}
{...uiProps}
/>
);
}}
</FormikField>
{/* Optional help text below field */}
{computedHelpText && (
<label className="helptext">{computedHelpText}</label>
)}
</>
);
};
CustomField.propTypes = {
fieldPath: PropTypes.string.isRequired,
fieldRepresentation: PropTypes.string,
icon: PropTypes.string,
disabled: PropTypes.bool,
optimized: PropTypes.bool,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
helpText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
required: PropTypes.bool,
placeholder: PropTypes.string,
};
export default CustomField;Key elements explained:
| Element | Purpose |
|---|---|
FastField / Field | Formik component for form state. Use FastField (optimized mode) for better performance - only re-renders when this specific field changes |
optimized prop | When true, uses FastField. Set to false only if your field depends on other form values |
getFieldData() | Retrieves label, helpText, placeholder, required, detail from your model’s YAML schema definition |
field prop | Spread Formik’s field props (value, onChange, onBlur, name) onto input |
meta prop | Access touched, error, initialError for validation display |
computedError | Combines Formik error with optional external error prop |
| Error handling | Supports both string errors and structured objects with severity/message/description |
| Props override | label, helpText, required, placeholder props override values from getFieldData() |
Using Custom Fields
Import and use your custom fields in FormFieldsContainer.jsx:
import { CustomField } from "./CustomField";
import { UppercaseField } from "./UppercaseField";
const FormFieldsContainerComponent = ({ record }) => {
return (
<React.Fragment>
<AccordionField includesPaths={["metadata.custom"]} active label="Custom">
<CustomField
fieldPath="metadata.custom"
icon="tag"
fieldRepresentation="full"
// Override label from schema if needed
label="Custom Field Label"
/>
{/* Field that automatically converts input to uppercase */}
<UppercaseField
fieldPath="metadata.code"
fieldRepresentation="compact"
/>
</AccordionField>
</React.Fragment>
);
};Optional: Custom Page Templates
Add a submission deadline banner that appears when a deadline is approaching:
Step 1: Create the JinjaX component
{# def record, extra_context, model_name, ... #}
{% extends model_name ~ "/deposit_create.html" %}Step 2: Create the template
{% extends "oarepo_ui/deposit_create.html" %}
{% block page_body %}
{% if extra_context.deadline %}
<div class="ui message warning">
<i class="icon clock"></i>
{{ _("Submission deadline:") }} {{ extra_context.deadline }}
</div>
{% endif %}
{{ super() }}
{% endblock %}Step 2: Register the template
Register in your UI resource config using dot notation:
class MymodelUIResourceConfig(RecordsUIResourceConfig):
# ...
templates = {
"deposit_create": "mymodel.DepositCreate",
"deposit_edit": "mymodel.DepositEdit",
}Note the dot notation: mymodel.DepositCreate refers to the JinjaX component
at mymodel/DepositCreate.jinja.
JinjaX Component Required: When using templates configuration, you must create a valid JinjaX page component (e.g., mymodel/DepositCreate.jinja) with a {# def #} block declaring all context variables. The component should extend your model’s base template. Without the {# def #} block, the template will fail to render.
Custom JavaScript
Add model-specific JavaScript to the deposit form without overriding the entire template:
Create ui/mymodel/templates/semantic-ui/mymodel/deposit_create/javascript.html:
<script>
// Custom validation before form submission
document.addEventListener('DOMContentLoaded', function() {
console.log('Custom deposit form behavior');
});
</script>The base template automatically includes this file if it exists at {model_name}/deposit_create/javascript.html.
Optional: Resource Components
Use resource component hooks to modify form configuration or add template context:
from oarepo_ui.resources.components import UIResourceComponent
class ExtraContextComponent(UIResourceComponent):
def before_ui_edit(self, resource, request, extra_context, **kwargs):
"""Add custom context to edit page."""
record = record_from_resource(request)
extra_context["record_history"] = self._get_history(record)Register in your UI resource config:
class MymodelUIResourceConfig(RecordsUIResourceConfig):
# ...
components = [ExtraContextComponent]