DEV Community

Cover image for Payload 3: Build Custom Component for ListView Header
Aaron K Saunders
Aaron K Saunders

Posted on

1 1 1

Payload 3: Build Custom Component for ListView Header

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.

screenshot of header custom component

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'],
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

A couple of interesting things going on here, first is the use of the generic

groupUsersByTeam<T extends { team?: string | null }>(users: T[])
Enter fullscreen mode Exit fullscreen mode

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]')
Enter fullscreen mode Exit fullscreen mode

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>
  )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'],
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

SOURCE CODE

Top comments (0)

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay