Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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
Please note: Since we initially published this blog post, we released new versions of Strapi and tutorials may be outdated. Sorry for the inconvenience if it's the case. Please help us by reporting it here.
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:
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
You need to have node v.12 installed and that's all.
Let's create the Strapi application, our plugin and two models
1
2
3
4
5
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
plugins
directory structure to be exactly like this:1
2
3
4
5
6
7
8
9
10
11
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"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:
1
2
3
4
5
6
7
8
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:
components
directory1
2
3
4
5
6
7
8
9
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
*
* HomePage
*
*/
import React, { memo } from 'react';
// import PropTypes from 'prop-types';
import pluginId from '../../pluginId';
const HomePage = () => {
return (
<div>
<h1>{pluginId}'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:
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:
1
2
3
4
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:
1
2
3
4
5
6
7
8
9
10
class UploadFileForm extends Component {
state = {
file: null,
type: null,
options: {
filename: null
}
};
}
export default UploadFileForm;
render
method:1
2
3
4
5
6
7
8
9
10
class UploadFileForm extends Component {
state = {
file: null,
type: null,
options: {
filename: null
}
};
render() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
class UploadFileForm extends Component {
state = {
file: null,
type: null,
options: {
filename: null
}
};
render() {
return <input name="file_input" accept=".csv" type="file" />;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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:
1
2
3
4
5
6
7
8
9
10
11
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(https://www.styled- components.com/docs/basics#motivation) You can checkout Strapi “Buffet js” components at this link
Row
directory and paste the following inside index.js
file:1
2
3
4
5
import styled from "styled-components";
const Row = styled.div`
padding-top: 18px;
`;
export default Row;
Label
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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;
index.js
file inside UploadFileForm
directory and import the newly created components:1
2
3
4
5
import React, { Component } from "react";
import PropTypes from "prop-types";
import P from "../P";
import Row from "../Row";
import Label from "../Label";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
.
1
2
3
4
5
6
7
8
state = {
file: null,
type: null,
options: {
filename: null
},
isDragging: false // <---
};
it’s time to show our beautiful upload form!
render
method as below:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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.
containers
directory and open the index.js
file inside HomePage
directory; change the contents with this:1
2
3
4
5
6
7
8
9
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;
index.js
file at UploadFileForm
directory. Append this Row
at the end of the render method output under the first Row
tag:1
2
3
4
5
6
7
8
9
...
<Row className={"row"}>
<Button
label={"Analyze"}
color={this.state.file ? "secondary" : "cancel"}
disabled={!this.state.file}
/>
</Row>
...
So your file should look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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;
index.js
file, we must append the import for our Button
as well:1
2
3
4
5
6
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:
index.js
file at HomePage
directory as below:1
2
3
4
5
6
7
8
9
10
11
12
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);
HomePage
adopted to being a class based component, let’s define a state for it:1
2
3
4
5
6
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
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.
UploadFileForm
component:1
2
3
4
5
...
render() {
return <UploadFileForm onRequestAnalysis={this.onRequestAnalysis} />;
}
...
UploadFileForm
has a way to send requests, let’s introduce the last thing to this component:1
2
3
4
5
6
7
8
9
10
...
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!
index.js
file at UploadFileForm
directory and add this function to the Label:1
2
3
4
5
6
7
8
9
10
<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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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”.
1
2
3
4
5
6
7
8
<Row className={"row"}>
<Button
label={"Analyze"}
color={this.state.file ? "secondary" : "cancel"}
disabled={!this.state.file}
onClick={this.clickAnalyzeUploadFile} // <---
/>
</Row>
1
2
3
4
5
6
...
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 };
index.js
file and paste the following as well:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
index.js
file at HomePage
directory and add the following at the top:1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
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}`;
...
render
output as below:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<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.
1
2
3
4
5
6
7
8
class HomePage extends Component {
importSources = [
{ label: "External URL ", value: "url" },
{ label: "Upload file", value: "upload" },
{ label: "Raw text", value: "raw" }
];
...
1
2
3
4
5
state = {
importSource: "upload", // <---
analyzing: false,
analysis: null
};
render
output as below:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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.
state
:1
2
3
4
5
...
selectImportSource = importSource => {
this.setState({ importSource });
};
...
Select
component:1
2
3
4
5
6
7
8
9
10
...
<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;
1
2
3
4
5
6
7
8
state = {
loading: true, // <---
modelOptions: [], // <---
models: [], // <---
importSource: "upload",
analyzing: false,
analysis: null
};
getModels
method:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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”
state
like the following:1
2
3
4
5
6
7
8
9
10
11
...
state = {
loading: true,
modelOptions: [],
models: [],
importSource: "upload",
analyzing: false,
analysis: null,
selectedContentType: "" // <---
};
...
componentDidMount
method just under your state:1
2
3
4
5
6
7
8
9
10
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.
div
just after the "Import source" div
:1
2
3
4
5
6
7
8
9
10
...
<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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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
state
:1
2
3
4
5
...
selectImportDest = selectedContentType => {
this.setState({ selectedContentType });
};
...
onChange
function:1
2
3
4
5
6
7
8
9
10
...
<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
Pouya is an active member of the Strapi community, who has been contributing actively with contributions in the core and plugins.