Tina Cloud Client


Tina Cloud is currently in alpha. Check it out here

The Tina Cloud Client can be used to interact with the GraphQL layer, and allow you to create the forms that your content team can interact with.

For a real-world example of how this is being used checkout the Tina Cloud Starter.

Getting started

Npm:

npm install --save-dev tina-graphql-gateway

Yarn:

yarn add --dev tina-graphql-gateway

The TinaCMS API Client

This package exports a class which acts as TinaCMS external API for the Tina Content API. This is a headless GraphQL API that's served via Tina Cloud or locally from within the Tina CLI.

import { Client, LocalClient } from 'tina-graphql-gateway'
const client = new Client({
  clientId: 'the client ID you get from Tina Cloud',
  branch: 'main',
  tokenStorage: 'LOCAL_STORAGE' | 'MEMORY' | 'CUSTOM',
})

// For a simpler setup while working locally you can instantiate the LocalClient as a convenience
const client = new LocalClient()

The Client does a few things:

  • Manages auth with Tina Cloud
  • Provides a request function for working with the GraphQL API

Start by initializing the LocalClient - which automatically connects with your locally-running GraphQL server. From there, you can make GraphQL requests:

client.request

const client = new LocalClient()

await client.request(
  gql => gql`#graphql
query BlogPostQuery($relativePath: String!) {
  {
    getPostsDocument(relativePath: "") {
      data {
        title
      }
    }
  }
}
`,
  { variables: { relativePath: 'hello-world.md' } }
)

This API currently doesn't support filtering and sorting "list" queries. We have plans to tackle that in upcoming cycles.

useGraphQLForms

While GraphQL is a great tool, using it with Tina can be difficult. GraphQL can query across multiple nodes, but since each document would require its own Tina form it could be difficult to sync the data with your query with all of the forms you'd need to build. The Tina GraphQL server knows all about your content schema so we're actually able to build forms automatically by inspecting your query. To see this in action, pass your query into the useGraphqlForms hook:

import { useGraphqlForms } from 'tina-graphql-gateway'

const query = gql => gql`#graphql
  query BlogPostQuery($relativePath: String!) {
    {
      getPostsDocument(relativePath: $relativePath) {
        data {
          title
        }
      }
    }
  }
`

const MyPage = (props) => {
  const [payload, isLoading] = useGraphqlForms<PostQueryResponseType>({
    query,
    variables: { relativePath: `${props.filename}.md` },
  });

  isLoading ? <div>Loading...</div> : <MyComponent {...payload}>
}

If Tina is enabled you can see a form for the getPostsDocument request. If you query for multiple documents, you should see multiple forms:

const query = gql => gql`#graphql
  query BlogPostQuery($relativePath: String!) {
    {
      # this generates a Tina Form
      getSiteNavsDocument(relativePath: "site-nav.md") {
        data {
          items {
            title
            link
          }
        }
      }
      # this generates a separate Tina Form
      getPostsDocument(relativePath: $relativePath) {
        data {
          title
        }
      }
    }
  }
`

Formify

Formify allows you to control the output of the forms created through useGraphqlForms. In the examples below we show you how to overide a particular id and also show you how to customize specific fields.

Form customization:
import { useGraphqlForms } from 'tina-graphql-gateway'
import { Form, GlobalFormPlugin, useCMS } from 'tinacms'

const [payload, isLoading] = useGraphqlForms({
  query,
  formify: ({ formConfig, createForm, skip }) => {
    if (formConfig.id === 'getSiteNavsDocument') {
      const form = new Form(formConfig)
      // The site nav will be a global plugin
      cms.plugins.add(new(form))
      return form
    }

    return createForm(formConfig)
  },
  variables: { relativePath: `${props.filename}.md` },
})

You can also skip a particlar id to stop it from creating a form altogether, this allows you to display content but stop it from being editable.

// or to skip the nav from creating a form altogether:
const [payload, isLoading] = useGraphqlForms({
  query,
  formify: ({ formConfig, createForm, skip }) => {
    if (formConfig.id === 'getSiteNavsDocument') {
      return skip()
    }

    return createForm(formConfig)
  },
  variables: { relativePath: `${props.filename}.md` },
})
Field customization:

Since your forms are built automatically through the schema you create, you can also use formify to customize specific fields:

const [payload, isLoading] = useGraphqlForms({
  query,
  formify: ({ formConfig, createForm, skip }) => {
    return createForm({
      ...formConfig,
      fields: formConfig.fields.map(field => {
        if (field.name === 'title') {
          // replace `text` with `textarea`
          field.component = 'textarea'
        }
        return field
      }),
    })
  },
  variables: { relativePath: `${props.filename}.md` },
})

useDocumentCreatorPlugin

An important part of any application or website is the ability to create new pages, you can use the useDocumentCreatorPlugin which allows the end user the ability to create new pages.

Note that you'll be responsible for redirecting the user after a new document has been created.

import { useDocumentCreatorPlugin } from 'tina-graphql-gateway'

// args is of type:
// {
//   collection: {
//     slug: string;
//   };
//   relativePath: string;
//   breadcrumbs: string[];
//   path: string;
// }
useDocumentCreatorPlugin(args => window.location.assign(buildMyRouter(args)))

Customizing the content creator options

In some cases you want to stop your content creators from creating new pages for a particular type of content, you can use options alongside filter :

// options are of type:
// {
//   label: string;
//   value: string;
// }[]
useDocumentCreatorPlugin(null, options =>
  options.filter(option => option.name !== 'post')
)

Authentication with Tina Cloud

While this package comes with low-level APIs for authentication with Tina Cloud, the easiest way to get started is to use the TinaCloudAuthWall component, which prevents children from rendering until a valid session has been established with Tina Cloud.

TinaCloudAuthWall

import { TinaCloudAuthWall, Client } from "tina-graphql-gateway";

const TinaWrapper = ({ children }) => {
  const cms = React.useMemo(() => {
    return new TinaCMS({
      apis: {
        tina: new Client({
          // config
        })
      },
      ...
    });
  }, []);

  return <TinaCloudAuthWall cms={cms}>{children}</TinaCloudAuthWall>;
};

Props for TinaCloudAuthWall

PropDescription
cmsAn instance of a CMS
getModalActions (optional)A function that returns a list of actions / buttons that will be rendered to the model. Each button has name, action, and can be primary or not. The name is the text that will be displayed. The action is a function that will be run when the button is clicked. See example below for more details
return (
    <TinaCloudAuthWall cms={cms} getModalActions={({closeModal})=>{
      return [{
         action: async ()=>{
          //  use your own state to get in and out of edit mode
           closeModal()
         },
         name: 'close',
         primary: false,
      }]
    }}>
      <Component {...pageProps} />
    </TinaCloudAuthWall>
);

Note: when using the LocalClient, TinaCloudAuthWall won't display a login screen, there is no authentication for the local GraphQL server.

Authenticating without TinaCloudAuthWall

You can also authenticate with the Client directly:

const client = new Client({
  // config
})

const EditSiteButton = () => {
  const cms = useCMS()
  const onClick = async () => {
    await client.authenticate().then(token => {
      cms.enable()
    })
  }

  return <button onClick={onClick}>Edit This Site</button>
}