Merge branch 'feat/db-adapters' of github.com:payloadcms/payload into feat/db-adapters
This commit is contained in:
@@ -206,7 +206,7 @@ describe('access control', () => {
|
||||
const duplicateAction = page.locator('.collection-edit__collection-actions >> li').last();
|
||||
await expect(duplicateAction).toContainText('Duplicate');
|
||||
|
||||
await page.locator('#field-approvedForRemoval + button').click();
|
||||
await page.locator('#field-approvedForRemoval').check();
|
||||
await page.locator('#action-save').click();
|
||||
|
||||
const deleteAction = page.locator('.collection-edit__collection-actions >> li').last();
|
||||
|
||||
@@ -186,6 +186,15 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'geo',
|
||||
fields: [
|
||||
{
|
||||
name: 'point',
|
||||
type: 'point',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
globals: [
|
||||
{
|
||||
@@ -259,5 +268,19 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await payload.create({
|
||||
collection: 'geo',
|
||||
data: {
|
||||
point: [7, -7],
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create({
|
||||
collection: 'geo',
|
||||
data: {
|
||||
point: [5, -5],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import qs from 'qs';
|
||||
import payload from '../../src';
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil';
|
||||
import { initPayloadE2E } from '../helpers/configHelpers';
|
||||
@@ -15,12 +16,13 @@ const title = 'title';
|
||||
const description = 'description';
|
||||
|
||||
let url: AdminUrlUtil;
|
||||
let serverURL: string;
|
||||
|
||||
describe('admin', () => {
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadE2E(__dirname);
|
||||
serverURL = (await initPayloadE2E(__dirname)).serverURL;
|
||||
await clearDocs(); // Clear any seeded data from onInit
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
@@ -198,7 +200,7 @@ describe('admin', () => {
|
||||
|
||||
await page.goto(url.list);
|
||||
|
||||
await page.locator('.select-all__input').click();
|
||||
await page.locator('input#select-all').check();
|
||||
|
||||
await page.locator('.delete-documents__toggle').click();
|
||||
|
||||
@@ -216,7 +218,7 @@ describe('admin', () => {
|
||||
const bulkTitle = 'Bulk update title';
|
||||
await page.goto(url.list);
|
||||
|
||||
await page.locator('.select-all__input').click();
|
||||
await page.locator('input#select-all').check();
|
||||
await page.locator('.edit-many__toggle').click();
|
||||
await page.locator('.field-select .rs__control').click();
|
||||
const options = page.locator('.rs__option');
|
||||
@@ -394,6 +396,73 @@ describe('admin', () => {
|
||||
await page.locator('.condition__actions-remove').click();
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should accept where query from valid URL where parameter', async () => {
|
||||
await createPost({ title: 'post1' });
|
||||
await createPost({ title: 'post2' });
|
||||
await page.goto(`${url.list}?limit=10&page=1&where[or][0][and][0][title][equals]=post1`);
|
||||
|
||||
await expect(page.locator('.react-select--single-value').first()).toContainText('Title en');
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should accept transformed where query from invalid URL where parameter', async () => {
|
||||
await createPost({ title: 'post1' });
|
||||
await createPost({ title: 'post2' });
|
||||
// [title][equals]=post1 should be getting transformed into a valid where[or][0][and][0][title][equals]=post1
|
||||
await page.goto(`${url.list}?limit=10&page=1&where[title][equals]=post1`);
|
||||
|
||||
await expect(page.locator('.react-select--single-value').first()).toContainText('Title en');
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should accept where query from complex, valid URL where parameter using the near operator', async () => {
|
||||
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [5,-5] point
|
||||
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}?limit=10&page=1&where[or][0][and][0][point][near]=6,-7,200000`);
|
||||
|
||||
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('6,-7,200000');
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should accept transformed where query from complex, invalid URL where parameter using the near operator', async () => {
|
||||
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [5,-5] point
|
||||
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}?limit=10&page=1&where[point][near]=6,-7,200000`);
|
||||
|
||||
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('6,-7,200000');
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should accept where query from complex, valid URL where parameter using the within operator', async () => {
|
||||
type Point = [number, number];
|
||||
const polygon: Point[] = [
|
||||
[3.5, -3.5], // bottom-left
|
||||
[3.5, -6.5], // top-left
|
||||
[6.5, -6.5], // top-right
|
||||
[6.5, -3.5], // bottom-right
|
||||
[3.5, -3.5], // back to starting point to close the polygon
|
||||
];
|
||||
|
||||
const whereQueryJSON = {
|
||||
point: {
|
||||
within: {
|
||||
type: 'Polygon',
|
||||
coordinates: [polygon],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const whereQuery = qs.stringify({
|
||||
...({ where: whereQueryJSON }),
|
||||
}, {
|
||||
addQueryPrefix: false,
|
||||
});
|
||||
|
||||
// We have one point collection with the point [5,-5] and one with [7,-7]. This where query should kick out the [7,-7] point, as it's not within the polygon
|
||||
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}?limit=10&page=1&${whereQuery}`);
|
||||
|
||||
await expect(page.getByPlaceholder('Enter a value')).toHaveValue('[object Object]');
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('table columns', () => {
|
||||
@@ -523,18 +592,18 @@ describe('admin', () => {
|
||||
});
|
||||
|
||||
test('should select multiple rows', async () => {
|
||||
const selectAll = page.locator('.select-all');
|
||||
await page.locator('.row-1 .select-row button').click();
|
||||
const selectAll = page.locator('.custom-checkbox:has(#select-all)');
|
||||
await page.locator('.row-1 .cell-_select input').check();
|
||||
|
||||
const indeterminateSelectAll = selectAll.locator('.icon--line');
|
||||
const indeterminateSelectAll = selectAll.locator('.custom-checkbox__icon.partial');
|
||||
expect(indeterminateSelectAll).toBeDefined();
|
||||
|
||||
await selectAll.locator('button').click();
|
||||
const emptySelectAll = selectAll.locator('.icon');
|
||||
await selectAll.locator('input').click();
|
||||
const emptySelectAll = selectAll.locator('.custom-checkbox__icon:not(.check):not(.partial)');
|
||||
await expect(emptySelectAll).toHaveCount(0);
|
||||
|
||||
await selectAll.locator('button').click();
|
||||
const checkSelectAll = selectAll.locator('.icon .icon--check');
|
||||
await selectAll.locator('input').click();
|
||||
const checkSelectAll = selectAll.locator('.custom-checkbox__icon.check');
|
||||
expect(checkSelectAll).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -542,16 +611,16 @@ describe('admin', () => {
|
||||
// delete should not appear without selection
|
||||
await expect(page.locator('#confirm-delete')).toHaveCount(0);
|
||||
// select one row
|
||||
await page.locator('.row-1 .select-row button').click();
|
||||
await page.locator('.row-1 .cell-_select input').check();
|
||||
|
||||
// delete button should be present
|
||||
await expect(page.locator('#confirm-delete')).toHaveCount(1);
|
||||
|
||||
await page.locator('.row-2 .select-row button').click();
|
||||
await page.locator('.row-2 .cell-_select input').check();
|
||||
|
||||
await page.locator('.delete-documents__toggle').click();
|
||||
await page.locator('#confirm-delete').click();
|
||||
await expect(page.locator('.select-row')).toHaveCount(1);
|
||||
await expect(await page.locator('.cell-_select')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import mongoose from 'mongoose';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { GraphQLClient } from 'graphql-request';
|
||||
import payload from '../../src';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { postgresAdapter } from '../packages/db-postgres/src';
|
||||
|
||||
const databaseAdapters = {
|
||||
mongoose: mongooseAdapter({
|
||||
url: 'mongodb://127.0.0.1/payload',
|
||||
url: process.env.MONGO_URL || 'mongodb://127.0.0.1/payload',
|
||||
}),
|
||||
postgres: postgresAdapter({
|
||||
client: {
|
||||
@@ -15,6 +15,9 @@ const databaseAdapters = {
|
||||
}),
|
||||
};
|
||||
|
||||
// TODO: temporary
|
||||
process.env.PAYLOAD_DATABASE = 'postgres';
|
||||
|
||||
export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<SanitizedConfig> {
|
||||
const [name] = process.argv.slice(2);
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ const collectionWithName = (collectionSlug: string): CollectionConfig => {
|
||||
|
||||
export const slug = 'posts';
|
||||
export const relationSlug = 'relation';
|
||||
|
||||
export const pointSlug = 'point';
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
graphQL: {
|
||||
schemaOutputFile: path.resolve(__dirname, 'schema.graphql'),
|
||||
@@ -41,6 +44,16 @@ export default buildConfigWithDefaults({
|
||||
access: openAccess,
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug: pointSlug,
|
||||
access: openAccess,
|
||||
fields: [
|
||||
{
|
||||
type: 'point',
|
||||
name: 'point',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug,
|
||||
access: openAccess,
|
||||
@@ -414,5 +427,12 @@ export default buildConfigWithDefaults({
|
||||
relation: payloadAPITest1.id,
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create({
|
||||
collection: pointSlug,
|
||||
data: {
|
||||
point: [10, 20],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { GraphQLClient } from 'graphql-request';
|
||||
import { initPayloadTest } from '../helpers/configHelpers';
|
||||
import configPromise, { slug } from './config';
|
||||
import configPromise, { pointSlug, slug } from './config';
|
||||
import payload from '../../src';
|
||||
import type { Post } from './payload-types';
|
||||
import { mapAsync } from '../../src/utilities/mapAsync';
|
||||
|
||||
const title = 'title';
|
||||
|
||||
@@ -383,6 +384,228 @@ describe('collections-graphql', () => {
|
||||
expect(docs).toContainEqual(expect.objectContaining({ id: specialPost.id }));
|
||||
});
|
||||
|
||||
describe('near', () => {
|
||||
const point = [10, 20];
|
||||
const [lat, lng] = point;
|
||||
|
||||
it('should return a document near a point', async () => {
|
||||
const nearQuery = `
|
||||
query {
|
||||
Points(
|
||||
where: {
|
||||
point: {
|
||||
near: [${lat + 0.01}, ${lng + 0.01}, 10000]
|
||||
}
|
||||
}
|
||||
) {
|
||||
docs {
|
||||
id
|
||||
point
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await client.request(nearQuery);
|
||||
const { docs } = response.Points;
|
||||
|
||||
expect(docs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not return a point far away', async () => {
|
||||
const nearQuery = `
|
||||
query {
|
||||
Points(
|
||||
where: {
|
||||
point: {
|
||||
near: [${lng + 1}, ${lat - 1}, 5000]
|
||||
}
|
||||
}
|
||||
) {
|
||||
docs {
|
||||
id
|
||||
point
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await client.request(nearQuery);
|
||||
const { docs } = response.Points;
|
||||
|
||||
expect(docs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should sort find results by nearest distance', async () => {
|
||||
// creating twice as many records as we are querying to get a random sample
|
||||
await mapAsync([...Array(10)], async () => {
|
||||
// setTimeout used to randomize the creation timestamp
|
||||
setTimeout(async () => {
|
||||
await payload.create({
|
||||
collection: pointSlug,
|
||||
data: {
|
||||
// only randomize longitude to make distance comparison easy
|
||||
point: [Math.random(), 0],
|
||||
},
|
||||
});
|
||||
}, Math.random());
|
||||
});
|
||||
|
||||
const nearQuery = `
|
||||
query {
|
||||
Points(
|
||||
where: {
|
||||
point: {
|
||||
near: [0, 0, 100000, 0]
|
||||
}
|
||||
},
|
||||
limit: 5
|
||||
) {
|
||||
docs {
|
||||
id
|
||||
point
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await client.request(nearQuery);
|
||||
const { docs } = response.Points;
|
||||
|
||||
let previous = 0;
|
||||
docs.forEach(({ point: coordinates }) => {
|
||||
// The next document point should always be greater than the one before
|
||||
expect(previous).toBeLessThanOrEqual(coordinates[0]);
|
||||
[previous] = coordinates;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('within', () => {
|
||||
type Point = [number, number];
|
||||
const polygon: Point[] = [
|
||||
[9.0, 19.0], // bottom-left
|
||||
[9.0, 21.0], // top-left
|
||||
[11.0, 21.0], // top-right
|
||||
[11.0, 19.0], // bottom-right
|
||||
[9.0, 19.0], // back to starting point to close the polygon
|
||||
];
|
||||
|
||||
it('should return a document with the point inside the polygon', async () => {
|
||||
const query = `
|
||||
query {
|
||||
Points(
|
||||
where: {
|
||||
point: {
|
||||
within: {
|
||||
type: "Polygon",
|
||||
coordinates: ${JSON.stringify([polygon])}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
docs {
|
||||
id
|
||||
point
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await client.request(query);
|
||||
const { docs } = response.Points;
|
||||
|
||||
expect(docs).toHaveLength(1);
|
||||
expect(docs[0].point).toEqual([10, 20]);
|
||||
});
|
||||
|
||||
it('should not return a document with the point outside the polygon', async () => {
|
||||
const reducedPolygon = polygon.map((vertex) => vertex.map((coord) => coord * 0.1));
|
||||
const query = `
|
||||
query {
|
||||
Points(
|
||||
where: {
|
||||
point: {
|
||||
within: {
|
||||
type: "Polygon",
|
||||
coordinates: ${JSON.stringify([reducedPolygon])}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
docs {
|
||||
id
|
||||
point
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await client.request(query);
|
||||
const { docs } = response.Points;
|
||||
|
||||
expect(docs).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('intersects', () => {
|
||||
type Point = [number, number];
|
||||
const polygon: Point[] = [
|
||||
[9.0, 19.0], // bottom-left
|
||||
[9.0, 21.0], // top-left
|
||||
[11.0, 21.0], // top-right
|
||||
[11.0, 19.0], // bottom-right
|
||||
[9.0, 19.0], // back to starting point to close the polygon
|
||||
];
|
||||
|
||||
it('should return a document with the point intersecting the polygon', async () => {
|
||||
const query = `
|
||||
query {
|
||||
Points(
|
||||
where: {
|
||||
point: {
|
||||
intersects: {
|
||||
type: "Polygon",
|
||||
coordinates: ${JSON.stringify([polygon])}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
docs {
|
||||
id
|
||||
point
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await client.request(query);
|
||||
const { docs } = response.Points;
|
||||
|
||||
expect(docs).toHaveLength(1);
|
||||
expect(docs[0].point).toEqual([10, 20]);
|
||||
});
|
||||
|
||||
it('should not return a document with the point not intersecting a smaller polygon', async () => {
|
||||
const reducedPolygon = polygon.map((vertex) => vertex.map((coord) => coord * 0.1));
|
||||
const query = `
|
||||
query {
|
||||
Points(
|
||||
where: {
|
||||
point: {
|
||||
within: {
|
||||
type: "Polygon",
|
||||
coordinates: ${JSON.stringify([reducedPolygon])}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
docs {
|
||||
id
|
||||
point
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await client.request(query);
|
||||
const { docs } = response.Points;
|
||||
|
||||
expect(docs).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('can query deeply nested fields within rows, tabs, collapsibles', async () => {
|
||||
const withNestedField = await createPost({ D1: { D2: { D3: { D4: 'nested message' } } } });
|
||||
const query = `{
|
||||
|
||||
@@ -862,6 +862,98 @@ describe('collections-rest', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('within', () => {
|
||||
type Point = [number, number];
|
||||
const polygon: Point[] = [
|
||||
[9.0, 19.0], // bottom-left
|
||||
[9.0, 21.0], // top-left
|
||||
[11.0, 21.0], // top-right
|
||||
[11.0, 19.0], // bottom-right
|
||||
[9.0, 19.0], // back to starting point to close the polygon
|
||||
];
|
||||
it('should return a document with the point inside the polygon', async () => {
|
||||
// There should be 1 total points document populated by default with the point [10, 20]
|
||||
const { status, result } = await client.find({
|
||||
slug: pointSlug,
|
||||
query: {
|
||||
point: {
|
||||
within: {
|
||||
type: 'Polygon',
|
||||
coordinates: [polygon],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not return a document with the point outside a smaller polygon', async () => {
|
||||
const { status, result } = await client.find({
|
||||
slug: pointSlug,
|
||||
query: {
|
||||
point: {
|
||||
within: {
|
||||
type: 'Polygon',
|
||||
coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('intersects', () => {
|
||||
type Point = [number, number];
|
||||
const polygon: Point[] = [
|
||||
[9.0, 19.0], // bottom-left
|
||||
[9.0, 21.0], // top-left
|
||||
[11.0, 21.0], // top-right
|
||||
[11.0, 19.0], // bottom-right
|
||||
[9.0, 19.0], // back to starting point to close the polygon
|
||||
];
|
||||
|
||||
it('should return a document with the point intersecting the polygon', async () => {
|
||||
// There should be 1 total points document populated by default with the point [10, 20]
|
||||
const { status, result } = await client.find({
|
||||
slug: pointSlug,
|
||||
query: {
|
||||
point: {
|
||||
intersects: {
|
||||
type: 'Polygon',
|
||||
coordinates: [polygon],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not return a document with the point not intersecting a smaller polygon', async () => {
|
||||
const { status, result } = await client.find({
|
||||
slug: pointSlug,
|
||||
query: {
|
||||
point: {
|
||||
intersects: {
|
||||
type: 'Polygon',
|
||||
coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('or', async () => {
|
||||
const post1 = await createPost({ title: 'post1' });
|
||||
const post2 = await createPost({ title: 'post2' });
|
||||
|
||||
@@ -57,6 +57,26 @@ const TextFields: CollectionConfig = {
|
||||
type: 'text',
|
||||
maxLength: 50000,
|
||||
},
|
||||
{
|
||||
name: 'fieldWithDefaultValue',
|
||||
type: 'text',
|
||||
defaultValue: async () => {
|
||||
const defaultValue = new Promise((resolve) => setTimeout(() => resolve('some-value'), 1000));
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dependentOnFieldWithDefaultValue',
|
||||
type: 'text',
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ data }) => {
|
||||
return data?.fieldWithDefaultValue || '';
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -48,6 +48,15 @@ describe('Fields', () => {
|
||||
expect(doc.defaultFunction).toEqual(defaultText);
|
||||
expect(doc.defaultAsync).toEqual(defaultText);
|
||||
});
|
||||
|
||||
it('should populate default values in beforeValidate hook', async () => {
|
||||
const { fieldWithDefaultValue, dependentOnFieldWithDefaultValue } = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text },
|
||||
});
|
||||
|
||||
await expect(fieldWithDefaultValue).toEqual(dependentOnFieldWithDefaultValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timestamps', () => {
|
||||
|
||||
71
test/hooks/collections/AfterOperation/index.ts
Normal file
71
test/hooks/collections/AfterOperation/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { AfterOperationHook, CollectionConfig } from '../../../../src/collections/config/types';
|
||||
import { AfterOperation } from '../../payload-types';
|
||||
|
||||
export const afterOperationSlug = 'afterOperation';
|
||||
|
||||
const AfterOperation: CollectionConfig = {
|
||||
slug: afterOperationSlug,
|
||||
hooks: {
|
||||
// beforeRead: [(operation) => operation.doc],
|
||||
afterOperation: [
|
||||
async ({ result, operation }) => {
|
||||
if (operation === 'create') {
|
||||
if ('docs' in result) {
|
||||
return {
|
||||
...result,
|
||||
docs: result.docs?.map((doc) => ({
|
||||
...doc,
|
||||
title: 'Title created',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return { ...result, title: 'Title created' };
|
||||
}
|
||||
|
||||
if (operation === 'find') {
|
||||
// only modify the first doc for `find` operations
|
||||
// this is so we can test against the other operations
|
||||
return {
|
||||
...result,
|
||||
docs: result.docs?.map((doc, index) => (index === 0 ? {
|
||||
...doc,
|
||||
title: 'Title read',
|
||||
} : doc)),
|
||||
};
|
||||
}
|
||||
|
||||
if (operation === 'findByID') {
|
||||
return { ...result, title: 'Title read' };
|
||||
}
|
||||
|
||||
if (operation === 'update') {
|
||||
if ('docs' in result) {
|
||||
return {
|
||||
...result,
|
||||
docs: result.docs?.map((doc) => ({
|
||||
...doc,
|
||||
title: 'Title updated',
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'updateByID') {
|
||||
return { ...result, title: 'Title updated' };
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
] as AfterOperationHook<AfterOperation>[],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default AfterOperation;
|
||||
@@ -4,11 +4,13 @@ import Hooks, { hooksSlug } from './collections/Hook';
|
||||
import NestedAfterReadHooks from './collections/NestedAfterReadHooks';
|
||||
import ChainingHooks from './collections/ChainingHooks';
|
||||
import Relations from './collections/Relations';
|
||||
import AfterOperation from './collections/AfterOperation';
|
||||
import Users, { seedHooksUsers } from './collections/Users';
|
||||
import ContextHooks from './collections/ContextHooks';
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [
|
||||
AfterOperation,
|
||||
ContextHooks,
|
||||
TransformHooks,
|
||||
Hooks,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { initPayloadTest } from '../helpers/configHelpers';
|
||||
import config from './config';
|
||||
import configPromise from './config';
|
||||
import payload from '../../src';
|
||||
import { RESTClient } from '../helpers/rest';
|
||||
import { transformSlug } from './collections/Transform';
|
||||
@@ -7,10 +7,10 @@ import { hooksSlug } from './collections/Hook';
|
||||
import { chainingHooksSlug } from './collections/ChainingHooks';
|
||||
import { generatedAfterReadText, nestedAfterReadHooksSlug } from './collections/NestedAfterReadHooks';
|
||||
import { relationsSlug } from './collections/Relations';
|
||||
import type { NestedAfterReadHook } from './payload-types';
|
||||
import { hooksUsersSlug } from './collections/Users';
|
||||
import { devUser, regularUser } from '../credentials';
|
||||
import { AuthenticationError } from '../../src/errors';
|
||||
import { afterOperationSlug } from './collections/AfterOperation';
|
||||
import { contextHooksSlug } from './collections/ContextHooks';
|
||||
|
||||
let client: RESTClient;
|
||||
@@ -19,6 +19,7 @@ let apiUrl;
|
||||
describe('Hooks', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
|
||||
const config = await configPromise;
|
||||
client = new RESTClient(config, { serverURL, defaultSlug: transformSlug });
|
||||
apiUrl = `${serverURL}/api`;
|
||||
});
|
||||
@@ -73,7 +74,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
|
||||
it('should save data generated with afterRead hooks in nested field structures', async () => {
|
||||
const document = await payload.create<NestedAfterReadHook>({
|
||||
const document = await payload.create({
|
||||
collection: nestedAfterReadHooksSlug,
|
||||
data: {
|
||||
text: 'ok',
|
||||
@@ -154,6 +155,62 @@ describe('Hooks', () => {
|
||||
expect(retrievedDocs[0].text).toEqual('ok!!');
|
||||
});
|
||||
|
||||
it('should execute collection afterOperation hook', async () => {
|
||||
const [doc1, doc2] = await Promise.all([
|
||||
await payload.create({
|
||||
collection: afterOperationSlug,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
await payload.create({
|
||||
collection: afterOperationSlug,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(doc1.title === 'Title created').toBeTruthy();
|
||||
expect(doc2.title === 'Title created').toBeTruthy();
|
||||
|
||||
const findResult = await payload.find({
|
||||
collection: afterOperationSlug,
|
||||
});
|
||||
|
||||
expect(findResult.docs).toHaveLength(2);
|
||||
expect(findResult.docs[0].title === 'Title read').toBeTruthy();
|
||||
expect(findResult.docs[1].title === 'Title').toBeTruthy();
|
||||
|
||||
const [updatedDoc1, updatedDoc2] = await Promise.all([
|
||||
await payload.update({
|
||||
collection: afterOperationSlug,
|
||||
id: doc1.id,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
await payload.update({
|
||||
collection: afterOperationSlug,
|
||||
id: doc2.id,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(updatedDoc1.title === 'Title updated').toBeTruthy();
|
||||
expect(updatedDoc2.title === 'Title updated').toBeTruthy();
|
||||
|
||||
const findResult2 = await payload.find({
|
||||
collection: afterOperationSlug,
|
||||
});
|
||||
|
||||
expect(findResult2.docs).toHaveLength(2);
|
||||
expect(findResult2.docs[0].title === 'Title read').toBeTruthy();
|
||||
expect(findResult2.docs[1].title === 'Title').toBeTruthy();
|
||||
});
|
||||
|
||||
it('should pass context from beforeChange to afterChange', async () => {
|
||||
const document = await payload.create({
|
||||
collection: contextHooksSlug,
|
||||
|
||||
@@ -5,11 +5,24 @@
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "transforms".
|
||||
*/
|
||||
export interface Config {
|
||||
collections: {
|
||||
afterOperation: AfterOperation;
|
||||
transforms: Transform;
|
||||
hooks: Hook;
|
||||
'nested-after-read-hooks': NestedAfterReadHook;
|
||||
'chaining-hooks': ChainingHook;
|
||||
relations: Relation;
|
||||
'hooks-users': HooksUser;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface AfterOperation {
|
||||
id: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Transform {
|
||||
id: string;
|
||||
/**
|
||||
@@ -22,13 +35,9 @@ export interface Transform {
|
||||
* @maxItems 2
|
||||
*/
|
||||
localizedTransform?: [number, number];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "hooks".
|
||||
*/
|
||||
export interface Hook {
|
||||
id: string;
|
||||
fieldBeforeValidate?: boolean;
|
||||
@@ -40,53 +49,48 @@ export interface Hook {
|
||||
collectionAfterChange?: boolean;
|
||||
collectionBeforeRead?: boolean;
|
||||
collectionAfterRead?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "nested-after-read-hooks".
|
||||
*/
|
||||
export interface NestedAfterReadHook {
|
||||
id: string;
|
||||
text?: string;
|
||||
group: {
|
||||
array: {
|
||||
group?: {
|
||||
array?: {
|
||||
input?: string;
|
||||
afterRead?: string;
|
||||
shouldPopulate?: string | Relation;
|
||||
id?: string;
|
||||
}[];
|
||||
subGroup: {
|
||||
subGroup?: {
|
||||
afterRead?: string;
|
||||
shouldPopulate?: string | Relation;
|
||||
};
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relations".
|
||||
*/
|
||||
export interface Relation {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface ChainingHook {
|
||||
id: string;
|
||||
text?: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "hooks-users".
|
||||
*/
|
||||
export interface HooksUser {
|
||||
id: string;
|
||||
roles: ('admin' | 'user')[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
372
test/localization-rtl/ar.js
Normal file
372
test/localization-rtl/ar.js
Normal file
@@ -0,0 +1,372 @@
|
||||
export const ar = {
|
||||
authentication: {
|
||||
account: 'الحساب',
|
||||
accountOfCurrentUser: 'حساب المستخدم الحالي',
|
||||
alreadyActivated: 'تمّ التّفعيل بالفعل',
|
||||
alreadyLoggedIn: 'تمّ تسجيل الدّخول بالفعل',
|
||||
apiKey: 'مفتاح API',
|
||||
backToLogin: 'العودة لتسجيل الدخول',
|
||||
beginCreateFirstUser: 'للبدء, قم بإنشاء المستخدم الأوّل.',
|
||||
changePassword: 'تغيير كلمة المرور',
|
||||
checkYourEmailForPasswordReset:
|
||||
'تحقّق من بريدك الإلكتروني بحثًا عن رابط يسمح لك بإعادة تعيين كلمة المرور الخاصّة بك بشكل آمن.',
|
||||
confirmGeneration: 'تأكيد التّوليد',
|
||||
confirmPassword: 'تأكيد كلمة المرور',
|
||||
createFirstUser: 'إنشاء المستخدم الأوّل',
|
||||
emailNotValid: 'البريد الإلكتروني غير صالح',
|
||||
emailSent: 'تمّ ارسال البريد الإلكتروني',
|
||||
enableAPIKey: 'تفعيل مفتاح API',
|
||||
failedToUnlock: 'فشل فتح القفل',
|
||||
forceUnlock: 'إجبار فتح القفل',
|
||||
forgotPassword: 'نسيت كلمة المرور',
|
||||
forgotPasswordEmailInstructions:
|
||||
'يرجى إدخال البريد الالكتروني أدناه. ستتلقّى رسالة بريد إلكتروني تحتوي على إرشادات حول كيفيّة إعادة تعيين كلمة المرور الخاصّة بك.',
|
||||
forgotPasswordQuestion: 'هل نسيت كلمة المرور؟',
|
||||
generate: 'توليد',
|
||||
generateNewAPIKey: 'توليد مفتاح API جديد',
|
||||
generatingNewAPIKeyWillInvalidate:
|
||||
'سيؤدّي إنشاء مفتاح API جديد إلى <1> إبطال </ 1> المفتاح السّابق. هل أنت متأكّد أنّك تريد المتابعة؟',
|
||||
lockUntil: 'قفل حتى',
|
||||
logBackIn: 'تسجيل الدّخول من جديد',
|
||||
logOut: 'تسجيل الخروج',
|
||||
loggedIn:
|
||||
'لتسجيل الدّخول مع مستخدم آخر ، يجب عليك <0> تسجيل الخروج </0> أوّلاً.',
|
||||
loggedInChangePassword:
|
||||
'لتغيير كلمة المرور الخاصّة بك ، انتقل إلى <0>حسابك</0> وقم بتعديل كلمة المرور هناك.',
|
||||
loggedOutInactivity: 'لقد تمّ تسجيل الخروج بسبب عدم النّشاط.',
|
||||
loggedOutSuccessfully: 'لقد تمّ تسجيل خروجك بنجاح.',
|
||||
login: 'تسجيل الدخول',
|
||||
loginAttempts: 'محاولات تسجيل الدخول',
|
||||
loginUser: 'تسجيل دخول المستخدم',
|
||||
loginWithAnotherUser:
|
||||
'لتسجيل الدخول مع مستخدم آخر ، يجب عليك <0> تسجيل الخروج </0> أوّلاً.',
|
||||
logout: 'تسجيل الخروج',
|
||||
logoutUser: 'تسجيل خروج المستخدم',
|
||||
newAPIKeyGenerated: 'تمّ توليد مفتاح API جديد.',
|
||||
newAccountCreated:
|
||||
'تمّ إنشاء حساب جديد لتتمكّن من الوصول إلى <a href="{{serverURL}}"> {{serverURL}} </a> الرّجاء النّقر فوق الرّابط التّالي أو لصق عنوان URL أدناه في متصفّحّك لتأكيد بريدك الإلكتروني : <a href="{{verificationURL}}"> {{verificationURL}} </a> <br> بعد التّحقّق من بريدك الإلكتروني ، ستتمكّن من تسجيل الدّخول بنجاح.',
|
||||
newPassword: 'كلمة مرور جديدة',
|
||||
resetPassword: 'إعادة تعيين كلمة المرور',
|
||||
resetPasswordExpiration: 'انتهاء صلاحيّة إعادة تعيين كلمة المرور',
|
||||
resetPasswordToken: 'رمز إعادة تعيين كلمة المرور',
|
||||
resetYourPassword: 'إعادة تعيين كلمة المرور الخاصّة بك',
|
||||
stayLoggedIn: 'ابق متّصلًا',
|
||||
successfullyUnlocked: 'تمّ فتح القفل بنجاح',
|
||||
unableToVerify: 'غير قادر على التحقق من',
|
||||
verified: 'تمّ التحقّق',
|
||||
verifiedSuccessfully: 'تمّ التحقّق بنجاح',
|
||||
verify: 'قم بالتّحقّق',
|
||||
verifyUser: 'قم بالتّحقّق من المستخدم',
|
||||
verifyYourEmail: 'قم بتأكيد بريدك الألكتروني',
|
||||
youAreInactive:
|
||||
'لم تكن نشطًا منذ فترة قصيرة وسيتمّ تسجيل خروجك قريبًا تلقائيًا من أجل أمنك. هل ترغب في البقاء مسجّلا؟',
|
||||
youAreReceivingResetPassword:
|
||||
'أنت تتلقّى هذا البريد الالكتروني لأنّك (أو لأنّ شخص آخر) طلبت إعادة تعيين كلمة المرور لحسابك. الرّجاء النّقر فوق الرّابط التّالي ، أو لصق هذا الرّابط في متصفّحك لإكمال العمليّة:',
|
||||
youDidNotRequestPassword:
|
||||
'إن لم تطلب هذا ، يرجى تجاهل هذا البريد الإلكتروني وستبقى كلمة مرورك ذاتها بدون تغيير.',
|
||||
},
|
||||
error: {
|
||||
accountAlreadyActivated: 'لقد تمّ تنشيط هذا الحساب بالفعل.',
|
||||
autosaving: 'حدث خطأ أثناء الحفظ التّلقائي لهذا المستند.',
|
||||
correctInvalidFields: 'الرّجاء تصحيح الحقول الغير صالحة.',
|
||||
deletingFile: 'حدث خطأ أثناء حذف الملفّ.',
|
||||
deletingTitle:
|
||||
'حدث خطأ أثناء حذف {{title}}. يرجى التحقّق من اتّصالك والمحاولة مرة أخرى.',
|
||||
emailOrPasswordIncorrect: 'البريد الإلكتروني أو كلمة المرور غير صحيح/ة.',
|
||||
followingFieldsInvalid_many: 'الحقول التّالية غير صالحة:',
|
||||
followingFieldsInvalid_one: 'الحقل التّالي غير صالح:',
|
||||
incorrectCollection: 'المجموعة غير صحيحة',
|
||||
invalidFileType: 'نوع الملفّ غير صالح',
|
||||
invalidFileTypeValue: 'نوع الملفّ غير صالح: {{value}}',
|
||||
loadingDocument: 'حدث خطأ أثناء تحميل المستند بمعرّف {{id}}.',
|
||||
missingEmail: 'البريد الإلكتروني مفقود.',
|
||||
missingIDOfDocument: 'معرّف المستند المراد تحديثه مفقود.',
|
||||
missingIDOfVersion: 'معرّف النسخة مفقود.',
|
||||
missingRequiredData: 'توجد بيانات مطلوبة مفقودة.',
|
||||
noFilesUploaded: 'لم يتمّ رفع أيّة ملفّات.',
|
||||
noMatchedField: 'لم يتمّ العثور على حقل مطابق لـ "{{label}}"',
|
||||
noUser: 'لا يوجد مستخدم',
|
||||
notAllowedToAccessPage: 'لا يسمح لك الوصول إلى هذه الصّفحة.',
|
||||
notAllowedToPerformAction: 'لا يسمح لك القيام بهذه العمليّة.',
|
||||
notFound: 'لم يتمّ العثور على المورد المطلوب.',
|
||||
previewing: 'حدث خطأ في اثناء معاينة هذا المستند.',
|
||||
problemUploadingFile: 'حدث خطأ اثناء رفع الملفّ.',
|
||||
tokenInvalidOrExpired: 'الرّمز إمّا غير صالح أو منتهي الصّلاحيّة.',
|
||||
unPublishingDocument: 'حدث خطأ أثناء إلغاء نشر هذا المستند.',
|
||||
unableToDeleteCount: 'يتعذّر حذف {{count}} من {{total}} {{label}}.',
|
||||
unableToUpdateCount: 'يتعذّر تحديث {{count}} من {{total}} {{label}}.',
|
||||
unauthorized:
|
||||
'غير مصرّح لك ، عليك أن تقوم بتسجيل الدّخول لتتمكّن من تقديم هذا الطّلب.',
|
||||
unknown: 'حدث خطأ غير معروف.',
|
||||
unspecific: 'حدث خطأ.',
|
||||
userLocked:
|
||||
'تمّ قفل هذا المستخدم نظرًا لوجود عدد كبير من محاولات تسجيل الدّخول الغير ناجحة.',
|
||||
valueMustBeUnique: 'على القيمة أن تكون فريدة',
|
||||
verificationTokenInvalid: 'رمز التحقّق غير صالح.',
|
||||
},
|
||||
fields: {
|
||||
addLabel: 'أضف {{label}}',
|
||||
addLink: 'أضف رابط',
|
||||
addNew: 'أضف جديد',
|
||||
addNewLabel: 'أضف {{label}} جديد',
|
||||
addRelationship: 'أضف علاقة',
|
||||
addUpload: 'أضف تحميل',
|
||||
block: 'وحدة محتوى',
|
||||
blockType: 'نوع وحدة المحتوى',
|
||||
blocks: 'وحدات المحتوى',
|
||||
chooseBetweenCustomTextOrDocument:
|
||||
'اختر بين إدخال عنوان URL نصّي مخصّص أو الرّبط بمستند آخر.',
|
||||
chooseDocumentToLink: 'اختر مستندًا للربط',
|
||||
chooseFromExisting: 'اختر من القائمة',
|
||||
chooseLabel: 'اختر {{label}}',
|
||||
collapseAll: 'طيّ الكلّ',
|
||||
customURL: 'URL مخصّص',
|
||||
editLabelData: 'عدّل بيانات {{label}}',
|
||||
editLink: 'عدّل الرّابط',
|
||||
editRelationship: 'عدّل العلاقة',
|
||||
enterURL: 'ادخل عنوان URL',
|
||||
internalLink: 'رابط داخلي',
|
||||
itemsAndMore: '{{items}} و {{count}} أخرى',
|
||||
labelRelationship: '{{label}} علاقة',
|
||||
latitude: 'خطّ العرض',
|
||||
linkType: 'نوع الرّابط',
|
||||
linkedTo: 'تمّ الرّبط ل <0>{{label}}</0>',
|
||||
longitude: 'خطّ الطّول',
|
||||
newLabel: '{{label}} جديد',
|
||||
openInNewTab: 'الفتح في علامة تبويب جديدة',
|
||||
passwordsDoNotMatch: 'كلمة المرور غير مطابقة.',
|
||||
relatedDocument: 'مستند مربوط',
|
||||
relationTo: 'ربط ل',
|
||||
removeRelationship: 'حذف العلاقة',
|
||||
removeUpload: 'حذف المحتوى المرفوع',
|
||||
saveChanges: 'حفظ التّغييرات',
|
||||
searchForBlock: 'ابحث عن وحدة محتوى',
|
||||
selectExistingLabel: 'اختيار {{label}} من القائمة',
|
||||
selectFieldsToEdit: 'حدّد الحقول اللتي تريد تعديلها',
|
||||
showAll: 'إظهار الكلّ',
|
||||
swapRelationship: 'تبديل العلاقة',
|
||||
swapUpload: 'تبديل المحتوى المرفوع',
|
||||
textToDisplay: 'النصّ الذي تريد إظهاره',
|
||||
toggleBlock: 'Toggle block',
|
||||
uploadNewLabel: 'رفع {{label}} جديد',
|
||||
},
|
||||
general: {
|
||||
aboutToDelete: 'أنت على وشك حذف {{label}} <1>{{title}}</1>. هل أنت متأكّد؟',
|
||||
aboutToDeleteCount_many: 'أنت على وشك حذف {{count}} {{label}}',
|
||||
aboutToDeleteCount_one: 'أنت على وشك حذف {{count}} {{label}}',
|
||||
aboutToDeleteCount_other: 'أنت على وشك حذف {{count}} {{label}}',
|
||||
addBelow: 'أضف في الاسفل',
|
||||
addFilter: 'أضف فلتر',
|
||||
adminTheme: 'شكل واجهة المستخدم',
|
||||
and: 'و',
|
||||
ascending: 'تصاعدي',
|
||||
automatic: 'تلقائي',
|
||||
backToDashboard: 'العودة للوحة التّحكّم',
|
||||
cancel: 'إلغاء',
|
||||
changesNotSaved: 'لم يتمّ حفظ التّغييرات. إن غادرت الآن ، ستفقد تغييراتك.',
|
||||
close: 'إغلاق',
|
||||
collections: 'المجموعات',
|
||||
columnToSort: 'التّرتيب حسب العامود',
|
||||
columns: 'الأعمدة',
|
||||
confirm: 'تأكيد',
|
||||
confirmDeletion: 'تأكيد الحذف',
|
||||
confirmDuplication: 'تأكيد التّكرار',
|
||||
copied: 'تمّ النّسخ',
|
||||
copy: 'نسخ',
|
||||
create: 'إنشاء',
|
||||
createNew: 'أنشاء جديد',
|
||||
createNewLabel: 'إنشاء {{label}} جديد',
|
||||
created: 'تمّ الإنشاء',
|
||||
createdAt: 'تمّ الإنشاء في',
|
||||
creating: 'يتمّ الإنشاء',
|
||||
dark: 'غامق',
|
||||
dashboard: 'لوحة التّحكّم',
|
||||
delete: 'حذف',
|
||||
deletedCountSuccessfully: 'تمّ حذف {{count}} {{label}} بنجاح.',
|
||||
deletedSuccessfully: 'تمّ الحذف بنجاح.',
|
||||
deleting: 'يتمّ الحذف...',
|
||||
descending: 'تنازلي',
|
||||
duplicate: 'تكرار',
|
||||
duplicateWithoutSaving: 'تكرار بدون حفظ التّغييرات',
|
||||
edit: 'تعديل',
|
||||
editLabel: 'تعديل {{label}}',
|
||||
editing: 'يتمّ التّعديل',
|
||||
editingLabel_many: 'يتمّ تعديل {{count}} {{label}}',
|
||||
editingLabel_one: 'يتمّ تعديل {{count}} {{label}}',
|
||||
editingLabel_other: 'يتمّ تعديل {{count}} {{label}}',
|
||||
email: 'البريد الالكتروني',
|
||||
emailAddress: 'عنوان البريد الالكتروني',
|
||||
enterAValue: 'أدخل قيمة',
|
||||
fallbackToDefaultLocale: 'يتمّ استخدام اللّغة الافتراضيّة',
|
||||
filter: 'فلتر',
|
||||
filterWhere: 'فلتر {{label}} أينما',
|
||||
filters: 'فلاتر',
|
||||
globals: 'المجموعات العامّة',
|
||||
language: 'اللّغة',
|
||||
lastModified: 'آخر تعديل في',
|
||||
leaveAnyway: 'المغادرة على أيّة حال',
|
||||
leaveWithoutSaving: 'المغادرة بدون حفظ',
|
||||
light: 'فاتح',
|
||||
loading: 'يتمّ التّحميل',
|
||||
locales: 'اللّغات',
|
||||
moveDown: 'التّحريك إلى الأسفل',
|
||||
moveUp: 'التّحريك إلى الأعلى',
|
||||
newPassword: 'كلمة مرور جديدة',
|
||||
noFiltersSet: 'لم يتمّ تحديد فلتر',
|
||||
noLabel: '<لا يوجد {{label}}>',
|
||||
noResults:
|
||||
'لم يتمّ العثور على {{label}}. إمّا أنّه لا يوجد {{label}} حتّى الآن أو أنّه لا يتطابق أيّ منها مع الفلاتر التّي حدّدتها أعلاه.',
|
||||
noValue: 'لا توجد قيمة',
|
||||
none: 'None',
|
||||
notFound: 'غير معثور عليه',
|
||||
nothingFound: 'لم يتمّ العثور على شيء',
|
||||
of: 'من',
|
||||
or: 'أو',
|
||||
order: 'التّرتيب',
|
||||
pageNotFound: 'الصّفحة غير موجودة',
|
||||
password: 'كلمة المرور',
|
||||
payloadSettings: 'الإعدادات',
|
||||
perPage: 'لكلّ صفحة: {{limit}}',
|
||||
remove: 'إزالة',
|
||||
row: 'سطر',
|
||||
rows: 'أسطُر',
|
||||
save: 'حفظ',
|
||||
saving: 'يتمّ الحفظ...',
|
||||
searchBy: 'البحث بواسطة {{label}}',
|
||||
selectAll: 'اختر الكلّ {{count}} {{label}}',
|
||||
selectValue: 'اختر قيمة',
|
||||
selectedCount: '{{count}} {{label}} تمّ اختيارها',
|
||||
sorryNotFound: 'عذرًا - ليس هناك ما يتوافق مع طلبك.',
|
||||
sort: 'ترتيب',
|
||||
stayOnThisPage: 'البقاء في هذه الصّفحة',
|
||||
submissionSuccessful: 'تمّ التّقديم بنجاح.',
|
||||
submit: 'تقديم',
|
||||
successfullyCreated: 'تمّ إنشاء {{label}} بنجاح.',
|
||||
successfullyDuplicated: 'تمّ التّكرار{{label}} بنجاح.',
|
||||
thisLanguage: 'العربيّة',
|
||||
titleDeleted: 'تمّ حذف {{label}} "{{title}}" بنجاح.',
|
||||
unauthorized: 'غير مصرّح',
|
||||
unsavedChangesDuplicate:
|
||||
'لم تحفظ التّغييرات. هل ترغب في الاستمرار في التّكرار?',
|
||||
untitled: 'غير مُعنوَن',
|
||||
updatedAt: 'تمّ التحديث في',
|
||||
updatedCountSuccessfully: 'تمّ تحديث {{count}} {{label}} بنجاح.',
|
||||
updatedSuccessfully: 'تمّ التّحديث بنجاح.',
|
||||
updating: 'يتمّ التّحديث',
|
||||
uploading: 'يتمّ الرّفع',
|
||||
user: 'مستخدم',
|
||||
users: 'مستخدمين',
|
||||
welcome: 'اهلاً وسهلاً بك',
|
||||
},
|
||||
operators: {
|
||||
contains: 'يحتوي',
|
||||
equals: 'يساوي',
|
||||
exists: 'موجود',
|
||||
isGreaterThan: 'أكبر من',
|
||||
isGreaterThanOrEqualTo: 'أكبر أو يساوي',
|
||||
isIn: 'موجود في',
|
||||
isLessThan: 'أصغر من',
|
||||
isLessThanOrEqualTo: 'أصغر أو يساوي',
|
||||
isLike: 'هو مثل',
|
||||
isNotEqualTo: 'لا يساوي',
|
||||
isNotIn: 'غير موجود في',
|
||||
near: 'قريب من',
|
||||
},
|
||||
upload: {
|
||||
dragAndDrop: 'قم بسحب وإسقاط ملفّ',
|
||||
dragAndDropHere: 'أو اسحب الملفّ وأفلته هنا',
|
||||
fileName: 'اسم الملفّ',
|
||||
fileSize: 'حجم الملفّ',
|
||||
height: 'الطّول',
|
||||
lessInfo: 'معلومات أقلّ',
|
||||
moreInfo: 'معلومات أكثر',
|
||||
selectCollectionToBrowse: 'حدّد مجموعة لاستعراضها',
|
||||
selectFile: 'اختر ملفّ',
|
||||
sizes: 'الاحجام',
|
||||
width: 'العرض',
|
||||
},
|
||||
validation: {
|
||||
emailAddress: 'يرجى إدخال عنوان بريد إلكتروني صالح.',
|
||||
enterNumber: 'يرجى إدخال رقم صالح.',
|
||||
fieldHasNo: 'هذا الحقل لا يحتوي على {{label}}',
|
||||
greaterThanMax: '"{{value}}" هو أكبر من القيمة القصوى المسموحة {{max}}.',
|
||||
invalidInput: 'هذا الحقل يحتوي على حقل غير صالح.',
|
||||
invalidSelection: 'هذا الحقل يحتوي تحديد غير صالح.',
|
||||
invalidSelections: 'هذا الحقل يحتوي التّحديدات الغير صالحة التّلية:',
|
||||
lessThanMin: '"{{value}}" هو أصغر من القيمة الدنيا المسموحة {{min}}.',
|
||||
longerThanMin:
|
||||
'يجب أن تكون هذه القيمة أطول من الحدّ الأدنى للطول وهو {{minLength}} حرفًا.',
|
||||
notValidDate: '"{{value}}" ليس تاريخًا صالحًا.',
|
||||
required: 'هذه الخانة مطلوبه.',
|
||||
requiresAtLeast: 'هذه الخانة تتطلب على الأقلّ {{count}} {{label}}.',
|
||||
requiresNoMoreThan: 'هذه الخانة تتطلّب ما لا يزيد عن {{count}} {{label}}.',
|
||||
requiresTwoNumbers: 'هذه الخانة تتطلّب رقمين.',
|
||||
shorterThanMax:
|
||||
'يجب أن تكون هذه القيمة أقصر من الحدّ الأقصى للطول وهو {{maxLength}} حرفًا.',
|
||||
trueOrFalse: 'هذه الخانة يجب أن تكون صحيح او خطأ.',
|
||||
validUploadID: 'هذه الخانة ليست معرّف تحميل صالح.',
|
||||
},
|
||||
version: {
|
||||
aboutToPublishSelection:
|
||||
'أنت على وشك نشر كلّ {{label}} في التّحديد. هل أنت متأكّد؟',
|
||||
aboutToRestore:
|
||||
'أنت على وشك استرجاع هذا المستند {{label}} إلى الحالة التّي كان عليها في {{versionDate}}.',
|
||||
aboutToRestoreGlobal:
|
||||
'أنت على وشك استرجاع الاعداد العامّ {{label}} إلى الحالة التي كان عليها في {{versionDate}}.',
|
||||
aboutToRevertToPublished:
|
||||
'أنت على وشك إعادة هذا المستند إلى حالته المنشورة. هل أنت متأكّد؟',
|
||||
aboutToUnpublish: 'أنت على وشك إلغاء نشر هذا المستند. هل أنت متأكّد؟',
|
||||
aboutToUnpublishSelection:
|
||||
'أنت على وشك إلغاء نشر كلّ {{label}} في التّحديد. هل أنت متأكّد؟',
|
||||
autosave: 'حفظ تلقائي',
|
||||
autosavedSuccessfully: 'تمّ الحفظ التّلقائي بنجاح.',
|
||||
autosavedVersion: 'النّسخة المحفوظة تلقائياً',
|
||||
changed: 'تمّ التّغيير',
|
||||
compareVersion: 'مقارنة النّسخة مع:',
|
||||
confirmPublish: 'تأكيد النّشر',
|
||||
confirmRevertToSaved: 'تأكيد الرّجوع للنسخة المنشورة',
|
||||
confirmUnpublish: 'تأكيد إلغاء النّشر',
|
||||
confirmVersionRestoration: 'تأكيد إستعادة النّسخة',
|
||||
currentDocumentStatus: 'المستند {{docStatus}} الحالي',
|
||||
draft: 'مسودّة',
|
||||
draftSavedSuccessfully: 'تمّ حفظ المسودّة بنجاح.',
|
||||
lastSavedAgo: 'آخر حفظ في {{distance, relativetime(minutes)}}',
|
||||
noFurtherVersionsFound: 'لم يتمّ العثور على نسخات أخرى',
|
||||
noRowsFound: 'لم يتمّ العثور على {{label}}',
|
||||
preview: 'معاينة',
|
||||
problemRestoringVersion: 'حدث خطأ في استعادة هذه النّسخة',
|
||||
publish: 'نشر',
|
||||
publishChanges: 'نشر التّغييرات',
|
||||
published: 'تمّ النّشر',
|
||||
restoreThisVersion: 'استعادة هذه النّسخة',
|
||||
restoredSuccessfully: 'تمّت الاستعادة بنحاح.',
|
||||
restoring: 'تتمّ الاستعادة...',
|
||||
revertToPublished: 'الرّجوع للنسخة المنشورة',
|
||||
reverting: 'يتمّ الاسترجاع...',
|
||||
saveDraft: 'حفظ المسودّة',
|
||||
selectLocales: 'حدّد اللّغات المراد عرضها',
|
||||
selectVersionToCompare: 'حدّد نسخة للمقارنة',
|
||||
showLocales: 'اظهر اللّغات:',
|
||||
showingVersionsFor: 'يتمّ عرض النًّسخ ل:',
|
||||
status: 'الحالة',
|
||||
type: 'النّوع',
|
||||
unpublish: 'الغاء النّشر',
|
||||
unpublishing: 'يتمّ الغاء النّشر...',
|
||||
version: 'النّسخة',
|
||||
versionCount_many: 'تمّ العثور على {{count}} نُسخ',
|
||||
versionCount_none: 'لم يتمّ العثور على أيّ من النّسخ',
|
||||
versionCount_one: 'تمّ العثور على {{count}} من النّسخ',
|
||||
versionCount_other: 'تمّ العثور على {{count}} نُسخ',
|
||||
versionCreatedOn: 'تمّ ﻹنشاء النّسخة في {{version}}:',
|
||||
versionID: 'مُعرّف النّسخة',
|
||||
versions: 'النُّسَخ',
|
||||
viewingVersion: 'يتمّ استعراض نسخة ل {{entityLabel}} {{documentTitle}}',
|
||||
viewingVersionGlobal: 'يتمّ استعراض نسخة للاعداد العامّ {{entityLabel}}',
|
||||
viewingVersions:
|
||||
'يتمّ استعراض النُّسَخ ل {{entityLabel}} {{documentTitle}}',
|
||||
viewingVersionsGlobal:
|
||||
'يتمّ استعراض النُّسَخ للاعداد العامّ {{entityLabel}}',
|
||||
},
|
||||
};
|
||||
|
||||
export default ar;
|
||||
47
test/localization-rtl/collections/posts.ts
Normal file
47
test/localization-rtl/collections/posts.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { CollectionConfig } from '../../../src/collections/config/types';
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
labels: {
|
||||
singular: {
|
||||
en: 'Post',
|
||||
ar: 'منشور',
|
||||
},
|
||||
plural: {
|
||||
en: 'Posts',
|
||||
ar: 'منشورات',
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
description: { en: 'Description', ar: 'وصف' },
|
||||
listSearchableFields: ['title', 'description'],
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['id', 'title', 'description'],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
label: {
|
||||
en: 'Title',
|
||||
ar: 'عنوان',
|
||||
},
|
||||
type: 'text',
|
||||
admin: {
|
||||
rtl: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
admin: {
|
||||
rtl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
7
test/localization-rtl/collections/users.ts
Normal file
7
test/localization-rtl/collections/users.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { CollectionConfig } from '../../../src/collections/config/types';
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
fields: [],
|
||||
};
|
||||
43
test/localization-rtl/config.ts
Normal file
43
test/localization-rtl/config.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { devUser } from '../credentials';
|
||||
import { localization } from './localization';
|
||||
import { Users } from './collections/users';
|
||||
import { Posts } from './collections/posts';
|
||||
import en from '../../src/translations/en.json';
|
||||
import { ar } from './ar';
|
||||
import deepMerge from './deepMerge';
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults';
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [Users, Posts],
|
||||
i18n: {
|
||||
fallbackLng: 'en', // default
|
||||
debug: false, // default
|
||||
resources: {
|
||||
ar: deepMerge(en, ar),
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
locales: [
|
||||
{
|
||||
label: 'English',
|
||||
code: 'en',
|
||||
},
|
||||
{
|
||||
label: 'Arabic',
|
||||
code: 'ar',
|
||||
rtl: true,
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
fallback: true,
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
36
test/localization-rtl/deepMerge.ts
Normal file
36
test/localization-rtl/deepMerge.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Simple object check.
|
||||
* @param item
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isObject(item: unknown): boolean {
|
||||
return Boolean(item && typeof item === 'object' && !Array.isArray(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects.
|
||||
* @param target
|
||||
* @param ...sources
|
||||
*/
|
||||
export default function deepMerge<T extends object, R extends object>(
|
||||
target: T,
|
||||
source: R,
|
||||
): T {
|
||||
const output = { ...target };
|
||||
if (isObject(target) && isObject(source)) {
|
||||
Object.keys(source).forEach((key) => {
|
||||
if (isObject(source[key])) {
|
||||
// @ts-ignore
|
||||
if (!(key in target)) {
|
||||
Object.assign(output, { [key]: source[key] });
|
||||
} else {
|
||||
output[key] = deepMerge(target[key], source[key]);
|
||||
}
|
||||
} else {
|
||||
Object.assign(output, { [key]: source[key] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
15
test/localization-rtl/localization.ts
Normal file
15
test/localization-rtl/localization.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const localization = {
|
||||
locales: [
|
||||
{
|
||||
label: 'English',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
label: 'Arabic',
|
||||
value: 'ar',
|
||||
rtl: true,
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
fallback: true,
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults';
|
||||
import { devUser } from '../credentials';
|
||||
import { ArrayCollection } from './collections/Array';
|
||||
import { LocalizedPost, RelationshipLocalized } from './payload-types';
|
||||
import { LocalizedPost } from './payload-types';
|
||||
import {
|
||||
defaultLocale,
|
||||
englishTitle,
|
||||
@@ -34,7 +34,23 @@ const openAccess = {
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
localization: {
|
||||
locales: [defaultLocale, spanishLocale],
|
||||
locales: [
|
||||
{
|
||||
label: 'en',
|
||||
code: defaultLocale,
|
||||
rtl: false,
|
||||
},
|
||||
{
|
||||
label: 'es',
|
||||
code: spanishLocale,
|
||||
rtl: false,
|
||||
},
|
||||
{
|
||||
label: 'ar',
|
||||
code: 'ar',
|
||||
rtl: true,
|
||||
},
|
||||
],
|
||||
defaultLocale,
|
||||
fallback: true,
|
||||
},
|
||||
@@ -223,7 +239,7 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
});
|
||||
|
||||
const localizedPost = await payload.create<LocalizedPost>({
|
||||
const localizedPost = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
title: englishTitle,
|
||||
@@ -239,7 +255,7 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
});
|
||||
|
||||
await payload.update<LocalizedPost>({
|
||||
await payload.update({
|
||||
collection,
|
||||
id: localizedPost.id,
|
||||
locale: spanishLocale,
|
||||
@@ -248,14 +264,14 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
});
|
||||
|
||||
const localizedRelation = await payload.create<LocalizedPost>({
|
||||
const localizedRelation = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
title: relationEnglishTitle,
|
||||
},
|
||||
});
|
||||
|
||||
await payload.update<LocalizedPost>({
|
||||
await payload.update({
|
||||
collection,
|
||||
id: localizedPost.id,
|
||||
locale: spanishLocale,
|
||||
@@ -264,13 +280,13 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
});
|
||||
|
||||
const localizedRelation2 = await payload.create<LocalizedPost>({
|
||||
const localizedRelation2 = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
title: relationEnglishTitle2,
|
||||
},
|
||||
});
|
||||
await payload.update<LocalizedPost>({
|
||||
await payload.update({
|
||||
collection,
|
||||
id: localizedPost.id,
|
||||
locale: spanishLocale,
|
||||
@@ -279,7 +295,7 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create<RelationshipLocalized>({
|
||||
await payload.create({
|
||||
collection: withLocalizedRelSlug,
|
||||
data: {
|
||||
relationship: localizedRelation.id,
|
||||
|
||||
@@ -23,6 +23,7 @@ let url: AdminUrlUtil;
|
||||
const defaultLocale = 'en';
|
||||
const title = 'english title';
|
||||
const spanishTitle = 'spanish title';
|
||||
const arabicTitle = 'arabic title';
|
||||
const description = 'description';
|
||||
|
||||
let page: Page;
|
||||
@@ -91,6 +92,34 @@ describe('Localization', () => {
|
||||
await expect(page.locator('#field-title')).toHaveValue(title);
|
||||
await expect(page.locator('#field-description')).toHaveValue(description);
|
||||
});
|
||||
|
||||
test('create arabic post, add english', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
const newLocale = 'ar';
|
||||
|
||||
// Change to Arabic
|
||||
await changeLocale(newLocale);
|
||||
|
||||
await fillValues({ title: arabicTitle, description });
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
// Change back to English
|
||||
await changeLocale(defaultLocale);
|
||||
|
||||
// Localized field should not be populated
|
||||
await expect(page.locator('#field-title')).toBeEmpty();
|
||||
await expect(page.locator('#field-description')).toHaveValue(description);
|
||||
|
||||
// Add English
|
||||
|
||||
await fillValues({ title, description });
|
||||
await saveDocAndAssert(page);
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
await expect(page.locator('#field-title')).toHaveValue(title);
|
||||
await expect(page.locator('#field-description')).toHaveValue(description);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localized duplicate', () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('refresh-permissions', () => {
|
||||
await expect(page.locator('#nav-global-test')).toBeHidden();
|
||||
|
||||
// Allow access to test global.
|
||||
await page.locator('.custom-checkbox:has(#field-test) button').click();
|
||||
await page.locator('.custom-checkbox:has(#field-test) input').check();
|
||||
await page.locator('#action-save').click();
|
||||
|
||||
// Now test collection should appear in the menu.
|
||||
|
||||
@@ -190,6 +190,12 @@ export default buildConfigWithDefaults({
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'movies',
|
||||
type: 'relationship',
|
||||
relationTo: 'movies',
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -358,6 +358,64 @@ describe('Relationships', () => {
|
||||
expect(query.docs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
describe('Multiple Docs', () => {
|
||||
const movieList = [
|
||||
'Pulp Fiction',
|
||||
'Reservoir Dogs',
|
||||
'Once Upon a Time in Hollywood',
|
||||
'Shrek',
|
||||
'Shrek 2',
|
||||
'Shrek 3',
|
||||
'Scream',
|
||||
'The Matrix',
|
||||
'The Matrix Reloaded',
|
||||
'The Matrix Revolutions',
|
||||
'The Matrix Resurrections',
|
||||
'The Haunting',
|
||||
'The Haunting of Hill House',
|
||||
'The Haunting of Bly Manor',
|
||||
'Insidious',
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
await Promise.all(movieList.map((movie) => {
|
||||
return payload.create({
|
||||
collection: 'movies',
|
||||
data: {
|
||||
name: movie,
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should return more than 10 docs in relationship', async () => {
|
||||
const allMovies = await payload.find({
|
||||
collection: 'movies',
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
const movieIDs = allMovies.docs.map((doc) => doc.id);
|
||||
|
||||
await payload.create({
|
||||
collection: 'directors',
|
||||
data: {
|
||||
name: 'Quentin Tarantino',
|
||||
movies: movieIDs,
|
||||
},
|
||||
});
|
||||
|
||||
const director = await payload.find({
|
||||
collection: 'directors',
|
||||
where: {
|
||||
name: {
|
||||
equals: 'Quentin Tarantino',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(director.docs[0].movies.length).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface Movie {
|
||||
export interface Director {
|
||||
id: string;
|
||||
name?: string;
|
||||
movies?: Array<string | Movie>;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('versions', () => {
|
||||
test('should bulk publish', async () => {
|
||||
await page.goto(url.list);
|
||||
|
||||
await page.locator('.select-all__input').click();
|
||||
await page.locator('.custom-checkbox:has(#select-all) input').check();
|
||||
|
||||
await page.locator('.publish-many__toggle').click();
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('versions', () => {
|
||||
test('should bulk unpublish', async () => {
|
||||
await page.goto(url.list);
|
||||
|
||||
await page.locator('.select-all__input').click();
|
||||
await page.locator('.custom-checkbox:has(#select-all) input').check();
|
||||
|
||||
await page.locator('.unpublish-many__toggle').click();
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('versions', () => {
|
||||
test('should publish while editing many', async () => {
|
||||
const description = 'published document';
|
||||
await page.goto(url.list);
|
||||
await page.locator('.select-all__input').click();
|
||||
await page.locator('.custom-checkbox:has(#select-all) input').check();
|
||||
await page.locator('.edit-many__toggle').click();
|
||||
await page.locator('.field-select .rs__control').click();
|
||||
const options = page.locator('.rs__option');
|
||||
@@ -97,7 +97,7 @@ describe('versions', () => {
|
||||
test('should save as draft while editing many', async () => {
|
||||
const description = 'draft document';
|
||||
await page.goto(url.list);
|
||||
await page.locator('.select-all__input').click();
|
||||
await page.locator('.custom-checkbox:has(#select-all) input').check();
|
||||
await page.locator('.edit-many__toggle').click();
|
||||
await page.locator('.field-select .rs__control').click();
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
Reference in New Issue
Block a user