Table of Contents

Simple Related List Component

Allows editing and relating mediatype elements that are in some way related to the main mediatype of the page, displayed as a list. The related elements can be sorted as the administrator wants.

The React shell renders it as a table sitting in a sibling tab of the main Form Component. Each row has its own remove button; the toolbar has "Refresh" and "Add Related" (with sub-options Add New, Add Existing). When the list is filterable, a Filters chip-row sits above the table.

Simple Related List on a user edit page, showing two groups (Basic, OB Administrators)

Related docs.
System Overview — RenderV2 — the request that returns {schema, data} to this widget.
6.0/React Components — Default List Schema/Data — the React shell renders Related Lists using the same Schema/Data shape as standalone lists.
Form Component — the typical parent component sitting in the same page.
Ordered Related List — variant that adds drag-and-drop reordering.

What the React renderer gives you

  • Sortable columns (click the header).
  • Filter chip-row above the table (when filtering is enabled).
  • ID search at the top of the table (with operator dropdown and search button).
  • Server-driven pagination — page numbers, page-size selector.
  • "Refresh" toolbar button (toggle via SHOW_REFRESH_BUTTON).
  • "Add Related" toolbar button with sub-options:
    • "Add New" — opens the create page for the related mediatype (controlled by ALLOW_ADD_NEW).
    • "Add Existing" — opens a side selector to pick existing records (controlled by ALLOW_ADD_EXISTING).
  • Per-row remove button (controlled by ALLOW_REMOVE).
  • Optional "Clear All" button to remove all related items at once (controlled by ALLOW_CLEAR_ALL).
  • Header Actions and Item Actions — extension points that let a customer remote add buttons to the component header and to each row, gated by the backend. See the next section.

Header Actions and Item Actions

The Related List widget exposes two extension points for custom buttons that customer customisations can plug in:

Extension point Location in the UI Typical use
Header Action The component's top-right toolbar, next to Add Related / Refresh Operate on the whole list — refresh an external cache, bulk-publish, export to a custom format
Item Action Per-row, next to the trash icon Operate on one record — preview, run a workflow, send a notification, mirror a state change

The example below shows a single header action button (a circular arrow icon) added to the right of the Refresh button on a customer deployment's related list:

Related List header showing a custom Header Action button (circular arrow) added by a customer remote

How the contract works

The contract is defined in the React shell at packages/modules/src/modules/decorators/related-list/types.ts:

type ItemActions   = { [actionKey: string]: ElementType };
type HeaderActions = { [actionKey: string]: ElementType };

export type RelatedList = {
  events?: {
    delete?:        (id: number | string) => void;
    deleteAll?:     () => void;
    refresh?:       () => void;
    refreshing?:    boolean;
    onChange?:      (value: RelatedListValue) => void;
    revalidateView?: () => void;
  };
  itemActions?:   ItemActions;
  headerActions?: HeaderActions;
};

A customer remote's main entry exports a relatedList object with the actions registered under string keys:

// In a customer federated remote (any project's React remote)
// src/core-decorators/related-list/index.ts
import refreshUsersCache from './header-actions/refresh-users-cache';
import kickSession       from './item-actions/kick-session';
import previewClip       from './item-actions/preview-clip';

const relatedList = {
  headerActions: {
    refreshUsersCache,
  },
  itemActions: {
    kickSession,
    previewClip,
  },
};

export default relatedList;

The shell merges every remote's decorator at startup (via the useDecorators('relatedList') hook) and reads them at render time. Multiple remotes can register actions independently — each one deployed alongside the customer's own React widgets.

Backend gates visibility

A registered action is only rendered when the backend signals it should be active. This keeps permission and feature-flag logic on the C# side and lets the React decorator declaration stay static across deployments.

Header actions are filtered against schema.configuration[actionKey]:

// packages/core-components/src/components/related-list/components/header-actions/helpers/...
return Object.entries(actions)
  .filter(([name]) => Boolean(configuration[name]))   // ← schema flag
  .map(([, component]) => component);

So to render a header action registered under the key refreshUsersCache, the C# component must emit schema.configuration.refreshUsersCache = true for the page(s) where the action should appear, and not for other pages.

Item actions are filtered against the row's permissions map:

// packages/core-components/src/components/related-list/components/item-actions/helpers/permissions.ts
return Object.entries(actions)
  .filter(([key]) => permissions[key])                // ← row permission
  .map(([, component]) => component);

So kickSession appears on a row only when data.items[i].permissions.kickSession === true. This gives fine-grained per-row visibility — e.g. don't show Reset PIN on a row whose underlying record doesn't currently have a session active.

Example — Item Action in a populated row

Per-row item actions render to the right of each row, between the row's data and the standard remove button. Here's a Related List where every row carries a custom Reset PIN item action installed by a customer remote — visible as the small circular-arrow icon at the right edge of each row:

A Related List of user PINs — each row carries a per-row Item Action button (the small circular-arrow icon on the right) registered by a customer remote

Whether the button renders is gated by data.items[i].permissions[actionKey] from the backend (the section below covers the contract). In this example all three rows carry the flag, so the action is visible on every row; on rows where the backend omits the flag, the action button is simply absent.

Props the actions receive

Header Action props (from HeaderActions.tsx):

{
  schema:       RelatedListComponentSchema;
  events?:      Decorators['relatedList']['events'];
  componentKey?: string;
}

Item Action props (from ItemActions.tsx):

{
  schema: RelatedListComponentSchema;
  row:    RelatedListComponentItem;
  size?:  'small' | 'middle' | 'large';
  items:  RelatedListComponentItem[];
  events?: Decorators['relatedList']['events'];
}

The events object exposes the shell-level lifecycle hooks that actions typically call into:

  • refresh() — re-pull the component's data.
  • delete(id) / deleteAll() — remove rows.
  • onChange(value) — notify the shell of a value change (triggers setDirty(true) upstream).
  • revalidateView() — full re-validate of the parent form.

Example — a Header Action

The screenshot earlier in this section shows a "refresh users cache" header action installed by a customer deployment. The action is a small button that emits a refresh event on a shared event bus and shows a loading spinner while the work runs. A minimal implementation looks like:

// Example from a customer remote — registered as `refreshUsersCache`
import { type FC, useEffect, useState } from 'react';
import { Tooltip } from '@agilecontent/ui';
import { ReloadOutlined } from '@agilecontent/ui/icons';

import { Button } from './refresh.styled';
import {
  emitRefreshEvent,
  listenRefreshEventFor,
} from 'common/event-bus/refresh-event';
import { RefreshEventType } from 'common/enums';

// Tag the action so the event bus can scope its fan-out
const pageConfig = {
  pageKey:             '<your-page-key>',
  templateComponentKey: '<your-template-component-key>',
};

const RefreshUsersCache: FC = () => {
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const stop = listenRefreshEventFor(
      RefreshEventType.UsersCacheRefreshCompleted,
      pageConfig,
      () => setIsLoading(false),
    );
    return stop;
  }, []);

  const handleClick = () => {
    setIsLoading(true);
    emitRefreshEvent(RefreshEventType.UsersCacheRefresh, pageConfig);
  };

  return (
    <Tooltip placement="top" title="Refresh users cache">
      <Button icon={<ReloadOutlined />} loading={isLoading} onClick={handleClick} />
    </Tooltip>
  );
};

export default RefreshUsersCache;

The corresponding C# code must add refreshUsersCache: true to the schema's configuration for the relevant component on the relevant page(s) — typically by extending the MapSchema's configuration dictionary on a customer-specific subclass of the Related List component.

Example — an Item Action

A typical per-row action receives the row data and runs an API call against the underlying record. Minimal implementation:

// Example from a customer remote — registered as `kickSession`
import { type FC, useState } from 'react';
import { Tooltip, useModal } from '@agilecontent/ui';
import { DisconnectOutlined } from '@agilecontent/ui/icons';
import { useContentContext, type Decorators } from '@agilecontent/mib-modules';

import { Button } from './kick-session.styled';
import { Kick }   from './api';

type Props = {
  row:    Record<string, unknown>;
  size?:  'small' | 'middle' | 'large';
  events?: Decorators['relatedList']['events'];
};

const KickSession: FC<Props> = ({ row, size }) => {
  const [modal, modalContext] = useModal();
  const contentContext        = useContentContext();
  const [isLoading, setIsLoading] = useState(false);

  const userIds = contentContext.pageIds?.split(',').map(Number) ?? [];
  const userId  = userIds[0] ?? null;

  const handleClick = () => {
    setIsLoading(true);
    Kick(userId as number, row.DeviceId as string, row.DeviceType as number)
      .then(()  => modal.info({ title: 'Kicked', content: 'Session has been kicked' }))
      .catch(() => modal.info({ title: 'Error',  content: 'Could not kick session' }))
      .finally(() => setIsLoading(false));
  };

  return (
    <Tooltip placement="top" title="Kick session">
      {modalContext}
      <Button size={size} icon={<DisconnectOutlined />} loading={isLoading} onClick={handleClick} />
    </Tooltip>
  );
};

export default KickSession;

The corresponding C# side must include kickSession: true in data.items[i].permissions for every row where the action should be clickable — typically computed at render time based on row state and user permissions.

Registering an action — recipe

  1. Write the React component in your federated remote, under src/core-decorators/related-list/{header,item}-actions/<action-name>/.
  2. Re-export the component from your remote's src/core-decorators/related-list/index.ts under the matching headerActions or itemActions key.
  3. C# backend — extend a customer-specific subclass of the Related List component (or use a plugin) and add the matching flag:
    • Header action → schema.configuration[actionKey] = true.
    • Item action → data.items[i].permissions[actionKey] = true.
  4. Test in Storybook with mock data:
    <RelatedList
      schema={{ ...mockSchema, configuration: { refreshUsersCache: true } }}
      data={mockData}
    />
    
  5. Deploy the remote (nginx picks up the new remoteEntry.js) and deploy the C# customisation if you added schema or permission flags.

The contract is intentionally simple — actions are plain React components with one prop contract and a string registration key. The visibility-gating mechanism keeps coupling between the customisation remote and the C# layer to a single shared flag name per action.

Where this applies

These extension points are scoped to the 'related-list' decorator namespace in packages/modules/src/modules/decorators/related-list/types.ts, consumed by the core React RelatedList widget. They apply to:

  • The Simple Related List Component (this doc).
  • Any custom widget that opts into the same decorator by calling useDecorators('relatedList').

They do not apply to the Related Form component out of the box — Related Form is rendered through a different path (currently legacy MVC or a custom federated remote) and does not subscribe to this decorator. If you ship a custom relatedform widget, you can re-use this namespace or define your own.

Configuration Keys

These are all available Configuration Keys for the Related List Component.

The configuration must be inserted into the database table:

MIB3UX_PAGE_COMPONENT_CONFIGURATIONS

TITLE

The title rendered in the component header.

ALLOW_ADD_EXISTING

Boolean. True (default): the Add Existing action is enabled. False: hidden.

ALLOW_ADD_NEW

Boolean. True: the Add New action is enabled. False: hidden.

Note

This pairs with RelatedCopyType for the copy-with-relateds flow and affects its applicability:

  • RelatedCopyType.DoNotCopy / ShallowCopy — if AllowAddNew is true and the user lacks write permission on the MediaType, the data is not copied.
  • RelatedCopyType.DeepCopy — if AllowAddNew is false or the user lacks write permission, the data is not copied.

ALLOW_BULK_EDIT

Boolean. True (default): page item can be bulk-edited. False: bulk-edit is disabled for this component.

ALLOW_EDIT

Boolean. True (default): the page item can be edited. False: component edition is disabled regardless of user permissions.

ALLOW_REMOVE

Boolean. True (default): items can be removed via the per-row remove button. False: removal is disabled regardless of user permissions.

ALLOW_CLEAR_ALL

Boolean. True: the "Clear All" button is enabled. False (default): the button is hidden.

ASIDE_FIELDS

Specifies which columns are shown in the related aside (the side-pane opened when adding an existing related record).

CONFIGURATION_KEY:   ASIDE_FIELDS
CONFIGURATION_VALUE: ID,NAME,SOURCE,DATEINS,INSTANCE_ID,OWNER

ASIDE_FILTERS

Custom filters for items shown in the related aside.

CONFIGURATION_KEY:   ASIDE_FILTERS
CONFIGURATION_VALUE: { Field: 'GVP_GENRES[NAME]', Operator: 'EQUAL', Value: 'action' }

Per-field form:

CONFIGURATION_KEY:   ASIDE_FILTERS[<RELATED_FIELD>]
CONFIGURATION_VALUE: <FILTER1>,<FILTER2>

Dynamic values referencing a form field via [templateComponentKey.columnName] are supported. See the Form Component for the syntax detail.

BATCH_REFERENCE_IDENTIFIER

Sets the batch reference to something other than the component's default TemplateComponentKey.

DENY_ON_READONLY

Boolean. True: deny save when the user lacks permission on a related component or field. False: allow save even if the user lacks permission on some.

EXCLUDE_FIELDS

A comma-separated list of admField column names to exclude from the display.

CONFIGURATION_KEY:   EXCLUDEFIELDS
CONFIGURATION_VALUE: name,timezone,genres

HIDE_BULK_EDIT

Boolean. True: bulk-edit is disabled. False (default): bulk-edit is enabled.

IGNORE_ADMFIELDS

Boolean. True: related admFields of the page are not loaded. False (default): all related mediatypes associated with the main one are loaded.

For a movies list page that contains related fields source / genres / content-category, setting IGNORE_ADMFIELDS=true means the API pre-load will skip those related-field queries.

INCLUDE_FIELDS

A comma-separated list of admField column names. Forces the component to display only these fields.

IS_CHILD_REQUIRED

Boolean. True: force the user to add at least one register before saving. False (default): no requirement.

For a movie form with a Simple Related List of genres, setting this on the genre component requires at least one genre to be added before the page can be saved.

MEDIATYPE

The name of the mediatype loaded in the component.

PARENT_BATCH_REFERENCE_IDENTIFIER

Sets the parent batch reference to something other than the default ParentTemplateComponentKey.

PARENT_MEDIATYPE

Links the main mediatype of the page to the mediatype of this related component.

CONFIGURATION_KEY:   PARENT_MEDIA_TYPE
CONFIGURATION_VALUE: MVP_MOVIES

READONLY

Boolean. True: forces the component to be read-only regardless of user permissions. False (default): honour user permissions.

READONLY_FIELDS

A list of fields to be made read-only regardless of user permissions.

SORTING_COLUMNS

A comma-separated list of columns to sort the list by.

CONFIGURATION_KEY:   SORTINGCOLUMNS
CONFIGURATION_VALUE: ID,SOURCE

SORTING_TYPE

ASC (default — ascending) or DESC (descending). Ignored if SORTING_COLUMNS is not set.

HIDE_IMAGE_PREVIEW

Boolean. Hides the image preview of IMAGES admField cells.

Configurations Exclusive to React (MibReact)

The settings below apply only when the page is rendered through the React shell (~/app/...).

MAX_FILE_SIZE

Limits upload sizes when the related list has a file mediatype (e.g. Images). May also be set application-wide via MaxFileSizeDefaultValue.

MAX_FILE_SIZE[FieldName]

Limits upload sizes for related fields that point to file mediatypes.

Integer. Limits the maximum quantity of items that can be related.

SHOW_REFRESH_BUTTON

Boolean. Whether to display the "Refresh" toolbar button.

Boolean. Toggles the filtering functionality on the list.

String. Adds a footer message to the component. May contain HTML.

CONFIGURATION_KEY:   FOOTER_INFORMATION
CONFIGURATION_VALUE: <b>Footer Information</b>

ASYNC_MODE

Boolean. Default false. When true, pagination is handled by the backend — data is loaded dynamically as the user navigates pages.

Note

Has no effect when ALLOW_ROW_REORDER is enabled. In that case pagination always occurs on the front-end regardless of ASYNC_MODE.

REDIRECT_TO_EDIT_PAGE_BY_FIELD

Dictionary<string, string>. Defines a field whose value, when clicked, redirects to the edit page of the mediatype specified in the value.

CONFIGURATION_KEY:   REDIRECT_TO_EDIT_PAGE_BY_FIELD[ID]
CONFIGURATION_VALUE: react_movies_edit

REFRESH_JSON

Used exclusively by custom related lists that want to use the React refresh endpoint. Allows creating component-specific payloads that are sent on the refresh request — the customised JSON becomes PostbackComponentRequest.JsonData in the C# refresh method.

{
    "pageId": {ID},
    "refreshAction": "ExecuteCustomRefresh",
    "otherCustomProp": 123
}

Custom field validation

Starting with MIB 6.0, custom validation criteria can be implemented for field persistence. See Custom Validation plugin.

Schema and Data shape

The Simple Related List speaks the same Default List Schema

  • Default List Data shapes as the standalone List Component — the React shell renders both with the same widget.

The difference is at the BFF: a Related List's data is scoped to the records related to the parent form's current item, while a standalone List's data is the full mediatype query.

Source reference

React type-key (effective) relatedList — set via COMPONENT_VIEW_TYPE on the template-component row, since the raw class name resolves to simplerelatedlist which is not directly mapped in the shell's registry
BFF assembly MibServer3.Web
BFF class MibServer3.Web.Component.SimpleRelatedListComponent
MIB3UX_COMPONENTS.COMPONENT_KEY mib_default_simplerelatedlistcomponent
React widget source packages/core-components/src/components/related-list/RelatedList.tsx
Schema / data contract Default List Schema — Related Lists reuse the standalone List schema; row scoping is at the BFF

Custom backend, core React. Emit the Default List Schema + Data shape and set COMPONENT_VIEW_TYPE = 'relatedList' on the MIB3UX_TEMPLATE_COMPONENTS row that points at your custom class. The RelatedList.tsx widget handles all four related-list variants (Simple, Ordered, Related Form, Related Auto Complete) — the variant behavior is driven by flags in the schema (Reorderable, Aside, Autocomplete) rather than by different React components. So a single custom backend can render as any variant by setting the right schema flags.

See also