diff --git a/docs/.vuepress/config-backup.js b/docs/.vuepress/config-backup.js index 32df1b997b..6872ab6d58 100644 --- a/docs/.vuepress/config-backup.js +++ b/docs/.vuepress/config-backup.js @@ -454,10 +454,10 @@ const sidebar = { ['/developer-docs/latest/guides/scheduled-publication', 'Scheduled publication'], // ['/developer-docs/latest/guides/secure-your-app', 'Secure your application'], // ['/developer-docs/latest/guides/send-email', 'Send email programmatically'], - // [ - // '/developer-docs/latest/guides/registering-a-field-in-admin', - // 'Registering a new field in the admin panel', - // ], + [ + '/developer-docs/latest/guides/registering-a-field-in-admin', + 'Registering a new field in the admin panel', + ], // ['/developer-docs/latest/guides/client', 'Setup a third party client'], ['/developer-docs/latest/guides/unit-testing', 'Unit testing'], ], diff --git a/docs/developer-docs/latest/assets/guides/register-field-admin/ckeditor-rich-text.png b/docs/developer-docs/latest/assets/guides/register-field-admin/ckeditor-rich-text.png new file mode 100644 index 0000000000..8b5da3b49a Binary files /dev/null and b/docs/developer-docs/latest/assets/guides/register-field-admin/ckeditor-rich-text.png differ diff --git a/docs/developer-docs/latest/developer-resources/plugin-api-reference/admin-panel.md b/docs/developer-docs/latest/developer-resources/plugin-api-reference/admin-panel.md index de71c0366e..dda17d1fc0 100644 --- a/docs/developer-docs/latest/developer-resources/plugin-api-reference/admin-panel.md +++ b/docs/developer-docs/latest/developer-resources/plugin-api-reference/admin-panel.md @@ -187,6 +187,10 @@ The Admin Panel API allows a plugin to take advantage of several small APIs to p | Inject a Component in an injection zone | [Injection Zones API](#injection-zones-api) | [`injectComponent()`](#injection-zones-api) | [`bootstrap()`](#register) | | Register a hook | [Hooks API](#hooks-api) | [`registerHook()`](#hooks-api) | [`bootstrap()`](#bootstrap) | +::: strapi Replacing the WYSIWYG +The WYSIWYG editor can be replaced by taking advantage of the [register lifecycle](#register) (see [register a new field in the admin panel](/developer-docs/latest/guides/registering-a-field-in-admin.md)). +::: + ::: tip The admin panel supports dotenv variables. diff --git a/docs/developer-docs/latest/guides/registering-a-field-in-admin.md b/docs/developer-docs/latest/guides/registering-a-field-in-admin.md index 69b8928569..6a396c72a5 100644 --- a/docs/developer-docs/latest/guides/registering-a-field-in-admin.md +++ b/docs/developer-docs/latest/guides/registering-a-field-in-admin.md @@ -6,203 +6,168 @@ canonicalUrl: https://docs.strapi.io/developer-docs/latest/guides/registering-a- # Creating a new Field in the administration panel -!!!include(developer-docs/latest/guides/snippets/guide-not-updated.md)!!! - In this guide we will see how you can create a new Field for your administration panel. -## Introduction - -For this example, we will see how to change the WYSIWYG with [CKEditor](https://ckeditor.com/ckeditor-5/) in the **`Content Manager`** plugin by creating a new plugin which will add a new **Field** in your application. +For this example, we will see how to change the WYSIWYG with [CKEditor](https://ckeditor.com/ckeditor-5/) in the Content Manager plugin by creating a new plugin that will add a new field in your application. -## Setup +## Setting up the plugin 1. Create a new project: -:::: tabs card - -::: tab yarn - -``` -# Create an application using SQLite and prevent the server from starting automatically as we will create a plugin -# right after the project generation -yarn create strapi-app my-app --quickstart --no-run -``` - -::: - -::: tab npx - -``` -# Create an application using SQLite and prevent the server from starting automatically as we will create a plugin -# right after the project generation -npx create-strapi-app@latest my-app --quickstart --no-run -``` - -::: - -:::: - -2. Generate a plugin: - -:::: tabs card + :::: tabs card -::: tab yarn + ::: tab yarn -``` -cd my-app -yarn strapi generate:plugin wysiwyg -``` + Create an application and prevent the server from starting automatically with the following command: -::: + ``` + yarn create strapi-app my-app --quickstart --no-run + ``` -::: tab npm + The `--no-run` flag was added as we will run additional commands to create a plugin right after the project generation. -``` -cd my-app -npm run strapi generate:plugin wysiwyg -``` + ::: -::: + ::: tab npx -::: tab strapi + Create an application and prevent the server from starting automatically with the following command: -``` -cd my-app -strapi generate:plugin wysiwyg -``` - -::: - -:::: - -3. Install the needed dependencies: - -:::: tabs card - -::: tab yarn - -``` -cd plugins/wysiwyg -yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic -``` + ``` + npx create-strapi-app@latest my-app --quickstart --no-run + ``` -::: + The `--no-run` flag was added as we will run additional commands to create a plugin right after the project generation. -::: tab npm + ::: -``` -cd plugins/wysiwyg -npm install @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic -``` + :::: -::: +2. Generate a plugin: -:::: + :::: tabs card -4. Start your application with the front-end development mode: + ::: tab yarn -:::: tabs card + ``` + cd my-app + yarn strapi generate + ``` -::: tab yarn + Choose "plugin" from the list, press Enter and name the plugin `wysiwyg`. -``` -# Go back to to strapi root folder -cd ../.. -yarn develop --watch-admin -``` + ::: -::: + ::: tab npm -::: tab npm + ``` + cd my-app + npm run strapi generate + ``` + Choose "plugin" from the list, press Enter and name the plugin `wysiwyg`. -``` -# Go back to to strapi root folder -cd ../.. -npm run develop -- --watch-admin -``` + ::: -::: + :::: -::: tab strapi +3. Enable the plugin by adding it to the [plugins configurations](/developer-docs/latest/setup-deployment-guides/configurations/optional/plugins.md) file: -``` -# Go back to to strapi root folder -cd ../.. -strapi develop --watch-admin -``` + ```js + // path: ./config/plugins.js + module.exports = { + // ... + 'wysiwyg': { + enabled: true, + resolve: './src/plugins/wysiwyg' // path to plugin folder + }, + // ... + } + ``` + +4. Install the required dependencies: + + + + ```bash + cd src/plugins/wysiwyg + yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic + ``` + + + + ```bash + cd src/plugins/wysiwyg + npm install @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic + ``` + + + +5. Start the application with the front-end development mode: + + + + ```bash + # Go back to the application root folder + cd ../../.. + yarn develop --watch-admin + ``` + + + + ```bash + # Go back to the application root folder + cd ../../.. + npm run develop -- --watch-admin + ``` + + + +::: note NOTE +Launching the Strapi server in watch mode without creating a user account first will open `localhost:1337` with a JSON format error. Creating a user on `localhost:8081` prevents this alert. ::: -:::: +Once this step is over all we need to do is to create our new WYSIWYG, which will replace the default one in the Content Manager plugin. -Once this step is over all we need to do is to create our new WYSIWYG which will replace the default one in the **Content Manager** plugin. +## Creating the WYSIWYG -### Creating the WYSIWYG +In this part we will create 3 components: -In this part we will create three components: +- a `MediaLib` component used to insert media in the editor +- an `Editor` component that uses [CKEditor](https://ckeditor.com/) as the WYSIWYG editor +- a `Wysiwyg` component to wrap the CKEditor -- MediaLib which will be used to insert media in the editor -- Wysiwyg which will wrap the CKEditor with a label and the errors -- CKEditor which will be the implementation of the new WYSIWYG +The following code examples can be used to implement the logic for the 3 components: -### Creating the MediaLib - -**Path —** `./plugins/wysiwyg/admin/src/components/MediaLib/index.js` +::: details Example of a MediaLib component used to insert media in the editor: ```js -import React, { useEffect, useState } from 'react'; -import { useStrapi, prefixFileUrlWithBackendUrl } from 'strapi-helper-plugin'; +// path: ./src/plugins/wysiwyg/admin/src/components/MediaLib/index.js + +import React from 'react'; +import { prefixFileUrlWithBackendUrl, useLibrary } from '@strapi/helper-plugin'; import PropTypes from 'prop-types'; const MediaLib = ({ isOpen, onChange, onToggle }) => { - const { - strapi: { - componentApi: { getComponent }, - }, - } = useStrapi(); - const [data, setData] = useState(null); - const [isDisplayed, setIsDisplayed] = useState(false); - - useEffect(() => { - if (isOpen) { - setIsDisplayed(true); - } - }, [isOpen]); - - const Component = getComponent('media-library').Component; + const { components } = useLibrary(); + const MediaLibraryDialog = components['media-library']; - const handleInputChange = data => { - if (data) { - const { url } = data; + const handleSelectAssets = files => { + const formattedFiles = files.map(f => ({ + alt: f.alternativeText || f.name, + url: prefixFileUrlWithBackendUrl(f.url), + mime: f.mime, + })); - setData({ ...data, url: prefixFileUrlWithBackendUrl(url) }); - } + onChange(formattedFiles); }; - const handleClosed = () => { - if (data) { - onChange(data); - } - - setData(null); - setIsDisplayed(false); + if(!isOpen) { + return null }; - if (Component && isDisplayed) { - return ( - - ); - } - - return null; + return( + + ); }; MediaLib.defaultProps = { @@ -219,127 +184,27 @@ MediaLib.propTypes = { export default MediaLib; ``` +::: -#### Creating the WYSIWYG Wrapper - -**Path —** `./plugins/wysiwyg/admin/src/components/Wysiwyg/index.js` +::: details Example of an Editor component using CKEditor as the WYSIWYG editor: ```js -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { isEmpty } from 'lodash'; -import { Button } from '@buffetjs/core'; -import { Label, InputDescription, InputErrors } from 'strapi-helper-plugin'; -import Editor from '../CKEditor'; -import MediaLib from '../MediaLib'; - -const Wysiwyg = ({ - inputDescription, - errors, - label, - name, - noErrorsDescription, - onChange, - value, -}) => { - const [isOpen, setIsOpen] = useState(false); - let spacer = !isEmpty(inputDescription) ?
:
; - - if (!noErrorsDescription && !isEmpty(errors)) { - spacer =
; - } +// path: ./src/plugins/wysiwyg/admin/src/components/Editor/index.js - const handleChange = data => { - if (data.mime.includes('image')) { - const imgTag = `

${data.alternativeText}

`; - const newValue = value ? `${value}${imgTag}` : imgTag; - - onChange({ target: { name, value: newValue } }); - } - - // Handle videos and other type of files by adding some code - }; - - const handleToggle = () => setIsOpen(prev => !prev); - - return ( -
-
- ); -}; - -Wysiwyg.defaultProps = { - errors: [], - inputDescription: null, - label: '', - noErrorsDescription: false, - value: '', -}; - -Wysiwyg.propTypes = { - errors: PropTypes.array, - inputDescription: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - PropTypes.shape({ - id: PropTypes.string, - params: PropTypes.object, - }), - ]), - label: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - PropTypes.shape({ - id: PropTypes.string, - params: PropTypes.object, - }), - ]), - name: PropTypes.string.isRequired, - noErrorsDescription: PropTypes.bool, - onChange: PropTypes.func.isRequired, - value: PropTypes.string, -}; - -export default Wysiwyg; -``` - -#### Implementing CKEditor - -**Path —** `./plugins/wysiwyg/admin/src/components/CKEditor/index.js` - -```js import React from 'react'; import PropTypes from 'prop-types'; +import styled from 'styled-components'; import { CKEditor } from '@ckeditor/ckeditor5-react'; import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; -import styled from 'styled-components'; +import { Box } from '@strapi/design-system/Box'; -const Wrapper = styled.div` +const Wrapper = styled(Box)` .ck-editor__main { - min-height: 200px; + min-height: ${200 / 16}em; > div { - min-height: 200px; + min-height: ${200 / 16}em; } + // Since Strapi resets css styles, it can be configured here (h2, h3, strong, i, ...) } `; @@ -364,14 +229,15 @@ const configuration = { ], }; -const Editor = ({ onChange, name, value }) => { +const Editor = ({ onChange, name, value, disabled }) => { return ( editor.setData(value)} + data={value || ''} + onReady={editor => editor.setData(value || '')} onChange={(event, editor) => { const data = editor.getData(); onChange({ target: { name, value: data } }); @@ -381,89 +247,158 @@ const Editor = ({ onChange, name, value }) => { ); }; +Editor.defaultProps = { + value: '', + disabled: false +}; + Editor.propTypes = { onChange: PropTypes.func.isRequired, name: PropTypes.string.isRequired, value: PropTypes.string, + disabled: PropTypes.bool }; export default Editor; ``` -At this point we have simply created a new plugin which is mounted in our project but our custom **Field** has not been registered yet. +::: -### Registering a our new Field +::: details Example of a Wysiwyg component wrapping CKEditor: -Since the goal of our plugin is to override the current WYSIWYG we don't want it to be displayed in the administration panel but we need it to register our new **Field**. -In order to do so, we will simply **modify** the front-end entry point of our plugin. +```js +// path: ./src/plugins/wysiwyg/admin/src/components/Wysiwyg/index.js -This file is already present. Please replace the content of this file with the following: +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Stack } from '@strapi/design-system/Stack'; +import { Box } from '@strapi/design-system/Box'; +import { Button } from '@strapi/design-system/Button'; +import { Typography } from '@strapi/design-system/Typography'; +import Landscape from '@strapi/icons/Landscape'; +import MediaLib from '../MediaLib'; +import Editor from '../Editor'; +import { useIntl } from 'react-intl'; -**Path —** `./plugins/wysiwyg/admin/src/index.js` +const Wysiwyg = ({ name, onChange, value, intlLabel, disabled, error, description, required }) => { + const { formatMessage } = useIntl(); + const [mediaLibVisible, setMediaLibVisible] = useState(false); -```js -import pluginPkg from '../../package.json'; -import Wysiwyg from './components/Wysiwyg'; -import pluginId from './pluginId'; - -export default strapi => { - const pluginDescription = pluginPkg.strapi.description || pluginPkg.description; - - const plugin = { - blockerComponent: null, - blockerComponentProps: {}, - description: pluginDescription, - icon: pluginPkg.strapi.icon, - id: pluginId, - initializer: () => null, - injectedComponents: [], - isReady: true, - isRequired: pluginPkg.strapi.required || false, - mainComponent: null, - name: pluginPkg.strapi.name, - preventComponentRendering: false, - settings: null, - trads: {}, - }; + const handleToggleMediaLib = () => setMediaLibVisible(prev => !prev); - strapi.registerField({ type: 'wysiwyg', Component: Wysiwyg }); + const handleChangeAssets = assets => { + let newValue = value ? value : ''; - return strapi.registerPlugin(plugin); -}; -``` + assets.map(asset => { + if (asset.mime.includes('image')) { + const imgTag = `

${asset.alt}

`; -Finally you will have to rebuild strapi so the new plugin is loaded correctly: + newValue = `${newValue}${imgTag}` + } -:::: tabs card + // Handle videos and other type of files by adding some code + }); -::: tab yarn + onChange({ target: { name, value: newValue } }); + handleToggleMediaLib(); + }; + + return ( + <> + + + + {formatMessage(intlLabel)} + + {required && + * + } + + + + {error && + + {formatMessage({ id: error, defaultMessage: error })} + + } + {description && + + {formatMessage(description)} + + } + + + + ); +}; -``` -yarn build +Wysiwyg.defaultProps = { + description: '', + disabled: false, + error: undefined, + intlLabel: '', + required: false, + value: '', +}; + +Wysiwyg.propTypes = { + description: PropTypes.shape({ + id: PropTypes.string, + defaultMessage: PropTypes.string, + }), + disabled: PropTypes.bool, + error: PropTypes.string, + intlLabel: PropTypes.shape({ + id: PropTypes.string, + defaultMessage: PropTypes.string, + }), + name: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + required: PropTypes.bool, + value: PropTypes.string, +}; + +export default Wysiwyg; ``` ::: -::: tab npm +## Registering the field -``` -npm run build -``` +The last step is to register the `wysiwyg` field with the `Wysiwyg` component using `addFields()`. Replace the content of the `admin/src/index.js` field of the plugin with the following code: -::: +```js +// path: ./src/plugins/wysiwyg/admin/src/index.js -::: tab strapi +import pluginPkg from "../../package.json"; +import Wysiwyg from "./components/Wysiwyg"; +import pluginId from "./pluginId"; -``` -strapi build -``` +const name = pluginPkg.strapi.name; -::: +export default { + register(app) { + app.addFields({ type: 'wysiwyg', Component: Wysiwyg }); -:::: + app.registerPlugin({ + id: pluginId, + isReady: true, + name, + }); + }, + bootstrap() {}, +}; +``` -::: tip -If the plugin still doesn't show up, you should probably empty the `.cache` folder too. -::: +And _voilà_, if you [create a new collection type or single type](/user-docs/latest/content-types-builder/creating-new-content-type.md) with a [rich text field](/user-docs/latest/content-types-builder/configuring-fields-content-type.md#rich-text) you will see the implementation of [CKEditor](https://ckeditor.com/ckeditor-5/) instead of the default WYSIWYG: -And VOILA, if you create a new `collectionType` or a `singleType` with a `richtext` field you will see the implementation of [CKEditor](https://ckeditor.com/ckeditor-5/) instead of the default WYSIWYG. +![Screenshot of Content Manager using CKEditor for rich text fields](../assets/guides/register-field-admin/ckeditor-rich-text.png)