From 1fcce4ca725f71769aab51d83b2430673db2d5c2 Mon Sep 17 00:00:00 2001 From: Lisbeth Lazala Date: Thu, 1 Oct 2020 18:09:42 -0400 Subject: [PATCH 1/4] web annotations and tutorial text --- .../steps-task-tracker-web-apollo.yaml | 59 +-- .../steps-task-tracker-web-realmapp.yaml | 39 +- ...tracker-web-client-base-file-structure.rst | 36 +- source/tutorial/web-graphql.txt | 346 ++++++------------ tutorial/web/package-lock.json | 14 + tutorial/web/src/App.js | 4 + tutorial/web/src/RealmApp.js | 19 +- .../src/components/EditPermissionsModal.js | 20 + tutorial/web/src/components/LoginScreen.js | 12 + tutorial/web/src/components/Sidebar.js | 8 + .../web/src/graphql/RealmApolloProvider.js | 19 +- tutorial/web/src/graphql/useProjects.js | 7 + tutorial/web/src/graphql/useTaskMutations.js | 47 ++- 13 files changed, 300 insertions(+), 330 deletions(-) diff --git a/source/includes/steps-task-tracker-web-apollo.yaml b/source/includes/steps-task-tracker-web-apollo.yaml index 8bf466cee4..597361b91b 100644 --- a/source/includes/steps-task-tracker-web-apollo.yaml +++ b/source/includes/steps-task-tracker-web-apollo.yaml @@ -2,65 +2,30 @@ title: Instantiate an ApolloClient ref: instantiate-an-apolloclient level: 4 content: | - The ``RealmApolloProvider`` component calls ``createApolloClient()`` to - instantiate the client. Update the function with the following code to create - an ``ApolloClient`` object that connects to your app: + The ``RealmApolloProvider`` component should call + ``createRealmApolloClient()`` to instantiate the client. Update the + component with the following code to create an ``ApolloClient`` object that + connects to your app: - .. code-block:: typescript - :caption: ``src/realm/RealmApolloProvider.tsx`` - - function createApolloClient(realmAppId: string, user: Realm.User): ApolloClient { - const graphql_url = `https://realm.mongodb.com/api/client/v2.0/app/${realmAppId}/graphql`; - - return new ApolloClient({ - link: new HttpLink({ - uri: graphql_url - }), - cache: new InMemoryCache(), - }); - } + .. literalinclude:: RealmApolloProvider.codeblock.realmApolloProvider.js + :caption: ``src/graphql/RealmApolloProvider.js`` + :emphasize-lines: 2-6 --- title: Authenticate GraphQL Requests ref: authenticate-graph-ql-requests level: 4 content: | - The ``createApolloClient()`` function now instantiates a client object, but + The ``createRealmApolloClient()`` function now instantiates a client object, but you won't be able to run any GraphQL queries or mutations just yet. Every GraphQL request must include an Authorization header that specifies a valid user access token. The current client does not include any Authorization headers, so all requests it makes will fail. - To fix this, update the ``createApolloClient()`` function to include the + To fix this, update the ``createRealmApolloClient()`` function to include the current user's access token in an Authorization header with every request: - .. code-block:: typescript - :caption: ``src/realm/RealmApolloProvider.tsx`` - :emphasize-lines: 17 + .. literalinclude:: RealmApolloProvider.codeblock.createRealmApolloClient.js + :caption: ``src/graphql/RealmApolloProvider.js`` + :emphasize-lines: 4, 13 - function createApolloClient(realmAppId: string, user: Realm.User): ApolloClient { - const graphql_url = `https://realm.mongodb.com/api/client/v2.0/app/${realmAppId}/graphql`; - - return new ApolloClient({ - link: new HttpLink({ - uri: graphql_url, - fetch: async (uri: RequestInfo, options: RequestInit) => { - if (!options.headers) { - options.headers = {} as Record; - } - // Refreshing custom data also ensures a valid access token - await user.refreshCustomData(); - const authenticatedOptions: RequestInit = { - ...options, - headers: { - ...options.headers, - Authorization: `Bearer ${user.accessToken}` - } - } - return fetch(uri, authenticatedOptions); - }, - }), - cache: new InMemoryCache(), - }); - } - } ... diff --git a/source/includes/steps-task-tracker-web-realmapp.yaml b/source/includes/steps-task-tracker-web-realmapp.yaml index c90e377cf6..dfd400d80e 100644 --- a/source/includes/steps-task-tracker-web-realmapp.yaml +++ b/source/includes/steps-task-tracker-web-realmapp.yaml @@ -3,19 +3,15 @@ ref: create-a-realm-app-client level: 4 content: | The app client is the primary interface to your Realm app from the SDK. In - ``src/realm/RealmApp.tsx``, add the following code immediately below the - imports at the top of the file to create the app client: + ``src/App.js``, replace ``"TODO"`` with your Realm App ID: - .. code-block:: typescript - :caption: ``src/realm/RealmApp.tsx`` - - const REALM_APP_ID = "" - const app = new Realm.App({ id: REALM_APP_ID }); + .. literalinclude:: App.codeblock.appID.js + :caption: ``src/App.js`` .. admonition:: Use Your App ID :class: note - Make sure to replace ```` with your app's unique + Make sure to replace ``"TODO"`` with your app's unique :guilabel:`App ID`. You can find your :guilabel:`App ID` by clicking the copy button next to the name of your app in the lefthand navigation of the Realm UI. @@ -29,31 +25,14 @@ level: 4 content: | The app client provides methods that allow you to authenticate and register users through the email/password authentication provider. In - ``src/realm/RealmApp.tsx``, the ``RealmApp`` component wraps these functions + ``src/RealmApp.js``, the ``RealmAppProvider`` component wraps these functions and keeps the app client in sync with local React state. These wrapping functions already have the state update calls but don't currently use the app client you created. You need to update the functions to actually call the SDK authentication and registration methods. - .. code-block:: typescript - :caption: ``src/realm/RealmApp.tsx`` - :emphasize-lines: 3, 8-9, 15 - - // Let new users register an account - const registerUser = async (email: string, password: string) => { - return await app.emailPasswordAuth.registerUser(email, password); - } - - // Let registered users log in - const logIn = async (email: string, password: string) => { - const credentials = Realm.Credentials.emailPassword(email, password); - await app.logIn(credentials); - setUser(app.currentUser); - } - - // Let logged in users log out - const logOut = async () => { - await app.logOut(); - setUser(app.currentUser); - } + .. literalinclude:: final/realmAppProvider.js + :caption: ``src/RealmApp.tsx`` + :emphasize-lines: 10, 16, 19 + diff --git a/source/includes/task-tracker-web-client-base-file-structure.rst b/source/includes/task-tracker-web-client-base-file-structure.rst index 2fdce731e9..5f3161805b 100644 --- a/source/includes/task-tracker-web-client-base-file-structure.rst +++ b/source/includes/task-tracker-web-client-base-file-structure.rst @@ -2,22 +2,22 @@ :copyable: False src/ - ├── index.tsx - ├── realm/ - │ ├── RealmApp.tsx - │ ├── RealmApolloProvider.tsx - │ ├── schema.graphql - │ └── operations.graphql + ├── index.js + ├── App.js + ├── RealmApp.js + ├── TaskApp.js ├── components/ - │ ├── App.tsx - │ ├── Board.tsx - │ ├── LoginScreen.tsx - │ ├── Navbar.tsx - │ ├── TaskCard.tsx - │ ├── TaskDetail.tsx - │ ├── TaskLists.tsx - │ └── TaskView.tsx - └── hooks/ - ├── useDraftTask.tsx - ├── useTaskLists.tsx - └── useTasks.tsx + │ ├── EditPermissionsModal.js + │ ├── Loading.js + │ ├── LoginScreen.js + │ ├── ProjectScreen.js + │ ├── SideBar.js + │ ├── StatusChange.js + │ ├── TaskContent.js + │ ├── TaskDetailModal.js + │ └── useChangeTaskStatusButton.js + └── graphql/ + ├── RealmApolloProvider.js + ├── useProjects.js + ├── useTaskMutations.js + └── useTasks.js diff --git a/source/tutorial/web-graphql.txt b/source/tutorial/web-graphql.txt index 68450c8349..e31c21c206 100644 --- a/source/tutorial/web-graphql.txt +++ b/source/tutorial/web-graphql.txt @@ -41,8 +41,10 @@ for you, so you don't need to know React to follow along. The app is a task tracker that allows users to: - Register and log in with an email/password account. -- Add, view, and delete tasks. +- View a list of their projects. +- Add, view, and delete tasks for projects they are a member of. - Switch tasks between Open, In Progress, and Complete statuses +- Add, view, and remove team members from projects. This tutorial should take around 30 minutes. @@ -132,25 +134,20 @@ client application and install its dependencies: cd realm-tutorial/web npm install -The ``main`` branch is a finished version of the app as it should look after -you complete this tutorial. To remove the Realm-specific source code that you'll -define in this tutorial, check out the ``todo`` branch: +The realm-tutorial-react-native repository contains two branches: ``final`` +and ``start``. The ``final`` branch is a finished version of the app as it +should look *after* you complete this tutorial. To walk through this +tutorial, please check out the ``start`` branch: .. code-block:: shell - git checkout todo - -.. note:: - - The ``realm-tutorial`` repo contains task tracker client applications for - multiple platforms. The project root for this tutorial is located in the - ``web`` subdirectory. + git checkout C. Explore the App Structure & Components ----------------------------------------- The web client is a standard React web application written in -:typescript:`TypeScript <>` and scaffolded with `create-react-app +:JavaScript:`JavaScript <>` and scaffolded with `create-react-app `__. We encourage you to explore the files in the app for a few minutes before you continue the tutorial. This will help you to familiarize yourself with what the app contains and where @@ -194,12 +191,12 @@ D. Connect to Your MongoDB Realm App ------------------------------------ The client app needs to connect to your Realm app so that users can register and -log in. In ``src/realm/RealmApp.tsx``, we import the Realm Web SDK to connect to +log in. In ``src/RealmApp.js``, we import the Realm Web SDK to connect to Realm and handle these actions. The file exports a React context provider that encapsulates this behavior and makes it available to other components in the app. -Some of the functionality in ``RealmApp.tsx`` is not fully defined. You need to +Some of the functionality in ``RealmApp.js`` is not fully defined. You need to update the code to use the SDK to connect to your Realm app and handle user authentication. @@ -208,199 +205,123 @@ authentication. .. admonition:: How We Use It :class: admonition-example - In ``/components/App.tsx``, we use the ``useRealmApp()`` hook to determine + In ``src/App.js``, we use the ``useRealmApp()`` hook to determine when the main application is ready to render. We also check for an authenticated user and always render exclusively the login screen unless a user is logged in. This guarantees that only authenticated users can access the rest of the app. - .. code-block:: typescript - :copyable: false - :emphasize-lines: 2, 3, 6 - - function RequireAuthentication() { - const app = useRealmApp(); - if (!app) { - return
Loading
; - } - return app.user ? ( - - - - ) : ( - - ); - } - - In ``/components/LoginScreen.tsx``, we use the wrapped authentication methods + .. literalinclude:: final/App.codeblock.requireLoggedInUser.js + :caption: ``src/App.js`` + + In ``/components/LoginScreen.js``, we use the wrapped authentication methods that you defined to log user in and register new users. + + Find the the ``handleLogin`` function and add the following code to process a + ``emailPassword`` credential by calling the ``logIn()`` method. - .. code-block:: typescript - :copyable: false - :emphasize-lines: 4, 16 - - const handleLogin = async () => { - setError((e) => ({ ...e, password: undefined })); - try { - return await app.logIn(email, password); - } catch (err) { - handleAuthenticationError(err); - } - }; - - const handleRegistrationAndLogin = async () => { - const isValidEmailAddress = validator.isEmail(email); - setError((e) => ({ ...e, password: undefined })); - if (isValidEmailAddress) { - try { - // Register the user and, if successful, log them in - await app.registerUser(email, password); - return await handleLogin(); - } catch (err) { - handleAuthenticationError(err); - } - } else { - setError((err) => ({ ...err, email: "Email is invalid." })); - } - }; + .. literalinclude:: LoginScreen.codeblock.handleLogin.js + :emphasize-lines: 5 + + Next, find the ``handleRegistrationAndLogin`` function and add the following code + to create a ``emailPassword`` credential. + + .. literalinclude:: LoginScreen.codeblock.handleRegistrationAndLogin.js + :emphasize-lines: 6 + E. Define the GraphQL Schema & Operations ----------------------------------------- -.. include:: /includes/steps/task-tracker-web-graphql.rst +A GraphQL schema defines all of the types, enums, and scalars that a GraphQL +API supports. Realm automatically generates a GraphQL schema for you, and you +can use it to generate useful TypeScript types for your data. -.. admonition:: How We Use It - :class: admonition-example - - We import the generated types defined in ``/src/types.ts`` from multiple - components throughout the app that need to use data from the GraphQL API. For - example, in ``/src/components/TaskView.tsx`` we import the generated ``Task`` - type and use it to type the component's ``task`` prop. - - .. code-block:: typescript - :caption: ``/src/components/TaskView.tsx`` - - import { Task } from "../types"; - - interface TaskViewProps { - task: Task; - } - - export function TaskView({ task }: TaskViewProps) { - const { assignee, name } = task; - const status = task.status as TaskStatus; - ... - } - - We use the custom Apollo hooks generated in ``/src/graphql-operations.ts`` to - call the GraphQL API as part of the task actions defined in ``useTasks()``. - - .. code-block:: typescript - :caption: ``/src/hooks/useTasks.tsx`` - :emphasize-lines: 13-17, 20-22 - - import { GetAllTasksQuery } from "./../types"; - import { - useGetAllTasksQuery, - useAddTaskMutation, - useUpdateTaskMutation, - useDeleteTaskMutation, - } from "./../graphql-operations"; - - export function useTasks() { - const [tasks, setTasks] = React.useState([]); - - // Query for Tasks - const { loading } = useGetAllTasksQuery({ onCompleted: (data: GetAllTasksQuery) => { - if(data?.tasks) { - setTasks(data.tasks as Task[]) - } - }}); - - // Create Task Mutation Functions - const [addTaskMutation] = useAddTaskMutation(); - const [updateTaskMutation] = useUpdateTaskMutation(); - const [deleteTaskMutation] = useDeleteTaskMutation(); - - ... - } - - The custom query and mutation hooks are lightweight wrappers around Apollo's - ``useQuery()`` and ``useMutation()`` hooks. For example, you could define - ``useAddTaskMutation()`` yourself with the following code: - - .. code-block:: typescript - - import { Task, TaskInsertInput } from "../types"; - import { useMutation } from "@apollo/client"; - import gql from 'graphql-tag'; - - type AddTaskMutation = { task: Task }; - type AddTaskMutationVariables = { task: TaskInsertInput }; - function useAddTaskMutation() { - return useMutation(gql` - mutation AddTask($task: TaskInsertInput!) { - task: insertOneTask(data: $task) { - _id - name - status - assignee { - _id - name - image - } - } - } - `); - } - - Later in the function, we use the functions returned from the mutation hooks - to execute the mutations. - - .. code-block:: typescript - :caption: ``/src/components/TaskView.tsx`` - :emphasize-lines: 5-14, 17 - - export function useTasks() { - ... - - const addTask = async (task: Task) => { - const variables: AddTaskMutationVariables = { - task: { - status: task.status, - name: task.name, - _partition: "My Project", - }, - }; - if(task.assignee) { - variables.task.assignee = { link: task.assignee._id } - } - const currentTasks = [...tasks]; - try { - const result = await addTaskMutation({ variables }); - const task = result.data?.task as Task; - setTasks([...tasks, task]); - } catch (err) { - setTasks(currentTasks); - throw new Error("Unable to add task"); - } - }; - } +In addition to generating types from a schema file, you can also generate +functions and GraphQL objects for operations such as queries and mutations. +You can split operations across multiple files, but in this tutorial we'll +define all of the CRUD operations that the app uses to work with tasks and +user documents in a single file. + +Navigate to the ``src/graphql/useTaskMutations.js`` file and define the +following mutations: + +* ``AddTaskMutation`` + + .. literalinclude:: useTaskMutations.codeblock.addTaskMutation.js + +* ``UpdateTaskMutation`` + + .. literalinclude:: useTaskMutations.codeblock.updateTaskMutation.js + +* ``DeleteTaskMutation`` + + .. literalinclude:: useTaskMutations.codeblock.deleteTaskMutation.js + +The custom query and mutation hooks are lightweight wrappers around Apollo's +``useQuery()`` and ``useMutation()`` hooks. Once you have completed defining +the task mutations, you will need to implement functions that execute those +mutations. In the same ``src/graphql/useTaskMutations.js``file, find the +following functions and call the appropriate mutations: + +* ``useAddTask`` + + .. literalinclude:: useTaskMutations.codeblock.useAddTask.js + :emphasize-lines: 18-26 + +* ``useUpdateTask`` + + .. literalinclude:: useTaskMutations.codeblock.useUpdateTask.js + :emphasize-lines: 3-8 + +* ``useDeleteTask`` + + .. literalinclude:: useTaskMutations.codeblock.useAddTask.js + :emphasize-lines: 3-8 F. Connect Apollo to the GraphQL API ------------------------------------ -We've defined GraphQL CRUD operations and used the code generator to create -custom query and mutation hooks. However, these hooks must be wrapped in an -Apollo context provider that makes an ``ApolloClient`` object available. +We've defined GraphQL CRUD operations and created custom query and mutation hooks +for tasks. However, these hooks must be wrapped in an Apollo context provider +that makes an ``ApolloClient`` object available. -In ``src/realm/RealmApolloProvider.tsx``, we export a React component that +In ``src/graphql/RealmApolloProvider.js``, we export a React component that provides the ``ApolloClient`` object but the function that instantiates the client is incomplete. You need to update the file to create a client that can connect to your app's GraphQL API. .. include:: /includes/steps/task-tracker-web-apollo.rst +G. Implement the Projects List +------------------------------ + +As defined by our data model, ``projects`` are an embedded object of the ``users`` object. +Therefore, if we want to retrieve a list of projects, we'll have to access the :ref:`user custom +data object `. + +In the file ``/graphql/useProjects.js``, we need to implement the retrieval of the +current user's projects. We'll need to add the following code: + +.. literalinclude:: useProjects.codeblock.useProjects.js + :emphasize-lines: 6 + +H. Use Realm functions +---------------------- + +In the ``EditPermissionsModal.js`` file, there are functions that rely on :ref:`{+service-short+} +functions `. {+service-short+} functions allow you to execute server-side +logic for your client applications. + +First, you need to import the functions for client-side usage. Next, you will need to +implement the function ``updateTeamMembers()`` so that it calls the ``getMyTeamMembers`` +function and updates the ``team`` variable with the current team members. Then you need to +implement the ``addTeamMember()`` and ``removeTeamMember()`` functions so that it adds/removes +the user as a team member when provided with their email. + +.. literalinclude:: EditPermissionsModal.codeblock.useTeamMembers.js + :emphasize-lines: 5, 6-9, 19-31 + Try It Out ---------- @@ -431,51 +352,20 @@ following: Open your browser to http://localhost:3000 to access the app. -Register a New User -~~~~~~~~~~~~~~~~~~~ - -You need to register a user account before you can log in and use the tracker. -On the login screen, click :guilabel:`Register one now` and enter the email -address and password that you want to log in with. This is just a sample app -that does not send any validation emails, so feel free to use a fake email -address and/or simple password. - -.. cssclass:: bordered-figure -.. figure:: /images/task-tracker-web-register-user.png - -Add Some Tasks -~~~~~~~~~~~~~~ - -Once you register, the app automatically logs you in. You can now add tasks and -use the tracker. To add a task, click :guilabel:`Add Task` at the bottom of any -of the lists, enter a name in the draft task that appears, then click -:guilabel:`Add`. - -.. cssclass:: bordered-figure -.. figure:: /images/task-tracker-web-add-task.png - -Move Tasks Around -~~~~~~~~~~~~~~~~~ - -You can change the status of a task by dragging it between lists. You can also -click on a task to open a detailed view with buttons that change the task's -status and allow you to delete the task entirely. - -.. cssclass:: bordered-figure -.. figure:: /images/task-tracker-web-move-task.png - :width: 750px - -Check Out the Logs -~~~~~~~~~~~~~~~~~~ + If the app builds successfully, here are some things you can try in the app: -Whenever you add, update, or delete a task, the client app sends a GraphQL -request to Realm. You can see a history of requests on the :guilabel:`Logs` page -of the Realm UI. Each GraphQL log entry shows the operation, compute usage, and -rule evaluation summary. + - Create a user with email *first@example.com* + - Explore the app, then log out or launch a second instance of the app on another device or simulator - Explore the app, then log out or launch a second instance of the app on another device or simulator + - Create another user with email second@example.com - Create another user with email *second@example.com* + - Navigate to second@example.com's project - Navigate to *second@example.com*'s project + - Add, update, and remove some tasks - Add, update, and remove some tasks + - Click "Manage Team" - Click "Manage Team" + - Add first@example.com to your team - Add *first@example.com* to your team + - Log out and log in as first@example.com - Log out and log in as *first@example.com* + - See two projects in the projects list - See two projects in the projects list + - Navigate to second@example.com's project - Navigate to *second@example.com*'s project + - Collaborate by adding, updating, and removing some new tasks - Collaborate by adding, updating, and removing some new tasks -.. cssclass:: bordered-figure -.. figure:: /images/task-tracker-web-graphql-logs.png - :width: 750px What's Next? ------------ @@ -488,9 +378,7 @@ options to keep practicing and learn more: - Extend the task tracker app with additional features. For example, you could: - - allow users to change a task's assignee - allow users to log in using another authentication provider - - support multiple projects that each have their own set of tasks - Follow another tutorial to build a mobile app for the task tracker. We have task tracker tutorials for the following platforms: diff --git a/tutorial/web/package-lock.json b/tutorial/web/package-lock.json index 521d6b8925..7d9b964d78 100644 --- a/tutorial/web/package-lock.json +++ b/tutorial/web/package-lock.json @@ -13026,6 +13026,12 @@ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, + "prettier": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz", + "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", + "dev": true + }, "pretty-bytes": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz", @@ -13571,6 +13577,14 @@ } } }, + "react-spinners": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.9.0.tgz", + "integrity": "sha512-+x6eD8tn/aYLdxZjNW7fSR1uoAXLb9qq6TFYZR1dFweJvckcf/HfP8Pa/cy5HOvB/cvI4JgrYXTjh2Me3S6Now==", + "requires": { + "@emotion/core": "^10.0.15" + } + }, "react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", diff --git a/tutorial/web/src/App.js b/tutorial/web/src/App.js index a9c33378f1..cb66ff4750 100644 --- a/tutorial/web/src/App.js +++ b/tutorial/web/src/App.js @@ -4,13 +4,17 @@ import TaskApp from "./TaskApp"; import RealmApolloProvider from "./graphql/RealmApolloProvider"; import { useRealmApp, RealmAppProvider } from "./RealmApp"; +// :code-block-start: appID export const APP_ID = "tasktracker-huhcb"; +// :code-block-end: +// :code-block-start: requireLoggedInUser const RequireLoggedInUser = ({ children }) => { // Only render children if there is a logged in user. const app = useRealmApp(); return app.currentUser ? children : ; }; +// :code-block-end: export default function App() { return ( diff --git a/tutorial/web/src/RealmApp.js b/tutorial/web/src/RealmApp.js index 2abe916204..7b8c5785af 100644 --- a/tutorial/web/src/RealmApp.js +++ b/tutorial/web/src/RealmApp.js @@ -13,8 +13,13 @@ export const useRealmApp = () => { return app; }; +// :code-block-start: realmAppProvider export const RealmAppProvider = ({ appId, children }) => { + // :hide-start: const [app, setApp] = React.useState(new Realm.App(appId)); + // :replace-with: + // // TODO: Wrap the Realm.App object with React state. + // :hide-end: React.useEffect(() => { setApp(new Realm.App(appId)); }, [appId]); @@ -22,17 +27,28 @@ export const RealmAppProvider = ({ appId, children }) => { // Wrap the Realm.App object's user state with React state const [currentUser, setCurrentUser] = React.useState(app.currentUser); async function logIn(credentials) { - // Log in with the given credentials + // :hide-start: await app.logIn(credentials); + // :replace-with: + // // TODO: Call the logIn() method with the given credentials + // :hide-end: // If successful, app.currentUser is the user that just logged in setCurrentUser(app.currentUser); } async function logOut() { // Log out the currently active user + // :hide-start: await app.currentUser?.logOut(); + // :replace-with: + // // TODO: Call the logOut() method on the current user. + // :hide-end: // If another user was logged in too, they're now the current user. // Otherwise, app.currentUser is null. + // :hide-start: setCurrentUser(app.currentUser); + // :replace-with: + // // TODO: Call the setCurrentUser() method on the app's current user. + // :hide-end: } const wrapped = { ...app, currentUser, logIn, logOut }; @@ -43,3 +59,4 @@ export const RealmAppProvider = ({ appId, children }) => { ); }; +// :code-block-end: diff --git a/tutorial/web/src/components/EditPermissionsModal.js b/tutorial/web/src/components/EditPermissionsModal.js index 5cca6bcb9a..80429f129b 100644 --- a/tutorial/web/src/components/EditPermissionsModal.js +++ b/tutorial/web/src/components/EditPermissionsModal.js @@ -11,15 +11,25 @@ import TextInput from "@leafygreen-ui/text-input"; import { uiColors } from "@leafygreen-ui/palette"; import { useRealmApp } from "../RealmApp"; +// :code-block-start: useTeamMembers function useTeamMembers() { const [teamMembers, setTeamMembers] = React.useState(null); const [newUserEmailError, setNewUserEmailError] = React.useState(null); const app = useRealmApp(); + // :hide-start: const { addTeamMember, removeTeamMember, getMyTeamMembers } = app.functions; + // :replace-with: + // // TODO: Import the Realm functions: addTeamMember, removeTeamMember, and getMyTeamMembers + // :hide-end: + // :hide-start: const updateTeamMembers = async () => { const team = await getMyTeamMembers(); setTeamMembers(team); }; + // :replace-with: + // // TODO: Implement the function updateTeamMembers so that it calls getMyTeamMembers and updates + // // the team variable with the current team members. + // :hide-end: /* eslint-disable react-hooks/exhaustive-deps */ React.useEffect(() => { // display team members on load @@ -29,6 +39,7 @@ function useTeamMembers() { return { teamMembers, errorMessage: newUserEmailError, + // :hide-start: addTeamMember: async (email) => { const { error } = await addTeamMember(email); if (error) { @@ -38,12 +49,21 @@ function useTeamMembers() { updateTeamMembers(); } }, + // :replace-with: + // // TODO: Call the addTeamMember() function and return updateTeamMembers if + // // addTeamMember() was successful. + // :hide-end: + // :hide-start: removeTeamMember: async (email) => { await removeTeamMember(email); updateTeamMembers(); }, + // :replace-with: + // // TODO: Call the removeTeamMember() + // :hide-end: }; } +// :code-block-end: export default function EditPermissionsModal({ isEditingPermissions, diff --git a/tutorial/web/src/components/LoginScreen.js b/tutorial/web/src/components/LoginScreen.js index e288480368..5833b3f6f0 100644 --- a/tutorial/web/src/components/LoginScreen.js +++ b/tutorial/web/src/components/LoginScreen.js @@ -29,23 +29,34 @@ export default function LoginScreen() { }, [mode]); const [isLoggingIn, setIsLoggingIn] = React.useState(false); + // :code-block-start: handleLogin const handleLogin = async () => { setIsLoggingIn(true); setError((e) => ({ ...e, password: null })); try { + // :hide-start: await app.logIn(Realm.Credentials.emailPassword(email, password)); + // :replace-with: + // // TODO: Call the logIn() method and pass it the emailPassword credentials. + // :hide-end: } catch (err) { handleAuthenticationError(err, setError); } }; + // :code-block-end: + // :code-block-start: handleRegistrationAndLogin const handleRegistrationAndLogin = async () => { const isValidEmailAddress = validator.isEmail(email); setError((e) => ({ ...e, password: null })); if (isValidEmailAddress) { try { // Register the user and, if successful, log them in + // :hide-start: await app.emailPasswordAuth.registerUser(email, password); + // :replace-with: + // // TODO: Create new emailPassword credentials by calling the registerUser() method. + // :hide-end: return await handleLogin(); } catch (err) { handleAuthenticationError(err, setError); @@ -54,6 +65,7 @@ export default function LoginScreen() { setError((err) => ({ ...err, email: "Email is invalid." })); } }; + // :code-block-end: return ( diff --git a/tutorial/web/src/components/Sidebar.js b/tutorial/web/src/components/Sidebar.js index 0b9ebe9c4d..9729497703 100644 --- a/tutorial/web/src/components/Sidebar.js +++ b/tutorial/web/src/components/Sidebar.js @@ -11,8 +11,12 @@ export default function Sidebar({ setCurrentProject, setIsEditingPermissions, }) { + // :code-block-start: sidebarSetUp const projects = useProjects(); const app = useRealmApp(); + // :code-block-end: + + // :code-block-start: sidebarContainer return ( @@ -20,7 +24,10 @@ export default function Sidebar({ {projects.map((project) => ( setCurrentProject(project)} isSelected={project.partition === currentProject?.partition} > @@ -41,6 +48,7 @@ export default function Sidebar({ ); } +// :code-block-end: const SidebarContainer = styled.div` display: flex; diff --git a/tutorial/web/src/graphql/RealmApolloProvider.js b/tutorial/web/src/graphql/RealmApolloProvider.js index b57702f63d..c3bdb9ad7b 100644 --- a/tutorial/web/src/graphql/RealmApolloProvider.js +++ b/tutorial/web/src/graphql/RealmApolloProvider.js @@ -7,10 +7,16 @@ import { ApolloProvider, } from "@apollo/client"; + // Create an ApolloClient that connects to the provided Realm.App's GraphQL API +// :code-block-start: createRealmApolloClient const createRealmApolloClient = (app) => { const link = new HttpLink({ + // :hide-start: // Realm apps use a standard GraphQL endpoint, identified by their App ID + // :replace-with: + // // TODO: Add your Realm App ID to the uri link to connect your app. + // :hide-end: uri: `https://realm.mongodb.com/api/client/v2.0/app/${app.id}/graphql`, // A custom fetch handler adds the logged in user's access token to GraphQL requests fetch: async (uri, options) => { @@ -19,23 +25,34 @@ const createRealmApolloClient = (app) => { } // Refreshing a user's custom data also refreshes their access token await app.currentUser.refreshCustomData(); + // :hide-start: // The handler adds a bearer token Authorization header to the otherwise unchanged request options.headers.Authorization = `Bearer ${app.currentUser.accessToken}`; + // :replace-with: + // // TODO: Include the current user's access token in an Authorization header with + // // every request. + // :hide-end: return fetch(uri, options); }, }); - // const cache = new InMemoryCache(); return new ApolloClient({ link, cache }); }; +// :code-block-end: +// :code-block-start: realmApolloProvider export default function RealmApolloProvider({ children }) { + // :hide-start: const app = useRealmApp(); const [client, setClient] = React.useState(createRealmApolloClient(app)); React.useEffect(() => { setClient(createRealmApolloClient(app)); }, [app]); + // :replace-with: + // // TODO: Create an ``ApolloClient`` object that connects to your app. + // :hide-end: return {children}; } +// :code-block-end: diff --git a/tutorial/web/src/graphql/useProjects.js b/tutorial/web/src/graphql/useProjects.js index 8eb09a0795..8df4113466 100644 --- a/tutorial/web/src/graphql/useProjects.js +++ b/tutorial/web/src/graphql/useProjects.js @@ -1,10 +1,17 @@ import { useRealmApp } from "../RealmApp"; +// :code-block-start: useProjects export default function useProjects() { const app = useRealmApp(); if (!app.currentUser) { throw new Error("Cannot list projects if there is no logged in user."); } + // :hide-start: const projects = app.currentUser.customData.memberOf; + // :replace-with: + // // TODO: Retrieve the current user's projects and assign it to `projects`. + // const projects; + // :hide-end: return projects; } +// :code-block-end: \ No newline at end of file diff --git a/tutorial/web/src/graphql/useTaskMutations.js b/tutorial/web/src/graphql/useTaskMutations.js index cffcbe8778..3f82ea5d8e 100644 --- a/tutorial/web/src/graphql/useTaskMutations.js +++ b/tutorial/web/src/graphql/useTaskMutations.js @@ -10,6 +10,8 @@ export default function useTaskMutations(project) { }; } +// :code-block-start: addTaskMutation +// :hide-start: const AddTaskMutation = gql` mutation AddTask($task: TaskInsertInput!) { addedTask: insertOneTask(data: $task) { @@ -20,7 +22,14 @@ const AddTaskMutation = gql` } } `; +// :replace-with: +// // TODO: Add the GraphGL mutation for adding a task. +// const AddTaskMutation = gql``; +// :hide-end: +// :code-block-end: +// :code-block-start: updateTaskMutation +// :hide-start: const UpdateTaskMutation = gql` mutation UpdateTask($taskId: ObjectId!, $updates: TaskUpdateInput!) { updatedTask: updateOneTask(query: { _id: $taskId }, set: $updates) { @@ -31,7 +40,14 @@ const UpdateTaskMutation = gql` } } `; +// :replace-with: +// // TODO: Add the GraphGL mutation for updating a task. +// const UpdateTaskMutation = gql``; +// :hide-end: +// :code-block-end: +// :code-block-start: deleteTaskMutation +// :hide-start: const DeleteTaskMutation = gql` mutation DeleteTask($taskId: ObjectId!) { deletedTask: deleteOneTask(query: { _id: taskId }) { @@ -42,6 +58,11 @@ const DeleteTaskMutation = gql` } } `; +// :replace-with: +// // TODO: Add the GraphGL mutation for deleting a task. +// const DeleteTaskMutation = gql``; +// :hide-end: +// :code-block-end: const TaskFieldsFragment = gql` fragment TaskFields on Task { @@ -52,6 +73,7 @@ const TaskFieldsFragment = gql` } `; +// :code-block-start: useAddTask function useAddTask(project) { const [addTaskMutation] = useMutation(AddTaskMutation, { // Manually save added Tasks into the Apollo cache so that Task queries automatically update @@ -72,6 +94,7 @@ function useAddTask(project) { }); const addTask = async (task) => { + // :hide-start: const { addedTask } = await addTaskMutation({ variables: { task: { @@ -81,35 +104,51 @@ function useAddTask(project) { ...task, }, }, + // :replace-with: + // // TODO: Use the functions returned from the addTaskMutation hook to execute the + // // mutation. + // :hide-end: }); return addedTask; }; return addTask; } +// :code-block-end: +// :code-block-start: useUpdateTask +// :hide-start: function useUpdateTask(project) { const [updateTaskMutation] = useMutation(UpdateTaskMutation); - + // :hide-start: const updateTask = async (task, updates) => { const { updatedTask } = await updateTaskMutation({ variables: { taskId: task._id, updates }, }); return updatedTask; }; - + // :replace-with: + // // TODO: Use the functions returned from the updateTaskMutation to execute the + // // mutation. + // :hide-end: return updateTask; } +// :code-block-end: +// :code-block-start: useDeleteTask function useDeleteTask(project) { const [deleteTaskMutation] = useMutation(DeleteTaskMutation); - + // :hide-start: const deleteTask = async (task) => { const { deletedTask } = await deleteTaskMutation({ variables: { taskId: task._id }, }); return deletedTask; }; - + // :replace-with: + // // TODO: Use the functions returned from the deleteTaskMutation to execute the + // // mutation. + // :hide-end: return deleteTask; } +// :code-block-end: From 7341fe9111cf12436e718438b6624bd40970247d Mon Sep 17 00:00:00 2001 From: Lisbeth Lazala Date: Thu, 1 Oct 2020 18:18:25 -0400 Subject: [PATCH 2/4] minor indentation fix --- source/tutorial/web-graphql.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/source/tutorial/web-graphql.txt b/source/tutorial/web-graphql.txt index e31c21c206..d275728624 100644 --- a/source/tutorial/web-graphql.txt +++ b/source/tutorial/web-graphql.txt @@ -352,19 +352,19 @@ following: Open your browser to http://localhost:3000 to access the app. - If the app builds successfully, here are some things you can try in the app: - - - Create a user with email *first@example.com* - - Explore the app, then log out or launch a second instance of the app on another device or simulator - Explore the app, then log out or launch a second instance of the app on another device or simulator - - Create another user with email second@example.com - Create another user with email *second@example.com* - - Navigate to second@example.com's project - Navigate to *second@example.com*'s project - - Add, update, and remove some tasks - Add, update, and remove some tasks - - Click "Manage Team" - Click "Manage Team" - - Add first@example.com to your team - Add *first@example.com* to your team - - Log out and log in as first@example.com - Log out and log in as *first@example.com* - - See two projects in the projects list - See two projects in the projects list - - Navigate to second@example.com's project - Navigate to *second@example.com*'s project - - Collaborate by adding, updating, and removing some new tasks - Collaborate by adding, updating, and removing some new tasks +If the app builds successfully, here are some things you can try in the app: + +- Create a user with email *first@example.com* +- Explore the app, then log out or launch a second instance of the app on another device or simulator - Explore the app, then log out or launch a second instance of the app on another device or simulator +- Create another user with email second@example.com - Create another user with email *second@example.com* +- Navigate to second@example.com's project - Navigate to *second@example.com*'s project +- Add, update, and remove some tasks - Add, update, and remove some tasks +- Click "Manage Team" - Click "Manage Team" +- Add first@example.com to your team - Add *first@example.com* to your team +- Log out and log in as first@example.com - Log out and log in as *first@example.com* +- See two projects in the projects list - See two projects in the projects list +- Navigate to second@example.com's project - Navigate to *second@example.com*'s project +- Collaborate by adding, updating, and removing some new tasks - Collaborate by adding, updating, and removing some new tasks What's Next? From 657ac44d0ade86eac977c22f2f8ac2cb7d9e0423 Mon Sep 17 00:00:00 2001 From: Lisbeth Lazala Date: Fri, 2 Oct 2020 09:53:09 -0400 Subject: [PATCH 3/4] removed doubled content --- source/tutorial/web-graphql.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/source/tutorial/web-graphql.txt b/source/tutorial/web-graphql.txt index d275728624..4d59620397 100644 --- a/source/tutorial/web-graphql.txt +++ b/source/tutorial/web-graphql.txt @@ -355,16 +355,16 @@ Open your browser to http://localhost:3000 to access the app. If the app builds successfully, here are some things you can try in the app: - Create a user with email *first@example.com* -- Explore the app, then log out or launch a second instance of the app on another device or simulator - Explore the app, then log out or launch a second instance of the app on another device or simulator -- Create another user with email second@example.com - Create another user with email *second@example.com* -- Navigate to second@example.com's project - Navigate to *second@example.com*'s project -- Add, update, and remove some tasks - Add, update, and remove some tasks -- Click "Manage Team" - Click "Manage Team" -- Add first@example.com to your team - Add *first@example.com* to your team -- Log out and log in as first@example.com - Log out and log in as *first@example.com* -- See two projects in the projects list - See two projects in the projects list -- Navigate to second@example.com's project - Navigate to *second@example.com*'s project -- Collaborate by adding, updating, and removing some new tasks - Collaborate by adding, updating, and removing some new tasks +- Explore the app, then log out or launch a second instance of the app in an incognito browser window +- Create another user with email *second@example.com* +- Navigate to *second@example.com*'s project +- Add, update, and remove some tasks +- Click "Manage Team" +- Add *first@example.com* to your team +- Log out and log in as *first@example.com* +- See two projects in the projects list +- Navigate to *second@example.com*'s project +- Collaborate by adding, updating, and removing some new tasks What's Next? From e0c60c6e23346f958b54f177b412d484cf485207 Mon Sep 17 00:00:00 2001 From: Lisbeth Lazala Date: Fri, 2 Oct 2020 11:05:41 -0400 Subject: [PATCH 4/4] added more to last two sections --- source/tutorial/nodejs-cli.txt | 65 +++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/source/tutorial/nodejs-cli.txt b/source/tutorial/nodejs-cli.txt index 3f101eaa79..35a437f284 100644 --- a/source/tutorial/nodejs-cli.txt +++ b/source/tutorial/nodejs-cli.txt @@ -357,10 +357,19 @@ Once you have completed the code, you should run the app and check functionality .. image:: /images/node-cli-start-screen.png :alt: Initial menu -#. If you do not yet have a user account, enter an email and password, and the - system will create a new account and log you in. At this point, you should see - the main "menu" of choices. All of the options should now work for you except - the "watch" functionality, which we'll enable in the next section. +If the app builds successfully, here are some things you can try in the app: + +- Create a user with email *first@example.com* +- Explore the app, then log out. +- Start up the app again and register as another user with email *second@example.com* +- Select *second@example.com*'s project +- Add, update, and remove some tasks +- Select the "Manage Team" menu option +- Add *first@example.com* to your team +- Log out and log in as *first@example.com* +- See two projects in the projects list +- Navigate to *second@example.com*'s project +- Collaborate by adding, updating, and removing some new tasks .. admonition:: Reminder @@ -370,7 +379,47 @@ Once you have completed the code, you should run the app and check functionality What's Next? ------------ -- Read our :ref:`Node.js SDK ` documentation. -- Try the :ref:`{+service+} Backend tutorial `. -- Find developer-oriented blog posts and integration tutorials on the `MongoDB Developer Hub `__. -- Join the `MongoDB Community forum `__ to learn from other MongoDB developers and technical experts. +You just built a functional task tracker web application built with MongoDB +Realm. Great job! + +Now that you have some hands-on experience with MongoDB Realm, consider these +options to keep practicing and learn more: + +- Extend the task tracker app with additional features. For example, you could: + + - allow users to log in using another authentication provider + +- Follow another tutorial to build a mobile app for the task tracker. We have + task tracker tutorials for the following platforms: + + - :doc:`iOS (Swift) ` + - :doc:`Android (Kotlin) ` + - :doc:`React Native (JavaScript) ` + - :doc:`Web with React and GraphQL (Javascript) ` + +- Dive deeper into the docs to learn more about MongoDB Realm. You'll find + information and guides on features like: + + - Serverless :doc:`functions ` that handle backend logic and + connect your app to external services. You can call functions from a + client app, either directly or as a :doc:`custom GraphQL resolver + `. + + - :doc:`Triggers ` and :ref:`incoming webhooks `, + which automatically call functions in response to events as they occur. You + can define :doc:`database triggers ` which + respond to changes in your data, :doc:`authentication triggers + ` which respond to user management and + authentication events, and :doc:`scheduled triggers + ` which run on a fixed schedule. + + - Built-in :doc:`authentication providers ` and + and user management tools. You can allow users to log in through multiple + methods, like API keys and Google OAuth, and associate :doc:`custom data + ` with every user. + +.. admonition:: Leave Feedback + :class: note + + How did it go? Please let us know if this tutorial was helpful or if you had + any issues by using the feedback widget on the bottom right of the page.