This tutorial is part of the « How to create your own plugin »:
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.
Table of contents
You can reach the code for this tutorial at this link
Go to containers
directory and create a new directory named HistoryPage
with an empty index.js
file inside:
admin/src/containers/HistoryPage/index.js
Before we dive into writing our HistoryPage
, we need to introduce a route to this new page.
App
directory inside containers
directory and add the following route to the index.js
file:1...
2<Route
3 path={`/plugins/${pluginId}/history`}
4 component={HistoryPage}
5 exact
6/>
7...
Notice that NotFound
is placed after our HistoryPage
and don’t forget to import our container at the top of this file:
import HistoryPage from "../HistoryPage";
Go back to our HistoryPage
directory and paste the below code inside index.js
:
1/* * * HistoryPage * */
2import React, { Component } from "react";
3import {
4 HeaderNav,
5 LoadingIndicator,
6 PluginHeader,
7 request
8} from "strapi-helper-plugin";
9import pluginId from "../../pluginId";
10import Row from "../../components/Row";
11import Block from "../../components/Block";
12
13const getUrl = to =>
14 to ? `/plugins/${pluginId}/${to}` : `/plugins/${pluginId}`;
15
16class HistoryPage extends Component {}
17
18export default HistoryPage;
We want this page to have a similar look with respect to our HomePage
.
1render() {
2 return (
3 <div className={"container-fluid"} style={{ padding: "18px 30px" }}>
4 <PluginHeader
5 title={"Import Content"}
6 description={"Import CSV and RSS-Feed into your Content Types"}
7 />
8 <HeaderNav
9 links={[
10 {
11 name: "Import Data",
12 to: getUrl("")
13 },
14 {
15 name: "Import History",
16 to: getUrl("history")
17 }
18 ]}
19 style={{ marginTop: "4.4rem" }}
20 />
21 <div className="row">
22 <Block
23 title="General"
24 description="Manage the Initiated Imports"
25 style={{ marginBottom: 12 }}
26 />
27 </div>
28 </div>
29 );
30}
What we need next, is a table that renders all the “import configs” available.
Go to components
directory and create a new directory named HistoryTable
with an empty index.js
file inside:
admin/src/components/HistoryTable/index.js
Open the index.js
file and paste the below code:
1import React, { Component } from "react";
2import PropTypes from "prop-types";
3import { Table, Button } from "@buffetjs/core";
4import moment from "moment";
5import { LoadingIndicator, PopUpWarning } from "strapi-helper-plugin";
6class HistoryTable extends Component {
7 state = {
8 showDeleteModal: false,
9 showUndoModal: false,
10 importToDelete: null,
11 importToUndo: null
12 };
13}
14HistoryTable.propTypes = {
15 configs: PropTypes.array.isRequired,
16 deleteImport: PropTypes.func,
17 undoImport: PropTypes.func
18};
19export default HistoryTable;
In our state, we are defining a couple of variables that are responsible for showing the delete
& undo
dialogs and in case the user confirms the delete
or undo
dialogs, we must know which “import config” we are supposed to remove or undo.
state
:1...
2 deleteImport = id => {
3 this.setState({ showDeleteModal: true, importToDelete: id });
4 };
5 undoImport = id => {
6 this.setState({ showUndoModal: true, importToUndo: id });
7 };
8...
It’s time to use the above functions. For our table we are about to use “Buffet js” table with “custom row” just like what we did for MappingTable
.
state
:1 CustomRow = ({ row }) => {
2 const { id, contentType, importedCount, ongoing, updated_at } = row;
3 const updatedAt = moment(updated_at);
4 let source;
5 switch (row.source) {
6 case "upload":
7 source = row.options.filename;
8 break;
9 case "url":
10 source = row.options.url;
11 break;
12 default:
13 source = "unknown";
14 }
15 return (
16 <tr style={{ paddingTop: 18 }}>
17 <td>{source}</td> <td>{contentType}</td>
18 <td>{updatedAt.format("LLL")}</td> <td>{importedCount}</td>
19 <td>{ongoing ? <LoadingIndicator /> : <span>Ready</span>} </td>
20 <td>
21 <div className={"row"}>
22 <div
23 style={{
24 marginRight: 18,
25 marginLeft: 18
26 }}
27 onClick={() => this.undoImport(id)}
28 >
29 <i className={"fa fa-undo"} role={"button"} />
30 </div>
31 <div onClick={() => this.deleteImport(id)}>
32 <i className={"fa fa-trash"} role={"button"} />
33 </div>
34 </div>
35 </td>
36 </tr>
37 );
38 };
We are rendering the details of each “import config” as a row in our table. Also we are showing some buttons for delete
& undo
actions as well.
1render() {
2 const { configs } = this.props;
3 const props = {
4 title: "Import History",
5 subtitle: "Manage the Initiated Imports"
6 };
7 const headers = [
8 { name: "Source", value: "source" },
9 { name: "Content Type", value: "contentType" },
10 { name: "Updated At", value: "updatedAt" },
11 { name: "Items", value: "items" },
12 { name: "Progress State", value: "progress" },
13 { name: "Actions", value: "actions" }
14 ];
15 const items = [...configs];
16 const {
17 importToDelete,
18 importToUndo,
19 showDeleteModal,
20 showUndoModal
21 } = this.state;
22 return (
23 <div className={"col-md-12"} style={{ paddingTop: 12 }}>
24 <PopUpWarning
25 isOpen={showDeleteModal}
26 toggleModal={() => this.setState({ showDeleteModal: null })}
27 content={{
28 title: `Please confirm`,
29 message: `Are you sure you want to delete this entry?`
30 }}
31 popUpWarningType="danger"
32 onConfirm={async () => {
33 importToDelete && (await this.props.deleteImport(importToDelete));
34 }}
35 />
36 <PopUpWarning
37 isOpen={showUndoModal}
38 toggleModal={() => this.setState({ showUndoModal: null })}
39 content={{
40 title: `Please confirm`,
41 message: `Are you sure you want to undo this entry?`
42 }}
43 popUpWarningType="danger"
44 onConfirm={async () => {
45 importToUndo && (await this.props.undoImport(importToUndo));
46 }}
47 />
48 <Table
49 {...props}
50 headers={headers}
51 rows={items}
52 customRow={this.CustomRow}
53 />
54 </div>
55 );
56}
Notice how “Strapi Helper Plugin” PopUpWarning
made it easy for us to show a beautiful confirm dialog for the delete
& undo
actions.
Our table is ready!
Go back to our HistoryPage
and add the following import at the top:
import HistoryTable from "../../components/HistoryTable";
1...
2state = {
3 loading: false,
4 importConfigs: []
5};
6...
As you know we must pass the list of “import configs” down to our HistoryTable
.
state
:1...
2getConfigs = async () => {
3 try {
4 const response = await request("/import-content", { method: "GET" });
5 return response;
6 } catch (e) {
7 strapi.notification.error(`${e}`);
8 return [];
9 }
10};
11...
state
:1...
2importConfigs() {
3 if (!this.state.loading) {
4 this.getConfigs().then(res => {
5 this.setState({ importConfigs: res });
6 });
7 }
8}
9...
The reason behind this method is that we are going to periodically fetch the list of “import configs” for having an up to date list. When the user goes to our HistoryPage
for the first time, we will fetch the list directly by calling getConfigs
method and after that we are going to periodically call the importConfigs
method for the rest of our lives!
1...
2componentDidMount() {
3 this.getConfigs().then(res => {
4 this.setState({ importConfigs: res, loading: false });
5 });
6 setTimeout(() => {
7 this.fetchInterval = setInterval(() => this.importConfigs(), 4000);
8 }, 200);
9}
10...
setInterval
function call by the below life cycle hook which is called before unmounting the component:1...
2componentWillUnmount() {
3 if (this.fetchInterval) {
4 clearInterval(this.fetchInterval);
5 }
6}
7...
beside the importConfigs
, we will pass 2 methods for delete
& undo
operations down to our HistoryTable
.
delete
operation:1...
2deleteImport = async id => {
3 this.setState({ loading: true }, async () => {
4 try {
5 await request(`/import-content/${id}`, { method: "DELETE" });
6
7 this.setState(prevState => ({
8 ...prevState,
9 importConfigs: prevState.importConfigs.filter(imp => imp.id !== id),
10 loading: false
11 }));
12
13 strapi.notification.success(`Deleted`);
14 } catch (e) {
15 this.setState({ loading: false }, () => {
16 strapi.notification.error(`${e}`);
17 strapi.notification.error(`Delete Failed`);
18 });
19 }
20 });
21};
22...
undo
operation:1...
2undoImport = async id => {
3 this.setState({ loading: true }, async () => {
4 await request(`/import-content/${id}/undo`, { method: "POST" });
5 this.setState({ loading: false }, () => {
6 strapi.notification.info(`Undo Started`);
7 });
8 });
9};
10...
render
method output as below:1 <Block
2 title="General"
3 description="Manage the Initiated Imports"
4 style={{ marginBottom: 12 }}
5 >
6 {this.state.loading && <LoadingIndicator />}
7 {!this.state.loading && this.state.importConfigs && (
8 <Row className={"row"}>
9 <HistoryTable
10 undoImport={this.undoImport}
11 deleteImport={this.deleteImport}
12 configs={this.state.importConfigs}
13 />
14 </Row>
15 )}
16 </Block>
Before visiting the plugin page, make sure that you have allowed public access on all the endpoints of our plugin:
Congratulations! If you have made it this far, you are ready to do some awesome things with Strapi plugins.
In this tutorial we have covered the basics as much as we could, what has been left is to integrate Redux into our plugin, writing tests for our plugin and some other tiny details. So keep an eye on our blog for the upcoming tutorials. Thanks to all of you guys! and special thanks to:
Soupette for his kind edits, testing's and suggestions on the react side to make the code as robust as possible!
Maxime Castres for his kind edits, testing's, suggestions and rewriting the whole tutorial from scratch in the Markup format. This tutorial would not make it without him!
Joe Beuckman who wrote this amazing plugin at the first place. In fact I learned a lot from his plugin myself and appreciate the effort he put into this plugin so far!
One last thing, we are trying to make the best possible tutorials for you, help us in this mission by answering this short survey https://strapisolutions.typeform.com/to/bwXvhA?channel=xxxxx
Pouya is an active member of the Strapi community, who has been contributing actively with contributions in the core and plugins.