diff --git a/docusaurus/docs/cms/admin-panel-customization.md b/docusaurus/docs/cms/admin-panel-customization.md index b0be2bd974..739ecd5521 100644 --- a/docusaurus/docs/cms/admin-panel-customization.md +++ b/docusaurus/docs/cms/admin-panel-customization.md @@ -25,6 +25,10 @@ Admin panel customization is done by tweaking the code of the `src/admin/app` fi - Replace some other parts of the admin panel, such as the Rich text editor and the bundler, - Extend the theme or the admin panel to add new features or customize the existing user interface. +:::strapi Plugins and Admin Panel API +In addition to supported customizations detailed in this section, you can go further and create plugins that tap into the [Admin Panel API](/cms/plugins-development/admin-panel-api). +::: + ## General considerations :::prerequisites @@ -84,6 +88,7 @@ The `config` object of `/src/admin/app` accepts the following parameters: | `tutorials` | Boolean | Toggles displaying the video tutorials | `notifications` | Object | Accepts the `releases` key (Boolean) to toggle displaying notifications about new releases | +
Click on any of the following cards to get more details about a specific topic: diff --git a/docusaurus/docs/cms/api/content-api.md b/docusaurus/docs/cms/api/content-api.md index 600b430e62..68f477aa86 100644 --- a/docusaurus/docs/cms/api/content-api.md +++ b/docusaurus/docs/cms/api/content-api.md @@ -2,7 +2,7 @@ title: Content API description: Learn more about Strapi 5's Content API displayed_sidebar: cmsSidebar -sidebar_label: APIs Introduction +sidebar_label: Content APIs Introduction pagination_prev: cms/setup-deployment pagination_next: cms/api/document tags: diff --git a/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/edit-view-layout-and-list-view-layout-rewritten.md b/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/edit-view-layout-and-list-view-layout-rewritten.md index 3ebdb9a368..a31d12f58c 100644 --- a/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/edit-view-layout-and-list-view-layout-rewritten.md +++ b/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/edit-view-layout-and-list-view-layout-rewritten.md @@ -48,7 +48,7 @@ The `content-manager_editViewLayoutManager` and `content-manager_listViewLayoutM ### Notes -Additional information about hooks can be found in the [Admin Panel API for plugins](/cms/plugins-development/admin-panel-api#hooks-api) documentation. +Additional information about hooks can be found in the [Admin Panel API for plugins](/cms/plugins-development/admin-hooks) documentation. ### Manual procedure diff --git a/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/inject-content-manager-component.md b/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/inject-content-manager-component.md index e5033be6ac..28089f6cc7 100644 --- a/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/inject-content-manager-component.md +++ b/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/inject-content-manager-component.md @@ -15,7 +15,7 @@ import MigrationIntro from '/docs/snippets/breaking-change-page-migration-intro. # `injectContentManagerComponent()` removed -In Strapi 5, the `injectContentManagerComponent` method is removed because the Content Manager is now a plugin. The [Admin Panel API](/cms/plugins-development/admin-panel-api#injecting-components) method is replaced by `getPlugin('content-manager').injectComponent()`. +In Strapi 5, the `injectContentManagerComponent` method is removed because the Content Manager is now a plugin. The [Admin Panel API](/cms/plugins-development/admin-injection-zones) method is replaced by `getPlugin('content-manager').injectComponent()`. diff --git a/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/license-only.md b/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/license-only.md index ae0cca1b3e..9a22ed53fc 100644 --- a/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/license-only.md +++ b/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/license-only.md @@ -58,7 +58,7 @@ A similar result can be achieved in Strapi v4 by adding the `lockIcon` property. - a settings menu item will look like the following: ![](/img/assets/plugins/lightning-icon-settings.png) -* Additional information and examples on how to use the `licenseOnly` property can be found in the [`addMenuLink()`](/cms/plugins-development/admin-panel-api#menu-api), [`addSettingsLink()`](/cms/plugins-development/admin-panel-api#addsettingslink), and [`addSettingsLinks()`](/cms/plugins-development/admin-panel-api#addsettingslinks) methods sections of the Admin Panel API documentation. +* Additional information and examples on how to use the `licenseOnly` property can be found in the [`addMenuLink()`](/cms/plugins-development/admin-navigation-settings#navigation-sidebar-menu-links), [`addSettingsLink()`](/cms/plugins-development/admin-navigation-settings#adding-links-to-existing-settings-sections), and [`addSettingsLinks()`](/cms/plugins-development/admin-navigation-settings#adding-links-to-existing-settings-sections) methods sections of the Admin Panel API documentation. ### Manual migration diff --git a/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/react-router-dom-6.md b/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/react-router-dom-6.md index e0e1928842..08ae849da8 100644 --- a/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/react-router-dom-6.md +++ b/docusaurus/docs/cms/migration/v4-to-v5/breaking-changes/react-router-dom-6.md @@ -15,7 +15,7 @@ import MigrationIntro from '/docs/snippets/breaking-change-page-migration-intro. # Strapi users `react-router-dom` v6 -Strapi 5 uses v6. This impacts the links added to [settings](/cms/plugins-development/admin-panel-api#settings-api) or to the [menu](/cms/plugins-development/admin-panel-api#menu-api) using the Admin Panel API. +Strapi 5 uses v6. This impacts the links added to [settings](/cms/plugins-development/admin-navigation-settings#settings) or to the [menu](/cms/plugins-development/admin-navigation-settings#navigation-sidebar-menu-links) using the Admin Panel API. @@ -61,4 +61,4 @@ Strapi 5 uses ). -Ensure that links added to [settings](/cms/plugins-development/admin-panel-api#settings-api) or to the [menu](/cms/plugins-development/admin-panel-api#menu-api) links using the Admin Panel API use relative paths. +Ensure that links added to [settings](/cms/plugins-development/admin-navigation-settings#settings) or to the [menu](/cms/plugins-development/admin-navigation-settings#navigation-sidebar-menu-links) links using the Admin Panel API use relative paths. diff --git a/docusaurus/docs/cms/plugins-development/admin-hooks.md b/docusaurus/docs/cms/plugins-development/admin-hooks.md new file mode 100644 index 0000000000..a45ef8d308 --- /dev/null +++ b/docusaurus/docs/cms/plugins-development/admin-hooks.md @@ -0,0 +1,401 @@ +--- +title: Admin hooks +description: Create and register hooks in Strapi plugins to let other plugins add personalized behavior to your application. +pagination_prev: cms/plugins-development/admin-redux-store +pagination_next: cms/plugins-development/admin-localization +displayed_sidebar: cmsSidebar +tags: +- admin panel +- admin panel customization +- admin panel API +- hooks API +- plugins development +--- + +import Prerequisite from '/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md' + +# Admin Panel API: Hooks + + + +The Hooks API lets plugins create extension points (`createHook` in `register`) and subscribe to them (`registerHook` in `bootstrap`). Hooks run in series, waterfall, or parallel. Strapi includes predefined hooks for the Content Manager's List and Edit views. + + + +The Hooks API allows a plugin to create and register hooks, i.e. places in the application where plugins can add personalized behavior. + + + +## Creating hooks + +Create hook extension points with `createHook()` during the [`register`](/cms/plugins-development/admin-panel-api#register) lifecycle. This declares that your plugin provides an extension point that other plugins can subscribe to. + + + + +```js title="my-plugin/admin/src/index.js" +export default { + register(app) { + app.createHook('My-PLUGIN/MY_HOOK'); + }, +}; +``` + + + + +```ts title="my-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + register(app: StrapiApp) { + app.createHook('My-PLUGIN/MY_HOOK'); + }, +}; +``` + + + + +:::note +For predictable interoperability between plugins, use stable namespaced hook IDs such as `my-plugin/my-hook`. +::: + +## Subscribing to hooks + +Subscribe to hooks with `registerHook()` during the [`bootstrap`](/cms/plugins-development/admin-panel-api#bootstrap) lifecycle, once all plugins are loaded. The callback receives arguments from the hook caller and should return the (optionally mutated) data. + + + + +```js title="my-other-plugin/admin/src/index.js" +export default { + bootstrap(app) { + app.registerHook('My-PLUGIN/MY_HOOK', (...args) => { + console.log(args); + + // important: return the mutated data + return args; + }); + }, +}; +``` + + + + +```ts title="my-other-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + bootstrap(app: StrapiApp) { + app.registerHook('My-PLUGIN/MY_HOOK', (...args: unknown[]) => { + console.log(args); + + // important: return the mutated data + return args; + }); + }, +}; +``` + + + + +Async callbacks are also supported: + + + + +```js title="my-other-plugin/admin/src/index.js" +export default { + bootstrap(app) { + app.registerHook('My-PLUGIN/MY_HOOK', async (data) => { + const enrichedData = await fetchExternalData(data); + + // always return data for waterfall hooks + return enrichedData; + }); + }, +}; +``` + + + + +```ts title="my-other-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + bootstrap(app: StrapiApp) { + app.registerHook('My-PLUGIN/MY_HOOK', async (data: unknown) => { + const enrichedData = await fetchExternalData(data); + + // always return data for waterfall hooks + return enrichedData; + }); + }, +}; +``` + + + + +## Running hooks + +Hooks can be run in 3 modes: + +| Mode | Function | Return value | +|---|---|---| +| Series | `runHookSeries` | Array of results from each function, in order | +| Parallel | `runHookParallel` | Array of resolved promise results, in order | +| Waterfall | `runHookWaterfall` | Single value after all transformations applied sequentially | + +:::caution +For `runHookWaterfall`, each subscriber must return the transformed value so that the next subscriber in the chain receives it. Failing to return a value will break the chain. +::: + +## Using predefined hooks + +Strapi includes predefined hooks for the Content Manager's List and Edit views. + +### `INJECT-COLUMN-IN-TABLE` + +The `Admin/CM/pages/ListView/inject-column-in-table` hook can add or mutate columns in the List View of the [Content Manager](/cms/intro): + +```tsx +runHookWaterfall(INJECT_COLUMN_IN_TABLE, { + displayedHeaders: ListFieldLayout[], + layout: ListFieldLayout, +}); +``` + +The following example subscribes to this hook to add a custom "External id" column: + + + + +```js title="my-plugin/admin/src/index.js" +export default { + bootstrap(app) { + app.registerHook( + 'Admin/CM/pages/ListView/inject-column-in-table', + ({ displayedHeaders, layout }) => { + return { + displayedHeaders: [ + ...displayedHeaders, + { + attribute: { type: 'custom' }, + label: 'External id', + name: 'externalId', + searchable: false, + sortable: false, + cellFormatter: (document) => document.externalId, + }, + ], + layout, + }; + } + ); + }, +}; +``` + + + + +```ts title="my-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + bootstrap(app: StrapiApp) { + app.registerHook( + 'Admin/CM/pages/ListView/inject-column-in-table', + ({ displayedHeaders, layout }) => { + return { + displayedHeaders: [ + ...displayedHeaders, + { + attribute: { type: 'custom' }, + label: 'External id', + name: 'externalId', + searchable: false, + sortable: false, + cellFormatter: (document) => document.externalId, + }, + ], + layout, + }; + } + ); + }, +}; +``` + + + + +**ListFieldLayout and ListLayout type definitions**: + + + +```tsx +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, + { 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; +} +``` + + + +### `MUTATE-EDIT-VIEW-LAYOUT` + +The `Admin/CM/pages/EditView/mutate-edit-view-layout` hook can mutate the Edit View layout of the [Content Manager](/cms/intro). + +The following example subscribes to this hook to force all fields to full width: + + + + +```js title="my-plugin/admin/src/index.js" +export default { + bootstrap(app) { + app.registerHook( + 'Admin/CM/pages/EditView/mutate-edit-view-layout', + ({ layout, ...rest }) => { + // Force all fields to full width in the default edit layout + const updatedLayout = layout.map((rowGroup) => + rowGroup.map((row) => row.map((field) => ({ ...field, size: 12 }))) + ); + + return { + ...rest, + layout: updatedLayout, + }; + } + ); + }, +}; +``` + + + + +```ts title="my-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + bootstrap(app: StrapiApp) { + app.registerHook( + 'Admin/CM/pages/EditView/mutate-edit-view-layout', + ({ layout, ...rest }) => { + // Force all fields to full width in the default edit layout + const updatedLayout = layout.map((rowGroup) => + rowGroup.map((row) => row.map((field) => ({ ...field, size: 12 }))) + ); + + return { + ...rest, + layout: updatedLayout, + }; + } + ); + }, +}; +``` + + + + +**EditLayout and EditFieldLayout type definitions:** + + + +```tsx +interface EditLayout { + layout: Array>; + components: { + [uid: string]: { + layout: Array; + 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 { + 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; + type: K; + }; +}[Attribute.Kind]; + +type LayoutOptions = Schema['options'] & Schema['pluginOptions'] & object; + +interface LayoutSettings extends Contracts.ContentTypes.Settings { + displayName?: string; + icon?: never; +} +``` + + + +:::note +The `EditLayout` and `ListLayout` shapes documented here come from the `useDocumentLayout` hook (see ). Internal package naming can vary, but plugin authors should rely on the `EditLayout` and `ListLayout` shapes exposed in this page. +::: \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/admin-injection-zones.md b/docusaurus/docs/cms/plugins-development/admin-injection-zones.md new file mode 100644 index 0000000000..a663b09967 --- /dev/null +++ b/docusaurus/docs/cms/plugins-development/admin-injection-zones.md @@ -0,0 +1,521 @@ +--- +title: Admin injection zones +description: Extend and customize the Strapi admin panel by injecting React components into predefined or custom injection zones. +pagination_prev: cms/plugins-development/content-manager-apis +pagination_next: cms/plugins-development/admin-redux-store +displayed_sidebar: cmsSidebar +toc_max_heading_level: 4 +tags: + - admin panel + - admin panel customization + - admin panel API + - injection zones + - plugin customization + - Content Manager + - plugins development +--- + +import Prerequisite from '/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md' +import InjectionVsCmApis from '/docs/snippets/injection-zones-vs-content-manager-apis.md' + +# Admin Panel API: Injection zones + + + +Injection zones are predefined areas in the admin UI where plugins can inject React components. Use `getPlugin('content-manager').injectComponent()` to extend built-in views, or define your own zones with `injectionZones` in `registerPlugin`. + + + +Plugins can extend and customize existing admin panel sections by injecting custom React components into predefined areas. This allows you to add functionality to Strapi's built-in interfaces without modifying core code. + + + +:::note +Injection zones are defined in the [`register`](/cms/plugins-development/admin-panel-api#register) lifecycle function, but components are injected in the [`bootstrap`](/cms/plugins-development/admin-panel-api#bootstrap) lifecycle function. +::: + +## Injection zones vs. Content Manager APIs + + + +## Predefined injection zones + +Strapi's Content Manager provides predefined injection zones that plugins can use: + +| View | Injection zone | Location | +|---|---|---| +| List view | `listView.actions` | Between the Filters and the cogs icon | +| List view | `listView.publishModalAdditionalInfos` | Informational content in the publish confirmation modal | +| List view | `listView.unpublishModalAdditionalInfos` | Informational content in the unpublish confirmation modal | +| List view | `listView.deleteModalAdditionalInfos` | Informational content in the delete confirmation modal | +| Edit view | `editView.right-links` | Between the "Configure the view" and "Edit" buttons | +| Edit view | `editView.informations` | In the informations box of the Edit view (internal, see note below) | +| Preview | `preview.actions` | In the preview view action area | + +The `listView.*ModalAdditionalInfos` zones are intended to enrich the informational content displayed in publish, unpublish, and delete confirmation modals. + +:::caution +The `editView.informations` zone exists in the Content Manager source code but is considered internal. For third-party plugins, `editView.right-links` is the most stable and officially recommended Edit view extension point. Use `editView.informations` only when you specifically need the information panel area and accept potential UI changes between versions. +::: + +### Injecting into Content Manager zones + +To inject a component into a Content Manager injection zone, use `getPlugin('content-manager').injectComponent()` in the `bootstrap` lifecycle: + + + + +```jsx title="admin/src/index.js" +import { MyCustomButton } from './components/MyCustomButton'; +import { PreviewAction } from './components/PreviewAction'; +import { PublishModalInfo } from './components/PublishModalInfo'; + +export default { + register(app) { + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + }); + }, + bootstrap(app) { + // Inject a button into the Edit view's right-links zone + // highlight-start + app + .getPlugin('content-manager') + .injectComponent('editView', 'right-links', { + name: 'my-plugin-custom-button', + Component: MyCustomButton, + }); + // highlight-end + + // Inject a component into the List view's actions zone + // highlight-start + app + .getPlugin('content-manager') + .injectComponent('listView', 'actions', { + name: 'my-plugin-list-action', + Component: () => , + }); + // highlight-end + + // Inject additional information into the publish modal + // highlight-start + app + .getPlugin('content-manager') + .injectComponent('listView', 'publishModalAdditionalInfos', { + name: 'my-plugin-publish-modal-info', + Component: PublishModalInfo, + }); + // highlight-end + + // Inject a component into the Preview view's actions zone + // highlight-start + app + .getPlugin('content-manager') + .injectComponent('preview', 'actions', { + name: 'my-plugin-preview-action', + Component: PreviewAction, + }); + // highlight-end + }, +}; +``` + + + + +```tsx title="admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; +import { MyCustomButton } from './components/MyCustomButton'; +import { PreviewAction } from './components/PreviewAction'; +import { PublishModalInfo } from './components/PublishModalInfo'; + +export default { + register(app: StrapiApp) { + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + }); + }, + bootstrap(app: StrapiApp) { + // Inject a button into the Edit view's right-links zone + // highlight-start + app + .getPlugin('content-manager') + .injectComponent('editView', 'right-links', { + name: 'my-plugin-custom-button', + Component: MyCustomButton, + }); + // highlight-end + + // Inject a component into the List view's actions zone + // highlight-start + app + .getPlugin('content-manager') + .injectComponent('listView', 'actions', { + name: 'my-plugin-list-action', + Component: () => , + }); + // highlight-end + + // Inject additional information into the publish modal + // highlight-start + app + .getPlugin('content-manager') + .injectComponent('listView', 'publishModalAdditionalInfos', { + name: 'my-plugin-publish-modal-info', + Component: PublishModalInfo, + }); + // highlight-end + + // Inject a component into the Preview view's actions zone + // highlight-start + app + .getPlugin('content-manager') + .injectComponent('preview', 'actions', { + name: 'my-plugin-preview-action', + Component: PreviewAction, + }); + // highlight-end + }, +}; +``` + + + + +## Custom injection zones + +Plugins can define their own injection zones to allow other plugins to extend their UI. Declare injection zones in the `registerPlugin` configuration: + + + + +```jsx title="admin/src/index.js" +export default { + register(app) { + app.registerPlugin({ + id: 'dashboard', + name: 'Dashboard', + // highlight-start + injectionZones: { + homePage: { + top: [], + middle: [], + bottom: [], + }, + sidebar: { + before: [], + after: [], + }, + }, + // highlight-end + }); + }, +}; +``` + + + + +```ts title="admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + register(app: StrapiApp) { + app.registerPlugin({ + id: 'dashboard', + name: 'Dashboard', + // highlight-start + injectionZones: { + homePage: { + top: [], + middle: [], + bottom: [], + }, + sidebar: { + before: [], + after: [], + }, + }, + // highlight-end + }); + }, +}; +``` + + + + +### Rendering injection zones in components + +In Strapi 5, the `InjectionZone` component from `@strapi/helper-plugin` is removed and has no direct replacement export. To render injected components, create your own component with `useStrapiApp` from `@strapi/strapi/admin`. + + + + +```jsx title="admin/src/components/CustomInjectionZone.jsx" +import { useStrapiApp } from '@strapi/strapi/admin'; + +export const CustomInjectionZone = ({ area, ...props }) => { + const getPlugin = useStrapiApp('CustomInjectionZone', (state) => state.getPlugin); + const [pluginName, view, zone] = area.split('.'); + + const plugin = getPlugin(pluginName); + const components = plugin?.getInjectedComponents(view, zone); + + if (!components?.length) { + return null; + } + + return components.map(({ name, Component }) => ); +}; +``` + +```jsx title="admin/src/pages/Dashboard.jsx" +import { CustomInjectionZone } from '../components/CustomInjectionZone'; + +const Dashboard = () => { + return ( +
+

Dashboard

+ + {/* Render components injected into the top zone */} + + +
{/* Main dashboard content */}
+ + {/* Render components injected into the bottom zone */} + +
+ ); +}; + +export default Dashboard; +``` + +
+ + +```tsx title="admin/src/components/CustomInjectionZone.tsx" +import { useStrapiApp } from '@strapi/strapi/admin'; + +type CustomInjectionZoneProps = { + area: `${string}.${string}.${string}`; + [key: string]: unknown; +}; + +export const CustomInjectionZone = ({ area, ...props }: CustomInjectionZoneProps) => { + const getPlugin = useStrapiApp('CustomInjectionZone', (state) => state.getPlugin); + const [pluginName, view, zone] = area.split('.'); + + const plugin = getPlugin(pluginName); + const components = plugin?.getInjectedComponents(view, zone); + + if (!components?.length) { + return null; + } + + return components.map(({ name, Component }) => ); +}; +``` + +```tsx title="admin/src/pages/Dashboard.tsx" +import { CustomInjectionZone } from '../components/CustomInjectionZone'; + +const Dashboard = () => { + return ( +
+

Dashboard

+ + {/* Render components injected into the top zone */} + + +
{/* Main dashboard content */}
+ + {/* Render components injected into the bottom zone */} + +
+ ); +}; + +export default Dashboard; +``` + +
+
+ +### Injecting into custom zones + +Other plugins can inject components into your custom injection zones using the `injectComponent()` method in their `bootstrap` lifecycle: + + + + +```jsx title="another-plugin/admin/src/index.js" +import { Widget } from './components/Widget'; + +export default { + register(app) { + app.registerPlugin({ + id: 'widget-plugin', + name: 'Widget Plugin', + }); + }, + // highlight-start + bootstrap(app) { + const dashboardPlugin = app.getPlugin('dashboard'); + + if (dashboardPlugin) { + dashboardPlugin.injectComponent('homePage', 'top', { + name: 'widget-plugin-statistics', + Component: Widget, + }); + } + }, + // highlight-end +}; +``` + + + + +```tsx title="another-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; +import { Widget } from './components/Widget'; + +export default { + register(app: StrapiApp) { + app.registerPlugin({ + id: 'widget-plugin', + name: 'Widget Plugin', + }); + }, + // highlight-start + bootstrap(app: StrapiApp) { + const dashboardPlugin = app.getPlugin('dashboard'); + + if (dashboardPlugin) { + dashboardPlugin.injectComponent('homePage', 'top', { + name: 'widget-plugin-statistics', + Component: Widget, + }); + } + }, + // highlight-end +}; +``` + + + + +## Injection component parameters + +The `injectComponent()` method accepts the following parameters: + +| Parameter | Type | Description | +|---|---|---| +| `view` | `string` | The view name where the component should be injected | +| `zone` | `string` | The zone name within the view where the component should be injected | +| `component` | `object` | Configuration object with `name` (unique string) and `Component` (React component to inject) | + +## Content Manager data access + +When injecting components into Content Manager injection zones, you can access the Edit View data using the `useContentManagerContext` hook: + + + + +```jsx title="admin/src/components/MyCustomButton.jsx" +import { + unstable_useContentManagerContext as useContentManagerContext, +} from '@strapi/strapi/admin'; + +export const MyCustomButton = () => { + const { + slug, // Content type slug (e.g., 'api::article.article') + model, // Content type model + id, // Document ID (undefined when creating) + collectionType, // 'single-types' or 'collection-types' + isCreatingEntry, // Whether creating a new entry + isSingleType, // Whether the content type is a single type + hasDraftAndPublish, // Whether draft & publish is enabled + contentType, // Content type schema + components, // Component schemas + layout, // Content type layout + form, // Form state and handlers + } = useContentManagerContext(); + + const { initialValues, values, onChange } = form; + + const handleCustomAction = () => { + onChange({ target: { name: 'customField', value: 'new value' } }); + }; + + return ; +}; +``` + + + + +```tsx title="admin/src/components/MyCustomButton.tsx" +import { + unstable_useContentManagerContext as useContentManagerContext, +} from '@strapi/strapi/admin'; + +export const MyCustomButton = () => { + const { + slug, // Content type slug (e.g., 'api::article.article') + model, // Content type model + id, // Document ID (undefined when creating) + collectionType, // 'single-types' or 'collection-types' + isCreatingEntry, // Whether creating a new entry + isSingleType, // Whether the content type is a single type + hasDraftAndPublish, // Whether draft & publish is enabled + contentType, // Content type schema + components, // Component schemas + layout, // Content type layout + form, // Form state and handlers + } = useContentManagerContext(); + + const { initialValues, values, onChange } = form; + + const handleCustomAction = () => { + onChange({ target: { name: 'customField', value: 'new value' } }); + }; + + return ; +}; +``` + + + + +:::caution +The `useContentManagerContext` hook is currently exported as `unstable_useContentManagerContext`. The `unstable_` prefix indicates the API may change in future releases. This hook replaces the [deprecated `useCMEditViewDataManager`](/cms/migration/v4-to-v5/additional-resources/helper-plugin#usecmeditviewdatamanager) from `@strapi/helper-plugin` which is not available in Strapi 5. +::: + +## Best practices + +- **Use descriptive zone names.** Choose clear names for your injection zones (e.g., `top`, `bottom`, `before`, `after`). + +- **Check plugin availability.** Always verify that a plugin exists before injecting components into its zones: + + ```js + bootstrap(app) { + const targetPlugin = app.getPlugin('target-plugin'); + if (targetPlugin) { + targetPlugin.injectComponent('view', 'zone', { + name: 'my-component', + Component: MyComponent, + }); + } + } + ``` + +- **Use unique component names.** Ensure component names are unique to avoid conflicts with other plugins. + +- **Handle missing zones gracefully.** Components should handle cases where injection zones might not be available. + +- **Document your injection zones.** Clearly document which injection zones your plugin provides and their intended use. \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/admin-localization.md b/docusaurus/docs/cms/plugins-development/admin-localization.md new file mode 100644 index 0000000000..cbcd2a965f --- /dev/null +++ b/docusaurus/docs/cms/plugins-development/admin-localization.md @@ -0,0 +1,464 @@ +--- +title: Admin localization +description: Provide translations for your Strapi plugin's admin panel interface using registerTrads and react-intl. +pagination_prev: cms/plugins-development/admin-hooks +pagination_next: cms/plugins-development/server-api +displayed_sidebar: cmsSidebar +toc_max_heading_level: 4 +tags: + - admin panel + - admin panel customization + - admin panel API + - internationalization + - plugins development +--- + +import Prerequisite from '/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md' + +# Admin Panel API: Localization + + + +Register translation files with `registerTrads`, prefix keys with your plugin ID to avoid conflicts, and use `react-intl`'s `useIntl` hook in components. Strapi merges plugin translations with core translations automatically. + + + +Plugins can provide translations for multiple languages to make the admin interface accessible to users worldwide. Strapi automatically loads and merges plugin translations with core translations, making them available throughout the admin panel. + + + +## Translation file structure + +Translation files are stored in the `admin/src/translations/` directory, with 1 JSON file per locale: + +``` +admin/src/translations/ + ├── en.json + ├── fr.json + ├── de.json + └── ... +``` + +Each translation file contains key-value pairs where keys are translation identifiers and values are the translated strings: + +```json title="admin/src/translations/en.json" +{ + "plugin.name": "My Plugin", + "plugin.description": "A custom Strapi plugin", + "settings.title": "Settings", + "settings.general": "General", + "settings.advanced": "Advanced" +} +``` + +## The `registerTrads` function {#registertrads} + +In Strapi plugins that have an admin part, `registerTrads()` is used to load translations for your plugin's UI labels and messages. If you don't need localization, you can omit it and the plugin can still run. + +The `registerTrads()` function is an async function that loads translation files for all configured locales. Strapi calls this function during admin panel initialization to collect translations from all plugins. + +### Basic implementation + + + + +```js title="admin/src/index.js" +import { prefixPluginTranslations } from './utils/prefixPluginTranslations'; +import { PLUGIN_ID } from './pluginId'; + +export default { + register(app) { + app.registerPlugin({ + id: PLUGIN_ID, + name: 'My Plugin', + }); + }, + async registerTrads({ locales }) { + const importedTranslations = await Promise.all( + locales.map((locale) => { + return import(`./translations/${locale}.json`) + .then(({ default: data }) => { + return { + data: prefixPluginTranslations(data, PLUGIN_ID), + locale, + }; + }) + .catch(() => { + return { + data: {}, + locale, + }; + }); + }), + ); + + return importedTranslations; + }, +}; +``` + + + + +```ts title="admin/src/index.ts" +import { prefixPluginTranslations } from './utils/prefixPluginTranslations'; +import { PLUGIN_ID } from './pluginId'; +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + register(app: StrapiApp) { + app.registerPlugin({ + id: PLUGIN_ID, + name: 'My Plugin', + }); + }, + async registerTrads({ locales }: { locales: string[] }) { + const importedTranslations = await Promise.all( + locales.map((locale) => { + return import(`./translations/${locale}.json`) + .then(({ default: data }) => { + return { + data: prefixPluginTranslations(data, PLUGIN_ID), + locale, + }; + }) + .catch(() => { + return { + data: {}, + locale, + }; + }); + }), + ); + + return importedTranslations; + }, +}; +``` + + + + +### Function parameters + +The `registerTrads` function receives an object with the following property: + +| Parameter | Type | Description | +|---|---|---| +| `locales` | `string[]` | Array of locale codes configured in the admin panel (e.g., `['en', 'fr', 'de']`) | + +### Return value + +The function must return a `Promise` that resolves to an array of translation objects. Each object has the following structure: + +```ts +{ + data: Record; // Translation key-value pairs + locale: string; // Locale code (e.g., 'en', 'fr') +} +``` + +### Translation key prefixing + +:::caution +Translation keys must be prefixed with your plugin ID to avoid conflicts with other plugins and core Strapi translations. For example, if your plugin ID is `my-plugin`, a key like `plugin.name` should become `my-plugin.plugin.name`. +::: + +Use the `prefixPluginTranslations` utility function to automatically prefix all keys: + + + + +```js title="admin/src/utils/prefixPluginTranslations.js" +const prefixPluginTranslations = (trad, pluginId) => { + if (!pluginId) { + throw new TypeError("pluginId can't be empty"); + } + return Object.keys(trad).reduce((acc, current) => { + acc[`${pluginId}.${current}`] = trad[current]; + return acc; + }, {}); +}; + +export { prefixPluginTranslations }; +``` + + + + +```ts title="admin/src/utils/prefixPluginTranslations.ts" +type TradOptions = Record; + +const prefixPluginTranslations = ( + trad: TradOptions, + pluginId: string, +): TradOptions => { + if (!pluginId) { + throw new TypeError("pluginId can't be empty"); + } + return Object.keys(trad).reduce((acc, current) => { + acc[`${pluginId}.${current}`] = trad[current]; + return acc; + }, {} as TradOptions); +}; + +export { prefixPluginTranslations }; +``` + + + + +For instance, if your translation file contains: + +```json +{ + "plugin.name": "My Plugin", + "settings.title": "Settings" +} +``` + +After prefixing with plugin ID `my-plugin`, these become: + +- `my-plugin.plugin.name` +- `my-plugin.settings.title` + +### Missing translation files + +The `registerTrads` function should gracefully handle missing translation files by returning an empty object for that locale. The `.catch()` handler in the example above ensures that if a translation file does not exist, the plugin still returns a valid translation object: + +```js +.catch(() => { + return { + data: {}, + locale, + }; +}); +``` + +This allows plugins to provide translations for only some locales (e.g., only English) without breaking the admin panel for other locales. + +## Translations in components + +To use translations in your React components, use the `useIntl` hook from `react-intl`: + + + + +```jsx title="admin/src/pages/HomePage.jsx" +import { useIntl } from 'react-intl'; +import { PLUGIN_ID } from '../pluginId'; + +const HomePage = () => { + const { formatMessage } = useIntl(); + + return ( +
+

+ {formatMessage({ + id: `${PLUGIN_ID}.plugin.name`, + defaultMessage: 'My Plugin', + })} +

+

+ {formatMessage({ + id: `${PLUGIN_ID}.plugin.description`, + defaultMessage: 'A custom Strapi plugin', + })} +

+
+ ); +}; + +export default HomePage; +``` + +
+ + +```tsx title="admin/src/pages/HomePage.tsx" +import { useIntl } from 'react-intl'; +import { PLUGIN_ID } from '../pluginId'; + +const HomePage = () => { + const { formatMessage } = useIntl(); + + return ( +
+

+ {formatMessage({ + id: `${PLUGIN_ID}.plugin.name`, + defaultMessage: 'My Plugin', + })} +

+

+ {formatMessage({ + id: `${PLUGIN_ID}.plugin.description`, + defaultMessage: 'A custom Strapi plugin', + })} +

+
+ ); +}; + +export default HomePage; +``` + +
+
+ +### Helper function for translation keys + +To avoid repeating the plugin ID prefix, create a helper function: + + + + +```js title="admin/src/utils/getTranslation.js" +import { PLUGIN_ID } from '../pluginId'; + +export const getTranslation = (id) => `${PLUGIN_ID}.${id}`; +``` + + + + +```ts title="admin/src/utils/getTranslation.ts" +import { PLUGIN_ID } from '../pluginId'; + +export const getTranslation = (id: string) => `${PLUGIN_ID}.${id}`; +``` + + + + +Then use it in components: + + + + +```jsx title="admin/src/pages/HomePage.jsx" +import { useIntl } from 'react-intl'; +import { getTranslation } from '../utils/getTranslation'; + +const HomePage = () => { + const { formatMessage } = useIntl(); + + return ( +
+

+ {formatMessage({ + id: getTranslation('plugin.name'), + defaultMessage: 'My Plugin', + })} +

+
+ ); +}; +``` + +
+ + +```tsx title="admin/src/pages/HomePage.tsx" +import { useIntl } from 'react-intl'; +import { getTranslation } from '../utils/getTranslation'; + +const HomePage = () => { + const { formatMessage } = useIntl(); + + return ( +
+

+ {formatMessage({ + id: getTranslation('plugin.name'), + defaultMessage: 'My Plugin', + })} +

+
+ ); +}; +``` + +
+
+ +## Translations in configuration + +Translation keys are also used when configuring menu links, settings sections, and other admin panel elements: + + + + +```js title="admin/src/index.js" +export default { + register(app) { + app.addMenuLink({ + to: '/plugins/my-plugin', + icon: PluginIcon, + intlLabel: { + id: 'my-plugin.plugin.name', // Prefixed translation key + defaultMessage: 'My Plugin', // Fallback if translation missing + }, + Component: async () => { + const { App } = await import('./pages/App'); + return App; + }, + }); + }, +}; +``` + + + + +```ts title="admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + register(app: StrapiApp) { + app.addMenuLink({ + to: '/plugins/my-plugin', + icon: PluginIcon, + intlLabel: { + id: 'my-plugin.plugin.name', // Prefixed translation key + defaultMessage: 'My Plugin', // Fallback if translation missing + }, + Component: async () => { + const { App } = await import('./pages/App'); + return App; + }, + }); + }, +}; +``` + + + + +## Plugin translation lifecycle + +Strapi's admin panel automatically: + +1. Calls `registerTrads` for all registered plugins during initialization +2. Merges translations from all plugins with core Strapi translations +3. Applies custom translations from the admin configuration (if any) +4. Makes translations available via `react-intl` throughout the admin panel + +In practice, core admin translations are loaded first, plugin translations are merged on top, and project-level overrides in `config.translations` let you customize the labels displayed in the admin panel. + +## Best practices + +- **Always prefix translation keys.** Use `prefixPluginTranslations` or manually prefix keys with your plugin ID to avoid conflicts. +- **Provide default messages.** Always include `defaultMessage` when using `formatMessage` as a fallback if translations are missing. +- **Handle missing translations gracefully.** The `registerTrads` function should return empty objects for missing locales rather than throwing errors. +- **Use descriptive key names.** Choose clear, hierarchical key names (e.g., `settings.general.title` rather than `title1`). +- **Support at least English.** Providing English translations ensures your plugin works out of the box. +- **Verify behavior with multiple locales.** Test that your plugin works correctly when different locales are selected in the admin panel. + +:::note +The `en` locale is always available in Strapi and serves as the fallback locale. If a translation is missing for a selected locale, Strapi uses the English translation. +::: + +:::tip +To see which locales are available in your Strapi instance, check the `config.locales` array in your `src/admin/app.ts` or `src/admin/app.js` file. For programmatic access at runtime, see [Accessing the Redux store](/cms/plugins-development/admin-redux-store) (note that internal store structure may change between versions). +::: \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/admin-navigation-settings.md b/docusaurus/docs/cms/plugins-development/admin-navigation-settings.md new file mode 100644 index 0000000000..85ccafec12 --- /dev/null +++ b/docusaurus/docs/cms/plugins-development/admin-navigation-settings.md @@ -0,0 +1,444 @@ +--- +title: Admin navigation & settings +description: Add menu links, create settings sections, and configure settings links for your Strapi plugin in the admin panel. +pagination_prev: cms/plugins-development/admin-panel-api +pagination_next: cms/plugins-development/content-manager-apis +displayed_sidebar: cmsSidebar +toc_max_heading_level: 4 +tags: + - admin panel + - admin panel customization + - admin panel API + - menu links + - settings + - plugins development +--- + +import Prerequisite from '/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md' + +# Admin Panel API: Navigation & settings + + + +Use `addMenuLink` in `register` to add sidebar links, `createSettingSection` in `register` to create settings groups, and `addSettingsLink`/`addSettingsLinks` in `bootstrap` to extend existing settings sections. + + + +Plugins can customize the admin panel's navigation sidebar and settings pages to provide access to their features. All functions described on this page are called within the [`register`](/cms/plugins-development/admin-panel-api#register) or [`bootstrap`](/cms/plugins-development/admin-panel-api#bootstrap) lifecycle functions of your plugin's entry file. + + + +## Navigation sidebar (menu links) + +The navigation sidebar is the main menu on the left side of the admin panel. Plugins can add links to this sidebar using the `addMenuLink()` method in the `register` lifecycle function. + +### Adding a menu link + +Adding a link to the navigation sidebar is done with the `addMenuLink()` function, which should be registered through the `register()` lifecycle function of your plugin. + + + +A menu link accepts the following parameters: + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `to` | `string` | ✅ | Path the link should point to (relative to the admin panel root) (see [additional information](#path-conventions-for-to)) | +| `icon` | `React.Component` | ✅ | React component for the icon to display in the navigation | +| `intlLabel` | `object` | ✅ | Label for the link, following the convention, with:
  • `id`: id used to insert the localized label
  • `defaultMessage`: default label for the link
| +| `Component` | `async function` | ✅ | Async function that returns a dynamic import of the plugin's main page component | +| `permissions` | `Array` | ❌ | Array of permission objects that control access to the link | +| `position` | `number` | ❌ | Numeric position in the menu (lower numbers appear first) | +| `licenseOnly` | `boolean` | ❌ | If `true`, displays a ⚡ icon to indicate the feature requires a paid license (default: `false`) | + +:::note +The `intlLabel.id` values should correspond to keys in your translation files located at `admin/src/translations/[locale].json`. See [Admin localization](/cms/plugins-development/admin-localization) for details. +::: + +:::caution +The `permissions` parameter only controls whether the link is visible in the navigation. It does not protect the page itself. A user who knows the URL can still access the page directly. To fully secure a plugin route, you must also check permissions inside your page component and register your RBAC actions on the server side with `actionProvider.registerMany`. See the [Admin permissions for plugins](/cms/plugins-development/guides/admin-permissions-for-plugins) guide for a complete walkthrough. +::: + + + + +```jsx title="admin/src/index.js" +import PluginIcon from './components/PluginIcon'; + +export default { + register(app) { + // highlight-start + app.addMenuLink({ + to: `/plugins/my-plugin`, + icon: PluginIcon, + intlLabel: { + id: 'my-plugin.plugin.name', + defaultMessage: 'My Plugin', + }, + Component: async () => { + const { App } = await import('./pages/App'); + return App; + }, + permissions: [], // Array of permission objects + position: 3, // Position in the menu (lower numbers appear first) + licenseOnly: false, // Set to true to show ⚡ icon for paid features + }); + // highlight-end + + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + }); + }, +}; +``` + + + + +```ts title="admin/src/index.ts" +import PluginIcon from './components/PluginIcon'; +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + register(app: StrapiApp) { + // highlight-start + app.addMenuLink({ + to: `/plugins/my-plugin`, + icon: PluginIcon, + intlLabel: { + id: 'my-plugin.plugin.name', + defaultMessage: 'My Plugin', + }, + Component: async () => { + const { App } = await import('./pages/App'); + return App; + }, + permissions: [], // Array of permission objects + position: 3, // Position in the menu (lower numbers appear first) + licenseOnly: false, // Set to true to show ⚡ icon for paid features + }); + // highlight-end + + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + }); + }, +}; +``` + + + + +## Settings + +The Settings API allows plugins to create new settings sections or add links to existing sections. Settings sections are organized groups of configuration pages accessible from the _Settings_ menu item in the navigation sidebar. + + + +### Creating a new settings section + +Use `createSettingSection()` in the `register` lifecycle function: + + + + +```jsx title="admin/src/index.js" +export default { + register(app) { + // highlight-start + app.createSettingSection( + { + id: 'my-plugin', + intlLabel: { + id: 'my-plugin.settings.section-label', + defaultMessage: 'My Plugin Settings', + }, + }, + [ + { + intlLabel: { + id: 'my-plugin.settings.general', + defaultMessage: 'General', + }, + id: 'general', + to: 'my-plugin/general', + Component: async () => { + const { GeneralSettings } = + await import('./pages/Settings/General'); + return GeneralSettings; + }, + }, + { + intlLabel: { + id: 'my-plugin.settings.advanced', + defaultMessage: 'Advanced', + }, + id: 'advanced', + to: 'my-plugin/advanced', + Component: async () => { + const { AdvancedSettings } = + await import('./pages/Settings/Advanced'); + return AdvancedSettings; + }, + }, + ], + ); + // highlight-end + + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + }); + }, +}; +``` + + + + +```ts title="admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + register(app: StrapiApp) { + // highlight-start + app.createSettingSection( + { + id: 'my-plugin', + intlLabel: { + id: 'my-plugin.settings.section-label', + defaultMessage: 'My Plugin Settings', + }, + }, + [ + { + intlLabel: { + id: 'my-plugin.settings.general', + defaultMessage: 'General', + }, + id: 'general', + to: 'my-plugin/general', + Component: async () => { + const { GeneralSettings } = + await import('./pages/Settings/General'); + return GeneralSettings; + }, + }, + { + intlLabel: { + id: 'my-plugin.settings.advanced', + defaultMessage: 'Advanced', + }, + id: 'advanced', + to: 'my-plugin/advanced', + Component: async () => { + const { AdvancedSettings } = + await import('./pages/Settings/Advanced'); + return AdvancedSettings; + }, + }, + ], + ); + // highlight-end + + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + }); + }, +}; +``` + + + + +The `createSettingSection()` function accepts the following parameters: + +* the first argument is the section configuration: + + | Parameter | Type | Required | Description | + |---|---|---|---| + | `id` | `string` | ✅ | Unique identifier for the settings section | + | `intlLabel` | `object` | ✅ | Localized label for the section, following the convention, with:
  • `id`: id used to insert the localized label
  • `defaultMessage`: default label for the section
| + +* the second argument is an array of link objects; each link object contains the following: + + | Parameter | Type | Required | Description | + |---|---|---|---| + | `id` | `string` | ✅ | Unique identifier for the settings link | + | `to` | `string` | ✅ | Path relative to the settings route (do not include `settings/` prefix) (see [additional information](#path-conventions-for-to)) | + | `intlLabel` | `object` | ✅ | Localized label object with `id` and `defaultMessage` | + | `Component` | `async function` | ✅ | Async function that returns a dynamic import of the settings page component | + | `permissions` | `Array` | ❌ | Array of permission objects that control access to the link | + | `licenseOnly` | `boolean` | ❌ | If `true`, displays a ⚡ icon (default: `false`) | + +### Adding links to existing settings sections + +To add a single link to an existing settings section, use `addSettingsLink()` in the `bootstrap()` lifecycle function. To add multiple links at once, use `addSettingsLinks()`. + + + + +```jsx title="admin/src/index.js" +export default { + register(app) { + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + }); + }, + bootstrap(app) { + // Add a single link to the global settings section + // highlight-start + app.addSettingsLink('global', { + intlLabel: { + id: 'my-plugin.settings.documentation', + defaultMessage: 'Documentation', + }, + id: 'documentation', + to: 'my-plugin/documentation', + Component: async () => { + const { DocumentationPage } = + await import('./pages/Settings/Documentation'); + return DocumentationPage; + }, + permissions: [], + licenseOnly: false, + }); + // highlight-end + + // Add multiple links at once to the global settings section + // highlight-start + app.addSettingsLinks('global', [ + { + intlLabel: { + id: 'my-plugin.settings.general', + defaultMessage: 'General', + }, + id: 'general', + to: 'my-plugin/general', + Component: async () => { + const { GeneralSettings } = + await import('./pages/Settings/General'); + return GeneralSettings; + }, + }, + { + intlLabel: { + id: 'my-plugin.settings.advanced', + defaultMessage: 'Advanced', + }, + id: 'advanced', + to: 'my-plugin/advanced', + Component: async () => { + const { AdvancedSettings } = + await import('./pages/Settings/Advanced'); + return AdvancedSettings; + }, + }, + ]); + // highlight-end + }, +}; +``` + + + + +```ts title="admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; + +export default { + register(app: StrapiApp) { + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + }); + }, + bootstrap(app: StrapiApp) { + // Add a single link to the global settings section + // highlight-start + app.addSettingsLink('global', { + intlLabel: { + id: 'my-plugin.settings.documentation', + defaultMessage: 'Documentation', + }, + id: 'documentation', + to: 'my-plugin/documentation', + Component: async () => { + const { DocumentationPage } = + await import('./pages/Settings/Documentation'); + return DocumentationPage; + }, + permissions: [], + licenseOnly: false, + }); + // highlight-end + + // Add multiple links at once to the global settings section + // highlight-start + app.addSettingsLinks('global', [ + { + intlLabel: { + id: 'my-plugin.settings.general', + defaultMessage: 'General', + }, + id: 'general', + to: 'my-plugin/general', + Component: async () => { + const { GeneralSettings } = + await import('./pages/Settings/General'); + return GeneralSettings; + }, + }, + { + intlLabel: { + id: 'my-plugin.settings.advanced', + defaultMessage: 'Advanced', + }, + id: 'advanced', + to: 'my-plugin/advanced', + Component: async () => { + const { AdvancedSettings } = + await import('./pages/Settings/Advanced'); + return AdvancedSettings; + }, + }, + ]); + // highlight-end + }, +}; +``` + + + + +`addSettingsLink` and `addSettingsLinks` both take a `sectionId` string as the first argument (e.g., `'global'` or `'permissions'`). The second argument is a single link object for `addSettingsLink` or an array of link objects for `addSettingsLinks`, using the same properties as the `links` array in [`createSettingSection()` (see table above)](#creating-a-new-settings-section). + +### Available settings sections + +Strapi provides built-in settings sections that plugins can extend: + +- `global`: General application settings +- `permissions`: Administration panel settings + +:::note +Creating a new settings section must be done in the `register` lifecycle function. Adding links to existing settings sections must be done in the `bootstrap` lifecycle function. +::: + +### Path conventions for `to` + +The `to` parameter behaves differently depending on the context: + +| Context | `to` value | Final URL | +|---|---|---| +| `addMenuLink` | `/plugins/my-plugin` | `http://localhost:1337/admin/plugins/my-plugin` | +| `createSettingSection` link | `my-plugin/general` | `http://localhost:1337/admin/settings/my-plugin/general` | +| `addSettingsLink` | `my-plugin/documentation` | `http://localhost:1337/admin/settings/my-plugin/documentation` | + +For menu links, the path is relative to the admin panel root (`/admin`). For settings links, the path is relative to the settings route (`/admin/settings`). Do not include the `settings/` prefix in settings link paths. + +:::strapi Securing plugin routes +The `permissions` parameter on links only controls visibility in the navigation. To fully protect your plugin pages and register RBAC actions, see the [Admin permissions for plugins](/cms/plugins-development/guides/admin-permissions-for-plugins) guide. +::: \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/admin-panel-api.md b/docusaurus/docs/cms/plugins-development/admin-panel-api.md index ae4e04691e..1eb29b6ad6 100644 --- a/docusaurus/docs/cms/plugins-development/admin-panel-api.md +++ b/docusaurus/docs/cms/plugins-development/admin-panel-api.md @@ -1,48 +1,37 @@ --- -title: Admin Panel API -pagination_prev: cms/plugins-development/plugin-structure -pagination_next: cms/plugins-development/content-manager-apis +title: Admin Panel API overview +pagination_prev: cms/plugins-development/plugin-sdk +pagination_next: cms/plugins-development/admin-navigation-settings toc_max_heading_level: 4 tags: - admin panel +- admin panel customization +- admin panel API - plugin APIs -- asynchronous function -- bootstrap function -- hooks API -- Injection Zones API - lifecycle function -- menu -- settings - plugins - plugins development -- register function -- reducers API -- redux --- -# Admin Panel API for plugins +import Prerequisite from '/docs/snippets/plugins-development-create-plugin-prerequisite.md' + +# Admin Panel API for plugins: An overview -The Admin Panel API exposes `register`, `bootstrap`, and `registerTrads` hooks to inject React components and translations into Strapi’s UI. Menu, settings, injection zone, reducer, and hook APIs let plugins add navigation, configuration panels, or custom actions. +The Admin Panel API exposes `register`, `bootstrap`, and `registerTrads` hooks to inject React components and translations into Strapi's UI. Menu, settings, injection zone, reducer, and hook APIs let plugins add navigation, configuration panels, or custom actions. -A Strapi plugin can interact with both the [back end](/cms/plugins-development/server-api) 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](/cms/intro). +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](/cms/intro). -The admin panel is a application that can embed other React applications. These other React applications are the admin parts of each Strapi plugin. +For more information on how plugins can interact with the back end part of Strapi, see [Server API](/cms/plugins-development/server-api). -:::prerequisites -You have [created a Strapi plugin](/cms/plugins-development/create-a-plugin). -::: +## General considerations -The Admin Panel API includes: +The admin panel of Strapi is a application that can embed other React applications. These other React applications are the admin parts of each Strapi feature or plugin. -- an [entry file](#entry-file) which exports the required interface, -- [lifecycle functions](#lifecycle-functions) and the `registerTrad()` [async function](#async-function), -- and several [specific APIs](#available-actions) for your plugin to interact with the admin panel. +To customize the admin panel of Strapi, you can use plugins and tap into the Admin Panel API. This consists in editing an [entry file](#entry-file) to export all the required interface, and choosing which [actions](#available-actions) you want to perform. -:::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](/cms/plugins-development/plugin-structure) created by the `strapi generate plugin` CLI generator command. -::: + ## Entry file @@ -50,14 +39,14 @@ The entry file for the Admin Panel API is `[plugin-name]/admin/src/index.js`. Th | Function type | Available functions | | ------------------- | ------------------------------------------------------------------------ | -| Lifecycle functions |
  • [register](#register)
  • [bootstrap](#bootstrap)
| -| Async function | [registerTrads](#registertrads) | +| Lifecycle functions |
  • [`register()`](#register)
  • [`bootstrap()`](#bootstrap)
| +| Async function | `registerTrads()` (see [admin localization](/cms/plugins-development/admin-localization) for details) | -## Lifecycle functions - -
+:::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](/cms/plugins-development/plugin-structure) created by the `strapi generate plugin` CLI generator command. +::: -### register() +### register() {#register} **Type:** `Function` @@ -65,34 +54,18 @@ This function is called to load the plugin, even before the app is actually [boo Within the register function, a plugin can: -* [register itself](#registerplugin) so it's available to the admin panel -* add a new link to the main navigation (see [Menu API](#menu-api)) -* [create a new settings section](#createsettingsection) -* define [injection zones](#injection-zones-api) -* [add reducers](#reducers-api) - -#### 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](#injection-zones-api) | - -:::note -Some parameters can be imported from the `package.json` file. -::: +* register itself through the [`registerPlugin`](#registerplugin) function so the plugin is available in the admin panel +* add a new link to the main navigation (see [Admin navigation & settings](/cms/plugins-development/admin-navigation-settings#navigation-sidebar-menu-links)) +* [create a new settings section](/cms/plugins-development/admin-navigation-settings#creating-a-new-settings-section) +* define [injection zones](/cms/plugins-development/admin-injection-zones) +* [add reducers](/cms/plugins-development/admin-redux-store#adding-custom-reducers) **Example:** -```js title="my-plugin/admin/src/index.js" + + +```js title="my-plugin/admin/src/index.js" // Auto-generated component import PluginIcon from './components/PluginIcon'; import pluginId from './pluginId' @@ -121,667 +94,197 @@ export default { }; ``` -### bootstrap() - -**Type**: `Function` - -Exposes the bootstrap function, executed after all the plugins are [registered](#register). - -Within the bootstrap function, a plugin can, for instance: - -* extend another plugin, using `getPlugin('plugin-name')`, -* register hooks (see [Hooks API](#hooks-api)), -* [add links to a settings section](#settings-api), -* add actions and options to the Content Manager's List view and Edit view (see details on the [Content Manager APIs page](/cms/plugins-development/content-manager-apis)). - -**Example:** - -```js -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()`](#register) and [`bootstrap()`](#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 - -```jsx -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](#menu-api) | [`addMenuLink()`](#menu-api) | [`register()`](#register) | -| Create a new settings section | [Settings API](#settings-api) | [`createSettingSection()`](#createsettingsection) | [`register()`](#register) | -| Declare an injection zone | [Injection Zones API](#injection-zones-api) | [`registerPlugin()`](#registerplugin) | [`register()`](#register) | -| Add a reducer | [Reducers API](#reducers-api) | [`addReducers()`](#reducers-api) | [`register()`](#register) | -| Create a hook | [Hooks API](#hooks-api) | [`createHook()`](#hooks-api) | [`register()`](#register) | -| Add a single link to a settings section | [Settings API](#settings-api) | [`addSettingsLink()`](#addsettingslink) | [`bootstrap()`](#bootstrap) | -| Add multiple links to a settings section | [Settings API](#settings-api) | [`addSettingsLinks()`](#addsettingslinks) | [`bootstrap()`](#bootstrap) | -| Inject a Component in an injection zone | [Injection Zones API](#injection-zones-api) | [`injectComponent()`](#injection-zones-api) | [`bootstrap()`](#register) | -| Add options and actions to the Content Manager's Edit view and List view | [Content Manager APIs](/cms/plugins-development/content-manager-apis) |
  • `addEditViewSidePanel()`
  • `addDocumentAction`
  • `addDocumentHeaderAction`
  • `addBulkAction`
| [`bootstrap()`](#bootstrap) | -| Register a hook | [Hooks API](#hooks-api) | [`registerHook()`](#hooks-api) | [`bootstrap()`](#bootstrap) | - -:::tip Replacing the WYSIWYG -The WYSIWYG editor can be replaced by taking advantage of [custom fields](/cms/features/custom-fields), for instance using the . -::: - -:::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`. -::: - -### 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 convention, with:
  • `id`: id used to insert the localized label
  • `defaultMessage`: default label for the link
| -| `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 | -| `position` | Integer | Position in the menu | -| `licenseOnly` | Boolean | If 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:** - - - - -```jsx title="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() {}, -}; -``` - ```ts title="my-plugin/admin/src/index.ts" import PluginIcon from './components/PluginIcon'; +import pluginId from './pluginId'; import type { StrapiApp } from '@strapi/admin/strapi-admin'; + export default { register(app: StrapiApp) { app.addMenuLink({ - to: '/plugins/my-plugin', + to: `/plugins/${pluginId}`, icon: PluginIcon, intlLabel: { - id: 'my-plugin.plugin.name', + id: `${pluginId}.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 + 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, }); - app.registerPlugin({ ... }); }, - bootstrap() {}, }; ``` -### Settings API - -The Settings API allows: - -* [creating a new setting section](#createsettingsection) -* adding [a single link](#addsettingslink) or [multiple links at once](#addsettingslinks) to existing settings sections - -:::note -Adding a new section happens in the [register](#register) lifecycle while adding links happens during the [bootstrap](#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 convention, with:
  • `id`: id used to insert the localized label
  • `defaultMessage`: default label for the link
| -| `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 | -| `licenseOnly` | Boolean | If 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() +#### registerPlugin() {#registerplugin} -**Type**: `Function` - -Create a new settings section. +**Type:** `Function` -The function takes 2 arguments: +Registers the plugin to make it available in the admin panel. This function is called within the [`register()`](#register) lifecycle function and returns an object with the following parameters: -| Argument | Type | Description | -| --------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| first argument | Object | Section label:
  • `id` (String): section id
  • `intlLabel` (Object): localized label for the section, following the convention, with:
    • `id`: id used to insert the localized label
    • `defaultMessage`: default label for the section
| -| second argument | Array of Objects | Links included in the section | +| Parameter | Type | Description | +| ---------------- | ------------------------ | -------------------------------------------------------------------------------------------------- | +| `id` | String | Plugin id | +| `name` | String | Plugin name | +| `apis` | `Record` | APIs exposed to other plugins | +| `initializer` | `React.ComponentType` | Component for plugin initialization | +| `injectionZones` | Object | Declaration of available [injection zones](/cms/plugins-development/admin-injection-zones) | +| `isReady` | Boolean | Plugin readiness status (default: `true`) | :::note -`intlLabel.id` are ids used in translation files (`[plugin-name]/admin/src/translations/[language].json`) +Some parameters can be imported from the `package.json` file. ::: **Example:** -```jsx title="my-plugin/admin/src/index.js" - -const myComponent = async () => { - const component = await import( - /* webpackChunkName: "users-providers-settings-page" */ './pages/Providers' - ); - - return component; -}; + + +```js title="my-plugin/admin/src/index.js" 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[], - }, - ] - ); + app.registerPlugin({ + id: 'my-plugin', + name: 'My Plugin', + apis: { + // APIs exposed to other plugins + }, + initializer: MyInitializerComponent, + injectionZones: { + // Areas where other plugins can inject components + }, + isReady: false, + }); }, }; ``` -#### addSettingsLink() - -**Type**: `Function` - -Add a unique link to an existing settings section. - -**Example:** - -```jsx title="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 - } - ) - } -} -``` - -#### addSettingsLinks() - -**Type**: `Function` - -Add multiple links to an existing settings section. - -**Example:** - -```jsx title="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: - -* Strapi's [predefined injection zones](#using-predefined-injection-zones) for the Content Manager, -* or custom injection zones, created by a plugin - -:::note -Injection zones are defined in the [register()](#register) lifecycle but components are injected in the [bootstrap()](#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](/cms/intro): - - - -| View | Injection zone name & Location | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| List view | `actions`: sits between Filters and the cogs icon -| 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 `` 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](#using-predefined-injection-zones), use the `getPlugin('content-manager').injectComponent()` function instead - -Both the `injectComponent()` and `getPlugin('content-manager').injectComponent()` 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:
  • `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: - -```jsx title="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: - -```jsx title="my-plugin/admin/src/injectionZones.js" -// Use the injection zone in a view - -import { InjectionZone } from '@strapi/helper-plugin'; - -const HomePage = () => { - return ( -
-

This is the homepage

- -
- ); -}; -``` + + -```jsx title="my-plugin/admin/src/index.js" -// Declare this injection zone in the register lifecycle of the plugin +```ts title="my-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; export default { - register() { + register(app: StrapiApp) { app.registerPlugin({ - // ... + id: 'my-plugin', + name: 'My Plugin', + apis: { + // APIs exposed to other plugins + }, + initializer: MyInitializerComponent, injectionZones: { - homePage: { - right: [] - } - } + // Areas where other plugins can inject components + }, + isReady: false, }); }, -} -``` - -```jsx title="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 `useContentManagerContext` 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 `useContentManagerContext` React hook. - -
-Example of a basic component using the 'useContentManagerContext' hook - -```js -import { - unstable_useContentManagerContext as useContentManagerContext, -} from '@strapi/strapi/admin'; - -const MyCompo = () => { - const { - slug, - isCreatingEntry, - isSingleType, - hasDraftAndPublish, - layout, - components, - contentType, - form, - model, - collectionType, - id, - } = useContentManagerContext(); - - // Form state and handlers - const { - initialValues, - values, - onChange, - } = form; - - /** - * Layout structure: - * - * `layout` is grouped by Content Manager views. - * - * - layout.edit.layout → edit view layout definition - * - layout.edit.components → component layouts used in the edit view - * - layout.list.layout → list view layout definition - */ - const { - edit: { layout: editLayout, components: editComponents }, - list: { layout: listLayout }, - } = layout; - - return null; -}; - -export default MyCompo; -``` - -
+
+
-### Reducers API +### bootstrap() {#bootstrap} -Reducers are reducers that can be used to share state between components. Reducers can be useful when: +**Type**: `Function` -* 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. +Exposes the bootstrap function, executed after all the plugins are [registered](#register). -Reducers can be added to a plugin interface with the `addReducers()` function during the [`register`](#register) lifecycle. +Within the bootstrap function, a plugin can, for instance: -A reducer is declared as an object with this syntax: +* extend another plugin, using `getPlugin('plugin-name')`, +* register hooks (see [Hooks](/cms/plugins-development/admin-hooks)), +* [add links to a settings section](/cms/plugins-development/admin-navigation-settings#adding-links-to-existing-settings-sections), +* add actions and options to the Content Manager's List view and Edit view (see details on the [Content Manager APIs page](/cms/plugins-development/content-manager-apis)). **Example:** -```js title="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() {}, +```js title="my-plugin/admin/src/index.js" +module.exports = () => { + return { + // ... + bootstrap(app) { + // execute some bootstrap code + app.getPlugin('content-manager').injectComponent('editView', 'right-links', { name: 'my-compo', Component: () => 'my-compo' }) + }, + }; }; - - ``` -### 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](#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 - -```jsx title="my-plugin/admin/src/index.js" -// Create a hook in a plugin -export default { - register(app) { - app.createHook('My-PLUGIN/MY_HOOK'); - } -} + + -``` +```ts title="my-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; -```jsx title="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({...}) - } -} + // ... + bootstrap(app: StrapiApp) { + // execute some bootstrap code + app.getPlugin('content-manager').injectComponent('editView', 'right-links', { name: 'my-compo', Component: () => 'my-compo' }) + }, +}; ``` -
- -#### 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](/cms/intro): +## Available actions -```jsx -runHookWaterfall(INJECT_COLUMN_IN_TABLE, { - displayedHeaders: ListFieldLayout[], - layout: ListFieldLayout, -}); -``` +The Admin Panel API allows a plugin to take advantage of several small APIs to perform actions that will modify the user interface, user experience, or behavior of the admin panel. -```tsx -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, - { 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; -} +Use the following table as a reference to know which API and function to use, and where to declare them, and click on any function name to learn more: -type LayoutOptions = Schema['options'] & Schema['pluginOptions'] & object; +| Action | Function to use | Related lifecycle function | +| ---------------------------------------- | ------------------------------------------------- | --------------------------- | +| Add a new link to the main navigation | [`addMenuLink()`](/cms/plugins-development/admin-navigation-settings#navigation-sidebar-menu-links) | [`register()`](#register) | +| Create a new settings section | [`createSettingSection()`](/cms/plugins-development/admin-navigation-settings#creating-a-new-settings-section) | [`register()`](#register) | +| Add a single link to a settings section | [`addSettingsLink()`](/cms/plugins-development/admin-navigation-settings#adding-links-to-existing-settings-sections) | [`bootstrap()`](#bootstrap) | +| Add multiple links to a settings section | [`addSettingsLinks()`](/cms/plugins-development/admin-navigation-settings#adding-links-to-existing-settings-sections) | [`bootstrap()`](#bootstrap) | +| Add panels, options, and actions to the Content Manager's Edit view and List view |
  • [`addEditViewSidePanel()`](/cms/plugins-development/content-manager-apis#addeditviewsidepanel)
  • [`addDocumentAction()`](/cms/plugins-development/content-manager-apis#adddocumentaction)
  • [`addDocumentHeaderAction()`](/cms/plugins-development/content-manager-apis#adddocumentheaderaction)
  • [`addBulkAction()`](/cms/plugins-development/content-manager-apis#addbulkaction)
| [`bootstrap()`](#bootstrap) | +| Declare an injection zone | [`registerPlugin()`](#registerplugin) | [`register()`](#register) | +| Inject a component in an injection zone | [`injectComponent()`](/cms/plugins-development/admin-injection-zones) | [`bootstrap()`](#bootstrap) | +| Add a reducer | [`addReducers()`](/cms/plugins-development/admin-redux-store#adding-custom-reducers) | [`register()`](#register) | +| Create a hook | [`createHook()`](/cms/plugins-development/admin-hooks) | [`register()`](#register) | +| Register a hook | [`registerHook()`](/cms/plugins-development/admin-hooks) | [`bootstrap()`](#bootstrap) | +| Provide translations for your plugin's admin interface | [`registerTrads()`](/cms/plugins-development/admin-localization#registertrads) | _(Handled in the async `registerTrads()` function itself)_ | -interface LayoutSettings extends Contracts.ContentTypes.Settings { - displayName?: string; - icon?: never; -} -``` +
+Click on any of the following cards to get more details about a specific topic: -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](/cms/intro): - -```tsx -interface EditLayout { - layout: Array>; - components: { - [uid: string]: { - layout: Array; - 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 { - 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; - type: K; - }; -}[Attribute.Kind]; + + + + + + + + -type LayoutOptions = Schema['options'] & Schema['pluginOptions'] & object; +:::tip Replacing the WYSIWYG +The WYSIWYG editor can be replaced by taking advantage of [custom fields](/cms/features/custom-fields), for instance using the . +::: -interface LayoutSettings extends Contracts.ContentTypes.Settings { - displayName?: string; - icon?: never; -} -``` +:::info +The admin panel supports dotenv variables. -:::note -`EditViewLayout` and `ListViewLayout` are parts of the `useDocumentLayout` hook (see ). -::: +All variables defined in a `.env` file and prefixed by `STRAPI_ADMIN_` are available while customizing the admin panel through `process.env`. +::: \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/admin-redux-store.md b/docusaurus/docs/cms/plugins-development/admin-redux-store.md new file mode 100644 index 0000000000..17ad08918a --- /dev/null +++ b/docusaurus/docs/cms/plugins-development/admin-redux-store.md @@ -0,0 +1,646 @@ +--- +title: Redux store & reducers +description: Add custom reducers, read state, dispatch actions, and subscribe to changes in Strapi's admin panel Redux store from your plugin. +pagination_prev: cms/plugins-development/admin-injection-zones +pagination_next: cms/plugins-development/admin-hooks +displayed_sidebar: cmsSidebar +toc_max_heading_level: 4 +tags: + - admin panel + - admin panel customization + - admin panel API + - plugins development +--- + +import Prerequisite from '/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md' + +# Admin Panel API: Redux store & reducers + + + +Use `addReducers()` during `register` to add custom state to the Redux store. Then read state with `useSelector`, update it with `useDispatch`, and subscribe to changes with `useStore`. The `admin_app` slice exposes theme, locale, permissions, and authentication data. + + + +Strapi's admin panel uses a global Redux store to manage application state. Plugins can access this store to read state, dispatch actions, and subscribe to state changes. This enables plugins to interact with core admin functionality like theme settings, language preferences, and authentication state. + + + +## Store overview + +The Redux store is automatically provided to all plugin components through React Redux's `Provider`. The store contains several slices: + +- `admin_app`: core admin state including theme, language, permissions, and authentication token +- `adminApi`: RTK Query API state for admin endpoints +- Plugin-specific slices: additional reducers added by plugins + +## Adding custom reducers + +Reducers are 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`](/cms/plugins-development/admin-panel-api#register) lifecycle. + +A reducer is declared as an object with this syntax: + + + + +```js title="my-plugin/admin/src/index.js" +import { exampleReducer } from './reducers' +import pluginId from './pluginId' + +const reducers = { + // Reducer Syntax + [`${pluginId}_exampleReducer`]: exampleReducer +} + +export default { + register(app) { + app.addReducers(reducers) + }, + bootstrap() {}, +}; +``` + + + + +```ts title="my-plugin/admin/src/index.ts" +import type { StrapiApp } from '@strapi/admin/strapi-admin'; +import { exampleReducer } from './reducers'; +import pluginId from './pluginId'; + +const reducers = { + [`${pluginId}_exampleReducer`]: exampleReducer, +}; + +export default { + register(app: StrapiApp) { + app.addReducers(reducers); + }, + bootstrap() {}, +}; +``` + + + + +## Reading state with `useSelector` + +The most common way to access Redux state in your plugin components is using the `useSelector` hook from `react-redux`: + + + + +```jsx title="admin/src/pages/HomePage.jsx" +import { useSelector } from 'react-redux'; + +const HomePage = () => { + // Read current theme + const currentTheme = useSelector( + (state) => state.admin_app?.theme?.currentTheme + ); + + // Read current locale + const currentLocale = useSelector( + (state) => state.admin_app?.language?.locale + ); + + // Read authentication status + const isAuthenticated = useSelector((state) => !!state.admin_app?.token); + + // Read available locales + const availableLocales = useSelector( + (state) => state.admin_app?.language?.localeNames || {} + ); + + return ( +
+

Current Theme: {currentTheme}

+

Current Locale: {currentLocale}

+

Authenticated: {isAuthenticated ? 'Yes' : 'No'}

+
+ ); +}; +``` + +
+ + +```tsx title="admin/src/pages/HomePage.tsx" +import { useSelector } from 'react-redux'; + +const HomePage = () => { + // Read current theme + const currentTheme = useSelector( + (state: any) => state.admin_app?.theme?.currentTheme + ); + + // Read current locale + const currentLocale = useSelector( + (state: any) => state.admin_app?.language?.locale + ); + + // Read authentication status + const isAuthenticated = useSelector((state: any) => !!state.admin_app?.token); + + // Read available locales + const availableLocales = useSelector( + (state: any) => state.admin_app?.language?.localeNames || {} + ); + + return ( +
+

Current Theme: {currentTheme}

+

Current Locale: {currentLocale}

+

Authenticated: {isAuthenticated ? 'Yes' : 'No'}

+
+ ); +}; +``` + +
+
+ +### Available state properties + +The `admin_app` slice contains the following state properties: + +| Property | Type | Description | +|---|---|---| +| `theme.currentTheme` | `string` | Current theme (`'light'`, `'dark'`, or `'system'`) | +| `theme.availableThemes` | `string[]` | Array of available theme names | +| `language.locale` | `string` | Current locale code (e.g., `'en'`, `'fr'`) | +| `language.localeNames` | `object` | Object mapping locale codes to display names | +| `token` | `string \| null` | Authentication token | +| `permissions` | `object` | User permissions object | + +## Dispatching actions + +To update the Redux store, use the `useDispatch` hook: + +:::note +The examples below dispatch actions to core admin state (theme, locale) for illustration purposes. In practice, most plugins should dispatch actions to their own custom reducers rather than modifying global admin state. +::: + + + + +```jsx title="admin/src/pages/HomePage.jsx" +import { useSelector, useDispatch } from 'react-redux'; + +const HomePage = () => { + const dispatch = useDispatch(); + const currentTheme = useSelector( + (state) => state.admin_app?.theme?.currentTheme + ); + + const handleToggleTheme = () => { + const newTheme = + currentTheme === 'light' + ? 'dark' + : currentTheme === 'dark' + ? 'system' + : 'light'; + dispatch({ + type: 'admin/setAppTheme', + payload: newTheme, + }); + }; + + const handleChangeLocale = (locale) => { + dispatch({ + type: 'admin/setLocale', + payload: locale, + }); + }; + + return ( +
+ + +
+ ); +}; +``` + +
+ + +```tsx title="admin/src/pages/HomePage.tsx" +import { useSelector, useDispatch } from 'react-redux'; + +const HomePage = () => { + const dispatch = useDispatch(); + const currentTheme = useSelector( + (state: any) => state.admin_app?.theme?.currentTheme + ); + + const handleToggleTheme = () => { + const newTheme = + currentTheme === 'light' + ? 'dark' + : currentTheme === 'dark' + ? 'system' + : 'light'; + dispatch({ + type: 'admin/setAppTheme', + payload: newTheme, + } as any); + }; + + const handleChangeLocale = (locale: string) => { + dispatch({ + type: 'admin/setLocale', + payload: locale, + } as any); + }; + + return ( +
+ + +
+ ); +}; +``` + +
+
+ +### Available actions + + + +The `admin_app` slice provides the following actions: + +| Action type | Payload type | Description | +|---|---|---| +| `admin/setAppTheme` | `string` | Set the theme (`'light'`, `'dark'`, or `'system'`) | +| `admin/setAvailableThemes` | `string[]` | Updates `theme.availableThemes` in `admin_app` | +| `admin/setLocale` | `string` | Set the locale (e.g., `'en'`, `'fr'`) | +| `admin/setToken` | `string \| null` | Set the authentication token | +| `admin/login` | `{ token: string, persist?: boolean }` | Login action with token and persistence option | +| `admin/logout` | `void` | Logout action (no payload) | + +:::note +When dispatching actions, use the Redux Toolkit action type format: `'sliceName/actionName'`. The admin slice is named `'admin'`, so actions follow the pattern `'admin/actionName'`. +::: + +## Accessing the store instance + +For advanced use cases, you can access the store instance directly using the `useStore` hook: + + + + +```jsx title="admin/src/pages/App.jsx" +import { useStore } from 'react-redux'; +import { useEffect } from 'react'; + +const App = () => { + const store = useStore(); + + useEffect(() => { + const state = store.getState(); + console.log('Redux Store State:', state); + + const unsubscribe = store.subscribe(() => { + const currentState = store.getState(); + console.log('Store state changed:', { + theme: currentState.admin_app?.theme?.currentTheme, + locale: currentState.admin_app?.language?.locale, + timestamp: new Date().toISOString(), + }); + }); + + return () => { + unsubscribe(); + }; + }, [store]); + + return
My Plugin
; +}; +``` + +
+ + +```tsx title="admin/src/pages/App.tsx" +import { useStore } from 'react-redux'; +import { useEffect } from 'react'; + +const App = () => { + const store = useStore(); + + useEffect(() => { + const state = store.getState(); + console.log('Redux Store State:', state); + + const unsubscribe = store.subscribe(() => { + const currentState = store.getState(); + console.log('Store state changed:', { + theme: currentState.admin_app?.theme?.currentTheme, + locale: currentState.admin_app?.language?.locale, + timestamp: new Date().toISOString(), + }); + }); + + return () => { + unsubscribe(); + }; + }, [store]); + + return
My Plugin
; +}; +``` + +
+
+ +## Complete example + +The following example combines all 3 patterns (useSelector, useDispatch, useStore) described on the present page: + + + + + + +```jsx title="admin/src/pages/HomePage.jsx" +import { Main } from '@strapi/design-system'; +import { Button, Box, Typography, Flex } from '@strapi/design-system'; +import { useSelector, useDispatch, useStore } from 'react-redux'; +import { useEffect, useState } from 'react'; + +const HomePage = () => { + const dispatch = useDispatch(); + const store = useStore(); + + // Reading state + const currentTheme = useSelector( + (state) => state.admin_app?.theme?.currentTheme + ); + const currentLocale = useSelector( + (state) => state.admin_app?.language?.locale + ); + const isAuthenticated = useSelector((state) => !!state.admin_app?.token); + const availableLocales = useSelector( + (state) => state.admin_app?.language?.localeNames || {} + ); + + // Dispatching actions + const handleToggleTheme = () => { + const newTheme = + currentTheme === 'light' + ? 'dark' + : currentTheme === 'dark' + ? 'system' + : 'light'; + dispatch({ type: 'admin/setAppTheme', payload: newTheme }); + }; + + const handleChangeLocale = (locale) => { + dispatch({ type: 'admin/setLocale', payload: locale }); + }; + + // Subscribing to store changes + const [storeChangeCount, setStoreChangeCount] = useState(0); + const [lastChange, setLastChange] = useState(''); + + useEffect(() => { + const unsubscribe = store.subscribe(() => { + setStoreChangeCount((prev) => prev + 1); + setLastChange(new Date().toLocaleTimeString()); + }); + return () => unsubscribe(); + }, [store]); + + return ( +
+ + + Redux Store Examples + + + + + + Reading state + + + + Current Theme: {currentTheme || 'system'} + + + Current Locale: {currentLocale || 'en'} + + + Authentication Status:{' '} + + {isAuthenticated ? 'Authenticated' : 'Not Authenticated'} + + + + + + + + Dispatching actions + + + + {Object.keys(availableLocales).map((locale) => ( + + ))} + + + + + + Subscribing to store changes + + + + Store has changed {storeChangeCount} time(s) + + {lastChange && ( + + Last change at: {lastChange} + + )} + + + + +
+ ); +}; + +export { HomePage }; +``` + +
+ +
+ +
+ + + + +```tsx title="admin/src/pages/HomePage.tsx" +import { Main } from '@strapi/design-system'; +import { Button, Box, Typography, Flex } from '@strapi/design-system'; +import { useSelector, useDispatch, useStore } from 'react-redux'; +import { useEffect, useState } from 'react'; + +const HomePage = () => { + const dispatch = useDispatch(); + const store = useStore(); + + // Reading state + const currentTheme = useSelector( + (state: any) => state.admin_app?.theme?.currentTheme + ); + const currentLocale = useSelector( + (state: any) => state.admin_app?.language?.locale + ); + const isAuthenticated = useSelector((state: any) => !!state.admin_app?.token); + const availableLocales = useSelector( + (state: any) => state.admin_app?.language?.localeNames || {} + ); + + // Dispatching actions + const handleToggleTheme = () => { + const newTheme = + currentTheme === 'light' + ? 'dark' + : currentTheme === 'dark' + ? 'system' + : 'light'; + dispatch({ type: 'admin/setAppTheme', payload: newTheme } as any); + }; + + const handleChangeLocale = (locale: string) => { + dispatch({ type: 'admin/setLocale', payload: locale } as any); + }; + + // Subscribing to store changes + const [storeChangeCount, setStoreChangeCount] = useState(0); + const [lastChange, setLastChange] = useState(''); + + useEffect(() => { + const unsubscribe = store.subscribe(() => { + setStoreChangeCount((prev) => prev + 1); + setLastChange(new Date().toLocaleTimeString()); + }); + return () => unsubscribe(); + }, [store]); + + return ( +
+ + + Redux Store Examples + + + + + + Reading state + + + + Current Theme: {currentTheme || 'system'} + + + Current Locale: {currentLocale || 'en'} + + + Authentication Status:{' '} + + {isAuthenticated ? 'Authenticated' : 'Not Authenticated'} + + + + + + + + Dispatching actions + + + + {Object.keys(availableLocales).map((locale) => ( + + ))} + + + + + + Subscribing to store changes + + + + Store has changed {storeChangeCount} time(s) + + {lastChange && ( + + Last change at: {lastChange} + + )} + + + + +
+ ); +}; + +export { HomePage }; +``` + +
+ +
+ +
+
+ +## Best practices + +- **Use `useSelector` for reading state.** Prefer [`useSelector`](#reading-state-with-useselector) over direct store access. It automatically subscribes to updates and re-renders components when the selected state changes. +- **Clean up subscriptions.** Always unsubscribe from store subscriptions in `useEffect` cleanup functions to prevent memory leaks. +- **Consider type safety.** For Redux state access in plugins, use `react-redux` hooks (`useSelector`, `useDispatch`) with plugin-local typing (for example `RootState` and `AppDispatch`). If you use Strapi admin utilities, import them from `@strapi/admin/strapi-admin` (not `@strapi/admin`). Avoid relying on undocumented typed Redux hooks as part of Strapi's public API until they are explicitly documented as stable. +- **Avoid unnecessary dispatches.** Only dispatch actions when you need to update state. Reading state does not require dispatching actions. +- **Respect core state.** Be careful when modifying core admin state (like theme or locale) as it affects the entire admin panel. Consider whether your plugin should modify global state or maintain its own local state. + +:::tip +To add your own state to the Redux store, see [Adding custom reducers](#adding-custom-reducers) above. +::: \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/content-manager-apis.md b/docusaurus/docs/cms/plugins-development/content-manager-apis.md index b5d551399d..ef718e64fc 100644 --- a/docusaurus/docs/cms/plugins-development/content-manager-apis.md +++ b/docusaurus/docs/cms/plugins-development/content-manager-apis.md @@ -1,62 +1,104 @@ --- title: Content Manager APIs description: The Content Manager APIs reference lists the APIs available to plugins for adding actions and options to the Content Manager List view and Edit view. +pagination_prev: cms/plugins-development/admin-navigation-settings +pagination_next: cms/plugins-development/admin-injection-zones displayed_sidebar: cmsSidebar toc_max_heading_level: 4 tags: +- admin panel - admin panel API +- admin panel customization - plugins development -- plugins --- +import Prerequisite from '/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md' +import InjectionVsCmApis from '/docs/snippets/injection-zones-vs-content-manager-apis.md' + # Content Manager APIs Content Manager APIs add panels and actions to list or edit views through `addEditViewSidePanel`, `addDocumentAction`, `addDocumentHeaderAction`, or `addBulkAction`. Each API accepts component functions with typed contexts, enabling precise control over document-aware UI injections. -Content-Manager APIs are part of the [Admin Panel API](/cms/plugins-development/admin-panel-api). They are a way to add content or options from plugins to the [Content-Manager](/cms/features/content-manager), so you can extend the Content-Manager by adding functionality from your own plugin, just like you can do it with [Injection Zones](/cms/plugins-development/admin-panel-api#injection-zones-api). - -Strapi 5 provides 4 Content-Manager APIs, all accessible through `app.getPlugin('content-manager').apis`: +Content Manager APIs are part of the [Admin Panel API](/cms/plugins-development/admin-panel-api). They are a way for Strapi plugins to add content or options to the [Content Manager](/cms/features/content-manager). The Content Manager APIs allow you to extend the Content Manager by adding functionality from your own plugin, just like you can do it with [Injection zones](/cms/plugins-development/admin-injection-zones). -- [`addEditViewSidePanel`](#addeditviewsidepanel), -- [`addDocumentAction`](#adddocumentaction), -- [`addDocumentHeaderAction`](#adddocumentheaderaction), -- and [`addBulkAction`](#addbulkaction). + ## General information +Strapi 5 provides 4 Content Manager APIs, all accessible through `app.getPlugin('content-manager').apis`. All the Content Manager APIs share the same API shape and must use components. +### Injection zones vs. Content Manager APIs + + + ### API shape -All Content Manager APIs works in the same way: to use them, call them on your plugin’s [bootstrap](/cms/plugins-development/admin-panel-api#bootstrap) function, in 2 possible ways. +All Content Manager APIs works in the same way: to use them, call them on your plugin's [`bootstrap()`](/cms/plugins-development/admin-panel-api#bootstrap) function, in 2 possible ways: -When using TypeScript, the `apis` property returned by `app.getPlugin()` is typed as `unknown`. Cast it to `ContentManagerPlugin['config']['apis']` before calling the APIs: +:::note +When using TypeScript, the `apis` property returned by `app.getPlugin()` is typed as `unknown`. Cast it to `ContentManagerPlugin['config']['apis']` before calling the APIs. +::: - Passing an array with what you want to add. For example, the following code would add the ReleasesPanel at the end of the current EditViewSidePanels: - ```tsx - import type { ContentManagerPlugin } from '@strapi/content-manager/strapi-admin'; + + + + ```js + const apis = app.getPlugin('content-manager').apis; + + apis.addEditViewSidePanel([ReleasesPanel]); + ``` + + + + + ```tsx + import type { ContentManagerPlugin } from '@strapi/content-manager/strapi-admin'; - const apis = - app.getPlugin('content-manager').apis as ContentManagerPlugin['config']['apis']; + const apis = + app.getPlugin('content-manager').apis as ContentManagerPlugin['config']['apis']; - apis.addEditViewSidePanel([ReleasesPanel]); - ``` + apis.addEditViewSidePanel([ReleasesPanel]); + ``` + + + - Passing a function that receives the current elements and return the new ones. This is useful if, for example, you want to add something in a specific position in the list, like in the following code: - ```tsx - const apis = - app.getPlugin('content-manager').apis as ContentManagerPlugin['config']['apis']; + + + + ```js + const apis = app.getPlugin('content-manager').apis; + + apis.addEditViewSidePanel((panels) => [SuperImportantPanel, ...panels]); + ``` + + + - apis.addEditViewSidePanel((panels) => [SuperImportantPanel, ...panels]); - ``` + ```tsx + const apis = + app.getPlugin('content-manager').apis as ContentManagerPlugin['config']['apis']; + + apis.addEditViewSidePanel((panels) => [SuperImportantPanel, ...panels]); + ``` + + + ### Components -You need to pass components to the API in order to add things to the Content Manager. These components are basically functions that receive some properties and return and object with some shape (depending on the function). Each component’s return object is different based on the function you’re using, but they receive similar properties, depending on whether you use a ListView or EditView API. These properties include important information about the document(s) you are viewing or editing. +You need to pass components to the API in order to add things to the Content Manager. + +Components are functions that receive some properties and return an object with some shape (depending on the function). Each component's return object is different based on the function you're using, but they receive similar properties, depending on whether you use a ListView or EditView API. + +Properties include important information about the document(s) you are viewing or editing. #### ListViewContext @@ -115,9 +157,31 @@ More information about types and APIs can be found in + + +```jsx title="my-plugin/components/my-panel.js" +const Panel = ({ + activeTab, + collectionType, + document, + documentId, + meta, + model +}) => { + return { + title: 'My Panel', + content:

I'm on {activeTab}

+ } +} +``` + +
+ + +```tsx title="my-plugin/components/my-panel.ts" import type { PanelComponent, PanelComponentProps } from '@strapi/content-manager/strapi-admin'; const Panel: PanelComponent = ({ @@ -135,6 +199,9 @@ const Panel: PanelComponent = ({ } ``` + +
+ ## Available APIs
@@ -203,9 +270,10 @@ interface DocumentActionDescription { * @default 'secondary' */ variant?: ButtonProps['variant']; + loading?: ButtonProps['loading']; } -type DocumentActionPosition = 'panel' | 'header' | 'table-row'; +type DocumentActionPosition = 'panel' | 'header' | 'table-row' | 'preview' | 'relation-modal'; interface DialogOptions { type: 'dialog'; @@ -312,4 +380,4 @@ interface BulkActionDescription { */ variant?: ButtonProps['variant']; } -``` +``` \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/create-a-plugin.md b/docusaurus/docs/cms/plugins-development/create-a-plugin.md index 15e51f1765..4d8cf29c07 100644 --- a/docusaurus/docs/cms/plugins-development/create-a-plugin.md +++ b/docusaurus/docs/cms/plugins-development/create-a-plugin.md @@ -9,6 +9,8 @@ tags: - plugins development --- +import UsingSDKplugin5 from '/docs/snippets/sdk-plugin-v5-v6.md' + # Plugin creation @@ -131,6 +133,17 @@ npm run build && npm run verify The above commands will not only build the plugin, but also verify that the output is valid and ready to be published. You can then publish your plugin to NPM as you would any other package. +:::tip Upgrading from SDK Plugin v5 +If you're upgrading from `@strapi/sdk-plugin` v5 to v6: +* Delete any `packup.config.ts` file from your plugin (it is no longer used). +* Rely on `package.json#exports` for build configuration (it is now derived automatically). +* Add `--sourcemap` to your build command if you need sourcemaps (they now default to off). + +No other changes are required. +::: + + + ## Working with the Plugin SDK in a monorepo environment {#monorepo} If you are working with a monorepo environment to develop your plugin, you don't need to use the `watch:link` command because the monorepo workspace setup will handle the symlink. You can use the `watch` command instead. @@ -183,7 +196,7 @@ This error often occurs when your plugin attempts to import core Strapi function import { unstable_useContentManagerContext as useContentManagerContext } from '@strapi/strapi/admin'; ``` -To resolve the issue, remove `@strapi/strapi` as a dev dependency from your plugin. This ensures that your plugin uses the same instance of Strapi’s core modules as the main application, preventing conflicts and the associated errors. +To resolve the issue, remove `@strapi/strapi` as a dev dependency from your plugin. This ensures that your plugin uses the same instance of Strapi's core modules as the main application, preventing conflicts and the associated errors. ## Setting a local plugin in a monorepo environment without the Plugin SDK @@ -194,7 +207,7 @@ In a monorepo, you can configure your local plugin without using the Plugin SDK ### Server entry point -The server entry point file initializes your plugin’s server-side functionalities. The expected structure for `strapi-server.js` (or its TypeScript variant) is: +The server entry point file initializes your plugin's server-side functionalities. The expected structure for `strapi-server.js` (or its TypeScript variant) is: ```js module.exports = () => { @@ -226,4 +239,4 @@ This object includes methods to register your plugin with the admin application, :::tip For a complete example of how to structure your local plugin in a monorepo environment, please check out our . -::: +::: \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/developing-plugins.md b/docusaurus/docs/cms/plugins-development/developing-plugins.md index ebfef00f3d..37d59d389d 100644 --- a/docusaurus/docs/cms/plugins-development/developing-plugins.md +++ b/docusaurus/docs/cms/plugins-development/developing-plugins.md @@ -47,6 +47,8 @@ Plugins can also be used to add [custom fields](/cms/features/custom-fields) to + +
diff --git a/docusaurus/docs/cms/plugins-development/guides/admin-permissions-for-plugins.md b/docusaurus/docs/cms/plugins-development/guides/admin-permissions-for-plugins.md index 0c176a3c93..5c8bded9b8 100644 --- a/docusaurus/docs/cms/plugins-development/guides/admin-permissions-for-plugins.md +++ b/docusaurus/docs/cms/plugins-development/guides/admin-permissions-for-plugins.md @@ -36,13 +36,13 @@ const bootstrap = ({ strapi }) => { { section: 'plugins', displayName: 'Access the overview page', - uid: 'overview', + uid: 'overview.access', pluginName: 'my-plugin', }, { section: 'plugins', displayName: 'Access the content manager sidebar', - uid: 'sidebar', + uid: 'sidebar.access', pluginName: 'my-plugin', }, ]; @@ -146,7 +146,7 @@ import pluginPermissions from './permissions'; export default { register(app) { app.addMenuLink({ - to: `plugins/${PluginIcon}`, + to: `plugins/${PLUGIN_ID}`, icon: PluginIcon, intlLabel: { id: `${PLUGIN_ID}.plugin.name`, @@ -198,4 +198,4 @@ const Sidebar = () => { }; export default Sidebar; -``` +``` \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/guides/create-components-for-plugins.md b/docusaurus/docs/cms/plugins-development/guides/create-components-for-plugins.md index e570cb71fa..ca4c2edbcb 100644 --- a/docusaurus/docs/cms/plugins-development/guides/create-components-for-plugins.md +++ b/docusaurus/docs/cms/plugins-development/guides/create-components-for-plugins.md @@ -25,6 +25,7 @@ You can create components for your plugins in 2 different ways: using the Conten ### Using the Content-Type Builder The recommended way to create components for your plugin is through the Content-Type Builder in the admin panel. + The [Content-Type Builder documentation](/cms/features/content-type-builder#new-component) provides more details on this process. ### Creating components manually @@ -54,33 +55,27 @@ Components in Strapi follow the following format in their definition: } ``` -## Making components visible in the admin panel +## Component schema example -To ensure your plugin's components are visible in the admin panel, you need to set the appropriate `pluginOptions` in your component schema: +A component schema defines the structure of a reusable data fragment. Here is an example of a component schema for a plugin: -```javascript {9-16} +```json title="my-plugin/server/components/my-category/my-component.json" { - "kind": "collectionType", - "collectionName": "my_plugin_components", + "collectionName": "components_my_category_my_components", "info": { - "singularName": "my-plugin-component", - "pluralName": "my-plugin-components", - "displayName": "My Plugin Component" - }, - "pluginOptions": { - "content-manager": { - "visible": true - }, - "content-type-builder": { - "visible": true - } + "displayName": "My Component", + "icon": "align-justify" }, "attributes": { "name": { - "type": "string" + "type": "string", + "required": true + }, + "description": { + "type": "text" } } } ``` -This configuration ensures your components will be visible and editable in both the Content-Type Builder and Content Manager. +This configuration ensures your components will be available in both the Content-Type Builder and Content Manager when used in a content-type that has `pluginOptions` visibility enabled. \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/guides/pass-data-from-server-to-admin.md b/docusaurus/docs/cms/plugins-development/guides/pass-data-from-server-to-admin.md index e5d4532979..9fb41b5a3b 100644 --- a/docusaurus/docs/cms/plugins-development/guides/pass-data-from-server-to-admin.md +++ b/docusaurus/docs/cms/plugins-development/guides/pass-data-from-server-to-admin.md @@ -99,15 +99,15 @@ For instance, within a React component, you could use `useEffect` to get the dat ```js title="/my-plugin/admin/src/components/MyComponent/index.js" import foobarRequests from "../../api/foobar"; -const [foobar, setFoobar] = useState([]); +const [foobar, setFoobar] = useState([]); // … useEffect(() => { foobarRequests.getFoobar().then(res => { - setSchemas(res.data); + setFoobar(res.data); }); }, [setFoobar]); // … ``` -This would set the `You are in the my-plugin-content-type controller!` text within the `foobar` data of the component's state. +This would set the `You are in the my-plugin-content-type controller!` text within the `foobar` data of the component's state. \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/guides/store-and-access-data.md b/docusaurus/docs/cms/plugins-development/guides/store-and-access-data.md index 48593324be..b205fd1594 100644 --- a/docusaurus/docs/cms/plugins-development/guides/store-and-access-data.md +++ b/docusaurus/docs/cms/plugins-development/guides/store-and-access-data.md @@ -11,12 +11,8 @@ tags: - plugins development guides --- -import NotV5 from '/docs/snippets/_not-updated-to-v5.md' - # How to store and access data from a Strapi plugin - - To store data with a Strapi [plugin](/cms/plugins-development/developing-plugins), use a plugin content-type. Plugin content-types work exactly like other [content-types](/cms/backend-customization/models). Once the content-type is [created](#create-a-content-type-for-your-plugin), you can start [interacting with the data](#interact-with-data-from-the-plugin). ## Create a content-type for your plugin @@ -165,9 +161,9 @@ Once you have created a content-type for your plugin, you can create, read, upda A plugin can only interact with data from the `/server` folder. If you need to update data from the admin panel, please refer to the [passing data guide](/cms/plugins-development/guides/pass-data-from-server-to-admin). ::: -To create, read, update, and delete data, you can use either the [Entity Service API](/cms/api/entity-service) or the [Query Engine API](/cms/api/query-engine). While it's recommended to use the Entity Service API, especially if you need access to components or dynamic zones, the Query Engine API is useful if you need unrestricted access to the underlying database. +To create, read, update, and delete data, you can use either the [Document Service API](/cms/api/document-service) or the [Query Engine API](/cms/api/query-engine). While it's recommended to use the Document Service API, especially if you need access to components or dynamic zones, the Query Engine API is useful if you need unrestricted access to the underlying database. -Use the `plugin::your-plugin-slug.the-plugin-content-type-name` syntax for content-type identifiers in Entity Service and Query Engine API queries. +Use the `plugin::your-plugin-slug.the-plugin-content-type-name` syntax for content-type identifiers in Document Service and Query Engine API queries. **Example:** @@ -183,4 +179,4 @@ let data = await strapi.db.query('plugin::my-plugin.my-plugin-content-type').fin :::tip You can access the database via the `strapi` object which can be found in `middlewares`, `policies`, `controllers`, `services`, as well as from the `register`, `boostrap`, `destroy` lifecycle functions. -::: +::: \ No newline at end of file diff --git a/docusaurus/docs/cms/plugins-development/plugin-sdk.md b/docusaurus/docs/cms/plugins-development/plugin-sdk.md index 9b8ea97e1c..7698616eb4 100644 --- a/docusaurus/docs/cms/plugins-development/plugin-sdk.md +++ b/docusaurus/docs/cms/plugins-development/plugin-sdk.md @@ -1,6 +1,7 @@ --- -title: Plugin SDK +title: Plugin SDK reference description: Reference documentation for Strapi's Plugin SDK commands +pagination_prev: cms/plugins-development/plugin-structure displayed_sidebar: cmsSidebar tags: - backend server diff --git a/docusaurus/docs/cms/plugins-development/plugin-structure.md b/docusaurus/docs/cms/plugins-development/plugin-structure.md index 75e60236f7..facf29c7ed 100644 --- a/docusaurus/docs/cms/plugins-development/plugin-structure.md +++ b/docusaurus/docs/cms/plugins-development/plugin-structure.md @@ -2,6 +2,8 @@ title: Plugin structure description: Learn more about the structure of a Strapi plugin displayed_sidebar: cmsSidebar +pagination_prev: cms/plugins-development/developing-plugins +pagination_next: cms/plugins-development/plugin-sdk tags: - admin panel - Command Line Interface (CLI) diff --git a/docusaurus/docs/cms/plugins-development/server-api.md b/docusaurus/docs/cms/plugins-development/server-api.md index df6a24bcb6..3b84194e62 100644 --- a/docusaurus/docs/cms/plugins-development/server-api.md +++ b/docusaurus/docs/cms/plugins-development/server-api.md @@ -1,6 +1,6 @@ --- title: Server API for plugins -sidebar_label: Server API +sidebar_label: Server API reference displayed_sidebar: cmsSidebar description: Strapi's Server API for plugins allows a Strapi plugin to customize the back end part (i.e. the server) of your application. tags: @@ -21,7 +21,9 @@ tags: # Server API for plugins -A Strapi plugin can interact with both the back end and the [front end](/cms/plugins-development/admin-panel-api) of a Strapi application. The Server API is about the back-end part, i.e. how the plugin interacts with the server part of a Strapi application. +A Strapi plugin can interact with both the back end and the front end of a Strapi application. The Server API is about the back-end part, i.e. how the plugin interacts with the server part of a Strapi application. + +For more information on how plugins can modify the front end part of Strapi, see [front end](/cms/plugins-development/admin-panel-api). :::prerequisites You have [created a Strapi plugin](/cms/plugins-development/create-a-plugin). diff --git a/docusaurus/docs/snippets/injection-zones-vs-content-manager-apis.md b/docusaurus/docs/snippets/injection-zones-vs-content-manager-apis.md new file mode 100644 index 0000000000..4de4f1fb4f --- /dev/null +++ b/docusaurus/docs/snippets/injection-zones-vs-content-manager-apis.md @@ -0,0 +1,55 @@ +:::tip tl;dr +For adding panels, actions, or buttons to the Content Manager, the [Content Manager APIs](/cms/plugins-development/content-manager-apis) (`addDocumentAction`, `addEditViewSidePanel`, etc.) are often more robust and better typed than injection zones. Use injection zones when you need to insert components into specific UI areas not covered by the Content Manager APIs. +::: + +Content Manager APIs and injection zones are both extension points to customize the admin panel, but they solve different needs: + +| Need | Recommended API | Why | +| --- | --- | --- | +| Add a custom panel in the Edit View side area | Content Manager API ([`addEditViewSidePanel`](/cms/plugins-development/content-manager-apis#addeditviewsidepanel)) | Best for contextual information or controls that stay visible while editing. | +| Add actions in a document action menu | Content Manager API ([`addDocumentAction`](/cms/plugins-development/content-manager-apis#adddocumentaction)) | Best for document-level actions in the Edit View actions menu. | +| Add actions in the Edit View header | Content Manager API ([`addDocumentHeaderAction`](/cms/plugins-development/content-manager-apis#adddocumentheaderaction)) | Best for quick, prominent actions next to the document title. | +| Add actions for selected entries in List View | Content Manager API ([`addBulkAction`](/cms/plugins-development/content-manager-apis#addbulkaction)) | Best for workflows that apply to multiple entries at once. | +| Add UI to a predefined zone in a plugin view (localized visual customization) | Injection Zones API ([`injectComponent`](/cms/plugins-development/admin-injection-zones#injecting-into-content-manager-zones)) | Best when you target a specific zone exposed by a plugin. | + +For implementation details and up-to-date API signatures, please refer to the file in the Strapi codebase. + + + +**Mini examples (inside `bootstrap(app)`)** + +```js +// Document action menu item +app.getPlugin('content-manager').apis.addDocumentAction(() => ({ + label: 'Run custom action', + onClick: ({ documentId }) => runCustomAction(documentId), +})); + +// Edit View header action +app.getPlugin('content-manager').apis.addDocumentHeaderAction(() => ({ + label: 'Open preview', + onClick: ({ document }) => openPreview(document), +})); + +// List View bulk action +app.getPlugin('content-manager').apis.addBulkAction(() => ({ + label: 'Bulk publish', + onClick: ({ documentIds }) => bulkPublish(documentIds), +})); + +// Edit View side panel +app.getPlugin('content-manager').apis.addEditViewSidePanel([ + { + name: 'my-plugin.side-panel', + Component: MySidePanel, + }, +]); + +// Injection zone (plugin-defined zone) +app.getPlugin('content-manager').injectComponent('editView', 'right-links', { + name: 'my-plugin.custom-link', + Component: MyCustomLink, +}); +``` + + \ No newline at end of file diff --git a/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md b/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md new file mode 100644 index 0000000000..49ae25c336 --- /dev/null +++ b/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite-admin-panel.md @@ -0,0 +1,5 @@ +:::prerequisites +Before diving deeper into the concepts on this page, please ensure you have: +- [created a Strapi plugin](/cms/plugins-development/create-a-plugin), +- read and understood the basics of the [Admin Panel API](/cms/plugins-development/admin-panel-api) +::: \ No newline at end of file diff --git a/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite-server.md b/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite-server.md new file mode 100644 index 0000000000..0ea77c60ca --- /dev/null +++ b/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite-server.md @@ -0,0 +1,5 @@ +:::prerequisites +Before diving deeper into the concepts on this page, please ensure you have: +- [created a Strapi plugin](/cms/plugins-development/create-a-plugin), +- read and understood the basics of the [Server API](/cms/plugins-development/server-api) +::: \ No newline at end of file diff --git a/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite.md b/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite.md new file mode 100644 index 0000000000..eeede95d09 --- /dev/null +++ b/docusaurus/docs/snippets/plugins-development-create-plugin-prerequisite.md @@ -0,0 +1,3 @@ +:::prerequisites +Before diving deeper into the concepts on this page, please ensure you [created a Strapi plugin](/cms/plugins-development/create-a-plugin). +::: \ No newline at end of file diff --git a/docusaurus/docs/snippets/sdk-plugin-v5-v6.md b/docusaurus/docs/snippets/sdk-plugin-v5-v6.md new file mode 100644 index 0000000000..78930ad86c --- /dev/null +++ b/docusaurus/docs/snippets/sdk-plugin-v5-v6.md @@ -0,0 +1,23 @@ +:::caution Using SDK Plugin v5 (legacy) +If you need to continue using the previous build system with `@strapi/pack-up`, you can pin to version 5.x: + + + + + +```bash +yarn add @strapi/sdk-plugin@5 +``` + + + + +```bash +npm install @strapi/sdk-plugin@5 +``` + + + + +Version 5.x of the SDK plugin supports `packup.config.ts` for custom build configuration. However, v6 is recommended for security updates and simplified configuration. +::: \ No newline at end of file diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js index 03b7a41e58..a16b32c273 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -162,7 +162,7 @@ const sidebars = { { // APIs type: 'category', - label: 'APIs', + label: 'Content APIs', className: 'category-cms-api', link: { type: 'doc', id: 'cms/api/content-api' }, collapsible: false, @@ -394,23 +394,23 @@ const sidebars = { collapsed: true, customProps: { updated: false, - tooltip: 'This section has been reorganized, see details below.', + // tooltip: 'This section has been reorganized, see details below.', }, items: [ - { - type: 'html', - value: 'placeholder', // a value is required for the HTML type, but it is not rendered - customProps: { - tooltipTitle: `The section has been reorganized`, - tooltipContent: `We have reorganized the admin panel customization section to make it easier to navigate and find what you need. -

-
The new structure groups customizations by their purpose, making it more intuitive to locate specific settings.
-
-
Note: - Deployment-related configuration, including host, port, and path configuration, has been moved to the Configurations > Admin panel > Admin panel server page. -
`, - }, - }, + // { + // type: 'html', + // value: 'placeholder', // a value is required for the HTML type, but it is not rendered + // customProps: { + // tooltipTitle: `The section has been reorganized`, + // tooltipContent: `We have reorganized the admin panel customization section to make it easier to navigate and find what you need. + //

+ //
The new structure groups customizations by their purpose, making it more intuitive to locate specific settings.
+ //
+ //
Note: + // Deployment-related configuration, including host, port, and path configuration, has been moved to the Configurations > Admin panel > Admin panel server page. + //
`, + // }, + // }, { type: 'doc', id: 'cms/admin-panel-customization', @@ -487,7 +487,7 @@ const sidebars = { { // Plugins type: 'category', - label: 'Plugins', + label: 'Plugins development', className: 'category-cms-plugins', collapsible: false, collapsed: false, @@ -497,29 +497,55 @@ const sidebars = { id: 'cms/plugins/installing-plugins-via-marketplace', label: 'Marketplace', }, + { + type: 'doc', + label: 'Developing plugins', + id: 'cms/plugins-development/developing-plugins', + }, { type: 'category', - label: 'Plugins development', + label: 'Basics', collapsed: true, items: [ - { - type: 'doc', - label: 'Developing plugins', - id: 'cms/plugins-development/developing-plugins', - }, 'cms/plugins-development/create-a-plugin', 'cms/plugins-development/plugin-structure', 'cms/plugins-development/plugin-sdk', + ], + }, + { + type: 'category', + label: 'Admin Panel', + collapsed: true, + items: [ 'cms/plugins-development/admin-panel-api', + 'cms/plugins-development/admin-navigation-settings', 'cms/plugins-development/content-manager-apis', + 'cms/plugins-development/admin-injection-zones', + 'cms/plugins-development/admin-redux-store', + 'cms/plugins-development/admin-hooks', + 'cms/plugins-development/admin-localization', + ], + }, + { + type: 'category', + label: 'Server', + collapsed: true, + items: [ 'cms/plugins-development/server-api', - 'cms/plugins-development/plugins-extension', + ], + }, + { + type: 'category', + label: 'Guides', + collapsed: true, + items: [ 'cms/plugins-development/guides/pass-data-from-server-to-admin', 'cms/plugins-development/guides/admin-permissions-for-plugins', 'cms/plugins-development/guides/store-and-access-data', 'cms/plugins-development/guides/create-components-for-plugins', ], }, + 'cms/plugins-development/plugins-extension', ], }, @@ -702,338 +728,6 @@ const sidebars = { ], }, ], - // devDocsRestApiSidebar: [ - // { - // type: 'link', - // label: '⬅️ Back to Dev Docs content', - // href: '/cms/intro' - // }, - // { - // type: 'category', - // collapsed: false, - // label: 'REST API reference', - // link: { - // type: 'doc', - // id: 'cms/api/rest' - // }, - // items: [ - // { - // type: 'category', - // label: 'Endpoints and basic requests', - // link: {type: 'doc', id: 'cms/api/rest'}, - // collapsed: false, - // items: [ - // { - // type: 'link', - // label: 'Endpoints', - // href: '/cms/api/rest#endpoints', - // }, - // { - // type: 'link', - // label: 'Get documents', - // href: '/cms/api/rest#get-all' - // }, - // { - // type: 'link', - // label: 'Get a document', - // href: '/cms/api/rest#get' - // }, - // { - // type: 'link', - // label: 'Create a document', - // href: '/cms/api/rest#create' - // }, - // { - // type: 'link', - // label: 'Update a document', - // href: '/cms/api/rest#update' - // }, - // { - // type: 'link', - // label: 'Delete a document', - // href: '/cms/api/rest#delete' - // }, - // ] - // }, - // { - // type: 'doc', - // id: 'cms/api/rest/interactive-query-builder', - // label: '✨ Interactive Query Builder' - // }, - // { - // type: 'doc', - // id: 'cms/api/rest/parameters' - // }, - // { - // type: 'category', - // label: 'Populate and Select', - // link: {type: 'doc', id: 'cms/api/rest/populate-select'}, - // collapsed: false, - // items: [ - // { - // type: 'link', - // label: 'Field selection', - // href: '/cms/api/rest/populate-select#field-selection', - // }, - // { - // type: 'link', - // label: 'Population', - // href: '/cms/api/rest/populate-select#population', - // }, - // ] - // }, - // { - // type: 'category', - // collapsed: false, - // label: 'Filters, Locale, Publication State', - // link: {type: 'doc', id: 'cms/api/rest/filters-locale-publication' }, - // items: [ - // { - // type: 'link', - // label: 'Filtering', - // href: '/cms/api/rest/filters' - // }, - // { - // type: 'link', - // label: 'Complex filtering', - // href: '/cms/api/rest/filters-locale-publication#complex-filtering', - // }, - // { - // type: 'link', - // label: 'Deep filtering', - // href: '/cms/api/rest/filters-locale-publication#deep-filtering', - // }, - // { - // type: 'link', - // label: 'Locale', - // href: '/cms/api/rest/locale', - // }, - // { - // type: 'link', - // label: 'Status', - // href: '/cms/api/rest/status', - // }, - // ], - // }, - // { - // type: 'category', - // collapsed: false, - // label: 'Sort and Pagination', - // link: { type: 'doc', id: 'cms/api/rest/sort-pagination'}, - // items: [ - // { - // type: 'link', - // label: 'Sorting', - // href: '/cms/api/rest/sort-pagination#sorting' - // }, - // { - // type: 'link', - // label: 'Pagination', - // href: '/cms/api/rest/sort-pagination#pagination' - // }, - // { - // type: 'link', - // label: 'Pagination by page', - // href: '/cms/api/rest/sort-pagination#pagination-by-page' - // }, - // { - // type: 'link', - // label: 'Pagination by offset', - // href: '/cms/api/rest/sort-pagination#pagination-by-offset' - // }, - // ] - // }, - // { - // type: 'category', - // collapsed: false, - // label: 'Relations', - // link: {type: 'doc', id: 'cms/api/rest/relations'}, - // items: [ - // { - // type: 'link', - // label: 'connect', - // href: '/cms/api/rest/relations#connect' - // }, - // { - // type: 'link', - // label: 'disconnect', - // href: '/cms/api/rest/relations#disconnect' - // }, - // { - // type: 'link', - // label: 'set', - // href: '/cms/api/rest/relations#set' - // }, - // ] - // }, - // ] - // }, - // { - // type: "category", - // label: "Rest API guides", - // collapsed: false, - // link: { - // type: 'doc', - // id: 'cms/api/rest/guides/intro', - // }, - // items: [ - // { - // type: "doc", - // label: "Understanding populate", - // id: 'cms/api/rest/guides/understanding-populate', - // }, - // { - // type: "doc", - // label: "How to populate creator fields", - // id: 'cms/api/rest/guides/populate-creator-fields', - // }, - // { - // type: 'link', - // label: 'Additional resources', - // href: '/cms/api/rest/guides/intro#additional-resources' - // }, - // ], - // } - // ], - // devDocsConfigSidebar: [ - // { - // type: 'link', - // label: '⬅️ Back to Dev Docs content', - // href: '/cms/intro' - // }, - // { - // type: 'category', - // collapsed: false, - // label: 'Configuration', - // link: { - // type: 'doc', - // id: 'cms/configurations', - // }, - // items: [ - // { - // type: 'doc', - // label: 'Introduction to configurations', - // id: 'cms/configurations', - // }, - // { - // type: 'category', - // collapsed: false, - // label: 'Base configurations', - // link: { - // type: 'doc', - // id: 'cms/configurations' - // }, - // items: [ - // 'cms/configurations/database', - // 'cms/configurations/server', - // 'cms/configurations/admin-panel', - // 'cms/configurations/middlewares', - // 'cms/configurations/api', - // ] - // }, - // { - // type: 'category', - // label: 'Additional configurations', - // collapsed: false, - // link: { - // type: 'doc', - // id: 'cms/configurations' - // }, - // items: [ - // 'cms/configurations/plugins', - // 'cms/configurations/typescript', - // 'cms/configurations/api-tokens', - // 'cms/configurations/functions', - // 'cms/configurations/cron', - // 'cms/configurations/environment', - // 'cms/configurations/sso', - // 'cms/configurations/features', - // ] - // }, - // { - // type: 'category', - // label: 'Guides', - // collapsed: false, - // link: { - // type: 'doc', - // id: 'cms/configurations' - // }, - // items: [ - // 'cms/configurations/guides/rbac', - // 'cms/configurations/guides/public-assets', - // 'cms/configurations/guides/access-cast-environment-variables', - // 'cms/configurations/guides/access-configuration-values', - // 'cms/configurations/guides/use-cron-jobs', - // ] - // } - // ] - // }, - // ], - // devDocsMigrationV5Sidebar: [ - // { - // type: 'link', - // label: '⬅️ Back to Dev Docs content', - // href: '/cms/intro' - // }, - // { - // type: 'category', - // collapsed: false, - // link: { - // type: 'doc', - // id: 'cms/migration/v4-to-v5/introduction-and-faq' - // }, - // label: 'Upgrade to Strapi 5', - // customProps: { - // new: true, - // }, - // items: [ - // { - // type: "doc", - // label: "Introduction and FAQ", - // id: "cms/migration/v4-to-v5/introduction-and-faq" - // }, - // { - // type: "doc", - // label: "Step-by-step guide", - // id: "cms/migration/v4-to-v5/step-by-step" - // }, - // { - // type: "doc", - // label: "Upgrade tool reference", - // id: 'cms/upgrade-tool', - // }, - // { - // type: "category", - // collapsible: true, - // collapsed: true, - // label: "Breaking changes", - // link: { - // type: 'doc', - // id: 'cms/migration/v4-to-v5/breaking-changes' - // }, - // items: [ - // { - // type: "autogenerated", - // dirName: 'cms/migration/v4-to-v5/breaking-changes' - // }, - // ] - // }, - // { - // type: 'category', - // label: 'Specific resources', - // collapsed: false, - // link: { type: 'doc', id: 'cms/migration/v4-to-v5/additional-resources/introduction' }, - // items: [ - // 'cms/migration/v4-to-v5/additional-resources/introduction', - // 'cms/migration/v4-to-v5/additional-resources/from-entity-service-to-document-service', - // 'cms/migration/v4-to-v5/additional-resources/plugins-migration', - // 'cms/migration/v4-to-v5/additional-resources/helper-plugin', - // ] - // } - // ] - // }, - - // ] }; -module.exports = sidebars; +module.exports = sidebars; \ No newline at end of file