Compare commits

..

26 Commits

Author SHA1 Message Date
Elliot DeNolf
e3ef443217 chore(release): db-postgres/0.1.2 2023-10-10 15:57:51 -04:00
Elliot DeNolf
a42e84bbb2 chore(eslint): prepare config for publishing 2023-10-10 15:10:22 -04:00
James Mikrut
470bdb72ff Merge pull request #3553 from payloadcms/fix/#3541
fix: #3541
2023-10-10 14:49:11 -04:00
James Mikrut
5c36be949c Merge pull request #3554 from payloadcms/fix/#3540
fix: #3540
2023-10-10 14:49:01 -04:00
James
2567ac58ba fix: #3540 2023-10-10 14:21:12 -04:00
Jacob Fletcher
9ff014bbfe fix: row field width (#3550) 2023-10-10 14:09:47 -04:00
James
e6f0d35985 fix: #3541 2023-10-10 14:07:26 -04:00
Alessio Gravili
b1e449e005 Merge pull request #3551 from payloadcms/fix/slate-toolbar
fix: Slate toolbar rendered even if it has no elements and leaves
2023-10-10 19:36:29 +02:00
Alessio Gravili
9ae585d23c fix: Slate toolbar rendered even if it has no elements and leaves 2023-10-10 19:22:08 +02:00
Elliot DeNolf
9de3320933 chore(release): richtext-lexical/0.1.5 2023-10-10 12:08:13 -04:00
Elliot DeNolf
5d429fa7ae chore(release): live-preview-react/0.1.2 2023-10-10 12:08:08 -04:00
Elliot DeNolf
dc8f1925f0 chore(release): live-preview/0.1.2 2023-10-10 12:07:40 -04:00
Jessica Chowdhury
15f650afde docs: adds build your own plugin page (#3184)
* docs: adds build your own plugin page

* chore(docs): adds npx command to plugin template doc

* docs: update plugin doc order values

* docs: update plugin admin compatibility to coming soon

---------

Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2023-10-10 11:37:53 -04:00
Jacob Fletcher
c945384d63 fix(live-preview-react): moves react to peer dependencies (#3545) 2023-10-10 11:36:44 -04:00
Jacob Fletcher
dfada1b238 chore(live-preview): removes react dependencies (#3544) 2023-10-10 11:07:18 -04:00
Elliot DeNolf
229bdda2c1 chore: rename publish script 2023-10-10 10:48:51 -04:00
Alessio Gravili
a1d51fb410 Merge pull request #3543 from payloadcms/fix/lexical-blocks-validation
fix(richtext-lexical): blocks: missing properties passed into validation calls
2023-10-10 16:41:39 +02:00
Alessio Gravili
46430f5598 chore: prefer config.collections over payload.collections for validations 2023-10-10 16:20:11 +02:00
Alessio Gravili
e41899cd27 fix(richtext-lexical): missing properties passed into validation functions 2023-10-10 16:06:39 +02:00
Elliot DeNolf
890af8be05 ci: optimize e2e and refactor workflow (#3530)
* ci: split e2e

* chore: 3 parts

* chore: use matrix

* chore: use playwright container, bump playwright

* chore: remove playwright container

* ci: move all packages into matrix

* ci: reusable action to restore build cache

* chore: revert custom action

* chore: cleanup logs
2023-10-09 23:43:34 -04:00
Tylan Davis
8bfae6b932 docs: removes MONGODB_URI (#3482) 2023-10-09 18:07:09 -04:00
Jacob Fletcher
ace3e577f6 fix: renders global label as page title (#3532) 2023-10-09 17:58:21 -04:00
Marcus R
9198245ad9 docs(configuration/collections): moves pagination options to admin config (#3533) 2023-10-09 17:55:34 -04:00
Jarrod Flesch
f0095937ba fix: increases document controls popup list button hitbox (#3529) 2023-10-09 16:44:57 -04:00
Jarrod Flesch
0bbd7137cd chore: properly clear cart with correct data shape (#3500) 2023-10-09 16:29:15 -04:00
Thomas Dudziak
ffed34cf27 chore(docs): corrects bundler package import paths (#3523) 2023-10-09 16:03:16 -04:00
51 changed files with 807 additions and 403 deletions

View File

@@ -31,6 +31,11 @@ body:
description: What version of Payload are you running?
validations:
required: true
- type: input
id: adapters-plugins
attributes:
label: Adapters and Plugins
description: What adapters and plugins are you using? ie. db-mongodb, db-postgres, bundler-webpack, etc.
- type: markdown
attributes:
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.

View File

@@ -108,6 +108,10 @@ jobs:
tests-e2e:
runs-on: ubuntu-latest
needs: core-build
strategy:
fail-fast: false
matrix:
part: [1/4, 2/4, 3/4, 4/4]
steps:
- name: Use Node.js 18
@@ -128,14 +132,14 @@ jobs:
key: ${{ github.sha }}-${{ github.run_number }}
- name: E2E Tests
run: pnpm test:e2e --bail
run: pnpm test:e2e --part ${{ matrix.part }} --bail
- uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: test-results/
retention-days: 30
retention-days: 1
tests-type-generation:
runs-on: ubuntu-latest
@@ -165,11 +169,22 @@ jobs:
- name: Generate GraphQL schema file
run: pnpm dev:generate-graphql-schema graphql-schema-gen
# DB Adapters
build-db-mongodb:
build-packages:
name: Build Packages
runs-on: ubuntu-latest
needs: core-build
strategy:
fail-fast: false
matrix:
pkg:
- db-mongodb
- db-postgres
- bundler-webpack
- bundler-vite
- richtext-slate
- richtext-lexical
- live-preview
- live-preview-react
steps:
- name: Use Node.js 18
@@ -189,162 +204,5 @@ jobs:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build db-mongodb
run: pnpm turbo run build --filter=db-mongodb
build-db-postgres:
runs-on: ubuntu-latest
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build db-postgres
run: pnpm turbo run build --filter=db-postgres
# Bundlers
build-bundler-webpack:
runs-on: ubuntu-latest
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build bundler-webpack
run: pnpm turbo run build --filter=bundler-webpack
build-bundler-vite:
runs-on: ubuntu-latest
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build bundler-vite
run: pnpm turbo run build --filter=bundler-vite
# Other Plugins
build-plugin-richtext-slate:
runs-on: ubuntu-latest
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build richtext-slate
run: pnpm turbo run build --filter=richtext-slate
build-plugin-richtext-lexical:
runs-on: ubuntu-latest
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build richtext-lexical
run: pnpm turbo run build --filter=richtext-lexical
build-plugin-live-preview:
runs-on: ubuntu-latest
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build live-preview
run: pnpm turbo run build --filter=live-preview
- name: Build live-preview-react
run: pnpm turbo run build --filter=live-preview-react
- name: Build ${{ matrix.pkg }}
run: pnpm turbo run build --filter=${{ matrix.pkg }}

2
.vscode/launch.json vendored
View File

@@ -17,7 +17,7 @@
"type": "node-terminal"
},
{
"command": "pnpm run dev:postgres collections-graphql",
"command": "pnpm run dev:postgres fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Postgres",
"request": "launch",

View File

@@ -12,13 +12,13 @@ Payload has two official bundlers, the [Webpack Bundler](/docs/admin/webpack) an
Webpack (recommended):
```text
yarn add @payloadcms/webpack-bundler
yarn add @payloadcms/bundler-webpack
```
Vite (beta):
```text
yarn add @payloadcms/vite-bundler
yarn add @payloadcms/bundler-vite
```
##### Configure the bundler

View File

@@ -29,7 +29,6 @@ It's often best practice to write your Collections in separate files and then im
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
| **`pagination`** | Set pagination-specific options for this collection. [More](#pagination) |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
_\* An asterisk denotes that a property is required._
@@ -84,6 +83,7 @@ property on a collection's config.
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More](#list-searchable-fields) |
| **`pagination`** | Set pagination-specific options for this collection. [More](#pagination) |
### Preview

View File

@@ -412,7 +412,7 @@ dotenv.config({
path: path.resolve(__dirname, '../.env'),
})
const { PAYLOAD_SECRET, MONGODB_URI } = process.env
const { PAYLOAD_SECRET } = process.env
const doAction = async (): Promise<void> => {
await payload.init({

View File

@@ -1,10 +1,10 @@
---
title: Admin panel compatibility
label: Admin compatibility
order: 20
order: 30
desc: NEEDS TO BE WRITTEN
---
TODO: talk about how plugins need to ensure compatibility with both Vite and Webpack
- It's best practice to alias your plugin to an admin-only version, so if you have admin-only changes, put them in the admin plugin, and then leave the full plugin, complete with server code, to be installed on the server side
<Banner type="success">
COMING SOON: This page is a work in progress. Check back soon for more information.
</Banner>

View File

@@ -0,0 +1,294 @@
---
title: Building Your Own Plugin
label: Build Your Own
order: 20
desc: Starting to build your own plugin? Find everything you need and learn best practices with the Payload plugin template.
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, express
---
Building your own plugin is easy, and if you&apos;re already familiar with Payload then you&apos;ll have everything you need to get started. You can either start from scratch or use the Payload plugin template to get up and running quickly.
<Banner type="success">
To use the template, run `npx create-payload-app@latest -t plugin -n my-new-plugin` directly in your terminal or [clone the template directly from GitHub](https://github.com/payloadcms/payload-plugin-template).
</Banner>
Our plugin template includes everything you need to build a full life-cycle plugin:
* Example files and functions for extending the payload config
* A local dev environment to develop the plugin
* Test suite with integrated GitHub workflow
By abstracting your code into a plugin, you&apos;ll be able to reuse your feature across multiple projects and make it available for other developers to use.
### Plugins Recap
Here is a brief recap of how to integrate plugins with Payload, to learn more head back to the [plugin overview page](https://payloadcms.com/docs/plugins/overview).
#### How to install a plugin
To install any plugin, simply add it to your Payload config in the plugins array.
```
import samplePlugin from 'sample-plugin';
const config = buildConfig({
plugins: [
// Add plugins here
samplePlugin({
enabled: true,
}),
],
});
export default config;
```
#### Initialization
The initialization process goes in the following order:
1. Incoming config is validated
2. Plugins execute
3. Default options are integrated
4. Sanitization cleans and validates data
5. Final config gets initialized
### Plugin Template
In the [Payload plugin template](https://github.com/payloadcms/payload-plugin-template), you will see a common file structure that is used across plugins:
1. root folder - general configuration
2. /src folder - everything related to the plugin
3. /dev folder - sanitized test project for development
#### Root
In the root folder, you will see various files related to the configuration of the plugin. We set up our environment in a similar manner in Payload core and across other projects. The only two files you need to modify are:
* **README**.md - This contains instructions on how to use the template. When you are ready, update this to contain instructions on how to use your Plugin.
* **package**.json - Contains necessary scripts and dependencies. Overwrite the metadata in this file to describe your Plugin.
#### Dev
The purpose of the **dev** folder is to provide a sanitized local Payload project. so you can run and test your plugin while you are actively developing it.
Do **not** store any of the plugin functionality in this folder - it is purely an environment to _assist_ you with developing the plugin.
If you&apos;re starting from scratch, you can easily setup a dev environment like this:
```
mkdir dev
cd dev
npx create-payload-app
```
If you&apos;re using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config()`.
```
plugins: [
// when you rename the plugin or add options, make sure to update it here
samplePlugin({
enabled: false,
})
]
```
You can add to the `dev/payload.config` and build out the dev project as needed to test your plugin.
When you&apos;re ready to start development, navigate into this folder with `cd dev`
And then start the project with `yarn dev` and pull up `http://localhost:3000` in your browser.
### Testing
Another benefit of the dev folder is that you have the perfect environment established for testing.
A good test suite is essential to ensure quality and stability in your plugin. Payload typically uses [Jest](https://jestjs.io/); a popular testing framework, widely used for testing JavaScript and particularly for applications built with React.
Jest organizes tests into test suites and cases. We recommend creating tests based on the expected behavior of your plugin from start to finish. Read more about tests in the [Jest documentation.](https://jestjs.io/)
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you&apos;re all set!
```
import payload from 'payload'
describe('Plugin tests', () => {
// Example test to check for seeded data
it('seeds data accordingly', async () => {
const newCollectionQuery = await payload.find({
collection: 'newCollection',
sort: 'createdAt',
})
newCollection = newCollectionQuery.docs
expect(newCollectionQuery.totalDocs).toEqual(1)
})
})
```
### Seeding data
For development and testing, you will likely need some data to work with. You can streamline this process by seeding and dropping your database - instead of manually entering data.
In the plugin template, you can navigate to `dev/src/server.ts` and see an example seed function.
```
if (process.env.PAYLOAD_SEED === 'true') {
await seed(payload)
}
```
A sample seed function has been created for you at `dev/src/seed`, update this file with additional data as needed.
```
export const seed = async (payload: Payload): Promise<void> => {
payload.logger.info('Seeding data...')
await payload.create({
collection: 'new-collection',
data: {
title: 'Seeded title',
},
})
// Add additional seed data here
}
```
#### Src
Now that we have our environment setup and dev project ready to go - it&apos;s time to build the plugin!
**index.ts**
First up, the `src/index.ts` file - this is where the plugin should be imported from. It is best practice not to build the plugin directly in this file, instead we use this to export the plugin and types from their respective files.
**Plugin.ts**
To reiterate, the essence of a payload plugin is simply to extend the Payload config - and that is exactly what we are doing in this file.
```
export const samplePlugin =
(pluginOptions: PluginTypes) =>
(incomingConfig: Config): Config => {
let config = { ...incomingConfig }
// do something cool with the config here
return config
}
```
1. First, you need to receive the existing Payload config along with any plugin options.
2. Then set the variable `config` to be equal to a copy of the existing config.
3. From here, you can extend the config however you like!
4. Finally, return the config and you&apos;re all set.
### Spread Syntax
[Spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) (or the spread operator) is a feature in JavaScript that uses the dot notation **(...)** to spread elements from arrays, strings, or objects into various contexts.
We are going to use spread syntax to allow us to add data to existing arrays without losing the existing data. It is crucial to spread the existing data correctly, else this can cause adverse behavior and conflicts with Payload config and other plugins.
Let&apos;s say you want to build a plugin that adds a new collection:
```
config.collections = [
...(config.collections || []),
newCollection,
// Add additional collections here
]
```
First, you need to spread the `config.collections` to ensure that we don&apos;t lose the existing collections. Then you can add any additional collections, just as you would in a regular payload config.
This same logic is applied to other properties like admin, globals, hooks:
```
config.globals = [
...(config.globals || []),
// Add additional globals here
]
config.hooks = {
...(config.hooks || {}),
// Add additional hooks here
}
```
Some properties will be slightly different to extend, for instance the `onInit` property:
```
config.onInit = async payload => {
if (incomingConfig.onInit) await incomingConfig.onInit(payload)
// Add additional onInit code by using the onInitExtension function
onInitExtension(pluginOptions, payload)
}
```
If you wish to add to the `onInit`, you must include the async/await. We don&apos;t use spread syntax in this case, instead you must await the existing `onInit` before running additional functionality.
In the template, we have stubbed out a basic `onInitExtension` file that you can use, if not needed feel free to delete it.
### Webpack
If any of your files use server only packages such as fs, stripe, nodemailer, etc, they will need to be removed from the browser bundle. To do that, you can [alias the file imports with webpack](https://payloadcms.com/docs/admin/webpack#aliasing-server-only-modules).
When files are bundled for the browser, the import paths are essentially crawled to determine what files to include in the bundle. To prevent the server only files from making it into the bundle, we can alias their import paths to a file that can be included in the browser. This will short-circuit the import path crawling and ensure browser only code is bundled.
Webpack is another part of the Payload config that can be a little more tricky to extend. To help here, the template includes a helper function `extendWebpackConfig()` which takes care of spreading the existing webpack, so you can just add your new stuff:
```
config.admin = {
...(config.admin || {}),
// Add your aliases to the helper function below
webpack: extendWebpackConfig(incomingConfig)
}
```
### Types
If your plugin has options, you should define and provide types for these options in a separate file which gets exported from the main `index.ts`.
```
export interface PluginTypes {
/**
* Enable or disable plugin
* @default false
*/
enabled?: boolean
}
```
If possible, include [JSDoc comments](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#types-1) to describe the options and their types. This allows a developer to see details about the options in their editor.
### Best practices
In addition to the setup covered above, here are other best practices to follow:
##### Providing an enable / disable option:
For a better user experience, provide a way to disable the plugin without uninstalling it. This is especially important if your plugin adds additional webpack aliases, this will allow you to still let the webpack run to prevent errors.
##### Include tests in your GitHub CI workflow:
If you&apos;ve configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs)
##### Publish your finished plugin to NPM:
The best way to share and allow others to use your plugin once it is complete is to publish an NPM package. This process is straightforward and well documented, find out more about [creating and publishing a NPM package here](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/).
##### Add payload-plugin topic tag:
Apply the tag **payload-plugin** to your GitHub repository. This will boost the visibility of your plugin and ensure it gets listed with [existing payload plugins](https://github.com/topics/payload-plugin).
##### Use [Semantic Versioning](https://semver.org/) (SemVar):
With the SemVar system you release version numbers that reflect the nature of changes (major, minor, patch). Ensure all major versions reference their Payload compatibility.

View File

@@ -160,7 +160,7 @@ DigitalOcean provides extremely helpful documentation that can walk you through
## Docker
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `MONGODB_URI` if needed.
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
```dockerfile
FROM node:18-alpine as base
@@ -210,7 +210,7 @@ services:
depends_on:
- mongo
environment:
MONGODB_URI: mongodb://mongo:27017/payload
DATABASE_URI: mongodb://mongo:27017/payload
PORT: 3000
NODE_ENV: development
PAYLOAD_SECRET: TESTING

View File

@@ -34,7 +34,7 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@playwright/test": "1.37.1",
"@playwright/test": "1.38.1",
"@swc/cli": "^0.1.62",
"@swc/jest": "0.2.29",
"@swc/register": "0.1.10",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.1",
"version": "0.1.2",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import type { Field } from 'payload/types'
import { fieldAffectsData } from 'payload/types'
import { fieldAffectsData, tabHasName } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types'
@@ -31,6 +31,42 @@ export const traverseFields = ({
topLevelTableName,
}: TraverseFieldArgs) => {
fields.forEach((field) => {
if (field.type === 'collapsible' || field.type === 'row') {
traverseFields({
_locales,
adapter,
currentArgs,
currentTableName,
depth,
fields: field.fields,
path,
topLevelArgs,
topLevelTableName,
})
return
}
if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
const tabPath = tabHasName(tab) ? `${path}${tab.name}_` : path
traverseFields({
_locales,
adapter,
currentArgs,
currentTableName,
depth,
fields: tab.fields,
path: tabPath,
topLevelArgs,
topLevelTableName,
})
})
return
}
if (fieldAffectsData(field)) {
switch (field.type) {
case 'array': {

View File

@@ -406,7 +406,7 @@ export const traverseFields = ({
indexes,
localesColumns,
localesIndexes,
newTableName: parentTableName,
newTableName,
parentTableName,
relationsToBuild,
relationships,

View File

@@ -3,6 +3,6 @@ module.exports = {
jest: true,
},
plugins: ['jest', 'jest-dom'],
extends: ['./rules/jest.cjs', './rules/jest-dom.cjs'].map(require.resolve),
extends: ['./rules/jest.js', './rules/jest-dom.js'].map(require.resolve),
rules: {},
}

View File

@@ -13,6 +13,6 @@ module.exports = {
jsx: true,
},
},
extends: ['./rules/react-a11y.cjs', './rules/react.cjs'].map(require.resolve),
extends: ['./rules/react-a11y.js', './rules/react.js'].map(require.resolve),
rules: {},
}

View File

@@ -11,8 +11,8 @@ module.exports = {
'plugin:regexp/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'./configs/jest/index.cjs',
'./configs/react/index.cjs',
'./configs/jest/index.js',
'./configs/react/index.js',
'prettier',
],
parser: '@typescript-eslint/parser',

View File

@@ -1,4 +1,4 @@
module.exports = {
root: true,
extends: ['./eslint-config/index.cjs'],
extends: ['./eslint-config/index.js'],
}

View File

@@ -1,7 +1,6 @@
{
"name": "@payloadcms/eslint-config",
"version": "0.0.1",
"private": true,
"description": "Payload styles for ESLint and Prettier",
"license": "MIT",
"author": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "0.1.1",
"version": "0.1.2",
"description": "The official live preview React SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -17,15 +17,16 @@
"prepublishOnly": "pnpm clean && pnpm build"
},
"dependencies": {
"react": "18.2.0",
"@payloadcms/live-preview": "workspace:*"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/node": "20.5.7",
"@types/react": "18.2.15",
"@types/react": "^18.2.0",
"payload": "workspace:*"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"exports": {
".": {
"default": "./src/index.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "0.1.1",
"version": "0.1.2",
"description": "The official live preview JavaScript SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -16,13 +16,8 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm build"
},
"dependencies": {
"react": "18.2.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/node": "20.5.7",
"@types/react": "18.2.15",
"payload": "workspace:*"
},
"exports": {

View File

@@ -13,6 +13,7 @@ import { useForm } from '../../forms/Form/context'
import MinimalTemplate from '../../templates/Minimal'
import { useConfig } from '../../utilities/Config'
import Button from '../Button'
import * as PopupList from '../Popup/PopupButtonList'
import './index.scss'
const baseClass = 'delete-document'
@@ -35,46 +36,52 @@ const DeleteDocument: React.FC<Props> = (props) => {
const { toggleModal } = useModal()
const history = useHistory()
const { i18n, t } = useTranslation('general')
const title = useTitle(collection)
const title = useTitle({ collection })
const titleToRender = titleFromProps || title
const modalSlug = `delete-${id}`
const addDefaultError = useCallback(() => {
setDeleting(false)
toast.error(t('error:deletingTitle', { title }))
}, [t, title])
const handleDelete = useCallback(() => {
const handleDelete = useCallback(async () => {
setDeleting(true)
setModified(false)
requests
.delete(`${serverURL}${api}/${slug}/${id}`, {
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
})
.then(async (res) => {
try {
const json = await res.json()
if (res.status < 400) {
try {
await requests
.delete(`${serverURL}${api}/${slug}/${id}`, {
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
})
.then(async (res) => {
try {
const json = await res.json()
if (res.status < 400) {
setDeleting(false)
toggleModal(modalSlug)
toast.success(t('titleDeleted', { label: getTranslation(singular, i18n), title }))
return history.push(`${admin}/collections/${slug}`)
}
toggleModal(modalSlug)
toast.success(t('titleDeleted', { label: getTranslation(singular, i18n), title }))
return history.push(`${admin}/collections/${slug}`)
}
toggleModal(modalSlug)
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
}
return false
} catch (e) {
return addDefaultError()
}
return false
} catch (e) {
return addDefaultError()
}
})
})
} catch (e) {
addDefaultError()
}
}, [
setModified,
serverURL,
@@ -95,9 +102,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
if (id) {
return (
<React.Fragment>
<Button
buttonStyle="none"
className={`${baseClass}__toggle`}
<PopupList.Button
id={buttonId}
onClick={() => {
setDeleting(false)
@@ -105,7 +110,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
}}
>
{t('delete')}
</Button>
</PopupList.Button>
<Modal className={baseClass} slug={modalSlug}>
<MinimalTemplate className={`${baseClass}__template`}>
<h1>{t('confirmDeletion')}</h1>

View File

@@ -214,20 +214,12 @@ export const DocumentControls: React.FC<{
</PopupList.Button>
{!collection?.admin?.disableDuplicate && isEditing && (
<PopupList.Button>
<DuplicateDocument
collection={collection}
id={id}
slug={collection?.slug}
/>
</PopupList.Button>
<DuplicateDocument collection={collection} id={id} slug={collection?.slug} />
)}
</React.Fragment>
)}
{'delete' in permissions && permissions?.delete?.permission && id && (
<PopupList.Button>
<DeleteDocument buttonId="action-delete" collection={collection} id={id} />
</PopupList.Button>
<DeleteDocument buttonId="action-delete" collection={collection} id={id} />
)}
</PopupList.ButtonGroup>
</Popup>

View File

@@ -27,16 +27,14 @@ export const DocumentHeader: React.FC<{
{customHeader && customHeader}
{!customHeader && (
<Fragment>
{collection && (
<RenderTitle
className={`${baseClass}__title`}
collection={collection}
data={data}
fallback={`[${t('untitled')}]`}
useAsTitle={collection?.admin?.useAsTitle}
/>
)}
{global && <h1 className={`${baseClass}__title`}>{global?.slug}</h1>}
<RenderTitle
className={`${baseClass}__title`}
collection={collection}
data={data}
fallback={`[${t('untitled')}]`}
global={global}
useAsTitle={collection?.admin?.useAsTitle}
/>
<DocumentTabs
apiURL={apiURL}
collection={collection}

View File

@@ -12,6 +12,7 @@ import { useForm, useFormModified } from '../../forms/Form/context'
import MinimalTemplate from '../../templates/Minimal'
import { useConfig } from '../../utilities/Config'
import Button from '../Button'
import * as PopupList from '../Popup/PopupButtonList'
import './index.scss'
const baseClass = 'duplicate'
@@ -179,14 +180,9 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
return (
<React.Fragment>
<Button
buttonStyle="none"
className={baseClass}
id="action-duplicate"
onClick={() => handleClick(false)}
>
<PopupList.Button id="action-duplicate" onClick={() => handleClick(false)}>
{t('duplicate')}
</Button>
</PopupList.Button>
{modified && hasClicked && (
<Modal className={`${baseClass}__modal`} slug={modalSlug}>
<MinimalTemplate className={`${baseClass}__modal-template`}>

View File

@@ -15,10 +15,11 @@ const RenderTitle: React.FC<Props> = (props) => {
data,
element = 'h1',
fallback = '[untitled]',
global,
title: titleFromProps,
} = props
const titleFromForm = useTitle(collection)
const titleFromForm = useTitle({ collection, global })
let title = titleFromForm
if (!title) title = data?.id

View File

@@ -3,18 +3,28 @@
.field-type.row {
display: flex;
flex-wrap: wrap;
width: 100%;
gap: var(--base);
width: calc(100% + var(--base));
margin-left: calc(var(--base) * -0.5);
margin-right: calc(var(--base) * -0.5);
> * {
flex-grow: 1;
padding-left: calc(var(--base) * 0.5);
padding-right: calc(var(--base) * 0.5);
}
@include mid-break {
flex-direction: column;
display: block;
margin-left: 0;
margin-right: 0;
width: 100%;
> * {
margin-left: 0;
margin-right: 0;
width: 100% !important;
padding-left: 0;
padding-right: 0;
}
}
}

View File

@@ -18,8 +18,8 @@ import { GetFilterOptions } from '../../../utilities/GetFilterOptions'
import Error from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
const baseClass = 'upload'
@@ -81,7 +81,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
})
useEffect(() => {
if (typeof value === 'string' && value !== '') {
if (typeof value !== 'undefined' && value !== '') {
const fetchFile = async () => {
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, {
credentials: 'include',

View File

@@ -49,8 +49,7 @@ export const SetStepNav: React.FC<
slug = globalFromProps?.slug
}
// This only applies to collections
const title = useTitle(collection)
const title = useTitle({ collection, global })
const { setStepNav } = useStepNav()
@@ -80,7 +79,7 @@ export const SetStepNav: React.FC<
}
} else if (global) {
nav.push({
label: getTranslation(global.label, i18n),
label: title,
url: `${admin}/globals/${slug}`,
})
}

View File

@@ -4,9 +4,11 @@ import { useTranslation } from 'react-i18next'
import type { SanitizedCollectionConfig } from '../../collections/config/types'
import type { SanitizedConfig } from '../../config/types'
import type { SanitizedGlobalConfig } from '../../globals/config/types'
import type { FormField } from '../components/forms/Form/types'
import { getObjectDotNotation } from '../../utilities/getObjectDotNotation'
import { getTranslation } from '../../utilities/getTranslation'
import { useFormFields } from '../components/forms/Form/context'
import { useConfig } from '../components/utilities/Config'
import { formatDate } from '../utilities/formatDate'
@@ -54,19 +56,30 @@ export const formatUseAsTitle = (args: {
// Keep `collection` optional so that component do need to worry about conditionally rendering hooks
// This is so that components which take both `collection` and `global` props can use this hook
const useTitle = (collection?: SanitizedCollectionConfig): string => {
const useTitle = (args: {
collection?: SanitizedCollectionConfig
global?: SanitizedGlobalConfig
}): string => {
const { collection, global } = args
const { i18n } = useTranslation()
const config = useConfig()
let title: string = ''
const field = useFormFields(([formFields]) => {
if (!collection) return
return formFields[collection?.admin?.useAsTitle]
})
const config = useConfig()
if (collection) {
title = formatUseAsTitle({ collection, config, field, i18n })
}
if (!collection) return ''
if (global) {
title = getTranslation(global.label, i18n) || global.slug
}
return formatUseAsTitle({ collection, config, field, i18n })
return title
}
export default useTitle

View File

@@ -193,20 +193,25 @@ describe('Field Validations', () => {
})
describe('relationship', () => {
const relationCollection = {
fields: [
{
name: 'id',
type: 'text',
},
],
slug: 'relation',
}
const relationshipOptions = {
...options,
config: {
collections: [relationCollection],
},
payload: {
collections: {
relation: {
config: {
fields: [
{
name: 'id',
type: 'text',
},
],
slug: 'relation',
},
config: relationCollection,
},
},
},

View File

@@ -82,12 +82,11 @@ export const number: Validate<unknown, unknown, NumberField> = (
export const text: Validate<unknown, unknown, TextField> = (
value: string,
{ maxLength: fieldMaxLength, minLength, payload, required, t },
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
) => {
let maxLength: number
if (typeof payload?.config?.defaultMaxTextLength === 'number')
maxLength = payload.config.defaultMaxTextLength
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
if (typeof fieldMaxLength === 'number') maxLength = fieldMaxLength
if (value && maxLength && value.length > maxLength) {
return t('validation:shorterThanMax', { maxLength })
@@ -108,12 +107,11 @@ export const text: Validate<unknown, unknown, TextField> = (
export const password: Validate<unknown, unknown, TextField> = (
value: string,
{ maxLength: fieldMaxLength, minLength, payload, required, t },
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
) => {
let maxLength: number
if (typeof payload?.config?.defaultMaxTextLength === 'number')
maxLength = payload.config.defaultMaxTextLength
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
if (typeof fieldMaxLength === 'number') maxLength = fieldMaxLength
if (value && maxLength && value.length > maxLength) {
@@ -141,12 +139,11 @@ export const email: Validate<unknown, unknown, EmailField> = (value: string, { r
export const textarea: Validate<unknown, unknown, TextareaField> = (
value: string,
{ maxLength: fieldMaxLength, minLength, payload, required, t },
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
) => {
let maxLength: number
if (typeof payload?.config?.defaultMaxTextLength === 'number')
maxLength = payload.config.defaultMaxTextLength
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
if (typeof fieldMaxLength === 'number') maxLength = fieldMaxLength
if (value && maxLength && value.length > maxLength) {
return t('validation:shorterThanMax', { maxLength })
@@ -318,9 +315,10 @@ export const upload: Validate<unknown, unknown, UploadField> = (value: string, o
}
if (!canUseDOM && typeof value !== 'undefined' && value !== null) {
const idField = options?.payload?.collections[options.relationTo]?.config?.fields?.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
const idField = options?.config?.collections
?.find((collection) => collection.slug === options.relationTo)
?.fields?.find((field) => fieldAffectsData(field) && field.name === 'id')
const type = getIDType(idField, options?.payload?.db?.defaultIDType)
if (!isValidID(value, type)) {
@@ -335,10 +333,10 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
value: RelationshipValue,
options,
) => {
const { maxRows, minRows, payload, relationTo, required, t } = options
const { config, maxRows, minRows, payload, relationTo, required, t } = options
if ((!value || (Array.isArray(value) && value.length === 0)) && required) {
return options.t('validation:required')
return t('validation:required')
}
if (Array.isArray(value)) {
@@ -355,11 +353,11 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
const values = Array.isArray(value) ? value : [value]
const invalidRelationships = values.filter((val) => {
let collection: string
let collectionSlug: string
let requestedID
if (typeof relationTo === 'string') {
collection = relationTo
collectionSlug = relationTo
// custom id
if (val) {
@@ -368,17 +366,17 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
}
if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) {
collection = val.relationTo
collectionSlug = val.relationTo
requestedID = val.value
}
if (requestedID === null) return false
const idField = payload.collections[collection]?.config?.fields?.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
const idField = config?.collections
?.find((collection) => collection.slug === collectionSlug)
?.fields?.find((field) => fieldAffectsData(field) && field.name === 'id')
const type = getIDType(idField, options?.payload?.db?.defaultIDType)
const type = getIDType(idField, payload?.db?.defaultIDType)
return !isValidID(requestedID, type)
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.1.4",
"version": "0.1.5",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -40,10 +40,12 @@ export const blockValidationHOC = (
if ('validate' in field && typeof field.validate === 'function' && field.validate) {
const fieldValue = 'name' in field ? node.fields.data[field.name] : null
const validationResult = await field.validate(fieldValue, {
...field,
id: validation.options.id,
config: payloadConfig,
data: fieldValue,
operation: validation.options.operation,
payload: validation.options.payload,
siblingData: validation.options.siblingData,
t: validation.options.t,
user: validation.options.user,

View File

@@ -329,43 +329,45 @@ const RichText: React.FC<FieldProps> = (props) => {
value={valueToRender as any[]}
>
<div className={`${baseClass}__wrapper`}>
<div
className={[`${baseClass}__toolbar`, drawerIsOpen && `${baseClass}__drawerIsOpen`]
.filter(Boolean)
.join(' ')}
ref={toolbarRef}
>
<div className={`${baseClass}__toolbar-wrap`}>
{elements.map((element, i) => {
let elementName: string
if (typeof element === 'object' && element?.name) elementName = element.name
if (typeof element === 'string') elementName = element
{elements?.length + leaves?.length > 0 && (
<div
className={[`${baseClass}__toolbar`, drawerIsOpen && `${baseClass}__drawerIsOpen`]
.filter(Boolean)
.join(' ')}
ref={toolbarRef}
>
<div className={`${baseClass}__toolbar-wrap`}>
{elements.map((element, i) => {
let elementName: string
if (typeof element === 'object' && element?.name) elementName = element.name
if (typeof element === 'string') elementName = element
const elementType = enabledElements[elementName]
const Button = elementType?.Button
const elementType = enabledElements[elementName]
const Button = elementType?.Button
if (Button) {
return <Button fieldProps={props} key={i} path={path} />
}
if (Button) {
return <Button fieldProps={props} key={i} path={path} />
}
return null
})}
{leaves.map((leaf, i) => {
let leafName: string
if (typeof leaf === 'object' && leaf?.name) leafName = leaf.name
if (typeof leaf === 'string') leafName = leaf
return null
})}
{leaves.map((leaf, i) => {
let leafName: string
if (typeof leaf === 'object' && leaf?.name) leafName = leaf.name
if (typeof leaf === 'string') leafName = leaf
const leafType = enabledLeaves[leafName]
const Button = leafType?.Button
const leafType = enabledLeaves[leafName]
const Button = leafType?.Button
if (Button) {
return <Button fieldProps={props} key={i} path={path} />
}
if (Button) {
return <Button fieldProps={props} key={i} path={path} />
}
return null
})}
return null
})}
</div>
</div>
</div>
)}
<div className={`${baseClass}__editor`} ref={editorRef}>
<Editable
className={`${baseClass}__input`}

44
pnpm-lock.yaml generated
View File

@@ -22,8 +22,8 @@ importers:
specifier: workspace:*
version: link:packages/eslint-config-payload
'@playwright/test':
specifier: 1.37.1
version: 1.37.1
specifier: 1.38.1
version: 1.38.1
'@swc/cli':
specifier: ^0.1.62
version: 0.1.62(@swc/core@1.3.76)
@@ -429,20 +429,10 @@ importers:
version: 1.15.0(eslint@8.48.0)
packages/live-preview:
dependencies:
react:
specifier: 18.2.0
version: 18.2.0
devDependencies:
'@payloadcms/eslint-config':
specifier: workspace:*
version: link:../eslint-config-payload
'@types/node':
specifier: 20.5.7
version: 20.5.7
'@types/react':
specifier: 18.2.15
version: 18.2.15
payload:
specifier: workspace:*
version: link:../payload
@@ -453,17 +443,14 @@ importers:
specifier: workspace:*
version: link:../live-preview
react:
specifier: 18.2.0
specifier: ^16.8.0 || ^17.0.0 || ^18.0.0
version: 18.2.0
devDependencies:
'@payloadcms/eslint-config':
specifier: workspace:*
version: link:../eslint-config-payload
'@types/node':
specifier: 20.5.7
version: 20.5.7
'@types/react':
specifier: 18.2.15
specifier: ^18.2.0
version: 18.2.15
payload:
specifier: workspace:*
@@ -3485,15 +3472,12 @@ packages:
'@octokit/openapi-types': 18.0.0
dev: true
/@playwright/test@1.37.1:
resolution: {integrity: sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg==}
/@playwright/test@1.38.1:
resolution: {integrity: sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==}
engines: {node: '>=16'}
hasBin: true
dependencies:
'@types/node': 20.6.2
playwright-core: 1.37.1
optionalDependencies:
fsevents: 2.3.2
playwright: 1.38.1
dev: true
/@pnpm/config.env-replace@1.1.0:
@@ -12086,12 +12070,22 @@ packages:
find-up: 3.0.0
dev: false
/playwright-core@1.37.1:
resolution: {integrity: sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==}
/playwright-core@1.38.1:
resolution: {integrity: sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==}
engines: {node: '>=16'}
hasBin: true
dev: true
/playwright@1.38.1:
resolution: {integrity: sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright-core: 1.38.1
optionalDependencies:
fsevents: 2.3.2
dev: true
/pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}

View File

@@ -1,6 +1,6 @@
import type { AfterChangeHook } from 'payload/dist/collections/config/types'
import type { Order, User } from '../../../payload-types'
import type { Order } from '../../../payload-types'
export const clearUserCart: AfterChangeHook<Order> = async ({ doc, req, operation }) => {
const { payload } = req
@@ -18,7 +18,9 @@ export const clearUserCart: AfterChangeHook<Order> = async ({ doc, req, operatio
collection: 'users',
id: orderedBy,
data: {
cart: [] as User['cart'],
cart: {
items: [],
},
},
})
}

View File

@@ -281,6 +281,9 @@ export default buildConfigWithDefaults({
},
{
slug: globalSlug,
label: {
en: 'My Global Label',
},
admin: {
group: 'Group',
},
@@ -301,7 +304,6 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: 'custom-global-views-one',
versions: true,
@@ -354,6 +356,7 @@ export default buildConfigWithDefaults({
},
{
slug: 'group-globals-one',
label: 'Group Globals 1',
admin: {
group: 'Group',
},

View File

@@ -149,6 +149,70 @@ describe('admin', () => {
})
})
describe('doc titles', () => {
test('collection - should render fallback titles when creating new', async () => {
await page.goto(url.create)
await expect(page.locator('.doc-header__title.render-title')).toContainText('[Untitled]')
await expect(page.locator('.step-nav.app-header__step-nav')).toContainText('Create New')
await saveDocAndAssert(page)
})
test('collection - should render `useAsTitle` field', async () => {
await page.goto(url.create)
const titleField = page.locator('#field-title')
await titleField.fill(title)
await expect(page.locator('.doc-header__title.render-title')).toContainText(title)
await saveDocAndAssert(page)
await expect(page.locator('.step-nav.app-header__step-nav')).toContainText(title)
})
test('collection - should render `id` as `useAsTitle` fallback', async () => {
await page.goto(url.create)
await page.locator('#field-title').fill(title)
await expect(page.locator('.doc-header__title.render-title')).toContainText(title)
await saveDocAndAssert(page)
await expect(page.locator('.step-nav.app-header__step-nav')).toContainText(title)
await page.locator('#field-title').fill('')
await expect(page.locator('.doc-header__title.render-title')).toContainText('ID: ')
await expect(page.locator('.step-nav.app-header__step-nav')).toContainText('[Untitled]')
await saveDocAndAssert(page)
})
test('global - should render custom, localized label', async () => {
await openNav(page)
const label = 'My Global Label'
const globalLabel = page.locator(`#nav-global-global`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await expect(page.locator('.doc-header__title.render-title')).toContainText(label)
await expect(page.locator('.step-nav.app-header__step-nav')).toContainText(label)
})
test('global - should render simple label strings', async () => {
await openNav(page)
const label = 'Group Globals 1'
const globalLabel = page.locator(`#nav-global-group-globals-one`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await expect(page.locator('.doc-header__title.render-title')).toContainText(label)
const nav = page.locator('.step-nav.app-header__step-nav')
await expect(nav).toContainText(label)
await saveDocAndAssert(page)
})
test('global - should render slug in sentence case as fallback', async () => {
await openNav(page)
const label = 'Group Globals Two'
const globalLabel = page.locator(`#nav-global-group-globals-two`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await expect(page.locator('.doc-header__title.render-title')).toContainText(label)
const nav = page.locator('.step-nav.app-header__step-nav')
await expect(nav).toContainText(label)
await saveDocAndAssert(page)
})
})
describe('CRUD', () => {
test('should create', async () => {
await page.goto(url.create)

View File

@@ -30,6 +30,18 @@ export const UploadAndRichTextBlock: Block = {
slug: 'uploadAndRichText',
}
export const RelationshipBlock: Block = {
fields: [
{
name: 'rel',
type: 'relationship',
relationTo: 'uploads',
required: true,
},
],
slug: 'relationshipBlock',
}
export const SelectFieldBlock: Block = {
fields: [
{

View File

@@ -8,7 +8,7 @@ import {
lexicalEditor,
} from '../../../../packages/richtext-lexical/src'
import { slateEditor } from '../../../../packages/richtext-slate/src'
import { SelectFieldBlock, TextBlock, UploadAndRichTextBlock } from './blocks'
import { RelationshipBlock, SelectFieldBlock, TextBlock, UploadAndRichTextBlock } from './blocks'
import { generateLexicalRichText } from './generateLexicalRichText'
import { generateSlateRichText } from './generateSlateRichText'
@@ -63,7 +63,7 @@ const RichTextFields: CollectionConfig = {
},
}),
BlocksFeature({
blocks: [TextBlock, UploadAndRichTextBlock, SelectFieldBlock],
blocks: [TextBlock, UploadAndRichTextBlock, SelectFieldBlock, RelationshipBlock],
}),
],
}),

View File

@@ -26,6 +26,27 @@ const RowFields: CollectionConfig = {
},
],
},
{
type: 'row',
fields: [
{
name: 'field_with_width_a',
label: 'Field with 50% width',
type: 'text',
admin: {
width: '50%',
},
},
{
name: 'field_with_width_b',
label: 'Field with 50% width',
type: 'text',
admin: {
width: '50%',
},
},
],
},
],
}

View File

@@ -79,42 +79,43 @@ describe('fields', () => {
await expect(textCell).toHaveText(String(numberDoc.number))
})
test('should filter Number fields in the collection view - greaterThanOrEqual', async () => {
await page.goto(url.list);
await page.goto(url.list)
// should have 3 entries
await expect(page.locator('table >> tbody >> tr')).toHaveCount(3);
await expect(page.locator('table >> tbody >> tr')).toHaveCount(3)
// open the filter options
await page.locator('.list-controls__toggle-where').click();
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible();
await page.locator('.where-builder__add-first-filter').click();
await page.locator('.list-controls__toggle-where').click()
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await page.locator('.where-builder__add-first-filter').click()
const initialField = page.locator('.condition__field');
const operatorField = page.locator('.condition__operator');
const valueField = page.locator('.condition__value >> input');
const initialField = page.locator('.condition__field')
const operatorField = page.locator('.condition__operator')
const valueField = page.locator('.condition__value >> input')
// select Number field to filter on
await initialField.click();
const initialFieldOptions = initialField.locator('.rs__option');
await initialFieldOptions.locator('text=number').first().click();
expect(initialField.locator('.rs__single-value')).toContainText('Number');
await initialField.click()
const initialFieldOptions = initialField.locator('.rs__option')
await initialFieldOptions.locator('text=number').first().click()
await expect(initialField.locator('.rs__single-value')).toContainText('Number')
// select >= operator
await operatorField.click();
const operatorOptions = operatorField.locator('.rs__option');
await operatorOptions.last().click();
expect(operatorField.locator('.rs__single-value')).toContainText('is greater than or equal to');
await operatorField.click()
const operatorOptions = operatorField.locator('.rs__option')
await operatorOptions.last().click()
await expect(operatorField.locator('.rs__single-value')).toContainText(
'is greater than or equal to',
)
// enter value of 3
await valueField.fill('3');
await expect(valueField).toHaveValue('3');
await wait(300);
await valueField.fill('3')
await expect(valueField).toHaveValue('3')
await wait(300)
// should have 2 entries after filtering
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2);
});
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
})
test('should create', async () => {
const input = 5
@@ -676,6 +677,22 @@ describe('fields', () => {
await page.locator('.tabs-field__tab-button:has-text("Tab with Row")').click()
await expect(page.locator('#field-textInRow')).toHaveValue(textInRowValue)
})
test('should render array data within unnamed tabs', async () => {
await page.goto(url.list)
await page.locator('.cell-id a').click()
await page.locator('.tabs-field__tab-button:has-text("Tab with Array")').click()
await expect(page.locator('#field-array__0__text')).toHaveValue("Hello, I'm the first row")
})
test('should render array data within named tabs', async () => {
await page.goto(url.list)
await page.locator('.cell-id a').click()
await page.locator('.tabs-field__tab-button:nth-child(5)').click()
await expect(page.locator('#field-tab__array__0__text')).toHaveValue(
"Hello, I'm the first row, in a named tab",
)
})
})
describe('richText', () => {
@@ -1430,6 +1447,19 @@ describe('fields', () => {
await expect(idHeadings).toBeVisible()
await expect(idHeadings).toHaveCount(1)
})
test('should render row fields inline', async () => {
await page.goto(url.create)
const fieldA = page.locator('input#field-field_with_width_a')
await expect(fieldA).toBeVisible()
const fieldB = page.locator('input#field-field_with_width_b')
await expect(fieldB).toBeVisible()
const fieldABox = await fieldA.boundingBox()
const fieldBBox = await fieldB.boundingBox()
// give it some wiggle room of like 2px to account for differences in rendering
const difference = Math.abs(fieldABox.width - fieldBBox.width)
expect(difference).toBeLessThanOrEqual(2)
})
})
describe('conditional logic', () => {

View File

@@ -18,11 +18,11 @@ export const PageClient: React.FC<{
return (
<React.Fragment>
<Hero {...data.hero} />
<Hero {...data?.hero} />
<Blocks
blocks={data.layout}
blocks={data?.layout}
disableTopPadding={
!data.hero || data.hero?.type === 'none' || data.hero?.type === 'lowImpact'
!data?.hero || data?.hero?.type === 'none' || data?.hero?.type === 'lowImpact'
}
/>
</React.Fragment>

View File

@@ -11,11 +11,13 @@
"dependencies": {
"@types/escape-html": "^1.0.2",
"next": "^13.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.68.0",
"slate": "^0.94.1"
},
"devDependencies": {
"@types/node": "20.6.2",
"@types/react": "18.2.22"
"@types/node": "^20.6.2",
"@types/react": "^18.2.22"
}
}

View File

@@ -64,20 +64,22 @@
resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.2.tgz#072b7b13784fb3cee9c2450c22f36405983f5e3c"
integrity sha512-gaBLT8pdcexFztLSPRtriHeXY/Kn4907uOCZ4Q3lncFBkheAWOuNt53ypsF8szgxbEJ513UeBzcf4utN0EzEwA==
"@types/node@20.6.2":
version "20.6.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.2.tgz#a065925409f59657022e9063275cd0b9bd7e1b12"
integrity sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==
"@types/node@^20.6.2":
version "20.8.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.4.tgz#0e9ebb2ff29d5c3302fc84477d066fa7c6b441aa"
integrity sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==
dependencies:
undici-types "~5.25.1"
"@types/prop-types@*":
version "15.7.8"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3"
integrity sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==
"@types/react@18.2.22":
version "18.2.22"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.22.tgz#abe778a1c95a07fa70df40a52d7300a40b949ccb"
integrity sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==
"@types/react@^18.2.22":
version "18.2.27"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.27.tgz#746e52b06f3ccd5d7a724fd53769b70792601440"
integrity sha512-Wfv7B7FZiR2r3MIqbAlXoY1+tXm4bOqfz4oRr+nyXdBqapDBZ0l/IGcSlAfvxIHEEJjkPU0MYAc/BlFPOcrgLw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -116,9 +118,9 @@ busboy@1.6.0:
streamsearch "^1.1.0"
caniuse-lite@^1.0.30001406:
version "1.0.30001543"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz#478a3e9dddbb353c5ab214b0ecb0dbed529ed1d8"
integrity sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==
version "1.0.30001547"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz#d4f92efc488aab3c7f92c738d3977c2a3180472b"
integrity sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==
"chokidar@>=3.0.0 <4.0.0":
version "3.5.3"
@@ -213,6 +215,18 @@ is-plain-object@^5.0.0:
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
@@ -265,6 +279,21 @@ postcss@8.4.31:
picocolors "^1.0.0"
source-map-js "^1.0.2"
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -273,14 +302,21 @@ readdirp@~3.6.0:
picomatch "^2.2.1"
sass@^1.68.0:
version "1.68.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.68.0.tgz#0034b0cc9a50248b7d1702ac166fd25990023669"
integrity sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==
version "1.69.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.1.tgz#659b3b04452245dcf82f731684831e990ddb0c89"
integrity sha512-nc969GvTVz38oqKgYYVHM/Iq7Yl33IILy5uqaH2CWSiSUmRCvw+UR7tA3845Sp4BD5ykCUimvrT3k1EjTwpVUA==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
dependencies:
loose-envify "^1.1.0"
slate@^0.94.1:
version "0.94.1"
resolved "https://registry.yarnpkg.com/slate/-/slate-0.94.1.tgz#13b0ba7d0a7eeb0ec89a87598e9111cbbd685696"
@@ -324,6 +360,11 @@ tslib@^2.4.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
undici-types@~5.25.1:
version "5.25.3"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3"
integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==
watchpack@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"

View File

@@ -1,4 +1,5 @@
import glob from 'glob'
import minimist from 'minimist'
import path from 'path'
import shelljs from 'shelljs'
import slash from 'slash'
@@ -8,14 +9,39 @@ shelljs.env.DISABLE_LOGGING = 'true'
const playwrightBin = path.resolve(__dirname, '../node_modules/.bin/playwright')
const testRunCodes: { code: number; suiteName: string }[] = []
const args = process.argv.slice(2)
const { _: args, bail, part } = minimist(process.argv.slice(2))
const suiteName = args[0]
// Run all
if (!suiteName || args[0].startsWith('-')) {
const bail = args.includes('--bail')
const files = glob.sync(`${path.resolve(__dirname).replace(/\\/g, '/')}/**/*e2e.spec.ts`)
console.log(`\n\nExecuting all ${files.length} E2E tests...`)
if (!suiteName) {
let files = glob.sync(`${path.resolve(__dirname).replace(/\\/g, '/')}/**/*e2e.spec.ts`)
const totalFiles = files.length
if (part) {
if (!part.includes('/')) {
throw new Error('part must be in the format of "1/2"')
}
const [partToRun, totalParts] = part.split('/').map((n) => parseInt(n))
if (partToRun > totalParts) {
throw new Error('part cannot be greater than totalParts')
}
const partSize = Math.ceil(files.length / totalParts)
const start = (partToRun - 1) * partSize
const end = start + partSize
files = files.slice(start, end)
}
if (files.length !== totalFiles) {
console.log(`\n\nExecuting part ${part}: ${files.length} of ${totalFiles} E2E tests...\n\n`)
} else {
console.log(`\n\nExecuting all ${files.length} E2E tests...\n\n`)
}
console.log(`${files.join('\n')}\n`)
files.forEach((file) => {
clearWebpackCache()
executePlaywright(file, bail)