docs/content/docs/walkthroughs/lesson-3.md
Learn how to create a publishing workflow to your app using Keystons’s select and timestamp fields.
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 { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
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',
prismaClientOptions: () => ({
adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }),
}),
},
lists,
})
We’re now going to extend our Keystone schema to support publishing needs.
Our publishing workflow needs the ability to reflect:
These will give us what we need to conditionally display and order posts in a frontend app.
Keystone’s timestamp field will let editors associate a date and time with the post:
import { list, config } from '@keystone-6/core';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
import { text, timestamp, 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(),
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',
prismaClientOptions: () => ({
adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }),
}),
},
lists,
});
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 { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
import { text, relationship, timestamp, select } 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(),
author: relationship({
ref: 'User.posts',
ui: {
displayMode: 'cards',
cardFields: ['name', 'email'],
inlineEdit: { fields: ['name', 'email'] },
linkToItem: true,
inlineCreate: { fields: ['name', 'email'] },
},
}),
status: select({
options: [
{ label: 'Published', value: 'published' },
{ label: 'Draft', value: 'draft' },
],
}),
},
}),
};
export default config({
db: {
provider: 'sqlite',
prismaClientOptions: () => ({
adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }),
}),
},
lists,
});
Let’s take a look at how everything comes together in Admin UI:
These defaults give us all the basic functionality we need, but there's room for improvement in the select field:
status options in a way that doesn't make editors click on the input to find out what options are availableAdding 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',
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:
import { list, config } from '@keystone-6/core';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
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',
prismaClientOptions: () => ({
adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }),
}),
},
lists,
});
Let's put it all together to see the changes:
{% hint kind="tip" %}
Protip: You can use hooks to automatically update publishedAt when a post’s status moves from draft to published.
{% /hint %}
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
}
}
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 { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
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',
prismaClientOptions: () => ({
adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }),
}),
},
lists,
})
{% related-content %} {% well heading="Lesson 4: Auth & Sessions" grad="grad1" href="/docs/walkthroughs/lesson-4" target="" %} Add sessions, password protection, and a sign-in screen to your Keystone app {% /well %} {% /related-content %}