Disclaimer: This is a guest post by Pouya Miralayi, a member of the Strapi community who volunteered to write a tutorial on how to create your own plugin on Strapi. I'll let him explain what you'll learn today

Introduction

Hello there! I am proud to be here talking about how we can develop a plugin in Strapi, our favorite headless CMS of all times! The plugin we are about to learn is originally developed by
Joe Beuckman and here is a link to his wonderful project

Before we begin, I must say that we will cover the basics of React too. So if you are not an expert on React, there is nothing to be worry about. Here is a glimpse of what the final project is going to look like:

Goal

Using this plugin, you can import external data’s right into your “content types”. You can make an import by uploading a file, providing an external url, or writing your data manually. The type of data supported by this plugin is “CSV” & “RSS” at the moment. Also you can see the history of all your imports and perform some actions on them including “undo” & “delete”.

You can reach the code for this tutorial at this link

This tutorial is part of the « How to create your own plugin »:

Table of contents

Prerequesites

You need to have node v.12 installed and that's all.

Setup

Let's create the Strapi application, our plugin and two models

  • Install strapi, generate a plugin and two models
yarn create strapi-app import-content-tutorial --quickstart --no-run  
cd import-content-tutorial  
strapi generate:plugin import-content  
strapi generate:model importconfig --plugin import-content  
strapi generate:model importeditem --plugin import-content  
  • With our models generated, check your plugins directory structure to be exactly like this:
    plugins
     |-- import-content
       |-- admin
       |-- config
       |-- controllers
       |-- models
         |-- Importconfig.js
         |-- Importconfig.settings.json
         |-- Importeditem.js
         |-- Importeditem.settings.json
       |-- services
  • Open Importconfig.settings.json inside models directory:

    plugins/import-content/models/Importconfig.settings.json

  • Paste the following:

{
  "connection": "default",
  "collectionName": "",
  "info": {
    "name": "importconfig",
    "description": ""
  },
  "options": { "timestamps": true, "increments": true, "comment": "" },
  "attributes": {
    "date": { "type": "date" },
    "source": {
      "type": "string"
    },
    "options": { "type": "json" },
    "contentType": {
      "type": "string"
    },
    "fieldMapping": { "type": "json" },
    "ongoing": {
      "type": "boolean"
    },
    "importeditems": {
      "collection": "importeditem",
      "via": "importconfig",
      "plugin": "import-content"
    }
  }
}
  • Open Importeditem.settings.json inside models directory:

    plugins/import-content/models/Importeditem.settings.json

  • Paste the following:

{
  "connection": "default",
  "collectionName": "",
  "info": {
    "name": "importeditem",
    "description": ""
  },
  "options": { "increments": true, "timestamps": true, "comment": "" },
  "attributes": {
    "ContentType": {
      "type": "string"
    },
    "ContentId": { "type": "integer" },
    "importconfig": {
      "model": "importconfig",
      "via": "importeditems",
      "plugin": "import-content"
    },
    "importedFiles": {
      "type": "json"
    }
  }
}

Now it’s time to install our dependencies.

  • yarn add content-type-parser@1.0.2 csv-parse@4.8.2 get-urls@9.2.0 moment rss-parser@3.7.3 request simple-statistics@7.0.7 striptags@3.1.1 lodash
  • yarn build
  • yarn develop --watch-admin

Let’s take a look at our plugin’s admin directory structure inside plugins/import-content directory:

admin  
 |-- src
   |-- containers
   |-- translations
   |-- utils
   |-- index.js
   |-- lifecycles.js
   |-- pluginId.js

As you can see we have a src directory which contains 3 other directories named containers , translations and utils. The containers directory is the main piece of puzzle and holds our plugin pages and their related logic. A react app as our plugin is one, is consisted of multiple pages, and each page is a composition of tiny blocks named components. The usage of components helps us to prevent repeating ourselves and keep the reusable code inside a component and use it in different places which for our case are pages (worth mentioning that our pages are also components themselves). So we can come up with another directory named components next to our containers directory, and keep the reusable code inside that directory:

  • Create a components directory
admin  
 |-- src
   |-- containers
   |-- components
   |-- translations
   |-- utils
   |-- index.js
   |-- lifecycles.js
   |-- pluginId.js

Now we are ready to start creating pages that are containers of those components. For more on components take a look at below link after you finished this tutorial! ReactJs
By default, Strapi has created a HomePage for us that acts as our starting page:

admin  
  |-- src
    |-- containers
      |-- App
      |-- HomePage
        |-- index.js
      |-- Initializer
    |-- components
    |-- translations
    |-- utils
    |-- index.js
    |-- lifecycles.js
    |-- pluginId.js

Now if you open up the index.js file inside this HomePage directory, you will be represented by this:

/*
 *
 * HomePage
 *
 */

import React, { memo } from 'react';  
// import PropTypes from 'prop-types';
import pluginId from '../../pluginId';

const HomePage = () => {  
  return (
    <div>
      <h1>{pluginId}&apos;s HomePage</h1>
      <p>Happy coding</p>
    </div>
  );
};

export default memo(HomePage);  

a react rendered page which is telling you: Happy coding! Before we actually change the contents of this page, Let’s build up the first component that we are about to use on this page: a form that will allow users to upload their data files:

UploadFileForm Component

  • Go to components directory and create a new directory called UploadFileForm with an empty index.js file inside:

    admin/src/components/UploadFileForm/index.js

  • Open the new index.js file and paste the following code:

import React, { Component } from "react";  
import PropTypes from "prop-types";  
class UploadFileForm extends Component {}  
export default UploadFileForm;  

what we are doing here is to create a new Component class called UploadFileForm. This class will represent our component for uploading files. There are some benefits on defining our Component as a class; the first benefit is that we can define a class member called state which will keep track of our reactive variables as the component’s state. This way, the UI will re-render itself based on changes upon the state. The second benefit would be the structure that will be introduced into our component; for instance, we are allowed to define a peculiar method that is responsible for rendering our UI.

For now, we want to keep track of a few properties of the uploaded file like it’s type, its name and the actual file itself:

  • Update the file with the following changes:
class UploadFileForm extends Component {  
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    }
  };
}
export default UploadFileForm;  
  • now let’s show something to the user by defining a render method:
class UploadFileForm extends Component {  
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    }
  };
  render() {}
}
  • in order to upload something, we need an input:
class UploadFileForm extends Component {  
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    }
  };
  render() {
    return <input name="file_input" accept=".csv" type="file" />;
  }
}
  • Awesome! Now we need an event listener that will get triggered on the file changes:
class UploadFileForm extends Component {  
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    }
  };
  onChangeImportFile = file => { // <---
    file &&
      this.setState({
        file,
        type: file.type,
        options: { ...this.state.options, filename: file.name }
      });
  };
  render() {
    return <input name="file_input" accept=".csv" type="file" />;
  }
}

As you can see, we are using a method called setState that will allow us to update our state in the right way and that would be another benefit of inheriting from “React Component”.

  • For our new method to actually get called on file changes, we need to introduce it to our input:
class UploadFileForm extends Component {  
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    }
  };
  onChangeImportFile = file => {
    file &&
      this.setState({
        file,
        type: file.type,
        options: { ...this.state.options, filename: file.name }
      });
  };
  render() {
    return <input 
    onChange={({target:{files}}) => files && this.onChangeImportFile(files[0])} name="file_input" accept=".csv" type="file" />;
  }
}

Well Done! Now we have an input for uploading files ;) the thing is that although it is working, it’s not as beautiful as other plugins look like. To work around this, we will create 3 other components in the components directory which are actually a bunch of CSS and nothing more:

  • Create these files:

    admin/src/components/Label/index.js

    admin/src/components/P/index.js

    admin/src/components/Row/index.js

  • Go to P directory and paste the following code inside index.js file:

import styled from "styled-components";  
const P = styled.p`  
  margin-top: 10px;
  text-align: center;
  font-size: 13px;
  color: #9ea7b8;
  u {
    color: #1c5de7;
  }
`;
export default P;  

Basically, what we are doing is to map our desired styles on a specific component. This will save us boilerplate code, and gives us the chance to provide CSS styles in a structured fashion as in separate components. No need to mention that we are writing our CSS inside JavaScript which is an awesome thing for itself! The other great benefit of using “styled components” is that it allows us to adopt styles based on “props”, so there wouldn’t be much difference between these components and those of react; You can see an example of using props in the Label component which we are going to write later. Worth mentioning that Strapi UI components library “Buffet js” is using “styled components” to provide us a collection of nicely designed UI components. Here is some of the “Buffet js” components we can use:

There are a lot more to “styled components” that you can look for on this link
You can checkout Strapi “Buffet js” components at this link

  • Go to Row directory and paste the following inside index.js file:
import styled from "styled-components";  
const Row = styled.div`  
  padding-top: 18px;
`;
export default Row;  
  • and finally for our Label:
import styled, { css, keyframes } from "styled-components";  
const Label = styled.label`  
  position: relative;
  height: 146px;
  width: 100%;
  padding-top: 28px;
  border: 2px dashed #e3e9f3;
  border-radius: 2px;
  text-align: center;
  > input {
    display: none;
  }
  .icon {
    width: 82px;
    path {
      fill: ${({ showLoader }) => (showLoader ? "#729BEF" : "#ccd0da")};
      transition: fill 0.3s ease;
    }
  }
  .isDragging {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
  }
  .underline {
    color: #1c5de7;
    text-decoration: underline;
    cursor: pointer;
  }
  &:hover {
    cursor: pointer;
  }
  ${({ isDragging }) => {
    if (isDragging) {
      return css`
        background-color: rgba(28, 93, 231, 0.01) !important;
        border: 2px dashed rgba(28, 93, 231, 0.1) !important;
      `;
    }
  }}
  ${({ showLoader }) => {
    if (showLoader) {
      return css`
        animation: ${smoothBlink("transparent", "rgba(28,93,231,0.05)")} 2s
          linear infinite;
      `;
    }
  }}
`;
const smoothBlink = (firstColor, secondColor) => keyframes`  
0% {  
fill: ${firstColor}; background-color: ${firstColor}; } 26% {  
fill: ${secondColor}; background-color: ${secondColor}; } 76% {  
fill: ${firstColor}; background-color: ${firstColor}; } `;  
export default Label;  

The styling for our Label is expecting 2 props provided by its parent: isDragging & showLoader. We are also leveraging from a custom defined function named smoothBlink for an animated blink. When the user is dragging something on the input, the background will get changed and whenever we are loading something, there would be a nice blink on our input which is really awesome!
let’s use these new beautiful pieces of CSS;

  • Go back to index.js file inside UploadFileForm directory and import the newly created components:
import React, { Component } from "react";  
import PropTypes from "prop-types";  
import P from "../P";  
import Row from "../Row";  
import Label from "../Label";  
  • Let’s define some event handlers for the purpose of allowing users to drag & drop their files in addition to browsing; paste the following methods inside the class:
class UploadFileForm extends Component {  
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    },
    isDragging: false
  };
  onChangeImportFile = file => {
    file &&
      this.setState({
        file,
        type: file.type,
        options: { ...this.state.options, filename: file.name }
      });
  };
  handleDragEnter = () => this.setState({ isDragging: true }); // <---
  handleDragLeave = () => this.setState({ isDragging: false }); // <---
  handleDrop = e => { // <---
    e.preventDefault();
    this.setState({ isDragging: false });
    const file = e.dataTransfer.files[0];
    this.onChangeImportFile(file);
  };
...

as you can see, they are operating on a specific state property called isDragging.

  • Let’s update the state definition as well:
state = {  
    file: null,
    type: null,
    options: {
      filename: null
    },
    isDragging: false // <---
  };

it’s time to show our beautiful upload form!

  • Write the render method as below:
  render() {
    return (
      <div className={"col-12"}>
        <Row className={"row"}>
          <Label
            isDragging={this.state.isDragging}
            onDrop={this.handleDrop}
            onDragEnter={this.handleDragEnter}
            onDragOver={e => {
              e.preventDefault();
              e.stopPropagation();
            }}
          >
            <svg
              className="icon"
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 104.40317 83.13328"
            >
              <g>
                <rect
                  x="5.02914"
                  y="8.63138"
                  width="77.33334"
                  height="62.29167"
                  rx="4"
                  ry="4"
                  transform="translate(-7.45722 9.32921) rotate(-12)"
                  fill="#fafafb"
                />
                <rect
                  x="5.52914"
                  y="9.13138"
                  width="76.33334"
                  height="61.29167"
                  rx="4"
                  ry="4"
                  transform="translate(-7.45722 9.32921) rotate(-12)"
                  fill="none"
                  stroke="#979797"
                />
                <path
                  d="M74.25543,36.05041l3.94166,18.54405L20.81242,66.79194l-1.68928-7.94745,10.2265-16.01791,7.92872,5.2368,16.3624-25.62865ZM71.974,6.07811,6.76414,19.93889a1.27175,1.27175,0,0,0- .83343.58815,1.31145,1.31145,0,0,0-.18922,1.01364L16.44028,71.87453a1.31145,1.31145,0,0,0,.58515.849,1.27176,1.27176,0,0,0,1.0006.19831L83.23586,59.06111a1.27177,1.27177,0,0,0,.83343- .58815,1.31146,1.31146,0,0,0,.18922-1.01364L73.55972,7.12547a1.31146,1.31146,0,0,0-.58514-.849A1.27177,1.27177,0,0,0,71.974,6.07811Zm6.80253- .0615L89.4753,56.35046A6.5712,6.5712,0,0,1,88.554,61.435a6.37055,6.37055,0,0,1-4.19192,2.92439L19.15221,78.22019a6.37056,6.37056,0,0,1-5.019-.96655,6.57121,6.57121,0,0,1-2.90975- 4.27024L.5247,22.64955A6.57121,6.57121,0,0,1,1.446,17.565a6.37056,6.37056,0,0,1,4.19192-2.92439L70.84779.77981a6.37055,6.37055,0,0,1,5.019.96655A6.5712,6.5712,0,0,1,78.77651,6.01661Z"
                  transform="translate(-0.14193 -0.62489)"
                  fill="#333740"
                />
                <rect
                  x="26.56627"
                  y="4.48824"
                  width="62.29167"
                  height="77.33333"
                  rx="4"
                  ry="4"
                  transform="translate(0.94874 87.10632) rotate(-75)"
                  fill="#fafafb"
                />
                <rect
                  x="27.06627"
                  y="4.98824"
                  width="61.29167"
                  height="76.33333"
                  rx="4"
                  ry="4"
                  transform="translate(0.94874 87.10632) rotate(-75)"
                  fill="none"
                  stroke="#979797"
                />
                <path
                  d="M49.62583,26.96884A7.89786,7.89786,0,0,1,45.88245,31.924a7.96,7.96,0,0,1-10.94716-2.93328,7.89786,7.89786,0,0,1-.76427-6.163,7.89787,7.89787,0,0,1,3.74338- 4.95519,7.96,7.96,0,0,1,10.94716,2.93328A7.89787,7.89787,0,0,1,49.62583,26.96884Zm37.007,26.73924L81.72608,72.02042,25.05843,56.83637l2.1029- 7.84815L43.54519,39.3589l4.68708,8.26558L74.44644,32.21756ZM98.20721,25.96681,33.81216,8.71221a1.27175,1.27175,0,0,0-1.00961.14568,1.31145,1.31145,0,0,0-
10  
.62878.81726L18.85537,59.38007a1.31145,1.31145,0,0,0,.13591,1.02215,1.27176,1.27176,0,0,0,.80151.631l64.39506,17.2546a1.27177,1.27177,0,0,0,1.0096-.14567,1.31146,1.31146,0,0,0,.62877-.81726l13.3184- 49.70493a1.31146,1.31146,0,0,0-.13591-1.02215A1.27177,1.27177,0,0,0,98.20721,25.96681Zm6.089,3.03348L90.97784,78.70523a6.5712,6.5712,0,0,1-3.12925,4.1121,6.37055,6.37055,0,0,1- 5.06267.70256L18.39086,66.26529a6.37056,6.37056,0,0,1-4.03313-3.13977,6.57121,6.57121,0,0,1-.654-5.12581L27.02217,8.29477a6.57121,6.57121,0,0,1,3.12925-4.11211,6.37056,6.37056,0,0,1,5.06267- .70255l64.39506,17.2546a6.37055,6.37055,0,0,1,4.03312,3.13977A6.5712,6.5712,0,0,1,104.29623,29.0003Z"
                  transform="translate(-0.14193 -0.62489)"
                  fill="#333740"
                />
              </g>
            </svg>
            <P>
              <span>
                Drag & drop your file into this area or
                <span className={"underline"}>browse</span> for a file to upload
              </span>
            </P>
            <div onDragLeave={this.handleDragLeave} className="isDragging" />
            <input
              name="file_input"
              accept=".csv"
              onChange={({ target: { files } }) =>
                files && this.onChangeImportFile(files[0])
              }
              type="file"
            />
          </Label>
        </Row>
      </div>
    );
  }

We are passing isDragging down to our Label; This way our Label will receive the value for isDragging from its parent and that is what we call a "prop" in react.

  • Go to containers directory and open the index.js file inside HomePage directory; change the contents with this:
import React, { memo } from "react";  
// import PropTypes from "prop-types";
import pluginId from "../../pluginId";  
import UploadFileForm from "../../components/UploadFileForm";

const HomePage = () => {  
  return <UploadFileForm />;
};
export default memo(HomePage);  

Visit http://localhost:8000/admin/plugins/import-content

Valla! Let’s take this even further by appending a button to the bottom of our beautiful upload form;

  • Go back to index.js file at UploadFileForm directory. Append this Row at the end of the render method output under the first Row tag:
...
    <Row className={"row"}>
          <Button
            label={"Analyze"}
            color={this.state.file ? "secondary" : "cancel"}
            disabled={!this.state.file}
          />
    </Row>
...

So your file should look like this:

import React, { Component } from "react";  
import PropTypes from "prop-types";  
import P from "../P";  
import Row from "../Row";  
import Label from "../Label";

class UploadFileForm extends Component {  
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    },
    isDragging: false
  };
  onChangeImportFile = file => {
    file &&
      this.setState({
        file,
        type: file.type,
        options: { ...this.state.options, filename: file.name }
      });
  };
  handleDragEnter = () => this.setState({ isDragging: true });
  handleDragLeave = () => this.setState({ isDragging: false });
  handleDrop = e => {
    e.preventDefault();
    this.setState({ isDragging: false });
    const file = e.dataTransfer.files[0];
    this.onChangeImportFile(file);
  };
  render() {
    return (
      <div className={"col-12"}>
        <Row className={"row"}>
          <Label
            isDragging={this.state.isDragging}
            onDrop={this.handleDrop}
            onDragEnter={this.handleDragEnter}
            onDragOver={e => {
              e.preventDefault();
              e.stopPropagation();
            }}
          >
            <svg
              className="icon"
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 104.40317 83.13328"
            >
              <g>
                <rect
                  x="5.02914"
                  y="8.63138"
                  width="77.33334"
                  height="62.29167"
                  rx="4"
                  ry="4"
                  transform="translate(-7.45722 9.32921) rotate(-12)"
                  fill="#fafafb"
                />
                <rect
                  x="5.52914"
                  y="9.13138"
                  width="76.33334"
                  height="61.29167"
                  rx="4"
                  ry="4"
                  transform="translate(-7.45722 9.32921) rotate(-12)"
                  fill="none"
                  stroke="#979797"
                />
                <path
                  d="M74.25543,36.05041l3.94166,18.54405L20.81242,66.79194l-1.68928-7.94745,10.2265-16.01791,7.92872,5.2368,16.3624-25.62865ZM71.974,6.07811,6.76414,19.93889a1.27175,1.27175,0,0,0- .83343.58815,1.31145,1.31145,0,0,0-.18922,1.01364L16.44028,71.87453a1.31145,1.31145,0,0,0,.58515.849,1.27176,1.27176,0,0,0,1.0006.19831L83.23586,59.06111a1.27177,1.27177,0,0,0,.83343- .58815,1.31146,1.31146,0,0,0,.18922-1.01364L73.55972,7.12547a1.31146,1.31146,0,0,0-.58514-.849A1.27177,1.27177,0,0,0,71.974,6.07811Zm6.80253- .0615L89.4753,56.35046A6.5712,6.5712,0,0,1,88.554,61.435a6.37055,6.37055,0,0,1-4.19192,2.92439L19.15221,78.22019a6.37056,6.37056,0,0,1-5.019-.96655,6.57121,6.57121,0,0,1-2.90975- 4.27024L.5247,22.64955A6.57121,6.57121,0,0,1,1.446,17.565a6.37056,6.37056,0,0,1,4.19192-2.92439L70.84779.77981a6.37055,6.37055,0,0,1,5.019.96655A6.5712,6.5712,0,0,1,78.77651,6.01661Z"
                  transform="translate(-0.14193 -0.62489)"
                  fill="#333740"
                />
                <rect
                  x="26.56627"
                  y="4.48824"
                  width="62.29167"
                  height="77.33333"
                  rx="4"
                  ry="4"
                  transform="translate(0.94874 87.10632) rotate(-75)"
                  fill="#fafafb"
                />
                <rect
                  x="27.06627"
                  y="4.98824"
                  width="61.29167"
                  height="76.33333"
                  rx="4"
                  ry="4"
                  transform="translate(0.94874 87.10632) rotate(-75)"
                  fill="none"
                  stroke="#979797"
                />
                <path
                  d="M49.62583,26.96884A7.89786,7.89786,0,0,1,45.88245,31.924a7.96,7.96,0,0,1-10.94716-2.93328,7.89786,7.89786,0,0,1-.76427-6.163,7.89787,7.89787,0,0,1,3.74338- 4.95519,7.96,7.96,0,0,1,10.94716,2.93328A7.89787,7.89787,0,0,1,49.62583,26.96884Zm37.007,26.73924L81.72608,72.02042,25.05843,56.83637l2.1029- 7.84815L43.54519,39.3589l4.68708,8.26558L74.44644,32.21756ZM98.20721,25.96681,33.81216,8.71221a1.27175,1.27175,0,0,0-1.00961.14568,1.31145,1.31145,0,0,0-
10  
.62878.81726L18.85537,59.38007a1.31145,1.31145,0,0,0,.13591,1.02215,1.27176,1.27176,0,0,0,.80151.631l64.39506,17.2546a1.27177,1.27177,0,0,0,1.0096-.14567,1.31146,1.31146,0,0,0,.62877-.81726l13.3184- 49.70493a1.31146,1.31146,0,0,0-.13591-1.02215A1.27177,1.27177,0,0,0,98.20721,25.96681Zm6.089,3.03348L90.97784,78.70523a6.5712,6.5712,0,0,1-3.12925,4.1121,6.37055,6.37055,0,0,1- 5.06267.70256L18.39086,66.26529a6.37056,6.37056,0,0,1-4.03313-3.13977,6.57121,6.57121,0,0,1-.654-5.12581L27.02217,8.29477a6.57121,6.57121,0,0,1,3.12925-4.11211,6.37056,6.37056,0,0,1,5.06267- .70255l64.39506,17.2546a6.37055,6.37055,0,0,1,4.03312,3.13977A6.5712,6.5712,0,0,1,104.29623,29.0003Z"
                  transform="translate(-0.14193 -0.62489)"
                  fill="#333740"
                />
              </g>
            </svg>
            <P>
              <span>
                Drag & drop your file into this area or
                <span className={"underline"}>browse</span> for a file to upload
              </span>
            </P>
            <div onDragLeave={this.handleDragLeave} className="isDragging" />
            <input
              name="file_input"
              accept=".csv"
              onChange={({ target: { files } }) =>
                files && this.onChangeImportFile(files[0])
              }
              type="file"
            />
          </Label>
        </Row>
        {/*---HERE---*/}
        <Row className={"row"}>
          <Button
            label={"Analyze"}
            color={this.state.file ? "secondary" : "cancel"}
            disabled={!this.state.file}
          />
        </Row>
      </div>
    );
  }
}
export default UploadFileForm;  
  • At the top of index.js file, we must append the import for our Button as well:
import React, { Component } from "react";  
import PropTypes from "prop-types";  
import P from "../P";  
import Row from "../Row";  
import Label from "../Label";  
import { Button } from "@buffetjs/core";  

The only thing left for this component is that when the user clicks on our “Analyze” button we must send some information about the uploaded file to our backend for analyzing. This information consists of the “file name”, the source of import (for our case “file upload”), the type of imported data (like “csv”), the data to import (the contents of the file) & etc.

For this we need to define a method named onRequestAnalysis in our HomePage and pass it down to our UploadFileForm. So whenever users click on “Analyze” button, this method will get called from our UploadFileForm component:

  • Change index.js file at HomePage directory as below:
import React, { memo, Component } from "react";  
import {request} from "strapi-helper-plugin";  
import PropTypes from "prop-types";  
import pluginId from "../../pluginId";  
import UploadFileForm from "../../components/UploadFileForm";

class HomePage extends Component {  
  render() {
    return <UploadFileForm />;
  };
}
export default memo(HomePage);  
  • with our HomePage adopted to being a class based component, let’s define a state for it:
class HomePage extends Component {  
  state = {
    analyzing: false,
    analysis: null
  };
...

analyzing will indicate if we are currently analyzing data or not; this way we can show our animated blink while analyzing request is being processed by the backend. analysis on the other hand is the result of analyzing process that we will receive from our backend per request.

  • It’s time to write a method that will actually make the “analyzing request”, Add the following method just under your state:
...
onRequestAnalysis = async analysisConfig => {  
    this.analysisConfig = analysisConfig;
    this.setState({ analyzing: true }, async () => {
      try {
        const response = await request("/import-content/preAnalyzeImportFile", {
          method: "POST",
          body: analysisConfig
        });

        this.setState({ analysis: response, analyzing: false }, () => {
          strapi.notification.success(`Analyzed Successfully`);
        });
      } catch (e) {
        this.setState({ analyzing: false }, () => {
          strapi.notification.error(`Analyze Failed, try again`);
          strapi.notification.error(`${e}`);
        });
      }
    });
  };
...

We will pass this method down to UploadFileForm component and basically what we are doing in this method is to send an “analyzing request” that contains information about our data (uploaded file). Notice how we are using strapi notification to inform users of the response, or show them an error. This information has been passed to this method from the UploadFileForm component.

  • So let’s introduce this method to our UploadFileForm component:
...
render() {  
    return <UploadFileForm onRequestAnalysis={this.onRequestAnalysis} />;
  }
...
  • Now that our UploadFileForm has a way to send requests, let’s introduce the last thing to this component:
...
render() {  
   return (
      <UploadFileForm
        onRequestAnalysis={this.onRequestAnalysis}
        loadingAnalysis={this.state.analyzing}
      />
    );
}
...

loadingAnalysis allows UploadFileForm component to show that nice blinking animation when we are in the middle of an analyzing process. Now that our UploadFileForm component has everything at hand, it is time to actually use them!

  • Open the index.js file at UploadFileForm directory and add this function to the Label:
<Label  
  showLoader={this.props.loadingAnalysis} // <---
  isDragging={this.state.isDragging}
  onDrop={this.handleDrop}
  onDragEnter={this.handleDragEnter}
  onDragOver={e => {
    e.preventDefault();
    e.stopPropagation();
  }}
>

The above code will do the job for blinking! Now it’s time for the “analyze request”; define the new methods as below:

readFileContent = file => {  
    const reader = new FileReader();
    return new Promise((resolve, reject) => {
      reader.onload = event => resolve(event.target.result);
      reader.onerror = reject;
      reader.readAsText(file);
    });
  };

  clickAnalyzeUploadFile = async () => {
    const { file, options } = this.state;
    const data = file && (await this.readFileContent(file));
    data &&
      this.props.onRequestAnalysis({
        source: "upload",
        type: file.type,
        options,
        data
      });
  };

The readFileContent will read the contents of the file (we need it for the analyzing request) and the clickAnalyzeUploadFile is the click handler for our “Analyze” button; as we said before, onRequestAnalysis is passed to this component as a prop and we are calling it with the information of the uploaded file to make our “analyze request”.

  • Now let’s introduce this to the button:
<Row className={"row"}>  
  <Button
    label={"Analyze"}
    color={this.state.file ? "secondary" : "cancel"}
    disabled={!this.state.file}
    onClick={this.clickAnalyzeUploadFile} // <---
  />
</Row>  
  • And at last, we will define our received props at the bottom of the file:
...
UploadFileForm.propTypes = {  
  onRequestAnalysis: PropTypes.func.isRequired,
  loadingAnalysis: PropTypes.bool.isRequired
};
export default UploadFileForm;  

Now that we have a working UploadFileForm component,
Let’s make our HomePage more like an actual plugin.

  • Go to components directory and create a new directory named Block:

    admin/src/components/Block/index.js admin/src/components/Block/components.js

  • Define 2 files named index.js and components.js inside that directory, then open the components.js file and paste the following:

import styled from "styled-components";  
const Wrapper = styled.div`  
  margin-bottom: 35px;
  background: #ffffff;
  padding: 22px 28px 18px;
  border-radius: 2px;
  box-shadow: 0 2px 4px #e3e9f3;
  -webkit-font-smoothing: antialiased;
`;
const Sub = styled.div`  
  padding-top: 0px;
  line-height: 18px;
  > p:first-child {
    margin-bottom: 1px;
    font-weight: 700;
    color: #333740;
    font-size: 1.8rem;
  }
  > p {
    color: #787e8f;
    font-size: 13px;
  }
`;
export { Wrapper, Sub };  
  • Open the index.js file and paste the following as well:
import React, { memo } from "react";  
import PropTypes from "prop-types";  
import { Wrapper, Sub } from "./components";  
const Block = ({ children, description, style, title }) => (  
  <div className="col-md-12">
    <Wrapper style={style}>
      <Sub>
        {!!title && <p>{title} </p>} {!!description && <p>{description} </p>}
      </Sub>
      {children}
    </Wrapper>
  </div>
);
Block.defaultProps = {  
  children: null,
  description: null,
  style: {},
  title: null
};
Block.propTypes = {  
  children: PropTypes.any,
  description: PropTypes.string,
  style: PropTypes.object,
  title: PropTypes.string
};
export default memo(Block);  
  • Open index.js file at HomePage directory and add the following at the top:
...
import {  
  HeaderNav,
  LoadingIndicator,
  PluginHeader
} from "strapi-helper-plugin";
import Row from "../../components/Row";  
import Block from "../../components/Block";  
import { Select, Label } from "@buffetjs/core";  
import { get, has, isEmpty, pickBy, set } from "lodash";

const getUrl = to =>  
  to ? `/plugins/${pluginId}/${to}` : `/plugins/${pluginId}`;
...
  • Change the render output as below:
      <div className={"container-fluid"} style={{ padding: "18px 30px" }}>
        <PluginHeader
          title={"Import Content"}
          description={"Import CSV and RSS-Feed into your Content Types"}
        />
        <HeaderNav
          links={[
            {
              name: "Import Data",
              to: getUrl("")
            },
            {
              name: "Import History",
              to: getUrl("history")
            }
          ]}
          style={{ marginTop: "4.4rem" }}
        />
        <div className="row">
          <Block
            title="General"
            description="Configure the Import Source & Destination"
            style={{ marginBottom: 12 }}
          >
            <UploadFileForm
              onRequestAnalysis={this.onRequestAnalysis}
              loadingAnalysis={this.state.analyzing}
            />
          </Block>
        </div>
      </div>

Valla! Now it looks like an actual plugin, well done!

Notice how we leveraged from “Strapi Helper Plugin” UI components like PluginHeader & HeaderNav. PluginHeader will render our plugin’s title & description; and HeaderNav will give us a tab based structure to navigate between pages.

For now, we have provided our users a way to upload their data files and this is really good, but what if they don’t possess a file at all? What if their data is an “RSS feed” or they want to manually type it? So users need more data sources than the current one. Do not worry about this because we are going to provide our users with more options as their data sources.

  • Define the import sources” at the begging of the class:
class HomePage extends Component {

 importSources = [
    { label: "External URL ", value: "url" },
    { label: "Upload file", value: "upload" },
    { label: "Raw text", value: "raw" }
  ];
...
  • Update the state:
state = {  
  importSource: "upload", // <---
  analyzing: false,
  analysis: null
};
  • Change the render output as below:
    return (
      <div className={"container-fluid"} style={{ padding: "18px 30px" }}>
        <PluginHeader
          title={"Import Content"}
          description={"Import CSV and RSS-Feed into your Content Types"}
        />
        <HeaderNav
          links={[
            {
              name: "Import Data",
              to: getUrl("")
            },
            {
              name: "Import History",
              to: getUrl("history")
            }
          ]}
          style={{ marginTop: "4.4rem" }}
        />
        <div className="row">
          <Block
            title="General"
            description="Configure the Import Source & Destination"
            style={{ marginBottom: 12 }}
          >
            <Row className={"row"}>
              <div className={"col-4"}>
                <Label htmlFor="importSource">Import Source</Label>
                <Select
                  name="importSource"
                  options={this.importSources}
                  value={this.state.importSource}
                />
              </div>
            </Row>
            <UploadFileForm
              onRequestAnalysis={this.onRequestAnalysis}
              loadingAnalysis={this.state.analyzing}
            />
          </Block>
        </div>
      </div>
    );

Very nice! But if you notice we cannot change the default option.

  • So let’s define a method to handle option changes just under your state:
...
selectImportSource = importSource => {  
    this.setState({ importSource });
 };
...
  • for our Select component:
...
<Select  
  name="importSource"
  options={this.importSources}
  value={this.state.importSource}
  onChange={({ target: { value } }) =>
    this.selectImportSource(value)
  }
/>
...

Good! Now that our users know how to import their data, it’s time to know where to import that data! We need to show the users a list of Content Types” to choose from;
For this we are going to use an already defined endpoint named getcontenttypes from the “Content Type Builder” plugin;

  • First go and allow public access to this end point at the “Roles & Permissions” plugin, then change the state:

state = {  
  loading: true, // <---
  modelOptions: [], // <---
  models: [], // <---
  importSource: "upload",
  analyzing: false,
  analysis: null
};
  • Define the getModels method:
getModels = async () => {  
    this.setState({ loading: true });
    try {
      const response = await request("/content-type-builder/content-types", {
        method: "GET"
      });

      // Remove non-user content types from models
      const models = get(response, ["data"], []).filter(
        obj => !has(obj, "plugin")
      );
      const modelOptions = models.map(model => {
        return {
          label: get(model, ["schema", "name"], ""), // (name is used for display_name)
          value: model.uid // (uid is used for table creations)
        };
      });

      this.setState({ loading: false });

      return { models, modelOptions };
    } catch (e) {
      this.setState({ loading: false }, () => {
        strapi.notification.error(`${e}`);
      });
    }
    return [];
  };

Awesome! We want this getModels to be called when the users navigate to our plugin page in order to show them a list of “Content Types”.

The best way to do this is to call our method in a “life cycle hook”

  • Update your state like the following:
...
state = {  
  loading: true,
  modelOptions: [],
  models: [],
  importSource: "upload",
  analyzing: false,
  analysis: null,
  selectedContentType: "" // <---
};
...
  • Create a componentDidMount method just under your state:
componentDidMount() {  
      this.getModels().then(res => {
      const { models, modelOptions } = res;
      this.setState({
        models,
        modelOptions,
        selectedContentType: modelOptions ? modelOptions[0].value : ""
      });
    });
}

selectedContentType is a way to keep track of user selected “Content Type” and we will fill it with the first “Content Type” as the default selected option. componentDidMount is a react life cycle hook which is called whenever our component is mounted; so every time the user comes to our plugin, this function will get called.

  • Now for the UI, add this div just after the "Import source" div:
...
<div className={"col-4"}>  
    <Label htmlFor="importDest">Import Destination</Label>
    <Select
        value={this.state.selectedContentType}
        name="importDest"
        options={this.state.modelOptions}
    />
</div>  
...

Your code should look like this:

import React, { memo, Component } from "react";  
import {request} from "strapi-helper-plugin";  
import PropTypes from "prop-types";  
import pluginId from "../../pluginId";  
import UploadFileForm from "../../components/UploadFileForm";

import {  
  HeaderNav,
  LoadingIndicator,
  PluginHeader
} from "strapi-helper-plugin";
import Row from "../../components/Row";  
import Block from "../../components/Block";  
import { Select, Label } from "@buffetjs/core";  
import { get, has, isEmpty, pickBy, set } from "lodash";

const getUrl = to =>  
  to ? `/plugins/${pluginId}/${to}` : `/plugins/${pluginId}`;

class HomePage extends Component {  
  importSources = [
    { label: "External URL ", value: "url" },
    { label: "Upload file", value: "upload" },
    { label: "Raw text", value: "raw" }
  ];

  state = {
    loading: true,
    modelOptions: [],
    models: [],
    importSource: "upload",
    analyzing: false,
    analysis: null,
    selectedContentType: ""
  };

  componentDidMount() {
      this.getModels().then(res => {
      const { models, modelOptions } = res;
      this.setState({
        models,
        modelOptions,
        selectedContentType: modelOptions ? modelOptions[0].value : ""
      });
    });
  }

  getModels = async () => {
    this.setState({ loading: true });
    try {
      const response = await request("/content-type-builder/content-types", {
        method: "GET"
      });

      // Remove content types from models
      const models = get(response, ["data"], []).filter(
        obj => !has(obj, "plugin")
      );
      const modelOptions = models.map(model => {
        return {
          label: get(model, ["schema", "name"], ""),
          value: model.uid
        };
      });

      this.setState({ loading: false });

      return { models, modelOptions };
    } catch (e) {
      this.setState({ loading: false }, () => {
        strapi.notification.error(`${e}`);
      });
    }
    return [];
  };

  selectImportSource = importSource => {
    this.setState({ importSource });
  };

  onRequestAnalysis = async analysisConfig => {
    this.analysisConfig = analysisConfig;
    this.setState({ analyzing: true }, async () => {
      try {
        const response = await request("/import-content/preAnalyzeImportFile", {
          method: "POST",
          body: analysisConfig
        });

        this.setState({ analysis: response, analyzing: false }, () => {
          strapi.notification.success(`Analyzed Successfully`);
        });
      } catch (e) {
        this.setState({ analyzing: false }, () => {
          strapi.notification.error(`Analyze Failed, try again`);
          strapi.notification.error(`${e}`);
        });
      }
    });
  };

  render() {
    return (
      <div className={"container-fluid"} style={{ padding: "18px 30px" }}>
        <PluginHeader
          title={"Import Content"}
          description={"Import CSV and RSS-Feed into your Content Types"}
        />
        <HeaderNav
          links={[
            {
              name: "Import Data",
              to: getUrl("")
            },
            {
              name: "Import History",
              to: getUrl("history")
            }
          ]}
          style={{ marginTop: "4.4rem" }}
        />
        <div className="row">
          <Block
            title="General"
            description="Configure the Import Source & Destination"
            style={{ marginBottom: 12 }}
          >
            <Row className={"row"}>
              <div className={"col-4"}>
                <Label htmlFor="importSource">Import Source</Label>
                <Select
                  name="importSource"
                  options={this.importSources}
                  value={this.state.importSource}
                  onChange={({ target: { value } }) =>
                    this.selectImportSource(value)
                  }
                />
              </div>
              <div className={"col-4"}>
                <Label htmlFor="importDest">Import Destination</Label>
                <Select
                  value={this.state.selectedContentType}
                  name="importDest"
                  options={this.state.modelOptions}
                />
              </div>
            </Row>
            <UploadFileForm
              onRequestAnalysis={this.onRequestAnalysis}
              loadingAnalysis={this.state.analyzing}
            />
          </Block>
        </div>
      </div>
    );
  }
}
export default memo(HomePage);  

Note Just before seeing the result, make sure that you have at least 1 “Content Type” available

  • And in order to actually respond to interactions, define the below method as our “Select” option changes handler, again, just under the state:
...
selectImportDest = selectedContentType => {  
  this.setState({ selectedContentType });
};
...
  • For our “Select”, just add this onChange function:
...
<Select  
  value={this.state.selectedContentType}
  name="importDest"
  options={this.state.modelOptions}
  onChange={({ target: { value } }) =>
    this.selectImportDest(value)
  }
/>
...

Awesome you finished the first part of this tutorial! Let's import data from url or raw input text now !
https://strapi.io/blog/how-to-create-an-import-content-plugin-part-2-4

News in your inbox

Did you enjoy this article? Subscribe to get the latest posts and the most important updates!