Admin Panel API for plugins
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.
You have created a Strapi plugin.
The Admin Panel API includes:
- an entry file which exports the required interface,
- lifecycle functions and the
registerTrad()
async function, - and several specific APIs for your plugin to interact with the admin panel.
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 type | Available functions |
---|---|
Lifecycle functions | |
Async function | registerTrads |
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:
- register itself so it's available to the admin panel
- add a new link to the main navigation (see Menu API)
- create a new settings section
- define injection zones
- add reducers
registerPlugin()
Type: Function
Registers the plugin to make it available in the admin panel.
This function returns an object with the following parameters:
Parameter | Type | Description |
---|---|---|
id | String | Plugin id |
name | String | Plugin name |
injectionZones | Object | Declaration of available injection zones |
Some parameters can be imported from the package.json
file.
Example:
// 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:
- extend another plugin, using
getPlugin('plugin-name')
, - register hooks (see Hooks API)
- add links to a settings section
Example:
module.exports = () => {
return {
// ...
bootstrap(app) {
// execute some bootstrap code
app.injectContentManagerComponent('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:
Action | API to use | Function to use | Related lifecycle function |
---|---|---|---|
Add a new link to the main navigation | Menu API | addMenuLink() | register() |
Create a new settings section | Settings API | createSettingSection() | register() |
Declare an injection zone | Injection Zones API | registerPlugin() | register() |
Add a reducer | Reducers API | addReducers() | register() |
Create a hook | Hooks API | createHook() | register() |
Add a single link to a settings section | Settings API | addSettingsLink() | bootstrap() |
Add multiple links to a settings section | Settings API | addSettingsLinks() | bootstrap() |
Inject a Component in an injection zone | Injection Zones API | injectComponent() | bootstrap() |
Register a hook | Hooks API | registerHook() | bootstrap() |
The WYSIWYG editor can be replaced by taking advantage of custom fields, for instance using the CKEditor custom field plugin.
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
.
Menu API
The Menu API allows a plugin to add a new link to the main navigation through the addMenuLink()
function with the following parameters:
Parameter | Type | Description |
---|---|---|
to | String | Path the link should point to |
icon | React Component | Icon to display in the main navigation |
intlLabel | Object | Label for the link, following the React Int'l convention, with:
|
Component | Async function | Returns a dynamic import of the plugin entry point |
permissions | Array of Objects | Permissions declared in the permissions.js file of the plugin |
intlLabel.id
are ids used in translation files ([plugin-name]/admin/src/translations/[language].json
)
Example:
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
});
app.registerPlugin({ ... });
},
bootstrap() {},
};
Settings API
The Settings API allows:
- creating a new setting section
- adding a single link or multiple links at once to existing settings sections
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:
Parameter | Type | Description |
---|---|---|
id | String | React id |
to | String | Path the link should point to |
intlLabel | Object | Label for the link, following the React Int'l convention, with:
|
Component | Async function | Returns a dynamic import of the plugin entry point |
permissions | Array of Objects | Permissions declared in the permissions.js file of the plugin |
createSettingSection()
Type: Function
Create a new settings section.
The function takes 2 arguments:
Argument | Type | Description |
---|---|---|
first argument | Object | Section label:
|
second argument | Array of Objects | Links included in the section |
intlLabel.id
are ids used in translation files ([plugin-name]/admin/src/translations/[language].json
)
Example:
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[],
},
]
);
},
};
addSettingsLink()
Type: Function
Add a unique link to an existing settings section.
Example:
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[]
}
)
}
}
addSettingsLinks()
Type: Function
Add multiple links to an existing settings section.
Example:
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[]
}]
)
}
}
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:
- Strapi's predefined injection zones for the Content Manager,
- or custom injection zones, created by a plugin
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:
View | Injection zone name & Location |
---|---|
List view |
|
Edit view |
|
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
injectContentManagerComponent()
function instead
Both the injectComponent()
and injectContentManagerComponent()
methods accept the following arguments:
Argument | Type | Description |
---|---|---|
first argument | String | The view where the component is injected |
second argument | String | The zone where the component is injected |
third argument | Object | An object with the following keys:
|
Example: Inject a component in the informations box of the Edit View of the Content Manager:
export default {
bootstrap(app) {
app.injectContentManagerComponent('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:
// 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>
);
};
// Declare this injection zone in the register lifecycle of the plugin
export default {
register() {
app.registerPlugin({
// ...
injectionZones: {
homePage: {
right: []
}
}
});
},
}
// 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:
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, orderedrunHookParallel
returns an array corresponding to the result of the promise resolved by the function executed, orderedrunHookWaterfall
returns a single value corresponding to all the transformations applied by the different functions starting with the initial valueargs
.
Example: Create a hook in a plugin and use it in another plugin
// Create a hook in a plugin
export default {
register(app) {
app.createHook('My-PLUGIN/MY_HOOK');
}
}
// 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 hook
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.
Example: 'Admin/CM/pages/ListView/inject-column-in-table' hook, as used by the Internationalization plugin to add the 'Content available in' column
import get from 'lodash/get';
import cellFormatter from './components/cellFormatter';
export default {
bootstrap(app) {
app.registerHook(
'Admin/CM/pages/ListView/inject-column-in-table',
({ displayedHeaders, layout }) => {
const isFieldLocalized = get(
layout,
'contentType.pluginOptions.i18n.localized',
false
);
if (!isFieldLocalized) {
return { displayedHeaders, layout };
}
return {
layout,
displayedHeaders: [
...displayedHeaders,
{
key: '__locale_key__', // Needed for the table
fieldSchema: { type: 'string' }, // Schema of the attribute
metadatas: {
label: 'Content available in', // Label of the header,
sortable: true | false, // Define if the column is sortable
}, // Metadatas for the label
// Name of the key in the data we will display
name: 'locales',
// Custom renderer: props => Object.keys(props).map(key => <p key={key}>key</p>)
cellFormatter,
},
],
};
}
);
},
}