datocms-plugin-netlify-forms 0.0.1

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,7 @@
1
+ # Netlify forms
2
+
3
+ Plugin that allows DatoCMS show and manage Netlify form submissions
4
+
5
+ ## Configuration
6
+
7
+ Please specify your Netlify access token on the plugin global settings
package/docs/cover.png ADDED
Binary file
package/docs/demo.gif ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "datocms-plugin-netlify-forms",
3
+ "version": "0.0.1",
4
+ "description": "Plugin that allows DatoCMS show and manage Netlify form submissions",
5
+ "homepage": "https://github.com/gempi/datocms-plugin-netlify-forms#readme",
6
+ "author": "Matthias Gemperli <hello@matthiasgemperli.ch>",
7
+ "license": "ISC",
8
+ "keywords": [
9
+ "datocms",
10
+ "datocms-plugin",
11
+ "netlify"
12
+ ],
13
+ "datoCmsPlugin": {
14
+ "title": "Netlify forms",
15
+ "coverImage": "docs/cover.png",
16
+ "previewImage": "docs/preview.mp4",
17
+ "entryPoint": "build/index.html",
18
+ "permissions": []
19
+ },
20
+ "dependencies": {
21
+ "@types/node": "^16.11.12",
22
+ "@types/react": "^17.0.37",
23
+ "@types/react-dom": "^17.0.11",
24
+ "datocms-plugin-sdk": "^0.3.20",
25
+ "datocms-react-ui": "^0.3.21",
26
+ "netlify": "^10.0.0",
27
+ "react": "^17.0.2",
28
+ "react-dom": "^17.0.2",
29
+ "react-final-form": "^6.5.7",
30
+ "react-scripts": "4.0.3",
31
+ "typescript": "^4.5.2"
32
+ },
33
+ "scripts": {
34
+ "start": "cross-env BROWSER='none' PUBLIC_URL='/' react-scripts start",
35
+ "build": "cross-env PUBLIC_URL='.' react-scripts build",
36
+ "test": "react-scripts test",
37
+ "eject": "react-scripts eject",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "eslintConfig": {
41
+ "extends": [
42
+ "react-app"
43
+ ]
44
+ },
45
+ "browserslist": {
46
+ "production": [
47
+ ">0.2%",
48
+ "not dead",
49
+ "not op_mini all"
50
+ ],
51
+ "development": [
52
+ "last 1 chrome version",
53
+ "last 1 firefox version",
54
+ "last 1 safari version"
55
+ ]
56
+ },
57
+ "devDependencies": {
58
+ "cross-env": "^7.0.3"
59
+ },
60
+ "main": "index.js",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/gempi/datocms-plugin-netlify-forms.git"
64
+ },
65
+ "bugs": {
66
+ "url": "https://github.com/gempi/datocms-plugin-netlify-forms/issues"
67
+ }
68
+ }
@@ -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,133 @@
1
+ import { RenderConfigScreenCtx } from "datocms-plugin-sdk";
2
+ import {
3
+ Button,
4
+ Canvas,
5
+ TextField,
6
+ Form,
7
+ FieldGroup,
8
+ SelectField,
9
+ } from "datocms-react-ui";
10
+ import { useEffect, useState } from "react";
11
+ import { Form as FormHandler, Field } from "react-final-form";
12
+
13
+ type PropTypes = {
14
+ ctx: RenderConfigScreenCtx;
15
+ };
16
+
17
+ export type ValidParameters = {
18
+ accessToken: string;
19
+ site: { label: string; value: string };
20
+ };
21
+
22
+ type Parameters = ValidParameters;
23
+
24
+ export default function ConfigScreen({ ctx }: PropTypes) {
25
+ const accessToken = ctx.plugin.attributes.parameters.accessToken;
26
+ const site = ctx.plugin.attributes.parameters.site;
27
+ const [sites, setSites] = useState([]);
28
+
29
+ useEffect(() => {
30
+ if (accessToken) {
31
+ setSites([]);
32
+ const getSites = async () => {
33
+ const response = await window.fetch(
34
+ "https://api.netlify.com/api/v1/sites/",
35
+ {
36
+ method: "GET",
37
+ headers: {
38
+ Authorization: `Bearer ${accessToken}`,
39
+ },
40
+ }
41
+ );
42
+
43
+ if (!response.ok) {
44
+ const message = `An error has occured: ${response.status}`;
45
+ ctx.alert(message);
46
+ }
47
+
48
+ const sites = await response.json();
49
+ setSites(sites);
50
+ };
51
+
52
+ getSites();
53
+ }
54
+ }, [ctx, accessToken]);
55
+
56
+ return (
57
+ <Canvas ctx={ctx}>
58
+ <FormHandler<Parameters>
59
+ initialValues={ctx.plugin.attributes.parameters}
60
+ validate={(values) => {
61
+ const errors: Record<string, string> = {};
62
+ if (!values.accessToken) {
63
+ errors.accessToken = "This field is required!";
64
+ }
65
+ return errors;
66
+ }}
67
+ onSubmit={async (values) => {
68
+ await ctx.updatePluginParameters(values);
69
+ ctx.notice("Settings updated successfully!");
70
+ }}
71
+ >
72
+ {({ handleSubmit, submitting, dirty }) => (
73
+ <Form onSubmit={handleSubmit}>
74
+ <FieldGroup>
75
+ <Field name="accessToken">
76
+ {({ input, meta: { error } }) => (
77
+ <TextField
78
+ id="accessToken"
79
+ label="Access token"
80
+ hint={
81
+ <>
82
+ You can generate a{" "}
83
+ <a
84
+ target="_blank"
85
+ rel="noreferrer"
86
+ href="https://app.netlify.com/user/applications#personal-access-tokens"
87
+ >
88
+ personal access token
89
+ </a>{" "}
90
+ in your Netlify user settings.
91
+ </>
92
+ }
93
+ required
94
+ error={error}
95
+ {...input}
96
+ />
97
+ )}
98
+ </Field>
99
+
100
+ {site || sites.length > 0 ? (
101
+ <Field name="site">
102
+ {({ input, meta: { error } }) => (
103
+ <SelectField
104
+ id="site"
105
+ label="Site"
106
+ error={error}
107
+ selectInputProps={{
108
+ options: sites.map((site: any) => ({
109
+ label: site.name,
110
+ value: site.site_id,
111
+ })),
112
+ }}
113
+ {...input}
114
+ />
115
+ )}
116
+ </Field>
117
+ ) : null}
118
+ </FieldGroup>
119
+ <Button
120
+ type="submit"
121
+ fullWidth
122
+ buttonSize="l"
123
+ buttonType="primary"
124
+ disabled={submitting || !dirty}
125
+ >
126
+ {!accessToken ? "Connect" : "Save settings"}
127
+ </Button>
128
+ </Form>
129
+ )}
130
+ </FormHandler>
131
+ </Canvas>
132
+ );
133
+ }
@@ -0,0 +1,3 @@
1
+ .inspector {
2
+ margin-top: var(--spacing-l);
3
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,41 @@
1
+ import {
2
+ connect,
3
+ IntentCtx,
4
+ RenderModalCtx,
5
+ RenderPageCtx,
6
+ } from "datocms-plugin-sdk";
7
+ import { render } from "./utils/render";
8
+ import ConfigScreen from "./entrypoints/ConfigScreen";
9
+ import "datocms-react-ui/styles.css";
10
+ import SubmissionsPage from "./pages/SubmissionsPage";
11
+ import ShowSubmissionModal from "./modals/ShowSubmissionModal";
12
+
13
+ connect({
14
+ renderConfigScreen(ctx) {
15
+ return render(<ConfigScreen ctx={ctx} />);
16
+ },
17
+ mainNavigationTabs(ctx: IntentCtx) {
18
+ return [
19
+ {
20
+ label: "Netlify forms",
21
+ icon: "file-signature",
22
+ pointsTo: {
23
+ pageId: "form-submissions",
24
+ },
25
+ placement: ["before", "settings"],
26
+ },
27
+ ];
28
+ },
29
+ renderPage(pageId, ctx: RenderPageCtx) {
30
+ switch (pageId) {
31
+ case "form-submissions":
32
+ return render(<SubmissionsPage ctx={ctx} />);
33
+ }
34
+ },
35
+ renderModal(modalId: string, ctx: RenderModalCtx) {
36
+ switch (modalId) {
37
+ case "showSubmission":
38
+ return render(<ShowSubmissionModal ctx={ctx} />);
39
+ }
40
+ },
41
+ });
@@ -0,0 +1,37 @@
1
+ import { RenderModalCtx } from "datocms-plugin-sdk";
2
+ import { Canvas } from "datocms-react-ui";
3
+
4
+ type PropTypes = {
5
+ ctx: RenderModalCtx;
6
+ };
7
+
8
+ export default function ShowSubmissionModal({ ctx }: PropTypes) {
9
+ const fields = ctx.parameters.ordered_human_fields as any;
10
+
11
+ return (
12
+ <Canvas ctx={ctx}>
13
+ {fields ? (
14
+ fields.map((item: any) => (
15
+ <div
16
+ key={item.title}
17
+ style={{
18
+ marginBottom: "var(--spacing-m)",
19
+ }}
20
+ >
21
+ <div
22
+ style={{
23
+ color: "var(--light-body-color)",
24
+ fontSize: "var(--font-size-s)",
25
+ }}
26
+ >
27
+ {item.title}
28
+ </div>
29
+ <div>{item.value}</div>
30
+ </div>
31
+ ))
32
+ ) : (
33
+ <span>No form fields found!</span>
34
+ )}
35
+ </Canvas>
36
+ );
37
+ }
@@ -0,0 +1,182 @@
1
+ import { RenderPageCtx } from "datocms-plugin-sdk";
2
+ import { Button, Canvas, Spinner } from "datocms-react-ui";
3
+ import { useEffect, useState } from "react";
4
+ import { ValidParameters } from "../entrypoints/ConfigScreen";
5
+
6
+ type PropTypes = {
7
+ ctx: RenderPageCtx;
8
+ };
9
+
10
+ export default function SubmissionsPage({ ctx }: PropTypes) {
11
+ const parameters = ctx.plugin.attributes.parameters as ValidParameters;
12
+ const site = parameters.site;
13
+ const accessToken = parameters.accessToken;
14
+
15
+ const [submissions, setSubmissions] = useState([]);
16
+ const [loading, setLoading] = useState(false);
17
+
18
+ useEffect(() => {
19
+ if (accessToken && site) {
20
+ const getSubmissions = async () => {
21
+ setLoading(true);
22
+ const response = await window.fetch(
23
+ `https://api.netlify.com/api/v1/sites/${site.value}/submissions`,
24
+ {
25
+ method: "GET",
26
+ headers: {
27
+ Authorization: `Bearer ${accessToken}`,
28
+ },
29
+ }
30
+ );
31
+
32
+ if (!response.ok) {
33
+ const message = `An error has occured: ${response.status}`;
34
+ ctx.alert(message);
35
+ }
36
+
37
+ const forms = await response.json();
38
+ setSubmissions(forms);
39
+ setLoading(false);
40
+ };
41
+
42
+ getSubmissions();
43
+ }
44
+ }, [site, accessToken, ctx]);
45
+
46
+ const handleShowSubmissionModal = async (submission: any) => {
47
+ const result: any = await ctx.openModal({
48
+ id: "showSubmission",
49
+ title: `Submission (${submission.id})`,
50
+ width: "l",
51
+ parameters: submission,
52
+ });
53
+
54
+ ctx.notice(result);
55
+ };
56
+
57
+ const handleOpenDeleteSubmissonModal = async (submission: any) => {
58
+ const result: any = await ctx.openConfirm({
59
+ title: "Delete record?",
60
+ content:
61
+ "Are you sure you want to delete this record? This operation is not reversible!",
62
+ choices: [
63
+ {
64
+ label: "Yes, delete this record",
65
+ value: "negative",
66
+ intent: "negative",
67
+ },
68
+ ],
69
+ cancel: {
70
+ label: "Cancel",
71
+ value: false,
72
+ },
73
+ });
74
+
75
+ if (result) {
76
+ const response = await window.fetch(
77
+ `https://api.netlify.com/api/v1/submissions/${submission.id}`,
78
+ {
79
+ method: "DELETE",
80
+ headers: {
81
+ Authorization: `Bearer ${accessToken}`,
82
+ },
83
+ }
84
+ );
85
+
86
+ if (!response.ok) {
87
+ ctx.alert(`An error has occured: ${response.status}`);
88
+ } else {
89
+ setSubmissions(
90
+ submissions.filter((item: any) => item.id !== submission.id)
91
+ );
92
+ ctx.notice("Record successfully removed");
93
+ }
94
+ }
95
+ };
96
+
97
+ return (
98
+ <Canvas ctx={ctx}>
99
+ <div
100
+ style={{
101
+ paddingTop: "var(--spacing-l)",
102
+ paddingLeft: "var(--spacing-xxl)",
103
+ paddingRight: "var(--spacing-xxl)",
104
+ }}
105
+ >
106
+ <h1 style={{ fontWeight: 500 }}>Form submissions</h1>
107
+ <div>
108
+ <div
109
+ style={{
110
+ display: "flex",
111
+ justifyContent: "space-between",
112
+ borderBottom: 1,
113
+ borderBottomColor: "var(--darker-border-color)",
114
+ borderBottomStyle: "solid",
115
+ paddingTop: "var(--spacing-m)",
116
+ paddingBottom: "var(--spacing-m)",
117
+ fontWeight: 500,
118
+ }}
119
+ >
120
+ <span style={{ width: "25%" }}>Name</span>
121
+ <span style={{ width: "25%" }}>Form</span>
122
+ <span style={{ width: "25%" }}>Date</span>
123
+ <span style={{ width: "25%", flexGrow: 0 }}></span>
124
+ <span></span>
125
+ </div>
126
+
127
+ {loading ? (
128
+ <div style={{ height: "200px", position: "relative" }}>
129
+ <Spinner size={48} placement="centered" />
130
+ </div>
131
+ ) : (
132
+ submissions.map((item: any) => (
133
+ <div
134
+ key={item.id}
135
+ style={{
136
+ display: "flex",
137
+ justifyContent: "space-between",
138
+ borderBottom: 1,
139
+ borderBottomColor: "var(--darker-border-color)",
140
+ borderBottomStyle: "solid",
141
+ paddingTop: "var(--spacing-m)",
142
+ paddingBottom: "var(--spacing-m)",
143
+ alignItems: "center",
144
+ }}
145
+ >
146
+ <span style={{ width: "25%", flexGrow: 0 }}>{item.name}</span>
147
+ <span style={{ width: "25%", flexGrow: 0 }}>
148
+ {item.form_name}
149
+ </span>
150
+ <span style={{ width: "25%", flexGrow: 0 }}>
151
+ {new Intl.DateTimeFormat("en-US").format(
152
+ new Date(item.created_at)
153
+ )}
154
+ </span>
155
+ <span style={{ width: "25%", flexGrow: 0, textAlign: "right" }}>
156
+ <Button
157
+ buttonSize="xs"
158
+ type="button"
159
+ onClick={() => handleShowSubmissionModal(item)}
160
+ style={{ marginRight: "var(--spacing-m)" }}
161
+ >
162
+ Show
163
+ </Button>
164
+ <Button
165
+ buttonSize="xs"
166
+ type="reset"
167
+ buttonType="negative"
168
+ onClick={() => handleOpenDeleteSubmissonModal(item)}
169
+ >
170
+ Delete
171
+ </Button>
172
+ </span>
173
+ </div>
174
+ ))
175
+ )}
176
+
177
+ {}
178
+ </div>
179
+ </div>
180
+ </Canvas>
181
+ );
182
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="react-scripts" />
@@ -0,0 +1,6 @@
1
+ import React, { StrictMode } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+
4
+ export function render(component: React.ReactNode): void {
5
+ ReactDOM.render(<StrictMode>{component}</StrictMode>, document.getElementById('root'));
6
+ }
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
+ }