From f4d79f0b5e35cb17f5b5505a5924aad7952c65f3 Mon Sep 17 00:00:00 2001 From: Mandi Haase Date: Wed, 14 Sep 2022 15:36:27 -0400 Subject: [PATCH 01/16] initial commit --- content-management/README.md | 86 + content-management/app/.env | 4 + content-management/app/.eslintrc.json | 3 + content-management/app/.prettierignore | 5 + .../components/Container/Container.module.css | 5 + .../app/components/Container/index.tsx | 33 + .../CreateNoteForm/CreateNoteForm.module.css | 66 + .../app/components/CreateNoteForm/index.tsx | 203 ++ .../app/components/Modal/Modal.module.css | 86 + .../app/components/Modal/index.tsx | 201 ++ .../app/components/Navbar/Navbar.module.css | 117 ++ .../app/components/Navbar/index.tsx | 75 + .../components/NoteCard/NoteCard.module.css | 133 ++ .../app/components/NoteCard/index.tsx | 223 ++ .../app/components/Notes/Notes.module.css | 19 + .../app/components/Notes/index.tsx | 54 + .../PaginationBar/PaginationBar.module.css | 53 + .../app/components/PaginationBar/index.tsx | 90 + .../app/components/Portal/index.tsx | 40 + content-management/app/lib/apollo-client.ts | 12 + content-management/app/lib/utils.ts | 45 + content-management/app/next-env.d.ts | 5 + content-management/app/next.config.mjs | 7 + content-management/app/openBrowser.js | 13 + content-management/app/package.json | 33 + content-management/app/pages/_app.tsx | 8 + .../app/pages/api/notes/delete/index.ts | 21 + .../app/pages/api/notes/index.ts | 24 + .../app/pages/api/notes/new/index.ts | 30 + .../app/pages/api/notes/update/index.ts | 26 + content-management/app/pages/index.tsx | 236 +++ content-management/app/queries/index.ts | 91 + content-management/app/styles/globals.css | 74 + content-management/app/tsconfig.json | 20 + content-management/app/yarn.lock | 1866 +++++++++++++++++ content-management/brightspot/brightspot.json | 3 + content-management/brightspot/package.json | 17 + .../example/content_management/Note.ts | 23 + .../content_management/NotesEndpoint.ts | 37 + .../content_management/NotesEndpointClient.ts | 52 + content-management/brightspot/tsconfig.json | 16 + content-management/brightspot/yarn.lock | 250 +++ 42 files changed, 4405 insertions(+) create mode 100644 content-management/README.md create mode 100644 content-management/app/.env create mode 100755 content-management/app/.eslintrc.json create mode 100644 content-management/app/.prettierignore create mode 100644 content-management/app/components/Container/Container.module.css create mode 100644 content-management/app/components/Container/index.tsx create mode 100644 content-management/app/components/CreateNoteForm/CreateNoteForm.module.css create mode 100644 content-management/app/components/CreateNoteForm/index.tsx create mode 100644 content-management/app/components/Modal/Modal.module.css create mode 100644 content-management/app/components/Modal/index.tsx create mode 100644 content-management/app/components/Navbar/Navbar.module.css create mode 100644 content-management/app/components/Navbar/index.tsx create mode 100644 content-management/app/components/NoteCard/NoteCard.module.css create mode 100644 content-management/app/components/NoteCard/index.tsx create mode 100644 content-management/app/components/Notes/Notes.module.css create mode 100644 content-management/app/components/Notes/index.tsx create mode 100644 content-management/app/components/PaginationBar/PaginationBar.module.css create mode 100644 content-management/app/components/PaginationBar/index.tsx create mode 100644 content-management/app/components/Portal/index.tsx create mode 100644 content-management/app/lib/apollo-client.ts create mode 100644 content-management/app/lib/utils.ts create mode 100755 content-management/app/next-env.d.ts create mode 100755 content-management/app/next.config.mjs create mode 100644 content-management/app/openBrowser.js create mode 100644 content-management/app/package.json create mode 100755 content-management/app/pages/_app.tsx create mode 100644 content-management/app/pages/api/notes/delete/index.ts create mode 100644 content-management/app/pages/api/notes/index.ts create mode 100644 content-management/app/pages/api/notes/new/index.ts create mode 100644 content-management/app/pages/api/notes/update/index.ts create mode 100755 content-management/app/pages/index.tsx create mode 100644 content-management/app/queries/index.ts create mode 100755 content-management/app/styles/globals.css create mode 100755 content-management/app/tsconfig.json create mode 100644 content-management/app/yarn.lock create mode 100644 content-management/brightspot/brightspot.json create mode 100644 content-management/brightspot/package.json create mode 100644 content-management/brightspot/src/brightspot/example/content_management/Note.ts create mode 100644 content-management/brightspot/src/brightspot/example/content_management/NotesEndpoint.ts create mode 100644 content-management/brightspot/src/brightspot/example/content_management/NotesEndpointClient.ts create mode 100644 content-management/brightspot/tsconfig.json create mode 100644 content-management/brightspot/yarn.lock diff --git a/content-management/README.md b/content-management/README.md new file mode 100644 index 00000000..484927fd --- /dev/null +++ b/content-management/README.md @@ -0,0 +1,86 @@ +# Content Management + +In this tutorial, you will learn how to use a Content Management API Endpoint provided by Brightspot to power a frontend application. + +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Running the example application + +Refer to the [README](/README.md) at the root of the `react-examples` repository for details on running example applications in depth. If you have run an example application before, make sure you have the docker instance for the example applications running, then follow the quick-start steps: + +brightspot (`http://localhost/cms`): + +``` +cd brightspot +yarn +npx brightspot types download +npx brightspot types upload src + +``` + +frontend: + +``` +cd app +yarn +yarn dev +``` + +The frontend application will open automatically in the browser. + +## Using the example application + +You can start with the frontend application. Click on the form at the top of the dashboard to create a new Note. Make sure to enter the Username that you are using in Brightspot (the name you logged in with). After creating your note, you should see it appear in the dashboard. Confirm in Brightspot that the note is also listed on the dashboard. You should see your Username avatar next to the note. + +## How everything works + +Brightspot makes it possible to create content that you can then query for using the GraphQL API. One GraphQL API Brightspot offers is the Content Management API (CMA). Refer to the Brightspot documentation for more information. + +Navigate to `brightspot/src/examples/content_management`. This directory contains the JS Classes files that are uploaded to Brightspot. + +#### JS Classes Files: + +- `Note.ts`: the model (class) that contains the business logic (fields, etc) +- `NotesEndpoint.ts`: the class that create a custom CMA Endpoint with the following configurations: + - `getEntryFields`: specify the class(es) to determine the schema for the custom endpoint + - `updateCorsConfiguration`: permit cross-origin resource sharing (CORS) to enable requests from localhost + - `getPaths`: specify the path(s) to send HTTP requests to (this path is added to `app/.env`) + - `Singleton`: create a 'one and only' instance of the custom endpoint +- `NotesEndpointClient.ts`: the class that creates an API Client. The API Client has a client ID and API Key that are stored in the `app/.env` file to access the CMA Endpoint + +#### Queries: + +All queries are located in `app/queries/index.ts`: + +- `GET_NOTES`: either query for notes ("\* matches ?") given certain arguments or pass in other filter options (ex: "not \_id matches ?") with arguments. +- `CREATE_AND_UPDATE_NOTE`: either create a new note (if no id is provided) or update an existing one +- `DELETE_NOTE`: delete a note by id - add `permanently: true` to delete without archiving. + +#### API Routes + +All GraphQL queries are made using the dynamic api routing provided by Next.js. Navigate to `pages/api/notes` to see how the GraphQL query requests are made. + +> **_Note_** This application is for demonstration purposes only. In a production-level application, you would want to implement authentication. Although this application requires a username for creating new content and updating content in the frontend, further authentication would be needed. + +## Try it yourself + +The following are suggestions for further exploration: + +1. Change one field in a note in the frontend (example, change the title of a note). In Brightspot, change the description of that same note. Refresh both the frontend and Brightspot. What do those fields display? + +2. Set the `limit` in the `GET_NOTES` query to a smaller number (like 2). Create more than 2 notes and see how server-side pagination is used. + +3. Remove `permanently: true` from the `DELETE_NOTE` query. Now try deleting that note, then navigate to Brightspot dashboard. Select `Note` for Content Type, then Status: Archived. Do you see the note you deleted? + +4. Test out the detailed error messages that the Brightspot GraphQL API provides. Remove one letter from a query in `app/queries/index.ts`. Notice the error message that displays. + +## Troubleshooting + +1. I am getting getting the following error on initial page load in the browser for the frontend: "Response not successful: Received status code 401".... + + - Verify that the Notes Endpoint API Client Client ID and API Key (found in Brightspot: Menu Button -> `Admin` -> `APIs` -> `Clients`) are the same values that are in the `app/.env` file. + +2. I get an error when trying to create or update a Note: "Variable 'toolUser' has an invalid value".... + - Verify the the Username you entered in the New Note Form or note Card is a Username in Brightspot. + +Having other issues running the example application? Refer to the [Common Issues](/README.md) section in the respository README for assistance. diff --git a/content-management/app/.env b/content-management/app/.env new file mode 100644 index 00000000..138a4881 --- /dev/null +++ b/content-management/app/.env @@ -0,0 +1,4 @@ +NEXT_PUBLIC_HOST=http://localhost:3000 +GRAPHQL_URL=http://localhost/graphql/management/notes +GRAPHQL_CLIENT_ID=d47b5d36c97d3eaf8858c471c124b2e8 +GRAPHQL_CLIENT_SECRET=90a52c3d-96d9-391a-878d-fe4ef4fe431e \ No newline at end of file diff --git a/content-management/app/.eslintrc.json b/content-management/app/.eslintrc.json new file mode 100755 index 00000000..4d765f28 --- /dev/null +++ b/content-management/app/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "prettier"] +} diff --git a/content-management/app/.prettierignore b/content-management/app/.prettierignore new file mode 100644 index 00000000..7e792df1 --- /dev/null +++ b/content-management/app/.prettierignore @@ -0,0 +1,5 @@ +node_modules +generated +styles +.next +public \ No newline at end of file diff --git a/content-management/app/components/Container/Container.module.css b/content-management/app/components/Container/Container.module.css new file mode 100644 index 00000000..ec679f2f --- /dev/null +++ b/content-management/app/components/Container/Container.module.css @@ -0,0 +1,5 @@ +.section { + max-width: 1240px; + padding: 7rem 1rem 0 1rem; + margin: 0 auto; +} diff --git a/content-management/app/components/Container/index.tsx b/content-management/app/components/Container/index.tsx new file mode 100644 index 00000000..abdf99be --- /dev/null +++ b/content-management/app/components/Container/index.tsx @@ -0,0 +1,33 @@ +import Notes from '../Notes' +import CreateNoteForm from '../CreateNoteForm' +import styles from './Container.module.css' +import { Data } from '../../pages/index' +import { Dispatch, SetStateAction } from 'react' + +type Props = { + items: Data[] + getItems: ( + pageNumber: number, + predicate: boolean, + queryItem?: string, + newItem?: Data + ) => void + pageNumber: number + setPageNumber: Dispatch> +} + +const Container = ({ items, getItems, pageNumber, setPageNumber }: Props) => { + return ( +
+ + +
+ ) +} + +export default Container diff --git a/content-management/app/components/CreateNoteForm/CreateNoteForm.module.css b/content-management/app/components/CreateNoteForm/CreateNoteForm.module.css new file mode 100644 index 00000000..6070f529 --- /dev/null +++ b/content-management/app/components/CreateNoteForm/CreateNoteForm.module.css @@ -0,0 +1,66 @@ +.form { + max-width: 540px; + background: #fff; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: var(--primaryTextColor); + border-radius: 10px; + box-shadow: var(--lightShadow); + margin: 0 auto; +} + +.collapsed { + display: none; +} + +.notCollapsed { + display: unset; +} + +.input { + width: 100%; + font-size: 1rem; + border: 0; + border-radius: 3px; + min-height: 1.7rem; + padding: 5px; + margin-bottom: 7px; + font-family: 'Noto Sans', sans-serif; +} + +.button { + font-size: 1rem; + padding: 7px; + margin-right: 10px; + color: var(--moduleBgColor); + font-weight: 700; + border: 0; + background-color: transparent; + border-radius: var(--mainBorderRadius); +} + +.button:hover { + cursor: pointer; + background-color: var(--grayscaleFour); +} + +.bottom { + width: 100%; + min-height: 30px; + text-align: end; + margin-bottom: 4px; +} + +.error { + font-size: 0.9rem; + color: var(--primaryRed); + font-style: italic; +} + +.wrapper { + margin-bottom: 5px; + width: 100%; + padding: 5px 10px; +} diff --git a/content-management/app/components/CreateNoteForm/index.tsx b/content-management/app/components/CreateNoteForm/index.tsx new file mode 100644 index 00000000..61f3506f --- /dev/null +++ b/content-management/app/components/CreateNoteForm/index.tsx @@ -0,0 +1,203 @@ +import { useState, useRef, useEffect } from 'react' +import styles from './CreateNoteForm.module.css' +import { Data } from '../../pages' +import { assertIsNode, runErrorWithTimeout } from '../../lib/utils' + +type Props = { + getItems: ( + pageNumber: number, + predicate: boolean, + queryItem?: string, + newItem?: Data + ) => void + pageNumber: number +} + +type SubmittedData = { + title?: string + description?: string + toolUser?: string +} + +type QueryResponse = { + error?: string + brightspot_example_content_management_NoteSave?: { + _id: string + title: string + description: string + _globals: { + com_psddev_cms_db_Content_ObjectModification: { + publishDate: number + publishUser: { + username: string + } + updateDate: number + updateUser: { + username: string + } + } + } + } +} + +const CreateNoteForm = ({ getItems, pageNumber }: Props) => { + const titleRef = useRef(null) + const descriptionRef = useRef(null) + const usernameRef = useRef(null) + const formRef = useRef(null) + const [error, setError] = useState(null) + const [expanded, setExpanded] = useState(false) + const url = `${process.env.NEXT_PUBLIC_HOST}/api/notes/new` + const params = (data: SubmittedData) => { + return { + body: JSON.stringify(data), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + } + } + + useEffect(() => { + runErrorWithTimeout(error, setError, 3000) + }, [error]) + + useEffect(() => { + const closeOnMouseClickOutside = ({ target }: MouseEvent) => { + assertIsNode(target) + if (expanded && formRef && !formRef?.current?.contains(target)) { + setExpanded(false) + } + } + + document.body.addEventListener('mousedown', closeOnMouseClickOutside) + return () => { + document.body.removeEventListener('mousedown', closeOnMouseClickOutside) + } + }, [expanded]) + + const processResponse = (res: QueryResponse) => { + if (res.brightspot_example_content_management_NoteSave) { + const newItem = res.brightspot_example_content_management_NoteSave + getItems(pageNumber, false, '', newItem) + if (titleRef?.current?.value) { + titleRef.current.value = '' + } + if (descriptionRef?.current?.value) { + descriptionRef.current.value = '' + } + if (usernameRef?.current?.value) { + usernameRef.current.value = '' + } + setExpanded(false) + } else if (res.error) { + setError(res.error) + } + } + + const submitNewNote = async () => { + const inputTitle = titleRef?.current?.value + const inputDescription = descriptionRef?.current?.value + const inputUsername = usernameRef?.current?.value + const dataToSubmit = { + title: inputTitle, + description: inputDescription, + toolUser: inputUsername, + } + + fetch(url, params(dataToSubmit)) + .then((res) => res.json()) + .then((res) => processResponse(res)) + .catch((err: Error) => { + if (!error) { + setError(err.message) + } + }) + } + + return ( +
{ + e.preventDefault() + submitNewNote() + }} + > +
setExpanded(true)} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' && !expanded) { + setExpanded(true) + } else if (e.key === 'Escape' && expanded) { + setExpanded(false) + } + }} + > + +
+