Files
payload/test/admin/collections/Posts.ts
Jacob Fletcher ac1e3cf69e feat(ui): form state queues (#11579)
Implements a form state task queue. This will prevent onChange handlers
within the form component from processing unnecessarily often, sometimes
long after the user has stopped making changes. This leads to a
potentially huge number of network requests if those changes were made
slower than the debounce rate. This is especially noticeable on slow
networks.

Does so through a new `useQueue` hook. This hook maintains a stack of
events that need processing but only processes the final event to
arrive. Every time a new event is pushed to the stack, the currently
running process is aborted (if any), and that event becomes the next in
the queue. This results in a shocking reduction in the time it takes
between final change to form state and the final network response, from
~1.5 minutes to ~3 seconds (depending on the scenario, see below).

This likely fixes a number of existing open issues. I will link those
issues here once they are identified and verifiably fixed.

Before:

I'm typing slowly here to ensure my changes aren't debounce by the form.
There are a total of 60 characters typed, triggering 58 network requests
and taking around 1.5 minutes to complete after the final change was
made.


https://github.com/user-attachments/assets/49ba0790-a8f8-4390-8421-87453ff8b650

After:

Here there are a total of 69 characters typed, triggering 11 network
requests and taking only about 3 seconds to complete after the final
change was made.


https://github.com/user-attachments/assets/447f8303-0957-41bd-bb2d-9e1151ed9ec3
2025-03-10 21:25:14 -04:00

275 lines
6.2 KiB
TypeScript

import type { CollectionConfig } from 'payload'
import { slateEditor } from '@payloadcms/richtext-slate'
import { customTabAdminDescription, slugPluralLabel, slugSingularLabel } from '../shared.js'
import { postsCollectionSlug, uploadCollectionSlug } from '../slugs.js'
export const Posts: CollectionConfig = {
slug: postsCollectionSlug,
admin: {
defaultColumns: ['id', 'number', 'title', 'description', 'demoUIField'],
description: 'This is a custom collection description.',
group: 'One',
listSearchableFields: ['id', 'title', 'description', 'number'],
components: {
beforeListTable: [
'/components/ResetColumns/index.js#ResetDefaultColumnsButton',
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'BeforeListTable custom component',
},
},
],
Description: {
path: '/components/ViewDescription/index.js#ViewDescription',
},
afterListTable: [
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'AfterListTable custom component',
},
},
],
listMenuItems: [
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'listMenuItems',
},
},
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'Many of them',
},
},
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'Ok last one',
},
},
],
afterList: [
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'AfterList custom component',
},
},
],
beforeList: [
{
path: '/components/Banner/index.js#Banner',
clientProps: {
message: 'BeforeList custom component',
},
},
],
},
pagination: {
defaultLimit: 5,
limits: [5, 10, 15],
},
meta: {
description: 'This is a custom meta description for posts',
openGraph: {
description: 'This is a custom OG description for posts',
title: 'This is a custom OG title for posts',
},
},
preview: () => 'https://payloadcms.com',
useAsTitle: 'title',
},
fields: [
{
type: 'tabs',
tabs: [
{
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
{
name: 'number',
type: 'number',
},
{
name: 'richText',
type: 'richText',
editor: slateEditor({
admin: {
elements: ['relationship'],
},
}),
},
{
name: 'demoUIField',
type: 'ui',
admin: {
components: {
Cell: '/components/DemoUIField/Cell.js#DemoUIFieldCell',
Field: '/components/DemoUIField/Field.js#DemoUIField',
},
},
label: 'Demo UI Field',
},
],
label: 'Tab 1',
admin: {
description: customTabAdminDescription,
},
},
{
label: 'Tab 2',
fields: [],
admin: {
description: () => `t:${customTabAdminDescription}`,
},
},
],
},
{
name: 'arrayOfFields',
type: 'array',
admin: {
initCollapsed: true,
},
fields: [
{
name: 'optional',
type: 'text',
},
{
name: 'innerArrayOfFields',
type: 'array',
fields: [
{
name: 'innerOptional',
type: 'text',
},
],
},
],
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'defaultValueField',
type: 'text',
defaultValue: 'testing',
},
{
name: 'title',
type: 'text',
},
],
},
{
name: 'someBlock',
type: 'blocks',
blocks: [
{
slug: 'textBlock',
fields: [
{
name: 'textFieldForBlock',
type: 'text',
},
],
},
],
},
{
name: 'defaultValueField',
type: 'text',
defaultValue: 'testing',
},
{
name: 'relationship',
type: 'relationship',
admin: {
position: 'sidebar',
},
relationTo: 'posts',
},
{
name: 'users',
type: 'relationship',
admin: {
position: 'sidebar',
},
relationTo: 'users',
},
{
name: 'customCell',
type: 'text',
admin: {
components: {
Cell: '/components/CustomCell/index.js#CustomCell',
},
},
},
{
name: 'upload',
type: 'upload',
relationTo: uploadCollectionSlug,
},
{
name: 'hiddenField',
type: 'text',
hidden: true,
},
{
name: 'adminHiddenField',
type: 'text',
admin: {
hidden: true,
},
},
{
name: 'disableListColumnText',
type: 'text',
admin: {
disableListColumn: true,
},
},
{
name: 'disableListFilterText',
type: 'text',
admin: {
disableListFilter: true,
},
},
{
name: 'sidebarField',
type: 'text',
access: {
update: () => false,
},
admin: {
description:
'This is a very long description that takes many characters to complete and hopefully will wrap instead of push the sidebar open, lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum voluptates. Quisquam, voluptatum voluptates.',
position: 'sidebar',
},
},
],
labels: {
plural: slugPluralLabel,
singular: slugSingularLabel,
},
versions: {
drafts: true,
},
}