As discussed in [this RFC](https://github.com/payloadcms/payload/discussions/12729), this PR supports collection-scoped folders. You can scope folders to multiple collection types or just one. This unlocks the possibility to have folders on a per collection instead of always being shared on every collection. You can combine this feature with the `browseByFolder: false` to completely isolate a collection from other collections. Things left to do: - [x] ~~Create a custom react component for the selecting of collectionSlugs to filter out available options based on the current folders parameters~~ https://github.com/user-attachments/assets/14cb1f09-8d70-4cb9-b1e2-09da89302995 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210564397815557
349 lines
9.4 KiB
TypeScript
349 lines
9.4 KiB
TypeScript
import type { Payload } from 'payload'
|
|
|
|
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
|
let payload: Payload
|
|
|
|
const filename = fileURLToPath(import.meta.url)
|
|
const dirname = path.dirname(filename)
|
|
|
|
describe('folders', () => {
|
|
beforeAll(async () => {
|
|
;({ payload } = await initPayloadInt(dirname))
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await payload.destroy()
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
await payload.delete({
|
|
collection: 'payload-folders',
|
|
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',
|
|
folderType: ['posts'],
|
|
},
|
|
})
|
|
const folderIDFromParams = parentFolder.id
|
|
|
|
await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Nested 1',
|
|
folder: folderIDFromParams,
|
|
folderType: ['posts'],
|
|
},
|
|
})
|
|
|
|
await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Nested 2',
|
|
folder: folderIDFromParams,
|
|
folderType: ['posts'],
|
|
},
|
|
})
|
|
|
|
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: {
|
|
folderType: ['posts'],
|
|
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: {
|
|
folderType: ['posts'],
|
|
name: 'Parent Folder',
|
|
},
|
|
})
|
|
|
|
const childFolder = await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
folderType: ['posts'],
|
|
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: {
|
|
folderType: ['posts'],
|
|
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: {
|
|
folderType: ['posts'],
|
|
name: 'Parent Folder',
|
|
},
|
|
})
|
|
const childFolder = await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Child Folder',
|
|
folder: parentFolder,
|
|
folderType: ['posts'],
|
|
},
|
|
})
|
|
|
|
await payload.delete({ collection: 'payload-folders', id: parentFolder.id })
|
|
|
|
await expect(
|
|
payload.findByID({
|
|
collection: 'payload-folders',
|
|
id: childFolder.id,
|
|
disableErrors: true,
|
|
}),
|
|
).resolves.toBeNull()
|
|
})
|
|
|
|
describe('ensureSafeCollectionsChange', () => {
|
|
it('should prevent narrowing scope of a folder if it contains documents of a removed type', async () => {
|
|
const sharedFolder = await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Posts and Drafts Folder',
|
|
folderType: ['posts', 'drafts'],
|
|
},
|
|
})
|
|
|
|
await payload.create({
|
|
collection: 'posts',
|
|
data: {
|
|
title: 'Post 1',
|
|
folder: sharedFolder.id,
|
|
},
|
|
})
|
|
|
|
await payload.create({
|
|
collection: 'drafts',
|
|
data: {
|
|
title: 'Post 1',
|
|
folder: sharedFolder.id,
|
|
},
|
|
})
|
|
|
|
try {
|
|
const updatedFolder = await payload.update({
|
|
collection: 'payload-folders',
|
|
id: sharedFolder.id,
|
|
data: {
|
|
folderType: ['posts'],
|
|
},
|
|
})
|
|
|
|
expect(updatedFolder).not.toBeDefined()
|
|
} catch (e: any) {
|
|
expect(e.message).toBe(
|
|
'The folder "Posts and Drafts Folder" contains documents that still belong to the following collections: Drafts',
|
|
)
|
|
}
|
|
})
|
|
|
|
it('should prevent adding scope to a folder if it contains documents outside of the new scope', async () => {
|
|
const folderAcceptsAnything = await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Anything Goes',
|
|
folderType: [],
|
|
},
|
|
})
|
|
|
|
await payload.create({
|
|
collection: 'posts',
|
|
data: {
|
|
title: 'Post 1',
|
|
folder: folderAcceptsAnything.id,
|
|
},
|
|
})
|
|
|
|
try {
|
|
const scopedFolder = await payload.update({
|
|
collection: 'payload-folders',
|
|
id: folderAcceptsAnything.id,
|
|
data: {
|
|
folderType: ['posts'],
|
|
},
|
|
})
|
|
|
|
expect(scopedFolder).not.toBeDefined()
|
|
} catch (e: any) {
|
|
expect(e.message).toBe(
|
|
'The folder "Anything Goes" contains documents that still belong to the following collections: Posts',
|
|
)
|
|
}
|
|
})
|
|
|
|
it('should prevent narrowing scope of a folder if subfolders are assigned to any of the removed types', async () => {
|
|
const parentFolder = await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Parent Folder',
|
|
folderType: ['posts', 'drafts'],
|
|
},
|
|
})
|
|
|
|
await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Parent Folder',
|
|
folderType: ['posts', 'drafts'],
|
|
folder: parentFolder.id,
|
|
},
|
|
})
|
|
|
|
try {
|
|
const updatedParent = await payload.update({
|
|
collection: 'payload-folders',
|
|
id: parentFolder.id,
|
|
data: {
|
|
folderType: ['posts'],
|
|
},
|
|
})
|
|
|
|
expect(updatedParent).not.toBeDefined()
|
|
} catch (e: any) {
|
|
expect(e.message).toBe(
|
|
'The folder "Parent Folder" contains folders that still belong to the following collections: Drafts',
|
|
)
|
|
}
|
|
})
|
|
|
|
it('should prevent widening scope on a scoped subfolder', async () => {
|
|
const unscopedFolder = await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Parent Folder',
|
|
folderType: [],
|
|
},
|
|
})
|
|
|
|
const level1Folder = await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Level 1 Folder',
|
|
folderType: ['posts', 'drafts'],
|
|
folder: unscopedFolder.id,
|
|
},
|
|
})
|
|
|
|
try {
|
|
const level2UnscopedFolder = await payload.create({
|
|
collection: 'payload-folders',
|
|
data: {
|
|
name: 'Level 2 Folder',
|
|
folder: level1Folder.id,
|
|
folderType: [],
|
|
},
|
|
})
|
|
|
|
expect(level2UnscopedFolder).not.toBeDefined()
|
|
} catch (e: any) {
|
|
expect(e.message).toBe(
|
|
'The folder "Level 2 Folder" must have folder-type set since its parent folder "Level 1 Folder" has a folder-type set.',
|
|
)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|