datocms-plugin-frontify-asset-source 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,3 @@
1
+ # Frontify asset source
2
+
3
+ Insert photos from your Frontify Account directly inside of your DatoCMS project
package/docs/cover.jpg ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "datocms-plugin-frontify-asset-source",
3
+ "version": "0.0.1",
4
+ "homepage": "https://github.com/gempi/datocms-plugin-frontify-asset-source#readme",
5
+ "keywords": [
6
+ "datocms-plugin",
7
+ "frontify"
8
+ ],
9
+ "description": "Insert photos from your Frontify Account directly inside of your DatoCMS project",
10
+ "dependencies": {
11
+ "@types/node": "^18.7.21",
12
+ "@types/react": "^18.0.21",
13
+ "@types/react-dom": "^18.0.6",
14
+ "datocms-plugin-sdk": "^0.6.4",
15
+ "datocms-react-ui": "^0.6.4",
16
+ "final-form": "^4.20.7",
17
+ "react": "^18.2.0",
18
+ "react-dom": "^18.2.0",
19
+ "react-final-form": "^6.5.9",
20
+ "react-scripts": "5.0.1",
21
+ "typescript": "^4.8.3",
22
+ "urql": "^3.0.3"
23
+ },
24
+ "scripts": {
25
+ "start": "cross-env BROWSER='none' PUBLIC_URL='/' react-scripts start",
26
+ "build": "cross-env PUBLIC_URL='.' react-scripts build",
27
+ "test": "react-scripts test",
28
+ "eject": "react-scripts eject",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "eslintConfig": {
32
+ "extends": [
33
+ "react-app"
34
+ ]
35
+ },
36
+ "browserslist": {
37
+ "production": [
38
+ ">0.2%",
39
+ "not dead",
40
+ "not op_mini all"
41
+ ],
42
+ "development": [
43
+ "last 1 chrome version",
44
+ "last 1 firefox version",
45
+ "last 1 safari version"
46
+ ]
47
+ },
48
+ "devDependencies": {
49
+ "cross-env": "^7.0.3"
50
+ },
51
+ "datoCmsPlugin": {
52
+ "title": "Asset source for Frontify",
53
+ "previewImage": "",
54
+ "coverImage": "docs/cover.jpg",
55
+ "entryPoint": "build/index.html",
56
+ "permissions": []
57
+ }
58
+ }
@@ -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,36 @@
1
+ import {
2
+ useState,
3
+ createContext,
4
+ Dispatch,
5
+ SetStateAction,
6
+ ReactNode,
7
+ } from "react";
8
+
9
+ interface AppContextInterface {
10
+ setLoading: Dispatch<SetStateAction<boolean>>;
11
+ loading: boolean;
12
+ setHasMore: Dispatch<SetStateAction<boolean>>;
13
+ hasMore: boolean;
14
+ }
15
+
16
+ const defaultValues: AppContextInterface = {
17
+ setLoading: () => {},
18
+ loading: true,
19
+ setHasMore: () => {},
20
+ hasMore: false,
21
+ };
22
+
23
+ export const AppContext = createContext<AppContextInterface>(defaultValues);
24
+
25
+ const AppProvider = ({ children }: { children: ReactNode }) => {
26
+ const [hasMore, setHasMore] = useState<boolean>(false);
27
+ const [loading, setLoading] = useState<boolean>(true);
28
+
29
+ return (
30
+ <AppContext.Provider value={{ hasMore, setHasMore, loading, setLoading }}>
31
+ {children}
32
+ </AppContext.Provider>
33
+ );
34
+ };
35
+
36
+ export default AppProvider;
@@ -0,0 +1,131 @@
1
+ import { RenderAssetSourceCtx } from "datocms-plugin-sdk";
2
+ import { Button, Canvas, Spinner, TextInput } from "datocms-react-ui";
3
+ import { useContext, useEffect, useState } from "react";
4
+ import { useQuery } from "urql";
5
+ import { AppContext } from "../../AppContext";
6
+ import { useRef } from "react";
7
+ import Page from "../Page/Page";
8
+
9
+ interface Brand {
10
+ id: string;
11
+ name: string;
12
+ }
13
+
14
+ interface BrandsData {
15
+ brands: Brand[];
16
+ }
17
+
18
+ const BrandsQuery = `
19
+ query {
20
+ brands {
21
+ id
22
+ name
23
+ }
24
+ }
25
+ `;
26
+
27
+ type AssetBrowserProps = {
28
+ ctx: RenderAssetSourceCtx;
29
+ };
30
+
31
+ function AssetBrowser({ ctx }: AssetBrowserProps) {
32
+ const searchRef = useRef<HTMLInputElement | null>(null);
33
+ const { hasMore, loading, setLoading } = useContext(AppContext);
34
+ const [searchTerm, setSearchTerm] = useState("");
35
+ const [pageVariables, setPageVariables] = useState([
36
+ {
37
+ page: 1,
38
+ hasNext: true,
39
+ },
40
+ ]);
41
+
42
+ const [{ data: brandsData, error }] = useQuery<BrandsData>({
43
+ query: BrandsQuery,
44
+ });
45
+
46
+ const brand = brandsData?.brands?.[0];
47
+
48
+ useEffect(() => {
49
+ if (error) {
50
+ ctx.alert(error.message);
51
+ setLoading(false);
52
+ }
53
+ }, [error, ctx, setLoading]);
54
+
55
+ return (
56
+ <Canvas ctx={ctx}>
57
+ <div style={{ paddingBottom: 8 }}>
58
+ <form
59
+ style={{
60
+ display: "flex",
61
+ gap: "8px",
62
+ }}
63
+ onSubmit={(e) => {
64
+ e.preventDefault();
65
+ setSearchTerm(searchRef.current?.value || "");
66
+ }}
67
+ >
68
+ <TextInput
69
+ inputRef={searchRef}
70
+ type="search"
71
+ placeholder="Search assets"
72
+ />
73
+ <Button type="submit" buttonType="primary">
74
+ Search
75
+ </Button>
76
+ </form>
77
+ </div>
78
+ <div style={{ position: "relative", minHeight: 200 }}>
79
+ {loading && (
80
+ <div
81
+ style={{
82
+ zIndex: 999,
83
+ height: "100%",
84
+ position: "absolute",
85
+ width: "100%",
86
+ background: "rgba(255,255,255,0.2)",
87
+ }}
88
+ >
89
+ <Spinner size={48} placement="centered" />
90
+ </div>
91
+ )}
92
+
93
+ <div
94
+ style={{
95
+ display: "grid",
96
+ gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
97
+ gap: "12px",
98
+ }}
99
+ >
100
+ {pageVariables.map((variables, i) => (
101
+ <Page
102
+ ctx={ctx}
103
+ key={i}
104
+ variables={variables}
105
+ brand={brand}
106
+ searchTerm={searchTerm}
107
+ />
108
+ ))}
109
+ </div>
110
+ </div>
111
+
112
+ {hasMore && (
113
+ <Button
114
+ style={{ marginTop: 12 }}
115
+ buttonType="muted"
116
+ fullWidth
117
+ onClick={() =>
118
+ setPageVariables([
119
+ ...pageVariables,
120
+ { page: pageVariables.length + 1, hasNext: false },
121
+ ])
122
+ }
123
+ >
124
+ Load more...
125
+ </Button>
126
+ )}
127
+ </Canvas>
128
+ );
129
+ }
130
+
131
+ export default AssetBrowser;
@@ -0,0 +1,27 @@
1
+ .assetInfo {
2
+ transition: 0.3s;
3
+ visibility: hidden;
4
+ opacity: 0;
5
+ transition: opacity 0.3s linear;
6
+ position: absolute;
7
+ top: 0;
8
+ bottom: 0;
9
+ left: 0;
10
+ right: 0;
11
+ padding: 12px;
12
+ color: white;
13
+ background: rgba(0, 0, 0, 0.5);
14
+ }
15
+
16
+ .assetDetail {
17
+ position: absolute;
18
+ bottom: 0;
19
+ left: 0;
20
+ right: 0;
21
+ padding: 12px;
22
+ }
23
+
24
+ .asset:hover .assetInfo {
25
+ visibility: visible;
26
+ opacity: 1;
27
+ }
@@ -0,0 +1,116 @@
1
+ import { RenderAssetSourceCtx } from "datocms-plugin-sdk";
2
+ import { useContext, useEffect } from "react";
3
+ import { useQuery } from "urql";
4
+ import styles from "./Page.module.css";
5
+ import { AppContext } from "../../AppContext";
6
+
7
+ const BrandLevelSearch = `
8
+ query BrandLevelSearch($id: ID!, $limit: Int, $page: Int, $term: String) {
9
+ brand(id: $id) {
10
+ search(limit: $limit, page: $page, query: {
11
+ term: $term
12
+ }) {
13
+ hasNextPage
14
+ page
15
+ total
16
+ items {
17
+ __typename
18
+ ... on Image {
19
+ id
20
+ title
21
+ description
22
+ filename
23
+ downloadUrl
24
+ previewUrl(width: 500, height: 500)
25
+ author
26
+ tags {
27
+ value
28
+ }
29
+ copyright {
30
+ status
31
+ notice
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ `;
39
+
40
+ type PageProps = {
41
+ ctx: RenderAssetSourceCtx;
42
+ brand: any;
43
+ variables: any;
44
+ searchTerm: any;
45
+ };
46
+
47
+ function Page({ ctx, brand, variables, searchTerm }: PageProps) {
48
+ const { setHasMore, setLoading } = useContext(AppContext);
49
+ const [{ data }] = useQuery({
50
+ query: BrandLevelSearch,
51
+ pause: !brand,
52
+ variables: {
53
+ id: brand?.id,
54
+ limit: 30,
55
+ page: variables.page,
56
+ term: searchTerm,
57
+ },
58
+ });
59
+
60
+ const handleSelect = (asset: any) => {
61
+ ctx.select({
62
+ resource: {
63
+ url: asset.downloadUrl,
64
+ filename: asset.filename,
65
+ },
66
+ author: asset.author,
67
+ notes: asset.description,
68
+ tags: asset.tags.map((tag: any) => tag.value),
69
+ copyright: asset.copyright.notice,
70
+ });
71
+ };
72
+
73
+ useEffect(() => {
74
+ setLoading(true);
75
+
76
+ if (data?.brand?.search) {
77
+ setHasMore(data.brand.search.hasNextPage);
78
+ setLoading(false);
79
+ }
80
+ }, [data, setHasMore, setLoading]);
81
+
82
+ return (
83
+ <>
84
+ {data?.brand?.search?.items?.map((asset: any) => {
85
+ return (
86
+ <div
87
+ key={asset.id}
88
+ onClick={() => handleSelect(asset)}
89
+ className={styles.asset}
90
+ style={{
91
+ position: "relative",
92
+ cursor: "pointer",
93
+ }}
94
+ >
95
+ <div className={styles.assetInfo}>
96
+ <div className={styles.assetDetail}>{asset.title}</div>
97
+ </div>
98
+ <img
99
+ style={{
100
+ aspectRatio: "1/1",
101
+ height: "100%",
102
+ width: "100%",
103
+ objectFit: "cover",
104
+ lineHeight: 0,
105
+ }}
106
+ src={asset.previewUrl}
107
+ alt=""
108
+ />
109
+ </div>
110
+ );
111
+ })}
112
+ </>
113
+ );
114
+ }
115
+
116
+ export default Page;
@@ -0,0 +1,80 @@
1
+ import { RenderConfigScreenCtx } from "datocms-plugin-sdk";
2
+ import { Canvas, Button, TextField, Form, FieldGroup } from "datocms-react-ui";
3
+ import { Form as FormHandler, Field } from "react-final-form";
4
+
5
+ type Props = {
6
+ ctx: RenderConfigScreenCtx;
7
+ };
8
+
9
+ export type ValidParameters = {
10
+ accessToken: string;
11
+ domain: string;
12
+ };
13
+
14
+ type Parameters = ValidParameters;
15
+
16
+ export default function ConfigScreen({ ctx }: Props) {
17
+ return (
18
+ <Canvas ctx={ctx}>
19
+ <FormHandler<Parameters>
20
+ initialValues={ctx.plugin.attributes.parameters}
21
+ validate={(values) => {
22
+ const errors: Record<string, string> = {};
23
+ if (!values.accessToken) {
24
+ errors.accessToken = "This field is required!";
25
+ }
26
+ if (!values.domain) {
27
+ errors.domain = "This field is required!";
28
+ }
29
+ return errors;
30
+ }}
31
+ onSubmit={async (values) => {
32
+ await ctx.updatePluginParameters(values);
33
+ ctx.notice("Settings updated successfully!");
34
+ }}
35
+ >
36
+ {({ handleSubmit, submitting, dirty }) => (
37
+ <Form onSubmit={handleSubmit}>
38
+ <FieldGroup>
39
+ <Field name="domain">
40
+ {({ input, meta: { error } }) => (
41
+ <TextField
42
+ hint="Your Frontify Domain, e.g. https://datocms.frontify.com"
43
+ id="domain"
44
+ label="Domain"
45
+ placeholder="Domain"
46
+ required
47
+ error={error}
48
+ {...input}
49
+ />
50
+ )}
51
+ </Field>
52
+ <Field name="accessToken">
53
+ {({ input, meta: { error } }) => (
54
+ <TextField
55
+ hint="Your Frontify Access Token (You can generate it here: https://{yourDomain}.frontify.com/api/oauth-access-token/show)"
56
+ id="accessToken"
57
+ label="Access Token"
58
+ placeholder="Access Token"
59
+ required
60
+ error={error}
61
+ {...input}
62
+ />
63
+ )}
64
+ </Field>
65
+ </FieldGroup>
66
+ <Button
67
+ type="submit"
68
+ fullWidth
69
+ buttonSize="l"
70
+ buttonType="primary"
71
+ disabled={submitting || !dirty}
72
+ >
73
+ Save settings
74
+ </Button>
75
+ </Form>
76
+ )}
77
+ </FormHandler>
78
+ </Canvas>
79
+ );
80
+ }
@@ -0,0 +1,3 @@
1
+ .inspector {
2
+ margin-top: var(--spacing-l);
3
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,55 @@
1
+ import { connect, RenderAssetSourceCtx } from "datocms-plugin-sdk";
2
+ import { render } from "./utils/render";
3
+ import ConfigScreen, { ValidParameters } from "./entrypoints/ConfigScreen";
4
+ import "datocms-react-ui/styles.css";
5
+ import AssetBrowser from "./components/AssetBrowser/AssetBrowser";
6
+ import { createClient, Provider } from "urql";
7
+ import AppProvider from "./AppContext";
8
+
9
+ connect({
10
+ renderConfigScreen(ctx) {
11
+ return render(<ConfigScreen ctx={ctx} />);
12
+ },
13
+ assetSources() {
14
+ return [
15
+ {
16
+ id: "frontify",
17
+ name: "Frontify",
18
+ icon: {
19
+ type: "svg",
20
+ viewBox: "0 0 53 50",
21
+ content:
22
+ '<path d="M51.0248 33.1169L45.264 29.8701C44.7702 29.5455 44.441 29.0584 44.441 28.4091V21.9156C44.441 20.6169 43.7826 19.3182 42.4658 18.6688L36.705 15.4221C36.3758 15.2597 36.0466 14.6104 36.0466 14.1234V7.62987C36.0466 6.33117 35.3882 5.03247 34.0714 4.38312L26.6646 0L19.0932 4.38312C17.941 5.03247 17.118 6.33117 17.118 7.62987V14.1234C17.118 14.7727 16.7888 15.2597 16.295 15.5844L10.5342 18.8312C9.38199 19.4805 8.55901 20.7792 8.55901 22.0779V28.5714C8.55901 29.2208 8.22981 29.7078 7.73602 30.0325L1.97516 33.2792C0.822982 33.9286 0 35.2273 0 36.526V45.1299L7.57143 49.513C8.7236 50.1623 10.205 50.1623 11.3571 49.513L17.118 46.2662C17.6118 45.9416 18.2702 45.9416 18.764 46.2662L24.5248 49.513C25.1832 49.8377 25.8416 50 26.5 50C27.1584 50 27.8168 49.8377 28.4752 49.513L34.236 46.2662C34.7298 45.9416 35.3882 45.9416 35.882 46.2662L41.6429 49.513C42.795 50.1623 44.2764 50.1623 45.4286 49.513L53 45.1299V36.526C53 35.0649 52.3416 33.9286 51.0248 33.1169ZM33.9068 7.62987V14.1234C33.9068 15.4221 34.5652 16.7208 35.882 17.3701L41.6429 20.6169C42.1367 20.9416 42.4658 21.4286 42.4658 22.0779V28.5714C42.4658 29.8701 43.1242 31.1688 44.441 31.8182L50.2019 35.0649C50.6957 35.3896 51.0248 35.8766 51.0248 36.526V42.6948L27.8168 29.5455V3.24675L33.0839 6.16883C33.5776 6.49351 33.9068 6.98052 33.9068 7.62987ZM3.12733 35.0649L8.8882 31.8182C10.0404 31.1688 10.8634 29.8701 10.8634 28.5714V22.0779C10.8634 21.4286 11.1925 20.9416 11.6863 20.6169L17.4472 17.3701C18.5994 16.7208 19.4224 15.4221 19.4224 14.1234V7.62987C19.4224 6.98052 19.7516 6.49351 20.2453 6.16883L25.677 3.08442V29.3831L2.30435 42.6948V36.526C2.30435 35.8766 2.63354 35.3896 3.12733 35.0649ZM44.2764 47.5649C43.7826 47.8896 43.1242 47.8896 42.6304 47.5649L36.8696 44.3182C35.7174 43.6688 34.236 43.6688 33.0839 44.3182L27.323 47.5649C26.8292 47.8896 26.1708 47.8896 25.677 47.5649L19.9162 44.3182C18.764 43.6688 17.2826 43.6688 16.1304 44.3182L10.3696 47.5649C9.87578 47.8896 9.21739 47.8896 8.7236 47.5649L3.45652 44.4805L26.6646 31.3312L49.8727 44.4805L44.2764 47.5649Z" fill="#2D3232"></path>',
23
+ },
24
+ modal: {
25
+ width: "xl",
26
+ },
27
+ },
28
+ ];
29
+ },
30
+ renderAssetSource(sourceId: string, ctx: RenderAssetSourceCtx) {
31
+ const parameters = ctx.plugin.attributes.parameters as ValidParameters;
32
+ const domain = parameters.domain;
33
+ const accessToken = parameters.accessToken;
34
+
35
+ const client = createClient({
36
+ url: `${domain}/graphql`,
37
+ fetchOptions: () => {
38
+ return {
39
+ headers: {
40
+ "X-Frontify-Beta": "enabled",
41
+ Authorization: accessToken ? `Bearer ${accessToken}` : "",
42
+ },
43
+ };
44
+ },
45
+ });
46
+
47
+ render(
48
+ <AppProvider>
49
+ <Provider value={client}>
50
+ <AssetBrowser ctx={ctx} />
51
+ </Provider>
52
+ </AppProvider>
53
+ );
54
+ },
55
+ });
@@ -0,0 +1 @@
1
+ /// <reference types="react-scripts" />
@@ -0,0 +1,9 @@
1
+ import React, { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ const container = document.getElementById("root");
5
+ const root = createRoot(container!);
6
+
7
+ export function render(component: React.ReactNode): void {
8
+ root.render(<StrictMode>{component}</StrictMode>);
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
+ }