Can I Use This?

This feature is available since Webiny v5.12.0.

What you'll learn
  • how to hook into the page settings views
  • how to add new fields to the existing page settings views

Overview
anchor

Page settings UI within the Webiny Page Builder, by default, contains 3 major sections: General settings, Social media, and SEO settings.

Page Settings ViewPage Settings View
(click to enlarge)

Adding a new field to the page settings involves the following steps:

  1. Extend the GraphQL API to be able to handle the new field.
  2. Add a form field to the UI to represent the value from the GraphQL API.
  3. Add the new field selection to the GraphQL query that is sent from the browser to the GraphQL API.

In this example, we’ll add a password field to the General Settings.

Add a New Settings Field to GraphQL API
anchor

We need to extend the following GraphQL types: PbGeneralPageSettings for queries, and PbGeneralPageSettingsInput for mutations.

But GraphQL is just a router, and page settings have some strict validation rules, so extending a GraphQL type is not enough to push the data into the database. We need to manually grab the input value and assign it to the page settings data.

The following code takes care of all the things described above. j

apps/api/graphql/src/plugins/pageSettings.ts
import { GraphQLSchemaPlugin } from "@webiny/handler-graphql/plugins";
import { ContextPlugin } from "@webiny/handler";
import { PbContext, OnBeforePageUpdateTopicParams } from "@webiny/api-page-builder/types";

// Make sure to import the `Context` interface and pass it to the `GraphQLSchemaPlugin`
// plugin. Apart from making your application code type-safe, it will also make the
// interaction with the `context` object significantly easier.
import { Context } from "~/types";

// We need this interface for the event subscription below.
// It will enable you to have proper autocomplete on your new fields.
interface CustomEventParams extends OnBeforePageUpdateTopicParams {
  page: OnBeforePageUpdateTopicParams["page"] & {
    settings: {
      general: {
        password: string;
      };
    };
  };
}

export default [
  // Add `password` to the page settings types
  new GraphQLSchemaPlugin<Context>({
    typeDefs: /* GraphQL */ `
      extend type PbGeneralPageSettings {
        password: String
      }

      extend input PbGeneralPageSettingsInput {
        password: String
      }
    `
  }),
  // Subscribe to the page update event using the ContextPlugin.
  new ContextPlugin<PbContext>(({ pageBuilder }) => {
    // We are passing a custom event type to allow us to use the new `password` field.
    pageBuilder.onBeforePageUpdate.subscribe<CustomEventParams>(({ page, input }) => {
      // Explicitly assign the field value from GraphQL input to the data that is used to update the page.
      page.settings.general.password = input.settings.general.password;
    });
  })
];

The code above can be placed in the api/graphql/src/plugins/pageSettings.ts file, which doesn’t exist by default, so you will have to create it manually. Furthermore, once the file is created, make sure that it’s actually imported and registered in the api/graphql/src/index.ts entrypoint file.

apps/api/graphql/src/index.ts
// ...

// my password field custom plugin
import passwordFieldPageSettingsPlugin from "./plugins/pageSettings";

export const handler = createHandler({
  plugins: [
    // ...
    // add this at the end of the plugins array list
    passwordFieldPageSettingsPlugin
  ],
  http: { debug }
});

With this plugin, once you redeploy the API project application, your GraphQL schema will contain the new password field:

New Password Field in the SchemaNew Password Field in the Schema
(click to enlarge)
Use the webiny watch command to continuously deploy application code changes into the cloud and instantly see them in action. For quick (manual) testing, you can use the built-in API Playground.

Add a Form Field to the UI
anchor

The view class responsible for the general settings is the GeneralSettingsView class. To modify it, we need to hook into it using the UIViewPlugin plugin. We also need to add the new field to the PbGetPage GraphQL query operation, which loads the page data.

apps/admin/src/plugins/pageSettings.ts
import { gql } from "graphql-tag";
import { AddQuerySelectionPlugin } from "@webiny/app/plugins/AddQuerySelectionPlugin";
import { UIViewPlugin } from "@webiny/app-admin/ui/UIView";
import { GeneralSettingsView } from "@webiny/app-page-builder/editor/ui/views/GeneralSettingsView";
import { InputElement } from "@webiny/app-admin/ui/elements/form/InputElement";

export default [
  // Add an input field for the password
  new UIViewPlugin(GeneralSettingsView, view => {
    view.addField(
      new InputElement("password", {
        name: "settings.general.password",
        label: "Password",
        description: "If set, the page will be protected by this password on the frontend."
      })
    );
  }),
  // Add a selection to outgoing `GetPage` GraphQL operation
  new AddQuerySelectionPlugin({
    operationName: "PbGetPage",
    selectionPath: "pageBuilder.getPage.data",
    addSelection: gql`
      {
        settings {
          general {
            password
          }
        }
      }
    `
  })
];

And then, make sure to import your new UI plugin:

apps/admin/src/plugins/pageBuilder/editorPlugins.tsx
//...
import pageSettings from "./../pageSettings";

export default [
  // Page settings
  pageSettingsPlugins,
  pageSettings // your plugin
];

After your application rebuilds, the new field will be rendered in the settings view. Using this technique, you can add as many fields as you need. You can also hook into other page settings views, using their corresponding classes: SocialSettingsView and SEOSettingsView.

Password Field Added to the General Settings ViewPassword Field Added to the General Settings View
(click to enlarge)

For the Admin Area, the work is done. You can now get and update the value of that new password field.

Add the New Query Selection in the Website Application
anchor

This step is optional. If you’re writing custom queries and are executing the GraphQL API calls yourself, you can skip this step.

Website application doesn’t know about the plugin we’ve added in the Admin Area application. If you want this new field to be automatically queried with the rest of the page data, you need to add a plugin, just like in the Admin Area app.

apps/website/src/plugins/pageSettings.ts
import { gql } from "graphql-tag";
import { AddQuerySelectionPlugin } from "@webiny/app/plugins/AddQuerySelectionPlugin";

export default new AddQuerySelectionPlugin({
  operationName: "PbGetPublishedPage",
  selectionPath: "pageBuilder.getPublishedPage.data",
  addSelection: gql`
    {
      settings {
        general {
          password
        }
      }
    }
  `
});

The code above can be placed in the apps/website/src/plugins/pageSettings.ts file, which doesn’t exist by default, so you will have to create it manually. Furthermore, once the file is created, make sure that it’s actually imported and registered in the apps/website/src/plugins/index.ts entrypoint file.

Once the application is rebuilt, the password field will be included in all PbGetPublishedPage GraphQL query operations:

Password Is Now Returned from the APIPassword Is Now Returned from the API
(click to enlarge)

Handling DateTime Fields
anchor

GraphQL DateTime type has a little peculiarity you should be aware of, to avoid unnecessary debugging. When you add a DateTime scalar to your GraphQL Schema, and perform a mutation, that DateTime field will be converted to a Date external link object during input validation (performed by GraphQL Schema), and it will be passed as such to the resolver.

It’s the default behaviour of the DateTime scalar, from the graphql-scalars external link library, which Webiny uses.

The following example demonstrates how you should handle the DateTime fields in your code, when storing those fields to the database:

Storing DateTime Fields to the Database
new PagePlugin<CustomPage>({
  beforeUpdate({ inputData, updateData }) {
    // Convert the Date object to ISO string.
    const eventStartISO = inputData.settings.general.eventStart.toISOString();

    // Now you can safely assign the value which will be stored to the database.
    updateData.settings.general.eventStart = eventStartISO;
  }
});

Add New Fields to listPublishedPages Query
anchor

If you need to add your new fields to the output of the listPublishedPages query, there are a couple of things to know about this query.

List operations are performed on Elasticsearch, and the data that comes back from the ES index goes directly into GraphQL resolvers. The data that is stored in the ES index only contains partial page data, relevant for searching.

When you add new fields to page settings, that data is not stored to ES, unless you provide a plugin which will add that data to the index.

You also need to extend the PbPageListItem GraphQL type, which is returned from listPublishedPages, as it is different from the one used in getPublishedPage query.

The following example shows how you can add the new password field into the ES index, so that it becomes available in the listPublishedPages query:

Add Data To Elasticsearch Index and GraphQL Schema
import { GraphQLSchemaPlugin } from "@webiny/handler-graphql/plugins";
import { IndexPageDataPlugin } from "@webiny/api-page-builder/plugins/IndexPageDataPlugin";

// Make sure to import the `Context` interface and pass it to the `GraphQLSchemaPlugin`
// plugin. Apart from making your application code type-safe, it will also make the
// interaction with the `context` object significantly easier.
import { Context } from "~/types";

export default [
  new GraphQLSchemaPlugin<Context>({
    typeDefs: /* GraphQL */ `
      extend type PbPageListItem {
        password: String
      }
    `
  }),

  // Add the new `password` field to the data going into ES index.
  new IndexPageDataPlugin<PageWithPassword>(({ data, page }) => {
    // `data` represents the current page's data that will be stored in Elasticsearch.
    // You can store your custom data anywhere on the object. It doesn't need to follow the actual settings structure
    // of the original `page` object, as this data is for `list` operations only.
    data.password = page.settings.general.password;
  })
];

With these plugins in place, you can now access the password field on the listPublishedPages query:

Password Field in the listPublishedPages QueryPassword Field in the listPublishedPages Query
(click to enlarge)
Important

After these plugins are deployed, you need to publish the page, to trigger ES index update. Only then your new data will be available for querying in the listPublishedPages query.