chore: move to monorepo structure

This commit is contained in:
Alessio Gravili
2023-08-23 12:20:30 +02:00
parent e24ad67168
commit a67278b29f
1398 changed files with 2392 additions and 16005 deletions

View File

@@ -1,4 +0,0 @@
.after-dashboard {
border-top: 1px solid var(--theme-elevation-100);
}

View File

@@ -1,18 +0,0 @@
import React from 'react';
import './index.scss';
const baseClass = 'after-dashboard';
const AfterDashboard: React.FC = () => {
return (
<div className={baseClass}>
<h4>Test Config</h4>
<p>
The /test directory is used for create custom configurations and data seeding for developing features, writing e2e and integration testing.
</p>
</div>
);
};
export default AfterDashboard;

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
// As this is the demo project, we import our dependencies from the `src` directory.
import Chevron from '../../../../src/admin/components/icons/Chevron';
import { useConfig } from '../../../../src/admin/components/utilities/Config';
// In your projects, you can import as follows:
// import { Chevron } from 'payload/components';
// import { useConfig } from 'payload/components/utilities';
const baseClass = 'after-nav-links';
const AfterNavLinks: React.FC = () => {
const { routes: { admin: adminRoute } } = useConfig();
return (
<div className={baseClass}>
<span className="nav__label">Custom Routes</span>
<nav>
<NavLink
className="nav__link"
activeClassName="active"
to={`${adminRoute}/custom-default-route`}
>
<Chevron />
Default Template
</NavLink>
<NavLink
className="nav__link"
activeClassName="active"
to={`${adminRoute}/custom-minimal-route`}
>
<Chevron />
Minimal Template
</NavLink>
</nav>
</div>
);
};
export default AfterNavLinks;

View File

@@ -1,26 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
const BeforeLogin: React.FC<{i18n}> = () => {
const { t } = useTranslation();
return (
<div>
<h3>{t('general:welcome')}</h3>
<p>
This demo is a set up to configure Payload for the develop and testing of features. To see a product demo of a Payload project
please visit:
{' '}
<a
href="https://demo.payloadcms.com"
target="_blank"
rel="noreferrer"
>
demo.payloadcms.com
</a>
.
</p>
</div>
);
};
export default BeforeLogin;

View File

@@ -1,29 +0,0 @@
import React, { createContext, useState, useContext } from 'react';
type CustomContext = {
getCustom
setCustom
}
const Context = createContext({} as CustomContext);
const CustomProvider: React.FC = ({ children }) => {
const [getCustom, setCustom] = useState({});
const value = {
getCustom,
setCustom,
};
console.log('custom provider called');
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
};
export default CustomProvider;
export const useCustom = () => useContext(Context);

View File

@@ -1,7 +0,0 @@
import React from 'react';
const DemoUIFieldCell: React.FC = () => (
<p>Demo UI Field Cell</p>
);
export default DemoUIFieldCell;

View File

@@ -1,7 +0,0 @@
import React from 'react';
const DemoUIField: React.FC = () => (
<p>Demo UI Field</p>
);
export default DemoUIField;

View File

@@ -1,23 +0,0 @@
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

@@ -1,33 +0,0 @@
@import '../../../../../../../src/admin/scss/styles.scss';
.button-rich-text-button {
.btn {
margin-right: base(1);
}
&__modal {
display: flex;
align-items: center;
height: 100%;
&.payload__modal-item--enterDone {
@include blur-bg;
}
}
&__header {
width: 100%;
margin-bottom: $baseline;
display: flex;
justify-content: space-between;
h3 {
margin: 0;
}
svg {
width: base(1.5);
height: base(1.5);
}
}
}

View File

@@ -1,129 +0,0 @@
import React, { Fragment, useCallback } from 'react';
import { Modal, useModal } from '@faceless-ui/modal';
import { Transforms } from 'slate';
import { useSlate, ReactEditor } from 'slate-react';
import MinimalTemplate from '../../../../../../../src/admin/components/templates/Minimal';
import ElementButton from '../../../../../../../src/admin/components/forms/field-types/RichText/elements/Button';
import X from '../../../../../../../src/admin/components/icons/X';
import Button from '../../../../../../../src/admin/components/elements/Button';
import Form from '../../../../../../../src/admin/components/forms/Form';
import Submit from '../../../../../../../src/admin/components/forms/Submit';
import reduceFieldsToValues from '../../../../../../../src/admin/components/forms/Form/reduceFieldsToValues';
import Text from '../../../../../../../src/admin/components/forms/field-types/Text';
import Checkbox from '../../../../../../../src/admin/components/forms/field-types/Checkbox';
import Select from '../../../../../../../src/admin/components/forms/field-types/Select';
import './index.scss';
const baseClass = 'button-rich-text-button';
const initialFormData = {
style: 'primary',
};
const insertButton = (editor, { href, label, style, newTab = false }: any) => {
const text = { text: ' ' };
const button = {
type: 'button',
href,
style,
newTab,
label,
children: [
text,
],
};
const nodes = [button, { children: [{ text: '' }] }];
if (editor.blurSelection) {
Transforms.select(editor, editor.blurSelection);
}
Transforms.insertNodes(editor, nodes);
const currentPath = editor.selection.anchor.path[0];
const newSelection = { anchor: { path: [currentPath + 1, 0], offset: 0 }, focus: { path: [currentPath + 1, 0], offset: 0 } };
Transforms.select(editor, newSelection);
ReactEditor.focus(editor);
};
const ToolbarButton: React.FC<{path: string}> = ({ path }) => {
const { open, closeAll } = useModal();
const editor = useSlate();
const handleAddButton = useCallback((fields) => {
const data = reduceFieldsToValues(fields);
insertButton(editor, data);
closeAll();
}, [editor, closeAll]);
const modalSlug = `${path}-add-button`;
return (
<Fragment>
<ElementButton
className={baseClass}
format="button"
onClick={() => open(modalSlug)}
>
Button
</ElementButton>
<Modal
slug={modalSlug}
className={`${baseClass}__modal`}
>
<MinimalTemplate>
<header className={`${baseClass}__header`}>
<h3>Add button</h3>
<Button
buttonStyle="none"
onClick={closeAll}
>
<X />
</Button>
</header>
<Form
onSubmit={handleAddButton}
initialData={initialFormData}
>
<Text
label="Label"
name="label"
required
/>
<Text
label="URL"
name="href"
required
/>
<Select
label="Style"
name="style"
options={[
{
label: 'Primary',
value: 'primary',
},
{
label: 'Secondary',
value: 'secondary',
},
]}
/>
<Checkbox
label="Open in new tab"
name="newTab"
/>
<Submit>
Add button
</Submit>
</Form>
</MinimalTemplate>
</Modal>
</Fragment>
);
};
export default ToolbarButton;

View File

@@ -1,19 +0,0 @@
@import '../../../../../../../src/admin/scss/styles.scss';
.rich-text-button {
margin: $baseline 0;
}
.rich-text-button__button {
padding: base(.5) base(1.5);
border-radius: $style-radius-s;
&--primary {
background-color: $color-dark-gray;
color: white;
}
&--secondary {
background-color: $color-light-gray;
}
}

View File

@@ -1,29 +0,0 @@
import React from 'react';
import './index.scss';
const baseClass = 'rich-text-button';
const ButtonElement: React.FC = ({ attributes, children, element }) => {
const { style = 'primary', label } = element;
return (
<div
className={baseClass}
contentEditable={false}
>
<span
{...attributes}
className={[
`${baseClass}__button`,
`${baseClass}__button--${style}`,
].join(' ')}
>
{label}
{children}
</span>
</div>
);
};
export default ButtonElement;

View File

@@ -1,15 +0,0 @@
import { RichTextCustomElement } from '../../../../../../src/fields/config/types';
import Button from './Button';
import Element from './Element';
import plugin from './plugin';
const button: RichTextCustomElement = {
name: 'button',
Button,
Element,
plugins: [
plugin,
],
};
export default button;

View File

@@ -1,12 +0,0 @@
import { Editor } from 'slate';
const withButton = (incomingEditor: Editor): Editor => {
const editor = incomingEditor;
const { isVoid } = editor;
editor.isVoid = (element) => (element.type === 'button' ? true : isVoid(element));
return editor;
};
export default withButton;

View File

@@ -1,10 +0,0 @@
import React from 'react';
import LeafButton from '../../../../../../../src/admin/components/forms/field-types/RichText/leaves/Button';
const Button = () => (
<LeafButton format="purple-background">
Purple Background
</LeafButton>
);
export default Button;

View File

@@ -1,12 +0,0 @@
import React from 'react';
const PurpleBackground: React.FC<any> = ({ attributes, children }) => (
<span
{...attributes}
style={{ backgroundColor: 'purple' }}
>
{children}
</span>
);
export default PurpleBackground;

View File

@@ -1,8 +0,0 @@
import Button from './Button';
import Leaf from './Leaf';
export default {
name: 'purple-background',
Button,
Leaf,
};

View File

@@ -1,64 +0,0 @@
import React, { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
// As this is the demo project, we import our dependencies from the `src` directory.
import DefaultTemplate from '../../../../../src/admin/components/templates/Default';
import Button from '../../../../../src/admin/components/elements/Button';
import Eyebrow from '../../../../../src/admin/components/elements/Eyebrow';
import { AdminView } from '../../../../../src/config/types';
import { useStepNav } from '../../../../../src/admin/components/elements/StepNav';
import { useConfig } from '../../../../../src/admin/components/utilities/Config';
import Meta from '../../../../../src/admin/components/utilities/Meta';
// In your projects, you can import as follows:
// import { DefaultTemplate } from 'payload/components/templates';
// import { Button, Eyebrow } from 'payload/components/elements';
// import { AdminView } from 'payload/config';
// import { useStepNav } from 'payload/components/hooks';
// import { useConfig, Meta } from 'payload/components/utilities';
const CustomDefaultRoute: AdminView = ({ user, canAccessAdmin }) => {
const { routes: { admin: adminRoute } } = useConfig();
const { setStepNav } = useStepNav();
// This effect will only run one time and will allow us
// to set the step nav to display our custom route name
useEffect(() => {
setStepNav([
{
label: 'Custom Route with Default Template',
},
]);
}, [setStepNav]);
// If an unauthorized user tries to navigate straight to this page,
// Boot 'em out
if (!user || (user && !canAccessAdmin)) {
return (
<Redirect to={`${adminRoute}/unauthorized`} />
);
}
return (
<DefaultTemplate>
<Meta
title="Custom Route with Default Template"
description="Building custom routes into Payload is easy."
keywords="Custom React Components, Payload, CMS"
/>
<Eyebrow />
<h1>Custom Route</h1>
<p>Here is a custom route that was added in the Payload config. It uses the Default Template, so the sidebar is rendered.</p>
<Button
el="link"
to={`${adminRoute}`}
buttonStyle="secondary"
>
Go to Dashboard
</Button>
</DefaultTemplate>
);
};
export default CustomDefaultRoute;

View File

@@ -1,12 +0,0 @@
// As this is the demo folder, we import Payload SCSS functions relatively.
@import '../../../../../scss';
// In your own projects, you'd import as follows:
// @import '~payload/scss';
.custom-minimal-route {
&__login-btn {
margin-right: base(.5);
}
}

View File

@@ -1,42 +0,0 @@
import React from 'react';
// As this is the demo project, we import our dependencies from the `src` directory.
import MinimalTemplate from '../../../../../src/admin/components/templates/Minimal';
import Button from '../../../../../src/admin/components/elements/Button';
import { useConfig } from '../../../../../src/admin/components/utilities/Config';
// In your projects, you can import as follows:
// import { MinimalTemplate } from 'payload/components/templates';
// import { Button } from 'payload/components/elements';
// import { useConfig } from 'payload/components/utilities';
import './index.scss';
const baseClass = 'custom-minimal-route';
const CustomMinimalRoute: React.FC = () => {
const { routes: { admin: adminRoute } } = useConfig();
return (
<MinimalTemplate className={baseClass}>
<h1>Custom Route</h1>
<p>Here is a custom route that was added in the Payload config.</p>
<Button
className={`${baseClass}__login-btn`}
el="link"
to={`${adminRoute}/login`}
>
Go to Login
</Button>
<Button
el="link"
to={`${adminRoute}`}
buttonStyle="secondary"
>
Go to Dashboard
</Button>
</MinimalTemplate>
);
};
export default CustomMinimalRoute;

View File

@@ -1,286 +0,0 @@
import path from 'path';
import { mapAsync } from '../../src/utilities/mapAsync';
import { devUser } from '../credentials';
import { buildConfigWithDefaults } from '../buildConfigWithDefaults';
import AfterDashboard from './components/AfterDashboard';
import CustomMinimalRoute from './components/views/CustomMinimal';
import CustomDefaultRoute from './components/views/CustomDefault';
import BeforeLogin from './components/BeforeLogin';
import AfterNavLinks from './components/AfterNavLinks';
import { globalSlug, slug } from './shared';
import Logout from './components/Logout';
import DemoUIFieldField from './components/DemoUIField/Field';
import DemoUIFieldCell from './components/DemoUIField/Cell';
export interface Post {
id: string;
title: string;
description: string;
createdAt: Date;
updatedAt: Date;
}
export default buildConfigWithDefaults({
admin: {
css: path.resolve(__dirname, 'styles.scss'),
components: {
// providers: [CustomProvider, CustomProvider],
routes: [
{
path: '/custom-minimal-route',
Component: CustomMinimalRoute,
},
{
path: '/custom-default-route',
Component: CustomDefaultRoute,
},
],
afterDashboard: [
AfterDashboard,
],
beforeLogin: [
BeforeLogin,
],
logout: {
Button: Logout,
},
afterNavLinks: [
AfterNavLinks,
],
views: {
// Dashboard: CustomDashboardView,
// Account: CustomAccountView,
},
},
},
i18n: {
resources: {
en: {
general: {
dashboard: 'Home',
},
},
},
},
collections: [
{
slug: 'users',
auth: true,
fields: [],
},
{
slug: 'hidden-collection',
admin: {
hidden: () => true,
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug,
labels: {
singular: {
en: 'Post en',
es: 'Post es',
},
plural: {
en: 'Posts en',
es: 'Posts es',
},
},
admin: {
description: { en: 'Description en', es: 'Description es' },
listSearchableFields: ['title', 'description', 'number'],
group: { en: 'One', es: 'Una' },
useAsTitle: 'title',
defaultColumns: ['id', 'number', 'title', 'description', 'demoUIField'],
},
fields: [
{
name: 'title',
label: {
en: 'Title en',
es: 'Title es',
},
type: 'text',
},
{
name: 'description',
type: 'text',
},
{
name: 'number',
type: 'number',
},
{
name: 'richText',
type: 'richText',
admin: {
elements: [
'relationship',
],
},
},
{
type: 'ui',
name: 'demoUIField',
label: { en: 'Demo UI Field', de: 'Demo UI Field de' },
admin: {
components: {
Field: DemoUIFieldField,
Cell: DemoUIFieldCell,
},
},
},
],
},
{
slug: 'group-one-collection-ones',
admin: {
group: { en: 'One', es: 'Una' },
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: 'group-one-collection-twos',
admin: {
group: { en: 'One', es: 'Una' },
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: 'group-two-collection-ones',
admin: {
group: 'Two',
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: 'group-two-collection-twos',
admin: {
group: 'Two',
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: 'geo',
fields: [
{
name: 'point',
type: 'point',
},
],
},
],
globals: [
{
slug: 'hidden-global',
admin: {
hidden: () => true,
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: globalSlug,
label: {
en: 'Global en',
es: 'Global es',
},
admin: {
group: 'Group',
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: 'group-globals-one',
admin: {
group: 'Group',
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: 'group-globals-two',
admin: {
group: 'Group',
},
fields: [
{
name: 'title',
type: 'text',
},
],
},
],
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
});
await mapAsync([...Array(11)], async () => {
await payload.create({
collection: slug,
data: {
title: 'title',
description: 'description',
},
});
});
await payload.create({
collection: 'geo',
data: {
point: [7, -7],
},
});
await payload.create({
collection: 'geo',
data: {
point: [5, -5],
},
});
},
});

View File

@@ -1,757 +0,0 @@
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';
import { saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers';
import type { Post } from './config';
import { globalSlug, slug } from './shared';
import { mapAsync } from '../../src/utilities/mapAsync';
import wait from '../../src/utilities/wait';
const { afterEach, beforeAll, beforeEach, describe } = test;
const title = 'title';
const description = 'description';
let url: AdminUrlUtil;
let serverURL: string;
describe('admin', () => {
let page: Page;
beforeAll(async ({ browser }) => {
serverURL = (await initPayloadE2E(__dirname)).serverURL;
await clearDocs(); // Clear any seeded data from onInit
url = new AdminUrlUtil(serverURL, slug);
const context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await clearDocs();
// clear preferences
await payload.preferences.Model.deleteMany();
});
describe('Nav', () => {
test('should nav to collection - sidebar', async () => {
await page.goto(url.admin);
const collectionLink = page.locator(`#nav-${slug}`);
await collectionLink.click();
expect(page.url()).toContain(url.list);
});
test('should nav to a global - sidebar', async () => {
await page.goto(url.admin);
await page.locator(`#nav-global-${globalSlug}`).click();
expect(page.url()).toContain(url.global(globalSlug));
});
test('should navigate to collection - card', async () => {
await page.goto(url.admin);
await page.locator(`#card-${slug}`).click();
expect(page.url()).toContain(url.list);
});
test('should collapse and expand collection groups', async () => {
await page.goto(url.admin);
const navGroup = page.locator('#nav-group-One .nav-group__toggle');
const link = page.locator('#nav-group-one-collection-ones');
await expect(navGroup).toContainText('One');
await expect(link).toBeVisible();
await navGroup.click();
await expect(link).toBeHidden();
await navGroup.click();
await expect(link).toBeVisible();
});
test('should collapse and expand globals groups', async () => {
await page.goto(url.admin);
const navGroup = page.locator('#nav-group-Group .nav-group__toggle');
const link = page.locator('#nav-global-group-globals-one');
await expect(navGroup).toContainText('Group');
await expect(link).toBeVisible();
await navGroup.click();
await expect(link).toBeHidden();
await navGroup.click();
await expect(link).toBeVisible();
});
test('should save nav group collapse preferences', async () => {
await page.goto(url.admin);
const navGroup = page.locator('#nav-group-One .nav-group__toggle');
await navGroup.click();
await page.goto(url.admin);
const link = page.locator('#nav-group-one-collection-ones');
await expect(link).toBeHidden();
});
test('breadcrumbs - from list to dashboard', async () => {
await page.goto(url.list);
await page.locator('.step-nav a[href="/admin"]').click();
expect(page.url()).toContain(url.admin);
});
test('breadcrumbs - from document to collection', async () => {
const { id } = await createPost();
await page.goto(url.edit(id));
await page.locator(`.step-nav >> text=${slug}`).click();
expect(page.url()).toContain(url.list);
});
test('should not show hidden collections and globals', async () => {
await page.goto(url.admin);
// nav menu
await expect(page.locator('#nav-hidden-collection')).toBeHidden();
await expect(page.locator('#nav-hidden-global')).toBeHidden();
// dashboard
await expect(page.locator('#card-hidden-collection')).toBeHidden();
await expect(page.locator('#card-hidden-global')).toBeHidden();
// routing
await page.goto(url.collection('hidden-collection'));
await expect(page.locator('.not-found')).toContainText('Nothing found');
await page.goto(url.global('hidden-global'));
await expect(page.locator('.not-found')).toContainText('Nothing found');
});
});
describe('CRUD', () => {
test('should create', async () => {
await page.goto(url.create);
await page.locator('#field-title').fill(title);
await page.locator('#field-description').fill(description);
await saveDocAndAssert(page);
await expect(page.locator('#field-title')).toHaveValue(title);
await expect(page.locator('#field-description')).toHaveValue(description);
});
test('should read existing', async () => {
const { id } = await createPost();
await page.goto(url.edit(id));
await expect(page.locator('#field-title')).toHaveValue(title);
await expect(page.locator('#field-description')).toHaveValue(description);
});
test('should update existing', async () => {
const { id } = await createPost();
await page.goto(url.edit(id));
const newTitle = 'new title';
const newDesc = 'new description';
await page.locator('#field-title').fill(newTitle);
await page.locator('#field-description').fill(newDesc);
await saveDocAndAssert(page);
await expect(page.locator('#field-title')).toHaveValue(newTitle);
await expect(page.locator('#field-description')).toHaveValue(newDesc);
});
test('should save using hotkey', async () => {
const { id } = await createPost();
await page.goto(url.edit(id));
const newTitle = 'new title';
await page.locator('#field-title').fill(newTitle);
await saveDocHotkeyAndAssert(page);
await expect(page.locator('#field-title')).toHaveValue(newTitle);
});
test('should delete existing', async () => {
const { id, ...post } = await createPost();
await page.goto(url.edit(id));
await page.locator('#action-delete').click();
await page.locator('#confirm-delete').click();
await expect(page.locator(`text=Post en "${post.title}" successfully deleted.`)).toBeVisible();
expect(page.url()).toContain(url.list);
});
test('should bulk delete', async () => {
createPost();
createPost();
createPost();
await page.goto(url.list);
await page.locator('input#select-all').check();
await page.locator('.delete-documents__toggle').click();
await page.locator('#confirm-delete').click();
await expect(page.locator('.Toastify__toast--success')).toHaveText('Deleted 3 Posts en successfully.');
await expect(page.locator('.collection-list__no-results')).toBeVisible();
});
test('should bulk update', async () => {
createPost();
createPost();
createPost();
const bulkTitle = 'Bulk update title';
await page.goto(url.list);
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');
const titleOption = options.locator('text=Title en');
await expect(titleOption).toHaveText('Title en');
await titleOption.click();
const titleInput = page.locator('#field-title');
await expect(titleInput).toBeVisible();
await titleInput.fill(bulkTitle);
await page.locator('.form-submit button[type="submit"]').click();
await expect(page.locator('.Toastify__toast--success')).toContainText('Updated 3 Posts en successfully.');
await expect(page.locator('.row-1 .cell-title')).toContainText(bulkTitle);
await expect(page.locator('.row-2 .cell-title')).toContainText(bulkTitle);
await expect(page.locator('.row-3 .cell-title')).toContainText(bulkTitle);
});
test('should save globals', async () => {
await page.goto(url.global(globalSlug));
await page.locator('#field-title').fill(title);
await saveDocAndAssert(page);
await expect(page.locator('#field-title')).toHaveValue(title);
});
});
describe('i18n', () => {
test('should allow changing language', async () => {
await page.goto(url.account);
const field = page.locator('.account__language .react-select');
await field.click();
const options = page.locator('.rs__option');
await options.locator('text=Español').click();
await expect(page.locator('.step-nav')).toContainText('Tablero');
await field.click();
await options.locator('text=English').click();
await field.click();
await expect(page.locator('.form-submit .btn')).toContainText('Save');
});
test('should allow custom translation', async () => {
await page.goto(url.account);
await expect(page.locator('.step-nav')).toContainText('Home');
});
});
describe('list view', () => {
const tableRowLocator = 'table >> tbody >> tr';
beforeEach(async () => {
await page.goto(url.list);
});
describe('filtering', () => {
test('search by id', async () => {
const { id } = await createPost();
await page.locator('.search-filter__input').fill(id);
const tableItems = page.locator(tableRowLocator);
await expect(tableItems).toHaveCount(1);
});
test('search by title or description', async () => {
await createPost({
title: 'find me',
description: 'this is fun',
});
await page.locator('.search-filter__input').fill('find me');
await expect(page.locator(tableRowLocator)).toHaveCount(1);
await page.locator('.search-filter__input').fill('this is fun');
await expect(page.locator(tableRowLocator)).toHaveCount(1);
});
test('toggle columns', async () => {
const columnCountLocator = 'table >> thead >> tr >> th';
await createPost();
await page.locator('.list-controls__toggle-columns').click();
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible();
const numberOfColumns = await page.locator(columnCountLocator).count();
await expect(page.locator('table >> thead >> tr >> th:nth-child(2)')).toHaveText('ID');
const idButton = page.locator('.column-selector >> text=ID');
// Remove ID column
await idButton.click();
// wait until .cell-id is not present on the page:
await page.locator('.cell-id').waitFor({ state: 'detached' });
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns - 1);
await expect(page.locator('table >> thead >> tr >> th:nth-child(2)')).toHaveText('Number');
// Add back ID column
await idButton.click();
await expect(page.locator('.cell-id')).toBeVisible();
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns);
await expect(page.locator('table >> thead >> tr >> th:nth-child(2)')).toHaveText('ID');
});
test('2nd cell is a link', async () => {
const { id } = await createPost();
const linkCell = page.locator(`${tableRowLocator} td`).nth(1).locator('a');
await expect(linkCell).toHaveAttribute('href', `/admin/collections/posts/${id}`);
// open the column controls
await page.locator('.list-controls__toggle-columns').click();
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible();
// toggle off the ID column
page.locator('.column-selector >> text=ID').click();
// wait until .cell-id is not present on the page:
await page.locator('.cell-id').waitFor({ state: 'detached' });
// recheck that the 2nd cell is still a link
await expect(linkCell).toHaveAttribute('href', `/admin/collections/posts/${id}`);
});
test('filter rows', async () => {
const { id } = await createPost({ title: 'post1' });
await createPost({ title: 'post2' });
// open the column controls
await page.locator('.list-controls__toggle-columns').click();
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible();
// ensure the ID column is active
const idButton = page.locator('.column-selector >> text=ID');
const buttonClasses = await idButton.getAttribute('class');
if (buttonClasses && !buttonClasses.includes('column-selector__column--active')) {
await idButton.click();
await expect(page.locator(tableRowLocator).first().locator('.cell-id')).toBeVisible();
}
await expect(page.locator(tableRowLocator)).toHaveCount(2);
await page.locator('.list-controls__toggle-where').click();
// wait until the filter UI is visible and fully expanded
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible();
await page.locator('.where-builder__add-first-filter').click();
const operatorField = page.locator('.condition__operator');
const valueField = page.locator('.condition__value >> input');
await operatorField.click();
const dropdownOptions = operatorField.locator('.rs__option');
await dropdownOptions.locator('text=equals').click();
await valueField.fill(id);
await expect(page.locator(tableRowLocator)).toHaveCount(1);
const firstId = await page.locator(tableRowLocator).first().locator('.cell-id').innerText();
expect(firstId).toEqual(id);
// Remove filter
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', () => {
const reorderColumns = async () => {
// open the column controls
await page.locator('.list-controls__toggle-columns').click();
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible();
const numberBoundingBox = await page.locator('.column-selector >> text=Number').boundingBox();
const idBoundingBox = await page.locator('.column-selector >> text=ID').boundingBox();
if (!numberBoundingBox || !idBoundingBox) return;
// drag the "number" column to the left of the "ID" column
await page.mouse.move(numberBoundingBox.x + 2, numberBoundingBox.y + 2, { steps: 10 });
await page.mouse.down();
await wait(300);
await page.mouse.move(idBoundingBox.x - 2, idBoundingBox.y - 2, { steps: 10 });
await page.mouse.up();
// ensure the "number" column is now first
await expect(page.locator('.list-controls .column-selector .column-selector__column').first()).toHaveText('Number');
await expect(page.locator('table thead tr th').nth(1)).toHaveText('Number');
// TODO: This wait makes sure the preferences are actually saved. Just waiting for the UI to update is not enough. We should replace this wait
await wait(1000);
};
test('should drag to reorder columns and save to preferences', async () => {
await createPost();
await reorderColumns();
// reload to ensure the preferred order was stored in the database
await page.reload();
await expect(page.locator('.list-controls .column-selector .column-selector__column').first()).toHaveText('Number');
await expect(page.locator('table thead tr th').nth(1)).toHaveText('Number');
});
test('should render drawer columns in order', async () => {
// Re-order columns like done in the previous test
await createPost();
await reorderColumns();
await page.reload();
await createPost();
await page.goto(url.create);
// Open the drawer
await page.locator('.rich-text .list-drawer__toggler').click();
const listDrawer = page.locator('[id^=list-drawer_1_]');
await expect(listDrawer).toBeVisible();
const collectionSelector = page.locator('[id^=list-drawer_1_] .list-drawer__select-collection.react-select');
// select the "Post en" collection
await collectionSelector.click();
await page.locator('[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option >> text="Post en"').click();
// open the column controls
const columnSelector = page.locator('[id^=list-drawer_1_] .list-controls__toggle-columns');
await columnSelector.click();
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible();
// ensure that the columns are in the correct order
await expect(page.locator('[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column').first()).toHaveText('Number');
});
test('should retain preferences when changing drawer collections', async () => {
await page.goto(url.create);
// Open the drawer
await page.locator('.rich-text .list-drawer__toggler').click();
const listDrawer = page.locator('[id^=list-drawer_1_]');
await expect(listDrawer).toBeVisible();
const collectionSelector = page.locator('[id^=list-drawer_1_] .list-drawer__select-collection.react-select');
const columnSelector = page.locator('[id^=list-drawer_1_] .list-controls__toggle-columns');
// open the column controls
await columnSelector.click();
// wait until the column toggle UI is visible and fully expanded
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible();
// deselect the "id" column
await page.locator('[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column >> text=ID').click();
// select the "Post en" collection
await collectionSelector.click();
await page.locator('[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option >> text="Post en"').click();
// deselect the "number" column
await page.locator('[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column >> text=Number').click();
// select the "User" collection again
await collectionSelector.click();
await page.locator('[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option >> text="User"').click();
// ensure that the "id" column is still deselected
await expect(page.locator('[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column').first()).not.toHaveClass('column-selector__column--active');
// select the "Post en" collection again
await collectionSelector.click();
await page.locator('[id^=list-drawer_1_] .list-drawer__select-collection.react-select .rs__option >> text="Post en"').click();
// ensure that the "number" column is still deselected
await expect(page.locator('[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column').first()).not.toHaveClass('column-selector__column--active');
});
test('should render custom table cell component', async () => {
await createPost();
await page.goto(url.list);
await expect(page.locator('table >> thead >> tr >> th >> text=Demo UI Field')).toBeVisible();
});
});
describe('multi-select', () => {
beforeEach(async () => {
await mapAsync([...Array(3)], async () => {
await createPost();
});
});
test('should select multiple rows', async () => {
const selectAll = page.locator('.custom-checkbox:has(#select-all)');
await page.locator('.row-1 .cell-_select input').check();
const indeterminateSelectAll = selectAll.locator('.custom-checkbox__icon.partial');
expect(indeterminateSelectAll).toBeDefined();
await selectAll.locator('input').click();
const emptySelectAll = selectAll.locator('.custom-checkbox__icon:not(.check):not(.partial)');
await expect(emptySelectAll).toHaveCount(0);
await selectAll.locator('input').click();
const checkSelectAll = selectAll.locator('.custom-checkbox__icon.check');
expect(checkSelectAll).toBeDefined();
});
test('should delete many', async () => {
// delete should not appear without selection
await expect(page.locator('#confirm-delete')).toHaveCount(0);
// select one row
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 .cell-_select input').check();
await page.locator('.delete-documents__toggle').click();
await page.locator('#confirm-delete').click();
await expect(await page.locator('.cell-_select')).toHaveCount(1);
});
});
describe('pagination', () => {
beforeAll(async () => {
await mapAsync([...Array(11)], async () => {
await createPost();
});
});
test('should paginate', async () => {
const pageInfo = page.locator('.collection-list__page-info');
const perPage = page.locator('.per-page');
const paginator = page.locator('.paginator');
const tableItems = page.locator(tableRowLocator);
await expect(tableItems).toHaveCount(10);
await expect(pageInfo).toHaveText('1-10 of 11');
await expect(perPage).toContainText('Per Page: 10');
// Forward one page and back using numbers
await paginator.locator('button').nth(1).click();
expect(page.url()).toContain('page=2');
await expect(tableItems).toHaveCount(1);
await paginator.locator('button').nth(0).click();
expect(page.url()).toContain('page=1');
await expect(tableItems).toHaveCount(10);
});
});
describe('custom css', () => {
test('should see custom css in admin UI', async () => {
await page.goto(url.admin);
const navControls = page.locator('.nav__controls');
await expect(navControls).toHaveCSS('font-family', 'monospace');
});
});
// TODO: Troubleshoot flaky suite
describe.skip('sorting', () => {
beforeAll(async () => {
await createPost();
await createPost();
});
test('should sort', async () => {
const upChevron = page.locator('#heading-id .sort-column__asc');
const downChevron = page.locator('#heading-id .sort-column__desc');
const firstId = await page.locator('.row-1 .cell-id').innerText();
const secondId = await page.locator('.row-2 .cell-id').innerText();
await upChevron.click({ delay: 200 });
// Order should have swapped
expect(await page.locator('.row-1 .cell-id').innerText()).toEqual(secondId);
expect(await page.locator('.row-2 .cell-id').innerText()).toEqual(firstId);
await downChevron.click({ delay: 200 });
// Swap back
expect(await page.locator('.row-1 .cell-id').innerText()).toEqual(firstId);
expect(await page.locator('.row-2 .cell-id').innerText()).toEqual(secondId);
});
});
describe('i18n', () => {
test('should display translated collections and globals config options', async () => {
await page.goto(url.list);
// collection label
await expect(page.locator('#nav-posts')).toContainText('Posts en');
// global label
await expect(page.locator('#nav-global-global')).toContainText('Global en');
// view description
await expect(page.locator('.view-description')).toContainText('Description en');
});
test('should display translated field titles', async () => {
await createPost();
// column controls
await page.locator('.list-controls__toggle-columns').click();
await expect(page.locator('.column-selector__column >> text=Title en')).toHaveText('Title en');
// filters
await page.locator('.list-controls__toggle-where').click();
await page.locator('.where-builder__add-first-filter').click();
await page.locator('.condition__field .rs__control').click();
const options = page.locator('.rs__option');
await expect(options.locator('text=Title en')).toHaveText('Title en');
// list columns
await expect(page.locator('#heading-title .sort-column__label')).toHaveText('Title en');
await expect(page.locator('.search-filter input')).toHaveAttribute('placeholder', /(Title en)/);
});
test('should use fallback language on field titles', async () => {
// change language German
await page.goto(url.account);
await page.locator('.account__language .react-select').click();
const languageSelect = page.locator('.rs__option');
// text field does not have a 'de' label
await languageSelect.locator('text=Deutsch').click();
await page.goto(url.list);
await page.locator('.list-controls__toggle-columns').click();
// expecting the label to fall back to english as default fallbackLng
await expect(page.locator('.column-selector__column >> text=Title en')).toHaveText('Title en');
});
});
});
});
async function createPost(overrides?: Partial<Post>): Promise<Post> {
return payload.create({
collection: slug,
data: {
title,
description,
...overrides,
},
});
}
async function clearDocs(): Promise<void> {
const allDocs = await payload.find({ collection: slug, limit: 100 });
const ids = allDocs.docs.map((doc) => doc.id);
await mapAsync(ids, async (id) => {
await payload.delete({ collection: slug, id });
});
}

View File

@@ -1,140 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
collections: {
users: User;
'hidden-collection': HiddenCollection;
posts: Post;
'group-one-collection-ones': GroupOneCollectionOne;
'group-one-collection-twos': GroupOneCollectionTwo;
'group-two-collection-ones': GroupTwoCollectionOne;
'group-two-collection-twos': GroupTwoCollectionTwo;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
globals: {
'hidden-global': HiddenGlobal;
global: Global;
'group-globals-one': GroupGlobalsOne;
'group-globals-two': GroupGlobalsTwo;
};
}
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
salt?: string;
hash?: string;
loginAttempts?: number;
lockUntil?: string;
password?: string;
}
export interface HiddenCollection {
id: string;
title?: string;
updatedAt: string;
createdAt: string;
}
export interface Post {
id: string;
title?: string;
description?: string;
number?: number;
richText?: {
[k: string]: unknown;
}[];
updatedAt: string;
createdAt: string;
}
export interface GroupOneCollectionOne {
id: string;
title?: string;
updatedAt: string;
createdAt: string;
}
export interface GroupOneCollectionTwo {
id: string;
title?: string;
updatedAt: string;
createdAt: string;
}
export interface GroupTwoCollectionOne {
id: string;
title?: string;
updatedAt: string;
createdAt: string;
}
export interface GroupTwoCollectionTwo {
id: string;
title?: string;
updatedAt: string;
createdAt: string;
}
export interface PayloadPreference {
id: string;
user: {
value: string | User;
relationTo: 'users';
};
key?: string;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
export interface PayloadMigration {
id: string;
name?: string;
batch?: number;
schema?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
export interface HiddenGlobal {
id: string;
title?: string;
updatedAt?: string;
createdAt?: string;
}
export interface Global {
id: string;
title?: string;
updatedAt?: string;
createdAt?: string;
}
export interface GroupGlobalsOne {
id: string;
title?: string;
updatedAt?: string;
createdAt?: string;
}
export interface GroupGlobalsTwo {
id: string;
title?: string;
updatedAt?: string;
createdAt?: string;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

View File

@@ -1,2 +0,0 @@
export const slug = 'posts';
export const globalSlug = 'global';

View File

@@ -1,7 +0,0 @@
.nav__controls {
font-family: monospace;
background-image: url('/placeholder.png');
}
.nav__controls:before {
content: 'custom-css';
}