Compare commits
10 Commits
plugin-red
...
plugin-for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a099f55a69 | ||
|
|
1f1445c798 | ||
|
|
741a5e3650 | ||
|
|
05e8914db7 | ||
|
|
ef43629502 | ||
|
|
7a4607897d | ||
|
|
1cad1a6954 | ||
|
|
9e8f14a897 | ||
|
|
f3748a1534 | ||
|
|
fee81bfbc4 |
68
CHANGELOG.md
68
CHANGELOG.md
@@ -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)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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: (
|
||||
<>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
23
packages/db-mongodb/src/utilities/handleError.ts
Normal file
23
packages/db-mongodb/src/utilities/handleError.ts
Normal 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
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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, '')] = []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 || []),
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -273,7 +273,12 @@ export type CollectionAdminOptions = {
|
||||
}
|
||||
)
|
||||
| AdminViewComponent
|
||||
List?: React.ComponentType<ListProps>
|
||||
List?:
|
||||
| {
|
||||
Component?: React.ComponentType<ListProps>
|
||||
actions?: React.ComponentType<any>[]
|
||||
}
|
||||
| React.ComponentType<ListProps>
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
3
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
22
test/admin/components/AdminButton/index.tsx
Normal file
22
test/admin/components/AdminButton/index.tsx
Normal 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
|
||||
22
test/admin/components/CollectionAPIButton/index.tsx
Normal file
22
test/admin/components/CollectionAPIButton/index.tsx
Normal 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
|
||||
22
test/admin/components/CollectionEditButton/index.tsx
Normal file
22
test/admin/components/CollectionEditButton/index.tsx
Normal 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
|
||||
22
test/admin/components/CollectionListButton/index.tsx
Normal file
22
test/admin/components/CollectionListButton/index.tsx
Normal 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
|
||||
22
test/admin/components/GlobalAPIButton/index.tsx
Normal file
22
test/admin/components/GlobalAPIButton/index.tsx
Normal 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
|
||||
22
test/admin/components/GlobalEditButton/index.tsx
Normal file
22
test/admin/components/GlobalEditButton/index.tsx
Normal 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
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
35
test/localization/collections/NestedToArrayAndBlock/index.ts
Normal file
35
test/localization/collections/NestedToArrayAndBlock/index.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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))
|
||||
|
||||
22
test/versions/elements/CollectionVersionButton/index.tsx
Normal file
22
test/versions/elements/CollectionVersionButton/index.tsx
Normal 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
|
||||
22
test/versions/elements/CollectionVersionsButton/index.tsx
Normal file
22
test/versions/elements/CollectionVersionsButton/index.tsx
Normal 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
|
||||
22
test/versions/elements/GlobalVersionButton/index.tsx
Normal file
22
test/versions/elements/GlobalVersionButton/index.tsx
Normal 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
|
||||
22
test/versions/elements/GlobalVersionsButton/index.tsx
Normal file
22
test/versions/elements/GlobalVersionsButton/index.tsx
Normal 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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user