Skip to content

feat: hierarchy with ui#15769

Merged
JarrodMFlesch merged 400 commits intomainfrom
folder-field-column-drawer
May 5, 2026
Merged

feat: hierarchy with ui#15769
JarrodMFlesch merged 400 commits intomainfrom
folder-field-column-drawer

Conversation

@JarrodMFlesch
Copy link
Copy Markdown
Contributor

@JarrodMFlesch JarrodMFlesch commented Feb 26, 2026

Hierarchy Feature

This PR introduces a comprehensive hierarchy system for Payload that enables collections to have parent-child relationships with automatic path generation, dedicated sidebar navigation, and specialized UI components for folder and tag patterns.

Overview

The hierarchy feature allows any collection to define parent-child relationships through a declarative hierarchy config property. When enabled, Payload automatically handles relationship management, path computation, circular reference prevention, and injects a dedicated sidebar tab with a tree view for navigation.

At its core, hierarchy manages a parentFieldName relationship field that references documents within the same collection. The system automatically creates this field if it doesn't exist, adds validation to prevent circular references (you can't move a folder into its own subfolder), and computes virtual path fields that provide breadcrumb-style paths from root to each document.

Path Generation

Two virtual fields are automatically added to hierarchy-enabled collections: _h_slugPath and _h_titlePath. These compute breadcrumb paths by walking up the parent chain to root during read operations. For example, a document nested three levels deep might have paths like engineering/frontend/components and Engineering / Frontend / Components. Path computation is cached per-request to avoid redundant ancestor queries, and uses overrideAccess: true to ensure complete paths even when users lack read permission on intermediate ancestors.

The field names are customizable via slugPathFieldName and titlePathFieldName in the hierarchy config. Path generation also respects localization, returning localized strings when the collection uses localized title fields.

Sidebar Tabs

The hierarchy feature builds on a new sidebar tabs system that allows rendering custom tabs alongside the default Collections tab. Each hierarchy collection automatically gets its own tab injected during config resolution. The tab displays a tree view of the hierarchy with expand/collapse functionality, search, and optional collection-type filtering.

When you click a node in the tree, the list view filters to show only that node's children and related documents. The URL updates with a ?parent=<id> parameter, and the tree highlights the currently selected node. Expanded node state persists across sessions via payload-preferences.

Folder and Tag Presets

Instead of wrapping your collection config in a HOC function, you now declare the preset directly on the collection config using the folders or tags property.

folders preset configures a folder-style hierarchy where each document can have only one parent (single-select). It enforces allowHasMany: false, applies a default folder icon, and enables the miller columns header button by default. The collection is hidden from the main nav since it's accessed via its sidebar tab.

tags preset configures a tag-style hierarchy where documents can have multiple parents (multi-select by default). This is useful for categorization systems where items can belong to multiple categories.

Both presets accept true for defaults, or a config object for customization.

createFolderField creates a relationship field for assigning a single folder to documents in other collections. The field renders as a header button that opens a miller columns drawer for folder selection instead of the standard relationship dropdown.

createTagField creates a relationship field for assigning multiple tags to documents. Unlike folder fields, tag fields use the standard relationship UI with hierarchy-aware features.

Setup

To add hierarchy to a collection, add the hierarchy property to your collection config:

const Categories: CollectionConfig = {
  slug: 'categories',
  admin: { useAsTitle: 'name' },
  fields: [{ name: 'name', type: 'text', required: true }],
  hierarchy: {
    parentFieldName: 'parent',
  },
}

For folder patterns, use the folders preset directly on the collection config:

import { createFolderField } from 'payload'

const Folders: CollectionConfig = {
  slug: 'folders',
  admin: { useAsTitle: 'name' },
  fields: [{ name: 'name', type: 'text', required: true }],
  folders: true, // or folders: { parentFieldName: 'folder', ... }
}

const Posts: CollectionConfig = {
  slug: 'posts',
  fields: [
    { name: 'title', type: 'text', required: true },
    createFolderField({ relationTo: 'folders' }),
  ],
}

For tag patterns, use the tags preset:

import { createTagField } from 'payload'

const Tags: CollectionConfig = {
  slug: 'tags',
  admin: { useAsTitle: 'name' },
  fields: [{ name: 'name', type: 'text', required: true }],
  tags: true, // or tags: { allowHasMany: true, parentFieldName: 'parent', ... }
}

const Posts: CollectionConfig = {
  slug: 'posts',
  fields: [
    { name: 'title', type: 'text', required: true },
    createTagField({ relationTo: 'tags' }),
  ],
}

Collection-Specific Folders

Folders can optionally restrict which collection types they accept. Enable this with collectionSpecific:

const Folders: CollectionConfig = {
  slug: 'folders',
  admin: { useAsTitle: 'name' },
  fields: [{ name: 'name', type: 'text', required: true }],
  folders: {
    collectionSpecific: true, // or { fieldName: 'allowedTypes' }
  },
}

This adds a multi-select field to folders where you can specify which collections can be placed in that folder. When filtering the sidebar tree, only folders that accept the current collection type are shown.

Join Field

For querying all children of a hierarchy item (both nested items and related documents from other collections), configure the joinField option:

folders: {
  parentFieldName: 'parent',
  joinField: { name: 'children' },
}

This creates a virtual join field that aggregates all documents referencing each hierarchy item as their parent.

⚠️ Migration from Previous Folders Implementation

If you previously used folders, migration to the new hierarchy system is straightforward, but you now need to define your folders collection explicitly (it is no longer created for you).

To preserve existing folders data, keep the collection slug as payload-folders and set parentFieldName to folder:

import { createFolderField } from 'payload'
import type { CollectionConfig } from 'payload'

const Folders: CollectionConfig = {
  slug: 'payload-folders', // important if migrating
  admin: { useAsTitle: 'name' },
  fields: [{ name: 'name', type: 'text', required: true }],
  folders: {
    parentFieldName: 'folder', // important if migrating
    collectionSpecific: { fieldName: 'folderType' }, // if you were using this before
  },
}

// In collections that should be assignable to folders:
const Posts: CollectionConfig = {
  slug: 'posts',
  fields: [
    { name: 'title', type: 'text', required: true },
    createFolderField({ relationTo: 'payload-folders' }),
  ],
}

Current UI

CleanShot.2026-03-17.at.08.26.22.mp4

JarrodMFlesch and others added 30 commits February 17, 2026 11:06
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Delete old folder-specific UI that's being replaced by unified taxonomy tree.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Delete old folder-specific backend utils and Next.js views.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add sanitizeFolder, validateFolderFields, getAncestors, and FolderSidebarTab.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Align folders with taxonomy system architecture.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update types and sanitization for folder/taxonomy alignment.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update Tree, TaxonomyTree, providers, and exports.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update views, exports, and server functions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update folder/taxonomy tests and minor config files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add new properties to HierarchyConfig and SanitizedHierarchyConfig:
- allowHasMany: for folder (single) vs tag (multiple) behavior
- collectionSpecific: for scoping to specific collections
- admin.components.Icon: custom icon component
- admin.treeLimit: max tree depth
- relatedCollections: auto-populated during validation
Add defaults for allowHasMany, collectionSpecific, admin.treeLimit,
admin.components.Icon, and slugify in sanitizeHierarchy.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Creates relationship field with hasMany:false, admin.hidden:true,
and custom.hierarchy.injectHeaderButton marker for UI injection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@JarrodMFlesch JarrodMFlesch enabled auto-merge (squash) May 5, 2026 16:06
PatrikKozak
PatrikKozak previously approved these changes May 5, 2026
PatrikKozak
PatrikKozak previously approved these changes May 5, 2026
@JarrodMFlesch JarrodMFlesch merged commit 2d77a3e into main May 5, 2026
324 of 326 checks passed
@JarrodMFlesch JarrodMFlesch deleted the folder-field-column-drawer branch May 5, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants