├── src
├── react-app-env.d.ts
├── setupTests.ts
├── locations
│ ├── Home.spec.tsx
│ ├── Page.spec.tsx
│ ├── Dialog.spec.tsx
│ ├── Field.spec.tsx
│ ├── Sidebar.spec.tsx
│ ├── Home.tsx
│ ├── Page.tsx
│ ├── Dialog.tsx
│ ├── EntryEditor.spec.tsx
│ ├── EntryEditor.tsx
│ ├── Sidebar.tsx
│ ├── ConfigScreen.spec.tsx
│ ├── Field.tsx
│ └── ConfigScreen.tsx
├── index.tsx
├── App.tsx
└── components
│ └── LocalhostWarning.tsx
├── test
└── mocks
│ ├── mockCma.ts
│ ├── index.ts
│ └── mockSdk.ts
├── tsconfig.json
├── .gitignore
├── public
└── index.html
├── package.json
└── README.md
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/test/mocks/mockCma.ts:
--------------------------------------------------------------------------------
1 | const mockCma: any = {};
2 |
3 | export { mockCma };
4 |
--------------------------------------------------------------------------------
/test/mocks/index.ts:
--------------------------------------------------------------------------------
1 | export { mockCma } from './mockCma';
2 | export { mockSdk } from './mockSdk';
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/create-react-app/tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | },
11 | "include": [
12 | "src"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/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 | ids: {
9 | app: 'test-app'
10 | }
11 | };
12 |
13 | export { mockSdk };
14 |
--------------------------------------------------------------------------------
/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 | import { configure } from '@testing-library/react';
7 |
8 | configure({
9 | testIdAttribute: 'data-test-id',
10 | });
11 |
--------------------------------------------------------------------------------
/.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 |
25 | .env
--------------------------------------------------------------------------------
/src/locations/Home.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Home from './Home';
3 | import { render } from '@testing-library/react';
4 | import { mockCma, mockSdk } from '../../test/mocks';
5 |
6 | jest.mock('@contentful/react-apps-toolkit', () => ({
7 | useSDK: () => mockSdk,
8 | useCMA: () => mockCma,
9 | }));
10 |
11 | describe('Home component', () => {
12 | it('Component text exists', () => {
13 | const { getByText } = render();
14 |
15 | expect(getByText('Hello Home Component (AppId: test-app)')).toBeInTheDocument();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/locations/Page.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Page from './Page';
3 | import { render } from '@testing-library/react';
4 | import { mockCma, mockSdk } from '../../test/mocks';
5 |
6 | jest.mock('@contentful/react-apps-toolkit', () => ({
7 | useSDK: () => mockSdk,
8 | useCMA: () => mockCma,
9 | }));
10 |
11 | describe('Page component', () => {
12 | it('Component text exists', () => {
13 | const { getByText } = render();
14 |
15 | expect(getByText('Hello Page Component (AppId: test-app)')).toBeInTheDocument();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/locations/Dialog.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Dialog from './Dialog';
3 | import { render } from '@testing-library/react';
4 | import { mockCma, mockSdk } from '../../test/mocks';
5 |
6 | jest.mock('@contentful/react-apps-toolkit', () => ({
7 | useSDK: () => mockSdk,
8 | useCMA: () => mockCma,
9 | }));
10 |
11 | describe('Dialog component', () => {
12 | it('Component text exists', () => {
13 | const { getByText } = render();
14 |
15 | expect(getByText('Hello Dialog Component (AppId: test-app)')).toBeInTheDocument();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/locations/Field.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Field from './Field';
3 | import { render } from '@testing-library/react';
4 | import { mockCma, mockSdk } from '../../test/mocks';
5 |
6 | jest.mock('@contentful/react-apps-toolkit', () => ({
7 | useSDK: () => mockSdk,
8 | useCMA: () => mockCma,
9 | }));
10 |
11 | describe('Field component', () => {
12 | it('Component text exists', () => {
13 | const { getByText } = render();
14 |
15 | expect(getByText('Hello Entry Field Component (AppId: test-app)')).toBeInTheDocument();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/locations/Sidebar.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Sidebar from './Sidebar';
3 | import { render } from '@testing-library/react';
4 | import { mockCma, mockSdk } from '../../test/mocks';
5 |
6 | jest.mock('@contentful/react-apps-toolkit', () => ({
7 | useSDK: () => mockSdk,
8 | useCMA: () => mockCma,
9 | }));
10 |
11 | describe('Sidebar component', () => {
12 | it('Component text exists', () => {
13 | const { getByText } = render();
14 |
15 | expect(getByText('Hello Sidebar Component (AppId: test-app)')).toBeInTheDocument();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/locations/Home.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paragraph } from '@contentful/f36-components';
3 | import { HomeExtensionSDK } from '@contentful/app-sdk';
4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit';
5 |
6 | const Home = () => {
7 | const sdk = useSDK();
8 | /*
9 | To use the cma, inject it as follows.
10 | If it is not needed, you can remove the next line.
11 | */
12 | // const cma = useCMA();
13 |
14 | return Hello Home Component (AppId: {sdk.ids.app});
15 | };
16 |
17 | export default Home;
18 |
--------------------------------------------------------------------------------
/src/locations/Page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paragraph } from '@contentful/f36-components';
3 | import { PageExtensionSDK } from '@contentful/app-sdk';
4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit';
5 |
6 | const Page = () => {
7 | const sdk = useSDK();
8 | /*
9 | To use the cma, inject it as follows.
10 | If it is not needed, you can remove the next line.
11 | */
12 | // const cma = useCMA();
13 |
14 | return Hello Page Component (AppId: {sdk.ids.app});
15 | };
16 |
17 | export default Page;
18 |
--------------------------------------------------------------------------------
/src/locations/Dialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paragraph } from '@contentful/f36-components';
3 | import { DialogExtensionSDK } from '@contentful/app-sdk';
4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit';
5 |
6 | const Dialog = () => {
7 | const sdk = useSDK();
8 | /*
9 | To use the cma, inject it as follows.
10 | If it is not needed, you can remove the next line.
11 | */
12 | // const cma = useCMA();
13 |
14 | return Hello Dialog Component (AppId: {sdk.ids.app});
15 | };
16 |
17 | export default Dialog;
18 |
--------------------------------------------------------------------------------
/src/locations/EntryEditor.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import EntryEditor from './EntryEditor';
3 | import { render } from '@testing-library/react';
4 | import { mockCma, mockSdk } from '../../test/mocks';
5 |
6 | jest.mock('@contentful/react-apps-toolkit', () => ({
7 | useSDK: () => mockSdk,
8 | useCMA: () => mockCma,
9 | }));
10 |
11 | describe('Entry component', () => {
12 | it('Component text exists', () => {
13 | const { getByText } = render();
14 |
15 | expect(getByText('Hello Entry Editor Component (AppId: test-app)')).toBeInTheDocument();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/locations/EntryEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paragraph } from '@contentful/f36-components';
3 | import { EditorExtensionSDK } from '@contentful/app-sdk';
4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit';
5 |
6 | const Entry = () => {
7 | const sdk = useSDK();
8 | /*
9 | To use the cma, inject it as follows.
10 | If it is not needed, you can remove the next line.
11 | */
12 | // const cma = useCMA();
13 |
14 | return Hello Entry Editor Component (AppId: {sdk.ids.app});
15 | };
16 |
17 | export default Entry;
18 |
--------------------------------------------------------------------------------
/src/locations/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paragraph } from '@contentful/f36-components';
3 | import { SidebarExtensionSDK } from '@contentful/app-sdk';
4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit';
5 |
6 | const Sidebar = () => {
7 | const sdk = useSDK();
8 | /*
9 | To use the cma, inject it as follows.
10 | If it is not needed, you can remove the next line.
11 | */
12 | // const cma = useCMA();
13 |
14 | return Hello Sidebar Component (AppId: {sdk.ids.app});
15 | };
16 |
17 | export default Sidebar;
18 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import { GlobalStyles } from '@contentful/f36-components';
5 | import { SDKProvider } from '@contentful/react-apps-toolkit';
6 |
7 | import LocalhostWarning from './components/LocalhostWarning';
8 | import App from './App';
9 |
10 | const root = document.getElementById('root');
11 |
12 | if (process.env.NODE_ENV === 'development' && window.self === window.top) {
13 | // You can remove this if block before deploying your app
14 | render(, root);
15 | } else {
16 | render(
17 |
18 |
19 |
20 | ,
21 | root
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/locations/ConfigScreen.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ConfigScreen from './ConfigScreen';
3 | import { render } from '@testing-library/react';
4 | import { mockCma, mockSdk } from '../../test/mocks';
5 |
6 | jest.mock('@contentful/react-apps-toolkit', () => ({
7 | useSDK: () => mockSdk,
8 | useCMA: () => mockCma,
9 | }));
10 |
11 | describe('Config Screen component', () => {
12 | it('Component text exists', async () => {
13 | const { getByText } = render();
14 |
15 | // simulate the user clicking the install button
16 | await mockSdk.app.onConfigure.mock.calls[0][0]();
17 |
18 | expect(
19 | getByText('Welcome to your contentful app. This is your config page.')
20 | ).toBeInTheDocument();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/locations/Field.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paragraph } from '@contentful/f36-components';
3 | import { FieldExtensionSDK } from '@contentful/app-sdk';
4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit';
5 |
6 | const Field = () => {
7 | const sdk = useSDK();
8 | /*
9 | To use the cma, inject it as follows.
10 | If it is not needed, you can remove the next line.
11 | */
12 | // const cma = useCMA();
13 | // If you only want to extend Contentful's default editing experience
14 | // reuse Contentful's editor components
15 | // -> https://www.contentful.com/developers/docs/extensibility/field-editors/
16 | return Hello Entry Field Component (AppId: {sdk.ids.app});
17 | };
18 |
19 | export default Field;
20 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { locations } from '@contentful/app-sdk';
3 | import ConfigScreen from './locations/ConfigScreen';
4 | import Field from './locations/Field';
5 | import EntryEditor from './locations/EntryEditor';
6 | import Dialog from './locations/Dialog';
7 | import Sidebar from './locations/Sidebar';
8 | import Page from './locations/Page';
9 | import Home from './locations/Home';
10 | import { useSDK } from '@contentful/react-apps-toolkit';
11 |
12 | const ComponentLocationSettings = {
13 | [locations.LOCATION_APP_CONFIG]: ConfigScreen,
14 | [locations.LOCATION_ENTRY_FIELD]: Field,
15 | [locations.LOCATION_ENTRY_EDITOR]: EntryEditor,
16 | [locations.LOCATION_DIALOG]: Dialog,
17 | [locations.LOCATION_ENTRY_SIDEBAR]: Sidebar,
18 | [locations.LOCATION_PAGE]: Page,
19 | [locations.LOCATION_HOME]: Home,
20 | };
21 |
22 | const App = () => {
23 | const sdk = useSDK();
24 |
25 | const Component = useMemo(() => {
26 | for (const [location, component] of Object.entries(ComponentLocationSettings)) {
27 | if (sdk.location.is(location)) {
28 | return component;
29 | }
30 | }
31 | }, [sdk.location]);
32 |
33 | return Component ? : null;
34 | };
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/src/components/LocalhostWarning.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paragraph, TextLink, Note, Flex } from '@contentful/f36-components';
3 |
4 | const LocalhostWarning = () => {
5 | return (
6 |
7 |
8 |
9 | Contentful Apps need to run inside the Contentful web app to function properly. Install
10 | the app into a space and render your app into one of the{' '}
11 |
12 | available locations
13 |
14 | .
15 |
16 |
17 |
18 |
19 | Follow{' '}
20 |
21 | our guide
22 | {' '}
23 | to get started or{' '}
24 | open Contentful{' '}
25 | to manage your app.
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default LocalhostWarning;
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground-testing",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@contentful/app-sdk": "4.13.0",
7 | "@contentful/f36-components": "4.21.8",
8 | "@contentful/f36-tokens": "4.0.1",
9 | "@contentful/react-apps-toolkit": "1.2.9",
10 | "contentful-management": "10.21.1",
11 | "emotion": "10.0.27",
12 | "react": "17.0.2",
13 | "react-dom": "17.0.2",
14 | "react-scripts": "5.0.1"
15 | },
16 | "scripts": {
17 | "start": "cross-env BROWSER=none react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject",
21 | "create-app-definition": "contentful-app-scripts create-app-definition",
22 | "upload": "contentful-app-scripts upload --bundle-dir ./build",
23 | "upload-ci": "contentful-app-scripts upload --ci --bundle-dir ./build --organization-id $CONTENTFUL_ORG_ID --definition-id $CONTENTFUL_APP_DEF_ID --token $CONTENTFUL_ACCESS_TOKEN",
24 | "open-playground": "source .env && open 'https://contentful-apps-playground.netlify.app/?token='$CONTENTFUL_ACCESS_TOKEN'&orgId='$CONTENTFUL_ORG_ID'&appId='$CONTENTFUL_APP_DEF_ID"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | },
41 | "devDependencies": {
42 | "@contentful/app-scripts": "1.2.1",
43 | "@testing-library/jest-dom": "5.16.5",
44 | "@testing-library/react": "12.1.5",
45 | "@tsconfig/create-react-app": "1.0.3",
46 | "@types/jest": "29.2.3",
47 | "@types/node": "18.11.9",
48 | "@types/react": "18.0.9",
49 | "@types/react-dom": "18.0.3",
50 | "cross-env": "7.0.3",
51 | "typescript": "4.9.3"
52 | },
53 | "homepage": "."
54 | }
55 |
--------------------------------------------------------------------------------
/src/locations/ConfigScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState, useEffect } from 'react';
2 | import { AppExtensionSDK } from '@contentful/app-sdk';
3 | import { Heading, Form, Paragraph, Flex } from '@contentful/f36-components';
4 | import { css } from 'emotion';
5 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit';
6 |
7 | export interface AppInstallationParameters {}
8 |
9 | const ConfigScreen = () => {
10 | const [parameters, setParameters] = useState({});
11 | const sdk = useSDK();
12 | /*
13 | To use the cma, inject it as follows.
14 | If it is not needed, you can remove the next line.
15 | */
16 | // const cma = useCMA();
17 |
18 | const onConfigure = useCallback(async () => {
19 | // This method will be called when a user clicks on "Install"
20 | // or "Save" in the configuration screen.
21 | // for more details see https://www.contentful.com/developers/docs/extensibility/ui-extensions/sdk-reference/#register-an-app-configuration-hook
22 |
23 | // Get current the state of EditorInterface and other entities
24 | // related to this app installation
25 | const currentState = await sdk.app.getCurrentState();
26 |
27 | return {
28 | // Parameters to be persisted as the app configuration.
29 | parameters,
30 | // In case you don't want to submit any update to app
31 | // locations, you can just pass the currentState as is
32 | targetState: currentState,
33 | };
34 | }, [parameters, sdk]);
35 |
36 | useEffect(() => {
37 | // `onConfigure` allows to configure a callback to be
38 | // invoked when a user attempts to install the app or update
39 | // its configuration.
40 | sdk.app.onConfigure(() => onConfigure());
41 | }, [sdk, onConfigure]);
42 |
43 | useEffect(() => {
44 | (async () => {
45 | // Get current parameters of the app.
46 | // If the app is not installed yet, `parameters` will be `null`.
47 | const currentParameters: AppInstallationParameters | null = await sdk.app.getParameters();
48 |
49 | if (currentParameters) {
50 | setParameters(currentParameters);
51 | }
52 |
53 | // Once preparation has finished, call `setReady` to hide
54 | // the loading screen and present the app to a user.
55 | sdk.app.setReady();
56 | })();
57 | }, [sdk]);
58 |
59 | return (
60 |
61 |
65 |
66 | );
67 | };
68 |
69 | export default ConfigScreen;
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create Contentful App](https://github.com/contentful/create-contentful-app).
2 |
3 | ## How to use
4 |
5 | Execute create-contentful-app with npm, npx or yarn to bootstrap the example:
6 |
7 | ```bash
8 | # npx
9 | npx create-contentful-app --typescript
10 |
11 | # npm
12 | npm init contentful-app -- --typescript
13 |
14 | # Yarn
15 | yarn create contentful-app --typescript
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 | #### `npm run upload`
39 |
40 | Uploads the build folder to contentful and creates a bundle that is automatically activated.
41 | The command guides you through the deployment process and asks for all required arguments.
42 | Read [here](https://www.contentful.com/developers/docs/extensibility/app-framework/create-contentful-app/#deploy-with-contentful) for more information about the deployment process.
43 |
44 | #### `npm run upload-ci`
45 |
46 | Similar to `npm run upload` it will upload your app to contentful and activate it. The only difference is
47 | that with this command all required arguments are read from the environment variables, for example when you add
48 | the upload command to your CI pipeline.
49 |
50 | For this command to work, the following environment variables must be set:
51 |
52 | - `CONTENTFUL_ORG_ID` - The ID of your organization
53 | - `CONTENTFUL_APP_DEF_ID` - The ID of the app to which to add the bundle
54 | - `CONTENTFUL_ACCESS_TOKEN` - A personal [access token](https://www.contentful.com/developers/docs/references/content-management-api/#/reference/personal-access-tokens)
55 |
56 | ## Libraries to use
57 |
58 | To make your app look and feel like Contentful use the following libraries:
59 |
60 | - [Forma 36](https://f36.contentful.com/) – Contentful's design system
61 | - [Contentful Field Editors](https://www.contentful.com/developers/docs/extensibility/field-editors/) – Contentful's field editor React components
62 |
63 | ## Using the `contentful-management` SDK
64 |
65 | In the default create contentful app output, a contentful management client is
66 | passed into each location. This can be used to interact with Contentful's
67 | management API. For example
68 |
69 | ```js
70 | // Use the client
71 | cma.locale.getMany({}).then((locales) => console.log(locales))
72 |
73 | ```
74 |
75 | Visit the [`contentful-management` documentation](https://www.contentful.com/developers/docs/extensibility/app-framework/sdk/#using-the-contentful-management-library)
76 | to find out more.
77 |
78 | ## Learn More
79 |
80 | [Read more](https://www.contentful.com/developers/docs/extensibility/app-framework/create-contentful-app/) and check out the video on how to use the CLI.
81 |
82 | Create Contentful App uses [Create React App](https://create-react-app.dev/). You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started) and how to further customize your app.
83 |
--------------------------------------------------------------------------------