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

Table of contents

You can reach the code for this tutorial at this link

HistoryPage

  • 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.

  • Go to App directory inside containers directory and add the following route to the index.js file:
...
<Route  
 path={`/plugins/${pluginId}/history`}
 component={HistoryPage}
 exact
/>
...

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:

/* * * HistoryPage * */
import React, { Component } from "react";  
import {  
  HeaderNav,
  LoadingIndicator,
  PluginHeader,
  request
} from "strapi-helper-plugin";
import pluginId from "../../pluginId";  
import Row from "../../components/Row";  
import Block from "../../components/Block";

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

class HistoryPage extends Component {}

export default HistoryPage;  

We want this page to have a similar look with respect to our HomePage.

  • So we will render something like this:
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="Manage the Initiated Imports"
            style={{ marginBottom: 12 }}
          />
        </div>
      </div>
    );
}

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:

import React, { Component } from "react";  
import PropTypes from "prop-types";  
import { Table, Button } from "@buffetjs/core";  
import moment from "moment";  
import { LoadingIndicator, PopUpWarning } from "strapi-helper-plugin";  
class HistoryTable extends Component {  
  state = {
    showDeleteModal: false,
    showUndoModal: false,
    importToDelete: null,
    importToUndo: null
  };
}
HistoryTable.propTypes = {  
  configs: PropTypes.array.isRequired,
  deleteImport: PropTypes.func,
  undoImport: PropTypes.func
};
export 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.

  • So the following functions will do the job. Add them just after your state:
...
  deleteImport = id => {
    this.setState({ showDeleteModal: true, importToDelete: id });
  };
  undoImport = id => {
    this.setState({ showUndoModal: true, importToUndo: id });
  };
...

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.

  • Append the below code as our “custom row” to the class definition just after your state:
  CustomRow = ({ row }) => {
    const { id, contentType, importedCount, ongoing, updated_at } = row;
    const updatedAt = moment(updated_at);
    let source;
    switch (row.source) {
      case "upload":
        source = row.options.filename;
        break;
      case "url":
        source = row.options.url;
        break;
      default:
        source = "unknown";
    }
    return (
      <tr style={{ paddingTop: 18 }}>
        <td>{source}</td> <td>{contentType}</td>
        <td>{updatedAt.format("LLL")}</td> <td>{importedCount}</td>
        <td>{ongoing ? <LoadingIndicator /> : <span>Ready</span>} </td>
        <td>
          <div className={"row"}>
            <div
              style={{
                marginRight: 18,
                marginLeft: 18
              }}
              onClick={() => this.undoImport(id)}
            >
              <i className={"fa fa-undo"} role={"button"} />
            </div>
            <div onClick={() => this.deleteImport(id)}>
              <i className={"fa fa-trash"} role={"button"} />
            </div>
          </div>
        </td>
      </tr>
    );
  };

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.

  • Finally, our render method:
render() {  
    const { configs } = this.props;
    const props = {
      title: "Import History",
      subtitle: "Manage the Initiated Imports"
    };
    const headers = [
      { name: "Source", value: "source" },
      { name: "Content Type", value: "contentType" },
      { name: "Updated At", value: "updatedAt" },
      { name: "Items", value: "items" },
      { name: "Progress State", value: "progress" },
      { name: "Actions", value: "actions" }
    ];
    const items = [...configs];
    const {
      importToDelete,
      importToUndo,
      showDeleteModal,
      showUndoModal
    } = this.state;
    return (
      <div className={"col-md-12"} style={{ paddingTop: 12 }}>
        <PopUpWarning
          isOpen={showDeleteModal}
          toggleModal={() => this.setState({ showDeleteModal: null })}
          content={{
            title: `Please confirm`,
            message: `Are you sure you want to delete this entry?`
          }}
          popUpWarningType="danger"
          onConfirm={async () => {
            importToDelete && (await this.props.deleteImport(importToDelete));
          }}
        />
        <PopUpWarning
          isOpen={showUndoModal}
          toggleModal={() => this.setState({ showUndoModal: null })}
          content={{
            title: `Please confirm`,
            message: `Are you sure you want to undo this entry?`
          }}
          popUpWarningType="danger"
          onConfirm={async () => {
            importToUndo && (await this.props.undoImport(importToUndo));
          }}
        />
        <Table
          {...props}
          headers={headers}
          rows={items}
          customRow={this.CustomRow}
        />
      </div>
    );
}

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";

  • then, define the state as below:

...
state = {  
 loading: false,
 importConfigs: []
};
...

As you know we must pass the list of “import configs” down to our HistoryTable.

  • For fetching the list of “import configs” we use the following method. Add it just after your state:
...
getConfigs = async () => {  
    try {
      const response = await request("/import-content", { method: "GET" });
      return response;
    } catch (e) {
      strapi.notification.error(`${e}`);
      return [];
    }
};
...
  • to actually call this method we use the following method. Add it just after your state:
...
importConfigs() {  
    if (!this.state.loading) {
      this.getConfigs().then(res => {
        this.setState({ importConfigs: res });
      });
    }
}
...

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!

  • To understand this better, see the following functions and add them to your file one at the time:
...
componentDidMount() {  
    this.getConfigs().then(res => {
      this.setState({ importConfigs: res, loading: false });
    });
    setTimeout(() => {
      this.fetchInterval = setInterval(() => this.importConfigs(), 4000);
    }, 200);
}
...
  • We will clean our setInterval function call by the below life cycle hook which is called before unmounting the component:
...
componentWillUnmount() {  
    if (this.fetchInterval) {
      clearInterval(this.fetchInterval);
    }
}
...

beside the importConfigs, we will pass 2 methods for delete & undo operations down to our HistoryTable.

  • For the delete operation:
...
deleteImport = async id => {  
      this.setState({ loading: true }, async () => {
      try {
        await request(`/import-content/${id}`, { method: "DELETE" });

        this.setState(prevState => ({
          ...prevState,
          importConfigs: prevState.importConfigs.filter(imp => imp.id !== id),
          loading: false
        }));

        strapi.notification.success(`Deleted`);
      } catch (e) {
        this.setState({ loading: false }, () => {
          strapi.notification.error(`${e}`);
          strapi.notification.error(`Delete Failed`);
        });
      }
    });
};
...
  • For the undo operation:
...
undoImport = async id => {  
    this.setState({ loading: true }, async () => {
      await request(`/import-content/${id}/undo`, { method: "POST" });
      this.setState({ loading: false }, () => {
        strapi.notification.info(`Undo Started`);
      });
    });
};
...
  • Change the render method output as below:
          <Block
            title="General"
            description="Manage the Initiated Imports"
            style={{ marginBottom: 12 }}
          >
            {this.state.loading && <LoadingIndicator />}
            {!this.state.loading && this.state.importConfigs && (
              <Row className={"row"}>
                <HistoryTable
                  undoImport={this.undoImport}
                  deleteImport={this.deleteImport}
                  configs={this.state.importConfigs}
                />
              </Row>
            )}
         </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!

Hail Strapi!

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