datocms-plugin-record-bin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # 🗑 Record Bin
2
+
3
+ Record Bin is a DatoCMS plugin that makes it so every record that is deleted through the dasboard is sent to a Bin, where it can then be restored in a single click, or permanently deleted.
4
+
5
+ To use this plugin an auxiliary lambda function is needed. The deployment of that lambda function is further described bellow in the installation section.
6
+
7
+ # Installation and usage
8
+
9
+ The video above shows a step by step tutorial on how to install and use the plugin.
10
+ It follows the following instructions:
11
+
12
+ 1. When you install the plugin, a modal will pop up prompting you to create the lambda function.
13
+ 2. By clicking the Vercel Deploy button you can start a step by step process that will create that lambda function (You will be asked your projects Full API token!)
14
+ 3. After deploying it, copy the Deployed URL and insert it in the modal, and "Finish installation"
15
+
16
+ The installation will then be complete, from then on, when you delete a record you will be able to find its trashed version inside a model called "🗑 Record Bin" (If the model doesn't exist it will be created).
17
+
18
+ If you open the trashed record inside that model, you will find a "Restore Record ♻️" button, that when clicked will restore the record, redirecting you to the resotred record, and deleting its trashed version.
19
+
20
+ In case the restoration fails a message will be shown, along with the option to see the entire API error log.
package/docs/cover.jpg ADDED
Binary file
package/docs/demo.mp4 ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "datocms-plugin-record-bin",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "homepage": "https://github.com/marcelofinamorvieira/record-bin-lambda-function#readme",
6
+ "description": "A plugin to manage and create a backup of deleted records in DatoCMS",
7
+ "keywords": [
8
+ "datocms",
9
+ "datocms-plugin",
10
+ "backup",
11
+ "record",
12
+ "bin"
13
+ ],
14
+ "datoCmsPlugin": {
15
+ "title": "Record bin",
16
+ "previewImage": "docs/demo.mp4",
17
+ "coverImage": "docs/cover.jpg",
18
+ "entryPoint": "build/index.html",
19
+ "permissions": []
20
+ },
21
+ "dependencies": {
22
+ "@types/node": "^16.11.52",
23
+ "@types/react": "^17.0.48",
24
+ "@types/react-dom": "^17.0.17",
25
+ "datocms-plugin-sdk": "^0.6.1",
26
+ "datocms-react-ui": "^0.6.0",
27
+ "react": "^18.2.0",
28
+ "react-dom": "^18.2.0",
29
+ "react-scripts": "5.0.1",
30
+ "typescript": "^4.7.4"
31
+ },
32
+ "scripts": {
33
+ "start": "cross-env BROWSER='none' PUBLIC_URL='/' react-scripts start",
34
+ "build": "cross-env PUBLIC_URL='.' react-scripts build",
35
+ "test": "react-scripts test",
36
+ "eject": "react-scripts eject",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "eslintConfig": {
40
+ "extends": [
41
+ "react-app"
42
+ ]
43
+ },
44
+ "browserslist": {
45
+ "production": [
46
+ ">0.2%",
47
+ "not dead",
48
+ "not op_mini all"
49
+ ],
50
+ "development": [
51
+ "last 1 chrome version",
52
+ "last 1 firefox version",
53
+ "last 1 safari version"
54
+ ]
55
+ },
56
+ "devDependencies": {
57
+ "cross-env": "^7.0.3"
58
+ }
59
+ }
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ </head>
7
+ <body>
8
+ <noscript>You need to enable JavaScript to run this app.</noscript>
9
+ <div id="root"></div>
10
+ </body>
11
+ </html>
@@ -0,0 +1,3 @@
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
@@ -0,0 +1,107 @@
1
+ import { RenderItemFormOutletCtx } from "datocms-plugin-sdk";
2
+ import { Button, Canvas, FieldGroup, Form, Section } from "datocms-react-ui";
3
+ import { useState } from "react";
4
+
5
+ type errorObject = {
6
+ simplifiedError: {
7
+ code: string;
8
+ details: {
9
+ code: string;
10
+ field: string;
11
+ field_id: string;
12
+ field_label: string;
13
+ field_type: string;
14
+ extraneous_attributes: string[];
15
+ fullPayload: string;
16
+ };
17
+ };
18
+ fullErrorPayload: string;
19
+ };
20
+
21
+ const BinOutlet = ({ ctx }: { ctx: RenderItemFormOutletCtx }) => {
22
+ const [isLoading, setLoading] = useState(false);
23
+ const [error, setError] = useState<errorObject>();
24
+
25
+ const restorationHandler = async () => {
26
+ setLoading(true);
27
+
28
+ const parsedBody = JSON.parse(ctx.formValues.record_body as string);
29
+ parsedBody.trashRecordID = ctx.item!.id;
30
+ const requestBody = JSON.stringify(parsedBody);
31
+
32
+ try {
33
+ const restoreResponse = await fetch(
34
+ ctx.plugin.attributes.parameters.vercelURL as string,
35
+ {
36
+ method: "POST",
37
+ body: requestBody,
38
+ headers: { Accept: "*/*", "Content-Type": "application/json" },
39
+ }
40
+ );
41
+ const parsedResponse = await restoreResponse.json();
42
+ if (restoreResponse.status !== 200) {
43
+ setError(parsedResponse.error);
44
+ throw new Error();
45
+ }
46
+
47
+ ctx.notice("The record has been successfully restored!");
48
+ ctx.navigateTo(
49
+ "/editor/item_types/" +
50
+ parsedResponse.restoredRecord.modelID +
51
+ "/items/" +
52
+ parsedResponse.restoredRecord.id
53
+ );
54
+ } catch (error) {
55
+ setLoading(false);
56
+ await ctx.alert("The record could not be restored!");
57
+ }
58
+ };
59
+
60
+ const errorModalHandler = async () => {
61
+ await ctx.openModal({
62
+ id: "errorModal",
63
+ title: "Restoration error",
64
+ width: "l",
65
+ parameters: { errorPayload: error!.fullErrorPayload },
66
+ });
67
+ };
68
+
69
+ return (
70
+ <Canvas ctx={ctx}>
71
+ <Form>
72
+ <FieldGroup>
73
+ {error && (
74
+ <Section title="Restoration error">
75
+ <p>Couldn't restore the record because of the following error:</p>
76
+ <p>
77
+ {error.simplifiedError.code}:{" "}
78
+ {error.simplifiedError.details.field ||
79
+ error.simplifiedError.details.extraneous_attributes}
80
+ </p>
81
+ <p>{error.simplifiedError.details.code}</p>
82
+ <Button onClick={errorModalHandler}>
83
+ See full restoration error report
84
+ </Button>
85
+ <p>
86
+ You can manually correct the errors on the record body, save the
87
+ record, and re-attempt to restore it.
88
+ </p>
89
+ </Section>
90
+ )}
91
+ </FieldGroup>
92
+ <FieldGroup>
93
+ <Button
94
+ buttonType={isLoading ? "muted" : "primary"}
95
+ disabled={isLoading}
96
+ fullWidth
97
+ onClick={restorationHandler}
98
+ >
99
+ Restore record ♻️
100
+ </Button>
101
+ </FieldGroup>
102
+ </Form>
103
+ </Canvas>
104
+ );
105
+ };
106
+
107
+ export default BinOutlet;
@@ -0,0 +1,71 @@
1
+ import { RenderConfigScreenCtx } from "datocms-plugin-sdk";
2
+ import { Button, Canvas, Form, TextField } from "datocms-react-ui";
3
+ import { useState } from "react";
4
+
5
+ type Props = {
6
+ ctx: RenderConfigScreenCtx;
7
+ };
8
+
9
+ export default function ConfigScreen({ ctx }: Props) {
10
+ const [numberOfDays, setNumberOfDays] = useState("7");
11
+ const [isLoading, setLoading] = useState(false);
12
+ const [error, setError] = useState("");
13
+
14
+ const deletionHandler = async () => {
15
+ const userInput = parseInt(numberOfDays);
16
+ if (isNaN(userInput)) {
17
+ setError("Days must be an integerer number");
18
+ return;
19
+ }
20
+
21
+ setLoading(true);
22
+
23
+ const requestBody = {
24
+ event_type: "cleanup",
25
+ numberOfDays: numberOfDays,
26
+ environment: ctx.environment,
27
+ };
28
+
29
+ const parsedBody = JSON.stringify(requestBody);
30
+
31
+ await fetch(ctx.plugin.attributes.parameters.vercelURL as URL, {
32
+ method: "POST",
33
+ body: parsedBody,
34
+ headers: { Accept: "*/*", "Content-Type": "application/json" },
35
+ });
36
+
37
+ ctx.notice(
38
+ `All records older than ${numberOfDays} days in the bin have been deleted.`
39
+ );
40
+
41
+ setLoading(false);
42
+ };
43
+
44
+ return (
45
+ <Canvas ctx={ctx}>
46
+ <h2>Delete all trashed records older than </h2>{" "}
47
+ <Form>
48
+ <TextField
49
+ error={error}
50
+ required
51
+ name="numberOfDays"
52
+ id="numberOfDays"
53
+ label="Days"
54
+ value={numberOfDays}
55
+ onChange={(event) => {
56
+ setNumberOfDays(event);
57
+ setError("");
58
+ }}
59
+ />
60
+ <Button
61
+ onClick={deletionHandler}
62
+ fullWidth
63
+ buttonType={isLoading ? "muted" : "negative"}
64
+ disabled={isLoading}
65
+ >
66
+ Delete
67
+ </Button>
68
+ </Form>
69
+ </Canvas>
70
+ );
71
+ }
@@ -0,0 +1,31 @@
1
+ import { RenderModalCtx } from "datocms-plugin-sdk";
2
+ import { Button, Canvas } from "datocms-react-ui";
3
+
4
+ type PropTypes = {
5
+ ctx: RenderModalCtx;
6
+ };
7
+
8
+ const ErrorModal = ({ ctx }: PropTypes) => {
9
+ const handleCancelationButtonClick = () => {
10
+ ctx.resolve("done");
11
+ };
12
+
13
+ const newString = (ctx.parameters.errorPayload as string)
14
+ .replaceAll("\\n", "\n")
15
+ .replaceAll("\\", "");
16
+
17
+ return (
18
+ <Canvas ctx={ctx}>
19
+ <pre>{newString}</pre>
20
+ <Button
21
+ onClick={handleCancelationButtonClick}
22
+ fullWidth
23
+ buttonType="primary"
24
+ >
25
+ Done
26
+ </Button>
27
+ </Canvas>
28
+ );
29
+ };
30
+
31
+ export default ErrorModal;
@@ -0,0 +1,103 @@
1
+ import { RenderModalCtx } from "datocms-plugin-sdk";
2
+ import { Button, Canvas, Form, TextField } from "datocms-react-ui";
3
+ import { useState } from "react";
4
+ import attemptVercelInitialization from "../utils/attemptVercelInitialization";
5
+
6
+ type PropTypes = {
7
+ ctx: RenderModalCtx;
8
+ };
9
+
10
+ const InstallationModal = ({ ctx }: PropTypes) => {
11
+ const [vercelURL, setVercelURL] = useState("");
12
+ const [isInvalid, setIsInvalid] = useState(false);
13
+ const [isLoading, setIsLoading] = useState(false);
14
+
15
+ const handleDeployButtonClick = () => {
16
+ window.open(
17
+ `https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmarcelofinamorvieira%2Frecord-bin-lambda-function&env=DATOCMS_FULLACCESS_API_TOKEN&project-name=datocms-record-bin-lambda-function&repo-name=datocms-record-bin-lambda-function`
18
+ );
19
+ };
20
+
21
+ const handleCancelationButtonClick = () => {
22
+ ctx.updatePluginParameters({ installationState: "cancelled" });
23
+ ctx.resolve("cancelled");
24
+ };
25
+
26
+ const handleFinishButtonClick = async () => {
27
+ setIsLoading(true);
28
+ try {
29
+ await attemptVercelInitialization(vercelURL, ctx.environment);
30
+ ctx.updatePluginParameters({ installationState: "installed", vercelURL });
31
+ ctx.resolve("installed");
32
+ } catch {
33
+ setIsLoading(false);
34
+ setIsInvalid(true);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <Canvas ctx={ctx}>
40
+ <div style={{ textAlign: "center" }}>
41
+ <h1>Before continuing:</h1>
42
+ <p>
43
+ Record bin requires a lambda function to manage the creation and
44
+ restoration of the deleted records. By clicking the deploy button
45
+ bellow you can create your own instance of that lambda function. Once
46
+ you are finished with the setup, insert the deployment URL in the
47
+ field bellow.
48
+ </p>
49
+ <p>
50
+ If you'd like, you also can see, clone, and read the documentation on
51
+ that lambda function on{" "}
52
+ <a
53
+ href="https://github.com/marcelofinamorvieira/record-bin-lambda-function"
54
+ target="_blank"
55
+ rel="noreferrer"
56
+ >
57
+ this repository
58
+ </a>
59
+ </p>
60
+ <h2>You can create your instance of that lambda function here:</h2>
61
+ <Form>
62
+ <Button
63
+ onClick={handleDeployButtonClick}
64
+ fullWidth
65
+ buttonType="muted"
66
+ >
67
+ Deploy on Vercel
68
+ </Button>
69
+
70
+ <TextField
71
+ name="vercelURL"
72
+ id="email"
73
+ label="Once deployed, insert your deployed URL"
74
+ value={vercelURL}
75
+ placeholder="https://record-bin.vercel.app/"
76
+ error={isInvalid ? "Please insert a valid URL" : ""}
77
+ onChange={(newValue) => {
78
+ setIsInvalid(false);
79
+ setVercelURL(newValue);
80
+ }}
81
+ />
82
+ <Button
83
+ fullWidth
84
+ buttonType={isLoading ? "muted" : "primary"}
85
+ disabled={isLoading}
86
+ onClick={handleFinishButtonClick}
87
+ >
88
+ Finish installation
89
+ </Button>
90
+ <Button
91
+ fullWidth
92
+ buttonType="negative"
93
+ onClick={handleCancelationButtonClick}
94
+ >
95
+ Cancel installation
96
+ </Button>
97
+ </Form>
98
+ </div>
99
+ </Canvas>
100
+ );
101
+ };
102
+
103
+ export default InstallationModal;
@@ -0,0 +1,28 @@
1
+ import { RenderConfigScreenCtx } from "datocms-plugin-sdk";
2
+ import { Button, Canvas } from "datocms-react-ui";
3
+
4
+ type Props = {
5
+ ctx: RenderConfigScreenCtx;
6
+ };
7
+
8
+ export default function PreInstallConfig({ ctx }: Props) {
9
+ const handleRetryInstallation = async () => {
10
+ ctx.updatePluginParameters({ installationState: null });
11
+ await ctx.openModal({
12
+ id: "installationModal",
13
+ title: "Record Bin setup",
14
+ width: "m",
15
+ parameters: { foo: "bar" },
16
+ closeDisabled: true,
17
+ });
18
+ };
19
+
20
+ return (
21
+ <Canvas ctx={ctx}>
22
+ <h2>The plugin installation could not be completed.</h2>
23
+ <Button onClick={handleRetryInstallation} fullWidth buttonType="primary">
24
+ Retry installation
25
+ </Button>
26
+ </Canvas>
27
+ );
28
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,65 @@
1
+ import {
2
+ connect,
3
+ IntentCtx,
4
+ ItemType,
5
+ RenderItemFormOutletCtx,
6
+ RenderModalCtx,
7
+ } from "datocms-plugin-sdk";
8
+ import { render } from "./utils/render";
9
+ import ConfigScreen from "./entrypoints/ConfigScreen";
10
+ import "datocms-react-ui/styles.css";
11
+ import BinOutlet from "./entrypoints/BinOutlet";
12
+ import InstallationModal from "./entrypoints/InstallationModal";
13
+ import PreInstallConfig from "./entrypoints/PreInstallConfig";
14
+ import ErrorModal from "./entrypoints/ErrorModal";
15
+
16
+ connect({
17
+ async onBoot(ctx) {
18
+ if (
19
+ !ctx.plugin.attributes.parameters.installationState &&
20
+ !ctx.plugin.attributes.parameters.hasBeenPrompted
21
+ ) {
22
+ ctx.updatePluginParameters({ hasBeenPrompted: true });
23
+ await ctx.openModal({
24
+ id: "installationModal",
25
+ title: "Record Bin setup",
26
+ width: "m",
27
+ parameters: { foo: "bar" },
28
+ closeDisabled: true,
29
+ });
30
+ }
31
+ },
32
+ renderConfigScreen(ctx) {
33
+ if (ctx.plugin.attributes.parameters.installationState === "installed") {
34
+ return render(<ConfigScreen ctx={ctx} />);
35
+ }
36
+ return render(<PreInstallConfig ctx={ctx} />);
37
+ },
38
+ itemFormOutlets(model: ItemType, ctx: IntentCtx) {
39
+ if (model.attributes.api_key === "record_bin") {
40
+ return [
41
+ {
42
+ id: "recordBin",
43
+ initialHeight: 0,
44
+ },
45
+ ];
46
+ }
47
+ return [];
48
+ },
49
+ renderItemFormOutlet(outletId, ctx: RenderItemFormOutletCtx) {
50
+ if (
51
+ outletId === "recordBin" &&
52
+ ctx.plugin.attributes.parameters.installationState === "installed"
53
+ ) {
54
+ render(<BinOutlet ctx={ctx} />);
55
+ }
56
+ },
57
+ renderModal(modalId: string, ctx: RenderModalCtx) {
58
+ switch (modalId) {
59
+ case "installationModal":
60
+ return render(<InstallationModal ctx={ctx} />);
61
+ case "errorModal":
62
+ return render(<ErrorModal ctx={ctx} />);
63
+ }
64
+ },
65
+ });
@@ -0,0 +1 @@
1
+ /// <reference types="react-scripts" />
@@ -0,0 +1,16 @@
1
+ const attemptVercelInitialization = async (
2
+ vercelURL: string,
3
+ environment: string
4
+ ) => {
5
+ const requestBody = { event_type: "initialization", environment, vercelURL };
6
+
7
+ const parsedBody = JSON.stringify(requestBody);
8
+
9
+ await fetch(vercelURL, {
10
+ method: "POST",
11
+ body: parsedBody,
12
+ headers: { Accept: "*/*", "Content-Type": "application/json" },
13
+ });
14
+ };
15
+
16
+ export default attemptVercelInitialization;
@@ -0,0 +1,9 @@
1
+ import React, { StrictMode } from "react";
2
+ import ReactDOM from "react-dom";
3
+
4
+ export function render(component: React.ReactNode): void {
5
+ ReactDOM.render(
6
+ <StrictMode>{component}</StrictMode>,
7
+ document.getElementById("root")
8
+ );
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "strict": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "module": "esnext",
17
+ "moduleResolution": "node",
18
+ "resolveJsonModule": true,
19
+ "isolatedModules": true,
20
+ "noEmit": true,
21
+ "jsx": "react-jsx"
22
+ },
23
+ "include": [
24
+ "src"
25
+ ]
26
+ }