Compare commits

..

10 Commits

Author SHA1 Message Date
Elliot DeNolf
a099f55a69 chore(release): plugin-form-builder/1.1.0 [skip ci] 2023-12-19 14:46:49 -05:00
Elliot DeNolf
1f1445c798 chore(release): richtext-lexical/0.5.0 [skip ci] 2023-12-19 14:45:27 -05:00
Elliot DeNolf
741a5e3650 chore(release): payload/2.5.0 [skip ci] 2023-12-19 14:41:55 -05:00
Dan Ribbens
05e8914db7 fix(db-mongodb): documentDB unique constraint throws incorrect error (#4513) 2023-12-19 14:14:51 -05:00
Ritsu
ef43629502 fix(db-postgres) incorrect currentTableName in find for blocks (#4524) 2023-12-19 10:30:13 -05:00
Jarrod Flesch
7a4607897d chore: exports the useNav hook (#4557) 2023-12-19 09:44:09 -05:00
Sajarin M
1cad1a6954 docs: fix typo in admin hooks page (#4556) 2023-12-19 09:43:25 -05:00
Patrik
9e8f14a897 feat: adds new actions property to admin customization (#4468) 2023-12-19 09:31:58 -05:00
Patrik
f3748a1534 fix: updates return value of empty arrays in getDataByPath (#4553)
* fix: sets the return value to [] instead of 0 for arrays in getDataByPath

* chore: simplifies empty array check
2023-12-19 09:01:24 -05:00
Alessio Gravili
fee81bfbc4 fix(templates/ecommerce): updates @payloadcms/plugin-stripe to v0.0.19 (#4554) 2023-12-18 16:28:38 -05:00
60 changed files with 1060 additions and 130 deletions

View File

@@ -1,3 +1,71 @@
## [2.5.0](https://github.com/payloadcms/payload/compare/v2.4.0...v2.5.0) (2023-12-19)
### Features
* add Chinese Traditional translation ([#4372](https://github.com/payloadcms/payload/issues/4372)) ([50253f6](https://github.com/payloadcms/payload/commit/50253f617c22d0d185bbac7f9d4304cddbc01f06))
* add context to auth and globals local API ([#4449](https://github.com/payloadcms/payload/issues/4449)) ([168d629](https://github.com/payloadcms/payload/commit/168d6296974042c3ff2a113f9f6c2bded7ba2b3e))
* adds new `actions` property to admin customization ([#4468](https://github.com/payloadcms/payload/issues/4468)) ([9e8f14a](https://github.com/payloadcms/payload/commit/9e8f14a897e77f6933eedb2410956a468f4187c3))
* async live preview urls ([#4339](https://github.com/payloadcms/payload/issues/4339)) ([5f17324](https://github.com/payloadcms/payload/commit/5f173241df6dc316d498767b1c81718e9c2b9a51))
* pass path to FieldDescription ([#4364](https://github.com/payloadcms/payload/issues/4364)) ([3b8a27d](https://github.com/payloadcms/payload/commit/3b8a27d199b3969cbca6ca750450798cb70f21e8))
* **plugin-form-builder:** Lexical support ([#4487](https://github.com/payloadcms/payload/issues/4487)) ([c6c5cab](https://github.com/payloadcms/payload/commit/c6c5cabfbb7eb954eea51170a6af7582b1f9b84b))
* prevent querying relationship when filterOptions returns false ([#4392](https://github.com/payloadcms/payload/issues/4392)) ([c1bd338](https://github.com/payloadcms/payload/commit/c1bd338d0d5e899f3892f1d18e355c00b265447a))
* **richtext-lexical:** improve floating select menu Dropdown classNames ([#4444](https://github.com/payloadcms/payload/issues/4444)) ([9331204](https://github.com/payloadcms/payload/commit/9331204295bfeaf7dd10bc075f42995b2cab2de4))
* **richtext-lexical:** improve link URL validation ([#4442](https://github.com/payloadcms/payload/issues/4442)) ([9babf68](https://github.com/payloadcms/payload/commit/9babf6804ce04d5828167eb8e7717727fe1cd472))
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server ([#4290](https://github.com/payloadcms/payload/issues/4290)) ([5de347f](https://github.com/payloadcms/payload/commit/5de347ffffca3bf38315d3d87d2ccc5c28cd2723))
* **richtext-lexical:** Link & Relationship Feature: field-level configurable allowed relationships ([#4182](https://github.com/payloadcms/payload/issues/4182)) ([7af8f29](https://github.com/payloadcms/payload/commit/7af8f29b4a8dddf389356e4db142f8d434cdc964))
* **richtext-lexical:** link node: change doc data format to be consistent with relationship field ([#4504](https://github.com/payloadcms/payload/issues/4504)) ([cc0ba89](https://github.com/payloadcms/payload/commit/cc0ba895188f40181c6ba3779f66d547d4ea66f9))
* **richtext-lexical:** rename TreeviewFeature into TreeViewFeature ([#4520](https://github.com/payloadcms/payload/issues/4520)) ([c49fd66](https://github.com/payloadcms/payload/commit/c49fd6692231b68ca61b079103a0fd7aa4673be1))
* **richtext-lexical:** Slate to Lexical converter: add blockquote conversion, convert custom link fields ([#4486](https://github.com/payloadcms/payload/issues/4486)) ([31f8f3c](https://github.com/payloadcms/payload/commit/31f8f3cac6bfd08f3adfa0a026a57c4b1b510045))
* **richtext-lexical:** Upload html serializer: Output picture element if the image has multiple sizes, improve absolute URL creation ([e558894](https://github.com/payloadcms/payload/commit/e55889480fceb8995646621923159d92de6e89c9))
### Bug Fixes
* adds bg color for year/month select options in datepicker ([#4508](https://github.com/payloadcms/payload/issues/4508)) ([07371b9](https://github.com/payloadcms/payload/commit/07371b9cad111999f2df4e1f709d6b95cd511c15))
* correctly fetches externally stored files when passing uploadEdits ([#4505](https://github.com/payloadcms/payload/issues/4505)) ([228d45c](https://github.com/payloadcms/payload/commit/228d45cf52e592cea6377cd93648fba75d73c88d))
* cursor jumping around inside json field ([#4453](https://github.com/payloadcms/payload/issues/4453)) ([6300037](https://github.com/payloadcms/payload/commit/63000373e66fb39443f882689e0ecf5c11ed8ad0))
* **db-mongodb:** documentDB unique constraint throws incorrect error ([#4513](https://github.com/payloadcms/payload/issues/4513)) ([05e8914](https://github.com/payloadcms/payload/commit/05e8914db70fa64bfb2d15ecfb58e9c229d71108))
* **db-postgres:** findOne correctly querying with where queries ([#4550](https://github.com/payloadcms/payload/issues/4550)) ([8bc31cd](https://github.com/payloadcms/payload/commit/8bc31cd5923517ab39ae1427aa0d0fb19d876dab))
* **db-postgres:** querying nested blocks fields ([#4404](https://github.com/payloadcms/payload/issues/4404)) ([6e9ae65](https://github.com/payloadcms/payload/commit/6e9ae65374124ee000cc2988ef77247c94b0dd18))
* **db-postgres:** sorting on a not-configured field throws error ([#4382](https://github.com/payloadcms/payload/issues/4382)) ([dbaecda](https://github.com/payloadcms/payload/commit/dbaecda0e92fcb0fa67b4c5ac085e025f02de53a))
* defaultValues computed on new globals ([#4380](https://github.com/payloadcms/payload/issues/4380)) ([b6cffce](https://github.com/payloadcms/payload/commit/b6cffcea07b9fa21698b00b8bbed6f27197ded41))
* disallow duplicate fieldNames to be used on the same level in the config ([#4381](https://github.com/payloadcms/payload/issues/4381)) ([a1d66b8](https://github.com/payloadcms/payload/commit/a1d66b83e0dbea21e8da549b73cd25c537a57938))
* ensure ui fields do not make it into gql schemas ([#4457](https://github.com/payloadcms/payload/issues/4457)) ([3a20ddc](https://github.com/payloadcms/payload/commit/3a20ddc5f85162a316006f22ba66ee1c7aab99e3))
* format fields within tab for list controls ([#4516](https://github.com/payloadcms/payload/issues/4516)) ([2650c70](https://github.com/payloadcms/payload/commit/2650c70960a7374307a8862c3940c97d337d1d30))
* formats locales with multiple labels for versions locale selector ([#4495](https://github.com/payloadcms/payload/issues/4495)) ([8257661](https://github.com/payloadcms/payload/commit/8257661c47b5b968a57fb2228d7045d876a3f484))
* graphql schema generation for fields without queryable subfields ([#4463](https://github.com/payloadcms/payload/issues/4463)) ([13e3e06](https://github.com/payloadcms/payload/commit/13e3e0671353ca34e603fece57a12199f2082ca0))
* handles null upload field values ([#4397](https://github.com/payloadcms/payload/issues/4397)) ([cf9a370](https://github.com/payloadcms/payload/commit/cf9a3704df21ce8b32feb0680793cba804cd66f7))
* **live-preview:** populates rte uploads and relationships ([#4379](https://github.com/payloadcms/payload/issues/4379)) ([4090aeb](https://github.com/payloadcms/payload/commit/4090aebb0e94e776258f0c1c761044a4744a1857))
* **live-preview:** sends raw js objects through window.postMessage instead of json ([#4354](https://github.com/payloadcms/payload/issues/4354)) ([03a3872](https://github.com/payloadcms/payload/commit/03a387233d1b8876a2fcaa5f3b3fd5ed512c0bc4))
* make admin navigation transition smoother ([#4217](https://github.com/payloadcms/payload/issues/4217)) ([eb6572e](https://github.com/payloadcms/payload/commit/eb6572e9e56e680cad331c1bc5da47e91306deb9))
* omit field default value if read access returns false ([#4518](https://github.com/payloadcms/payload/issues/4518)) ([3e9ef84](https://github.com/payloadcms/payload/commit/3e9ef849cd8e69e1e8d7f2f653f0647e93c8ab39))
* pin ts-node versions which are causing swc errors ([#4447](https://github.com/payloadcms/payload/issues/4447)) ([b9c0248](https://github.com/payloadcms/payload/commit/b9c024882350d14edd57f0f662a2269ed37975e3))
* properly spreads collection fields into non-tabbed configs [#50](https://github.com/payloadcms/payload/issues/50) ([#51](https://github.com/payloadcms/payload/issues/51)) ([7e88159](https://github.com/payloadcms/payload/commit/7e88159e99e2afdc10addc02cf299c11fe188be7))
* **plugin-form-builder:** removes use of slate in rich-text serializer ([#4451](https://github.com/payloadcms/payload/issues/4451)) ([3df52a8](https://github.com/payloadcms/payload/commit/3df52a88568622f8fafeabad47c7501229e4ea5f))
* **plugin-nested-docs:** properly exports field utilities ([#4462](https://github.com/payloadcms/payload/issues/4462)) ([1cc87bd](https://github.com/payloadcms/payload/commit/1cc87bd8ea575dfa2e1f5ce5b38414bbba95b2cb))
* **richtext-*:** loosen RichTextAdapter types due to re-occuring ts strict mode errors ([#4416](https://github.com/payloadcms/payload/issues/4416)) ([48f1299](https://github.com/payloadcms/payload/commit/48f1299fcba3e3811c6a7f31499f238537f9a5e3))
* **richtext-lexical:** Blocks field: should not prompt for unsaved changes due to value comparison between null and non-existent props ([#4450](https://github.com/payloadcms/payload/issues/4450)) ([548e78c](https://github.com/payloadcms/payload/commit/548e78c598cb6d029e7cc40f80d9855754f043bc))
* **richtext-lexical:** do not add unnecessary paragraph before upload, relationship and blocks nodes ([#4441](https://github.com/payloadcms/payload/issues/4441)) ([5c2739e](https://github.com/payloadcms/payload/commit/5c2739ebd144620cfd4ff02531f5812dd62ac61d))
* **richtext-lexical:** lexicalHTML field not working when used inside of Blocks field ([128f9c4](https://github.com/payloadcms/payload/commit/128f9c4e7e6e20dd1ee221f49428a5bce5179c5f))
* **richtext-lexical:** lexicalHTML field now works when used inside of row fields ([#4440](https://github.com/payloadcms/payload/issues/4440)) ([0421173](https://github.com/payloadcms/payload/commit/0421173f9e2d6db1b6a94b25884ea807921f2d09))
* **richtext-lexical:** not all types of URLs are validated correctly ([ac7f980](https://github.com/payloadcms/payload/commit/ac7f9809bc2b9fb6a52b48c10f7d51414801e4de))
* searching by id sends undefined in where query param ([#4464](https://github.com/payloadcms/payload/issues/4464)) ([46e8c01](https://github.com/payloadcms/payload/commit/46e8c01fbed68856be68804f2bd9744c4c6f5a95))
* simplifies query validation and fixes nested relationship fields ([#4391](https://github.com/payloadcms/payload/issues/4391)) ([4b5453e](https://github.com/payloadcms/payload/commit/4b5453e8e5484f7afcadbf5bccf8369b552969c6))
* updates return value of empty arrays in getDataByPath ([#4553](https://github.com/payloadcms/payload/issues/4553)) ([f3748a1](https://github.com/payloadcms/payload/commit/f3748a1534a13e6d844aadd9f0e3e6acbe483d03))
* upload editing error with plugin-cloud ([#4170](https://github.com/payloadcms/payload/issues/4170)) ([fcbe574](https://github.com/payloadcms/payload/commit/fcbe5744d945dc43642cdaa2007ddc252ecafafa))
* upload related issues, cropping, fetching local file, external preview image ([#4461](https://github.com/payloadcms/payload/issues/4461)) ([45c472d](https://github.com/payloadcms/payload/commit/45c472d6b35c41e597038089ad1755cdb88193b6))
* uploads files after validation ([#4218](https://github.com/payloadcms/payload/issues/4218)) ([65adfd2](https://github.com/payloadcms/payload/commit/65adfd21ed538b79628dc4f8ce9e1a5a1bba6aed))
### ⚠ BREAKING CHANGES
#### @payloadcms/richtext-lexical
* **richtext-lexical:** rename TreeviewFeature into TreeViewFeature (#4520)
* **richtext-lexical:** link node: change doc data format to be consistent with relationship field (#4504)
* **richtext-lexical:** improve floating select menu Dropdown classNames (#4444)
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server (#4290)
## @payloadcms/richtext-*
### [@payloadcms/richtext-lexical 0.4.1](https://github.com/payloadcms/payload/compare/richtext-lexical/0.4.0...richtext-lexical/0.4.1) (2023-12-07)

View File

@@ -27,14 +27,15 @@ You can override a set of admin panel-wide components by providing a component t
| **`BeforeNavLinks`** | Array of components to inject into the built-in Nav, _before_ the links themselves. |
| **`AfterNavLinks`** | Array of components to inject into the built-in Nav, _after_ the links. |
| **`BeforeDashboard`** | Array of components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
| **`AfterDashboard`** | Array of components to inject into the built-in Dashboard, _after_ the default dashboard contents. [Demo](https://github.com/payloadcms/payload/tree/main/test/admin/components/AfterDashboard/index.tsx) |
| **`AfterDashboard`** | Array of components to inject into the built-in Dashboard, _after_ the default dashboard contents. [Demo](https://github.com/payloadcms/payload/tree/main/test/admin/components/AfterDashboard/index.tsx) |
| **`BeforeLogin`** | Array of components to inject into the built-in Login, _before_ the default login form. |
| **`AfterLogin`** | Array of components to inject into the built-in Login, _after_ the default login form. |
| **`logout.Button`** | A custom React component. |
| **`graphics.Icon`** | Used as a graphic within the `Nav` component. Often represents a condensed version of a full logo. |
| **`graphics.Logo`** | The full logo to be used in contexts like the `Login` view. |
| **`providers`** | Define your own provider components that will wrap the Payload Admin UI. [More](#custom-providers) |
| **`views`** | Override or create new views within the Payload Admin UI. [More](#views) |
| **`actions`** | Array of custom components to be rendered in the Payload Admin UI header, providing additional interactivity and functionality. |
| **`views`** | Override or create new views within the Payload Admin UI. [More](#views) |
Here is a full example showing how to swap some of these components for your own.
@@ -50,6 +51,7 @@ import {
MyCustomAccount,
MyCustomDashboard,
MyProvider,
MyCustomAdminAction,
} from './customComponents'
export default buildConfig({
@@ -60,6 +62,7 @@ export default buildConfig({
Icon: MyCustomIcon,
Logo: MyCustomLogo,
},
actions: [MyCustomAdminAction],
views: {
Account: MyCustomAccount,
Dashboard: MyCustomDashboard,
@@ -243,7 +246,11 @@ To swap out any of these views, simply pass in your custom component to the `adm
_For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component)._
To swap specific _nested_ views within the parent `Edit` view, you can use the `admin.components.views.Edit` property on the globals's config. This will only replace the nested view, leaving the page breadcrumbs, title, tabs, etc intact.
**Customizing Nested Views within 'Edit' in Collections**
The `Edit` view in collections consists of several nested views, each serving a unique purpose. You can customize these nested views using the `admin.components.views.Edit` property in the collection's configuration. This approach allows you to replace specific nested views while keeping the overall structure of the `Edit` view intact, including the page breadcrumbs, title, tabs, etc.
Here's an example of how you can customize nested views within the `Edit` view in collections, including the use of the `actions` property:
```ts
// Collection.ts
@@ -253,7 +260,29 @@ To swap specific _nested_ views within the parent `Edit` view, you can use the `
components: {
views: {
Edit: {
Default: MyCustomDefaultTab,
Default: {
Component: MyCustomDefaultTab,
actions: [CollectionEditButton], // Custom actions for the default edit view
},
API: {
Component: MyCustomAPIView,
actions: [CollectionAPIButton], // Custom actions for API view
},
LivePreview: {
Component: MyCustomLivePreviewView,
actions: [CollectionLivePreviewButton], // Custom actions for Live Preview
},
Version: {
Component: MyCustomVersionView,
actions: [CollectionVersionButton], // Custom actions for Version view
},
Versions: {
Component: MyCustomVersionsView,
actions: [CollectionVersionsButton], // Custom actions for Versions view
},
},
List: {
actions: [CollectionListButton],
},
},
},
@@ -261,6 +290,8 @@ To swap specific _nested_ views within the parent `Edit` view, you can use the `
}
```
**Adding New Tabs to 'Edit' View**
You can also add _new_ tabs to the `Edit` view by adding another key to the `components.views.Edit[key]` object with a `path` and `Component` property. See [Custom Tabs](#custom-tabs) for more information.
### Globals
@@ -301,7 +332,11 @@ To swap out any of these views, simply pass in your custom component to the `adm
_For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component)._
To swap specific _nested_ views within the parent `Edit` view, you can use the `admin.components.views.Edit` property on the globals's config. This will only replace the nested view, leaving the page breadcrumbs, title, and tabs intact.
**Customizing Nested Views within 'Edit' in Globals**
Similar to collections, Globals allow for detailed customization within the `Edit` view. This includes the ability to swap specific nested views while maintaining the overall structure of the `Edit` view. You can use the `admin.components.views.Edit` property in the Globals configuration to achieve this, and this will only replace the nested view, leaving the page breadcrumbs, title, and tabs intact.
Here's how you can customize nested views within the `Edit` view in Globals, including the use of the `actions` property:
```ts
// Global.ts
@@ -311,7 +346,26 @@ To swap specific _nested_ views within the parent `Edit` view, you can use the `
components: {
views: {
Edit: {
Default: MyCustomDefaultTab,
Default: {
Component: MyCustomGlobalDefaultTab,
actions: [GlobalEditButton], // Custom actions for the default edit view
},
API: {
Component: MyCustomGlobalAPIView,
actions: [GlobalAPIButton], // Custom actions for API view
},
LivePreview: {
Component: MyCustomGlobalLivePreviewView,
actions: [GlobalLivePreviewButton], // Custom actions for Live Preview
},
Version: {
Component: MyCustomGlobalVersionView,
actions: [GlobalVersionButton], // Custom actions for Version view
},
Versions: {
Component: MyCustomGlobalVersionsView,
actions: [GlobalVersionsButton], // Custom actions for Versions view
},
},
},
},

View File

@@ -324,7 +324,7 @@ The `useForm` hook returns an object with the following properties: |
},
{
drawerTitle: 'addFieldRow',
drawerDescription: 'A useful method to programtically add a row to an array or block field.',
drawerDescription: 'A useful method to programmatically add a row to an array or block field.',
drawerSlug: 'addFieldRow',
drawerContent: (
<>
@@ -434,7 +434,7 @@ export const CustomArrayManager = () => {
},
{
drawerTitle: 'removeFieldRow',
drawerDescription: 'A useful method to programtically remove a row from an array or block field.',
drawerDescription: 'A useful method to programmatically remove a row from an array or block field.',
drawerSlug: 'removeFieldRow',
drawerContent: (
<>
@@ -531,7 +531,7 @@ export const CustomArrayManager = () => {
},
{
drawerTitle: 'replaceFieldRow',
drawerDescription: 'A useful method to programtically replace a row from an array or block field.',
drawerDescription: 'A useful method to programmatically replace a row from an array or block field.',
drawerSlug: 'replaceFieldRow',
drawerContent: (
<>

View File

@@ -27,6 +27,7 @@
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"http-status": "1.6.2",
"uuid": "9.0.0"
},
"devDependencies": {

View File

@@ -3,9 +3,8 @@ import type { Document, PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from '.'
import handleError from './utilities/handleError'
import { withSession } from './withSession'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
export const create: Create = async function create(
this: MongooseAdapter,
@@ -17,18 +16,7 @@ export const create: Create = async function create(
try {
;[doc] = await Model.create([data], options)
} catch (error) {
// Handle uniqueness error from MongoDB
throw error.code === 11000 && error.keyValue
? new ValidationError(
[
{
field: Object.keys(error.keyValue)[0],
message: req.t('error:valueMustBeUnique'),
},
],
req?.t ?? i18nInit(this.payload.config.i18n).t,
)
: error
handleError(error, req)
}
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here

View File

@@ -1,11 +1,9 @@
import type { UpdateOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
import type { MongooseAdapter } from '.'
import handleError from './utilities/handleError'
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
import { withSession } from './withSession'
@@ -31,18 +29,7 @@ export const updateOne: UpdateOne = async function updateOne(
try {
result = await Model.findOneAndUpdate(query, data, options)
} catch (error) {
// Handle uniqueness error from MongoDB
throw error.code === 11000 && error.keyValue
? new ValidationError(
[
{
field: Object.keys(error.keyValue)[0],
message: 'Value must be unique',
},
],
req?.t ?? i18nInit(this.payload.config.i18n).t,
)
: error
handleError(error, req)
}
result = JSON.parse(JSON.stringify(result))

View File

@@ -0,0 +1,23 @@
import httpStatus from 'http-status'
import { APIError, ValidationError } from 'payload/errors'
const handleError = (error, req) => {
// Handle uniqueness error from MongoDB
if (error.code === 11000 && error.keyValue) {
throw new ValidationError(
[
{
field: Object.keys(error.keyValue)[0],
message: req.t('error:valueMustBeUnique'),
},
],
req.t,
)
} else if (error.code === 11000) {
throw new APIError(req.t('error:valueMustBeUnique'), httpStatus.BAD_REQUEST)
} else {
throw error
}
}
export default handleError

View File

@@ -128,15 +128,16 @@ export const traverseFields = ({
with: {},
}
if (adapter.tables[`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}_locales`])
withBlock.with._locales = _locales
const tableName = `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`
if (adapter.tables[`${tableName}_locales`]) withBlock.with._locales = _locales
topLevelArgs.with[blockKey] = withBlock
traverseFields({
_locales,
adapter,
currentArgs: withBlock,
currentTableName,
currentTableName: tableName,
depth,
fields: block.fields,
path: '',

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.4.0",
"version": "2.5.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",

View File

@@ -98,11 +98,57 @@
}
}
&__controls {
&__actions-wrapper {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: flex-end;
gap: calc(var(--base) / 2);
margin-right: var(--base);
}
&__gradient-placeholder {
position: absolute;
top: 0;
right: 0;
width: var(--base);
height: var(--base);
background: linear-gradient(to right, transparent, var(--theme-bg));
}
&__actions {
display: flex;
align-items: center;
gap: calc(var(--base) / 2);
flex-shrink: 0;
max-width: 600px;
overflow: auto;
white-space: nowrap;
&::-webkit-scrollbar {
display: none;
}
}
&__last-action {
margin-right: var(--base);
}
@include large-break {
&__actions {
max-width: 500px;
}
}
@include mid-break {
&__gradient-placeholder {
right: var(--base);
}
&__actions {
max-width: 300px;
margin-right: var(--base);
}
}
@include small-break {
@@ -129,5 +175,14 @@
// TODO: overflow the step header instead of hide it
display: none;
}
&__gradient-placeholder {
right: 0;
}
&__actions {
max-width: 150px;
margin-right: 0;
}
}
}

View File

@@ -1,8 +1,9 @@
import React from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import Account from '../../graphics/Account'
import { useActions } from '../../utilities/ActionsProvider'
import { useConfig } from '../../utilities/Config'
import { Hamburger } from '../Hamburger'
import Localizer from '../Localizer'
@@ -22,8 +23,30 @@ export const AppHeader: React.FC = () => {
routes: { admin: adminRoute },
} = useConfig()
const { actions } = useActions()
const { navOpen } = useNav()
const customControlsRef = useRef<HTMLDivElement>(null)
const [isScrollable, setIsScrollable] = useState(false)
useEffect(() => {
const checkIsScrollable = () => {
const el = customControlsRef.current
if (el) {
const scrollable = el.scrollWidth > el.clientWidth
setIsScrollable(scrollable)
}
}
checkIsScrollable()
window.addEventListener('resize', checkIsScrollable)
return () => {
window.removeEventListener('resize', checkIsScrollable)
}
}, [actions])
return (
<header className={[baseClass, navOpen && `${baseClass}--nav-open`].filter(Boolean).join(' ')}>
<div className={`${baseClass}__bg`} />
@@ -36,22 +59,33 @@ export const AppHeader: React.FC = () => {
<div className={`${baseClass}__step-nav-wrapper`}>
<StepNav className={`${baseClass}__step-nav`} />
</div>
<div className={`${baseClass}__controls`}>
{localization && (
<LocalizerLabel
ariaLabel="invisible"
className={`${baseClass}__localizer-spacing`}
/>
)}
<Link
aria-label={t('authentication:account')}
className={`${baseClass}__account`}
tabIndex={0}
to={`${adminRoute}/account`}
>
<Account />
</Link>
<div className={`${baseClass}__actions-wrapper`}>
<div className={`${baseClass}__actions`} ref={customControlsRef}>
{Array.isArray(actions) &&
actions.map((Component, i) => (
<div
className={
isScrollable && i === actions.length - 1 ? `${baseClass}__last-action` : ''
}
key={i}
>
<Component />
</div>
))}
</div>
{isScrollable && <div className={`${baseClass}__gradient-placeholder`} />}
</div>
{localization && (
<LocalizerLabel ariaLabel="invisible" className={`${baseClass}__localizer-spacing`} />
)}
<Link
aria-label={t('authentication:account')}
className={`${baseClass}__account`}
tabIndex={0}
to={`${adminRoute}/account`}
>
<Account />
</Link>
</div>
</div>
</div>

View File

@@ -215,6 +215,15 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
return null
}
const listComponent = selectedCollectionConfig?.admin?.components?.views?.List
let ListToRender = null
if (listComponent && typeof listComponent === 'function') {
ListToRender = listComponent
} else if (typeof listComponent === 'object' && typeof listComponent.Component === 'function') {
ListToRender = listComponent.Component
}
return (
<TableColumnsProvider
cellProps={[
@@ -235,7 +244,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
>
<DocumentInfoProvider collection={selectedCollectionConfig}>
<RenderCustomComponent
CustomComponent={selectedCollectionConfig?.admin?.components?.views?.List}
CustomComponent={ListToRender}
DefaultComponent={DefaultList}
componentProps={{
collection: {

View File

@@ -10,6 +10,10 @@ const getDataByPath = <T = unknown>(fields: Fields, path: string): T => {
Object.keys(fields).forEach((key) => {
if (!fields[key].disableFormData && (key.indexOf(`${path}.`) === 0 || key === path)) {
data[key.replace(pathPrefixToRemove, '')] = fields[key].value
if (fields[key]?.rows && fields[key].rows.length === 0) {
data[key.replace(pathPrefixToRemove, '')] = []
}
}
})

View File

@@ -1,6 +1,7 @@
import React, { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../exports/types'
import type { Props } from './types'
import { Hamburger } from '../../elements/Hamburger'
@@ -15,7 +16,9 @@ import './index.scss'
const baseClass = 'template-default'
const Default: React.FC<Props> = ({ children, className }) => {
const Default: React.FC<
Props & { collection?: SanitizedCollectionConfig; global?: SanitizedGlobalConfig }
> = ({ children, className }) => {
const {
admin: {
components: { Nav: CustomNav } = {

View File

@@ -0,0 +1,38 @@
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useConfig } from '../../utilities/Config'
type ActionsContextType = {
actions: React.ComponentType<any>[]
setViewActions: (actions: React.ComponentType<any>[]) => void
}
const ActionsContext = createContext<ActionsContextType>({
actions: [],
setViewActions: () => {},
})
export const useActions = () => useContext(ActionsContext)
export const ActionsProvider = ({ children }) => {
const [viewActions, setViewActions] = useState([])
const [adminActions, setAdminActions] = useState([])
const {
admin: {
components: { actions: configAdminActions },
},
} = useConfig()
useEffect(() => {
setAdminActions(configAdminActions || [])
}, [configAdminActions])
const combinedActions = [...viewActions, ...adminActions]
return (
<ActionsContext.Provider value={{ actions: combinedActions, setViewActions }}>
{children}
</ActionsContext.Provider>
)
}

View File

@@ -10,6 +10,7 @@ import { Gutter } from '../../elements/Gutter'
import { CheckboxInput } from '../../forms/field-types/Checkbox/Input'
import SelectInput from '../../forms/field-types/Select/Input'
import { MinimizeMaximize } from '../../icons/MinimizeMaximize'
import { useActions } from '../../utilities/ActionsProvider'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale'
@@ -182,6 +183,8 @@ export const API: React.FC<EditViewProps> = (props) => {
const { code } = useLocale()
const url = createURL(apiURL)
const { setViewActions } = useActions()
const draftsEnabled = collection?.versions?.drafts || global?.versions?.drafts
const docEndpoint = global ? `/globals/${global.slug}` : `/${collection.slug}/${id}`
@@ -210,6 +213,14 @@ export const API: React.FC<EditViewProps> = (props) => {
fetchData()
}, [i18n.language, fetchURL, authenticated])
React.useEffect(() => {
const editConfig = (collection || global)?.admin?.components?.views?.Edit
const apiActions =
editConfig && 'API' in editConfig && 'actions' in editConfig.API ? editConfig.API.actions : []
setViewActions(apiActions)
}, [collection, global, setViewActions])
const localeOptions =
localization &&
localization.locales.map((locale) => ({ label: locale.label, value: locale.code }))

View File

@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react'
import type { SanitizedGlobalConfig } from '../../../../exports/types'
import { useStepNav } from '../../elements/StepNav'
import { useActions } from '../../utilities/ActionsProvider'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import RenderCustomComponent from '../../utilities/RenderCustomComponent'
@@ -19,6 +20,8 @@ const Dashboard: React.FC = () => {
globals,
} = useConfig()
const { setViewActions } = useActions()
useEffect(() => {
setFilteredGlobals(
globals.filter((global) => permissions?.globals?.[global.slug]?.read?.permission),
@@ -29,6 +32,10 @@ const Dashboard: React.FC = () => {
setStepNav([])
}, [setStepNav])
useEffect(() => {
setViewActions([])
}, [setViewActions])
return (
<RenderCustomComponent
CustomComponent={

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import type { FieldTypes } from '../../forms/field-types'
@@ -8,6 +8,7 @@ import { getTranslation } from '../../../../utilities/getTranslation'
import { DocumentHeader } from '../../elements/DocumentHeader'
import { FormLoadingOverlayToggle } from '../../elements/Loading'
import Form from '../../forms/Form'
import { useActions } from '../../utilities/ActionsProvider'
import { OperationContext } from '../../utilities/OperationProvider'
import { SetStepNav } from '../collections/Edit/SetStepNav'
import { GlobalRoutes } from './Routes'
@@ -37,10 +38,28 @@ const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
permissions,
} = props
const { setViewActions } = useActions()
const { label } = global
const hasSavePermission = permissions?.update?.permission
useEffect(() => {
const path = location.pathname
if (!path.endsWith(global.slug)) {
return
}
const editConfig = global?.admin?.components?.views?.Edit
const defaultActions =
editConfig && 'Default' in editConfig && 'actions' in editConfig.Default
? editConfig.Default.actions
: []
setViewActions(defaultActions)
}, [global.slug, location.pathname, global?.admin?.components?.views?.Edit, setViewActions])
return (
<main className={baseClass}>
<OperationContext.Provider value="update">

View File

@@ -12,6 +12,7 @@ import { getTranslation } from '../../../../utilities/getTranslation'
import { DocumentControls } from '../../elements/DocumentControls'
import { DocumentFields } from '../../elements/DocumentFields'
import { LeaveWithoutSaving } from '../../modals/LeaveWithoutSaving'
import { useActions } from '../../utilities/ActionsProvider'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale'
@@ -142,6 +143,11 @@ export const LivePreviewView: React.FC<
const documentInfo = useDocumentInfo()
const locale = useLocale()
const { setViewActions } = useActions()
const collection = documentInfo.collection
const global = documentInfo.global
let livePreviewConfig: LivePreviewConfig = config?.admin?.livePreview
if ('collection' in props) {
@@ -179,6 +185,16 @@ export const LivePreviewView: React.FC<
getURL() // eslint-disable-line @typescript-eslint/no-floating-promises
}, [data, documentInfo, locale, livePreviewConfig])
useEffect(() => {
const editConfig = (collection || global)?.admin?.components?.views?.Edit
const livePreviewActions =
editConfig && 'LivePreview' in editConfig && 'actions' in editConfig.LivePreview
? editConfig.LivePreview.actions
: []
setViewActions(livePreviewActions)
}, [collection, global, setViewActions])
const breakpoints: LivePreviewConfig['breakpoints'] = [
...(livePreviewConfig?.breakpoints || []),
{

View File

@@ -6,6 +6,7 @@ import { requests } from '../../../api'
import { LoadingOverlayToggle } from '../../elements/Loading'
import StayLoggedIn from '../../modals/StayLoggedIn'
import DefaultTemplate from '../../templates/Default'
import { ActionsProvider } from '../../utilities/ActionsProvider'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import { DocumentInfoProvider } from '../../utilities/DocumentInfo'
@@ -146,37 +147,39 @@ export const Routes: React.FC = () => {
{user ? (
<Fragment>
{canAccessAdmin && (
<DefaultTemplate>
<Switch>
<Route exact path={`${match.url}/`}>
<Dashboard />
</Route>
<Route path={`${match.url}/account`}>
<DocumentInfoProvider
collection={collections.find(({ slug }) => slug === userSlug)}
id={user.id}
>
<Account />
</DocumentInfoProvider>
</Route>
{collectionRoutes({
collections,
match,
permissions,
user,
})}
{globalRoutes({
globals,
locale,
match,
permissions,
user,
})}
<Route path={`${match.url}*`}>
<NotFound />
</Route>
</Switch>
</DefaultTemplate>
<ActionsProvider>
<DefaultTemplate>
<Switch>
<Route exact path={`${match.url}/`}>
<Dashboard />
</Route>
<Route path={`${match.url}/account`}>
<DocumentInfoProvider
collection={collections.find(({ slug }) => slug === userSlug)}
id={user.id}
>
<Account />
</DocumentInfoProvider>
</Route>
{collectionRoutes({
collections,
match,
permissions,
user,
})}
{globalRoutes({
globals,
locale,
match,
permissions,
user,
})}
<Route path={`${match.url}*`}>
<NotFound />
</Route>
</Switch>
</DefaultTemplate>
</ActionsProvider>
)}
{canAccessAdmin === false && <Unauthorized />}
</Fragment>

View File

@@ -14,6 +14,7 @@ import usePayloadAPI from '../../../hooks/usePayloadAPI'
import { formatDate } from '../../../utilities/formatDate'
import { Gutter } from '../../elements/Gutter'
import { useStepNav } from '../../elements/StepNav'
import { useActions } from '../../utilities/ActionsProvider'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
@@ -39,6 +40,8 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
} = useConfig()
const { setStepNav } = useStepNav()
const { setViewActions } = useActions()
const {
params: { id, versionID },
} = useRouteMatch<{ id?: string; versionID: string }>()
@@ -173,6 +176,16 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
setStepNav(nav)
}, [setStepNav, collection, global, dateFormat, doc, mostRecentDoc, admin, id, locale, t, i18n])
useEffect(() => {
const editConfig = (collection || global)?.admin?.components?.views?.Edit
const versionActions =
editConfig && 'Version' in editConfig && 'actions' in editConfig.Version
? editConfig.Version.actions
: []
setViewActions(versionActions)
}, [collection, global, setViewActions])
let metaTitle: string
let metaDesc: string
const formattedCreatedAt = doc?.createdAt

View File

@@ -5,6 +5,7 @@ import type { IndexProps } from './types'
import { getTranslation } from '../../../../utilities/getTranslation'
import usePayloadAPI from '../../../hooks/usePayloadAPI'
import { useActions } from '../../utilities/ActionsProvider'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import { EditDepthContext } from '../../utilities/EditDepth'
@@ -19,6 +20,8 @@ const VersionsView: React.FC<IndexProps> = (props) => {
const [fetchURL, setFetchURL] = useState('')
const { setViewActions } = useActions()
const {
routes: { admin, api },
serverURL,
@@ -45,18 +48,22 @@ const VersionsView: React.FC<IndexProps> = (props) => {
// 1. "components.Edit"
// 2. "components.Edit.Versions"
// 3. "components.Edit.Versions.Component"
const Edit = collection?.admin?.components?.views?.Edit
const EditCollection = collection?.admin?.components?.views?.Edit
CustomVersionsView =
typeof Edit === 'function'
? Edit
: typeof Edit === 'object' && typeof Edit.Versions === 'function'
? Edit.Versions
: typeof Edit?.Versions === 'object' &&
'Component' in Edit.Versions &&
typeof Edit.Versions.Component === 'function'
? Edit.Versions.Component
: undefined
if (typeof EditCollection === 'function') {
CustomVersionsView = EditCollection
} else if (
typeof EditCollection === 'object' &&
typeof EditCollection.Versions === 'function'
) {
CustomVersionsView = EditCollection.Versions
} else if (
typeof EditCollection?.Versions === 'object' &&
'Component' in EditCollection.Versions &&
typeof EditCollection.Versions.Component === 'function'
) {
CustomVersionsView = EditCollection.Versions.Component
}
}
if (global) {
@@ -66,18 +73,19 @@ const VersionsView: React.FC<IndexProps> = (props) => {
editURL = `${admin}/globals/${global.slug}`
// See note above about cascading component definitions
const Edit = global?.admin?.components?.views?.Edit
const EditGlobal = global?.admin?.components?.views?.Edit
CustomVersionsView =
typeof Edit === 'function'
? Edit
: typeof Edit === 'object' && typeof Edit.Versions === 'function'
? Edit.Versions
: typeof Edit?.Versions === 'object' &&
'Component' in Edit.Versions &&
typeof Edit.Versions.Component === 'function'
? Edit.Versions.Component
: undefined
if (typeof EditGlobal === 'function') {
CustomVersionsView = EditGlobal
} else if (typeof EditGlobal === 'object' && typeof EditGlobal.Versions === 'function') {
CustomVersionsView = EditGlobal.Versions
} else if (
typeof EditGlobal?.Versions === 'object' &&
'Component' in EditGlobal.Versions &&
typeof EditGlobal.Versions.Component === 'function'
) {
CustomVersionsView = EditGlobal.Versions.Component
}
}
const [{ data, isLoading }] = usePayloadAPI(docURL, { initialParams: { draft: 'true' } })
@@ -120,6 +128,16 @@ const VersionsView: React.FC<IndexProps> = (props) => {
setParams(params)
}, [setParams, page, sort, limit, serverURL, api, id, global, collection])
useEffect(() => {
const editConfig = (collection || global)?.admin?.components?.views?.Edit
const versionsActions =
editConfig && 'Versions' in editConfig && 'actions' in editConfig.Versions
? editConfig.Versions.actions
: []
setViewActions(versionsActions)
}, [collection, global, setViewActions])
return (
<EditDepthContext.Provider value={1}>
<RenderCustomComponent

View File

@@ -1,5 +1,6 @@
import React, { useCallback } from 'react'
import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import type { FieldTypes } from '../../../forms/field-types'
import type { CollectionEditViewProps } from '../../types'
@@ -8,6 +9,7 @@ import { getTranslation } from '../../../../../utilities/getTranslation'
import { DocumentHeader } from '../../../elements/DocumentHeader'
import { FormLoadingOverlayToggle } from '../../../elements/Loading'
import Form from '../../../forms/Form'
import { useActions } from '../../../utilities/ActionsProvider'
import { useAuth } from '../../../utilities/Auth'
import { useDocumentEvents } from '../../../utilities/DocumentEvents'
import { OperationContext } from '../../../utilities/OperationProvider'
@@ -43,12 +45,16 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
onSave: onSaveFromProps,
} = props
const { setViewActions } = useActions()
const { reportUpdate } = useDocumentEvents()
const { auth } = collection
const classes = [baseClass, isEditing && `${baseClass}--is-editing`].filter(Boolean).join(' ')
const location = useLocation()
const onSave = useCallback(
async (json) => {
reportUpdate({
@@ -72,6 +78,21 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
const operation = isEditing ? 'update' : 'create'
useEffect(() => {
const path = location.pathname
if (!(path.endsWith(id) || path.endsWith('/create'))) {
return
}
const editConfig = collection?.admin?.components?.views?.Edit
const defaultActions =
editConfig && 'Default' in editConfig && 'actions' in editConfig.Default
? editConfig.Default.actions
: []
setViewActions(defaultActions)
}, [id, location.pathname, collection?.admin?.components?.views?.Edit, setViewActions])
return (
<main className={classes}>
<OperationContext.Provider value={operation}>

View File

@@ -12,6 +12,7 @@ import usePayloadAPI from '../../../../hooks/usePayloadAPI'
import { useUseTitleField } from '../../../../hooks/useUseAsTitle'
import { useStepNav } from '../../../elements/StepNav'
import { TableColumnsProvider } from '../../../elements/TableColumns'
import { useActions } from '../../../utilities/ActionsProvider'
import { useAuth } from '../../../utilities/Auth'
import { useConfig } from '../../../utilities/Config'
import { usePreferences } from '../../../utilities/Preferences'
@@ -60,6 +61,9 @@ const ListView: React.FC<ListIndexProps> = (props) => {
routes: { admin, api },
serverURL,
} = useConfig()
const { setViewActions } = useActions()
const preferenceKey = `${collection.slug}-list`
const { permissions } = useAuth()
const { setStepNav } = useStepNav()
@@ -75,6 +79,12 @@ const ListView: React.FC<ListIndexProps> = (props) => {
const [{ data }, { setParams }] = usePayloadAPI(fetchURL, { initialParams: { page: 1 } })
const titleField = useUseTitleField(collection)
useEffect(() => {
if (CustomList && typeof CustomList === 'object' && 'actions' in CustomList) {
setViewActions(CustomList.actions || [])
}
}, [CustomList, setViewActions])
useEffect(() => {
setStepNav([
{
@@ -205,10 +215,18 @@ const ListView: React.FC<ListIndexProps> = (props) => {
}
}, [data, history, resetParams])
let ListToRender = null
if (CustomList && typeof CustomList === 'function') {
ListToRender = CustomList
} else if (typeof CustomList === 'object' && typeof CustomList.Component === 'function') {
ListToRender = CustomList.Component
}
return (
<TableColumnsProvider collection={collection}>
<RenderCustomComponent
CustomComponent={CustomList}
CustomComponent={ListToRender}
DefaultComponent={DefaultList}
componentProps={{
collection: { ...collection, fields },

View File

@@ -47,7 +47,13 @@ const collectionSchema = joi.object().keys({
// References
}),
),
List: componentSchema,
List: joi.alternatives().try(
componentSchema,
joi.object({
Component: componentSchema,
actions: joi.array().items(componentSchema),
}),
),
}),
}),
defaultColumns: joi.array().items(joi.string()),

View File

@@ -273,7 +273,12 @@ export type CollectionAdminOptions = {
}
)
| AdminViewComponent
List?: React.ComponentType<ListProps>
List?:
| {
Component?: React.ComponentType<ListProps>
actions?: React.ComponentType<any>[]
}
| React.ComponentType<ListProps>
}
}
/**

View File

@@ -39,6 +39,7 @@ export default joi.object({
},
components: joi.object().keys({
Nav: component,
actions: joi.array().items(component),
afterDashboard: joi.array().items(component),
afterLogin: joi.array().items(component),
afterNavLinks: joi.array().items(component),

View File

@@ -14,6 +14,7 @@ export const documentTabSchema = {
export const customViewSchema = joi.object({
Component: componentSchema,
Tab: joi.alternatives().try(documentTabSchema, componentSchema),
actions: joi.array().items(componentSchema),
path: joi.string(),
})

View File

@@ -272,6 +272,9 @@ export type EditViewConfig =
Component: AdminViewComponent
path: string
}
| {
actions?: React.ComponentType<any>[]
}
/**
* Override existing views
@@ -397,6 +400,10 @@ export type Config = {
* Replace the navigation with a custom component
*/
Nav?: React.ComponentType<any>
/**
* Add custom components to the top right of the Admin Panel
*/
actions?: React.ComponentType<any>[]
/**
* Add custom components after the collection overview
*/

View File

@@ -28,3 +28,6 @@ export {
DescriptionComponent,
DescriptionFunction,
} from '../../admin/components/forms/FieldDescription/types'
export { useNav } from '../../admin/components/elements/Nav/context'
export { default as NavGroup } from '../../admin/components/elements/NavGroup'

View File

@@ -1,7 +1,7 @@
{
"name": "@payloadcms/plugin-form-builder",
"description": "Form builder plugin for Payload CMS",
"version": "1.0.15",
"version": "1.1.0",
"homepage:": "https://payloadcms.com",
"repository": "git@github.com:payloadcms/plugin-form-builder.git",
"main": "dist/index.js",

View File

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

3
pnpm-lock.yaml generated
View File

@@ -435,6 +435,9 @@ importers:
get-port:
specifier: 5.1.1
version: 5.1.1
http-status:
specifier: 1.6.2
version: 1.6.2
mongoose:
specifier: 6.12.0
version: 6.12.0

View File

@@ -28,7 +28,7 @@
"@payloadcms/plugin-nested-docs": "^1.0.8",
"@payloadcms/plugin-redirects": "^1.0.0",
"@payloadcms/plugin-seo": "^1.0.10",
"@payloadcms/plugin-stripe": "^0.0.14",
"@payloadcms/plugin-stripe": "^0.0.19",
"@payloadcms/richtext-slate": "^1.0.0",
"@stripe/react-stripe-js": "^1.16.3",
"@stripe/stripe-js": "^1.46.0",

View File

@@ -1521,10 +1521,10 @@
resolved "https://registry.yarnpkg.com/@payloadcms/plugin-seo/-/plugin-seo-1.0.15.tgz#ca794897d1e8c3291a8dd74339b7f28f10bba815"
integrity sha512-7nU0DD3UZOOHsV2UIkOWL2JNCX+u1WNbEvZOiGpO6lB6YekuVIMqxHKbTdVR73UeW44lApvS9LTgif3XLQ5HDA==
"@payloadcms/plugin-stripe@^0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@payloadcms/plugin-stripe/-/plugin-stripe-0.0.14.tgz#c74609ddc193e5e40b07a36ea7f5e485566b9c45"
integrity sha512-KFeyDNvt1Xvhp9mzxGqeQo9GjW52R1boyO1kYD+1c+4DAWWF9oXmf2OXXi1mbt2arSn+IBxcLnGE1EYPwT1nTA==
"@payloadcms/plugin-stripe@^0.0.19":
version "0.0.19"
resolved "https://registry.yarnpkg.com/@payloadcms/plugin-stripe/-/plugin-stripe-0.0.19.tgz#ea08493ed9eb4c747799c7a8dbb429bc20aedcda"
integrity sha512-5eB2ae38dDEUfVgWs5qkB3feQuuYV8t5R0Jn4qhNPBx11sdxhPGVcNyrK3KJizUSIkt/B9T9q31PfWBc0NT7kA==
dependencies:
"@types/uuid" "^9.0.0"
lodash.get "^4.4.2"

View File

@@ -1,9 +1,29 @@
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
import CollectionAPIButton from '../components/CollectionAPIButton'
import CollectionEditButton from '../components/CollectionEditButton'
import CollectionListButton from '../components/CollectionListButton'
import { geoCollectionSlug } from '../slugs'
export const Geo: CollectionConfig = {
slug: geoCollectionSlug,
admin: {
components: {
views: {
Edit: {
Default: {
actions: [CollectionEditButton],
},
API: {
actions: [CollectionAPIButton],
},
},
List: {
actions: [CollectionListButton],
},
},
},
},
fields: [
{
name: 'point',

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'admin-button'
const AdminButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Admin Button
</p>
</div>
)
}
export default AdminButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'collection-api-button'
const CollectionAPIButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Collection API Button
</p>
</div>
)
}
export default CollectionAPIButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'collection-edit-button'
const CollectionEditButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Collection Edit Button
</p>
</div>
)
}
export default CollectionEditButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'collection-list-button'
const CollectionListButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Collection List Button
</p>
</div>
)
}
export default CollectionListButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'global-api-button'
const GlobalAPIButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Global API Button
</p>
</div>
)
}
export default GlobalAPIButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'global-edit-button'
const GlobalEditButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Global Edit Button
</p>
</div>
)
}
export default GlobalEditButton

View File

@@ -12,6 +12,7 @@ import { CollectionHidden } from './collections/Hidden'
import { CollectionNoApiView } from './collections/NoApiView'
import { Posts } from './collections/Posts'
import { Users } from './collections/Users'
import AdminButton from './components/AdminButton'
import AfterDashboard from './components/AfterDashboard'
import AfterNavLinks from './components/AfterNavLinks'
import BeforeLogin from './components/BeforeLogin'
@@ -35,6 +36,7 @@ export default buildConfigWithDefaults({
css: path.resolve(__dirname, 'styles.scss'),
components: {
// providers: [CustomProvider, CustomProvider],
actions: [AdminButton],
afterDashboard: [AfterDashboard],
beforeLogin: [BeforeLogin],
logout: {

View File

@@ -264,6 +264,54 @@ describe('admin', () => {
})
})
describe('app-header', () => {
test('should show admin level action in admin panel', async () => {
await page.goto(url.admin)
// Check if the element with the class .admin-button exists
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
})
test('should show admin level action in collection list view', async () => {
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}`)
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
})
test('should show admin level action in collection edit view', async () => {
const { id } = await createGeo()
await page.goto(geoUrl.edit(id))
await expect(page.locator('.app-header .admin-button')).toHaveCount(1)
})
test('should show collection list view level action in collection list view', async () => {
await page.goto(`${new AdminUrlUtil(serverURL, 'geo').list}`)
await expect(page.locator('.app-header .collection-list-button')).toHaveCount(1)
})
test('should show collection edit view level action in collection edit view', async () => {
const { id } = await createGeo()
await page.goto(geoUrl.edit(id))
await expect(page.locator('.app-header .collection-edit-button')).toHaveCount(1)
})
test('should show collection api view level action in collection api view', async () => {
const { id } = await createGeo()
await page.goto(`${geoUrl.edit(id)}/api`)
await expect(page.locator('.app-header .collection-api-button')).toHaveCount(1)
})
test('should show global edit view level action in globals edit view', async () => {
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
await page.goto(globalWithPreview.global(globalSlug))
await expect(page.locator('.app-header .global-edit-button')).toHaveCount(1)
})
test('should show global api view level action in globals api view', async () => {
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
await page.goto(`${globalWithPreview.global(globalSlug)}/api`)
await expect(page.locator('.app-header .global-api-button')).toHaveCount(1)
})
})
describe('ui', () => {
test('collection - should render preview button when `admin.preview` is set', async () => {
const collectionWithPreview = new AdminUrlUtil(serverURL, postsCollectionSlug)

View File

@@ -1,5 +1,7 @@
import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types'
import GlobalAPIButton from '../components/GlobalAPIButton'
import GlobalEditButton from '../components/GlobalEditButton'
import { globalSlug } from '../slugs'
export const Global: GlobalConfig = {
@@ -8,6 +10,18 @@ export const Global: GlobalConfig = {
en: 'My Global Label',
},
admin: {
components: {
views: {
Edit: {
Default: {
actions: [GlobalEditButton],
},
API: {
actions: [GlobalAPIButton],
},
},
},
},
group: 'Group',
preview: () => 'https://payloadcms.com',
},

View File

@@ -5,6 +5,7 @@ import { Archive } from '../blocks/ArchiveBlock'
import { CallToAction } from '../blocks/CallToAction'
import { Content } from '../blocks/Content'
import { MediaBlock } from '../blocks/MediaBlock'
import CollectionLivePreviewButton from '../components/CollectionLivePreviewButton'
import { hero } from '../fields/hero'
import { pagesSlug, tenantsSlug } from '../shared'
@@ -19,6 +20,15 @@ export const Pages: CollectionConfig = {
admin: {
useAsTitle: 'title',
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
components: {
views: {
Edit: {
LivePreview: {
actions: [CollectionLivePreviewButton],
},
},
},
},
},
fields: [
{

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'collection-live-preview-button'
const CollectionLivePreviewButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Collection Live Preview Button
</p>
</div>
)
}
export default CollectionLivePreviewButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'global-live-preview-button'
const GlobalLivePreviewButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Global Live Preview Button
</p>
</div>
)
}
export default GlobalLivePreviewButton

View File

@@ -81,6 +81,16 @@ describe('Live Preview', () => {
await saveDocAndAssert(page)
})
test('collection - should show live-preview view level action in live-preview view', async () => {
await goToCollectionPreview(page)
await expect(page.locator('.app-header .collection-live-preview-button')).toHaveCount(1)
})
test('global - should show live-preview view level action in live-preview view', async () => {
await goToGlobalPreview(page, 'footer')
await expect(page.locator('.app-header .global-live-preview-button')).toHaveCount(1)
})
test('global - has tab', async () => {
const global = new AdminUrlUtil(serverURL, 'header')
await page.goto(global.global('header'))
@@ -176,11 +186,11 @@ describe('Live Preview', () => {
.click()
// Make sure the value has been set
expect(breakpointSelector).toContainText(mobileBreakpoint.label)
await expect(breakpointSelector).toContainText(mobileBreakpoint.label)
const option = page.locator(
'.live-preview-toolbar-controls__breakpoint button.popup-button-list__button--selected',
)
expect(option).toHaveText(mobileBreakpoint.label)
await expect(option).toHaveText(mobileBreakpoint.label)
// Measure the size of the iframe against the specified breakpoint
const iframe = page.locator('iframe')

View File

@@ -1,5 +1,6 @@
import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types'
import GlobalLivePreviewButton from '../components/GlobalLivePreviewButton'
import link from '../fields/link'
export const Footer: GlobalConfig = {
@@ -8,6 +9,17 @@ export const Footer: GlobalConfig = {
read: () => true,
update: () => true,
},
admin: {
components: {
views: {
Edit: {
LivePreview: {
actions: [GlobalLivePreviewButton],
},
},
},
},
},
fields: [
{
name: 'navItems',

View File

@@ -0,0 +1,35 @@
import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types'
export const nestedToArrayAndBlockCollectionSlug = 'nested-to-array-and-block'
export const NestedToArrayAndBlock: CollectionConfig = {
slug: nestedToArrayAndBlockCollectionSlug,
fields: [
{
type: 'blocks',
name: 'blocks',
blocks: [
{
slug: 'block',
fields: [
{
name: 'array',
type: 'array',
fields: [
{
name: 'text',
type: 'text',
localized: true,
},
{
name: 'textNotLocalized',
type: 'text',
},
],
},
],
},
],
},
],
}

View File

@@ -3,6 +3,7 @@ import type { LocalizedPost } from './payload-types'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import { devUser } from '../credentials'
import { ArrayCollection } from './collections/Array'
import { NestedToArrayAndBlock } from './collections/NestedToArrayAndBlock'
import {
defaultLocale,
englishTitle,
@@ -231,6 +232,7 @@ export default buildConfigWithDefaults({
},
],
},
NestedToArrayAndBlock,
],
globals: [
{

View File

@@ -9,9 +9,10 @@ import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers'
import { RESTClient } from '../helpers/rest'
import { arrayCollectionSlug } from './collections/Array'
import { nestedToArrayAndBlockCollectionSlug } from './collections/NestedToArrayAndBlock'
import configPromise from './config'
import { defaultLocale } from './shared'
import {
defaultLocale,
englishTitle,
localizedPostsSlug,
relationEnglishTitle,
@@ -805,6 +806,60 @@ describe('Localization', () => {
expect(nestedFieldRes.docs.map(({ id }) => id)).toContain(post1.id)
})
})
describe('Nested To Array And Block', () => {
it('should be equal to the created document', async () => {
const { id, blocks } = await payload.create({
collection: nestedToArrayAndBlockCollectionSlug,
locale: defaultLocale,
data: {
blocks: [
{
blockType: 'block',
array: [
{
text: 'english',
textNotLocalized: 'test',
},
],
},
],
},
})
await payload.update({
collection: nestedToArrayAndBlockCollectionSlug,
locale: spanishLocale,
id,
data: {
blocks: (blocks as { array: { text: string }[] }[]).map((block) => ({
...block,
array: block.array.map((item) => ({ ...item, text: 'spanish' })),
})),
},
})
const docDefaultLocale = await payload.findByID({
collection: nestedToArrayAndBlockCollectionSlug,
locale: defaultLocale,
id,
})
const docSpanishLocale = await payload.findByID({
collection: nestedToArrayAndBlockCollectionSlug,
locale: spanishLocale,
id,
})
const rowDefault = docDefaultLocale.blocks[0].array[0]
const rowSpanish = docSpanishLocale.blocks[0].array[0]
expect(rowDefault.text).toEqual('english')
expect(rowDefault.textNotLocalized).toEqual('test')
expect(rowSpanish.text).toEqual('spanish')
expect(rowSpanish.textNotLocalized).toEqual('test')
})
})
})
async function createLocalizedPost(data: {

View File

@@ -1,6 +1,8 @@
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
import { extractTranslations } from '../../../packages/payload/src/translations/extractTranslations'
import CollectionVersionButton from '../elements/CollectionVersionButton'
import CollectionVersionsButton from '../elements/CollectionVersionsButton'
import { CustomPublishButton } from '../elements/CustomSaveButton'
import { draftCollectionSlug } from '../slugs'
@@ -35,6 +37,16 @@ const DraftPosts: CollectionConfig = {
edit: {
PublishButton: CustomPublishButton,
},
views: {
Edit: {
Version: {
actions: [CollectionVersionButton],
},
Versions: {
actions: [CollectionVersionsButton],
},
},
},
},
defaultColumns: ['title', 'description', 'createdAt', '_status'],
preview: () => 'https://payloadcms.com',

View File

@@ -229,6 +229,21 @@ describe('versions', () => {
expect(page.url()).toMatch(/\/versions$/)
})
test('should show collection versions view level action in collection versions view', async () => {
await page.goto(url.list)
await page.locator('tbody tr .cell-title a').first().click()
await page.goto(`${page.url()}/versions`)
await expect(page.locator('.app-header .collection-versions-button')).toHaveCount(1)
})
test('should show global versions view level action in globals versions view', async () => {
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
await page.goto(`${global.global(draftGlobalSlug)}/versions`)
await expect(page.locator('.app-header .global-versions-button')).toHaveCount(1)
})
// TODO: Check versions/:version-id view for collections / globals
test('global - has versions tab', async () => {
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
await page.goto(global.global(draftGlobalSlug))

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'collection-version-button'
const CollectionVersionButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Collection Version Button
</p>
</div>
)
}
export default CollectionVersionButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'collection-versions-button'
const CollectionVersionsButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Collection Versions Button
</p>
</div>
)
}
export default CollectionVersionsButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'global-version-button'
const GlobalVersionButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Global Version Button
</p>
</div>
)
}
export default GlobalVersionButton

View File

@@ -0,0 +1,22 @@
import React from 'react'
const baseClass = 'global-versions-button'
const GlobalVersionsButton: React.FC = () => {
return (
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Global Versions Button
</p>
</div>
)
}
export default GlobalVersionsButton

View File

@@ -1,5 +1,7 @@
import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types'
import GlobalVersionButton from '../elements/GlobalVersionButton'
import GlobalVersionsButton from '../elements/GlobalVersionsButton'
import { draftGlobalSlug } from '../slugs'
const DraftGlobal: GlobalConfig = {
@@ -7,6 +9,18 @@ const DraftGlobal: GlobalConfig = {
label: 'Draft Global',
admin: {
preview: () => 'https://payloadcms.com',
components: {
views: {
Edit: {
Version: {
actions: [GlobalVersionButton],
},
Versions: {
actions: [GlobalVersionsButton],
},
},
},
},
},
versions: {
max: 20,