Overview
Payload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.
This is a tutorial using a payload custom component in the admin list view of a collection to summarize the collection data and create shortcut buttons for filtering the data.
Video
This blog post can be used as a companion to the related video.
User Collection
For this project I create a Payload 3 blank application using sqlite as my database to make the configuration easier.
After creating the database, I modified the User
collection to include additional fields that I would be filtering on in the header.
The new fields are team
and role
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
// Email added by default
// Add more fields as needed
{
name: 'team',
type: 'text',
},
{
name: 'role',
type: 'select',
options: ['admin', 'user'],
},
],
}
Custom Component
In order to create the custom buttons for filtering by team
, I need the following
- List of all of the teams in the collection
- Number of users in each of the team
This can be done in two ways in the custom component, you can search for the information using local api or you cna drill down and access the underlying database, which in my case was postgres database through Payloads DrizzleORM integration.
This is the code for querying using local api
async function getPayloadUsers(payload: Payload) {
const result = await payload.find({
collection: 'users',
limit: 1000,
depth: 0,
})
return groupUsersByTeam(result.docs)
}
And this is the code using the drizzle approach
Note when using the drizzle approach, you must properly generate the schema using this command
npx payload generate:db-schema
async function getDrizzleUsers(payload: Payload) {
const users = await payload.db.drizzle.query.users.findMany()
return groupUsersByTeam(users)
}
The next part is to get the count and group by team
function groupUsersByTeam<T extends { team?: string | null }>(users: T[]) {
const usersByTeam = users.reduce(
(acc, user) => {
const team = user.team || 'No Team'
if (!acc[team]) {
acc[team] = []
}
acc[team].push(user)
return acc
},
{} as Record<string, T[]>,
)
return {
users,
usersByTeam,
}
}
A couple of interesting things going on here, first is the use of the generic
groupUsersByTeam<T extends { team?: string | null }>(users: T[])
This basically says except any type, but it must have a property called team
. We need this because the structure of the data from the two calls will be different. The local api call will be structured like the results from the API query on the user
collection, which the result from the drizzle call will be structured like the database schema.
Use Information to Create UI With Buttons
We want to highlight the currently selected filter so we use this code to derive that information from the url of the page. This code will get us the name of thecurrentTeam
in the url.
// Get current team filter from URL
const headersList = await headers()
const fullUrl = headersList.get('referer') || ''
const currentTeam = new URLSearchParams(fullUrl.split('?')[1] || '').get('where[team][equals]')
Here is how we create the list of buttons based on the results of the function to get and group users, adn we use the currentTeam
property from the code above to properly style the active filter.
// Using Drizzle in this example, but you can switch to getPayloadUsers
const { users, usersByTeam } = await getDrizzleUsers(payload);
return (
<div style={{ margin: 64, border: '1px solid gray', padding: 16 }}>
<h3>Teams:</h3>
<div >
{/* ALL BUTTON, resets filter */}
<Link
href="?"
style={{
...buttonStyle,
fontWeight: !currentTeam ? 'bold' : 'normal',
}}
>
All ({users.length})
</Link>
{/* Loop through data to get team info */}
{Object.entries(usersByTeam).map(([team, users]) => (
<Link
key={team}
href={`?where[team][equals]=${encodeURIComponent(team === 'No Team' ? '' : team)}`}
style={{
...buttonStyle,
fontWeight: currentTeam === (team === 'No Team' ? '' : team) ? 'bold' : 'normal',
}}
>
{team} ({users.length})
</Link>
))}
</div>
</div>
)
we construct the url from the Link
component using Payload query format and the team
value from the loop.
This is the complete code for the custom component
// src/components/Users/BeforeList.tsx
import { Payload } from 'payload'
import React from 'react'
import Link from 'next/link'
import { headers } from 'next/headers'
// Method 1: Using Drizzle directly
async function getDrizzleUsers(payload: Payload) {
const users = await payload.db.drizzle.query.users.findMany()
return groupUsersByTeam(users)
}
// Method 2: Using Payload's local API
async function getPayloadUsers(payload: Payload) {
const result = await payload.find({
collection: 'users',
limit: 1000,
depth: 0,
})
return groupUsersByTeam(result.docs)
}
// Helper function to group users by team
// This is a generic function that can be used with any type of user object
// as long as it has a team property
function groupUsersByTeam<T extends { team?: string | null }>(users: T[]) {
const usersByTeam = users.reduce(
(acc, user) => {
const team = user.team || 'No Team'
if (!acc[team]) {
acc[team] = []
}
acc[team].push(user)
return acc
},
{} as Record<string, T[]>,
)
return {
users,
usersByTeam,
}
}
const BeforeList: React.FC<{ payload: Payload }> = async ({ payload, ...rest }) => {
// Using Drizzle in this example, but you can switch to getPayloadUsers
const { users, usersByTeam } = await getDrizzleUsers(payload)
// Get current team filter from URL
const headersList = await headers()
const fullUrl = headersList.get('referer') || ''
const currentTeam = new URLSearchParams(fullUrl.split('?')[1] || '').get('where[team][equals]')
const buttonStyle = {
padding: '8px 16px',
borderRadius: '4px',
border: '1px solid #ccc',
cursor: 'pointer',
textDecoration: 'none',
backgroundColor: '#fff',
color: '#000',
}
return (
<div style={{ margin: 64, border: '1px solid gray', padding: 16 }}>
<h3>Teams:</h3>
<div
style={{
display: 'flex',
gap: '8px',
flexWrap: 'wrap',
marginBottom: '16px',
marginTop: '16px',
}}
>
<Link
href="?"
style={{
...buttonStyle,
fontWeight: !currentTeam ? 'bold' : 'normal',
}}
>
All ({users.length})
</Link>
{Object.entries(usersByTeam).map(([team, users]) => (
<Link
key={team}
href={`?where[team][equals]=${encodeURIComponent(team === 'No Team' ? '' : team)}`}
style={{
...buttonStyle,
fontWeight: currentTeam === (team === 'No Team' ? '' : team) ? 'bold' : 'normal',
}}
>
{team} ({users.length})
</Link>
))}
</div>
</div>
)
}
export default BeforeList
Adding Custom Component to Collection
Now in the collection we update the admin
section of the file to include the path
of the Custom Component.
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
// NEW CODE BELOW
components: {
beforeListTable: [
{
path: 'src/components/Users/BeforeList.tsx',
},
],
},
},
auth: true,
fields: [
// Email added by default
// Add more fields as needed
{
name: 'team',
type: 'text',
},
{
name: 'role',
type: 'select',
options: ['admin', 'user'],
},
],
}
Top comments (0)