Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b054921
data wip
JarrodMFlesch Sep 12, 2025
6024ff6
simplified-ish logic
JarrodMFlesch Sep 13, 2025
378e501
more cleaning
JarrodMFlesch Sep 15, 2025
60ccddc
small fix
JarrodMFlesch Sep 15, 2025
3fcd316
rename hierarchy to tree view
JarrodMFlesch Sep 15, 2025
8cecdd5
add test suite
JarrodMFlesch Sep 16, 2025
2123e9c
Merge branch 'main' into feat/hierarchy
JarrodMFlesch Sep 25, 2025
dacc9fb
rendering placeholder, updated drag logic
JarrodMFlesch Oct 2, 2025
7787029
working dnd hover states
JarrodMFlesch Oct 3, 2025
52c5256
cleanup
JarrodMFlesch Oct 3, 2025
897963f
add types
JarrodMFlesch Oct 3, 2025
7f12449
simplifications
JarrodMFlesch Oct 3, 2025
f6d2b95
updates
JarrodMFlesch Oct 7, 2025
a8863f6
move draggable with click
JarrodMFlesch Oct 8, 2025
c898fb0
working drag placeholders
JarrodMFlesch Oct 8, 2025
c60bd17
fix rendering
JarrodMFlesch Oct 8, 2025
6add9bf
rename ComponentToRender to TableComponent
JarrodMFlesch Oct 8, 2025
ef1a8f4
chore: optimize DraggableWithClick pointer event handling
JarrodMFlesch Oct 8, 2025
117bf38
chore: working dropareas after drop
JarrodMFlesch Oct 8, 2025
6888ec7
style updates, ui logic refactor
JarrodMFlesch Oct 9, 2025
1107f9b
change documents verbiage to items
JarrodMFlesch Oct 9, 2025
915f659
update loading state for rows
JarrodMFlesch Oct 9, 2025
b94dc77
fix loading sticking around
JarrodMFlesch Oct 9, 2025
80f8bae
fix: expand items from prefs
JarrodMFlesch Oct 9, 2025
be2711b
update styles for groupd of selected rows
JarrodMFlesch Oct 9, 2025
4520bbd
update row selection css
JarrodMFlesch Oct 9, 2025
d870daa
add non-selected disabled item styles
JarrodMFlesch Oct 9, 2025
2dc7c2a
simplifies invalidTargetIDs
JarrodMFlesch Oct 9, 2025
3bce086
auto-select child elements in table when parent selected
JarrodMFlesch Oct 9, 2025
a06d83e
seed
JarrodMFlesch Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions packages/next/src/views/CollectionTreeView/buildView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type {
AdminViewServerProps,
BuildCollectionFolderViewResult,
ListQuery,
TreeViewClientProps,
} from 'payload'

import { DefaultCollectionTreeView, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { getTreeViewResultsComponentAndData, upsertPreferences } from '@payloadcms/ui/rsc'

import { getPreferences } from '../../utilities/getPreferences.js'

export type BuildCollectionTreeViewStateArgs = {
disableBulkDelete?: boolean
disableBulkEdit?: boolean
enableRowSelections: boolean
isInDrawer?: boolean
overrideEntityVisibility?: boolean
query: ListQuery
} & AdminViewServerProps

export const buildCollectionTreeView = async (
args: BuildCollectionTreeViewStateArgs,
): Promise<BuildCollectionFolderViewResult> => {
const {
disableBulkDelete,
disableBulkEdit,
enableRowSelections,
initPageResult,
overrideEntityVisibility,
params,
query: queryFromArgs,
searchParams,
} = args

const {
collectionConfig,
collectionConfig: { slug: collectionSlug },
locale: fullLocale,
permissions,
req: {
i18n,
payload,
payload: { config },
query: queryFromReq,
user,
},
visibleEntities,
} = initPageResult

if (!config.treeView) {
throw new Error('not-found')
}

if (!permissions?.collections?.[collectionSlug]?.read) {
throw new Error('not-found')
}

if (collectionConfig) {
if (!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) {
throw new Error('not-found')
}

const query = queryFromArgs || queryFromReq

const preferences = await getPreferences<{
expandedIDs: (number | string)[]
// sort: SortPreference
}>(`collection-${collectionSlug}-treeView`, payload, user.id, payload.config.admin.user)
const { items, TreeViewComponent } = await getTreeViewResultsComponentAndData({
collectionSlug,
expandedItemIDs: preferences?.value.expandedIDs || [],
req: initPageResult.req,
sort: 'titlePath',
// sort: sortPreference,
// search,
})

const serverProps: any = {
collectionConfig,
i18n,
locale: fullLocale,
params,
payload,
permissions,
searchParams,
user,
}

// We could support slots in the folder view in the future
// const folderViewSlots = renderFolderViewSlots({
// clientProps: {
// collectionSlug,
// hasCreatePermission,
// newDocumentURL,
// },
// collectionConfig,
// description: typeof collectionConfig.admin.description === 'function'
// ? collectionConfig.admin.description({ t: i18n.t })
// : collectionConfig.admin.description,
// payload,
// serverProps,
// })

const search = query?.search as string

return {
View: (
<>
<HydrateAuthProvider permissions={permissions} />
{RenderServerComponent({
clientProps: {
// ...folderViewSlots,
breadcrumbs: [],
collectionSlug,
disableBulkDelete,
disableBulkEdit,
enableRowSelections,
expandedItemIDs: preferences?.value.expandedIDs || [],
items,
parentFieldName: '_parentDoc',
search,
TreeViewComponent,
// sort: sortPreference,
} satisfies TreeViewClientProps,
// Component: collectionConfig?.admin?.components?.views?.TreeView?.Component,
Fallback: DefaultCollectionTreeView,
importMap: payload.importMap,
serverProps,
})}
</>
),
}
}

throw new Error('not-found')
}
23 changes: 23 additions & 0 deletions packages/next/src/views/CollectionTreeView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type React from 'react'

import { notFound } from 'next/navigation.js'

import type { BuildCollectionTreeViewStateArgs } from './buildView.js'

import { buildCollectionTreeView } from './buildView.js'

export const CollectionTreeView: React.FC<BuildCollectionTreeViewStateArgs> = async (args) => {
try {
const { View } = await buildCollectionTreeView(args)
return View
} catch (error) {
if (error?.message === 'NEXT_REDIRECT') {
throw error
}
if (error.message === 'not-found') {
notFound()
} else {
console.error(error) // eslint-disable-line no-console
}
}
}
4 changes: 3 additions & 1 deletion packages/next/src/views/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ListQuery,
ListViewClientProps,
ListViewServerPropsOnly,
ListViewTypes,
PaginatedDocs,
QueryPreset,
SanitizedCollectionPermission,
Expand Down Expand Up @@ -47,7 +48,8 @@ type RenderListViewArgs = {
* @experimental This prop is subject to change in future releases.
*/
trash?: boolean
} & AdminViewServerProps
viewType: ListViewTypes
} & Omit<AdminViewServerProps, 'viewType'>

/**
* This function is responsible for rendering
Expand Down
31 changes: 30 additions & 1 deletion packages/next/src/views/Root/getRouteData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Account } from '../Account/index.js'
import { BrowseByFolder } from '../BrowseByFolder/index.js'
import { CollectionFolderView } from '../CollectionFolders/index.js'
import { TrashView } from '../CollectionTrash/index.js'
import { CollectionTreeView } from '../CollectionTreeView/index.js'
import { CreateFirstUserView } from '../CreateFirstUser/index.js'
import { Dashboard } from '../Dashboard/index.js'
import { Document as DocumentView } from '../Document/index.js'
Expand Down Expand Up @@ -225,8 +226,10 @@ export const getRouteData = ({
routeParams.collection = collectionConfig.slug

if (
config.folders &&
collectionConfig.folders &&
collectionPreferences?.listViewType &&
collectionPreferences.listViewType === 'folders'
collectionPreferences.listViewType === 'collection-folders'
) {
// Render folder view by default if set in preferences
ViewToRender = {
Expand All @@ -236,6 +239,20 @@ export const getRouteData = ({
templateClassName = `collection-folders`
templateType = 'default'
viewType = 'collection-folders'
} else if (
config.treeView &&
collectionConfig.treeView &&
collectionPreferences?.listViewType &&
collectionPreferences.listViewType === 'collection-tree-view'
) {
// Render tree view by default if set in preferences
ViewToRender = {
Component: CollectionTreeView,
}

templateClassName = `collection-tree-view`
templateType = 'default'
viewType = 'collection-tree-view'
} else {
ViewToRender = {
Component: ListView,
Expand Down Expand Up @@ -338,6 +355,18 @@ export const getRouteData = ({
templateType = 'default'
viewType = 'collection-folders'

viewActions.push(...(collectionConfig.admin.components?.views?.list?.actions || []))
} else if (config.treeView && segmentThree === 'tree-view' && collectionConfig.treeView) {
// Collection Tree View
// --> /collections/:collectionSlug/tree-view
ViewToRender = {
Component: CollectionTreeView,
}

templateClassName = `collection-tree-view`
templateType = 'default'
viewType = 'collection-tree-view'

viewActions.push(...(collectionConfig.admin.components?.views?.list?.actions || []))
} else {
// Collection Edit Views
Expand Down
10 changes: 9 additions & 1 deletion packages/next/src/views/Root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,15 @@ export const RootPage = async ({
let collectionPreferences: CollectionPreferences = undefined

if (collectionConfig && segments.length === 2) {
if (config.folders && collectionConfig.folders && segments[1] !== config.folders.slug) {
const isFolderViewEnabled =
config.folders && collectionConfig.folders && segments[1] !== config.folders.slug
const isTreeViewEnabled = collectionConfig.treeView && config.treeView

/**
* Fetch collection level preferences, used for:
* - getting default list view tab (list | folder | tree-view)
*/
if (isFolderViewEnabled || isTreeViewEnabled) {
await getPreferences<CollectionPreferences>(
`collection-${collectionConfig.slug}`,
req.payload,
Expand Down
24 changes: 24 additions & 0 deletions packages/payload/src/admin/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,27 @@ export type GetFolderResultsComponentAndDataArgs = {
*/
sort: FolderSortKeys
}

export type BuildCollectionTreeViewResult = {
View: React.ReactNode
}

export type GetTreeViewResultsComponentAndDataArgs = {
/**
* The slug of the collection to get tree view data for.
* i.e. 'posts'
*/
collectionSlug: CollectionSlug
/**
* An array of item IDs that are currently expanded.
* This is used to determine which items to fetch from the database.
* i.e. ['123', '456']
*/
expandedItemIDs?: (number | string)[]
// search?: string
req: PayloadRequest
/**
* The sort order for the results.
*/
sort: any
}
22 changes: 22 additions & 0 deletions packages/payload/src/admin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ export type {
BuildTableStateArgs,
DefaultServerFunctionArgs,
GetFolderResultsComponentAndDataArgs,
GetTreeViewResultsComponentAndDataArgs,
ListQuery,
ServerFunction,
ServerFunctionArgs,
Expand Down Expand Up @@ -666,6 +667,7 @@ export type {
AdminViewServerProps,
AdminViewServerPropsOnly,
InitPageResult,
ListViewTypes,
ServerPropsFromView,
ViewDescriptionClientProps,
ViewDescriptionServerProps,
Expand Down Expand Up @@ -694,6 +696,26 @@ export type {
ListViewSlotSharedClientProps,
} from './views/list.js'

export type {
AfterTreeViewListClientProps,
AfterTreeViewListServerProps,
AfterTreeViewListServerPropsOnly,
AfterTreeViewListTableClientProps,
AfterTreeViewListTableServerProps,
AfterTreeViewListTableServerPropsOnly,
BeforeTreeViewListClientProps,
BeforeTreeViewListServerProps,
BeforeTreeViewListServerPropsOnly,
BeforeTreeViewListTableClientProps,
BeforeTreeViewListTableServerProps,
BeforeTreeViewListTableServerPropsOnly,
TreeViewClientProps,
TreeViewServerProps,
TreeViewServerPropsOnly,
TreeViewSlots,
TreeViewSlotSharedClientProps,
} from './views/treeViewList.js'

type SchemaPath = {} & string
export type FieldSchemaMap = Map<
SchemaPath,
Expand Down
6 changes: 6 additions & 0 deletions packages/payload/src/admin/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export type InitPageResult = {
export type ViewTypes =
| 'account'
| 'collection-folders'
| 'collection-tree-view'
| 'createFirstUser'
| 'dashboard'
| 'document'
Expand All @@ -101,6 +102,11 @@ export type ViewTypes =
| 'verify'
| 'version'

export type ListViewTypes = Extract<
ViewTypes,
'collection-folders' | 'collection-tree-view' | 'folders' | 'list' | 'trash'
>

export type ServerPropsFromView = {
collectionConfig?: SanitizedConfig['collections'][number]
globalConfig?: SanitizedConfig['globals'][number]
Expand Down
4 changes: 2 additions & 2 deletions packages/payload/src/admin/views/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { CollectionPreferences } from '../../preferences/types.js'
import type { QueryPreset } from '../../query-presets/types.js'
import type { ResolvedFilterOptions } from '../../types/index.js'
import type { Column } from '../elements/Table.js'
import type { Data, ViewTypes } from '../types.js'
import type { Data, ListViewTypes, ViewTypes } from '../types.js'

export type ListViewSlots = {
AfterList?: React.ReactNode
Expand Down Expand Up @@ -59,7 +59,7 @@ export type ListViewClientProps = {
queryPresetPermissions?: SanitizedCollectionPermission
renderedFilters?: Map<string, React.ReactNode>
resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
viewType: ViewTypes
viewType: ListViewTypes
} & ListViewSlots

export type ListViewSlotSharedClientProps = {
Expand Down
Loading
Loading