Skip to Page NavigationSkip to Page NavigationSkip to Content
Keystone 6 is now in General Availability!

Lesson 3: Publishing workflows

Learn how to create a publishing workflow to your app using Keystons’s select and timestamp fields.

Where we left off

In the last lesson we added a post list to our blog app and setup our first connection between posts and users with Keystone’s relationship field. Here's the code:

// keystone.ts
import { list, config } from '@keystone-6/core';
import { text, relationship } from '@keystone-6/core/fields';
const lists = {
User: list({
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),
posts: relationship({ ref: 'Post.author', many: true }),
},
}),
Post: list({
fields: {
title: text(),
author: relationship({
ref: 'User.posts',
ui: {
displayMode: 'cards',
cardFields: ['name', 'email'],
inlineEdit: { fields: ['name', 'email'] },
linkToItem: true,
inlineCreate: { fields: ['name', 'email'] },
},
}),
},
}),
};
export default config({
db: {
provider: 'sqlite',
url: 'file:./keystone.db',
},
lists,
});

We’re now going to extend our Keystone schema to support publishing needs.

Build a publishing workflow

Our publishing workflow needs the ability to reflect:

  • When the post should be published
  • The post’s status – whether it’s still being drafted, or is ready for publishing

These will give us what we need to conditionally display and order posts in a frontend app.

Add a publish date

Keystone’s timestamp field will let editors associate a date and time with the post:

import { list, config } from '@keystone-6/core';
import { text, timestamp, relationship } from '@keystone-6/core/fields';
const lists = {
Post: list({
fields: {
title: text(),
publishedAt: timestamp(),
author: relationship({
}),
},
}),
};

Add a published status

Keystone’s select field gives editors the ability to choose a value from a predetermined set of options. Let’s use this to capture our post's publish status.

To set the the field’s desired values we add options to the field’s configuration. They will be the only options available to editors in Admin UI and through Keystone’s auto-generated GraphQL types:

import { list, config } from '@keystone-6/core';
import { text, relationship, timestamp, select } from '@keystone-6/core/fields';
const lists = {
Post: list({
fields: {
title: text(),
publishedAt: timestamp(),
author: relationship({
}),
status: select({
options: [
{ label: 'Published', value: 'published' },
{ label: 'Draft', value: 'draft' },
],
}),
},
}),
};

Let’s take a look at how everything comes together in Admin UI:

Adding a date-time value and sellecting a publsh status in Admin UI

These defaults give us all the basic functionality we need, but there's room for improvement in the select field:

  • It would be helpful to present all the status options in a way that doesn't make editors click on the input to find out what options are available
  • Setting a default value for new posts could reduce the likelihood of editors accidentally publishing their work before it's done

Set a default value

Adding a defaultValue to the select field’s config will ensure that newly created posts start out with a preferred status. Let's set the default to draft:

defaultValue: 'draft',

Implement a segmented control input

When a select field only has a few values to choose from, changing the UI's displayMode from the default select to segmented-control will expose all those values in the interface so editor's don't have to click the input in order to know which value to choose. Our limited set of draft/published options is a perfect fit for this!

ui: { displayMode: 'segmented-control' },

This now gives us:

const lists = {
Post: list({
status: select({
options: [
{ label: 'Published', value: 'published' },
{ label: 'Draft', value: 'draft' },
],
defaultValue: 'draft',
ui: { displayMode: 'segmented-control' },
}),
author: relationship({ ref: 'User.posts' }),
},
}),
};

Let's put it all together to see the changes:

Post list type showing "status" select input as segmented control type with default value of "draft"

Protip: To take the editing experience even farther you can use hooks to automatically update publishedAt when a post’s status moves from draft to published.

Looking at the GraphQL API

We can now query these values using Keystone's GraphQL API playground to show all the published posts

query getAllPosts {
posts {
title
}
}

And/or posts that have a published status, and were published after a certain date:

query getPublishedPosts {
posts(where: { status: { equals: "published" } }) {
title
}
}

What we have now

We’ve successfully added two new fields to our post type that give us the information our system will need to display posts:

// keystone.ts
import { list, config } from '@keystone-6/core';
import { text, timestamp, select, relationship } from '@keystone-6/core/fields';
const lists = {
User: list({
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),
posts: relationship({ ref: 'Post.author', many: true }),
},
}),
Post: list({
fields: {
title: text(),
publishedAt: timestamp(),
status: select({
options: [
{ label: 'Published', value: 'published' },
{ label: 'Draft', value: 'draft' },
],
defaultValue: 'draft',
ui: { displayMode: 'segmented-control' },
}),
author: relationship({ ref: 'User.posts' }),
},
}),
};
export default config({
db: {
provider: 'sqlite',
url: 'file:./keystone.db',
},
lists,
});

Next lesson