Merge branch 'main' into feat/folders

This commit is contained in:
Jarrod Flesch
2025-05-20 13:35:23 -04:00
155 changed files with 2197 additions and 125 deletions

View File

@@ -460,6 +460,7 @@ export default buildConfigWithDefaults({
{
slug: 'virtual-relations',
admin: { useAsTitle: 'postTitle' },
access: { read: () => true },
fields: [
{
name: 'postTitle',

View File

@@ -2151,8 +2151,6 @@ describe('database', () => {
expect(descDocs[0]?.id).toBe(doc_2.id)
})
it.todo('should allow to sort by a virtual field with reference')
it('should allow virtual field 2x deep', async () => {
const category = await payload.create({
collection: 'categories',
@@ -2213,6 +2211,77 @@ describe('database', () => {
})
expect(globalData.postTitle).toBe('post')
})
it('should allow to sort by a virtual field with a refence, Local / GraphQL', async () => {
const post_1 = await payload.create({ collection: 'posts', data: { title: 'A' } })
const post_2 = await payload.create({ collection: 'posts', data: { title: 'B' } })
const doc_1 = await payload.create({
collection: 'virtual-relations',
data: { post: post_1 },
})
const doc_2 = await payload.create({
collection: 'virtual-relations',
data: { post: post_2 },
})
const queryDesc = `query {
VirtualRelations(
where: {OR: [{ id: { equals: ${JSON.stringify(doc_1.id)} } }, { id: { equals: ${JSON.stringify(doc_2.id)} } }],
}, sort: "-postTitle") {
docs {
id
}
}
}`
const {
data: {
VirtualRelations: { docs: graphqlDesc },
},
} = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: queryDesc }) })
.then((res) => res.json())
const { docs: localDesc } = await payload.find({
collection: 'virtual-relations',
sort: '-postTitle',
where: { id: { in: [doc_1.id, doc_2.id] } },
})
expect(graphqlDesc[0].id).toBe(doc_2.id)
expect(graphqlDesc[1].id).toBe(doc_1.id)
expect(localDesc[0].id).toBe(doc_2.id)
expect(localDesc[1].id).toBe(doc_1.id)
const queryAsc = `query {
VirtualRelations(
where: {OR: [{ id: { equals: ${JSON.stringify(doc_1.id)} } }, { id: { equals: ${JSON.stringify(doc_2.id)} } }],
}, sort: "postTitle") {
docs {
id
}
}
}`
const {
data: {
VirtualRelations: { docs: graphqlAsc },
},
} = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: queryAsc }) })
.then((res) => res.json())
const { docs: localAsc } = await payload.find({
collection: 'virtual-relations',
sort: 'postTitle',
where: { id: { in: [doc_1.id, doc_2.id] } },
})
expect(graphqlAsc[1].id).toBe(doc_2.id)
expect(graphqlAsc[0].id).toBe(doc_1.id)
expect(localAsc[1].id).toBe(doc_2.id)
expect(localAsc[0].id).toBe(doc_1.id)
})
})
it('should convert numbers to text', async () => {

View File

@@ -5,8 +5,8 @@ import { pagesSlug } from '../shared.js'
export const Pages: CollectionConfig = {
slug: pagesSlug,
labels: {
singular: 'Page',
plural: 'Pages',
singular: { en: 'Page', es: 'Página' },
plural: { en: 'Pages', es: 'Páginas' },
},
admin: {
useAsTitle: 'title',

View File

@@ -3,12 +3,13 @@ import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import { importExportPlugin } from '@payloadcms/plugin-import-export'
import { en } from '@payloadcms/translations/languages/en'
import { es } from '@payloadcms/translations/languages/es'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { Pages } from './collections/Pages.js'
import { Users } from './collections/Users.js'
import { seed } from './seed/index.js'
export default buildConfigWithDefaults({
admin: {
importMap: {
@@ -21,6 +22,13 @@ export default buildConfigWithDefaults({
fallback: true,
locales: ['en', 'es', 'de'],
},
i18n: {
supportedLanguages: {
en,
es,
},
fallbackLanguage: 'en',
},
onInit: async (payload) => {
await seed(payload)
},

View File

@@ -41,7 +41,7 @@ export default buildConfigWithDefaults({
isGlobal: true,
},
},
tenantSelectorLabel: 'Site',
tenantSelectorLabel: { en: 'Site', es: 'Site in es' },
}),
],
typescript: {

View File

@@ -0,0 +1,17 @@
import type { CollectionConfig } from 'payload'
export const nonUniqueSortSlug = 'non-unique-sort'
export const NonUniqueSortCollection: CollectionConfig = {
slug: nonUniqueSortSlug,
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'order',
type: 'number',
},
],
}

View File

@@ -2,12 +2,14 @@ import type { CollectionSlug, Payload } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { wait } from 'payload/shared'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { DefaultSortCollection } from './collections/DefaultSort/index.js'
import { DraftsCollection } from './collections/Drafts/index.js'
import { LocalizedCollection } from './collections/Localized/index.js'
import { NonUniqueSortCollection, nonUniqueSortSlug } from './collections/NonUniqueSort/index.js'
import { OrderableCollection } from './collections/Orderable/index.js'
import { OrderableJoinCollection } from './collections/OrderableJoin/index.js'
import { PostsCollection } from './collections/Posts/index.js'
@@ -19,6 +21,7 @@ export default buildConfigWithDefaults({
PostsCollection,
DraftsCollection,
DefaultSortCollection,
NonUniqueSortCollection,
LocalizedCollection,
OrderableCollection,
OrderableJoinCollection,
@@ -87,6 +90,28 @@ async function seedSortable(payload: Payload) {
await payload.create({ collection: 'orderable-join', data: { title: 'Join B' } })
// Create 10 items to be sorted by non-unique field
for (const i of Array.from({ length: 10 }, (_, index) => index)) {
let order = 1
if (i > 3) {
order = 2
} else if (i > 6) {
order = 3
}
await payload.create({
collection: nonUniqueSortSlug,
data: {
title: `Post ${i}`,
order,
},
})
// Wait 2 seconds to guarantee that the createdAt date is different
// await wait(2000)
}
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
status: 200,

View File

@@ -8,6 +8,7 @@ import type { Draft, Orderable, OrderableJoin } from './payload-types.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { draftsSlug } from './collections/Drafts/index.js'
import { nonUniqueSortSlug } from './collections/NonUniqueSort/index.js'
import { orderableSlug } from './collections/Orderable/index.js'
import { orderableJoinSlug } from './collections/OrderableJoin/index.js'
@@ -133,6 +134,154 @@ describe('Sort', () => {
})
})
describe('Non-unique sorting', () => {
// There are situations where the sort order is not guaranteed to be consistent, such as when sorting by a non-unique field in MongoDB which does not keep an internal order of items
// As a result, every time you fetch, including fetching specific pages, the order of items may change and appear as duplicated to some users.
it('should always be consistent when sorting', async () => {
const posts = await payload.find({
collection: nonUniqueSortSlug,
sort: 'order',
})
const initialMap = posts.docs.map((post) => post.title)
const dataFetches = await Promise.all(
Array.from({ length: 3 }).map(() =>
payload.find({
collection: nonUniqueSortSlug,
sort: 'order',
}),
),
)
const [fetch1, fetch2, fetch3] = dataFetches.map((fetch) =>
fetch.docs.map((post) => post.title),
)
expect(fetch1).toEqual(initialMap)
expect(fetch2).toEqual(initialMap)
expect(fetch3).toEqual(initialMap)
})
it('should always be consistent when sorting - with limited pages', async () => {
const posts = await payload.find({
collection: nonUniqueSortSlug,
sort: 'order',
limit: 5,
page: 2,
})
const initialMap = posts.docs.map((post) => post.title)
const dataFetches = await Promise.all(
Array.from({ length: 3 }).map(() =>
payload.find({
collection: nonUniqueSortSlug,
sort: 'order',
limit: 5,
page: 2,
}),
),
)
const [fetch1, fetch2, fetch3] = dataFetches.map((fetch) =>
fetch.docs.map((post) => post.title),
)
expect(fetch1).toEqual(initialMap)
expect(fetch2).toEqual(initialMap)
expect(fetch3).toEqual(initialMap)
})
it('should sort by createdAt as fallback', async () => {
// This is the (reverse - newest first) order that the posts are created in so this should remain consistent as the sort should fallback to '-createdAt'
const postsInOrder = ['Post 9', 'Post 8', 'Post 7', 'Post 6']
const dataFetches = await Promise.all(
Array.from({ length: 3 }).map(() =>
payload.find({
collection: nonUniqueSortSlug,
sort: 'order',
page: 2,
limit: 4,
}),
),
)
const [fetch1, fetch2, fetch3] = dataFetches.map((fetch) =>
fetch.docs.map((post) => post.title),
)
console.log({ fetch1, fetch2, fetch3 })
expect(fetch1).toEqual(postsInOrder)
expect(fetch2).toEqual(postsInOrder)
expect(fetch3).toEqual(postsInOrder)
})
it('should always be consistent without sort params in the query', async () => {
const posts = await payload.find({
collection: nonUniqueSortSlug,
})
const initialMap = posts.docs.map((post) => post.title)
const dataFetches = await Promise.all(
Array.from({ length: 3 }).map(() =>
payload.find({
collection: nonUniqueSortSlug,
}),
),
)
const [fetch1, fetch2, fetch3] = dataFetches.map((fetch) =>
fetch.docs.map((post) => post.title),
)
expect(fetch1).toEqual(initialMap)
expect(fetch2).toEqual(initialMap)
expect(fetch3).toEqual(initialMap)
})
it('should always be consistent without sort params in the query - with limited pages', async () => {
const posts = await payload.find({
collection: nonUniqueSortSlug,
page: 2,
limit: 4,
})
const initialMap = posts.docs.map((post) => post.title)
const dataFetches = await Promise.all(
Array.from({ length: 3 }).map(() =>
payload.find({
collection: nonUniqueSortSlug,
page: 2,
limit: 4,
}),
),
)
const [fetch1, fetch2, fetch3] = dataFetches.map((fetch) =>
fetch.docs.map((post) => post.title),
)
expect(fetch1).toEqual(initialMap)
expect(fetch2).toEqual(initialMap)
expect(fetch3).toEqual(initialMap)
})
})
describe('Sort by multiple fields', () => {
it('should sort posts by multiple fields', async () => {
const posts = await payload.find({

View File

@@ -70,6 +70,7 @@ export interface Config {
posts: Post;
drafts: Draft;
'default-sort': DefaultSort;
'non-unique-sort': NonUniqueSort;
localized: Localized;
orderable: Orderable;
'orderable-join': OrderableJoin;
@@ -89,6 +90,7 @@ export interface Config {
posts: PostsSelect<false> | PostsSelect<true>;
drafts: DraftsSelect<false> | DraftsSelect<true>;
'default-sort': DefaultSortSelect<false> | DefaultSortSelect<true>;
'non-unique-sort': NonUniqueSortSelect<false> | NonUniqueSortSelect<true>;
localized: LocalizedSelect<false> | LocalizedSelect<true>;
orderable: OrderableSelect<false> | OrderableSelect<true>;
'orderable-join': OrderableJoinSelect<false> | OrderableJoinSelect<true>;
@@ -98,7 +100,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
defaultIDType: number;
};
globals: {};
globalsSelect: {};
@@ -134,7 +136,7 @@ export interface UserAuthOperations {
* via the `definition` "posts".
*/
export interface Post {
id: string;
id: number;
text?: string | null;
number?: number | null;
number2?: number | null;
@@ -164,18 +166,29 @@ export interface Draft {
* via the `definition` "default-sort".
*/
export interface DefaultSort {
id: string;
id: number;
text?: string | null;
number?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "non-unique-sort".
*/
export interface NonUniqueSort {
id: number;
title?: string | null;
order?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized".
*/
export interface Localized {
id: string;
id: number;
text?: string | null;
number?: number | null;
number2?: number | null;
@@ -196,7 +209,7 @@ export interface Orderable {
_orderable_orderableJoinField1_order?: string | null;
_order?: string | null;
title?: string | null;
orderableField?: (string | null) | OrderableJoin;
orderableField?: (number | null) | OrderableJoin;
updatedAt: string;
createdAt: string;
}
@@ -205,20 +218,20 @@ export interface Orderable {
* via the `definition` "orderable-join".
*/
export interface OrderableJoin {
id: string;
id: number;
title?: string | null;
orderableJoinField1?: {
docs?: (string | Orderable)[];
docs?: (number | Orderable)[];
hasNextPage?: boolean;
totalDocs?: number;
};
orderableJoinField2?: {
docs?: (string | Orderable)[];
docs?: (number | Orderable)[];
hasNextPage?: boolean;
totalDocs?: number;
};
nonOrderableJoinField?: {
docs?: (string | Orderable)[];
docs?: (number | Orderable)[];
hasNextPage?: boolean;
totalDocs?: number;
};
@@ -230,7 +243,7 @@ export interface OrderableJoin {
* via the `definition` "users".
*/
export interface User {
id: string;
id: number;
updatedAt: string;
createdAt: string;
email: string;
@@ -247,40 +260,44 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
id: number;
document?:
| ({
relationTo: 'posts';
value: string | Post;
value: number | Post;
} | null)
| ({
relationTo: 'drafts';
value: string | Draft;
value: number | Draft;
} | null)
| ({
relationTo: 'default-sort';
value: string | DefaultSort;
value: number | DefaultSort;
} | null)
| ({
relationTo: 'non-unique-sort';
value: number | NonUniqueSort;
} | null)
| ({
relationTo: 'localized';
value: string | Localized;
value: number | Localized;
} | null)
| ({
relationTo: 'orderable';
value: string | Orderable;
value: number | Orderable;
} | null)
| ({
relationTo: 'orderable-join';
value: string | OrderableJoin;
value: number | OrderableJoin;
} | null)
| ({
relationTo: 'users';
value: string | User;
value: number | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
updatedAt: string;
createdAt: string;
@@ -290,10 +307,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
id: number;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
key?: string | null;
value?:
@@ -313,7 +330,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;
@@ -359,6 +376,16 @@ export interface DefaultSortSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "non-unique-sort_select".
*/
export interface NonUniqueSortSelect<T extends boolean = true> {
title?: T;
order?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized_select".
@@ -458,6 +485,6 @@ export interface Auth {
declare module 'payload' {
// @ts-ignore
// @ts-ignore
export interface GeneratedTypes extends Config {}
}
}

View File

@@ -30,6 +30,7 @@ import {
relationSlug,
unstoredMediaSlug,
versionSlug,
withoutEnlargeSlug,
} from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -490,6 +491,19 @@ export default buildConfigWithDefaults({
staticDir: path.resolve(dirname, './media/enlarge'),
},
},
{
slug: withoutEnlargeSlug,
fields: [],
upload: {
resizeOptions: {
width: 1000,
height: undefined,
fit: 'inside',
withoutEnlargement: true,
},
staticDir: path.resolve(dirname, './media/without-enlarge'),
},
},
{
slug: reduceSlug,
fields: [],

View File

@@ -38,6 +38,7 @@ import {
relationSlug,
withMetadataSlug,
withOnlyJPEGMetadataSlug,
withoutEnlargeSlug,
withoutMetadataSlug,
} from './shared.js'
import { startMockCorsServer } from './startMockCorsServer.js'
@@ -69,6 +70,7 @@ let uploadsTwo: AdminUrlUtil
let customUploadFieldURL: AdminUrlUtil
let hideFileInputOnCreateURL: AdminUrlUtil
let bestFitURL: AdminUrlUtil
let withoutEnlargementResizeOptionsURL: AdminUrlUtil
let consoleErrorsFromPage: string[] = []
let collectErrorsFromPage: () => boolean
let stopCollectingErrorsFromPage: () => boolean
@@ -104,6 +106,7 @@ describe('Uploads', () => {
customUploadFieldURL = new AdminUrlUtil(serverURL, customUploadFieldSlug)
hideFileInputOnCreateURL = new AdminUrlUtil(serverURL, hideFileInputOnCreateSlug)
bestFitURL = new AdminUrlUtil(serverURL, 'best-fit')
withoutEnlargementResizeOptionsURL = new AdminUrlUtil(serverURL, withoutEnlargeSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -1257,7 +1260,7 @@ describe('Uploads', () => {
})
// without focal point update this generated size was equal to 1736
expect(redDoc.sizes.focalTest.filesize).toEqual(1598)
expect(redDoc.sizes.focalTest.filesize).toEqual(1586)
})
test('should resize image after crop if resizeOptions defined', async () => {
@@ -1355,6 +1358,35 @@ describe('Uploads', () => {
await expect(page.locator('.file-field .file-details__remove')).toBeHidden()
})
test('should skip applying resizeOptions after updating an image if resizeOptions.withoutEnlargement is true and the original image size is smaller than the dimensions defined in resizeOptions', async () => {
await page.goto(withoutEnlargementResizeOptionsURL.create)
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooser = await fileChooserPromise
await wait(1000)
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
await page.waitForSelector('button#action-save')
await page.locator('button#action-save').click()
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
await wait(1000)
await page.locator('.file-field__edit').click()
// no need to make any changes to the image if resizeOptions.withoutEnlargement is actually being respected now
await page.locator('button:has-text("Apply Changes")').click()
await page.waitForSelector('button#action-save')
await page.locator('button#action-save').click()
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
await wait(1000)
const resizeOptionMedia = page.locator('.file-meta .file-meta__size-type')
// expect the image to be the original size since the original image is smaller than the dimensions defined in resizeOptions
await expect(resizeOptionMedia).toContainText('800x800')
})
describe('imageSizes best fit', () => {
test('should select adminThumbnail if one exists', async () => {
await page.goto(bestFitURL.create)

View File

@@ -3,6 +3,7 @@ export const mediaSlug = 'media'
export const relationSlug = 'relation'
export const audioSlug = 'audio'
export const enlargeSlug = 'enlarge'
export const withoutEnlargeSlug = 'without-enlarge'
export const focalNoSizesSlug = 'focal-no-sizes'
export const focalOnlySlug = 'focal-only'
export const reduceSlug = 'reduce'