Skip to main content

Admin Panel API for plugins

🏗 Work in progress

The content of this page might not be fully up-to-date with Strapi 5 yet.

A Strapi plugin can interact with both the back end and the front end of a Strapi application. The Admin Panel API is about the front end part, i.e. it allows a plugin to customize Strapi's admin panel.

The admin panel is a React application that can embed other React applications. These other React applications are the admin parts of each Strapi plugin.

☑️ Prerequisites

You have created a Strapi plugin.

The Admin Panel API includes:

✏️ Note

The whole code for the admin panel part of your plugin could live in the /strapi-admin.js|ts or /admin/src/index.js|ts file. However, it's recommended to split the code into different folders, just like the structure created by the strapi generate plugin CLI generator command.

Entry file

The entry file for the Admin Panel API is [plugin-name]/admin/src/index.js. This file exports the required interface, with the following functions available:

Function typeAvailable functions
Lifecycle functions
Async functionregisterTrads

Lifecycle functions

register()

Type: Function

This function is called to load the plugin, even before the app is actually bootstrapped. It takes the running Strapi application as an argument (app).

Within the register function, a plugin can:

registerPlugin()

Type: Function

Registers the plugin to make it available in the admin panel.

This function returns an object with the following parameters:

ParameterTypeDescription
idStringPlugin id
nameStringPlugin name
injectionZonesObjectDeclaration of available injection zones
✏️ Note

Some parameters can be imported from the package.json file.

Example:

my-plugin/admin/src/index.js

// Auto-generated component
import PluginIcon from './components/PluginIcon';
import pluginId from './pluginId'

export default {
register(app) {
app.addMenuLink({
to: `/plugins/${pluginId}`,
icon: PluginIcon,
intlLabel: {
id: `${pluginId}.plugin.name`,
defaultMessage: 'My plugin',
},
Component: async () => {
const component = await import(/* webpackChunkName: "my-plugin" */ './pages/App');

return component;
},
permissions: [], // array of permissions (object), allow a user to access a plugin depending on its permissions
});
app.registerPlugin({
id: pluginId,
name,
});
},
};

bootstrap()

Type: Function

Exposes the bootstrap function, executed after all the plugins are registered.

Within the bootstrap function, a plugin can:

Example:

module.exports = () => {
return {
// ...
bootstrap(app) {
// execute some bootstrap code
app.getPlugin('content-manager').injectComponent('editView', 'right-links', { name: 'my-compo', Component: () => 'my-compo' })
},
};
};

Async function

While register() and bootstrap() are lifecycle functions, registerTrads() is an async function.

registerTrads()

Type: Function

To reduce the build size, the admin panel is only shipped with 2 locales by default (en and fr). The registerTrads() function is used to register a plugin's translations files and to create separate chunks for the application translations. It does not need to be modified.

Example: Register a plugin's translation files
export default {
async registerTrads({ locales }) {
const importedTrads = await Promise.all(
locales.map(locale => {
return import(
/* webpackChunkName: "[pluginId]-[request]" */ `./translations/${locale}.json`
)
.then(({ default: data }) => {
return {
data: prefixPluginTranslations(data, pluginId),
locale,
};
})
.catch(() => {
return {
data: {},
locale,
};
});
})
);

return Promise.resolve(importedTrads);
},
};

Available actions

The Admin Panel API allows a plugin to take advantage of several small APIs to perform actions. Use this table as a reference:

ActionAPI to useFunction to useRelated lifecycle function
Add a new link to the main navigationMenu APIaddMenuLink()register()
Create a new settings sectionSettings APIcreateSettingSection()register()
Declare an injection zoneInjection Zones APIregisterPlugin()register()
Add a reducerReducers APIaddReducers()register()
Create a hookHooks APIcreateHook()register()
Add a single link to a settings sectionSettings APIaddSettingsLink()bootstrap()
Add multiple links to a settings sectionSettings APIaddSettingsLinks()bootstrap()
Inject a Component in an injection zoneInjection Zones APIinjectComponent()bootstrap()
Register a hookHooks APIregisterHook()bootstrap()
💡 Replacing the WYSIWYG

The WYSIWYG editor can be replaced by taking advantage of custom fields, for instance using the CKEditor custom field plugin.

👀 Info

The admin panel supports dotenv variables.

All variables defined in a .env file and prefixed by STRAPI_ADMIN_ are available while customizing the admin panel through process.env.

The Menu API allows a plugin to add a new link to the main navigation through the addMenuLink() function with the following parameters:

ParameterTypeDescription
toStringPath the link should point to
iconReact ComponentIcon to display in the main navigation
intlLabelObjectLabel for the link, following the React Int'l convention, with:
  • id: id used to insert the localized label
  • defaultMessage: default label for the link
ComponentAsync functionReturns a dynamic import of the plugin entry point
permissionsArray of ObjectsPermissions declared in the permissions.js file of the plugin
positionIntegerPosition in the menu
licenseOnlyBooleanIf set to true, adds a lightning ⚡️ icon next to the icon or menu entry to indicate that the feature or plugin requires a paid license.
(Defaults to false)
✏️ Note

intlLabel.id are ids used in translation files ([plugin-name]/admin/src/translations/[language].json)

Example:

my-plugin/admin/src/index.js
import PluginIcon from './components/PluginIcon';

export default {
register(app) {
app.addMenuLink({
to: '/plugins/my-plugin',
icon: PluginIcon,
intlLabel: {
id: 'my-plugin.plugin.name',
defaultMessage: 'My plugin',
},
Component: () => 'My plugin',
permissions: [], // permissions to apply to the link
position: 3, // position in the menu
licenseOnly: true, // mark the feature as a paid one not available in your license
});
app.registerPlugin({ ... });
},
bootstrap() {},
};

Settings API

The Settings API allows:

✏️ Note

Adding a new section happens in the register lifecycle while adding links happens during the bootstrap lifecycle.

All functions accept links as objects with the following parameters:

ParameterTypeDescription
idStringReact id
toStringPath the link should point to
intlLabelObjectLabel for the link, following the React Int'l convention, with:
  • id: id used to insert the localized label
  • defaultMessage: default label for the link
ComponentAsync functionReturns a dynamic import of the plugin entry point
permissionsArray of ObjectsPermissions declared in the permissions.js file of the plugin
licenseOnlyBooleanIf set to true, adds a lightning ⚡️ icon next to the icon or menu entry to indicate that the feature or plugin requires a paid license.
(Defaults to false)

createSettingSection()

Type: Function

Create a new settings section.

The function takes 2 arguments:

ArgumentTypeDescription
first argumentObjectSection label:
  • id (String): section id
  • intlLabel (Object): localized label for the section, following the React Int'l convention, with:
    • id: id used to insert the localized label
    • defaultMessage: default label for the section
second argumentArray of ObjectsLinks included in the section
✏️ Note

intlLabel.id are ids used in translation files ([plugin-name]/admin/src/translations/[language].json)

Example:

my-plugin/admin/src/index.js

const myComponent = async () => {
const component = await import(
/* webpackChunkName: "users-providers-settings-page" */ './pages/Providers'
);

return component;
};

export default {
register(app) {
app.createSettingSection(
{ id: String, intlLabel: { id: String, defaultMessage: String } }, // Section to create
[
// links
{
intlLabel: { id: String, defaultMessage: String },
id: String,
to: String,
Component: myComponent,
permissions: Object[],
},
]
);
},
};

Type: Function

Add a unique link to an existing settings section.

Example:

my-plugin/admin/src/index.js

const myComponent = async () => {
const component = await import(
/* webpackChunkName: "users-providers-settings-page" */ './pages/Providers'
);

return component;
};

export default {
bootstrap(app) {
// Adding a single link
app.addSettingsLink(
'global', // id of the section to add the link to
{
intlLabel: { id: String, defaultMessage: String },
id: String,
to: String,
Component: myComponent,
permissions: Object[],
licenseOnly: true, // mark the feature as a paid one not available in your license
}
)
}
}

Type: Function

Add multiple links to an existing settings section.

Example:

my-plugin/admin/src/index.js

const myComponent = async () => {
const component = await import(
/* webpackChunkName: "users-providers-settings-page" */ './pages/Providers'
);

return component;
};

export default {
bootstrap(app) {
// Adding several links at once
app.addSettingsLinks(
'global', // id of the section to add the link in
[{
intlLabel: { id: String, defaultMessage: String },
id: String,
to: String,
Component: myComponent,
permissions: Object[],
licenseOnly: true, // mark the feature as a paid one not available in your license
}]
)
}
}

Injection Zones API

Injection zones refer to areas of a view's layout where a plugin allows another to inject a custom React component (e.g. a UI element like a button).

Plugins can use:

✏️ Note

Injection zones are defined in the register() lifecycle but components are injected in the bootstrap() lifecycle.

Using predefined injection zones

Strapi admin panel comes with predefined injection zones so components can be added to the UI of the Content Manager:

ViewInjection zone name & Location
List view
  • actions: sits between Filters and the cogs icon
  • deleteModalAdditionalInfos(): sits at the bottom of the modal displayed when deleting items
Edit view
  • informations: sits at the top right of the edit view
  • right-links: sits between "Configure the view" and "Edit" buttons

Creating a custom injection zone

To create a custom injection zone, declare it as a <InjectionZone /> React component with an area prop that takes a string with the following naming convention: plugin-name.viewName.injectionZoneName.

Injecting components

A plugin has 2 different ways of injecting a component:

  • to inject a component from a plugin into another plugin's injection zones, use the injectComponent() function
  • to specifically inject a component into one of the Content Manager's predefined injection zones, use the getPlugin('content-manager').injectComponent() function instead

Both the injectComponent() and getPlugin('content-manager').injectComponent() methods accept the following arguments:

ArgumentTypeDescription
first argumentStringThe view where the component is injected
second argumentStringThe zone where the component is injected
third argumentObjectAn object with the following keys:
  • name (string): the name of the component
  • Component (function or class): the React component to be injected
Example: Inject a component in the informations box of the Edit View of the Content Manager:
my-plugin/admin/src/index.js

export default {
bootstrap(app) {
app.getPlugin('content-manager').injectComponent()('editView', 'informations', {
name: 'my-plugin-my-compo',
Component: () => 'my-compo',
});
}
}
Example: Creating a new injection zone and injecting it from a plugin to another one:
my-plugin/admin/src/injectionZones.js
// Use the injection zone in a view

import { InjectionZone } from '@strapi/helper-plugin';

const HomePage = () => {
return (
<main>
<h1>This is the homepage</h1>
<InjectionZone area="my-plugin.homePage.right" />
</main>
);
};
my-plugin/admin/src/index.js
// Declare this injection zone in the register lifecycle of the plugin

export default {
register() {
app.registerPlugin({
// ...
injectionZones: {
homePage: {
right: []
}
}
});
},
}
my-other-plugin/admin/src/index.js
// Inject the component from a plugin in another plugin

export default {
register() {
// ...
},
bootstrap(app) {
app.getPlugin('my-plugin').injectComponent('homePage', 'right', {
name: 'my-other-plugin-component',
Component: () => 'This component is injected',
});
}
};

Accessing data with the useCMEditViewDataManager React hook

Once an injection zone is defined, the component to be injected in the Content Manager can have access to all the data of the Edit View through the useCMEditViewDataManager React hook.

Example of a basic component using the 'useCMEditViewDataManager' hook
import { useCMEditViewDataManager } from '@strapi/helper-plugin';

const MyCompo = () => {
const {
createActionAllowedFields: [], // Array of fields that the user is allowed to edit
formErrors: {}, // Object errors
readActionAllowedFields: [], // Array of field that the user is allowed to edit
slug: 'api::address.address', // Slug of the content-type
updateActionAllowedFields: [],
allLayoutData: {
components: {}, // components layout
contentType: {}, // content-type layout
},
initialData: {},
isCreatingEntry: true,
isSingleType: true,
status: 'resolved',
layout: {}, // Current content-type layout
hasDraftAndPublish: true,
modifiedData: {},
onPublish: () => {},
onUnpublish: () => {},
addComponentToDynamicZone: () => {},
addNonRepeatableComponentToField: () => {},
addRelation: () => {},
addRepeatableComponentToField: () => {},
moveComponentDown: () => {},
moveComponentField: () => {},
moveComponentUp: () => {},
moveRelation: () => {},
onChange: () => {},
onRemoveRelation: () => {},
removeComponentFromDynamicZone: () => {},
removeComponentFromField: () => {},
removeRepeatableField: () => {},
} = useCMEditViewDataManager()

return null
}

Reducers API

Reducers are Redux reducers that can be used to share state between components. Reducers can be useful when:

  • Large amounts of application state are needed in many places in the application.
  • The application state is updated frequently.
  • The logic to update that state may be complex.

Reducers can be added to a plugin interface with the addReducers() function during the register lifecycle.

A reducer is declared as an object with this syntax:

Example:

my-plugin/admin/src/index.js
import { exampleReducer } from './reducers'

const reducers = {
// Reducer Syntax
[`${pluginId}_exampleReducer`]: exampleReducer
}

export default {
register(app) {
app.addReducers(reducers)
},
bootstrap() {},
};


Hooks API

The Hooks API allows a plugin to create and register hooks, i.e. places in the application where plugins can add personalized behavior.

Hooks should be registered during the bootstrap lifecycle of a plugin.

Hooks can then be run in series, in waterfall or in parallel:

  • runHookSeries returns an array corresponding to the result of each function executed, ordered
  • runHookParallel returns an array corresponding to the result of the promise resolved by the function executed, ordered
  • runHookWaterfall returns a single value corresponding to all the transformations applied by the different functions starting with the initial value args.
Example: Create a hook in a plugin and use it in another plugin
my-plugin/admin/src/index.js
// Create a hook in a plugin
export default {
register(app) {
app.createHook('My-PLUGIN/MY_HOOK');
}
}

my-other-plugin/admin/src/index.js
// Use the hook in another plugin
export default {
bootstrap(app) {
app.registerHook('My-PLUGIN/MY_HOOK', (...args) => {
console.log(args)

// important: return the mutated data
return args
});

app.registerPlugin({...})
}
}

Predefined hooks

Strapi includes a predefined Admin/CM/pages/ListView/inject-column-in-table hook that can be used to add or mutate a column of the List View of the Content Manager:

runHookWaterfall(INJECT_COLUMN_IN_TABLE, {
displayedHeaders: ListFieldLayout[],
layout: ListFieldLayout,
});
interface ListFieldLayout {
/**
* The attribute data from the content-type's schema for the field
*/
attribute: Attribute.Any | { type: 'custom' };
/**
* Typically used by plugins to render a custom cell
*/
cellFormatter?: (
data: Document,
header: Omit<ListFieldLayout, 'cellFormatter'>,
{ collectionType, model }: { collectionType: string; model: string }
) => React.ReactNode;
label: string | MessageDescriptor;
/**
* the name of the attribute we use to display the actual name e.g. relations
* are just ids, so we use the mainField to display something meaninginful by
* looking at the target's schema
*/
mainField?: string;
name: string;
searchable?: boolean;
sortable?: boolean;
}

interface ListLayout {
layout: ListFieldLayout[];
components?: never;
metadatas: {
[K in keyof Contracts.ContentTypes.Metadatas]: Contracts.ContentTypes.Metadatas[K]['list'];
};
options: LayoutOptions;
settings: LayoutSettings;
}

type LayoutOptions = Schema['options'] & Schema['pluginOptions'] & object;

interface LayoutSettings extends Contracts.ContentTypes.Settings {
displayName?: string;
icon?: never;
}

Strapi also includes a Admin/CM/pages/EditView/mutate-edit-view-layout hook that can be used to mutate the Edit View of the Content Manager:

interface EditLayout {
layout: Array<Array<EditFieldLayout[]>>;
components: {
[uid: string]: {
layout: Array<EditFieldLayout[]>;
settings: Contracts.Components.ComponentConfiguration['settings'] & {
displayName?: string;
icon?: string;
};
};
};
metadatas: {
[K in keyof Contracts.ContentTypes.Metadatas]: Contracts.ContentTypes.Metadatas[K]['edit'];
};
options: LayoutOptions;
settings: LayoutSettings;
}

interface EditFieldSharedProps extends Omit<InputProps, 'hint' | 'type'> {
hint?: string;
mainField?: string;
size: number;
unique?: boolean;
visible?: boolean;
}

/**
* Map over all the types in Attribute Types and use that to create a union of new types where the attribute type
* is under the property attribute and the type is under the property type.
*/
type EditFieldLayout = {
[K in Attribute.Kind]: EditFieldSharedProps & {
attribute: Extract<Attribute.Any, { type: K }>;
type: K;
};
}[Attribute.Kind];

type LayoutOptions = Schema['options'] & Schema['pluginOptions'] & object;

interface LayoutSettings extends Contracts.ContentTypes.Settings {
displayName?: string;
icon?: never;
}
✏️ Note

EditViewLayout and ListViewLayout are parts of the useDocumentLayout hook (see source code).