feat: optionally exclude collection documents from appearing in browse-by-folder (#12654)
Adds configurations for browse-by-folder document results. This PR **does NOT** allow for filtering out folders on a per collection basis. That will be addressed in a future PR 👍 ### Disable browse-by-folder all together ```ts type RootFoldersConfiguration = { /** * If true, the browse by folder view will be enabled * * @default true */ browseByFolder?: boolean // ...rest of type } ``` ### Remove document types from appearing in the browse by folder view ```ts type CollectionFoldersConfiguration = | boolean | { /** * If true, the collection documents will be included in the browse by folder view * * @default true */ browseByFolder?: boolean } ``` ### Misc Fixes https://github.com/payloadcms/payload/issues/12631 where adding folders.collectionOverrides was being set on the client config - it should be omitted. Fixes an issue where `baseListFilters` were not being respected.
This commit is contained in:
@@ -23,6 +23,12 @@ On the payload config, you can configure the following settings under the `folde
|
|||||||
// Type definition
|
// Type definition
|
||||||
|
|
||||||
type RootFoldersConfiguration = {
|
type RootFoldersConfiguration = {
|
||||||
|
/**
|
||||||
|
* If true, the browse by folder view will be enabled
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
browseByFolder?: boolean
|
||||||
/**
|
/**
|
||||||
* An array of functions to be ran when the folder collection is initialized
|
* An array of functions to be ran when the folder collection is initialized
|
||||||
* This allows plugins to modify the collection configuration
|
* This allows plugins to modify the collection configuration
|
||||||
@@ -82,7 +88,16 @@ To enable folders on a collection, you need to set the `admin.folders` property
|
|||||||
```ts
|
```ts
|
||||||
// Type definition
|
// Type definition
|
||||||
|
|
||||||
type CollectionFoldersConfiguration = boolean
|
type CollectionFoldersConfiguration =
|
||||||
|
| boolean
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* If true, the collection will be included in the browse by folder view
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
browseByFolder?: boolean
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -23,20 +23,11 @@ export const DefaultNavClient: React.FC<{
|
|||||||
admin: {
|
admin: {
|
||||||
routes: { browseByFolder: foldersRoute },
|
routes: { browseByFolder: foldersRoute },
|
||||||
},
|
},
|
||||||
collections,
|
folders,
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
},
|
},
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
const [folderCollectionSlugs] = React.useState<string[]>(() => {
|
|
||||||
return collections.reduce<string[]>((acc, collection) => {
|
|
||||||
if (collection.folders) {
|
|
||||||
acc.push(collection.slug)
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, [])
|
|
||||||
})
|
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
const folderURL = formatAdminURL({
|
const folderURL = formatAdminURL({
|
||||||
@@ -48,7 +39,7 @@ export const DefaultNavClient: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{folderCollectionSlugs.length > 0 && <BrowseByFolderButton active={viewingRootFolderView} />}
|
{folders && folders.browseByFolder && <BrowseByFolderButton active={viewingRootFolderView} />}
|
||||||
{groups.map(({ entities, label }, key) => {
|
{groups.map(({ entities, label }, key) => {
|
||||||
return (
|
return (
|
||||||
<NavGroup isOpen={navPreferences?.groups?.[label]?.open} key={key} label={label}>
|
<NavGroup isOpen={navPreferences?.groups?.[label]?.open} key={key} label={label}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
BuildCollectionFolderViewResult,
|
BuildCollectionFolderViewResult,
|
||||||
FolderListViewServerPropsOnly,
|
FolderListViewServerPropsOnly,
|
||||||
ListQuery,
|
ListQuery,
|
||||||
|
Where,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { DefaultBrowseByFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
import { DefaultBrowseByFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||||
@@ -10,6 +11,7 @@ import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerCompo
|
|||||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||||
import { redirect } from 'next/navigation.js'
|
import { redirect } from 'next/navigation.js'
|
||||||
import { getFolderData } from 'payload'
|
import { getFolderData } from 'payload'
|
||||||
|
import { buildFolderWhereConstraints } from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { getPreferences } from '../../utilities/getPreferences.js'
|
import { getPreferences } from '../../utilities/getPreferences.js'
|
||||||
@@ -29,10 +31,10 @@ export const buildBrowseByFolderView = async (
|
|||||||
args: BuildFolderViewArgs,
|
args: BuildFolderViewArgs,
|
||||||
): Promise<BuildCollectionFolderViewResult> => {
|
): Promise<BuildCollectionFolderViewResult> => {
|
||||||
const {
|
const {
|
||||||
|
browseByFolderSlugs: browseByFolderSlugsFromArgs = [],
|
||||||
disableBulkDelete,
|
disableBulkDelete,
|
||||||
disableBulkEdit,
|
disableBulkEdit,
|
||||||
enableRowSelections,
|
enableRowSelections,
|
||||||
folderCollectionSlugs,
|
|
||||||
folderID,
|
folderID,
|
||||||
initPageResult,
|
initPageResult,
|
||||||
isInDrawer,
|
isInDrawer,
|
||||||
@@ -54,30 +56,83 @@ export const buildBrowseByFolderView = async (
|
|||||||
visibleEntities,
|
visibleEntities,
|
||||||
} = initPageResult
|
} = initPageResult
|
||||||
|
|
||||||
const collections = folderCollectionSlugs.filter(
|
const browseByFolderSlugs = browseByFolderSlugsFromArgs.filter(
|
||||||
(collectionSlug) =>
|
(collectionSlug) =>
|
||||||
permissions?.collections?.[collectionSlug]?.read &&
|
permissions?.collections?.[collectionSlug]?.read &&
|
||||||
visibleEntities.collections.includes(collectionSlug),
|
visibleEntities.collections.includes(collectionSlug),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!collections.length) {
|
if (config.folders === false || config.folders.browseByFolder === false) {
|
||||||
throw new Error('not-found')
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = queryFromArgs || queryFromReq
|
const query = queryFromArgs || queryFromReq
|
||||||
const selectedCollectionSlugs: string[] =
|
const selectedCollectionSlugs: string[] =
|
||||||
Array.isArray(query?.relationTo) && query.relationTo.length
|
Array.isArray(query?.relationTo) && query.relationTo.length
|
||||||
? query.relationTo
|
? query.relationTo.filter(
|
||||||
: [...folderCollectionSlugs, config.folders.slug]
|
(slug) =>
|
||||||
|
browseByFolderSlugs.includes(slug) || (config.folders && slug === config.folders.slug),
|
||||||
|
)
|
||||||
|
: [...browseByFolderSlugs, config.folders.slug]
|
||||||
|
|
||||||
const {
|
const {
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
|
const folderCollectionConfig = payload.collections[config.folders.slug].config
|
||||||
|
|
||||||
|
const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>(
|
||||||
|
'browse-by-folder',
|
||||||
|
payload,
|
||||||
|
user.id,
|
||||||
|
user.collection,
|
||||||
|
)
|
||||||
|
|
||||||
|
let documentWhere: undefined | Where = undefined
|
||||||
|
let folderWhere: undefined | Where = undefined
|
||||||
|
// if folderID, dont make a documentWhere since it only queries root folders
|
||||||
|
for (const collectionSlug of selectedCollectionSlugs) {
|
||||||
|
if (collectionSlug === config.folders.slug) {
|
||||||
|
const folderCollectionConstraints = await buildFolderWhereConstraints({
|
||||||
|
collectionConfig: folderCollectionConfig,
|
||||||
|
folderID,
|
||||||
|
localeCode: fullLocale?.code,
|
||||||
|
req: initPageResult.req,
|
||||||
|
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (folderCollectionConstraints) {
|
||||||
|
folderWhere = folderCollectionConstraints
|
||||||
|
}
|
||||||
|
} else if (folderID) {
|
||||||
|
if (!documentWhere) {
|
||||||
|
documentWhere = {
|
||||||
|
or: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionConfig = payload.collections[collectionSlug].config
|
||||||
|
if (collectionConfig.folders && collectionConfig.folders.browseByFolder === true) {
|
||||||
|
const collectionConstraints = await buildFolderWhereConstraints({
|
||||||
|
collectionConfig,
|
||||||
|
folderID,
|
||||||
|
localeCode: fullLocale?.code,
|
||||||
|
req: initPageResult.req,
|
||||||
|
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (collectionConstraints) {
|
||||||
|
documentWhere.or.push(collectionConstraints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { breadcrumbs, documents, subfolders } = await getFolderData({
|
const { breadcrumbs, documents, subfolders } = await getFolderData({
|
||||||
|
documentWhere,
|
||||||
folderID,
|
folderID,
|
||||||
|
folderWhere,
|
||||||
req: initPageResult.req,
|
req: initPageResult.req,
|
||||||
search: query?.search as string,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
|
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
|
||||||
@@ -96,13 +151,6 @@ export const buildBrowseByFolderView = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>(
|
|
||||||
'browse-by-folder',
|
|
||||||
payload,
|
|
||||||
user.id,
|
|
||||||
user.collection,
|
|
||||||
)
|
|
||||||
|
|
||||||
const serverProps: Omit<FolderListViewServerPropsOnly, 'collectionConfig' | 'listPreferences'> = {
|
const serverProps: Omit<FolderListViewServerPropsOnly, 'collectionConfig' | 'listPreferences'> = {
|
||||||
documents,
|
documents,
|
||||||
i18n,
|
i18n,
|
||||||
@@ -125,7 +173,7 @@ export const buildBrowseByFolderView = async (
|
|||||||
|
|
||||||
// documents cannot be created without a parent folder in this view
|
// documents cannot be created without a parent folder in this view
|
||||||
const hasCreatePermissionCollectionSlugs = folderID
|
const hasCreatePermissionCollectionSlugs = folderID
|
||||||
? [config.folders.slug, ...folderCollectionSlugs]
|
? [config.folders.slug, ...browseByFolderSlugs]
|
||||||
: [config.folders.slug]
|
: [config.folders.slug]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -134,7 +182,8 @@ export const buildBrowseByFolderView = async (
|
|||||||
breadcrumbs={breadcrumbs}
|
breadcrumbs={breadcrumbs}
|
||||||
documents={documents}
|
documents={documents}
|
||||||
filteredCollectionSlugs={selectedCollectionSlugs}
|
filteredCollectionSlugs={selectedCollectionSlugs}
|
||||||
folderCollectionSlugs={folderCollectionSlugs}
|
folderCollectionSlugs={browseByFolderSlugs}
|
||||||
|
folderFieldName={config.folders.fieldName}
|
||||||
folderID={folderID}
|
folderID={folderID}
|
||||||
subfolders={subfolders}
|
subfolders={subfolders}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import type {
|
|||||||
|
|
||||||
import { DefaultCollectionFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
import { DefaultCollectionFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
|
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||||
import { redirect } from 'next/navigation.js'
|
import { redirect } from 'next/navigation.js'
|
||||||
import { getFolderData, parseDocumentID } from 'payload'
|
import { getFolderData } from 'payload'
|
||||||
|
import { buildFolderWhereConstraints } from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { getPreferences } from '../../utilities/getPreferences.js'
|
import { getPreferences } from '../../utilities/getPreferences.js'
|
||||||
@@ -37,7 +38,6 @@ export const buildCollectionFolderView = async (
|
|||||||
disableBulkDelete,
|
disableBulkDelete,
|
||||||
disableBulkEdit,
|
disableBulkEdit,
|
||||||
enableRowSelections,
|
enableRowSelections,
|
||||||
folderCollectionSlugs,
|
|
||||||
folderID,
|
folderID,
|
||||||
initPageResult,
|
initPageResult,
|
||||||
isInDrawer,
|
isInDrawer,
|
||||||
@@ -69,12 +69,12 @@ export const buildCollectionFolderView = async (
|
|||||||
if (collectionConfig) {
|
if (collectionConfig) {
|
||||||
const query = queryFromArgs || queryFromReq
|
const query = queryFromArgs || queryFromReq
|
||||||
|
|
||||||
const collectionFolderPreferences = await getPreferences<{ viewPreference: string }>(
|
const collectionFolderPreferences = await getPreferences<{
|
||||||
`${collectionSlug}-collection-folder`,
|
sort?: string
|
||||||
payload,
|
viewPreference: string
|
||||||
user.id,
|
}>(`${collectionSlug}-collection-folder`, payload, user.id, user.collection)
|
||||||
user.collection,
|
|
||||||
)
|
const sortPreference = collectionFolderPreferences?.value.sort
|
||||||
|
|
||||||
const {
|
const {
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
@@ -82,38 +82,45 @@ export const buildCollectionFolderView = async (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) ||
|
(!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) ||
|
||||||
!folderCollectionSlugs.includes(collectionSlug)
|
!config.folders
|
||||||
) {
|
) {
|
||||||
throw new Error('not-found')
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereConstraints = [
|
let folderWhere: undefined | Where
|
||||||
mergeListSearchAndWhere({
|
const folderCollectionConfig = payload.collections[config.folders.slug].config
|
||||||
collectionConfig,
|
const folderCollectionConstraints = await buildFolderWhereConstraints({
|
||||||
search: typeof query?.search === 'string' ? query.search : undefined,
|
collectionConfig: folderCollectionConfig,
|
||||||
where: (query?.where as Where) || undefined,
|
folderID,
|
||||||
}),
|
localeCode: fullLocale?.code,
|
||||||
]
|
req: initPageResult.req,
|
||||||
|
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||||
|
sort: sortPreference,
|
||||||
|
})
|
||||||
|
|
||||||
if (folderID) {
|
if (folderCollectionConstraints) {
|
||||||
whereConstraints.push({
|
folderWhere = folderCollectionConstraints
|
||||||
[config.folders.fieldName]: {
|
}
|
||||||
equals: parseDocumentID({ id: folderID, collectionSlug, payload }),
|
|
||||||
},
|
let documentWhere: undefined | Where
|
||||||
})
|
const collectionConstraints = await buildFolderWhereConstraints({
|
||||||
} else {
|
collectionConfig,
|
||||||
whereConstraints.push({
|
folderID,
|
||||||
[config.folders.fieldName]: {
|
localeCode: fullLocale?.code,
|
||||||
exists: false,
|
req: initPageResult.req,
|
||||||
},
|
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||||
})
|
sort: sortPreference,
|
||||||
|
})
|
||||||
|
if (collectionConstraints) {
|
||||||
|
documentWhere = collectionConstraints
|
||||||
}
|
}
|
||||||
|
|
||||||
const { breadcrumbs, documents, subfolders } = await getFolderData({
|
const { breadcrumbs, documents, subfolders } = await getFolderData({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
|
documentWhere,
|
||||||
folderID,
|
folderID,
|
||||||
|
folderWhere,
|
||||||
req: initPageResult.req,
|
req: initPageResult.req,
|
||||||
search: query?.search as string,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
|
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
|
||||||
@@ -175,7 +182,8 @@ export const buildCollectionFolderView = async (
|
|||||||
breadcrumbs={breadcrumbs}
|
breadcrumbs={breadcrumbs}
|
||||||
collectionSlug={collectionSlug}
|
collectionSlug={collectionSlug}
|
||||||
documents={documents}
|
documents={documents}
|
||||||
folderCollectionSlugs={folderCollectionSlugs}
|
folderCollectionSlugs={[collectionSlug]}
|
||||||
|
folderFieldName={config.folders.fieldName}
|
||||||
folderID={folderID}
|
folderID={folderID}
|
||||||
search={search}
|
search={search}
|
||||||
subfolders={subfolders}
|
subfolders={subfolders}
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ type GetRouteDataArgs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetRouteDataResult = {
|
type GetRouteDataResult = {
|
||||||
|
browseByFolderSlugs: CollectionSlug[]
|
||||||
DefaultView: ViewFromConfig
|
DefaultView: ViewFromConfig
|
||||||
documentSubViewType?: DocumentSubViewTypes
|
documentSubViewType?: DocumentSubViewTypes
|
||||||
folderCollectionSlugs: CollectionSlug[]
|
|
||||||
folderID?: string
|
folderID?: string
|
||||||
initPageOptions: Parameters<typeof initPage>[0]
|
initPageOptions: Parameters<typeof initPage>[0]
|
||||||
serverProps: ServerPropsFromView
|
serverProps: ServerPropsFromView
|
||||||
@@ -113,12 +113,16 @@ export const getRouteData = ({
|
|||||||
let matchedCollection: SanitizedConfig['collections'][number] = undefined
|
let matchedCollection: SanitizedConfig['collections'][number] = undefined
|
||||||
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
|
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
|
||||||
|
|
||||||
const folderCollectionSlugs = config.collections.reduce((acc, { slug, folders }) => {
|
const isBrowseByFolderEnabled = config.folders && config.folders.browseByFolder
|
||||||
if (folders) {
|
const browseByFolderSlugs =
|
||||||
return [...acc, slug]
|
(isBrowseByFolderEnabled &&
|
||||||
}
|
config.collections.reduce((acc, { slug, folders }) => {
|
||||||
return acc
|
if (folders && folders.browseByFolder) {
|
||||||
}, [])
|
return [...acc, slug]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])) ||
|
||||||
|
[]
|
||||||
|
|
||||||
const serverProps: ServerPropsFromView = {
|
const serverProps: ServerPropsFromView = {
|
||||||
viewActions: config?.admin?.components?.actions || [],
|
viewActions: config?.admin?.components?.actions || [],
|
||||||
@@ -187,7 +191,7 @@ export const getRouteData = ({
|
|||||||
viewType = 'account'
|
viewType = 'account'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folderCollectionSlugs.length && viewKey === 'browseByFolder') {
|
if (isBrowseByFolderEnabled && viewKey === 'browseByFolder') {
|
||||||
templateType = 'default'
|
templateType = 'default'
|
||||||
viewType = 'folders'
|
viewType = 'folders'
|
||||||
}
|
}
|
||||||
@@ -204,7 +208,7 @@ export const getRouteData = ({
|
|||||||
templateType = 'minimal'
|
templateType = 'minimal'
|
||||||
viewType = 'reset'
|
viewType = 'reset'
|
||||||
} else if (
|
} else if (
|
||||||
folderCollectionSlugs.length &&
|
isBrowseByFolderEnabled &&
|
||||||
`/${segmentOne}` === config.admin.routes.browseByFolder
|
`/${segmentOne}` === config.admin.routes.browseByFolder
|
||||||
) {
|
) {
|
||||||
// --> /browse-by-folder/:folderID
|
// --> /browse-by-folder/:folderID
|
||||||
@@ -260,10 +264,7 @@ export const getRouteData = ({
|
|||||||
templateType = 'minimal'
|
templateType = 'minimal'
|
||||||
viewType = 'verify'
|
viewType = 'verify'
|
||||||
} else if (isCollection && matchedCollection) {
|
} else if (isCollection && matchedCollection) {
|
||||||
if (
|
if (config.folders && segmentThree === config.folders.slug && matchedCollection.folders) {
|
||||||
segmentThree === config.folders.slug &&
|
|
||||||
folderCollectionSlugs.includes(matchedCollection.slug)
|
|
||||||
) {
|
|
||||||
// Collection Folder Views
|
// Collection Folder Views
|
||||||
// --> /collections/:collectionSlug/:folderCollectionSlug
|
// --> /collections/:collectionSlug/:folderCollectionSlug
|
||||||
// --> /collections/:collectionSlug/:folderCollectionSlug/:folderID
|
// --> /collections/:collectionSlug/:folderCollectionSlug/:folderID
|
||||||
@@ -333,9 +334,9 @@ export const getRouteData = ({
|
|||||||
serverProps.viewActions.reverse()
|
serverProps.viewActions.reverse()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
browseByFolderSlugs,
|
||||||
DefaultView: ViewToRender,
|
DefaultView: ViewToRender,
|
||||||
documentSubViewType,
|
documentSubViewType,
|
||||||
folderCollectionSlugs,
|
|
||||||
folderID,
|
folderID,
|
||||||
initPageOptions,
|
initPageOptions,
|
||||||
serverProps,
|
serverProps,
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ export const RootPage = async ({
|
|||||||
const searchParams = await searchParamsPromise
|
const searchParams = await searchParamsPromise
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
browseByFolderSlugs,
|
||||||
DefaultView,
|
DefaultView,
|
||||||
documentSubViewType,
|
documentSubViewType,
|
||||||
folderCollectionSlugs,
|
|
||||||
folderID: folderIDParam,
|
folderID: folderIDParam,
|
||||||
initPageOptions,
|
initPageOptions,
|
||||||
serverProps,
|
serverProps,
|
||||||
@@ -140,17 +140,19 @@ export const RootPage = async ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const payload = initPageResult?.req.payload
|
const payload = initPageResult?.req.payload
|
||||||
const folderID = parseDocumentID({
|
const folderID = payload.config.folders
|
||||||
id: folderIDParam,
|
? parseDocumentID({
|
||||||
collectionSlug: payload.config.folders.slug,
|
id: folderIDParam,
|
||||||
payload,
|
collectionSlug: payload.config.folders.slug,
|
||||||
})
|
payload,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
|
||||||
const RenderedView = RenderServerComponent({
|
const RenderedView = RenderServerComponent({
|
||||||
clientProps: {
|
clientProps: {
|
||||||
|
browseByFolderSlugs,
|
||||||
clientConfig,
|
clientConfig,
|
||||||
documentSubViewType,
|
documentSubViewType,
|
||||||
folderCollectionSlugs,
|
|
||||||
viewType,
|
viewType,
|
||||||
} satisfies AdminViewClientProps,
|
} satisfies AdminViewClientProps,
|
||||||
Component: DefaultView.payloadComponent,
|
Component: DefaultView.payloadComponent,
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export const generatePageMetadata = async ({
|
|||||||
// --> /:collectionSlug/verify/:token
|
// --> /:collectionSlug/verify/:token
|
||||||
meta = await generateVerifyViewMetadata({ config, i18n })
|
meta = await generateVerifyViewMetadata({ config, i18n })
|
||||||
} else if (isCollection) {
|
} else if (isCollection) {
|
||||||
if (segmentThree === config.folders.slug) {
|
if (config.folders && segmentThree === config.folders.slug) {
|
||||||
if (folderCollectionSlugs.includes(collectionConfig.slug)) {
|
if (folderCollectionSlugs.includes(collectionConfig.slug)) {
|
||||||
// Collection Folder Views
|
// Collection Folder Views
|
||||||
// --> /collections/:collectionSlug/:folderCollectionSlug
|
// --> /collections/:collectionSlug/:folderCollectionSlug
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export type AdminViewConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AdminViewClientProps = {
|
export type AdminViewClientProps = {
|
||||||
|
browseByFolderSlugs?: SanitizedCollectionConfig['slug'][]
|
||||||
clientConfig: ClientConfig
|
clientConfig: ClientConfig
|
||||||
documentSubViewType?: DocumentSubViewTypes
|
documentSubViewType?: DocumentSubViewTypes
|
||||||
folderCollectionSlugs?: SanitizedCollectionConfig['slug'][]
|
|
||||||
viewType: ViewTypes
|
viewType: ViewTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,14 @@ export const sanitizeCollection = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sanitized.folders === true) {
|
||||||
|
sanitized.folders = {
|
||||||
|
browseByFolder: true,
|
||||||
|
}
|
||||||
|
} else if (sanitized.folders) {
|
||||||
|
sanitized.folders.browseByFolder = sanitized.folders.browseByFolder ?? true
|
||||||
|
}
|
||||||
|
|
||||||
if (sanitized.upload) {
|
if (sanitized.upload) {
|
||||||
if (sanitized.upload === true) {
|
if (sanitized.upload === true) {
|
||||||
sanitized.upload = {}
|
sanitized.upload = {}
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
|
|||||||
/**
|
/**
|
||||||
* Enables folders for this collection
|
* Enables folders for this collection
|
||||||
*/
|
*/
|
||||||
folders?: CollectionFoldersConfiguration
|
folders?: boolean | CollectionFoldersConfiguration
|
||||||
/**
|
/**
|
||||||
* Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks
|
* Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks
|
||||||
*/
|
*/
|
||||||
@@ -602,7 +602,7 @@ export type SanitizedJoins = {
|
|||||||
export interface SanitizedCollectionConfig
|
export interface SanitizedCollectionConfig
|
||||||
extends Omit<
|
extends Omit<
|
||||||
DeepRequired<CollectionConfig>,
|
DeepRequired<CollectionConfig>,
|
||||||
'admin' | 'auth' | 'endpoints' | 'fields' | 'slug' | 'upload' | 'versions'
|
'admin' | 'auth' | 'endpoints' | 'fields' | 'folders' | 'slug' | 'upload' | 'versions'
|
||||||
> {
|
> {
|
||||||
admin: CollectionAdminOptions
|
admin: CollectionAdminOptions
|
||||||
auth: Auth
|
auth: Auth
|
||||||
@@ -616,6 +616,7 @@ export interface SanitizedCollectionConfig
|
|||||||
/**
|
/**
|
||||||
* Object of collections to join 'Join Fields object keyed by collection
|
* Object of collections to join 'Join Fields object keyed by collection
|
||||||
*/
|
*/
|
||||||
|
folders: CollectionFoldersConfiguration | false
|
||||||
joins: SanitizedJoins
|
joins: SanitizedJoins
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -144,6 +144,17 @@ export const createClientConfig = ({
|
|||||||
importMap,
|
importMap,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
case 'folders':
|
||||||
|
if (config.folders) {
|
||||||
|
clientConfig.folders = {
|
||||||
|
slug: config.folders.slug,
|
||||||
|
browseByFolder: config.folders.browseByFolder,
|
||||||
|
debug: config.folders.debug,
|
||||||
|
fieldName: config.folders.fieldName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
case 'globals':
|
case 'globals':
|
||||||
;(clientConfig.globals as ClientGlobalConfig[]) = createClientGlobalConfigs({
|
;(clientConfig.globals as ClientGlobalConfig[]) = createClientGlobalConfigs({
|
||||||
defaultIDType: config.db.defaultIDType,
|
defaultIDType: config.db.defaultIDType,
|
||||||
@@ -152,7 +163,6 @@ export const createClientConfig = ({
|
|||||||
importMap,
|
importMap,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'localization':
|
case 'localization':
|
||||||
if (typeof config.localization === 'object' && config.localization) {
|
if (typeof config.localization === 'object' && config.localization) {
|
||||||
clientConfig.localization = {}
|
clientConfig.localization = {}
|
||||||
|
|||||||
@@ -112,13 +112,6 @@ export const addDefaultsToConfig = (config: Config): Config => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
config.folders = {
|
|
||||||
slug: foldersSlug,
|
|
||||||
debug: false,
|
|
||||||
fieldName: parentFolderFieldName,
|
|
||||||
...(config.folders || {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
config.bin = config.bin ?? []
|
config.bin = config.bin ?? []
|
||||||
config.collections = config.collections ?? []
|
config.collections = config.collections ?? []
|
||||||
config.cookiePrefix = config.cookiePrefix ?? 'payload'
|
config.cookiePrefix = config.cookiePrefix ?? 'payload'
|
||||||
@@ -169,5 +162,18 @@ export const addDefaultsToConfig = (config: Config): Config => {
|
|||||||
...(config.auth || {}),
|
...(config.auth || {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasFolderCollections = config.collections.some((collection) => Boolean(collection.folders))
|
||||||
|
if (hasFolderCollections) {
|
||||||
|
config.folders = {
|
||||||
|
slug: foldersSlug,
|
||||||
|
browseByFolder: true,
|
||||||
|
debug: false,
|
||||||
|
fieldName: parentFolderFieldName,
|
||||||
|
...(config.folders || {}),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.folders = false
|
||||||
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -991,7 +991,7 @@ export type Config = {
|
|||||||
* Options for folder view within the admin panel
|
* Options for folder view within the admin panel
|
||||||
* @experimental this feature may change in minor versions until it is fully stable
|
* @experimental this feature may change in minor versions until it is fully stable
|
||||||
*/
|
*/
|
||||||
folders?: RootFoldersConfiguration
|
folders?: false | RootFoldersConfiguration
|
||||||
/**
|
/**
|
||||||
* @see https://payloadcms.com/docs/configuration/globals#global-configs
|
* @see https://payloadcms.com/docs/configuration/globals#global-configs
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export type {
|
|||||||
Subfolder,
|
Subfolder,
|
||||||
} from '../folders/types.js'
|
} from '../folders/types.js'
|
||||||
|
|
||||||
|
export { buildFolderWhereConstraints } from '../folders/utils/buildFolderWhereConstraints.js'
|
||||||
export { formatFolderOrDocumentItem } from '../folders/utils/formatFolderOrDocumentItem.js'
|
export { formatFolderOrDocumentItem } from '../folders/utils/formatFolderOrDocumentItem.js'
|
||||||
export { validOperators, validOperatorSet } from '../types/constants.js'
|
export { validOperators, validOperatorSet } from '../types/constants.js'
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { CollectionSlug } from '../index.js'
|
|||||||
import { createFolderCollection } from './createFolderCollection.js'
|
import { createFolderCollection } from './createFolderCollection.js'
|
||||||
|
|
||||||
export async function addFolderCollections(config: NonNullable<Config>): Promise<void> {
|
export async function addFolderCollections(config: NonNullable<Config>): Promise<void> {
|
||||||
if (!config.collections) {
|
if (!config.collections || !config.folders) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import httpStatus from 'http-status'
|
import httpStatus from 'http-status'
|
||||||
|
|
||||||
import type { Endpoint } from '../../index.js'
|
import type { Endpoint, Where } from '../../index.js'
|
||||||
|
|
||||||
|
import { buildFolderWhereConstraints } from '../utils/buildFolderWhereConstraints.js'
|
||||||
import { getFolderData } from '../utils/getFolderData.js'
|
import { getFolderData } from '../utils/getFolderData.js'
|
||||||
|
|
||||||
export const populateFolderDataEndpoint: Endpoint = {
|
export const populateFolderDataEndpoint: Endpoint = {
|
||||||
@@ -17,9 +18,12 @@ export const populateFolderDataEndpoint: Endpoint = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderCollection = Boolean(req.payload.collections?.[req.payload.config.folders.slug])
|
if (
|
||||||
|
!(
|
||||||
if (!folderCollection) {
|
req.payload.config.folders &&
|
||||||
|
Boolean(req.payload.collections?.[req.payload.config.folders.slug])
|
||||||
|
)
|
||||||
|
) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
message: 'Folders are not configured',
|
message: 'Folders are not configured',
|
||||||
@@ -30,13 +34,100 @@ export const populateFolderDataEndpoint: Endpoint = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await getFolderData({
|
// if collectionSlug exists, we need to create constraints for that _specific collection_ and the folder collection
|
||||||
collectionSlug: req.searchParams?.get('collectionSlug') || undefined,
|
// if collectionSlug does not exist, we need to create constraints for _all folder enabled collections_ and the folder collection
|
||||||
|
let documentWhere: undefined | Where
|
||||||
|
let folderWhere: undefined | Where
|
||||||
|
const collectionSlug = req.searchParams?.get('collectionSlug')
|
||||||
|
|
||||||
|
if (collectionSlug) {
|
||||||
|
const collectionConfig = req.payload.collections?.[collectionSlug]?.config
|
||||||
|
|
||||||
|
if (!collectionConfig) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
message: `Collection with slug "${collectionSlug}" not found`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: httpStatus.NOT_FOUND,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionConstraints = await buildFolderWhereConstraints({
|
||||||
|
collectionConfig,
|
||||||
|
folderID: req.searchParams?.get('folderID') || undefined,
|
||||||
|
localeCode: typeof req?.locale === 'string' ? req.locale : undefined,
|
||||||
|
req,
|
||||||
|
search: req.searchParams?.get('search') || undefined,
|
||||||
|
sort: req.searchParams?.get('sort') || undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (collectionConstraints) {
|
||||||
|
documentWhere = collectionConstraints
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// loop over all folder enabled collections and build constraints for each
|
||||||
|
for (const collectionSlug of Object.keys(req.payload.collections)) {
|
||||||
|
const collectionConfig = req.payload.collections[collectionSlug]?.config
|
||||||
|
|
||||||
|
if (collectionConfig?.folders) {
|
||||||
|
const collectionConstraints = await buildFolderWhereConstraints({
|
||||||
|
collectionConfig,
|
||||||
|
folderID: req.searchParams?.get('folderID') || undefined,
|
||||||
|
localeCode: typeof req?.locale === 'string' ? req.locale : undefined,
|
||||||
|
req,
|
||||||
|
search: req.searchParams?.get('search') || undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (collectionConstraints) {
|
||||||
|
if (!documentWhere) {
|
||||||
|
documentWhere = { or: [] }
|
||||||
|
}
|
||||||
|
if (!Array.isArray(documentWhere.or)) {
|
||||||
|
documentWhere.or = [documentWhere]
|
||||||
|
} else if (Array.isArray(documentWhere.or)) {
|
||||||
|
documentWhere.or.push(collectionConstraints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderCollectionConfig =
|
||||||
|
req.payload.collections?.[req.payload.config.folders.slug]?.config
|
||||||
|
|
||||||
|
if (!folderCollectionConfig) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
message: 'Folder collection not found',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: httpStatus.NOT_FOUND,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderConstraints = await buildFolderWhereConstraints({
|
||||||
|
collectionConfig: folderCollectionConfig,
|
||||||
folderID: req.searchParams?.get('folderID') || undefined,
|
folderID: req.searchParams?.get('folderID') || undefined,
|
||||||
|
localeCode: typeof req?.locale === 'string' ? req.locale : undefined,
|
||||||
req,
|
req,
|
||||||
search: req.searchParams?.get('search') || undefined,
|
search: req.searchParams?.get('search') || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (folderConstraints) {
|
||||||
|
folderWhere = folderConstraints
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getFolderData({
|
||||||
|
collectionSlug: req.searchParams?.get('collectionSlug') || undefined,
|
||||||
|
documentWhere: documentWhere ? documentWhere : undefined,
|
||||||
|
folderID: req.searchParams?.get('folderID') || undefined,
|
||||||
|
folderWhere,
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
|
||||||
return Response.json(data)
|
return Response.json(data)
|
||||||
},
|
},
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { CollectionAfterChangeHook, Payload } from '../../index.js'
|
|||||||
import { extractID } from '../../utilities/extractID.js'
|
import { extractID } from '../../utilities/extractID.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
folderCollectionSlug: string
|
||||||
folderFieldName: string
|
folderFieldName: string
|
||||||
folderID: number | string
|
folderID: number | string
|
||||||
parentIDToFind: number | string
|
parentIDToFind: number | string
|
||||||
@@ -14,6 +15,7 @@ type Args = {
|
|||||||
* recursively checking upwards through the folder hierarchy.
|
* recursively checking upwards through the folder hierarchy.
|
||||||
*/
|
*/
|
||||||
async function isChildOfFolder({
|
async function isChildOfFolder({
|
||||||
|
folderCollectionSlug,
|
||||||
folderFieldName,
|
folderFieldName,
|
||||||
folderID,
|
folderID,
|
||||||
parentIDToFind,
|
parentIDToFind,
|
||||||
@@ -21,7 +23,7 @@ async function isChildOfFolder({
|
|||||||
}: Args): Promise<boolean> {
|
}: Args): Promise<boolean> {
|
||||||
const parentFolder = await payload.findByID({
|
const parentFolder = await payload.findByID({
|
||||||
id: folderID,
|
id: folderID,
|
||||||
collection: payload.config.folders.slug,
|
collection: folderCollectionSlug,
|
||||||
})
|
})
|
||||||
|
|
||||||
const parentFolderID = parentFolder[folderFieldName]
|
const parentFolderID = parentFolder[folderFieldName]
|
||||||
@@ -39,6 +41,7 @@ async function isChildOfFolder({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return isChildOfFolder({
|
return isChildOfFolder({
|
||||||
|
folderCollectionSlug,
|
||||||
folderFieldName,
|
folderFieldName,
|
||||||
folderID: parentFolderID,
|
folderID: parentFolderID,
|
||||||
parentIDToFind,
|
parentIDToFind,
|
||||||
@@ -71,10 +74,15 @@ export const reparentChildFolder = ({
|
|||||||
folderFieldName: string
|
folderFieldName: string
|
||||||
}): CollectionAfterChangeHook => {
|
}): CollectionAfterChangeHook => {
|
||||||
return async ({ doc, previousDoc, req }) => {
|
return async ({ doc, previousDoc, req }) => {
|
||||||
if (previousDoc[folderFieldName] !== doc[folderFieldName] && doc[folderFieldName]) {
|
if (
|
||||||
|
previousDoc[folderFieldName] !== doc[folderFieldName] &&
|
||||||
|
doc[folderFieldName] &&
|
||||||
|
req.payload.config.folders
|
||||||
|
) {
|
||||||
const newParentFolderID = extractID(doc[folderFieldName])
|
const newParentFolderID = extractID(doc[folderFieldName])
|
||||||
const isMovingToChild = newParentFolderID
|
const isMovingToChild = newParentFolderID
|
||||||
? await isChildOfFolder({
|
? await isChildOfFolder({
|
||||||
|
folderCollectionSlug: req.payload.config.folders.slug,
|
||||||
folderFieldName,
|
folderFieldName,
|
||||||
folderID: newParentFolderID,
|
folderID: newParentFolderID,
|
||||||
parentIDToFind: doc.id,
|
parentIDToFind: doc.id,
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ export type GetFolderDataResult = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RootFoldersConfiguration = {
|
export type RootFoldersConfiguration = {
|
||||||
|
/**
|
||||||
|
* If true, the browse by folder view will be enabled
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
browseByFolder?: boolean
|
||||||
/**
|
/**
|
||||||
* An array of functions to be ran when the folder collection is initialized
|
* An array of functions to be ran when the folder collection is initialized
|
||||||
* This allows plugins to modify the collection configuration
|
* This allows plugins to modify the collection configuration
|
||||||
@@ -99,4 +105,11 @@ export type RootFoldersConfiguration = {
|
|||||||
slug?: string
|
slug?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CollectionFoldersConfiguration = boolean
|
export type CollectionFoldersConfiguration = {
|
||||||
|
/**
|
||||||
|
* If true, the collection will be included in the browse by folder view
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
browseByFolder?: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||||
|
import type { PayloadRequest, Where } from '../../types/index.js'
|
||||||
|
|
||||||
|
import { combineWhereConstraints } from '../../utilities/combineWhereConstraints.js'
|
||||||
|
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
collectionConfig: SanitizedCollectionConfig
|
||||||
|
folderID?: number | string
|
||||||
|
localeCode?: string
|
||||||
|
req: PayloadRequest
|
||||||
|
search?: string
|
||||||
|
sort?: string
|
||||||
|
}
|
||||||
|
export async function buildFolderWhereConstraints({
|
||||||
|
collectionConfig,
|
||||||
|
folderID,
|
||||||
|
localeCode,
|
||||||
|
req,
|
||||||
|
search = '',
|
||||||
|
sort,
|
||||||
|
}: Args): Promise<undefined | Where> {
|
||||||
|
const constraints: Where[] = [
|
||||||
|
mergeListSearchAndWhere({
|
||||||
|
collectionConfig,
|
||||||
|
search,
|
||||||
|
// where // cannot have where since fields in folders and collection will differ
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (typeof collectionConfig.admin?.baseListFilter === 'function') {
|
||||||
|
const baseListFilterConstraint = await collectionConfig.admin.baseListFilter({
|
||||||
|
limit: 0,
|
||||||
|
locale: localeCode,
|
||||||
|
page: 1,
|
||||||
|
req,
|
||||||
|
sort:
|
||||||
|
sort ||
|
||||||
|
(typeof collectionConfig.defaultSort === 'string' ? collectionConfig.defaultSort : 'id'),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (baseListFilterConstraint) {
|
||||||
|
constraints.push(baseListFilterConstraint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folderID) {
|
||||||
|
// build folder join where constraints
|
||||||
|
constraints.push({
|
||||||
|
relationTo: {
|
||||||
|
equals: collectionConfig.slug,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredConstraints = constraints.filter(Boolean)
|
||||||
|
|
||||||
|
if (filteredConstraints.length > 1) {
|
||||||
|
return combineWhereConstraints(filteredConstraints)
|
||||||
|
} else if (filteredConstraints.length === 1) {
|
||||||
|
return filteredConstraints[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@ export const getFolderBreadcrumbs = async ({
|
|||||||
req,
|
req,
|
||||||
}: GetFolderBreadcrumbsArgs): Promise<FolderBreadcrumb[] | null> => {
|
}: GetFolderBreadcrumbsArgs): Promise<FolderBreadcrumb[] | null> => {
|
||||||
const { payload, user } = req
|
const { payload, user } = req
|
||||||
const folderFieldName: string = payload.config.folders.fieldName
|
if (folderID && payload.config.folders) {
|
||||||
if (folderID) {
|
const folderFieldName: string = payload.config.folders.fieldName
|
||||||
const folderQuery = await payload.find({
|
const folderQuery = await payload.find({
|
||||||
collection: payload.config.folders.slug,
|
collection: payload.config.folders.slug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { CollectionSlug } from '../../index.js'
|
import type { CollectionSlug } from '../../index.js'
|
||||||
import type { PayloadRequest } from '../../types/index.js'
|
import type { PayloadRequest, Where } from '../../types/index.js'
|
||||||
import type { GetFolderDataResult } from '../types.js'
|
import type { GetFolderDataResult } from '../types.js'
|
||||||
|
|
||||||
import { parseDocumentID } from '../../index.js'
|
import { parseDocumentID } from '../../index.js'
|
||||||
@@ -14,27 +14,38 @@ type Args = {
|
|||||||
* @example 'posts'
|
* @example 'posts'
|
||||||
*/
|
*/
|
||||||
collectionSlug?: CollectionSlug
|
collectionSlug?: CollectionSlug
|
||||||
|
/**
|
||||||
|
* Optional where clause to filter documents by
|
||||||
|
* @default undefined
|
||||||
|
*/
|
||||||
|
documentWhere?: Where
|
||||||
/**
|
/**
|
||||||
* The ID of the folder to query documents from
|
* The ID of the folder to query documents from
|
||||||
* @default undefined
|
* @default undefined
|
||||||
*/
|
*/
|
||||||
folderID?: number | string
|
folderID?: number | string
|
||||||
req: PayloadRequest
|
/** Optional where clause to filter subfolders by
|
||||||
/**
|
* @default undefined
|
||||||
* Search term to filter documents by - only applicable IF `collectionSlug` exists and NO `folderID` is provided
|
|
||||||
*/
|
*/
|
||||||
search?: string
|
folderWhere?: Where
|
||||||
|
req: PayloadRequest
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Query for documents, subfolders and breadcrumbs for a given folder
|
* Query for documents, subfolders and breadcrumbs for a given folder
|
||||||
*/
|
*/
|
||||||
export const getFolderData = async ({
|
export const getFolderData = async ({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
|
documentWhere,
|
||||||
folderID: _folderID,
|
folderID: _folderID,
|
||||||
|
folderWhere,
|
||||||
req,
|
req,
|
||||||
search,
|
|
||||||
}: Args): Promise<GetFolderDataResult> => {
|
}: Args): Promise<GetFolderDataResult> => {
|
||||||
const { payload } = req
|
const { payload } = req
|
||||||
|
|
||||||
|
if (payload.config.folders === false) {
|
||||||
|
throw new Error('Folders are not enabled')
|
||||||
|
}
|
||||||
|
|
||||||
const parentFolderID = parseDocumentID({
|
const parentFolderID = parseDocumentID({
|
||||||
id: _folderID,
|
id: _folderID,
|
||||||
collectionSlug: payload.config.folders.slug,
|
collectionSlug: payload.config.folders.slug,
|
||||||
@@ -49,7 +60,8 @@ export const getFolderData = async ({
|
|||||||
if (parentFolderID) {
|
if (parentFolderID) {
|
||||||
// subfolders and documents are queried together
|
// subfolders and documents are queried together
|
||||||
const documentAndSubfolderPromise = queryDocumentsAndFoldersFromJoin({
|
const documentAndSubfolderPromise = queryDocumentsAndFoldersFromJoin({
|
||||||
collectionSlug,
|
documentWhere,
|
||||||
|
folderWhere,
|
||||||
parentFolderID,
|
parentFolderID,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
@@ -67,14 +79,16 @@ export const getFolderData = async ({
|
|||||||
// subfolders and documents are queried separately
|
// subfolders and documents are queried separately
|
||||||
const subfoldersPromise = getOrphanedDocs({
|
const subfoldersPromise = getOrphanedDocs({
|
||||||
collectionSlug: payload.config.folders.slug,
|
collectionSlug: payload.config.folders.slug,
|
||||||
|
folderFieldName: payload.config.folders.fieldName,
|
||||||
req,
|
req,
|
||||||
search,
|
where: folderWhere,
|
||||||
})
|
})
|
||||||
const documentsPromise = collectionSlug
|
const documentsPromise = collectionSlug
|
||||||
? getOrphanedDocs({
|
? getOrphanedDocs({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
|
folderFieldName: payload.config.folders.fieldName,
|
||||||
req,
|
req,
|
||||||
search,
|
where: documentWhere,
|
||||||
})
|
})
|
||||||
: Promise.resolve([])
|
: Promise.resolve([])
|
||||||
const [breadcrumbs, subfolders, documents] = await Promise.all([
|
const [breadcrumbs, subfolders, documents] = await Promise.all([
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { PaginatedDocs } from '../../database/types.js'
|
import type { PaginatedDocs } from '../../database/types.js'
|
||||||
import type { CollectionSlug } from '../../index.js'
|
import type { Document, PayloadRequest, Where } from '../../types/index.js'
|
||||||
import type { Document, PayloadRequest } from '../../types/index.js'
|
|
||||||
import type { FolderOrDocument } from '../types.js'
|
import type { FolderOrDocument } from '../types.js'
|
||||||
|
|
||||||
|
import { APIError } from '../../errors/APIError.js'
|
||||||
|
import { combineWhereConstraints } from '../../utilities/combineWhereConstraints.js'
|
||||||
import { formatFolderOrDocumentItem } from './formatFolderOrDocumentItem.js'
|
import { formatFolderOrDocumentItem } from './formatFolderOrDocumentItem.js'
|
||||||
|
|
||||||
type QueryDocumentsAndFoldersResults = {
|
type QueryDocumentsAndFoldersResults = {
|
||||||
@@ -10,40 +11,37 @@ type QueryDocumentsAndFoldersResults = {
|
|||||||
subfolders: FolderOrDocument[]
|
subfolders: FolderOrDocument[]
|
||||||
}
|
}
|
||||||
type QueryDocumentsAndFoldersArgs = {
|
type QueryDocumentsAndFoldersArgs = {
|
||||||
collectionSlug?: CollectionSlug
|
/**
|
||||||
|
* Optional where clause to filter documents by
|
||||||
|
* @default undefined
|
||||||
|
*/
|
||||||
|
documentWhere?: Where
|
||||||
|
/** Optional where clause to filter subfolders by
|
||||||
|
* @default undefined
|
||||||
|
*/
|
||||||
|
folderWhere?: Where
|
||||||
parentFolderID: number | string
|
parentFolderID: number | string
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
}
|
}
|
||||||
export async function queryDocumentsAndFoldersFromJoin({
|
export async function queryDocumentsAndFoldersFromJoin({
|
||||||
collectionSlug,
|
documentWhere,
|
||||||
|
folderWhere,
|
||||||
parentFolderID,
|
parentFolderID,
|
||||||
req,
|
req,
|
||||||
}: QueryDocumentsAndFoldersArgs): Promise<QueryDocumentsAndFoldersResults> {
|
}: QueryDocumentsAndFoldersArgs): Promise<QueryDocumentsAndFoldersResults> {
|
||||||
const { payload, user } = req
|
const { payload, user } = req
|
||||||
const folderCollectionSlugs: string[] = payload.config.collections.reduce<string[]>(
|
|
||||||
(acc, collection) => {
|
if (payload.config.folders === false) {
|
||||||
if (collection?.folders) {
|
throw new APIError('Folders are not enabled', 500)
|
||||||
acc.push(collection.slug)
|
}
|
||||||
}
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const subfolderDoc = (await payload.find({
|
const subfolderDoc = (await payload.find({
|
||||||
collection: payload.config.folders.slug,
|
collection: payload.config.folders.slug,
|
||||||
joins: {
|
joins: {
|
||||||
documentsAndFolders: {
|
documentsAndFolders: {
|
||||||
limit: 100_000,
|
limit: 100_000_000,
|
||||||
sort: 'name',
|
sort: 'name',
|
||||||
where: {
|
where: combineWhereConstraints([folderWhere, documentWhere], 'or'),
|
||||||
relationTo: {
|
|
||||||
in: [
|
|
||||||
payload.config.folders.slug,
|
|
||||||
...(collectionSlug ? [collectionSlug] : folderCollectionSlugs),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
limit: 1,
|
limit: 1,
|
||||||
@@ -61,6 +59,9 @@ export async function queryDocumentsAndFoldersFromJoin({
|
|||||||
|
|
||||||
const results: QueryDocumentsAndFoldersResults = childrenDocs.reduce(
|
const results: QueryDocumentsAndFoldersResults = childrenDocs.reduce(
|
||||||
(acc: QueryDocumentsAndFoldersResults, doc: Document) => {
|
(acc: QueryDocumentsAndFoldersResults, doc: Document) => {
|
||||||
|
if (!payload.config.folders) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
const { relationTo, value } = doc
|
const { relationTo, value } = doc
|
||||||
const item = formatFolderOrDocumentItem({
|
const item = formatFolderOrDocumentItem({
|
||||||
folderFieldName: payload.config.folders.fieldName,
|
folderFieldName: payload.config.folders.fieldName,
|
||||||
|
|||||||
@@ -1,42 +1,41 @@
|
|||||||
import type { CollectionSlug, PayloadRequest, Where } from '../../index.js'
|
import type { CollectionSlug, PayloadRequest, Where } from '../../index.js'
|
||||||
import type { FolderOrDocument } from '../types.js'
|
import type { FolderOrDocument } from '../types.js'
|
||||||
|
|
||||||
|
import { combineWhereConstraints } from '../../utilities/combineWhereConstraints.js'
|
||||||
import { formatFolderOrDocumentItem } from './formatFolderOrDocumentItem.js'
|
import { formatFolderOrDocumentItem } from './formatFolderOrDocumentItem.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
collectionSlug: CollectionSlug
|
collectionSlug: CollectionSlug
|
||||||
|
folderFieldName: string
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
search?: string
|
/**
|
||||||
|
* Optional where clause to filter documents by
|
||||||
|
* @default undefined
|
||||||
|
*/
|
||||||
|
where?: Where
|
||||||
}
|
}
|
||||||
export async function getOrphanedDocs({
|
export async function getOrphanedDocs({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
|
folderFieldName,
|
||||||
req,
|
req,
|
||||||
search,
|
where,
|
||||||
}: Args): Promise<FolderOrDocument[]> {
|
}: Args): Promise<FolderOrDocument[]> {
|
||||||
const { payload, user } = req
|
const { payload, user } = req
|
||||||
let whereConstraints: Where = {
|
const noParentFolderConstraint: Where = {
|
||||||
or: [
|
or: [
|
||||||
{
|
{
|
||||||
[payload.config.folders.fieldName]: {
|
[folderFieldName]: {
|
||||||
exists: false,
|
exists: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[payload.config.folders.fieldName]: {
|
[folderFieldName]: {
|
||||||
equals: null,
|
equals: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionSlug && search && payload.collections[collectionSlug]?.config.admin?.useAsTitle) {
|
|
||||||
whereConstraints = {
|
|
||||||
[payload.collections[collectionSlug].config.admin.useAsTitle]: {
|
|
||||||
like: search,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const orphanedFolders = await payload.find({
|
const orphanedFolders = await payload.find({
|
||||||
collection: collectionSlug,
|
collection: collectionSlug,
|
||||||
limit: 0,
|
limit: 0,
|
||||||
@@ -44,13 +43,15 @@ export async function getOrphanedDocs({
|
|||||||
req,
|
req,
|
||||||
sort: payload.collections[collectionSlug]?.config.admin.useAsTitle,
|
sort: payload.collections[collectionSlug]?.config.admin.useAsTitle,
|
||||||
user,
|
user,
|
||||||
where: whereConstraints,
|
where: where
|
||||||
|
? combineWhereConstraints([noParentFolderConstraint, where])
|
||||||
|
: noParentFolderConstraint,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
orphanedFolders?.docs.map((doc) =>
|
orphanedFolders?.docs.map((doc) =>
|
||||||
formatFolderOrDocumentItem({
|
formatFolderOrDocumentItem({
|
||||||
folderFieldName: payload.config.folders.fieldName,
|
folderFieldName,
|
||||||
isUpload: Boolean(payload.collections[collectionSlug]?.config.upload),
|
isUpload: Boolean(payload.collections[collectionSlug]?.config.upload),
|
||||||
relationTo: collectionSlug,
|
relationTo: collectionSlug,
|
||||||
useAsTitle: payload.collections[collectionSlug]?.config.admin.useAsTitle,
|
useAsTitle: payload.collections[collectionSlug]?.config.admin.useAsTitle,
|
||||||
|
|||||||
@@ -169,12 +169,13 @@ export function EditForm({
|
|||||||
<Upload_v4
|
<Upload_v4
|
||||||
collectionSlug={collectionConfig.slug}
|
collectionSlug={collectionConfig.slug}
|
||||||
customActions={[
|
customActions={[
|
||||||
collectionConfig.folders && (
|
folders && collectionConfig.folders && (
|
||||||
<MoveDocToFolder
|
<MoveDocToFolder
|
||||||
buttonProps={{
|
buttonProps={{
|
||||||
buttonStyle: 'pill',
|
buttonStyle: 'pill',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
}}
|
}}
|
||||||
|
folderCollectionSlug={folders.slug}
|
||||||
folderFieldName={folders.fieldName}
|
folderFieldName={folders.fieldName}
|
||||||
key="move-doc-to-folder"
|
key="move-doc-to-folder"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -171,7 +171,12 @@ export const DocumentControls: React.FC<{
|
|||||||
{showLockedMetaIcon && (
|
{showLockedMetaIcon && (
|
||||||
<Locked className={`${baseClass}__locked-controls`} user={user} />
|
<Locked className={`${baseClass}__locked-controls`} user={user} />
|
||||||
)}
|
)}
|
||||||
{showFolderMetaIcon && <MoveDocToFolder folderFieldName={config.folders.fieldName} />}
|
{showFolderMetaIcon && config.folders && (
|
||||||
|
<MoveDocToFolder
|
||||||
|
folderCollectionSlug={config.folders.slug}
|
||||||
|
folderFieldName={config.folders.fieldName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ import React, { useEffect } from 'react'
|
|||||||
import { MoveDocToFolderButton, useConfig, useTranslation } from '../../../exports/client/index.js'
|
import { MoveDocToFolderButton, useConfig, useTranslation } from '../../../exports/client/index.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collectionSlug: string
|
readonly collectionSlug: string
|
||||||
data: Data
|
readonly data: Data
|
||||||
docTitle: string
|
readonly docTitle: string
|
||||||
folderFieldName: string
|
readonly folderCollectionSlug: string
|
||||||
|
readonly folderFieldName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FolderTableCellClient = ({
|
export const FolderTableCellClient = ({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
data,
|
data,
|
||||||
docTitle,
|
docTitle,
|
||||||
|
folderCollectionSlug,
|
||||||
folderFieldName,
|
folderFieldName,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const docID = data.id
|
const docID = data.id
|
||||||
@@ -54,14 +56,14 @@ export const FolderTableCellClient = ({
|
|||||||
console.error('Error moving document to folder', error)
|
console.error('Error moving document to folder', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[config.routes.api, collectionSlug, docID, t],
|
[config.routes.api, collectionSlug, docID, folderFieldName, t],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadFolderName = async () => {
|
const loadFolderName = async () => {
|
||||||
try {
|
try {
|
||||||
const req = await fetch(
|
const req = await fetch(
|
||||||
`${config.routes.api}/${config.folders.slug}${intialFolderID ? `/${intialFolderID}` : ''}`,
|
`${config.routes.api}/${folderCollectionSlug}${intialFolderID ? `/${intialFolderID}` : ''}`,
|
||||||
{
|
{
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -83,7 +85,7 @@ export const FolderTableCellClient = ({
|
|||||||
void loadFolderName()
|
void loadFolderName()
|
||||||
hasLoadedFolderName.current = true
|
hasLoadedFolderName.current = true
|
||||||
}
|
}
|
||||||
}, [])
|
}, [config.routes.api, folderCollectionSlug, intialFolderID, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MoveDocToFolderButton
|
<MoveDocToFolderButton
|
||||||
@@ -94,6 +96,8 @@ export const FolderTableCellClient = ({
|
|||||||
docData={data as FolderOrDocument['value']}
|
docData={data as FolderOrDocument['value']}
|
||||||
docID={docID}
|
docID={docID}
|
||||||
docTitle={docTitle}
|
docTitle={docTitle}
|
||||||
|
folderCollectionSlug={folderCollectionSlug}
|
||||||
|
folderFieldName={folderFieldName}
|
||||||
fromFolderID={fromFolderID}
|
fromFolderID={fromFolderID}
|
||||||
fromFolderName={fromFolderName}
|
fromFolderName={fromFolderName}
|
||||||
modalSlug={`move-doc-to-folder-cell--${docID}`}
|
modalSlug={`move-doc-to-folder-cell--${docID}`}
|
||||||
|
|||||||
@@ -9,11 +9,16 @@ export const FolderTableCell = (props: DefaultServerCellComponentProps) => {
|
|||||||
(props.collectionConfig.upload ? props.rowData?.filename : props.rowData?.title) ||
|
(props.collectionConfig.upload ? props.rowData?.filename : props.rowData?.title) ||
|
||||||
props.rowData.id
|
props.rowData.id
|
||||||
|
|
||||||
|
if (!props.payload.config.folders) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FolderTableCellClient
|
<FolderTableCellClient
|
||||||
collectionSlug={props.collectionSlug}
|
collectionSlug={props.collectionSlug}
|
||||||
data={props.rowData}
|
data={props.rowData}
|
||||||
docTitle={titleToRender}
|
docTitle={titleToRender}
|
||||||
|
folderCollectionSlug={props.payload.config.folders.slug}
|
||||||
folderFieldName={props.payload.config.folders.fieldName}
|
folderFieldName={props.payload.config.folders.fieldName}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import './index.scss'
|
|||||||
const baseClass = 'collection-type'
|
const baseClass = 'collection-type'
|
||||||
|
|
||||||
export function CollectionTypePill() {
|
export function CollectionTypePill() {
|
||||||
const { filterItems, folderCollectionSlug, visibleCollectionSlugs } = useFolder()
|
const { filterItems, folderCollectionSlug, folderCollectionSlugs, visibleCollectionSlugs } =
|
||||||
|
useFolder()
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const { config, getEntityConfig } = useConfig()
|
const { config, getEntityConfig } = useConfig()
|
||||||
|
|
||||||
const [allCollectionOptions] = React.useState(() => {
|
const [allCollectionOptions] = React.useState(() => {
|
||||||
return config.collections.reduce(
|
return config.collections.reduce(
|
||||||
(acc, collection) => {
|
(acc, collection) => {
|
||||||
if (collection.folders) {
|
if (collection.folders && folderCollectionSlugs.includes(collection.slug)) {
|
||||||
acc.push({
|
acc.push({
|
||||||
label: getTranslation(collection.labels?.plural, i18n),
|
label: getTranslation(collection.labels?.plural, i18n),
|
||||||
value: collection.slug,
|
value: collection.slug,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function CurrentFolderActions({ className }: Props) {
|
|||||||
currentFolder,
|
currentFolder,
|
||||||
folderCollectionConfig,
|
folderCollectionConfig,
|
||||||
folderCollectionSlug,
|
folderCollectionSlug,
|
||||||
|
folderFieldName,
|
||||||
folderID,
|
folderID,
|
||||||
moveToFolder,
|
moveToFolder,
|
||||||
renameFolder,
|
renameFolder,
|
||||||
@@ -84,6 +85,8 @@ export function CurrentFolderActions({ className }: Props) {
|
|||||||
<MoveItemsToFolderDrawer
|
<MoveItemsToFolderDrawer
|
||||||
action="moveItemToFolder"
|
action="moveItemToFolder"
|
||||||
drawerSlug={moveToFolderDrawerSlug}
|
drawerSlug={moveToFolderDrawerSlug}
|
||||||
|
folderCollectionSlug={folderCollectionSlug}
|
||||||
|
folderFieldName={folderFieldName}
|
||||||
fromFolderID={currentFolder?.value.id}
|
fromFolderID={currentFolder?.value.id}
|
||||||
fromFolderName={currentFolder?.value._folderOrDocumentTitle}
|
fromFolderName={currentFolder?.value._folderOrDocumentTitle}
|
||||||
itemsToMove={[currentFolder]}
|
itemsToMove={[currentFolder]}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ type ActionProps =
|
|||||||
}
|
}
|
||||||
export type MoveToFolderDrawerProps = {
|
export type MoveToFolderDrawerProps = {
|
||||||
readonly drawerSlug: string
|
readonly drawerSlug: string
|
||||||
|
readonly folderCollectionSlug: string
|
||||||
|
readonly folderFieldName: string
|
||||||
readonly fromFolderID?: number | string
|
readonly fromFolderID?: number | string
|
||||||
readonly fromFolderName?: string
|
readonly fromFolderName?: string
|
||||||
readonly itemsToMove: FolderOrDocument[]
|
readonly itemsToMove: FolderOrDocument[]
|
||||||
@@ -75,11 +77,7 @@ export function MoveItemsToFolderDrawer(props: MoveToFolderDrawerProps) {
|
|||||||
|
|
||||||
function LoadFolderData(props: MoveToFolderDrawerProps) {
|
function LoadFolderData(props: MoveToFolderDrawerProps) {
|
||||||
const {
|
const {
|
||||||
config: {
|
config: { routes, serverURL },
|
||||||
folders: { slug: folderCollectionSlug },
|
|
||||||
routes,
|
|
||||||
serverURL,
|
|
||||||
},
|
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
const [subfolders, setSubfolders] = React.useState<FolderOrDocument[]>([])
|
const [subfolders, setSubfolders] = React.useState<FolderOrDocument[]>([])
|
||||||
const [documents, setDocuments] = React.useState<FolderOrDocument[]>([])
|
const [documents, setDocuments] = React.useState<FolderOrDocument[]>([])
|
||||||
@@ -92,7 +90,7 @@ function LoadFolderData(props: MoveToFolderDrawerProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const folderDataReq = await fetch(
|
const folderDataReq = await fetch(
|
||||||
`${serverURL}${routes.api}/${folderCollectionSlug}/populate-folder-data${props.fromFolderID ? `?folderID=${props.fromFolderID}` : ''}`,
|
`${serverURL}${routes.api}/${props.folderCollectionSlug}/populate-folder-data${props.fromFolderID ? `?folderID=${props.fromFolderID}` : ''}`,
|
||||||
{
|
{
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -122,7 +120,7 @@ function LoadFolderData(props: MoveToFolderDrawerProps) {
|
|||||||
if (!hasLoaded) {
|
if (!hasLoaded) {
|
||||||
void onLoad()
|
void onLoad()
|
||||||
}
|
}
|
||||||
}, [folderCollectionSlug, routes.api, serverURL, hasLoaded, props.fromFolderID])
|
}, [props.folderCollectionSlug, routes.api, serverURL, hasLoaded, props.fromFolderID])
|
||||||
|
|
||||||
if (!hasLoaded) {
|
if (!hasLoaded) {
|
||||||
return <LoadingOverlay />
|
return <LoadingOverlay />
|
||||||
@@ -134,6 +132,7 @@ function LoadFolderData(props: MoveToFolderDrawerProps) {
|
|||||||
breadcrumbs={breadcrumbs}
|
breadcrumbs={breadcrumbs}
|
||||||
documents={documents}
|
documents={documents}
|
||||||
folderCollectionSlugs={[]}
|
folderCollectionSlugs={[]}
|
||||||
|
folderFieldName={props.folderFieldName}
|
||||||
folderID={props.fromFolderID}
|
folderID={props.fromFolderID}
|
||||||
subfolders={subfolders}
|
subfolders={subfolders}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ import './index.scss'
|
|||||||
const baseClass = 'folder-edit-field'
|
const baseClass = 'folder-edit-field'
|
||||||
|
|
||||||
export const FolderEditField = (props: RelationshipFieldServerProps) => {
|
export const FolderEditField = (props: RelationshipFieldServerProps) => {
|
||||||
|
if (props.payload.config.folders === false) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<MoveDocToFolder
|
<MoveDocToFolder
|
||||||
className={baseClass}
|
className={baseClass}
|
||||||
|
folderCollectionSlug={props.payload.config.folders.slug}
|
||||||
folderFieldName={props.payload.config.folders.fieldName}
|
folderFieldName={props.payload.config.folders.fieldName}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ const baseClass = 'move-doc-to-folder'
|
|||||||
export function MoveDocToFolder({
|
export function MoveDocToFolder({
|
||||||
buttonProps,
|
buttonProps,
|
||||||
className = '',
|
className = '',
|
||||||
|
folderCollectionSlug,
|
||||||
folderFieldName,
|
folderFieldName,
|
||||||
}: {
|
}: {
|
||||||
buttonProps?: Partial<ButtonProps>
|
readonly buttonProps?: Partial<ButtonProps>
|
||||||
className?: string
|
readonly className?: string
|
||||||
folderFieldName: string
|
readonly folderCollectionSlug: string
|
||||||
|
readonly folderFieldName: string
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
|
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
|
||||||
@@ -49,7 +51,7 @@ export function MoveDocToFolder({
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function fetchFolderLabel() {
|
async function fetchFolderLabel() {
|
||||||
if (fromFolderID && (typeof fromFolderID === 'string' || typeof fromFolderID === 'number')) {
|
if (fromFolderID && (typeof fromFolderID === 'string' || typeof fromFolderID === 'number')) {
|
||||||
const response = await fetch(`${config.routes.api}/${config.folders.slug}/${fromFolderID}`)
|
const response = await fetch(`${config.routes.api}/${folderCollectionSlug}/${fromFolderID}`)
|
||||||
const folderData = await response.json()
|
const folderData = await response.json()
|
||||||
setFromFolderName(folderData?.name || t('folder:noFolder'))
|
setFromFolderName(folderData?.name || t('folder:noFolder'))
|
||||||
} else {
|
} else {
|
||||||
@@ -58,7 +60,7 @@ export function MoveDocToFolder({
|
|||||||
}
|
}
|
||||||
|
|
||||||
void fetchFolderLabel()
|
void fetchFolderLabel()
|
||||||
}, [config.folders.slug, config.routes.api, fromFolderID, t])
|
}, [folderCollectionSlug, config.routes.api, fromFolderID, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MoveDocToFolderButton
|
<MoveDocToFolderButton
|
||||||
@@ -68,6 +70,8 @@ export function MoveDocToFolder({
|
|||||||
docData={initialData as FolderOrDocument['value']}
|
docData={initialData as FolderOrDocument['value']}
|
||||||
docID={id}
|
docID={id}
|
||||||
docTitle={title}
|
docTitle={title}
|
||||||
|
folderCollectionSlug={folderCollectionSlug}
|
||||||
|
folderFieldName={folderFieldName}
|
||||||
fromFolderID={fromFolderID as number | string}
|
fromFolderID={fromFolderID as number | string}
|
||||||
fromFolderName={fromFolderName}
|
fromFolderName={fromFolderName}
|
||||||
modalSlug={`move-to-folder-${modalID}`}
|
modalSlug={`move-to-folder-${modalID}`}
|
||||||
@@ -87,17 +91,19 @@ export function MoveDocToFolder({
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MoveDocToFolderButtonProps = {
|
type MoveDocToFolderButtonProps = {
|
||||||
buttonProps?: Partial<ButtonProps>
|
readonly buttonProps?: Partial<ButtonProps>
|
||||||
className?: string
|
readonly className?: string
|
||||||
collectionSlug: string
|
readonly collectionSlug: string
|
||||||
docData: FolderOrDocument['value']
|
readonly docData: FolderOrDocument['value']
|
||||||
docID: number | string
|
readonly docID: number | string
|
||||||
docTitle?: string
|
readonly docTitle?: string
|
||||||
fromFolderID?: number | string
|
readonly folderCollectionSlug: string
|
||||||
fromFolderName: string
|
readonly folderFieldName: string
|
||||||
modalSlug: string
|
readonly fromFolderID?: number | string
|
||||||
onConfirm?: (args: { id: number | string; name: string }) => Promise<void> | void
|
readonly fromFolderName: string
|
||||||
skipConfirmModal?: boolean
|
readonly modalSlug: string
|
||||||
|
readonly onConfirm?: (args: { id: number | string; name: string }) => Promise<void> | void
|
||||||
|
readonly skipConfirmModal?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,6 +116,8 @@ export const MoveDocToFolderButton = ({
|
|||||||
docData,
|
docData,
|
||||||
docID,
|
docID,
|
||||||
docTitle,
|
docTitle,
|
||||||
|
folderCollectionSlug,
|
||||||
|
folderFieldName,
|
||||||
fromFolderID,
|
fromFolderID,
|
||||||
fromFolderName,
|
fromFolderName,
|
||||||
modalSlug,
|
modalSlug,
|
||||||
@@ -143,6 +151,8 @@ export const MoveDocToFolderButton = ({
|
|||||||
<MoveItemsToFolderDrawer
|
<MoveItemsToFolderDrawer
|
||||||
action="moveItemToFolder"
|
action="moveItemToFolder"
|
||||||
drawerSlug={drawerSlug}
|
drawerSlug={drawerSlug}
|
||||||
|
folderCollectionSlug={folderCollectionSlug}
|
||||||
|
folderFieldName={folderFieldName}
|
||||||
fromFolderID={fromFolderID}
|
fromFolderID={fromFolderID}
|
||||||
fromFolderName={fromFolderName}
|
fromFolderName={fromFolderName}
|
||||||
itemsToMove={[
|
itemsToMove={[
|
||||||
|
|||||||
@@ -13,14 +13,23 @@ import './index.scss'
|
|||||||
const baseClass = 'list-folder-pills'
|
const baseClass = 'list-folder-pills'
|
||||||
|
|
||||||
type ListFolderPillsProps = {
|
type ListFolderPillsProps = {
|
||||||
collectionConfig: ClientCollectionConfig
|
readonly collectionConfig: ClientCollectionConfig
|
||||||
viewType: 'folders' | 'list'
|
readonly folderCollectionSlug: string
|
||||||
|
readonly viewType: 'folders' | 'list'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListFolderPills({ collectionConfig, viewType }: ListFolderPillsProps) {
|
export function ListFolderPills({
|
||||||
|
collectionConfig,
|
||||||
|
folderCollectionSlug,
|
||||||
|
viewType,
|
||||||
|
}: ListFolderPillsProps) {
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const { config } = useConfig()
|
const { config } = useConfig()
|
||||||
|
|
||||||
|
if (!folderCollectionSlug) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<Button
|
<Button
|
||||||
@@ -35,7 +44,7 @@ export function ListFolderPills({ collectionConfig, viewType }: ListFolderPillsP
|
|||||||
el={viewType === 'list' ? 'link' : 'div'}
|
el={viewType === 'list' ? 'link' : 'div'}
|
||||||
to={formatAdminURL({
|
to={formatAdminURL({
|
||||||
adminRoute: config.routes.admin,
|
adminRoute: config.routes.admin,
|
||||||
path: `/collections/${collectionConfig.slug}/${config.folders.slug}`,
|
path: `/collections/${collectionConfig.slug}/${folderCollectionSlug}`,
|
||||||
serverURL: config.serverURL,
|
serverURL: config.serverURL,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function ListCreateNewDocInFolderButton({
|
|||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
const { closeModal, openModal } = useModal()
|
const { closeModal, openModal } = useModal()
|
||||||
const { config } = useConfig()
|
const { config } = useConfig()
|
||||||
const { folderCollectionConfig, folderID } = useFolder()
|
const { folderCollectionConfig, folderFieldName, folderID } = useFolder()
|
||||||
const [createCollectionSlug, setCreateCollectionSlug] = React.useState<string | undefined>()
|
const [createCollectionSlug, setCreateCollectionSlug] = React.useState<string | undefined>()
|
||||||
const [enabledCollections] = React.useState<ClientCollectionConfig[]>(() =>
|
const [enabledCollections] = React.useState<ClientCollectionConfig[]>(() =>
|
||||||
collectionSlugs.reduce((acc, collectionSlug) => {
|
collectionSlugs.reduce((acc, collectionSlug) => {
|
||||||
@@ -114,7 +114,7 @@ export function ListCreateNewDocInFolderButton({
|
|||||||
collectionSlug={createCollectionSlug}
|
collectionSlug={createCollectionSlug}
|
||||||
drawerSlug={newDocInFolderDrawerSlug}
|
drawerSlug={newDocInFolderDrawerSlug}
|
||||||
initialData={{
|
initialData={{
|
||||||
[config.folders.fieldName]: folderID,
|
[folderFieldName]: folderID,
|
||||||
}}
|
}}
|
||||||
onSave={({ doc }) => {
|
onSave={({ doc }) => {
|
||||||
closeModal(newDocInFolderDrawerSlug)
|
closeModal(newDocInFolderDrawerSlug)
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ export type FolderContextValue = {
|
|||||||
focusedRowIndex: number
|
focusedRowIndex: number
|
||||||
folderCollectionConfig: ClientCollectionConfig
|
folderCollectionConfig: ClientCollectionConfig
|
||||||
folderCollectionSlug: string
|
folderCollectionSlug: string
|
||||||
|
/**
|
||||||
|
* Folder enabled collection slugs that can be populated within the provider
|
||||||
|
*/
|
||||||
|
readonly folderCollectionSlugs?: CollectionSlug[]
|
||||||
folderFieldName: string
|
folderFieldName: string
|
||||||
folderID?: number | string
|
folderID?: number | string
|
||||||
getSelectedItems?: () => FolderOrDocument[]
|
getSelectedItems?: () => FolderOrDocument[]
|
||||||
@@ -86,6 +90,7 @@ const Context = React.createContext<FolderContextValue>({
|
|||||||
focusedRowIndex: -1,
|
focusedRowIndex: -1,
|
||||||
folderCollectionConfig: null,
|
folderCollectionConfig: null,
|
||||||
folderCollectionSlug: '',
|
folderCollectionSlug: '',
|
||||||
|
folderCollectionSlugs: [],
|
||||||
folderFieldName: 'folder',
|
folderFieldName: 'folder',
|
||||||
folderID: undefined,
|
folderID: undefined,
|
||||||
getSelectedItems: () => [],
|
getSelectedItems: () => [],
|
||||||
@@ -158,9 +163,13 @@ export type FolderProviderProps = {
|
|||||||
*/
|
*/
|
||||||
readonly filteredCollectionSlugs?: CollectionSlug[]
|
readonly filteredCollectionSlugs?: CollectionSlug[]
|
||||||
/**
|
/**
|
||||||
* Folder enabled collection slugs
|
* Folder enabled collection slugs that can be populated within the provider
|
||||||
*/
|
*/
|
||||||
readonly folderCollectionSlugs: CollectionSlug[]
|
readonly folderCollectionSlugs: CollectionSlug[]
|
||||||
|
/**
|
||||||
|
* The name of the field that contains the folder relation
|
||||||
|
*/
|
||||||
|
readonly folderFieldName: string
|
||||||
/**
|
/**
|
||||||
* The ID of the current folder
|
* The ID of the current folder
|
||||||
*/
|
*/
|
||||||
@@ -190,6 +199,7 @@ export function FolderProvider({
|
|||||||
documents: allDocumentsFromProps = [],
|
documents: allDocumentsFromProps = [],
|
||||||
filteredCollectionSlugs,
|
filteredCollectionSlugs,
|
||||||
folderCollectionSlugs = [],
|
folderCollectionSlugs = [],
|
||||||
|
folderFieldName,
|
||||||
folderID: _folderIDFromProps = undefined,
|
folderID: _folderIDFromProps = undefined,
|
||||||
search: _searchFromProps,
|
search: _searchFromProps,
|
||||||
sort,
|
sort,
|
||||||
@@ -197,7 +207,6 @@ export function FolderProvider({
|
|||||||
}: FolderProviderProps) {
|
}: FolderProviderProps) {
|
||||||
const parentFolderContext = useFolder()
|
const parentFolderContext = useFolder()
|
||||||
const { config, getEntityConfig } = useConfig()
|
const { config, getEntityConfig } = useConfig()
|
||||||
const folderFieldName = config.folders.fieldName
|
|
||||||
const { routes, serverURL } = config
|
const { routes, serverURL } = config
|
||||||
const drawerDepth = useDrawerDepth()
|
const drawerDepth = useDrawerDepth()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -205,7 +214,9 @@ export function FolderProvider({
|
|||||||
const { startRouteTransition } = useRouteTransition()
|
const { startRouteTransition } = useRouteTransition()
|
||||||
|
|
||||||
const [folderCollectionConfig] = React.useState(() =>
|
const [folderCollectionConfig] = React.useState(() =>
|
||||||
config.collections.find((collection) => collection.slug === config.folders.slug),
|
config.collections.find(
|
||||||
|
(collection) => config.folders && collection.slug === config.folders.slug,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
const folderCollectionSlug = folderCollectionConfig.slug
|
const folderCollectionSlug = folderCollectionConfig.slug
|
||||||
|
|
||||||
@@ -874,7 +885,7 @@ export function FolderProvider({
|
|||||||
setAllSubfolders(sortedAllSubfolders)
|
setAllSubfolders(sortedAllSubfolders)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeFolderID, documents, separateItems, sortItems, subfolders],
|
[activeFolderID, documents, separateItems, sortItems, subfolders, parentFolderContext],
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -964,7 +975,7 @@ export function FolderProvider({
|
|||||||
const formattedItems: FolderOrDocument[] = docs.map<FolderOrDocument>(
|
const formattedItems: FolderOrDocument[] = docs.map<FolderOrDocument>(
|
||||||
(doc: Document) =>
|
(doc: Document) =>
|
||||||
formatFolderOrDocumentItem({
|
formatFolderOrDocumentItem({
|
||||||
folderFieldName: config.folders.fieldName,
|
folderFieldName,
|
||||||
isUpload: Boolean(collectionConfig.upload),
|
isUpload: Boolean(collectionConfig.upload),
|
||||||
relationTo: collectionSlug,
|
relationTo: collectionSlug,
|
||||||
useAsTitle: collectionConfig.admin.useAsTitle,
|
useAsTitle: collectionConfig.admin.useAsTitle,
|
||||||
@@ -1039,6 +1050,7 @@ export function FolderProvider({
|
|||||||
clearSelections,
|
clearSelections,
|
||||||
serverURL,
|
serverURL,
|
||||||
routes.api,
|
routes.api,
|
||||||
|
folderFieldName,
|
||||||
t,
|
t,
|
||||||
getFolderData,
|
getFolderData,
|
||||||
getEntityConfig,
|
getEntityConfig,
|
||||||
@@ -1112,6 +1124,7 @@ export function FolderProvider({
|
|||||||
focusedRowIndex,
|
focusedRowIndex,
|
||||||
folderCollectionConfig,
|
folderCollectionConfig,
|
||||||
folderCollectionSlug,
|
folderCollectionSlug,
|
||||||
|
folderCollectionSlugs,
|
||||||
folderFieldName,
|
folderFieldName,
|
||||||
folderID: activeFolderID,
|
folderID: activeFolderID,
|
||||||
getSelectedItems,
|
getSelectedItems,
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export function DefaultBrowseByFolderView(
|
|||||||
filterItems,
|
filterItems,
|
||||||
focusedRowIndex,
|
focusedRowIndex,
|
||||||
folderCollectionConfig,
|
folderCollectionConfig,
|
||||||
|
folderFieldName,
|
||||||
folderID,
|
folderID,
|
||||||
getSelectedItems,
|
getSelectedItems,
|
||||||
isDragging,
|
isDragging,
|
||||||
@@ -133,7 +134,7 @@ export function DefaultBrowseByFolderView(
|
|||||||
const collectionConfig = getEntityConfig({ collectionSlug })
|
const collectionConfig = getEntityConfig({ collectionSlug })
|
||||||
void addItems([
|
void addItems([
|
||||||
formatFolderOrDocumentItem({
|
formatFolderOrDocumentItem({
|
||||||
folderFieldName: config.folders.fieldName,
|
folderFieldName,
|
||||||
isUpload: Boolean(collectionConfig?.upload),
|
isUpload: Boolean(collectionConfig?.upload),
|
||||||
relationTo: collectionSlug,
|
relationTo: collectionSlug,
|
||||||
useAsTitle: collectionConfig.admin.useAsTitle,
|
useAsTitle: collectionConfig.admin.useAsTitle,
|
||||||
@@ -141,7 +142,7 @@ export function DefaultBrowseByFolderView(
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
[getEntityConfig, addItems, config.folders.fieldName],
|
[getEntityConfig, addItems, folderFieldName],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedItemKeys = React.useMemo(() => {
|
const selectedItemKeys = React.useMemo(() => {
|
||||||
@@ -157,12 +158,15 @@ export function DefaultBrowseByFolderView(
|
|||||||
)
|
)
|
||||||
}, [getSelectedItems])
|
}, [getSelectedItems])
|
||||||
|
|
||||||
const handleSetViewType = React.useCallback((view: 'grid' | 'list') => {
|
const handleSetViewType = React.useCallback(
|
||||||
void setPreference('browse-by-folder', {
|
(view: 'grid' | 'list') => {
|
||||||
viewPreference: view,
|
void setPreference('browse-by-folder', {
|
||||||
})
|
viewPreference: view,
|
||||||
setActiveView(view)
|
})
|
||||||
}, [])
|
setActiveView(view)
|
||||||
|
},
|
||||||
|
[setPreference],
|
||||||
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!drawerDepth) {
|
if (!drawerDepth) {
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const ListSelection: React.FC<ListSelectionProps> = ({
|
|||||||
const {
|
const {
|
||||||
clearSelections,
|
clearSelections,
|
||||||
currentFolder,
|
currentFolder,
|
||||||
|
folderCollectionSlug,
|
||||||
|
folderFieldName,
|
||||||
folderID,
|
folderID,
|
||||||
getSelectedItems,
|
getSelectedItems,
|
||||||
moveToFolder,
|
moveToFolder,
|
||||||
@@ -71,7 +73,7 @@ export const ListSelection: React.FC<ListSelectionProps> = ({
|
|||||||
const count = items.length
|
const count = items.length
|
||||||
const singleNonFolderCollectionSelected =
|
const singleNonFolderCollectionSelected =
|
||||||
Object.keys(groupedSelections).length === 1 &&
|
Object.keys(groupedSelections).length === 1 &&
|
||||||
Object.keys(groupedSelections)[0] !== config.folders.slug
|
Object.keys(groupedSelections)[0] !== folderCollectionSlug
|
||||||
const collectionConfig = singleNonFolderCollectionSelected
|
const collectionConfig = singleNonFolderCollectionSelected
|
||||||
? config.collections.find((collection) => {
|
? config.collections.find((collection) => {
|
||||||
return collection.slug === Object.keys(groupedSelections)[0]
|
return collection.slug === Object.keys(groupedSelections)[0]
|
||||||
@@ -146,6 +148,8 @@ export const ListSelection: React.FC<ListSelectionProps> = ({
|
|||||||
<MoveItemsToFolderDrawer
|
<MoveItemsToFolderDrawer
|
||||||
action="moveItemsToFolder"
|
action="moveItemsToFolder"
|
||||||
drawerSlug={moveToFolderDrawerSlug}
|
drawerSlug={moveToFolderDrawerSlug}
|
||||||
|
folderCollectionSlug={folderCollectionSlug}
|
||||||
|
folderFieldName={folderFieldName}
|
||||||
fromFolderID={folderID}
|
fromFolderID={folderID}
|
||||||
fromFolderName={currentFolder?.value?._folderOrDocumentTitle}
|
fromFolderName={currentFolder?.value?._folderOrDocumentTitle}
|
||||||
itemsToMove={getSelectedItems()}
|
itemsToMove={getSelectedItems()}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
|
|||||||
filterItems,
|
filterItems,
|
||||||
focusedRowIndex,
|
focusedRowIndex,
|
||||||
folderCollectionConfig,
|
folderCollectionConfig,
|
||||||
|
folderCollectionSlug,
|
||||||
|
folderFieldName,
|
||||||
getSelectedItems,
|
getSelectedItems,
|
||||||
isDragging,
|
isDragging,
|
||||||
lastSelectedIndex,
|
lastSelectedIndex,
|
||||||
@@ -110,7 +112,7 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
|
|||||||
const collectionConfig = getEntityConfig({ collectionSlug })
|
const collectionConfig = getEntityConfig({ collectionSlug })
|
||||||
void addItems([
|
void addItems([
|
||||||
formatFolderOrDocumentItem({
|
formatFolderOrDocumentItem({
|
||||||
folderFieldName: config.folders.fieldName,
|
folderFieldName,
|
||||||
isUpload: Boolean(collectionConfig?.upload),
|
isUpload: Boolean(collectionConfig?.upload),
|
||||||
relationTo: collectionSlug,
|
relationTo: collectionSlug,
|
||||||
useAsTitle: collectionConfig.admin.useAsTitle,
|
useAsTitle: collectionConfig.admin.useAsTitle,
|
||||||
@@ -118,7 +120,7 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
[getEntityConfig, addItems, config.folders.fieldName],
|
[getEntityConfig, addItems, folderFieldName],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedItemKeys = React.useMemo(() => {
|
const selectedItemKeys = React.useMemo(() => {
|
||||||
@@ -134,12 +136,15 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
|
|||||||
)
|
)
|
||||||
}, [getSelectedItems])
|
}, [getSelectedItems])
|
||||||
|
|
||||||
const handleSetViewType = React.useCallback((view: 'grid' | 'list') => {
|
const handleSetViewType = React.useCallback(
|
||||||
void setPreference(`${collectionSlug}-collection-folder`, {
|
(view: 'grid' | 'list') => {
|
||||||
viewPreference: view,
|
void setPreference(`${collectionSlug}-collection-folder`, {
|
||||||
})
|
viewPreference: view,
|
||||||
setActiveView(view)
|
})
|
||||||
}, [])
|
setActiveView(view)
|
||||||
|
},
|
||||||
|
[collectionSlug, setPreference],
|
||||||
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!drawerDepth) {
|
if (!drawerDepth) {
|
||||||
@@ -213,11 +218,14 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
|
|||||||
key="list-selection"
|
key="list-selection"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
<ListFolderPills
|
config.folders && collectionConfig.folders && (
|
||||||
collectionConfig={collectionConfig}
|
<ListFolderPills
|
||||||
key="list-header-buttons"
|
collectionConfig={collectionConfig}
|
||||||
viewType="folders"
|
folderCollectionSlug={folderCollectionSlug}
|
||||||
/>,
|
key="list-header-buttons"
|
||||||
|
viewType="folders"
|
||||||
|
/>
|
||||||
|
),
|
||||||
].filter(Boolean)}
|
].filter(Boolean)}
|
||||||
AfterListHeaderContent={Description}
|
AfterListHeaderContent={Description}
|
||||||
title={getTranslation(labels?.plural, i18n)}
|
title={getTranslation(labels?.plural, i18n)}
|
||||||
|
|||||||
@@ -107,9 +107,10 @@ export const CollectionListHeader: React.FC<ListHeaderProps> = ({
|
|||||||
label={getTranslation(collectionConfig?.labels?.plural, i18n)}
|
label={getTranslation(collectionConfig?.labels?.plural, i18n)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
collectionConfig.folders && (
|
collectionConfig.folders && config.folders && (
|
||||||
<ListFolderPills
|
<ListFolderPills
|
||||||
collectionConfig={collectionConfig}
|
collectionConfig={collectionConfig}
|
||||||
|
folderCollectionSlug={config.folders.slug}
|
||||||
key="list-header-buttons"
|
key="list-header-buttons"
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ export const testEslintConfig = [
|
|||||||
'runFilterOptionsTest',
|
'runFilterOptionsTest',
|
||||||
'assertNetworkRequests',
|
'assertNetworkRequests',
|
||||||
'assertRequestBody',
|
'assertRequestBody',
|
||||||
|
'expectNoResultsAndCreateFolderButton',
|
||||||
|
'createFolder',
|
||||||
|
'createFolderFromDoc',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
0
test/folders-browse-by-disabled/README.md
Normal file
0
test/folders-browse-by-disabled/README.md
Normal file
17
test/folders-browse-by-disabled/collections/Posts/index.ts
Normal file
17
test/folders-browse-by-disabled/collections/Posts/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import { postSlug } from '../../shared.js'
|
||||||
|
|
||||||
|
export const Posts: CollectionConfig = {
|
||||||
|
slug: postSlug,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
},
|
||||||
|
folders: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
49
test/folders-browse-by-disabled/config.ts
Normal file
49
test/folders-browse-by-disabled/config.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'path'
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { Posts } from './collections/Posts/index.js'
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
admin: {
|
||||||
|
importMap: {
|
||||||
|
baseDir: path.resolve(dirname),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
folders: {
|
||||||
|
// debug: true,
|
||||||
|
collectionOverrides: [
|
||||||
|
({ collection }) => {
|
||||||
|
return collection
|
||||||
|
},
|
||||||
|
],
|
||||||
|
browseByFolder: false,
|
||||||
|
},
|
||||||
|
collections: [Posts],
|
||||||
|
globals: [
|
||||||
|
{
|
||||||
|
slug: 'global',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// await seed(payload)
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
|
},
|
||||||
|
})
|
||||||
44
test/folders-browse-by-disabled/e2e.spec.ts
Normal file
44
test/folders-browse-by-disabled/e2e.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { reInitializeDB } from 'helpers/reInitializeDB.js'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
|
||||||
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||||
|
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
test.describe('Browse By Folders Disabled', () => {
|
||||||
|
let page: Page
|
||||||
|
let serverURL: string
|
||||||
|
|
||||||
|
test.beforeAll(async ({ browser }, testInfo) => {
|
||||||
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||||
|
|
||||||
|
const { serverURL: serverFromInit } = await initPayloadE2ENoConfig({ dirname })
|
||||||
|
serverURL = serverFromInit
|
||||||
|
|
||||||
|
const context = await browser.newContext()
|
||||||
|
page = await context.newPage()
|
||||||
|
initPageConsoleErrorCatch(page)
|
||||||
|
await ensureCompilationIsDone({ page, serverURL })
|
||||||
|
})
|
||||||
|
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
await reInitializeDB({
|
||||||
|
serverURL,
|
||||||
|
snapshotKey: 'BrowseByFoldersDisabledTest',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not show the browse-by-folder button in the nav', async () => {
|
||||||
|
await page.goto(`${serverURL}/admin`)
|
||||||
|
await page.locator('#nav-toggler button.nav-toggler').click()
|
||||||
|
await expect(page.locator('#nav-toggler button.nav-toggler--is-open')).toBeVisible()
|
||||||
|
await expect(page.locator('.browse-by-folder-button')).toBeHidden()
|
||||||
|
})
|
||||||
|
})
|
||||||
195
test/folders-browse-by-disabled/int.spec.ts
Normal file
195
test/folders-browse-by-disabled/int.spec.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import type { Payload } from 'payload'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||||
|
|
||||||
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
|
let payload: Payload
|
||||||
|
let restClient: NextRESTClient
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
describe('folders', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (typeof payload.db.destroy === 'function') {
|
||||||
|
await payload.db.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await payload.delete({
|
||||||
|
collection: 'posts',
|
||||||
|
depth: 0,
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await payload.delete({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
depth: 0,
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('folder > subfolder querying', () => {
|
||||||
|
it('should populate subfolders for folder by ID', async () => {
|
||||||
|
const parentFolder = await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Parent Folder',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const folderIDFromParams = parentFolder.id
|
||||||
|
|
||||||
|
await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Nested 1',
|
||||||
|
folder: folderIDFromParams,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Nested 2',
|
||||||
|
folder: folderIDFromParams,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const parentFolderQuery = await payload.findByID({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
id: folderIDFromParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parentFolderQuery.documentsAndFolders.docs).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('folder > file querying', () => {
|
||||||
|
it('should populate files for folder by ID', async () => {
|
||||||
|
const parentFolder = await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Parent Folder',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const folderIDFromParams = parentFolder.id
|
||||||
|
|
||||||
|
await payload.create({
|
||||||
|
collection: 'posts',
|
||||||
|
data: {
|
||||||
|
title: 'Post 1',
|
||||||
|
folder: folderIDFromParams,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await payload.create({
|
||||||
|
collection: 'posts',
|
||||||
|
data: {
|
||||||
|
title: 'Post 2',
|
||||||
|
folder: folderIDFromParams,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const parentFolderQuery = await payload.findByID({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
id: folderIDFromParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parentFolderQuery.documentsAndFolders.docs).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('hooks', () => {
|
||||||
|
it('reparentChildFolder should change the child after updating the parent', async () => {
|
||||||
|
const parentFolder = await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Parent Folder',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const childFolder = await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Child Folder',
|
||||||
|
folder: parentFolder,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await payload.update({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: { folder: childFolder },
|
||||||
|
id: parentFolder.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const parentAfter = await payload.findByID({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
id: parentFolder.id,
|
||||||
|
depth: 0,
|
||||||
|
})
|
||||||
|
const childAfter = await payload.findByID({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
id: childFolder.id,
|
||||||
|
depth: 0,
|
||||||
|
})
|
||||||
|
expect(childAfter.folder).toBeFalsy()
|
||||||
|
expect(parentAfter.folder).toBe(childFolder.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dissasociateAfterDelete should delete _folder value in children after deleting the folder', async () => {
|
||||||
|
const parentFolder = await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Parent Folder',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const post = await payload.create({ collection: 'posts', data: { folder: parentFolder } })
|
||||||
|
|
||||||
|
await payload.delete({ collection: 'payload-folders', id: parentFolder.id })
|
||||||
|
const postAfter = await payload.findByID({ collection: 'posts', id: post.id })
|
||||||
|
expect(postAfter.folder).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deleteSubfoldersBeforeDelete deletes subfolders after deleting the parent folder', async () => {
|
||||||
|
const parentFolder = await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Parent Folder',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const childFolder = await payload.create({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
data: {
|
||||||
|
name: 'Child Folder',
|
||||||
|
folder: parentFolder,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await payload.delete({ collection: 'payload-folders', id: parentFolder.id })
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
payload.findByID({
|
||||||
|
collection: 'payload-folders',
|
||||||
|
id: childFolder.id,
|
||||||
|
disableErrors: true,
|
||||||
|
}),
|
||||||
|
).resolves.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
340
test/folders-browse-by-disabled/payload-types.ts
Normal file
340
test/folders-browse-by-disabled/payload-types.ts
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported timezones in IANA format.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "supportedTimezones".
|
||||||
|
*/
|
||||||
|
export type SupportedTimezones =
|
||||||
|
| 'Pacific/Midway'
|
||||||
|
| 'Pacific/Niue'
|
||||||
|
| 'Pacific/Honolulu'
|
||||||
|
| 'Pacific/Rarotonga'
|
||||||
|
| 'America/Anchorage'
|
||||||
|
| 'Pacific/Gambier'
|
||||||
|
| 'America/Los_Angeles'
|
||||||
|
| 'America/Tijuana'
|
||||||
|
| 'America/Denver'
|
||||||
|
| 'America/Phoenix'
|
||||||
|
| 'America/Chicago'
|
||||||
|
| 'America/Guatemala'
|
||||||
|
| 'America/New_York'
|
||||||
|
| 'America/Bogota'
|
||||||
|
| 'America/Caracas'
|
||||||
|
| 'America/Santiago'
|
||||||
|
| 'America/Buenos_Aires'
|
||||||
|
| 'America/Sao_Paulo'
|
||||||
|
| 'Atlantic/South_Georgia'
|
||||||
|
| 'Atlantic/Azores'
|
||||||
|
| 'Atlantic/Cape_Verde'
|
||||||
|
| 'Europe/London'
|
||||||
|
| 'Europe/Berlin'
|
||||||
|
| 'Africa/Lagos'
|
||||||
|
| 'Europe/Athens'
|
||||||
|
| 'Africa/Cairo'
|
||||||
|
| 'Europe/Moscow'
|
||||||
|
| 'Asia/Riyadh'
|
||||||
|
| 'Asia/Dubai'
|
||||||
|
| 'Asia/Baku'
|
||||||
|
| 'Asia/Karachi'
|
||||||
|
| 'Asia/Tashkent'
|
||||||
|
| 'Asia/Calcutta'
|
||||||
|
| 'Asia/Dhaka'
|
||||||
|
| 'Asia/Almaty'
|
||||||
|
| 'Asia/Jakarta'
|
||||||
|
| 'Asia/Bangkok'
|
||||||
|
| 'Asia/Shanghai'
|
||||||
|
| 'Asia/Singapore'
|
||||||
|
| 'Asia/Tokyo'
|
||||||
|
| 'Asia/Seoul'
|
||||||
|
| 'Australia/Brisbane'
|
||||||
|
| 'Australia/Sydney'
|
||||||
|
| 'Pacific/Guam'
|
||||||
|
| 'Pacific/Noumea'
|
||||||
|
| 'Pacific/Auckland'
|
||||||
|
| 'Pacific/Fiji';
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
auth: {
|
||||||
|
users: UserAuthOperations;
|
||||||
|
};
|
||||||
|
blocks: {};
|
||||||
|
collections: {
|
||||||
|
posts: Post;
|
||||||
|
users: User;
|
||||||
|
'payload-folders': FolderInterface;
|
||||||
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
|
'payload-preferences': PayloadPreference;
|
||||||
|
'payload-migrations': PayloadMigration;
|
||||||
|
};
|
||||||
|
collectionsJoins: {
|
||||||
|
'payload-folders': {
|
||||||
|
documentsAndFolders: 'payload-folders' | 'posts';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
collectionsSelect: {
|
||||||
|
posts: PostsSelect<false> | PostsSelect<true>;
|
||||||
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
|
'payload-folders': PayloadFoldersSelect<false> | PayloadFoldersSelect<true>;
|
||||||
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: string;
|
||||||
|
};
|
||||||
|
globals: {
|
||||||
|
global: Global;
|
||||||
|
};
|
||||||
|
globalsSelect: {
|
||||||
|
global: GlobalSelect<false> | GlobalSelect<true>;
|
||||||
|
};
|
||||||
|
locale: null;
|
||||||
|
user: User & {
|
||||||
|
collection: 'users';
|
||||||
|
};
|
||||||
|
jobs: {
|
||||||
|
tasks: unknown;
|
||||||
|
workflows: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface UserAuthOperations {
|
||||||
|
forgotPassword: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
login: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
registerFirstUser: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
unlock: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts".
|
||||||
|
*/
|
||||||
|
export interface Post {
|
||||||
|
id: string;
|
||||||
|
title?: string | null;
|
||||||
|
folder?: (string | null) | FolderInterface;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-folders".
|
||||||
|
*/
|
||||||
|
export interface FolderInterface {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
folder?: (string | null) | FolderInterface;
|
||||||
|
documentsAndFolders?: {
|
||||||
|
docs?: (
|
||||||
|
| {
|
||||||
|
relationTo?: 'payload-folders';
|
||||||
|
value: string | FolderInterface;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
relationTo?: 'posts';
|
||||||
|
value: string | Post;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
totalDocs?: number;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users".
|
||||||
|
*/
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
email: string;
|
||||||
|
resetPasswordToken?: string | null;
|
||||||
|
resetPasswordExpiration?: string | null;
|
||||||
|
salt?: string | null;
|
||||||
|
hash?: string | null;
|
||||||
|
loginAttempts?: number | null;
|
||||||
|
lockUntil?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocument {
|
||||||
|
id: string;
|
||||||
|
document?:
|
||||||
|
| ({
|
||||||
|
relationTo: 'posts';
|
||||||
|
value: string | Post;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'payload-folders';
|
||||||
|
value: string | FolderInterface;
|
||||||
|
} | null);
|
||||||
|
globalSlug?: string | null;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreference {
|
||||||
|
id: string;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
};
|
||||||
|
key?: string | null;
|
||||||
|
value?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigration {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
batch?: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts_select".
|
||||||
|
*/
|
||||||
|
export interface PostsSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
folder?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users_select".
|
||||||
|
*/
|
||||||
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
email?: T;
|
||||||
|
resetPasswordToken?: T;
|
||||||
|
resetPasswordExpiration?: T;
|
||||||
|
salt?: T;
|
||||||
|
hash?: T;
|
||||||
|
loginAttempts?: T;
|
||||||
|
lockUntil?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-folders_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadFoldersSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
folder?: T;
|
||||||
|
documentsAndFolders?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||||
|
document?: T;
|
||||||
|
globalSlug?: T;
|
||||||
|
user?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||||
|
user?: T;
|
||||||
|
key?: T;
|
||||||
|
value?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
batch?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "global".
|
||||||
|
*/
|
||||||
|
export interface Global {
|
||||||
|
id: string;
|
||||||
|
title?: string | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "global_select".
|
||||||
|
*/
|
||||||
|
export interface GlobalSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
globalType?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "auth".
|
||||||
|
*/
|
||||||
|
export interface Auth {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare module 'payload' {
|
||||||
|
// @ts-ignore
|
||||||
|
export interface GeneratedTypes extends Config {}
|
||||||
|
}
|
||||||
1
test/folders-browse-by-disabled/shared.ts
Normal file
1
test/folders-browse-by-disabled/shared.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const postSlug = 'posts'
|
||||||
13
test/folders-browse-by-disabled/tsconfig.eslint.json
Normal file
13
test/folders-browse-by-disabled/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
23
test/folders/collections/OmittedFromBrowseBy/index.ts
Normal file
23
test/folders/collections/OmittedFromBrowseBy/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import { omittedFromBrowseBySlug } from '../../shared.js'
|
||||||
|
|
||||||
|
export const OmittedFromBrowseBy: CollectionConfig = {
|
||||||
|
slug: omittedFromBrowseBySlug,
|
||||||
|
labels: {
|
||||||
|
singular: 'Omitted From Browse By',
|
||||||
|
plural: 'Omitted From Browse By',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
},
|
||||||
|
folders: {
|
||||||
|
browseByFolder: false,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@ import { devUser } from '../credentials.js'
|
|||||||
import { Autosave } from './collections/Autosave/index.js'
|
import { Autosave } from './collections/Autosave/index.js'
|
||||||
import { Drafts } from './collections/Drafts/index.js'
|
import { Drafts } from './collections/Drafts/index.js'
|
||||||
import { Media } from './collections/Media/index.js'
|
import { Media } from './collections/Media/index.js'
|
||||||
|
import { OmittedFromBrowseBy } from './collections/OmittedFromBrowseBy/index.js'
|
||||||
import { Posts } from './collections/Posts/index.js'
|
import { Posts } from './collections/Posts/index.js'
|
||||||
import { seed } from './seed/index.js'
|
// import { seed } from './seed/index.js'
|
||||||
|
|
||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
admin: {
|
admin: {
|
||||||
@@ -18,8 +19,13 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
folders: {
|
folders: {
|
||||||
// debug: true,
|
// debug: true,
|
||||||
|
collectionOverrides: [
|
||||||
|
({ collection }) => {
|
||||||
|
return collection
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
collections: [Posts, Media, Drafts, Autosave],
|
collections: [Posts, Media, Drafts, Autosave, OmittedFromBrowseBy],
|
||||||
globals: [
|
globals: [
|
||||||
{
|
{
|
||||||
slug: 'global',
|
slug: 'global',
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ import { fileURLToPath } from 'url'
|
|||||||
|
|
||||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
|
import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
|
||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
|
import { clickFolderCard } from '../helpers/folders/clickFolderCard.js'
|
||||||
|
import { createFolder } from '../helpers/folders/createFolder.js'
|
||||||
|
import { createFolderFromDoc } from '../helpers/folders/createFolderFromDoc.js'
|
||||||
|
import { expectNoResultsAndCreateFolderButton } from '../helpers/folders/expectNoResultsAndCreateFolderButton.js'
|
||||||
|
import { selectFolderAndConfirmMove } from '../helpers/folders/selectFolderAndConfirmMove.js'
|
||||||
|
import { selectFolderAndConfirmMoveFromList } from '../helpers/folders/selectFolderAndConfirmMoveFromList.js'
|
||||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||||
import { postSlug } from './shared.js'
|
import { omittedFromBrowseBySlug, postSlug } from './shared.js'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -17,6 +23,7 @@ const dirname = path.dirname(filename)
|
|||||||
test.describe('Folders', () => {
|
test.describe('Folders', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let postURL: AdminUrlUtil
|
let postURL: AdminUrlUtil
|
||||||
|
let OmittedFromBrowseBy: AdminUrlUtil
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
|
|
||||||
test.beforeAll(async ({ browser }, testInfo) => {
|
test.beforeAll(async ({ browser }, testInfo) => {
|
||||||
@@ -25,6 +32,7 @@ test.describe('Folders', () => {
|
|||||||
const { serverURL: serverFromInit } = await initPayloadE2ENoConfig({ dirname })
|
const { serverURL: serverFromInit } = await initPayloadE2ENoConfig({ dirname })
|
||||||
serverURL = serverFromInit
|
serverURL = serverFromInit
|
||||||
postURL = new AdminUrlUtil(serverURL, postSlug)
|
postURL = new AdminUrlUtil(serverURL, postSlug)
|
||||||
|
OmittedFromBrowseBy = new AdminUrlUtil(serverURL, omittedFromBrowseBySlug)
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -40,31 +48,29 @@ test.describe('Folders', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test.describe('No folders', () => {
|
test.describe('No folders', () => {
|
||||||
/* eslint-disable playwright/expect-expect */
|
|
||||||
test('should show no results and create button in folder view', async () => {
|
test('should show no results and create button in folder view', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await expectNoResultsAndCreateFolderButton()
|
await expectNoResultsAndCreateFolderButton({ page })
|
||||||
})
|
})
|
||||||
/* eslint-disable playwright/expect-expect */
|
|
||||||
test('should show no results and create button in document view', async () => {
|
test('should show no results and create button in document view', async () => {
|
||||||
await page.goto(postURL.create)
|
await page.goto(postURL.create)
|
||||||
const folderButton = page.getByRole('button', { name: 'No Folder' })
|
const folderButton = page.getByRole('button', { name: 'No Folder' })
|
||||||
await expect(folderButton).toBeVisible()
|
await expect(folderButton).toBeVisible()
|
||||||
await folderButton.click()
|
await folderButton.click()
|
||||||
await expectNoResultsAndCreateFolderButton()
|
await expectNoResultsAndCreateFolderButton({ page })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Creating folders', () => {
|
test.describe('Creating folders', () => {
|
||||||
test('should create new folder from folder view', async () => {
|
test('should create new folder from folder view', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('New Folder From Root')
|
await createFolder({ folderName: 'New Folder From Root', page })
|
||||||
})
|
})
|
||||||
|
|
||||||
/* eslint-disable playwright/expect-expect */
|
|
||||||
test('should create new folder from collection view', async () => {
|
test('should create new folder from collection view', async () => {
|
||||||
await page.goto(postURL.byFolder)
|
await page.goto(postURL.byFolder)
|
||||||
await createFolder('New Folder From Collection', true)
|
await createFolder({ folderName: 'New Folder From Collection', fromDropdown: true, page })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should create new folder from document view', async () => {
|
test('should create new folder from document view', async () => {
|
||||||
@@ -72,7 +78,7 @@ test.describe('Folders', () => {
|
|||||||
await createPostWithNoFolder()
|
await createPostWithNoFolder()
|
||||||
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
||||||
await folderPill.click()
|
await folderPill.click()
|
||||||
await createFolderFromDoc('New Folder From Doc')
|
await createFolderFromDoc({ folderName: 'New Folder From Doc', page })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -85,8 +91,8 @@ test.describe('Folders', () => {
|
|||||||
|
|
||||||
test('should rename folder', async () => {
|
test('should rename folder', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Test Folder')
|
await createFolder({ folderName: 'Test Folder', page })
|
||||||
await clickFolderCard('Test Folder')
|
await clickFolderCard({ folderName: 'Test Folder', page })
|
||||||
const renameButton = page.locator('.list-selection__actions button', {
|
const renameButton = page.locator('.list-selection__actions button', {
|
||||||
hasText: 'Rename',
|
hasText: 'Rename',
|
||||||
})
|
})
|
||||||
@@ -108,8 +114,8 @@ test.describe('Folders', () => {
|
|||||||
|
|
||||||
test('should delete folder', async () => {
|
test('should delete folder', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Delete This Folder')
|
await createFolder({ folderName: 'Delete This Folder', page })
|
||||||
await clickFolderCard('Delete This Folder')
|
await clickFolderCard({ folderName: 'Delete This Folder', page })
|
||||||
const deleteButton = page.locator('.list-selection__actions button', {
|
const deleteButton = page.locator('.list-selection__actions button', {
|
||||||
hasText: 'Delete',
|
hasText: 'Delete',
|
||||||
})
|
})
|
||||||
@@ -125,12 +131,12 @@ test.describe('Folders', () => {
|
|||||||
|
|
||||||
test('should delete folder but not delete documents', async () => {
|
test('should delete folder but not delete documents', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Folder With Documents')
|
await createFolder({ folderName: 'Folder With Documents', page })
|
||||||
await createPostWithExistingFolder('Document 1', 'Folder With Documents')
|
await createPostWithExistingFolder('Document 1', 'Folder With Documents')
|
||||||
await createPostWithExistingFolder('Document 2', 'Folder With Documents')
|
await createPostWithExistingFolder('Document 2', 'Folder With Documents')
|
||||||
|
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await clickFolderCard('Folder With Documents')
|
await clickFolderCard({ folderName: 'Folder With Documents', page })
|
||||||
const deleteButton = page.locator('.list-selection__actions button', {
|
const deleteButton = page.locator('.list-selection__actions button', {
|
||||||
hasText: 'Delete',
|
hasText: 'Delete',
|
||||||
})
|
})
|
||||||
@@ -152,9 +158,9 @@ test.describe('Folders', () => {
|
|||||||
|
|
||||||
test('should move folder', async () => {
|
test('should move folder', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Move Into This Folder')
|
await createFolder({ folderName: 'Move Into This Folder', page })
|
||||||
await createFolder('Move Me')
|
await createFolder({ folderName: 'Move Me', page })
|
||||||
await clickFolderCard('Move Me')
|
await clickFolderCard({ folderName: 'Move Me', page })
|
||||||
const moveButton = page.locator('.list-selection__actions button', {
|
const moveButton = page.locator('.list-selection__actions button', {
|
||||||
hasText: 'Move',
|
hasText: 'Move',
|
||||||
})
|
})
|
||||||
@@ -187,8 +193,8 @@ test.describe('Folders', () => {
|
|||||||
// this test currently fails in postgres
|
// this test currently fails in postgres
|
||||||
test('should create new document from folder', async () => {
|
test('should create new document from folder', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Create New Here')
|
await createFolder({ folderName: 'Create New Here', page })
|
||||||
await clickFolderCard('Create New Here', true)
|
await clickFolderCard({ folderName: 'Create New Here', page, doubleClick: true })
|
||||||
const createDocButton = page.locator('.create-new-doc-in-folder__popup-button', {
|
const createDocButton = page.locator('.create-new-doc-in-folder__popup-button', {
|
||||||
hasText: 'Create document',
|
hasText: 'Create document',
|
||||||
})
|
})
|
||||||
@@ -212,11 +218,10 @@ test.describe('Folders', () => {
|
|||||||
await expect(folderCard).toBeVisible()
|
await expect(folderCard).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
// this test currently fails in postgres
|
|
||||||
test('should create nested folder from folder view', async () => {
|
test('should create nested folder from folder view', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Parent Folder')
|
await createFolder({ folderName: 'Parent Folder', page })
|
||||||
await clickFolderCard('Parent Folder', true)
|
await clickFolderCard({ folderName: 'Parent Folder', page, doubleClick: true })
|
||||||
const pageTitle = page.locator('h1.list-header__title')
|
const pageTitle = page.locator('h1.list-header__title')
|
||||||
await expect(pageTitle).toHaveText('Parent Folder')
|
await expect(pageTitle).toHaveText('Parent Folder')
|
||||||
|
|
||||||
@@ -248,7 +253,7 @@ test.describe('Folders', () => {
|
|||||||
|
|
||||||
test('should toggle between grid and list view', async () => {
|
test('should toggle between grid and list view', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Test Folder')
|
await createFolder({ folderName: 'Test Folder', page })
|
||||||
const listViewButton = page.locator('.folder-view-toggle-button').nth(1)
|
const listViewButton = page.locator('.folder-view-toggle-button').nth(1)
|
||||||
await listViewButton.click()
|
await listViewButton.click()
|
||||||
const listView = page.locator('.simple-table')
|
const listView = page.locator('.simple-table')
|
||||||
@@ -262,9 +267,9 @@ test.describe('Folders', () => {
|
|||||||
|
|
||||||
test('should sort folders', async () => {
|
test('should sort folders', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('A Folder')
|
await createFolder({ folderName: 'A Folder', page })
|
||||||
await createFolder('B Folder')
|
await createFolder({ folderName: 'B Folder', page })
|
||||||
await createFolder('C Folder')
|
await createFolder({ folderName: 'C Folder', page })
|
||||||
|
|
||||||
const firstFolderCard = page.locator('.folder-file-card__name').first()
|
const firstFolderCard = page.locator('.folder-file-card__name').first()
|
||||||
await expect(firstFolderCard).toHaveText('A Folder')
|
await expect(firstFolderCard).toHaveText('A Folder')
|
||||||
@@ -282,8 +287,8 @@ test.describe('Folders', () => {
|
|||||||
|
|
||||||
test('should allow filtering within folders', async () => {
|
test('should allow filtering within folders', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Filtering Folder')
|
await createFolder({ folderName: 'Filtering Folder', page })
|
||||||
await clickFolderCard('Filtering Folder', true)
|
await clickFolderCard({ folderName: 'Filtering Folder', page, doubleClick: true })
|
||||||
|
|
||||||
const createNewDropdown = page.locator('.create-new-doc-in-folder__popup-button', {
|
const createNewDropdown = page.locator('.create-new-doc-in-folder__popup-button', {
|
||||||
hasText: 'Create New',
|
hasText: 'Create New',
|
||||||
@@ -328,8 +333,8 @@ test.describe('Folders', () => {
|
|||||||
|
|
||||||
test('should allow searching within folders', async () => {
|
test('should allow searching within folders', async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Test')
|
await createFolder({ folderName: 'Test', page })
|
||||||
await createFolder('Search Me')
|
await createFolder({ folderName: 'Search Me', page })
|
||||||
|
|
||||||
const testFolderCard = page.locator('.folder-file-card__name', {
|
const testFolderCard = page.locator('.folder-file-card__name', {
|
||||||
hasText: 'Test',
|
hasText: 'Test',
|
||||||
@@ -351,7 +356,7 @@ test.describe('Folders', () => {
|
|||||||
test.describe('Collection view actions', () => {
|
test.describe('Collection view actions', () => {
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Move Into This Folder')
|
await createFolder({ folderName: 'Move Into This Folder', page })
|
||||||
await createPostWithNoFolder()
|
await createPostWithNoFolder()
|
||||||
await page.goto(postURL.list)
|
await page.goto(postURL.list)
|
||||||
})
|
})
|
||||||
@@ -375,33 +380,33 @@ test.describe('Folders', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should update folder from doc folder pill', async () => {
|
test('should update folder from doc folder pill', async () => {
|
||||||
await selectFolderAndConfirmMoveFromList('Move Into This Folder')
|
await selectFolderAndConfirmMoveFromList({ folderName: 'Move Into This Folder', page })
|
||||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||||
'Test Post has been moved',
|
'Test Post has been moved',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should resolve folder pills and not get stuck as Loading...', async () => {
|
test('should resolve folder pills and not get stuck as Loading...', async () => {
|
||||||
await selectFolderAndConfirmMoveFromList('Move Into This Folder')
|
await selectFolderAndConfirmMoveFromList({ folderName: 'Move Into This Folder', page })
|
||||||
const folderPill = page.locator('tbody .row-1 .move-doc-to-folder')
|
const folderPill = page.locator('tbody .row-1 .move-doc-to-folder')
|
||||||
await page.reload()
|
await page.reload()
|
||||||
await expect(folderPill).not.toHaveText('Loading...')
|
await expect(folderPill).not.toHaveText('Loading...')
|
||||||
})
|
})
|
||||||
test('should show updated folder pill after folder change', async () => {
|
test('should show updated folder pill after folder change', async () => {
|
||||||
const folderPill = page.locator('tbody .row-1 .move-doc-to-folder')
|
const folderPill = page.locator('tbody .row-1 .move-doc-to-folder')
|
||||||
await selectFolderAndConfirmMoveFromList('Move Into This Folder')
|
await selectFolderAndConfirmMoveFromList({ folderName: 'Move Into This Folder', page })
|
||||||
await expect(folderPill).toHaveText('Move Into This Folder')
|
await expect(folderPill).toHaveText('Move Into This Folder')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should show updated folder pill after removing doc folder', async () => {
|
test('should show updated folder pill after removing doc folder', async () => {
|
||||||
const folderPill = page.locator('tbody .row-1 .move-doc-to-folder')
|
const folderPill = page.locator('tbody .row-1 .move-doc-to-folder')
|
||||||
await selectFolderAndConfirmMoveFromList('Move Into This Folder')
|
await selectFolderAndConfirmMoveFromList({ folderName: 'Move Into This Folder', page })
|
||||||
await expect(folderPill).toHaveText('Move Into This Folder')
|
await expect(folderPill).toHaveText('Move Into This Folder')
|
||||||
await page.reload()
|
await page.reload()
|
||||||
await folderPill.click()
|
await folderPill.click()
|
||||||
const folderBreadcrumb = page.locator('.folderBreadcrumbs__crumb-item', { hasText: 'Folder' })
|
const folderBreadcrumb = page.locator('.folderBreadcrumbs__crumb-item', { hasText: 'Folder' })
|
||||||
await folderBreadcrumb.click()
|
await folderBreadcrumb.click()
|
||||||
await selectFolderAndConfirmMove()
|
await selectFolderAndConfirmMove({ page })
|
||||||
await expect(folderPill).toHaveText('No Folder')
|
await expect(folderPill).toHaveText('No Folder')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -428,7 +433,7 @@ test.describe('Folders', () => {
|
|||||||
test.describe('Document view actions', () => {
|
test.describe('Document view actions', () => {
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Test Folder')
|
await createFolder({ folderName: 'Test Folder', page })
|
||||||
await createPostWithNoFolder()
|
await createPostWithNoFolder()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -440,7 +445,7 @@ test.describe('Folders', () => {
|
|||||||
test('should update folder from folder pill in doc controls', async () => {
|
test('should update folder from folder pill in doc controls', async () => {
|
||||||
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
||||||
await folderPill.click()
|
await folderPill.click()
|
||||||
await clickFolderCard('Test Folder', true)
|
await clickFolderCard({ folderName: 'Test Folder', doubleClick: true, page })
|
||||||
const selectButton = page
|
const selectButton = page
|
||||||
.locator('button[aria-label="Apply Changes"]')
|
.locator('button[aria-label="Apply Changes"]')
|
||||||
.filter({ hasText: 'Select' })
|
.filter({ hasText: 'Select' })
|
||||||
@@ -453,7 +458,7 @@ test.describe('Folders', () => {
|
|||||||
test('should show updated folder pill after folder change', async () => {
|
test('should show updated folder pill after folder change', async () => {
|
||||||
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
||||||
await folderPill.click()
|
await folderPill.click()
|
||||||
await clickFolderCard('Test Folder', true)
|
await clickFolderCard({ folderName: 'Test Folder', doubleClick: true, page })
|
||||||
const selectButton = page
|
const selectButton = page
|
||||||
.locator('button[aria-label="Apply Changes"]')
|
.locator('button[aria-label="Apply Changes"]')
|
||||||
.filter({ hasText: 'Select' })
|
.filter({ hasText: 'Select' })
|
||||||
@@ -468,9 +473,9 @@ test.describe('Folders', () => {
|
|||||||
test.describe('Multiple select options', () => {
|
test.describe('Multiple select options', () => {
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
await createFolder('Test Folder 1')
|
await createFolder({ folderName: 'Test Folder 1', page })
|
||||||
await createFolder('Test Folder 2')
|
await createFolder({ folderName: 'Test Folder 2', page })
|
||||||
await createFolder('Test Folder 3')
|
await createFolder({ folderName: 'Test Folder 3', page })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should show how many folders are selected', async () => {
|
test('should show how many folders are selected', async () => {
|
||||||
@@ -513,7 +518,7 @@ test.describe('Folders', () => {
|
|||||||
await expect(firstFolderCard).toBeHidden()
|
await expect(firstFolderCard).toBeHidden()
|
||||||
})
|
})
|
||||||
test('should move multiple folders', async () => {
|
test('should move multiple folders', async () => {
|
||||||
await createFolder('Move into here')
|
await createFolder({ folderName: 'Move into here', page })
|
||||||
const firstFolderCard = page.locator('.folder-file-card', {
|
const firstFolderCard = page.locator('.folder-file-card', {
|
||||||
hasText: 'Test Folder 1',
|
hasText: 'Test Folder 1',
|
||||||
})
|
})
|
||||||
@@ -534,121 +539,58 @@ test.describe('Folders', () => {
|
|||||||
hasText: 'Move into here',
|
hasText: 'Move into here',
|
||||||
})
|
})
|
||||||
await destinationFolder.click()
|
await destinationFolder.click()
|
||||||
await selectFolderAndConfirmMove()
|
await selectFolderAndConfirmMove({ page })
|
||||||
await expect(page.locator('.payload-toast-container')).toContainText('moved')
|
await expect(page.locator('.payload-toast-container')).toContainText('moved')
|
||||||
await expect(firstFolderCard).toBeHidden()
|
await expect(firstFolderCard).toBeHidden()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function expectNoResultsAndCreateFolderButton() {
|
test.describe('Collection with browse by folders disabled', () => {
|
||||||
const noResultsDiv = page.locator('div.no-results')
|
const folderName = 'Folder without omitted Docs'
|
||||||
await expect(noResultsDiv).toBeVisible()
|
test('should not show omitted collection documents in browse by folder view', async () => {
|
||||||
const createFolderButton = page.locator('text=Create Folder')
|
await page.goto(OmittedFromBrowseBy.byFolder)
|
||||||
await expect(createFolderButton).toBeVisible()
|
await createFolder({ folderName, page, fromDropdown: true })
|
||||||
}
|
|
||||||
|
|
||||||
async function createFolder(folderName: string, fromDropdown: boolean = false) {
|
// create document
|
||||||
if (fromDropdown) {
|
await page.goto(OmittedFromBrowseBy.create)
|
||||||
const folderDropdown = page.locator('.create-new-doc-in-folder__popup-button', {
|
const titleInput = page.locator('input[name="title"]')
|
||||||
hasText: 'Create',
|
await titleInput.fill('Omitted Doc')
|
||||||
})
|
await saveDocAndAssert(page)
|
||||||
await folderDropdown.click()
|
|
||||||
const createFolderButton = page.locator('.popup-button-list__button', {
|
|
||||||
hasText: 'Folder',
|
|
||||||
})
|
|
||||||
await createFolderButton.click()
|
|
||||||
} else {
|
|
||||||
const createFolderButton = page.locator(
|
|
||||||
'.create-new-doc-in-folder__button:has-text("Create New")',
|
|
||||||
)
|
|
||||||
await createFolderButton.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const folderNameInput = page.locator(
|
// assign to folder
|
||||||
'dialog#create-document--header-pill-new-folder-drawer div.drawer-content-container input#field-name',
|
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
||||||
)
|
await folderPill.click()
|
||||||
|
await clickFolderCard({ folderName, page })
|
||||||
|
const selectButton = page
|
||||||
|
.locator('button[aria-label="Apply Changes"]')
|
||||||
|
.filter({ hasText: 'Select' })
|
||||||
|
await selectButton.click()
|
||||||
|
|
||||||
await folderNameInput.fill(folderName)
|
// go to browse by folder view
|
||||||
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
|
await clickFolderCard({ folderName, page, doubleClick: true })
|
||||||
|
|
||||||
const createButton = page.getByRole('button', { name: 'Apply Changes' })
|
// folder should be empty
|
||||||
await createButton.click()
|
await expectNoResultsAndCreateFolderButton({ page })
|
||||||
|
|
||||||
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
|
||||||
|
|
||||||
const folderCard = page.locator('.folder-file-card__name', { hasText: folderName }).first()
|
|
||||||
await expect(folderCard).toBeVisible()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createFolderFromDoc(folderName: string) {
|
|
||||||
const addFolderButton = page.locator('.create-new-doc-in-folder__button', {
|
|
||||||
hasText: 'Create folder',
|
|
||||||
})
|
})
|
||||||
await addFolderButton.click()
|
|
||||||
|
|
||||||
const folderNameInput = page.locator('div.drawer-content-container input#field-name')
|
test('should not show collection type in browse by folder view', async () => {
|
||||||
|
const folderName = 'omitted collection pill test folder'
|
||||||
|
await page.goto(`${serverURL}/admin/browse-by-folder`)
|
||||||
|
await createFolder({ folderName, page })
|
||||||
|
await clickFolderCard({ folderName, page, doubleClick: true })
|
||||||
|
|
||||||
await folderNameInput.fill(folderName)
|
await page.locator('button:has(.collection-type__count)').click()
|
||||||
|
|
||||||
const createButton = page
|
await expect(
|
||||||
.locator('button[aria-label="Apply Changes"]')
|
page.locator('.checkbox-input .field-label', {
|
||||||
.filter({ hasText: 'Create' })
|
hasText: 'Omitted From Browse By',
|
||||||
await createButton.click()
|
}),
|
||||||
|
).toBeHidden()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
// Helper functions
|
||||||
|
|
||||||
const folderCard = page.locator('.folder-file-card__name', { hasText: folderName }).first()
|
|
||||||
await expect(folderCard).toBeVisible()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clickFolderCard(folderName: string, doubleClick: boolean = false) {
|
|
||||||
const folderCard = page
|
|
||||||
.locator('.folder-file-card')
|
|
||||||
.filter({
|
|
||||||
has: page.locator('.folder-file-card__name', { hasText: folderName }),
|
|
||||||
})
|
|
||||||
.first()
|
|
||||||
|
|
||||||
const dragHandleButton = folderCard.locator('div[role="button"].folder-file-card__drag-handle')
|
|
||||||
|
|
||||||
if (doubleClick) {
|
|
||||||
await dragHandleButton.dblclick()
|
|
||||||
} else {
|
|
||||||
await dragHandleButton.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function selectFolderAndConfirmMoveFromList(folderName: string | undefined = undefined) {
|
|
||||||
const firstListItem = page.locator('tbody .row-1')
|
|
||||||
const folderPill = firstListItem.locator('.move-doc-to-folder')
|
|
||||||
await folderPill.click()
|
|
||||||
|
|
||||||
if (folderName) {
|
|
||||||
await clickFolderCard(folderName, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectButton = page
|
|
||||||
.locator('button[aria-label="Apply Changes"]')
|
|
||||||
.filter({ hasText: 'Select' })
|
|
||||||
await selectButton.click()
|
|
||||||
const confirmMoveButton = page
|
|
||||||
.locator('dialog#move-folder-drawer-confirm-move')
|
|
||||||
.getByRole('button', { name: 'Move' })
|
|
||||||
await confirmMoveButton.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectFolderAndConfirmMove(folderName: string | undefined = undefined) {
|
|
||||||
if (folderName) {
|
|
||||||
await clickFolderCard(folderName, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectButton = page
|
|
||||||
.locator('button[aria-label="Apply Changes"]')
|
|
||||||
.filter({ hasText: 'Select' })
|
|
||||||
await selectButton.click()
|
|
||||||
const confirmMoveButton = page
|
|
||||||
.locator('dialog#move-folder-drawer-confirm-move')
|
|
||||||
.getByRole('button', { name: 'Move' })
|
|
||||||
await confirmMoveButton.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createPostWithNoFolder() {
|
async function createPostWithNoFolder() {
|
||||||
await page.goto(postURL.create)
|
await page.goto(postURL.create)
|
||||||
@@ -664,7 +606,7 @@ test.describe('Folders', () => {
|
|||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
const folderPill = page.locator('.doc-controls .move-doc-to-folder', { hasText: 'No Folder' })
|
||||||
await folderPill.click()
|
await folderPill.click()
|
||||||
await clickFolderCard(folderName)
|
await clickFolderCard({ folderName, page })
|
||||||
const selectButton = page
|
const selectButton = page
|
||||||
.locator('button[aria-label="Apply Changes"]')
|
.locator('button[aria-label="Apply Changes"]')
|
||||||
.filter({ hasText: 'Select' })
|
.filter({ hasText: 'Select' })
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export interface Config {
|
|||||||
media: Media;
|
media: Media;
|
||||||
drafts: Draft;
|
drafts: Draft;
|
||||||
autosave: Autosave;
|
autosave: Autosave;
|
||||||
|
'omitted-from-browse-by': OmittedFromBrowseBy;
|
||||||
users: User;
|
users: User;
|
||||||
'payload-folders': FolderInterface;
|
'payload-folders': FolderInterface;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
@@ -79,7 +80,7 @@ export interface Config {
|
|||||||
};
|
};
|
||||||
collectionsJoins: {
|
collectionsJoins: {
|
||||||
'payload-folders': {
|
'payload-folders': {
|
||||||
documentsAndFolders: 'payload-folders' | 'posts' | 'media' | 'drafts' | 'autosave';
|
documentsAndFolders: 'payload-folders' | 'posts' | 'media' | 'drafts' | 'autosave' | 'omitted-from-browse-by';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
collectionsSelect: {
|
collectionsSelect: {
|
||||||
@@ -87,6 +88,7 @@ export interface Config {
|
|||||||
media: MediaSelect<false> | MediaSelect<true>;
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
drafts: DraftsSelect<false> | DraftsSelect<true>;
|
drafts: DraftsSelect<false> | DraftsSelect<true>;
|
||||||
autosave: AutosaveSelect<false> | AutosaveSelect<true>;
|
autosave: AutosaveSelect<false> | AutosaveSelect<true>;
|
||||||
|
'omitted-from-browse-by': OmittedFromBrowseBySelect<false> | OmittedFromBrowseBySelect<true>;
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
'payload-folders': PayloadFoldersSelect<false> | PayloadFoldersSelect<true>;
|
'payload-folders': PayloadFoldersSelect<false> | PayloadFoldersSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
@@ -191,6 +193,10 @@ export interface FolderInterface {
|
|||||||
relationTo?: 'autosave';
|
relationTo?: 'autosave';
|
||||||
value: string | Autosave;
|
value: string | Autosave;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
relationTo?: 'omitted-from-browse-by';
|
||||||
|
value: string | OmittedFromBrowseBy;
|
||||||
|
}
|
||||||
)[];
|
)[];
|
||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
totalDocs?: number;
|
totalDocs?: number;
|
||||||
@@ -222,6 +228,17 @@ export interface Autosave {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
_status?: ('draft' | 'published') | null;
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "omitted-from-browse-by".
|
||||||
|
*/
|
||||||
|
export interface OmittedFromBrowseBy {
|
||||||
|
id: string;
|
||||||
|
title?: string | null;
|
||||||
|
folder?: (string | null) | FolderInterface;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
@@ -262,6 +279,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'autosave';
|
relationTo: 'autosave';
|
||||||
value: string | Autosave;
|
value: string | Autosave;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'omitted-from-browse-by';
|
||||||
|
value: string | OmittedFromBrowseBy;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: string | User;
|
||||||
@@ -364,6 +385,16 @@ export interface AutosaveSelect<T extends boolean = true> {
|
|||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
_status?: T;
|
_status?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "omitted-from-browse-by_select".
|
||||||
|
*/
|
||||||
|
export interface OmittedFromBrowseBySelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
folder?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users_select".
|
* via the `definition` "users_select".
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const postSlug = 'posts'
|
export const postSlug = 'posts'
|
||||||
|
export const omittedFromBrowseBySlug = 'omitted-from-browse-by'
|
||||||
|
|||||||
27
test/helpers/folders/clickFolderCard.ts
Normal file
27
test/helpers/folders/clickFolderCard.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
doubleClick?: boolean
|
||||||
|
folderName: string
|
||||||
|
page: Page
|
||||||
|
}
|
||||||
|
export async function clickFolderCard({
|
||||||
|
page,
|
||||||
|
folderName,
|
||||||
|
doubleClick = false,
|
||||||
|
}: Args): Promise<void> {
|
||||||
|
const folderCard = page
|
||||||
|
.locator('.folder-file-card')
|
||||||
|
.filter({
|
||||||
|
has: page.locator('.folder-file-card__name', { hasText: folderName }),
|
||||||
|
})
|
||||||
|
.first()
|
||||||
|
|
||||||
|
const dragHandleButton = folderCard.locator('div[role="button"].folder-file-card__drag-handle')
|
||||||
|
|
||||||
|
if (doubleClick) {
|
||||||
|
await dragHandleButton.dblclick()
|
||||||
|
} else {
|
||||||
|
await dragHandleButton.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
42
test/helpers/folders/createFolder.ts
Normal file
42
test/helpers/folders/createFolder.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { expect, type Page } from '@playwright/test'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
folderName: string
|
||||||
|
fromDropdown?: boolean
|
||||||
|
page: Page
|
||||||
|
}
|
||||||
|
export async function createFolder({
|
||||||
|
folderName,
|
||||||
|
fromDropdown = false,
|
||||||
|
page,
|
||||||
|
}: Args): Promise<void> {
|
||||||
|
if (fromDropdown) {
|
||||||
|
const folderDropdown = page.locator('.create-new-doc-in-folder__popup-button', {
|
||||||
|
hasText: 'Create',
|
||||||
|
})
|
||||||
|
await folderDropdown.click()
|
||||||
|
const createFolderButton = page.locator('.popup-button-list__button', {
|
||||||
|
hasText: 'Folder',
|
||||||
|
})
|
||||||
|
await createFolderButton.click()
|
||||||
|
} else {
|
||||||
|
const createFolderButton = page.locator(
|
||||||
|
'.create-new-doc-in-folder__button:has-text("Create New")',
|
||||||
|
)
|
||||||
|
await createFolderButton.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderNameInput = page.locator(
|
||||||
|
'dialog#create-document--header-pill-new-folder-drawer div.drawer-content-container input#field-name',
|
||||||
|
)
|
||||||
|
|
||||||
|
await folderNameInput.fill(folderName)
|
||||||
|
|
||||||
|
const createButton = page.getByRole('button', { name: 'Apply Changes' })
|
||||||
|
await createButton.click()
|
||||||
|
|
||||||
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
||||||
|
|
||||||
|
const folderCard = page.locator('.folder-file-card__name', { hasText: folderName }).first()
|
||||||
|
await expect(folderCard).toBeVisible()
|
||||||
|
}
|
||||||
27
test/helpers/folders/createFolderFromDoc.ts
Normal file
27
test/helpers/folders/createFolderFromDoc.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { expect, type Page } from '@playwright/test'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
folderName: string
|
||||||
|
page: Page
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFolderFromDoc({ folderName, page }: Args): Promise<void> {
|
||||||
|
const addFolderButton = page.locator('.create-new-doc-in-folder__button', {
|
||||||
|
hasText: 'Create folder',
|
||||||
|
})
|
||||||
|
await addFolderButton.click()
|
||||||
|
|
||||||
|
const folderNameInput = page.locator('div.drawer-content-container input#field-name')
|
||||||
|
|
||||||
|
await folderNameInput.fill(folderName)
|
||||||
|
|
||||||
|
const createButton = page
|
||||||
|
.locator('button[aria-label="Apply Changes"]')
|
||||||
|
.filter({ hasText: 'Create' })
|
||||||
|
await createButton.click()
|
||||||
|
|
||||||
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
||||||
|
|
||||||
|
const folderCard = page.locator('.folder-file-card__name', { hasText: folderName }).first()
|
||||||
|
await expect(folderCard).toBeVisible()
|
||||||
|
}
|
||||||
12
test/helpers/folders/expectNoResultsAndCreateFolderButton.ts
Normal file
12
test/helpers/folders/expectNoResultsAndCreateFolderButton.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { expect, type Page } from '@playwright/test'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
page: Page
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectNoResultsAndCreateFolderButton({ page }: Args): Promise<void> {
|
||||||
|
const noResultsDiv = page.locator('div.no-results')
|
||||||
|
await expect(noResultsDiv).toBeVisible()
|
||||||
|
const createFolderButton = page.locator('text=Create Folder')
|
||||||
|
await expect(createFolderButton).toBeVisible()
|
||||||
|
}
|
||||||
22
test/helpers/folders/selectFolderAndConfirmMove.ts
Normal file
22
test/helpers/folders/selectFolderAndConfirmMove.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import { clickFolderCard } from './clickFolderCard.js'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
folderName?: string
|
||||||
|
page: Page
|
||||||
|
}
|
||||||
|
export async function selectFolderAndConfirmMove({ folderName, page }: Args): Promise<void> {
|
||||||
|
if (folderName) {
|
||||||
|
await clickFolderCard({ folderName, doubleClick: true, page })
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectButton = page
|
||||||
|
.locator('button[aria-label="Apply Changes"]')
|
||||||
|
.filter({ hasText: 'Select' })
|
||||||
|
await selectButton.click()
|
||||||
|
const confirmMoveButton = page
|
||||||
|
.locator('dialog#move-folder-drawer-confirm-move')
|
||||||
|
.getByRole('button', { name: 'Move' })
|
||||||
|
await confirmMoveButton.click()
|
||||||
|
}
|
||||||
31
test/helpers/folders/selectFolderAndConfirmMoveFromList.ts
Normal file
31
test/helpers/folders/selectFolderAndConfirmMoveFromList.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import { clickFolderCard } from './clickFolderCard.js'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
folderName?: string
|
||||||
|
page: Page
|
||||||
|
rowIndex?: number
|
||||||
|
}
|
||||||
|
export async function selectFolderAndConfirmMoveFromList({
|
||||||
|
page,
|
||||||
|
folderName,
|
||||||
|
rowIndex = 1,
|
||||||
|
}: Args): Promise<void> {
|
||||||
|
const firstListItem = page.locator(`tbody .row-${rowIndex}`)
|
||||||
|
const folderPill = firstListItem.locator('.move-doc-to-folder')
|
||||||
|
await folderPill.click()
|
||||||
|
|
||||||
|
if (folderName) {
|
||||||
|
await clickFolderCard({ folderName, doubleClick: true, page })
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectButton = page
|
||||||
|
.locator('button[aria-label="Apply Changes"]')
|
||||||
|
.filter({ hasText: 'Select' })
|
||||||
|
await selectButton.click()
|
||||||
|
const confirmMoveButton = page
|
||||||
|
.locator('dialog#move-folder-drawer-confirm-move')
|
||||||
|
.getByRole('button', { name: 'Move' })
|
||||||
|
await confirmMoveButton.click()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user