Merge remote-tracking branch 'origin/master' into pr/bigmistqke/1223
This commit is contained in:
23
test/admin/components/Logout/index.tsx
Normal file
23
test/admin/components/Logout/index.tsx
Normal 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;
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -3,3 +3,8 @@ export const devUser = {
|
||||
password: 'test',
|
||||
roles: ['admin'],
|
||||
};
|
||||
export const regularUser = {
|
||||
email: 'user@payloadcms.com',
|
||||
password: 'test2',
|
||||
roles: ['user'],
|
||||
};
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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',
|
||||
|
||||
22
test/fields/collections/Relationship/index.ts
Normal file
22
test/fields/collections/Relationship/index.ts
Normal 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;
|
||||
@@ -32,6 +32,12 @@ const TextFields: CollectionConfig = {
|
||||
}, 1));
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Override the 40k text length default',
|
||||
name: 'overrideLength',
|
||||
type: 'text',
|
||||
maxLength: 50000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
46
test/hooks/collections/Users/index.ts
Normal file
46
test/hooks/collections/Users/index.ts
Normal 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;
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user