Merge remote-tracking branch 'origin/master' into pr/bigmistqke/1223

This commit is contained in:
Jarrod Flesch
2022-11-16 09:00:53 -05:00
146 changed files with 5511 additions and 3839 deletions

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { useConfig } from '../../../../src/admin/components/utilities/Config';
import LogOut from '../../../../src/admin/components/icons/LogOut';
const Logout: React.FC = () => {
const config = useConfig();
const {
routes: {
admin,
},
admin: {
logoutRoute
},
} = config;
return (
<a href={`${admin}${logoutRoute}#custom`}>
<LogOut />
</a>
);
};
export default Logout;

View File

@@ -8,6 +8,7 @@ import CustomDefaultRoute from './components/views/CustomDefault';
import BeforeLogin from './components/BeforeLogin';
import AfterNavLinks from './components/AfterNavLinks';
import { slug, globalSlug } from './shared';
import Logout from './components/Logout';
export interface Post {
id: string;
@@ -38,6 +39,9 @@ export default buildConfig({
beforeLogin: [
BeforeLogin,
],
logout: {
Button: Logout,
},
afterNavLinks: [
AfterNavLinks,
],

View File

@@ -3,3 +3,8 @@ export const devUser = {
password: 'test',
roles: ['admin'],
};
export const regularUser = {
email: 'user@payloadcms.com',
password: 'test2',
roles: ['user'],
};

View File

@@ -15,6 +15,11 @@ require('@babel/register')({
const [testSuiteDir] = process.argv.slice(2);
if (!testSuiteDir) {
console.error('ERROR: You must provide an argument for "testSuiteDir"');
process.exit(1);
}
const configPath = path.resolve(__dirname, testSuiteDir, 'config.ts');
if (!fs.existsSync(configPath)) {

View File

@@ -3,7 +3,6 @@ import { v4 as uuid } from 'uuid';
import payload from '../src';
const expressApp = express();
const init = async () => {
await payload.initAsync({
secret: uuid(),

View File

@@ -1,7 +1,26 @@
import type { CollectionConfig } from '../../../../src/collections/config/types';
import type { BeforeDuplicate, CollectionConfig } from '../../../../src/collections/config/types';
import { IndexedField } from '../../payload-types';
const beforeDuplicate: BeforeDuplicate<IndexedField> = ({ data }) => {
return {
...data,
uniqueText: data.uniqueText ? `${data.uniqueText}-copy` : '',
group: {
...data.group || {},
localizedUnique: data.group?.localizedUnique ? `${data.group?.localizedUnique}-copy` : '',
},
collapsibleTextUnique: data.collapsibleTextUnique ? `${data.collapsibleTextUnique}-copy` : '',
collapsibleLocalizedUnique: data.collapsibleLocalizedUnique ? `${data.collapsibleLocalizedUnique}-copy` : '',
};
};
const IndexedFields: CollectionConfig = {
slug: 'indexed-fields',
admin: {
hooks: {
beforeDuplicate,
},
},
fields: [
{
name: 'text',

View File

@@ -0,0 +1,22 @@
import type { CollectionConfig } from '../../../../src/collections/config/types';
export const relationshipFieldsSlug = 'relationship-fields';
const RelationshipFields: CollectionConfig = {
slug: relationshipFieldsSlug,
fields: [
{
name: 'relationship',
type: 'relationship',
relationTo: ['text-fields', 'array-fields'],
required: true,
},
{
name: 'relationToSelf',
type: 'relationship',
relationTo: relationshipFieldsSlug,
},
],
};
export default RelationshipFields;

View File

@@ -32,6 +32,12 @@ const TextFields: CollectionConfig = {
}, 1));
},
},
{
label: 'Override the 40k text length default',
name: 'overrideLength',
type: 'text',
maxLength: 50000,
},
],
};

View File

@@ -1,5 +1,5 @@
import { CollectionConfig } from '../../../../src/collections/config/types';
import path from 'path';
import { CollectionConfig } from '../../../../src/collections/config/types';
const Uploads: CollectionConfig = {
slug: 'uploads',
@@ -11,6 +11,11 @@ const Uploads: CollectionConfig = {
type: 'text',
name: 'text',
},
{
type: 'upload',
name: 'media',
relationTo: 'uploads',
},
],
};

View File

@@ -19,6 +19,7 @@ import Uploads, { uploadsDoc } from './collections/Upload';
import IndexedFields from './collections/Indexed';
import NumberFields, { numberDoc } from './collections/Number';
import CodeFields, { codeDoc } from './collections/Code';
import RelationshipFields from './collections/Relationship';
export default buildConfig({
admin: {
@@ -39,16 +40,17 @@ export default buildConfig({
CodeFields,
CollapsibleFields,
ConditionalLogic,
DateFields,
GroupFields,
IndexedFields,
NumberFields,
PointFields,
RelationshipFields,
RichTextFields,
SelectFields,
TabsFields,
TextFields,
NumberFields,
Uploads,
IndexedFields,
DateFields,
],
localization: {
defaultLocale: 'en',

View File

@@ -9,6 +9,7 @@ import { pointFieldsSlug } from './collections/Point';
import { tabsSlug } from './collections/Tabs';
import { collapsibleFieldsSlug } from './collections/Collapsible';
import wait from '../../src/utilities/wait';
import { relationshipFieldsSlug } from './collections/Relationship';
const { beforeAll, describe } = test;
@@ -270,5 +271,75 @@ describe('fields', () => {
await expect(editLinkModal).not.toBeVisible();
});
});
describe('relationship', () => {
let url: AdminUrlUtil;
beforeAll(() => {
url = new AdminUrlUtil(serverURL, relationshipFieldsSlug);
});
test('should create inline relationship within field with many relations', async () => {
await page.goto(url.create);
const button = page.locator('#relationship-add-new .relationship-add-new__add-button');
await button.click();
await page.locator('.relationship-add-new__relation-button--text-fields').click();
const textField = page.locator('#field-text');
const textValue = 'hello';
await textField.fill(textValue);
await page.locator('#relationship-add-modal-depth-1 #action-save').click();
await expect(page.locator('.Toastify')).toContainText('successfully');
await expect(page.locator('#field-relationship .rs__single-value')).toContainText(textValue);
await page.locator('#action-save').click();
await expect(page.locator('.Toastify')).toContainText('successfully');
});
test('should create nested inline relationships', async () => {
await page.goto(url.create);
// Open first modal
await page.locator('#relationToSelf-add-new .relationship-add-new__add-button').click();
// Fill first modal's required relationship field
await page.locator('#relationToSelf-add-modal-depth-1 #field-relationship').click();
await page.locator('#relationToSelf-add-modal-depth-1 .rs__option:has-text("Seeded text document")').click();
// Open second modal
await page.locator('#relationToSelf-add-modal-depth-1 #relationToSelf-add-new button').click();
// Fill second modal's required relationship field
await page.locator('#relationToSelf-add-modal-depth-2 #field-relationship').click();
await page.locator('#relationToSelf-add-modal-depth-2 .rs__option:has-text("Seeded text document")').click();
// Save second modal
await page.locator('#relationToSelf-add-modal-depth-2 #action-save').click();
// Assert that the first modal is still open
// and that the Relation to Self field now has a value in its input
await expect(page.locator('#relationToSelf-add-modal-depth-1 #field-relationToSelf .rs__single-value')).toBeVisible();
// Save first modal
await page.locator('#relationToSelf-add-modal-depth-1 #action-save').click();
await wait(200);
// Expect the original field to have a value filled
await expect(page.locator('#field-relationToSelf .rs__single-value')).toBeVisible();
// Fill the required field
await page.locator('#field-relationship').click();
await page.locator('.rs__option:has-text("Seeded text document")').click();
await page.locator('#action-save').click();
await expect(page.locator('.Toastify')).toContainText('successfully');
});
});
});
});

View File

@@ -0,0 +1,46 @@
import { Payload } from '../../../../src';
import { BeforeLoginHook, CollectionConfig } from '../../../../src/collections/config/types';
import { AuthenticationError } from '../../../../src/errors';
import { devUser, regularUser } from '../../../credentials';
const beforeLoginHook: BeforeLoginHook = ({ user }) => {
const isAdmin = user.roles.includes('admin') ? user : undefined;
if (!isAdmin) {
throw new AuthenticationError();
}
return user;
};
export const seedHooksUsers = async (payload: Payload) => {
await payload.create({
collection: hooksUsersSlug,
data: devUser,
});
await payload.create({
collection: hooksUsersSlug,
data: regularUser,
});
};
export const hooksUsersSlug = 'hooks-users';
const Users: CollectionConfig = {
slug: hooksUsersSlug,
auth: true,
fields: [
{
name: 'roles',
label: 'Role',
type: 'select',
options: ['admin', 'user'],
defaultValue: 'user',
required: true,
saveToJWT: true,
hasMany: true,
},
],
hooks: {
beforeLogin: [beforeLoginHook],
},
};
export default Users;

View File

@@ -3,6 +3,7 @@ import TransformHooks from './collections/Transform';
import Hooks, { hooksSlug } from './collections/Hook';
import NestedAfterReadHooks from './collections/NestedAfterReadHooks';
import Relations from './collections/Relations';
import Users, { seedHooksUsers } from './collections/Users';
export default buildConfig({
collections: [
@@ -10,8 +11,10 @@ export default buildConfig({
Hooks,
NestedAfterReadHooks,
Relations,
Users,
],
onInit: async (payload) => {
await seedHooksUsers(payload);
await payload.create({
collection: hooksSlug,
data: {

View File

@@ -8,6 +8,9 @@ import { hooksSlug } from './collections/Hook';
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';
let client: RESTClient;
@@ -117,4 +120,20 @@ describe('Hooks', () => {
expect(retrievedDoc.group.subGroup.shouldPopulate.title).toEqual(relation.title);
});
});
describe('auth collection hooks', () => {
it('allow admin login', async () => {
const { user } = await payload.login({
collection: hooksUsersSlug,
data: {
email: devUser.email,
password: devUser.password,
},
});
expect(user).toBeDefined();
});
it('deny user login', async () => {
await expect(() => payload.login({ collection: hooksUsersSlug, data: { email: regularUser.email, password: regularUser.password } })).rejects.toThrow(AuthenticationError);
});
});
});

View File

@@ -15,7 +15,7 @@ const suiteName = args[0];
// Run all
if (!suiteName || args[0].startsWith('-')) {
const bail = args.includes('--bail');
const files = glob.sync(`${path.resolve(__dirname)}/**/*e2e.spec.ts`);
const files = glob.sync(`${path.resolve(__dirname).replace(/\\/g, '/')}/**/*e2e.spec.ts`);
console.log(`\n\nExecuting all ${files.length} E2E tests...`);
files.forEach((file) => {
clearWebpackCache();