├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── json-screenshot.png ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── assets │ └── json-screenshot.png ├── components │ ├── ConfigScreen.spec.tsx │ ├── ConfigScreen.tsx │ ├── Dialog.spec.tsx │ ├── Dialog.tsx │ ├── EntryEditor.spec.tsx │ ├── EntryEditor.tsx │ ├── Field.spec.tsx │ ├── Field.tsx │ ├── LocalhostWarning.tsx │ ├── Page.spec.tsx │ ├── Page.tsx │ ├── Sidebar.spec.tsx │ ├── Sidebar.tsx │ └── styles.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts └── setupTests.ts ├── test └── mocks │ └── mockSdk.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mariano Alvarez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Contentful JSON Viewer 3 | [![Install to Contentful](https://www.ctfstatic.com/button/install-small.svg)](https://app.contentful.com/deeplink?link=apps&id=69mKkEFmMO9ue4lwWfto2C) 4 | 5 | 6 | This app adds a new tab on the entry aditor allowing you to visualize the JSON payload for the current entry. This object is the result of calling the `space.getEntry(entryId)` App framework method on the entry. 7 | 8 | It leverages an open source library called [react-json-view](https://github.com/mac-s-g/react-json-view) to format the JSON object in a user-friendly manner. 9 | 10 | ![image](https://user-images.githubusercontent.com/840764/125956326-49ee7285-85aa-4d74-a223-aa09c2fc45aa.png) 11 | 12 | ## Installation 13 | 14 | When creating the App definition within the Contentful Org, make sure to select the following app locations: 15 | ![image](https://user-images.githubusercontent.com/840764/125956578-c0775b00-3b67-4d53-bf73-dd7b17f5dd71.png) 16 | 17 | 18 | ## Available Scripts 19 | 20 | In the project directory, you can run: 21 | 22 | #### `npm start` 23 | 24 | Creates or updates your app definition in Contentful, and runs the app in development mode. 25 | Open your app to view it in the browser. 26 | 27 | The page will reload if you make edits. 28 | You will also see any lint errors in the console. 29 | 30 | #### `npm run build` 31 | 32 | Builds the app for production to the `build` folder. 33 | It correctly bundles React in production mode and optimizes the build for the best performance. 34 | 35 | The build is minified and the filenames include the hashes. 36 | Your app is ready to be deployed! 37 | 38 | -------------------------------------------------------------------------------- /json-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marianoa/contentful-json-viewer/ed94193601fac6514ed016c7dfccc306e7156c02/json-screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-viewer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@contentful/app-sdk": "^4.3.0", 7 | "@contentful/field-editor-single-line": "^0.14.1", 8 | "@contentful/field-editor-test-utils": "^0.11.1", 9 | "@contentful/forma-36-fcss": "^0.3.1", 10 | "@contentful/forma-36-react-components": "^3.89.0", 11 | "@contentful/forma-36-tokens": "^0.10.1", 12 | "@testing-library/jest-dom": "^5.12.0", 13 | "@testing-library/react": "^11.2.6", 14 | "@testing-library/user-event": "^13.1.8", 15 | "@emotion/react": "^11.4.0", 16 | "@emotion/core": "^11.0.0", 17 | "@emotion/styled": "^11.3.0", 18 | "@types/jest": "^26.0.23", 19 | "@types/node": "^14.14.43", 20 | "@types/react": "^17.0.4", 21 | "@types/react-dom": "^17.0.3", 22 | "cross-env": "^7.0.3", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "react-scripts": "4.0.3", 26 | "typescript": "^4.2.4", 27 | "react-json-view": "latest", 28 | "contentful-management": "^7.5.1" 29 | }, 30 | "scripts": { 31 | "start": "cross-env BROWSER=none react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "homepage": "." 52 | } 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/json-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marianoa/contentful-json-viewer/ed94193601fac6514ed016c7dfccc306e7156c02/src/assets/json-screenshot.png -------------------------------------------------------------------------------- /src/components/ConfigScreen.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ConfigScreen from './ConfigScreen'; 3 | import { render } from '@testing-library/react'; 4 | import { mockSdk } from '../../test/mocks/mockSdk'; 5 | 6 | describe('Config Screen component', () => { 7 | it('Component text exists', async () => { 8 | const { getByText } = render(); 9 | 10 | // simulate the user clicking the install button 11 | await mockSdk.app.onConfigure.mock.calls[0][0](); 12 | 13 | expect( 14 | getByText('Welcome to your contentful app. This is your config page.') 15 | ).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/ConfigScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { AppExtensionSDK } from '@contentful/app-sdk'; 3 | import { Typography, Heading, Paragraph} from '@contentful/forma-36-react-components'; 4 | import appScreenShot from '../assets/json-screenshot.png'; 5 | import styles from './styles'; 6 | 7 | export interface AppInstallationParameters {} 8 | 9 | interface ConfigProps { 10 | sdk: AppExtensionSDK; 11 | } 12 | 13 | interface ConfigState { 14 | data: { active: boolean; contentType: any }[]; 15 | } 16 | 17 | export default class Config extends Component { 18 | constructor(props: ConfigProps) { 19 | super(props); 20 | 21 | this.state = { 22 | data: [], 23 | }; 24 | 25 | props.sdk.app.onConfigure(() => this.onConfigure()); 26 | } 27 | 28 | onConfigure = async () => { 29 | // This method will be called when a user clicks on "Install" 30 | // or "Save" on the configuration screen. 31 | // For more details see https://www.contentful.com/developers/docs/extensibility/ui-extensions/sdk-reference/#register-an-app-configuration-hook 32 | 33 | // Generate a new target state with the App assigned to the selected 34 | // content types 35 | const targetState = await this.createTargetState(); 36 | 37 | return { 38 | targetState, 39 | }; 40 | }; 41 | 42 | createTargetState = async () => { 43 | 44 | const currentState = await this.props.sdk.app.getCurrentState(); 45 | 46 | const EditorInterface = this.state.data.reduce( 47 | (editorInterface: any, { active, contentType }) => { 48 | if (active) { 49 | console.log(contentType.sys.id) 50 | editorInterface[contentType.sys.id] = { 51 | //placing the JSON Viewer tab as the second tab 52 | //ideally we would place as the last tab (is it possible?) 53 | editors: { position : 1}, 54 | }; 55 | } 56 | console.log(editorInterface); 57 | return editorInterface; 58 | }, 59 | currentState?.EditorInterface || {} 60 | ); 61 | 62 | return { ...currentState, EditorInterface }; 63 | }; 64 | 65 | async componentDidMount() { 66 | const data = await this.getContentTypesUsingEditor(); 67 | this.setState({ data }, () => { 68 | this.props.sdk.app.setReady(); 69 | }); 70 | } 71 | 72 | async getContentTypesUsingEditor() { 73 | const { space, ids } = this.props.sdk; 74 | 75 | const editorInterfaces = await space.getEditorInterfaces(); 76 | 77 | const appIncludedInEditors = (appId: any, editorInterface: any) => { 78 | if (editorInterface.editor) { 79 | return appId === editorInterface.editor.widgetId; 80 | } else if (editorInterface.editors) { 81 | return editorInterface.editors.some(({ widgetId }: any) => widgetId === appId); 82 | } else { 83 | return false; 84 | } 85 | }; 86 | 87 | return Promise.all( 88 | editorInterfaces.items.map(async (ei: any) => { 89 | const contentTypeId = ei.sys?.contentType?.sys?.id; 90 | const contentType = await space.getContentType(contentTypeId); 91 | 92 | return { contentType, active: true }; 93 | }) 94 | ); 95 | } 96 | 97 | render() { 98 | return ( 99 | <> 100 |
101 |
102 |
103 | 104 | About the JSON Viewer app 105 | 106 | 107 | This app adds a new tab on the entry aditor allowing you to visualize the JSON payload for the current entry. This object is the result of calling the Preview API on the entry. 108 | 109 | 110 | 111 | Installing this app will add the entry editor tab to all Content Types in your space. 112 | 113 | 114 |
115 |
116 | JSON Viewer 117 |
118 |
119 | 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/components/Dialog.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from './Dialog'; 3 | import { render } from '@testing-library/react'; 4 | import { mockSdk } from '../../test/mocks/mockSdk'; 5 | 6 | describe('Dialog component', () => { 7 | it('Component text exists', () => { 8 | const { getByText } = render(); 9 | 10 | expect(getByText('Hello Dialog Component')).toBeInTheDocument(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/forma-36-react-components'; 3 | import { DialogExtensionSDK } from '@contentful/app-sdk'; 4 | 5 | interface DialogProps { 6 | sdk: DialogExtensionSDK; 7 | } 8 | 9 | const Dialog = (props: DialogProps) => { 10 | return Hello Dialog Component; 11 | }; 12 | 13 | export default Dialog; 14 | -------------------------------------------------------------------------------- /src/components/EntryEditor.spec.tsx: -------------------------------------------------------------------------------- 1 | import EntryEditor from './EntryEditor'; 2 | import { render } from '@testing-library/react'; 3 | import { mockSdk } from '../../test/mocks/mockSdk'; 4 | 5 | describe('Entry component', () => { 6 | it('Component text exists', () => { 7 | const { getByText } = render(); 8 | 9 | expect(getByText('Hello Entry Editor Component')).toBeInTheDocument(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/EntryEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import { EditorExtensionSDK } from '@contentful/app-sdk'; 3 | import { PlainClientAPI } from 'contentful-management'; 4 | // import the react-json-view component 5 | import ReactJson from 'react-json-view'; 6 | import { Button, Dropdown, DropdownList, DropdownListItem, Flex } from '@contentful/forma-36-react-components'; 7 | 8 | interface EditorProps { 9 | sdk: EditorExtensionSDK; 10 | cma: PlainClientAPI; 11 | } 12 | 13 | const Entry = (props: EditorProps) => { 14 | const [json, setJson] = useState(JSON.stringify({})); 15 | const [sys, setSys] = useState({}); 16 | const [isOpen, setOpen] = useState(false); 17 | const [depthState, SetDepthState] = useState(0); 18 | 19 | const { sdk, cma } = props; 20 | 21 | //get the entryId 22 | const entryId = sdk.entry.getSys().id; 23 | 24 | //refresh the entry when there's a change, to get latest changes 25 | useEffect(() => { 26 | sdk.entry.onSysChanged((sys) => { 27 | setSys(sys); 28 | }) 29 | }) 30 | 31 | // Handle Nested Include Depth 32 | const setDepthHandler = (event: React.MouseEvent, depth: React.SetStateAction) => { 33 | SetDepthState(depth); 34 | setOpen(!isOpen); // close the select list 35 | } 36 | 37 | // @ts-ignore 38 | useEffect(async () => { 39 | if (depthState >= 1) { 40 | const data = await cma.entry.references({ 41 | entryId: entryId, 42 | include: depthState 43 | }); 44 | setJson(JSON.stringify(data, null, 2)) 45 | } 46 | else { 47 | const data = await sdk.space.getEntry(entryId); 48 | setJson(JSON.stringify(data, null, 2)) 49 | } 50 | }, [sys, depthState]); 51 | 52 | 53 | return (<> 54 | 55 | 56 | setOpen(false)} 59 | toggleElement={ 60 | 63 | } 64 | > 65 | 66 | setDepthHandler(event, 0)}>No Nested Items 67 | setDepthHandler(event, 1)}>1 68 | setDepthHandler(event, 2)}>2 69 | setDepthHandler(event, 3)}>3 70 | setDepthHandler(event, 4)}>4 71 | setDepthHandler(event, 5)}>5 72 | setDepthHandler(event, 6)}>6 73 | setDepthHandler(event, 7)}>7 74 | setDepthHandler(event, 8)}>8 75 | setDepthHandler(event, 9)}>9 76 | setDepthHandler(event, 10)}>10 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | }; 89 | 90 | export default Entry; 91 | -------------------------------------------------------------------------------- /src/components/Field.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Field from './Field'; 3 | import { render } from '@testing-library/react'; 4 | 5 | describe('Field component', () => { 6 | it('Component text exists', () => { 7 | const { getByText } = render(); 8 | 9 | expect(getByText('Hello Entry Field Component')).toBeInTheDocument(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/Field.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/forma-36-react-components'; 3 | import { FieldExtensionSDK } from '@contentful/app-sdk'; 4 | 5 | interface FieldProps { 6 | sdk: FieldExtensionSDK; 7 | } 8 | 9 | const Field = (props: FieldProps) => { 10 | // If you only want to extend Contentful's default editing experience 11 | // reuse Contentful's editor components 12 | // -> https://www.contentful.com/developers/docs/extensibility/field-editors/ 13 | return Hello Entry Field Component; 14 | }; 15 | 16 | export default Field; 17 | -------------------------------------------------------------------------------- /src/components/LocalhostWarning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph, TextLink, Note } from '@contentful/forma-36-react-components'; 3 | 4 | const LocalhostWarning = () => { 5 | return ( 6 |
12 | 13 | 14 | Contentful Apps need to run inside the Contentful web app to function properly. Install 15 | the app into a space and render your app into one of the{' '} 16 | 17 | available locations 18 | 19 | . 20 | 21 |
22 | 23 | 24 | Follow{' '} 25 | 26 | our guide 27 | {' '} 28 | to get started or{' '} 29 | open Contentful{' '} 30 | to manage your app. 31 | 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default LocalhostWarning; 38 | -------------------------------------------------------------------------------- /src/components/Page.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import { render } from '@testing-library/react'; 4 | import { mockSdk } from '../../test/mocks/mockSdk'; 5 | 6 | describe('Page component', () => { 7 | it('Component text exists', () => { 8 | const { getByText } = render(); 9 | 10 | expect(getByText('Hello Page Component')).toBeInTheDocument(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/forma-36-react-components'; 3 | import { PageExtensionSDK } from '@contentful/app-sdk'; 4 | 5 | interface PageProps { 6 | sdk: PageExtensionSDK; 7 | } 8 | 9 | const Page = (props: PageProps) => { 10 | return Hello Page Component; 11 | }; 12 | 13 | export default Page; 14 | -------------------------------------------------------------------------------- /src/components/Sidebar.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Sidebar from './Sidebar'; 3 | import { render } from '@testing-library/react'; 4 | import { mockSdk } from '../../test/mocks/mockSdk'; 5 | 6 | describe('Sidebar component', () => { 7 | it('Component text exists', () => { 8 | const { getByText } = render(); 9 | 10 | expect(getByText('Hello Sidebar Component')).toBeInTheDocument(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/forma-36-react-components'; 3 | import { SidebarExtensionSDK } from '@contentful/app-sdk'; 4 | 5 | interface SidebarProps { 6 | sdk: SidebarExtensionSDK; 7 | } 8 | 9 | const Sidebar = (props: SidebarProps) => { 10 | return Hello Sidebar Component; 11 | }; 12 | 13 | export default Sidebar; 14 | -------------------------------------------------------------------------------- /src/components/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import tokens from '@contentful/forma-36-tokens'; 3 | 4 | export default { 5 | body: css({ 6 | height: 'auto', 7 | minHeight: '65vh', 8 | margin: '0 auto', 9 | marginTop: tokens.spacingXl, 10 | padding: `${tokens.spacingXl} ${tokens.spacing2Xl}`, 11 | maxWidth: tokens.contentWidthText, 12 | backgroundColor: tokens.colorWhite, 13 | zIndex: 2, 14 | boxShadow: '0px 0px 20px rgba(0, 0, 0, 0.1)', 15 | borderRadius: '2px' 16 | }), 17 | lightText: css({ 18 | color: tokens.colorTextLight 19 | }), 20 | signInButton: css({ 21 | textAlign: 'center', 22 | dispay: 'none', 23 | margin: 'auto', 24 | cursor: 'pointer' 25 | }), 26 | splitter: css({ 27 | marginTop: tokens.spacingL, 28 | marginBottom: tokens.spacingL, 29 | border: 0, 30 | height: '1px', 31 | backgroundColor: tokens.colorElementMid 32 | }), 33 | background: css({ 34 | display: 'block', 35 | position: 'absolute', 36 | zIndex: -1, 37 | top: '0', 38 | width: '100%', 39 | height: '300px', 40 | backgroundColor: '#f8ab00' 41 | }), 42 | contentTypeGrid: css({ 43 | display: 'grid', 44 | gridTemplateColumns: 'repeat(3, 1fr) max-content', 45 | gridGap: tokens.spacingXs 46 | }), 47 | contentTypeGridInputs: css({ 48 | marginBottom: tokens.spacingM 49 | }), 50 | header: css({ 51 | display: 'grid', 52 | gridTemplateColumns: '1.5fr 1.25fr', 53 | gridColumnGap: tokens.spacing2Xs, 54 | width: '100%', 55 | alignItems: 'self-end', 56 | marginBottom: tokens.spacingXs 57 | }), 58 | invisible: css({ 59 | visibility: 'hidden' 60 | }), 61 | hidden: css({ 62 | display: 'none' 63 | }), 64 | slug: css({ 65 | color: tokens.colorTextLight, 66 | fontSize: tokens.fontSizeS, 67 | marginBottom: tokens.spacingM 68 | }), 69 | spaced: css({ 70 | marginBottom: tokens.spacingL 71 | }), 72 | timeline: css({ 73 | position: 'relative' 74 | }), 75 | timelineChart: css({ 76 | minHeight: '200px' 77 | }), 78 | timelineSkeleton: css({ 79 | position: 'absolute', 80 | top: 0, 81 | left: 0 82 | }), 83 | pageViews: css({ 84 | opacity: 1, 85 | transition: `opacity ${tokens.transitionDurationShort}` 86 | }), 87 | pageViewsLoading: css({ 88 | opacity: 0, 89 | transition: `opacity ${tokens.transitionDurationShort}` 90 | }), 91 | logo: css({ 92 | display: 'flex', 93 | justifyContent: 'center', 94 | margin: `${tokens.spacing2Xl} 0 ${tokens.spacing4Xl}` 95 | }) 96 | }; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div { 4 | margin: 0; 5 | padding: 0; 6 | border: 0; 7 | font-size: 100%; 8 | font: inherit; 9 | vertical-align: baseline; 10 | } 11 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createClient } from 'contentful-management'; 4 | 5 | import { 6 | AppExtensionSDK, 7 | FieldExtensionSDK, 8 | SidebarExtensionSDK, 9 | DialogExtensionSDK, 10 | EditorExtensionSDK, 11 | PageExtensionSDK, 12 | init, 13 | locations, 14 | } from '@contentful/app-sdk'; 15 | import type { KnownSDK } from '@contentful/app-sdk'; 16 | import '@contentful/forma-36-react-components/dist/styles.css'; 17 | import '@contentful/forma-36-fcss/dist/styles.css'; 18 | import '@contentful/forma-36-tokens/dist/css/index.css'; 19 | import './index.css'; 20 | 21 | import Config from './components/ConfigScreen'; 22 | import EntryEditor from './components/EntryEditor'; 23 | import Page from './components/Page'; 24 | import Sidebar from './components/Sidebar'; 25 | import Field from './components/Field'; 26 | import Dialog from './components/Dialog'; 27 | 28 | import LocalhostWarning from './components/LocalhostWarning'; 29 | 30 | if (process.env.NODE_ENV === 'development' && window.self === window.top) { 31 | // You can remove this if block before deploying your app 32 | const root = document.getElementById('root'); 33 | render(, root); 34 | } else { 35 | init((sdk: KnownSDK) => { 36 | const root = document.getElementById('root'); 37 | 38 | // Creating a CMA client allows you to use the contentful-management library 39 | // within your app. See the contentful-management documentation at https://contentful.github.io/contentful-management.js/contentful-management/latest/ 40 | // to learn what is possible. 41 | const cma = createClient( 42 | { apiAdapter: sdk.cmaAdapter }, 43 | { 44 | type: 'plain', 45 | defaults: { 46 | environmentId: sdk.ids.environment, 47 | spaceId: sdk.ids.space, 48 | }, 49 | } 50 | ); 51 | 52 | // All possible locations for your app 53 | // Feel free to remove unused locations 54 | // Dont forget to delete the file too :) 55 | const ComponentLocationSettings = [ 56 | { 57 | location: locations.LOCATION_APP_CONFIG, 58 | component: , 59 | }, 60 | { 61 | location: locations.LOCATION_ENTRY_FIELD, 62 | component: , 63 | }, 64 | { 65 | location: locations.LOCATION_ENTRY_EDITOR, 66 | component: , 67 | }, 68 | { 69 | location: locations.LOCATION_DIALOG, 70 | component: , 71 | }, 72 | { 73 | location: locations.LOCATION_ENTRY_SIDEBAR, 74 | component: , 75 | }, 76 | { 77 | location: locations.LOCATION_PAGE, 78 | component: , 79 | }, 80 | ]; 81 | 82 | // Select a component depending on a location in which the app is rendered. 83 | ComponentLocationSettings.forEach((componentLocationSetting) => { 84 | if (sdk.location.is(componentLocationSetting.location)) { 85 | render(componentLocationSetting.component, root); 86 | } 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /test/mocks/mockSdk.ts: -------------------------------------------------------------------------------- 1 | const mockSdk: any = { 2 | app: { 3 | onConfigure: jest.fn(), 4 | getParameters: jest.fn().mockReturnValueOnce({}), 5 | setReady: jest.fn(), 6 | getCurrentState: jest.fn(), 7 | }, 8 | }; 9 | 10 | export { mockSdk }; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 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 | } 27 | --------------------------------------------------------------------------------