Compare commits

...

31 Commits

Author SHA1 Message Date
Elliot DeNolf
ab9074220a chore(release): payload/2.27.0 [skip ci] 2024-08-26 14:01:47 -04:00
Paul
afa90a4362 chore: update docs for stripe plugin webhook (#7763)
Closes https://github.com/payloadcms/payload/issues/7740
2024-08-19 13:12:24 -06:00
Elliot DeNolf
bc0516da90 chore(dependabot): add .github/actions dir 2024-08-14 22:20:42 -04:00
Elliot DeNolf
46daf473c8 chore(dependabot): add .github/workflows dir 2024-08-14 22:02:01 -04:00
Elliot DeNolf
337b8ccbf3 chore: add packageManager property for dependabot 2024-08-14 21:52:06 -04:00
Elliot DeNolf
ba2e4c278f chore: remove explicit dependabot versioning-strategy 2024-08-14 21:23:02 -04:00
Elliot DeNolf
3196036ae9 chore: dependabot time format 2024-08-14 21:19:43 -04:00
Elliot DeNolf
9bc3ad5159 chore: add dependabot.yml 2024-08-14 21:17:28 -04:00
Alessio Gravili
94d18e8d74 feat: upgrade react-toastify dependency, and upgrade to pnpm v9 in our monorepo (#7667) 2024-08-14 20:05:04 -04:00
Patrik
c624eea0d8 fix: update state of field if either valid status or errorMessage changes (#7632)
## Description

Fixes #6413 

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-13 11:23:51 -04:00
Paul
f97627092c feat: add support for custom image size file names (#7637)
Add support for custom file names in images sizes

```ts
{
  name: 'thumbnail',
  width: 400,
  height: 300,
  generateImageName: ({ height, sizeName, extension, width }) => {
    return `custom-${sizeName}-${height}-${width}.${extension}`
  },
}
```
2024-08-12 14:36:09 -06:00
Elliot DeNolf
f00183029e chore(release): richtext-lexical/0.11.3 [skip ci] 2024-08-09 09:39:40 -04:00
Elliot DeNolf
b6c5aaa966 chore(release): db-mongodb/1.7.2 [skip ci] 2024-08-09 09:39:18 -04:00
Elliot DeNolf
517aaa0665 chore(release): payload/2.26.0 [skip ci] 2024-08-09 09:37:40 -04:00
Jarrod Flesch
2c2ffe406f chore: allow password to be mutated by hooks (#7537)
Fixes https://github.com/payloadcms/payload/issues/7531

Allows passwords to be updated in hooks.
2024-08-09 09:27:09 -04:00
James Mikrut
7f39afa192 feat: adds classnames to edit, list views (#7595)
## Description

Adds classnames to List and Edit views to be able to more easily target
individual entity views via CSS / similar.

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-08 19:44:09 -04:00
Patrik
fc4d24aa88 fix: render singular label for ArrayCell when length is 1 (#7585)
## Description

Fixes #6099

![Screenshot 2024-08-08 at 2 40
25 PM](https://github.com/user-attachments/assets/0a7ac732-adfe-456b-80c6-1e4b6ce4c4c8)

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-08 15:44:35 -04:00
Patrik
efa56cefc1 fix: filtering by non-poly relationships with not_equals operator (#7573)
## Description

Fixes #5212

Fixes #6278 

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-08-08 11:22:47 -04:00
Patrik
907d7d1d3a fix: filtering by polymorphic relationships with drafts enabled (#7565)
## Description

Fixes #6880 

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-08-07 15:31:47 -04:00
Patrik
eca1517237 fix: deprecated inflight package (#6558)
Fixes #6492
2024-08-07 10:32:17 -04:00
Patrik
9865ae998b fix: enable relationship & upload field population in versions (#7533) 2024-08-06 12:09:53 -04:00
Patrik
1a0ef4824b fix: prevents hasMany text going outside of input boundaries (#7454)
## Description

Fixes #6034

`Before`:
![Screenshot 2024-07-31 at 12 26
25 PM](https://github.com/user-attachments/assets/df2cfcda-d81e-42cf-a97d-9552a420b9e8)

`After`:
![Screenshot 2024-07-31 at 12 26
10 PM](https://github.com/user-attachments/assets/fa7c369f-efc3-4aff-95ad-3e2b2525d3c3)

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-05 17:09:29 -04:00
Radosław Kłos
39e110e633 feat: adds upload's relationship thumbnail (#5015)
## Description

I've made an implementation of the feature requested here:
https://github.com/payloadcms/payload/discussions/3407

Before:
![CleanShot 2024-02-07 at 00 39
47](https://github.com/payloadcms/payload/assets/34719093/4b182118-41bd-47f7-af03-a0b739f7e407)

After:
![CleanShot 2024-02-07 at 00 40
17](https://github.com/payloadcms/payload/assets/34719093/d813de81-bab5-40b2-b31c-5a7ee107dabd)


- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-08-01 15:09:59 +01:00
Paul
3e780b9815 feat(ui): expose custom errors in delete many (#7439)
Exposes any custom errors out to the delete many toast as well.
Closes https://github.com/payloadcms/payload/issues/7214


![image](https://github.com/user-attachments/assets/e5d1fc92-3f22-4906-b09c-e94caf82eb64)
2024-07-31 17:25:23 -04:00
Dan Ribbens
a308d6384f fix(db-postgres): localized array inside blocks field (#7458)
fixes #5240
Copy of https://github.com/payloadcms/payload/pull/7457
2024-07-31 16:31:19 -04:00
Jarrod Flesch
492ed30cb8 chore: fix generic usage, fixes CI (#7421) 2024-07-29 16:28:15 -04:00
Francisco Lourenço
fca5a404db fix: previousValue missing from ValidateOptions type (#6931) 2024-07-29 11:49:19 -04:00
Jason Toups
b13f7e8843 chore: updates all of the Readme localhost:3000 Code to Links (#7252) 2024-07-29 11:22:50 -04:00
Ante
25dfdb66cd chore: croatian translation improvements (#7377) 2024-07-29 11:20:40 -04:00
Patrik
9c9e6896a5 fix(payload): retained date milliseconds (#7393)
## Description

Fixes #6108 

Defaults `milliseconds` to `0` for date field picker.

`Before`:
![Screenshot 2024-07-26 at 3 56
45 PM](https://github.com/user-attachments/assets/1806801a-b457-476e-ad84-bcfe3248b61e)


`After`:
![Screenshot 2024-07-26 at 3 54
14 PM](https://github.com/user-attachments/assets/ad92a106-df95-4184-9de2-666d08b636ab)

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-26 16:16:45 -04:00
Elliot DeNolf
a3085435ef chore(release): db-mongodb/1.7.1 [skip ci] 2024-07-26 11:38:00 -04:00
64 changed files with 14075 additions and 9885 deletions

47
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
# docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: github-actions
directories:
- /
- /.github/workflows
- /.github/actions/* # Not working until resolved: https://github.com/dependabot/dependabot-core/issues/6345
- /.github/actions/setup
target-branch: beta
schedule:
interval: monthly
timezone: America/Detroit
time: '06:00'
groups:
github_actions:
patterns:
- '*'
- package-ecosystem: npm
directory: /
target-branch: beta
schedule:
interval: weekly
day: sunday
timezone: America/Detroit
time: '06:00'
commit-message:
prefix: 'chore(deps)'
labels:
- dependencies
groups:
production:
dependency-type: production
update-types:
- minor
- patch
patterns:
- '*'
dev:
dependency-type: development
update-types:
- minor
- patch
patterns:
- '*'

View File

@@ -61,7 +61,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Get pnpm store directory
@@ -116,7 +116,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build
@@ -201,7 +201,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build
@@ -242,7 +242,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build
@@ -286,7 +286,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build
@@ -327,7 +327,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build

View File

@@ -1,3 +1,38 @@
## [2.27.0](https://github.com/payloadcms/payload/compare/v2.26.0...v2.27.0) (2024-08-26)
### Features
* add support for custom image size file names ([#7637](https://github.com/payloadcms/payload/issues/7637)) ([f976270](https://github.com/payloadcms/payload/commit/f97627092cabe4eabbebefa75afc53579188386b))
* upgrade react-toastify dependency, and upgrade to pnpm v9 in our monorepo ([#7667](https://github.com/payloadcms/payload/issues/7667)) ([94d18e8](https://github.com/payloadcms/payload/commit/94d18e8d747588efce225cde0b621db9b513e7c1))
### Bug Fixes
* update state of field if either `valid` status or `errorMessage` changes ([#7632](https://github.com/payloadcms/payload/issues/7632)) ([c624eea](https://github.com/payloadcms/payload/commit/c624eea0d868938f4603860fa25be3df580ba7fe)), closes [#6413](https://github.com/payloadcms/payload/issues/6413)
## [2.26.0](https://github.com/payloadcms/payload/compare/v2.25.0...v2.26.0) (2024-08-09)
### Features
* adds classnames to edit, list views ([#7595](https://github.com/payloadcms/payload/issues/7595)) ([7f39afa](https://github.com/payloadcms/payload/commit/7f39afa1928b118451138e811ea71a04fce021d5))
* adds upload's relationship thumbnail ([#5015](https://github.com/payloadcms/payload/issues/5015)) ([39e110e](https://github.com/payloadcms/payload/commit/39e110e6331efff0ca8ca7174780076243a016de))
* **ui:** expose custom errors in delete many ([#7439](https://github.com/payloadcms/payload/issues/7439)) ([3e780b9](https://github.com/payloadcms/payload/commit/3e780b98155550f877021996dd094ba435dff81b))
### Bug Fixes
* **db-postgres:** localized array inside blocks field ([#7458](https://github.com/payloadcms/payload/issues/7458)) ([a308d63](https://github.com/payloadcms/payload/commit/a308d6384f9724c5ff330382070a5803fbcf167c)), closes [#5240](https://github.com/payloadcms/payload/issues/5240)
* deprecated `inflight` package ([#6558](https://github.com/payloadcms/payload/issues/6558)) ([eca1517](https://github.com/payloadcms/payload/commit/eca1517237c78983c192f4bafa92a86d94a0de9e)), closes [#6492](https://github.com/payloadcms/payload/issues/6492)
* enable `relationship` & `upload` field population in `versions` ([#7533](https://github.com/payloadcms/payload/issues/7533)) ([9865ae9](https://github.com/payloadcms/payload/commit/9865ae998b9aeb5d72724023976bb203133e19ff))
* filtering by non-poly `relationships` with `not_equals` operator ([#7573](https://github.com/payloadcms/payload/issues/7573)) ([efa56ce](https://github.com/payloadcms/payload/commit/efa56cefc15a48cd45b3aaba2eddacca79e1be30)), closes [#5212](https://github.com/payloadcms/payload/issues/5212) [#6278](https://github.com/payloadcms/payload/issues/6278)
* filtering by polymorphic `relationships` with `drafts` enabled ([#7565](https://github.com/payloadcms/payload/issues/7565)) ([907d7d1](https://github.com/payloadcms/payload/commit/907d7d1d3a89ed22bb991a1f238bb77d54e3e173)), closes [#6880](https://github.com/payloadcms/payload/issues/6880)
* retained date milliseconds ([#7393](https://github.com/payloadcms/payload/issues/7393)) ([9c9e689](https://github.com/payloadcms/payload/commit/9c9e6896a502de209c6cccf63cc5cfc0f0143bf3)), closes [#6108](https://github.com/payloadcms/payload/issues/6108)
* prevents `hasMany` text going outside of input boundaries ([#7454](https://github.com/payloadcms/payload/issues/7454)) ([1a0ef48](https://github.com/payloadcms/payload/commit/1a0ef4824b3d6548d36e7f28a2030640361c0655)), closes [#6034](https://github.com/payloadcms/payload/issues/6034)
* previousValue missing from ValidateOptions type ([#6931](https://github.com/payloadcms/payload/issues/6931)) ([fca5a40](https://github.com/payloadcms/payload/commit/fca5a404dbf3b440b428e55cf5e03db647f9a453))
* render singular label for `ArrayCell` when length is 1 ([#7585](https://github.com/payloadcms/payload/issues/7585)) ([fc4d24a](https://github.com/payloadcms/payload/commit/fc4d24aa8889ac9be76059a92478d5532b142b5c)), closes [#6099](https://github.com/payloadcms/payload/issues/6099)
## [2.25.0](https://github.com/payloadcms/payload/compare/v2.24.2...v2.25.0) (2024-07-26)

View File

@@ -49,6 +49,7 @@ caption="Admin panel screenshot of an Upload field"
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |

View File

@@ -76,7 +76,7 @@ The following custom endpoints are automatically opened for you:
| Endpoint | Method | Description |
| --- | --- | --- |
| `/api/stripe/rest` | `POST` | Proxies the [Stripe REST API](https://stripe.com/docs/api) behind [Payload access control](https://payloadcms.com/docs/access-control/overview) and returns the result. See the [REST Proxy](#stripe-rest-proxy) section for more details. |
| `/api/stripe/webhooks` | `POST` | Handles all Stripe webhook events |
| `/stripe/webhooks` | `POST` | Handles all Stripe webhook events |
##### Stripe REST Proxy
@@ -114,13 +114,13 @@ const res = await fetch(`/api/stripe/rest`, {
Development:
1. Login using Stripe cli `stripe login`
1. Forward events to localhost `stripe listen --forward-to localhost:3000/api/stripe/webhooks`
1. Forward events to localhost `stripe listen --forward-to localhost:3000/stripe/webhooks`
1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET`
Production:
1. Login and [create a new webhook](https://dashboard.stripe.com/test/webhooks/create) from the Stripe dashboard
1. Paste `YOUR_DOMAIN_NAME/api/stripe/webhooks` as the "Webhook Endpoint URL"
1. Paste `YOUR_DOMAIN_NAME/stripe/webhooks` as the "Webhook Endpoint URL"
1. Select which events to broadcast
1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET`
1. Then, handle these events using the `webhooks` portion of this plugin's config:

View File

@@ -47,6 +47,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config). |
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
@@ -167,6 +168,22 @@ When an uploaded image is smaller than the defined image size, we have 3 options
Use the `withoutEnlargement` prop to change this.
</Banner>
#### Custom file name per size
Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image.
This function receives the original file name, the resize name, the extension, height and width as arguments.
```ts
{
name: 'thumbnail',
width: 400,
height: 300,
generateImageName: ({ height, sizeName, extension, width }) => {
return `custom-${sizeName}-${height}-${width}.${extension}`
},
}
```
### Crop and Focal Point Selector
This feature is only available for image file types.

View File

@@ -15,7 +15,7 @@ To spin up this example locally, follow these steps:
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.

View File

@@ -9,7 +9,7 @@ To spin up this example locally, follow these steps:
1. First clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn dev`
1. Now `open http://localhost:3000/admin` to access the admin panel
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
1. Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.

View File

@@ -13,7 +13,7 @@ Follow the instructions in each respective README to get started. If you are set
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.

View File

@@ -9,7 +9,7 @@ To spin up the project locally, follow these steps:
1. First clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn dev` (or `docker-compose up`, see [Docker](#docker))
1. Now `open http://localhost:3000/admin` to access the admin panel
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
1. Create your first admin user using the form on the page
That's it! Changes made in `./src` will be reflected in your app.

View File

@@ -15,7 +15,7 @@ Follow the instructions in each respective README to get started. If you are set
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.

View File

@@ -9,7 +9,7 @@ To spin up this example locally, follow these steps:
1. First clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn dev`
1. Now `open http://localhost:3000/admin` to access the admin panel
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
1. Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details on how to log in as a tenant.

View File

@@ -17,7 +17,7 @@ To spin up this example locally, follow these steps:
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
## How it works

View File

@@ -16,7 +16,7 @@ To spin up this example locally, follow these steps:
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
## How it works

View File

@@ -91,7 +91,7 @@
"prompts": "2.4.2",
"qs": "6.11.2",
"read-stream": "^2.1.1",
"rimraf": "3.0.2",
"rimraf": "4.4.1",
"semver": "^7.5.4",
"shelljs": "0.8.5",
"simple-git": "^3.20.0",
@@ -120,8 +120,9 @@
},
"engines": {
"node": ">=14",
"pnpm": ">=8"
"pnpm": ">=9.7.0"
},
"packageManager": "pnpm@9.7.0",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write"

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.7.0",
"version": "1.7.2",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -3,7 +3,7 @@ import type { PathToQuery } from 'payload/database'
import type { Field } from 'payload/types'
import type { Operator } from 'payload/types'
import objectID from 'bson-objectid'
import ObjectIdImport from 'bson-objectid'
import mongoose from 'mongoose'
import { getLocalizedPaths } from 'payload/database'
import { fieldAffectsData } from 'payload/types'
@@ -14,6 +14,8 @@ import type { MongooseAdapter } from '..'
import { operatorMap } from './operatorMap'
import { sanitizeQueryValue } from './sanitizeQueryValue'
const ObjectId = ObjectIdImport
type SearchParam = {
path?: string
rawQuery?: unknown
@@ -195,16 +197,20 @@ export async function buildSearchParam({
if (field.type === 'relationship' || field.type === 'upload') {
let hasNumberIDRelation
let multiIDCondition = '$or'
if (operatorKey === '$ne') multiIDCondition = '$and'
const result = {
value: {
$or: [{ [path]: { [operatorKey]: formattedValue } }],
[multiIDCondition]: [{ [path]: { [operatorKey]: formattedValue } }],
},
}
if (typeof formattedValue === 'string') {
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
result.value.$or.push({ [path]: { [operatorKey]: objectID(formattedValue) } })
result.value[multiIDCondition].push({
[path]: { [operatorKey]: ObjectId(formattedValue) },
})
} else {
;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach(
(relationTo) => {
@@ -225,11 +231,13 @@ export async function buildSearchParam({
)
if (hasNumberIDRelation)
result.value.$or.push({ [path]: { [operatorKey]: parseFloat(formattedValue) } })
result.value[multiIDCondition].push({
[path]: { [operatorKey]: parseFloat(formattedValue) },
})
}
}
if (result.value.$or.length > 1) {
if (result.value[multiIDCondition].length > 1) {
return result
}
}

View File

@@ -4,8 +4,10 @@ import { fieldAffectsData, fieldHasSubFields } from 'payload/types'
export const hasLocalesTable = (fields: Field[]): boolean => {
return fields.some((field) => {
// arrays always get a separate table
if (field.type === 'array') return false
if (fieldAffectsData(field) && field.localized) return true
if (fieldHasSubFields(field) && field.type !== 'array') return hasLocalesTable(field.fields)
if (fieldHasSubFields(field)) return hasLocalesTable(field.fields)
if (field.type === 'tabs') return field.tabs.some((tab) => hasLocalesTable(tab.fields))
return false
})

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.25.0",
"version": "2.27.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -96,7 +96,7 @@
"is-plain-object": "5.0.0",
"isomorphic-fetch": "3.0.0",
"joi": "17.9.2",
"json-schema-to-typescript": "11.0.3",
"json-schema-to-typescript": "14.0.5",
"jsonwebtoken": "9.0.1",
"jwt-decode": "3.1.2",
"md5": "2.3.0",
@@ -112,7 +112,7 @@
"passport-jwt": "4.0.1",
"passport-local": "1.0.0",
"pino": "8.15.0",
"pino-pretty": "10.2.0",
"pino-pretty": "10.3.1",
"pluralize": "8.0.0",
"probe-image-size": "6.0.0",
"process": "0.11.10",
@@ -129,7 +129,7 @@
"react-router-dom": "5.3.4",
"react-router-navigation-prompt": "1.9.6",
"react-select": "5.7.4",
"react-toastify": "8.2.0",
"react-toastify": "10.0.5",
"sanitize-filename": "1.6.3",
"sass": "1.69.4",
"scheduler": "0.23.0",
@@ -199,7 +199,7 @@
"postcss-loader": "6.2.1",
"postcss-preset-env": "9.0.0",
"release-it": "16.1.3",
"rimraf": "3.0.2",
"rimraf": "4.4.1",
"sass-loader": "12.6.0",
"serve-static": "1.15.0",
"swc-loader": "^0.2.3",

View File

@@ -54,9 +54,12 @@ const DateTime: React.FC<Props> = (props) => {
const onChange = (incomingDate: Date) => {
const newDate = incomingDate
if (newDate instanceof Date && ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
const tzOffset = incomingDate.getTimezoneOffset() / 60
newDate.setHours(12 - tzOffset, 0)
if (newDate instanceof Date) {
newDate.setMilliseconds(0)
if (['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
const tzOffset = incomingDate.getTimezoneOffset() / 60
newDate.setHours(12 - tzOffset, 0)
}
}
if (typeof onChangeFromProps === 'function') onChangeFromProps(newDate)
}

View File

@@ -18,7 +18,7 @@ import './index.scss'
const baseClass = 'delete-documents'
const DeleteMany: React.FC<Props> = (props) => {
const { collection: { labels: { plural }, slug } = {}, resetParams } = props
const { collection: { slug, labels: { plural } } = {}, resetParams } = props
const { permissions } = useAuth()
const {
@@ -41,7 +41,7 @@ const DeleteMany: React.FC<Props> = (props) => {
const handleDelete = useCallback(() => {
setDeleting(true)
requests
void requests
.delete(`${serverURL}${api}/${slug}${getQueryParams()}`, {
headers: {
'Accept-Language': i18n.language,
@@ -60,7 +60,15 @@ const DeleteMany: React.FC<Props> = (props) => {
}
if (json.errors) {
toast.error(json.message)
let message = json.message
if (json.errors) {
json.errors.forEach((error) => {
message = message + '\n' + error.message
})
}
toast.error(message)
} else {
addDefaultError()
}

View File

@@ -22,6 +22,7 @@
padding-top: base(0.25);
padding-bottom: base(0.25);
padding-left: base(0.25);
padding-right: base(0.25);
.rs__multi-value {
margin: calc(#{base(0.125)} - #{$style-stroke-width-s * 2});

View File

@@ -10,6 +10,12 @@
}
}
.has-many {
.rs__input-container {
overflow: hidden;
}
}
html[data-theme='light'] {
.field-type.text {
&.error {

View File

@@ -36,6 +36,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
const showError = valid === false && submitted
const prevValid = useRef(valid)
const prevErrorMessage = useRef(field?.errorMessage)
const prevValue = useRef(value)
// Method to return from `useField`, used to
@@ -128,8 +129,9 @@ const useField = <T,>(options: Options): FieldType<T> => {
// Only dispatch if the validation result has changed
// This will prevent unnecessary rerenders
if (valid !== prevValid.current) {
if (valid !== prevValid.current || errorMessage !== prevErrorMessage.current) {
prevValid.current = valid
prevErrorMessage.current = errorMessage
if (typeof dispatchField === 'function') {
dispatchField({

View File

@@ -65,7 +65,7 @@ const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
}, [global.slug, location.pathname, global?.admin?.components?.views?.Edit, setViewActions])
return (
<main className={baseClass}>
<main className={`${baseClass} ${baseClass}--${global.slug}`}>
<OperationContext.Provider value="update">
<SetStepNav global={global} />
<Form

View File

@@ -25,7 +25,13 @@ const generateLabelFromValue = (
locale: string,
value: { relationTo: string; value: RelationshipValue } | RelationshipValue,
): string => {
let relation: string
if (Array.isArray(value)) {
return value
.map((v) => generateLabelFromValue(collections, field, locale, v))
.filter(Boolean) // Filters out any undefined or empty values
.join(', ')
}
let relatedDoc: RelationshipValue
let valueToReturn = '' as any
@@ -33,38 +39,58 @@ const generateLabelFromValue = (
return String(value)
}
if (Array.isArray(field.relationTo)) {
if (typeof value === 'object') {
relation = value.relationTo
relatedDoc = value.value
}
const relationTo = 'relationTo' in field ? field.relationTo : undefined
if (value === null || typeof value === 'undefined') {
return String(value)
}
if (typeof value === 'object' && 'relationTo' in value) {
relatedDoc = value.value
} else {
relation = field.relationTo
// Non-polymorphic relationship
relatedDoc = value
}
const relatedCollection = collections.find((c) => c.slug === relation)
const relatedCollection = relationTo
? collections.find(
(c) =>
c.slug ===
(typeof value === 'object' && 'relationTo' in value ? value.relationTo : relationTo),
)
: null
if (relatedCollection) {
const useAsTitle = relatedCollection?.admin?.useAsTitle
// eslint-disable-next-line react-hooks/rules-of-hooks
const useAsTitleField = useUseTitleField(relatedCollection)
let titleFieldIsLocalized = false
if (useAsTitleField && fieldAffectsData(useAsTitleField))
if (useAsTitleField && fieldAffectsData(useAsTitleField)) {
titleFieldIsLocalized = useAsTitleField.localized
}
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
valueToReturn = relatedDoc[useAsTitle]
} else if (typeof relatedDoc?.id !== 'undefined') {
valueToReturn = relatedDoc.id
} else {
valueToReturn = relatedDoc
}
if (typeof valueToReturn === 'object' && titleFieldIsLocalized) {
valueToReturn = valueToReturn[locale]
}
} else if (relatedDoc) {
// Handle non-polymorphic `hasMany` relationships or fallback
if (typeof relatedDoc.id !== 'undefined') {
valueToReturn = relatedDoc.id
} else {
valueToReturn = relatedDoc
}
}
if (typeof valueToReturn === 'object' && valueToReturn !== null) {
valueToReturn = JSON.stringify(valueToReturn)
}
return valueToReturn
@@ -79,25 +105,31 @@ const Relationship: React.FC<Props & { field: RelationshipField }> = ({
const { i18n, t } = useTranslation('general')
const { code: locale } = useLocale()
let placeholder = ''
const placeholder = `[${t('noValue')}]`
if (version === comparison) placeholder = `[${t('noValue')}]`
let versionToRender: string | undefined = placeholder
let comparisonToRender: string | undefined = placeholder
let versionToRender = version
let comparisonToRender = comparison
if (version) {
if ('hasMany' in field && field.hasMany && Array.isArray(version)) {
versionToRender =
version.map((val) => generateLabelFromValue(collections, field, locale, val)).join(', ') ||
placeholder
} else {
versionToRender = generateLabelFromValue(collections, field, locale, version) || placeholder
}
}
if (field.hasMany) {
if (Array.isArray(version))
versionToRender = version
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ')
if (Array.isArray(comparison))
comparisonToRender = comparison
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ')
} else {
versionToRender = generateLabelFromValue(collections, field, locale, version)
comparisonToRender = generateLabelFromValue(collections, field, locale, comparison)
if (comparison) {
if ('hasMany' in field && field.hasMany && Array.isArray(comparison)) {
comparisonToRender =
comparison
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ') || placeholder
} else {
comparisonToRender =
generateLabelFromValue(collections, field, locale, comparison) || placeholder
}
}
return (

View File

@@ -108,6 +108,8 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
initialParams: { depth: 1, draft: 'true', locale: '*' },
})
const hasDraftsEnabled = collection?.versions?.drafts || global?.versions?.drafts
const sharedParams = (status) => {
return {
depth: 0,
@@ -122,24 +124,26 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
}
const [{ data: draft }] = usePayloadAPI(compareBaseURL, {
initialParams: { ...sharedParams('draft') },
initialParams: hasDraftsEnabled ? { ...sharedParams('draft') } : {},
})
const [{ data: published }] = usePayloadAPI(compareBaseURL, {
initialParams: { ...sharedParams('published') },
initialParams: hasDraftsEnabled ? { ...sharedParams('published') } : {},
})
useEffect(() => {
const formattedPublished = published?.docs?.length > 0 && published?.docs[0]
const formattedDraft = draft?.docs?.length > 0 && draft?.docs[0]
if (hasDraftsEnabled) {
const formattedPublished = published?.docs?.length > 0 && published?.docs[0]
const formattedDraft = draft?.docs?.length > 0 && draft?.docs[0]
if (!formattedPublished || !formattedDraft) return
if (!formattedPublished || !formattedDraft) return
const publishedNewerThanDraft = formattedPublished?.updatedAt > formattedDraft?.updatedAt
const publishedNewerThanDraft = formattedPublished?.updatedAt > formattedDraft?.updatedAt
setLatestDraftVersion(publishedNewerThanDraft ? undefined : formattedDraft?.id)
setLatestPublishedVersion(formattedPublished.latest ? formattedPublished?.id : undefined)
}, [draft, published])
setLatestDraftVersion(publishedNewerThanDraft ? undefined : formattedDraft?.id)
setLatestPublishedVersion(formattedPublished.latest ? formattedPublished?.id : undefined)
}
}, [hasDraftsEnabled, draft, published])
useEffect(() => {
let nav: StepNavItem[] = []

View File

@@ -47,45 +47,57 @@ export const buildVersionColumns = (
t: TFunction,
latestDraftVersion?: string,
latestPublishedVersion?: string,
): Column[] => [
{
name: '',
accessor: 'updatedAt',
active: true,
components: {
Heading: <SortColumn label={t('general:updatedAt')} name="updatedAt" />,
renderCell: (row, data) => (
<CreatedAtCell collection={collection} date={data} global={global} id={row?.id} />
),
},
label: '',
},
{
name: '',
accessor: 'id',
active: true,
components: {
Heading: <SortColumn disable label={t('versionID')} name="id" />,
renderCell: (row, data) => <TextCell>{data}</TextCell>,
},
label: '',
},
{
name: '',
accessor: 'autosave',
active: true,
components: {
Heading: <SortColumn disable label={t('status')} name="autosave" />,
renderCell: (row) => {
return (
<AutosaveCell
latestDraftVersion={latestDraftVersion}
latestPublishedVersion={latestPublishedVersion}
rowData={row}
/>
)
): Column[] => {
const entityConfig = collection || global
const columns: Column[] = [
{
name: '',
accessor: 'updatedAt',
active: true,
components: {
Heading: <SortColumn label={t('general:updatedAt')} name="updatedAt" />,
renderCell: (row, data) => (
<CreatedAtCell collection={collection} date={data} global={global} id={row?.id} />
),
},
label: '',
},
label: '',
},
]
{
name: '',
accessor: 'id',
active: true,
components: {
Heading: <SortColumn disable label={t('versionID')} name="id" />,
renderCell: (row, data) => <TextCell>{data}</TextCell>,
},
label: '',
},
]
if (
entityConfig?.versions?.drafts ||
(entityConfig?.versions?.drafts && entityConfig.versions.drafts?.autosave)
) {
columns.push({
name: '',
accessor: 'autosave',
active: true,
components: {
Heading: <SortColumn disable label={t('status')} name="autosave" />,
renderCell: (row) => {
return (
<AutosaveCell
latestDraftVersion={latestDraftVersion}
latestPublishedVersion={latestPublishedVersion}
rowData={row}
/>
)
},
},
label: '',
})
}
return columns
}

View File

@@ -94,6 +94,8 @@ const VersionsView: React.FC<IndexProps> = (props) => {
const [{ data: versionsData, isLoading: isLoadingVersions }, { setParams }] =
usePayloadAPI(fetchURL)
const hasDraftsEnabled = collection?.versions?.drafts || global?.versions?.drafts
const sharedParams = (status) => {
return {
depth: 0,
@@ -108,23 +110,25 @@ const VersionsView: React.FC<IndexProps> = (props) => {
}
const [{ data: draft }] = usePayloadAPI(fetchURL, {
initialParams: { ...sharedParams('draft') },
initialParams: hasDraftsEnabled ? { ...sharedParams('draft') } : {},
})
const [{ data: published }] = usePayloadAPI(fetchURL, {
initialParams: { ...sharedParams('published') },
initialParams: hasDraftsEnabled ? { ...sharedParams('published') } : {},
})
useEffect(() => {
const formattedPublished = published?.docs?.length > 0 && published?.docs[0]
const formattedDraft = draft?.docs?.length > 0 && draft?.docs[0]
if (hasDraftsEnabled) {
const formattedPublished = published?.docs?.length > 0 && published?.docs[0]
const formattedDraft = draft?.docs?.length > 0 && draft?.docs[0]
if (!formattedPublished || !formattedDraft) return
if (!formattedPublished || !formattedDraft) return
const publishedNewerThanDraft = formattedPublished?.updatedAt > formattedDraft?.updatedAt
setLatestDraftVersion(publishedNewerThanDraft ? undefined : formattedDraft?.id)
setLatestPublishedVersion(formattedPublished.latest ? formattedPublished?.id : undefined)
}, [draft, published])
const publishedNewerThanDraft = formattedPublished?.updatedAt > formattedDraft?.updatedAt
setLatestDraftVersion(publishedNewerThanDraft ? undefined : formattedDraft?.id)
setLatestPublishedVersion(formattedPublished.latest ? formattedPublished?.id : undefined)
}
}, [hasDraftsEnabled, draft, published])
useEffect(() => {
const params = {

View File

@@ -50,7 +50,13 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
const { auth } = collection
const classes = [baseClass, isEditing && `${baseClass}--is-editing`].filter(Boolean).join(' ')
const classes = [
baseClass,
`${baseClass}--${collection.slug}`,
isEditing && `${baseClass}--is-editing`,
]
.filter(Boolean)
.join(' ')
const location = useLocation()

View File

@@ -12,7 +12,11 @@ const ArrayCell: React.FC<CellComponentProps<ArrayField, Record<string, unknown>
}) => {
const { i18n, t } = useTranslation('general')
const arrayFields = data ?? []
const label = `${arrayFields.length} ${getTranslation(field?.labels?.plural || t('rows'), i18n)}`
const label =
arrayFields.length === 1
? `${arrayFields.length} ${getTranslation(field?.labels?.singular || t('row'), i18n)}`
: `${arrayFields.length} ${getTranslation(field?.labels?.plural || t('rows'), i18n)}`
return <span>{label}</span>
}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { RelationshipField } from '../../../../../../../../exports/types'
import type { RelationshipField, UploadField } from '../../../../../../../../exports/types'
import type { CellComponentProps } from '../../types'
import { getTranslation } from '../../../../../../../../utilities/getTranslation'
@@ -9,13 +9,14 @@ import useIntersect from '../../../../../../../hooks/useIntersect'
import { formatUseAsTitle } from '../../../../../../../hooks/useTitle'
import { useConfig } from '../../../../../../utilities/Config'
import { useListRelationships } from '../../../RelationshipProvider'
import File from '../File'
import './index.scss'
type Value = { relationTo: string; value: number | string }
const baseClass = 'relationship-cell'
const totalToShow = 3
const RelationshipCell: React.FC<CellComponentProps<RelationshipField>> = (props) => {
const RelationshipCell: React.FC<CellComponentProps<RelationshipField | UploadField>> = (props) => {
const { data: cellData, field } = props
const config = useConfig()
const { collections, routes } = config
@@ -68,11 +69,24 @@ const RelationshipCell: React.FC<CellComponentProps<RelationshipField>> = (props
i18n,
})
let fileField = null
if (field.type === 'upload') {
const relatedCollectionPreview = !!relatedCollection.upload.displayPreview
const fieldPreview = field.displayPreview
const previewAllowed =
fieldPreview || (relatedCollectionPreview && fieldPreview !== false)
if (previewAllowed && document) {
fileField = (
<File collection={relatedCollection} data={label} field={field} rowData={document} />
)
}
}
return (
<React.Fragment key={i}>
{document === false && `${t('untitled')} - ID: ${value}`}
{document === null && `${t('loading')}...`}
{document && (label || `${t('untitled')} - ID: ${value}`)}
{document && (fileField || label || `${t('untitled')} - ID: ${value}`)}
{values.length > i + 1 && ', '}
</React.Fragment>
)

View File

@@ -68,7 +68,7 @@ const DefaultList: React.FC<Props> = (props) => {
}
return (
<div className={baseClass}>
<div className={`${baseClass} ${baseClass}--${collection.slug}`}>
{Array.isArray(BeforeList) &&
BeforeList.map((Component, i) => <Component key={i} {...props} />)}

View File

@@ -171,6 +171,7 @@ const collectionSchema = joi.object().keys({
adminThumbnail: joi.alternatives().try(joi.string(), joi.func()),
crop: joi.bool(),
disableLocalStorage: joi.bool(),
displayPreview: joi.bool().default(false),
externalFileHeaderFilter: joi.func(),
filesRequiredOnCreate: joi.bool(),
focalPoint: joi.bool(),

View File

@@ -91,9 +91,9 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
}
let { data } = args
const { password } = data
const dataHasPassword = 'password' in data && data.password
const shouldSaveDraft = Boolean(draftArg && collectionConfig.versions.drafts)
const shouldSavePassword = Boolean(password && collectionConfig.auth && !shouldSaveDraft)
const shouldSavePassword = Boolean(dataHasPassword && collectionConfig.auth && !shouldSaveDraft)
// /////////////////////////////////////
// Access
@@ -256,7 +256,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
const dataToUpdate: Record<string, unknown> = { ...result }
const { password } = dataToUpdate
if (shouldSavePassword && typeof password === 'string') {
const { hash, salt } = await generatePasswordSaltHash({ password })
dataToUpdate.salt = salt

View File

@@ -109,7 +109,8 @@ export async function getLocalizedPaths({
if (typeof matchedField.relationTo !== 'string') {
const lastSegmentIsValid =
['relationTo', 'value'].includes(pathSegments[pathSegments.length - 1]) ||
pathSegments.length === 1
pathSegments.length === 1 ||
(pathSegments.length === 2 && pathSegments[0] === 'version')
if (lastSegmentIsValid) {
lastIncompletePath.complete = true

View File

@@ -342,6 +342,7 @@ export const upload = baseField.keys({
}),
}),
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
displayPreview: joi.boolean().default(false),
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
maxDepth: joi.number(),
relationTo: joi.string().required(),

View File

@@ -128,12 +128,13 @@ export type Labels = {
singular: Record<string, string> | string
}
export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
export type ValidateOptions<TData, TSiblingData, TFieldConfig, TValue> = {
config: SanitizedConfig
data: Partial<TData>
id?: number | string
operation?: Operation
payload?: Payload
previousValue?: TValue
req?: PayloadRequest
siblingData: Partial<TSiblingData>
t: TFunction
@@ -143,7 +144,7 @@ export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
// TODO: Having TFieldConfig as any breaks all type checking / auto-completions for the base ValidateOptions properties.
export type Validate<TValue = any, TData = any, TSiblingData = any, TFieldConfig = any> = (
value: TValue,
options: ValidateOptions<TData, TSiblingData, TFieldConfig>,
options: ValidateOptions<TData, TSiblingData, TFieldConfig, TValue>,
) => Promise<string | true> | string | true
export type OptionObject = {
@@ -407,6 +408,7 @@ export type UploadField = FieldBase & {
Label?: React.ComponentType<LabelProps>
}
}
displayPreview?: boolean
filterOptions?: FilterOptions
maxDepth?: number
relationTo: string

View File

@@ -125,24 +125,25 @@ const relationshipPopulationPromise = async ({
if (fieldSupportsMany(field) && field.hasMany) {
if (
field.localized &&
locale === 'all' &&
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null
) {
Object.keys(siblingDoc[field.name]).forEach((key) => {
if (Array.isArray(siblingDoc[field.name][key])) {
siblingDoc[field.name][key].forEach((relatedDoc, index) => {
Object.keys(siblingDoc[field.name]).forEach((localeKey) => {
if (Array.isArray(siblingDoc[field.name][localeKey])) {
siblingDoc[field.name][localeKey].forEach((relatedDoc, index) => {
const rowPromise = async () => {
await populate({
currentDepth,
data: siblingDoc[field.name][key][index],
data: siblingDoc[field.name][localeKey][index],
dataReference: resultingDoc,
depth: populateDepth,
draft,
fallbackLocale,
field,
index,
key,
key: localeKey,
locale,
overrideAccess,
req,
@@ -178,21 +179,22 @@ const relationshipPopulationPromise = async ({
})
}
} else if (
field.localized &&
locale === 'all' &&
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null &&
locale === 'all'
siblingDoc[field.name] !== null
) {
Object.keys(siblingDoc[field.name]).forEach((key) => {
Object.keys(siblingDoc[field.name]).forEach((localeKey) => {
const rowPromise = async () => {
await populate({
currentDepth,
data: siblingDoc[field.name][key],
data: siblingDoc[field.name][localeKey],
dataReference: resultingDoc,
depth: populateDepth,
draft,
fallbackLocale,
field,
key,
key: localeKey,
locale,
overrideAccess,
req,

View File

@@ -2,36 +2,36 @@
"$schema": "./translation-schema.json",
"authentication": {
"account": "Račun",
"accountOfCurrentUser": "Račun od trenutnog korisnika",
"accountOfCurrentUser": "Račun trenutnog korisnika",
"alreadyActivated": "Već aktivirano",
"alreadyLoggedIn": "Već prijavljen",
"alreadyLoggedIn": "Već prijavljeni",
"apiKey": "API ključ",
"authenticated": "Autenticiran",
"backToLogin": "Nazad na prijavu",
"beginCreateFirstUser": "Za početak, kreiraj svog prvog korisnika.",
"backToLogin": "Natrag na prijavu",
"beginCreateFirstUser": "Za početak, kreirajte prvog korisnika.",
"changePassword": "Promjeni lozinku",
"checkYourEmailForPasswordReset": "Provjerite email s poveznicom koja će Vam omogućiti sigurnu promjenu lozinke.",
"confirmGeneration": "Potvrdi kreiranje",
"checkYourEmailForPasswordReset": "Provjerite e-mail s poveznicom koja će vam omogućiti sigurnu promjenu lozinke.",
"confirmGeneration": "Potvrdi generiranje",
"confirmPassword": "Potvrdi lozinku",
"createFirstUser": "Kreiraj prvog korisnika",
"emailNotValid": "Email nije ispravan",
"emailSent": "Email poslan",
"emailNotValid": "E-mail adresa nije ispravna",
"emailSent": "E-mail poslan",
"enableAPIKey": "Omogući API ključ",
"failedToUnlock": "Neuspješno otključavanje.",
"failedToUnlock": "Otključavanje nije uspjelo.",
"forceUnlock": "Prisilno otključaj",
"forgotPassword": "Zaboravljena lozinka",
"forgotPasswordEmailInstructions": "Molim unesite svoj email. Primit ćete poruku s uputama za ponovno postavljanje lozinke.",
"forgotPasswordEmailInstructions": "Molimo unesite svoju e-mail adresu. Primit ćete poruku s uputama za ponovno postavljanje lozinke.",
"forgotPasswordQuestion": "Zaboravljena lozinka?",
"generate": "Generiraj",
"generateNewAPIKey": "Generiraj novi API ključ",
"generatingNewAPIKeyWillInvalidate": "Generiranje novog API ključa će <1>poništiti</1> prethodni ključ. Jeste li sigurni da želite nastaviti?",
"lockUntil": "Zaključaj dok",
"logBackIn": "Ponovna prijava",
"logBackIn": "Ponovo se prijavite",
"logOut": "Odjava",
"loggedIn": "Za prijavu s drugim korisničkim računom potrebno je prvo <0>odjaviti se</0>",
"loggedInChangePassword": "Da biste promijenili lozinku, otvorite svoj <0>račun</0> i promijenite lozinku tamo.",
"loggedOutInactivity": "Odjavljeni se zbog neaktivnosti.",
"loggedOutSuccessfully": "Uspješno ste odjavljeni..",
"loggedIn": "Za prijavu s drugim korisničkim računom potrebno se prvo <0>odjaviti</0>",
"loggedInChangePassword": "Da biste promijenili lozinku, otvorite svoj <0>račun</0> i promijenite je tamo.",
"loggedOutInactivity": "Odjavljeni ste zbog neaktivnosti.",
"loggedOutSuccessfully": "Uspješno ste odjavljeni.",
"login": "Prijava",
"loginAttempts": "Pokušaji prijave",
"loginUser": "Prijava korisnika",
@@ -39,32 +39,32 @@
"logout": "Odjava",
"logoutUser": "Odjava korisnika",
"newAPIKeyGenerated": "Novi API ključ generiran.",
"newAccountCreated": "Novi račun je kreiran. Pristupite računu klikom na <a href=\"{{serverURL}}\">{{serverURL}}</a>. Molim kliknite na sljedeći link ili zalijepite URL, koji se nalazi ispod, u preglednik da biste potvrdili svoj email: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Nakon što potvrdite email, moći ćete se prijaviti.",
"newAccountCreated": "Novi račun je kreiran. Pristupite računu klikom na: <a href=\"{{serverURL}}\">{{serverURL}}</a>. Molimo kliknite na sljedeću poveznicu ili zalijepite URL, koji se nalazi ispod, u preglednik da biste potvrdili svoju e-mail adresu: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Nakon što potvrdite e-mail adresu, moći ćete se prijaviti.",
"newPassword": "Nova lozinka",
"resetPassword": "Restartiranje lozinke",
"resetPasswordExpiration": "Restartiranje roka trajanja lozinke",
"resetPasswordToken": "Restartiranje lozinke tokena",
"resetYourPassword": "Restartiraj svoju lozinku",
"stayLoggedIn": "Ostani prijavljen",
"resetPassword": "Resetiranje lozinke",
"resetPasswordExpiration": "Rok trajanja resetiranja lozinke",
"resetPasswordToken": "Resetiranje lozinke tokena",
"resetYourPassword": "Resetirajte svoju lozinku",
"stayLoggedIn": "Ostanite prijavljeni",
"successfullyUnlocked": "Uspješno otključano",
"unableToVerify": "Nije moguće potvrditi",
"verified": "Potvrđeno",
"verifiedSuccessfully": "Uspješno potvrđeno",
"verify": "Potvrdi",
"verifyUser": "Potvrdi korisnika",
"verifyYourEmail": "Potvrdi svoj email",
"verifyYourEmail": "Potvrdi svoju e-mail adresu",
"youAreInactive": "Neaktivni ste neko vrijeme i uskoro ćete biti automatski odjavljeni zbog vlastite sigurnosti. Želite li ostati prijavljeni?",
"youAreReceivingResetPassword": "Primili ste ovo jer ste Vi (ili netko drugi) zatražili promjenu lozinke za Vaš račun. Molim kliknite na poveznicu ili zalijepite ovo u svoje preglednik da biste završili proces:",
"youDidNotRequestPassword": "Ako niste zatražili ovo, molim ignorirajte ovaj email i Vaša lozinka ostat će nepromijenjena."
"youAreReceivingResetPassword": "Primili ste ovo jer ste Vi (ili netko drugi) zatražili promjenu lozinke za Vaš račun. Molimo kliknite na poveznicu ili zalijepite ovo u svoje preglednik da biste završili proces:",
"youDidNotRequestPassword": "Ako niste zatražili ovo, molimo ignorirajte ovaj e-mail i Vaša će lozinka ostati nepromijenjena."
},
"error": {
"accountAlreadyActivated": "Ovaj račun je već aktiviran.",
"autosaving": "Nastao je problem pri automatskom spremanju ovog dokumenta.",
"correctInvalidFields": "Molim ispravite nevaljana polja.",
"correctInvalidFields": "Molimo ispravite nevaljana polja.",
"deletingFile": "Dogodila se pogreška pri brisanju datoteke.",
"deletingTitle": "Dogodila se pogreška pri brisanju {{title}}. Molim provjerite svoju internetsku vezu i pokušajte ponovno.",
"emailOrPasswordIncorrect": "Email ili lozinka netočni.",
"followingFieldsInvalid_one": " Ovo polje je nevaljano:",
"deletingTitle": "Dogodila se pogreška pri brisanju {{title}}. Molimo provjerite svoju internet vezu i pokušajte ponovno.",
"emailOrPasswordIncorrect": "E-mail adresa ili lozinka netočni.",
"followingFieldsInvalid_one": "Ovo polje je nevaljano:",
"followingFieldsInvalid_other": "Ova polja su nevaljana:",
"incorrectCollection": "Nevaljana kolekcija",
"invalidFileType": "Nevaljan tip datoteke",
@@ -72,7 +72,7 @@
"loadingDocument": "Pojavio se problem pri učitavanju dokumenta čiji je ID {{id}}.",
"localesNotSaved_one": "Sljedeću lokalnu postavku nije bilo moguće spremiti:",
"localesNotSaved_other": "Sljedeće lokalne postavke nije bilo moguće spremiti:",
"missingEmail": "Nedostaje email.",
"missingEmail": "Nedostaje e-mail.",
"missingIDOfDocument": "Nedostaje ID dokumenta da bi se ažurirao.",
"missingIDOfVersion": "Nedostaje ID verzije.",
"missingRequiredData": "Nedostaju obvezni podaci.",
@@ -88,10 +88,10 @@
"unPublishingDocument": "Pojavio se problem pri poništavanju objave ovog dokumenta.",
"unableToDeleteCount": "Nije moguće izbrisati {{count}} od {{total}} {{label}}.",
"unableToUpdateCount": "Nije moguće ažurirati {{count}} od {{total}} {{label}}.",
"unauthorized": "Neovlašten, morate biti prijavljeni da biste uputili ovaj zahtjev.",
"unauthorized": "Neovlašteno, morate biti prijavljeni da biste uputili ovaj zahtjev.",
"unknown": "Došlo je do nepoznate pogreške.",
"unspecific": "Došlo je do pogreške.",
"userEmailAlreadyRegistered": "Korisnik s navedenom e-poštom je već registriran.",
"userEmailAlreadyRegistered": "Korisnik s navedenom e-mail adresom je već registriran.",
"userLocked": "Ovaj korisnik je zaključan zbog previše neuspješnih pokušaja prijave.",
"valueMustBeUnique": "Vrijednost mora biti jedinstvena.",
"verificationTokenInvalid": "Verifikacijski token je nevaljan."
@@ -121,18 +121,18 @@
"labelRelationship": "{{label}} veza",
"latitude": "Zemljopisna širina",
"linkType": "Tip poveznce",
"linkedTo": "Povezabi sa <0>{{label}}</0>",
"linkedTo": "Povezan s <0>{{label}}</0>",
"longitude": "Zemljopisna dužina",
"newLabel": "Novo {{label}}",
"openInNewTab": "Otvori u novoj kartici.",
"passwordsDoNotMatch": "Lozinke nisu iste.",
"passwordsDoNotMatch": "Lozinke nisu jednake.",
"relatedDocument": "Povezani dokument",
"relationTo": "Veza sa",
"removeRelationship": "Ukloni vezu",
"removeUpload": "Ukloni prijenos",
"saveChanges": "Spremi promjene",
"searchForBlock": "Potraži blok",
"selectExistingLabel": "Odaberi postojeće{{label}}",
"selectExistingLabel": "Odaberi postojeće {{label}}",
"selectFieldsToEdit": "Odaberite polja za uređivanje",
"showAll": "Pokaži sve",
"swapRelationship": "Zamijeni vezu",
@@ -149,7 +149,7 @@
"addBelow": "Dodaj ispod",
"addFilter": "Dodaj filter",
"adminTheme": "Administratorska tema",
"and": "I",
"and": "i",
"applyChanges": "Primijeni promjene",
"ascending": "Uzlazno",
"automatic": "Automatsko",
@@ -177,7 +177,7 @@
"dashboard": "Nadzorna ploča",
"delete": "Obriši",
"deletedCountSuccessfully": "Uspješno izbrisano {{count}} {{label}}.",
"deletedSuccessfully": "Uspješno obrisano.",
"deletedSuccessfully": "Uspješno izbrisano.",
"deleting": "Brisanje...",
"depth": "Dubina",
"descending": "Silazno",
@@ -192,8 +192,8 @@
"editingLabel_many": "Uređivanje {{count}} {{label}}",
"editingLabel_one": "Uređivanje {{count}} {{label}}",
"editingLabel_other": "Uređivanje {{count}} {{label}}",
"email": "Email",
"emailAddress": "Email adresa",
"email": "E-mail",
"emailAddress": "E-mail adresa",
"enterAValue": "Unesi vrijednost",
"error": "Greška",
"errors": "Greške",
@@ -224,9 +224,9 @@
"none": "Nijedan",
"notFound": "Nije pronađeno",
"nothingFound": "Ništa nije pronađeno",
"of": "Od",
"of": "od",
"open": "Otvori",
"or": "Ili",
"or": "ili",
"order": "Poredak",
"pageNotFound": "Stranica nije pronađena",
"password": "Lozinka",
@@ -284,7 +284,7 @@
},
"upload": {
"addFile": "Dodaj datoteku",
"crop": "Usjev",
"crop": "Izreži",
"cropToolDescription": "Povucite kutove odabranog područja, nacrtajte novo područje ili prilagodite vrijednosti ispod.",
"dragAndDrop": "Povucite i ispustite datoteku",
"dragAndDropHere": "ili povucite i ispustite datoteku ovdje",
@@ -307,8 +307,8 @@
"width": "Širina"
},
"validation": {
"emailAddress": "Molim unestie valjanu email adresu.",
"enterNumber": "Molim unesite valjani broj.",
"emailAddress": "Molimo unesite valjanu e-mail adresu.",
"enterNumber": "Molimo unesite valjani broj.",
"fieldHasNo": "Ovo polje nema {{label}}",
"greaterThanMax": "{{value}} exceeds the maximum allowable {{label}} limit of {{max}}.",
"invalidInput": "Ovo polje ima nevaljan unos.",
@@ -327,12 +327,12 @@
"validUploadID": "Ovo polje nije valjani ID prijenosa."
},
"version": {
"aboutToPublishSelection": "Upravo ćete objaviti sve {{label}} u izboru. Jesi li siguran?",
"aboutToPublishSelection": "Upravo ćete objaviti sve {{label}} u izboru. Jeste li sigurani?",
"aboutToRestore": "Vratit ćete {{label}} dokument u stanje u kojem je bio {{versionDate}}",
"aboutToRestoreGlobal": "Vratit ćete globalni {{label}} u stanje u kojem je bio {{versionDate}}.",
"aboutToRevertToPublished": "Vratit ćete promjene u dokumentu u objavljeno stanje. Jeste li sigurni? ",
"aboutToUnpublish": "Poništit ćete objavu ovog dokumenta. Jeste li sigurni?",
"aboutToUnpublishSelection": "Upravo ćete poništiti objavu svih {{label}} u odabiru. Jesi li siguran?",
"aboutToUnpublishSelection": "Upravo ćete poništiti objavu svih {{label}} u odabiru. Jeste li sigurni?",
"autosave": "Automatsko spremanje",
"autosavedSuccessfully": "Automatsko spremanje uspješno.",
"autosavedVersion": "Verzija automatski spremljenog dokumenta",
@@ -343,8 +343,8 @@
"confirmUnpublish": "Potvrdite poništavanje objave",
"confirmVersionRestoration": "Potvrdite vraćanje verzije",
"currentDocumentStatus": "Trenutni {{docStatus}} dokumenta",
"currentDraft": "Trenutni nacrt",
"currentPublishedVersion": "Trenutno objavljena verzija",
"currentDraft": "Trenutni nacrt",
"currentPublishedVersion": "Trenutno objavljena verzija",
"draft": "Nacrt",
"draftSavedSuccessfully": "Nacrt uspješno spremljen.",
"lastSavedAgo": "Zadnji put spremljeno prije {{distance}",

View File

@@ -431,15 +431,26 @@ export default async function resizeAndTransformImageSizes({
const mimeInfo = await fromBuffer(bufferData)
const imageNameWithDimensions = createImageName({
extension: mimeInfo?.ext || sanitizedImage.ext,
height: extractHeightFromImage({
...originalImageMeta,
height: bufferInfo.height,
}),
outputImageName: sanitizedImage.name,
width: bufferInfo.width,
})
const imageNameWithDimensions = imageResizeConfig.generateImageName
? imageResizeConfig.generateImageName({
extension: mimeInfo?.ext || sanitizedImage.ext,
height: extractHeightFromImage({
...originalImageMeta,
height: bufferInfo.height,
}),
originalName: sanitizedImage.name,
sizeName: imageResizeConfig.name,
width: bufferInfo.width,
})
: createImageName({
extension: mimeInfo?.ext || sanitizedImage.ext,
height: extractHeightFromImage({
...originalImageMeta,
height: bufferInfo.height,
}),
outputImageName: sanitizedImage.name,
width: bufferInfo.width,
})
const imagePath = `${staticPath}/${imageNameWithDimensions}`

View File

@@ -5,7 +5,8 @@ import { mimeTypeValidator } from './mimeTypeValidator'
const options = { siblingData: { filename: 'file.xyz' } } as ValidateOptions<
undefined,
undefined,
undefined
undefined,
string
>
describe('mimeTypeValidator', () => {

View File

@@ -51,12 +51,24 @@ export type ImageUploadFormatOptions = {
*/
export type ImageUploadTrimOptions = Parameters<Sharp['trim']>[0]
export type GenerateImageName = (args: {
extension: string
height: number
originalName: string
sizeName: string
width: number
}) => string
export type ImageSize = Omit<ResizeOptions, 'withoutEnlargement'> & {
/**
* @deprecated prefer position
*/
crop?: string // comes from sharp package
formatOptions?: ImageUploadFormatOptions
/**
* Generate a custom name for the file of this image size.
*/
generateImageName?: GenerateImageName
name: string
trimOptions?: ImageUploadTrimOptions
/**
@@ -77,6 +89,7 @@ export type IncomingUploadType = {
adminThumbnail?: GetAdminThumbnail | string
crop?: boolean
disableLocalStorage?: boolean
displayPreview?: boolean
/**
* Accepts existing headers and can filter/modify them.
*
@@ -102,6 +115,7 @@ export type Upload = {
adminThumbnail?: GetAdminThumbnail | string
crop?: boolean
disableLocalStorage?: boolean
displayPreview?: boolean
filesRequiredOnCreate?: boolean
focalPoint?: boolean
formatOptions?: ImageUploadFormatOptions

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.11.2",
"version": "0.11.3",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": {
"type": "git",

View File

@@ -58,7 +58,7 @@ export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNo
nodeValidations: Map<string, Array<NodeValidation>>
payloadConfig: SanitizedConfig
validation: {
options: ValidateOptions<SerializedEditorState, unknown, RichTextField>
options: ValidateOptions<SerializedEditorState, unknown, RichTextField, SerializedEditorState>
value: SerializedEditorState
}
}) => Promise<string | true> | string | true

View File

@@ -14,7 +14,7 @@ export async function validateNodes({
nodes: SerializedLexicalNode[]
payloadConfig: SanitizedConfig
validation: {
options: ValidateOptions<SerializedEditorState, unknown, RichTextField>
options: ValidateOptions<SerializedEditorState, unknown, RichTextField, SerializedEditorState>
value: SerializedEditorState
}
}): Promise<string | true> {

22726
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ To spin up the project locally, follow these steps:
1. First clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn dev` (or `docker-compose up`, see [Docker](#docker))
1. Now `open http://localhost:3000/admin` to access the admin panel
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
1. Create your first admin user using the form on the page
That's it! Changes made in `./src` will be reflected in your app.

View File

@@ -49,7 +49,7 @@ If you have not done so already, you need to have standalone copy of this repo o
1. First [clone the repo](#clone) if you have not done so already
1. `cd my-project && cp .env.example .env` to copy the example environment variables
1. `yarn && yarn dev` to install dependencies and start the dev server
1. `open http://localhost:3000` to open the app in your browser
1. Open [http://localhost:3000](http://localhost:3000) to open the app in your browser
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. To begin accepting payment, follow the [Stripe](#stripe) guide. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live.

View File

@@ -50,7 +50,7 @@ If you have not done so already, you need to have standalone copy of this repo o
1. First [clone the repo](#clone) if you have not done so already
1. `cd my-project && cp .env.example .env` to copy the example environment variables
1. `yarn && yarn dev` to install dependencies and start the dev server
1. `open http://localhost:3000` to open the app in your browser
1. Open [http://localhost:3000](http://localhost:3000) to open the app in your browser
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live.

View File

@@ -0,0 +1,9 @@
import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types'
export const usersSlug = 'users'
export const UsersCollection: CollectionConfig = {
fields: [],
auth: true,
slug: usersSlug,
}

View File

@@ -2,11 +2,13 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import { devUser } from '../credentials'
import { MediaCollection } from './collections/Media'
import { PostsCollection, postsSlug } from './collections/Posts'
import { UsersCollection } from './collections/Users'
import { MenuGlobal } from './globals/Menu'
export default buildConfigWithDefaults({
// ...extend config here
collections: [
UsersCollection,
PostsCollection,
MediaCollection,
// ...add more collections here

View File

@@ -9,3 +9,4 @@ export const relationWithTitleSlug = 'relation-with-title'
export const relationUpdatedExternallySlug = 'relation-updated-externally'
export const collection1Slug = 'collection-1'
export const collection2Slug = 'collection-2'
export const versionedRelationshipFieldSlug = 'versioned-relationship-field'

View File

@@ -0,0 +1,23 @@
import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types'
import { collection1Slug, versionedRelationshipFieldSlug } from '../../collectionSlugs'
export const VersionedRelationshipFieldCollection: CollectionConfig = {
slug: versionedRelationshipFieldSlug,
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'relationshipField',
type: 'relationship',
relationTo: [collection1Slug],
hasMany: true,
},
],
versions: {
drafts: true,
},
}

View File

@@ -1,7 +1,6 @@
import type { CollectionConfig } from '../../packages/payload/src/collections/config/types'
import type { FilterOptionsProps } from '../../packages/payload/src/fields/config/types'
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import { devUser } from '../credentials'
import { PrePopulateFieldUI } from './PrePopulateFieldUI'
@@ -17,6 +16,7 @@ import {
relationWithTitleSlug,
slug,
} from './collectionSlugs'
import { VersionedRelationshipFieldCollection } from './collections/VersionedRelationshipField'
export interface FieldsRelationship {
createdAt: Date
@@ -301,6 +301,9 @@ export default buildConfigWithDefaults({
},
],
slug: collection1Slug,
admin: {
useAsTitle: 'name',
},
},
{
fields: [
@@ -311,7 +314,13 @@ export default buildConfigWithDefaults({
],
slug: collection2Slug,
},
VersionedRelationshipFieldCollection,
],
localization: {
locales: ['en'],
defaultLocale: 'en',
fallback: true,
},
onInit: async (payload) => {
await payload.create({
collection: 'users',
@@ -319,6 +328,8 @@ export default buildConfigWithDefaults({
email: devUser.email,
password: devUser.password,
},
depth: 0,
overrideAccess: true,
})
// Create docs to relate to
const { id: relationOneDocId } = await payload.create({
@@ -326,29 +337,35 @@ export default buildConfigWithDefaults({
data: {
name: relationOneSlug,
},
depth: 0,
overrideAccess: true,
})
const relationOneIDs: string[] = []
await mapAsync([...Array(11)], async () => {
for (let i = 0; i < 11; i++) {
const doc = await payload.create({
collection: relationOneSlug,
data: {
name: relationOneSlug,
},
depth: 0,
overrideAccess: true,
})
relationOneIDs.push(doc.id)
})
}
const relationTwoIDs: string[] = []
await mapAsync([...Array(11)], async () => {
for (let i = 0; i < 11; i++) {
const doc = await payload.create({
collection: relationTwoSlug,
data: {
name: relationTwoSlug,
},
depth: 0,
overrideAccess: true,
})
relationTwoIDs.push(doc.id)
})
}
// Existing relationships
const { id: restrictedDocId } = await payload.create({
@@ -356,13 +373,17 @@ export default buildConfigWithDefaults({
data: {
name: 'relation-restricted',
},
depth: 0,
overrideAccess: true,
})
const relationsWithTitle: string[] = []
await mapAsync(['relation-title', 'word boundary search'], async (title) => {
for (const title of ['relation-title', 'word boundary search']) {
const { id } = await payload.create({
collection: relationWithTitleSlug,
depth: 0,
overrideAccess: true,
data: {
name: title,
meta: {
@@ -371,19 +392,24 @@ export default buildConfigWithDefaults({
},
})
relationsWithTitle.push(id)
})
}
await payload.create({
collection: slug,
depth: 0,
overrideAccess: true,
data: {
relationship: relationOneDocId,
relationshipRestricted: restrictedDocId,
relationshipWithTitle: relationsWithTitle[0],
},
})
await mapAsync([...Array(11)], async () => {
for (let i = 0; i < 11; i++) {
await payload.create({
collection: slug,
depth: 0,
overrideAccess: true,
data: {
relationship: relationOneDocId,
relationshipHasManyMultiple: relationOneIDs.map((id) => ({
@@ -393,9 +419,9 @@ export default buildConfigWithDefaults({
relationshipRestricted: restrictedDocId,
},
})
})
}
await mapAsync([...Array(15)], async () => {
for (let i = 0; i < 15; i++) {
const relationOneID = relationOneIDs[Math.floor(Math.random() * 10)]
const relationTwoID = relationTwoIDs[Math.floor(Math.random() * 10)]
await payload.create({
@@ -408,20 +434,25 @@ export default buildConfigWithDefaults({
relationshipRestricted: restrictedDocId,
},
})
})
;[...Array(15)].forEach((_, i) => {
payload.create({
}
for (let i = 0; i < 15; i++) {
await payload.create({
collection: collection1Slug,
depth: 0,
overrideAccess: true,
data: {
name: `relationship-test ${i}`,
},
})
payload.create({
await payload.create({
collection: collection2Slug,
depth: 0,
overrideAccess: true,
data: {
name: `relationship-test ${i}`,
},
})
})
}
},
})

View File

@@ -3,11 +3,13 @@ import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import type {
Collection1,
FieldsRelationship as CollectionWithRelationships,
RelationOne,
RelationRestricted,
RelationTwo,
RelationWithTitle,
VersionedRelationshipField,
} from './payload-types'
import payload from '../../packages/payload/src'
@@ -16,6 +18,7 @@ import { initPageConsoleErrorCatch, openDocControls, saveDocAndAssert } from '..
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers'
import {
collection1Slug,
relationFalseFilterOptionSlug,
relationOneSlug,
relationRestrictedSlug,
@@ -24,13 +27,16 @@ import {
relationUpdatedExternallySlug,
relationWithTitleSlug,
slug,
versionedRelationshipFieldSlug,
} from './collectionSlugs'
const { beforeAll, beforeEach, describe } = test
describe('fields - relationship', () => {
let url: AdminUrlUtil
let versionedRelationshipFieldURL: AdminUrlUtil
let page: Page
let collectionOneDoc: Collection1
let relationOneDoc: RelationOne
let anotherRelationOneDoc: RelationOne
let relationTwoDoc: RelationTwo
@@ -45,6 +51,7 @@ describe('fields - relationship', () => {
serverURL = serverURLFromConfig
url = new AdminUrlUtil(serverURL, slug)
versionedRelationshipFieldURL = new AdminUrlUtil(serverURL, versionedRelationshipFieldSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -107,6 +114,14 @@ describe('fields - relationship', () => {
},
})
// Collection 1 Doc
collectionOneDoc = (await payload.create({
collection: collection1Slug,
data: {
name: 'One',
},
})) as any
// Add restricted doc as relation
docWithExistingRelations = (await payload.create({
collection: slug,
@@ -120,6 +135,8 @@ describe('fields - relationship', () => {
})) as any
})
const tableRowLocator = 'table > tbody > tr'
test('should create relationship', async () => {
await page.goto(url.create)
@@ -464,6 +481,42 @@ describe('fields - relationship', () => {
).toHaveCount(1)
})
test('should allow filtering by polymorphic relationships with version drafts enabled', async () => {
await createVersionedRelationshipFieldDoc('Without relationship')
await createVersionedRelationshipFieldDoc('with relationship', [
{
value: collectionOneDoc.id,
relationTo: collection1Slug,
},
])
await page.goto(versionedRelationshipFieldURL.list)
await page.locator('.list-controls__toggle-columns').click()
await page.locator('.list-controls__toggle-where').click()
await page.waitForSelector('.list-controls__where.rah-static--height-auto')
await page.locator('.where-builder__add-first-filter').click()
const conditionField = page.locator('.condition__field')
await conditionField.click()
const dropdownFieldOptions = conditionField.locator('.rs__option')
await dropdownFieldOptions.locator('text=Relationship Field').nth(0).click()
const operatorField = page.locator('.condition__operator')
await operatorField.click()
const dropdownOperatorOptions = operatorField.locator('.rs__option')
await dropdownOperatorOptions.locator('text=exists').click()
const valueField = page.locator('.condition__value')
await valueField.click()
const dropdownValueOptions = valueField.locator('.rs__option')
await dropdownValueOptions.locator('text=True').click()
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})
describe('existing relationships', () => {
test('should highlight existing relationship', async () => {
await page.goto(url.edit(docWithExistingRelations.id))
@@ -580,6 +633,7 @@ async function clearAllDocs(): Promise<void> {
await clearCollectionDocs(relationTwoSlug)
await clearCollectionDocs(relationRestrictedSlug)
await clearCollectionDocs(relationWithTitleSlug)
await clearCollectionDocs(versionedRelationshipFieldSlug)
}
async function clearCollectionDocs(collectionSlug: string): Promise<void> {
@@ -590,3 +644,18 @@ async function clearCollectionDocs(collectionSlug: string): Promise<void> {
},
})
}
async function createVersionedRelationshipFieldDoc(
title: VersionedRelationshipField['title'],
relationshipField?: VersionedRelationshipField['relationshipField'],
overrides?: Partial<VersionedRelationshipField>,
): Promise<VersionedRelationshipField> {
return payload.create({
collection: versionedRelationshipFieldSlug,
data: {
title,
relationshipField,
...overrides,
},
}) as unknown as Promise<VersionedRelationshipField>
}

View File

@@ -0,0 +1,112 @@
import type { Collection1 } from './payload-types'
import payload from '../../packages/payload/src'
import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers'
import { collection1Slug, versionedRelationshipFieldSlug } from './collectionSlugs'
let apiUrl: string
let jwt
const headers = {
'Content-Type': 'application/json',
}
const { email, password } = devUser
describe('Relationship Fields', () => {
beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
apiUrl = `${serverURL}/api`
const response = await fetch(`${apiUrl}/users/login`, {
body: JSON.stringify({
email,
password,
}),
headers,
method: 'post',
})
const data = await response.json()
jwt = data.token
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy(payload)
}
})
describe('Versioned Relationship Field', () => {
let version2ID: string
const relatedDocName = 'Related Doc'
beforeAll(async () => {
const relatedDoc = await payload.create({
collection: collection1Slug,
data: {
name: relatedDocName,
},
})
const version1 = await payload.create({
collection: versionedRelationshipFieldSlug,
data: {
title: 'Version 1 Title',
relationshipField: {
value: relatedDoc.id,
relationTo: collection1Slug,
},
},
})
const version2 = await payload.update({
collection: versionedRelationshipFieldSlug,
id: version1.id,
data: {
title: 'Version 2 Title',
},
})
const versions = await payload.findVersions({
collection: versionedRelationshipFieldSlug,
where: {
parent: {
equals: version2.id,
},
},
sort: '-updatedAt',
limit: 1,
})
version2ID = versions.docs[0].id
})
it('should return the correct versioned relationship field via REST', async () => {
const version2Data = await fetch(
`${apiUrl}/${versionedRelationshipFieldSlug}/versions/${version2ID}?locale=all`,
{
method: 'GET',
headers: {
...headers,
Authorization: `JWT ${jwt}`,
},
},
).then((res) => res.json())
expect(version2Data.version.title).toEqual('Version 2 Title')
expect(version2Data.version.relationshipField[0].value.name).toEqual(relatedDocName)
})
it('should return the correct versioned relationship field via LocalAPI', async () => {
const version2Data = await payload.findVersionByID({
collection: versionedRelationshipFieldSlug,
id: version2ID,
locale: 'all',
})
expect(version2Data.version.title).toEqual('Version 2 Title')
expect((version2Data.version.relationshipField[0].value as Collection1).name).toEqual(
relatedDocName,
)
})
})
})

View File

@@ -16,11 +16,19 @@ export interface Config {
'relation-updated-externally': RelationUpdatedExternally
'collection-1': Collection1
'collection-2': Collection2
'versioned-relationship-field': VersionedRelationshipField
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
db: {
defaultIDType: string
}
globals: {}
locale: 'en'
user: User & {
collection: 'users'
}
}
export interface FieldsRelationship {
id: string
@@ -126,6 +134,22 @@ export interface Collection2 {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "versioned-relationship-field".
*/
export interface VersionedRelationshipField {
id: string
title: string
relationshipField?:
| {
relationTo: 'collection-1'
value: string | Collection1
}[]
| null
updatedAt: string
createdAt: string
}
export interface User {
id: string
updatedAt: string

View File

@@ -31,6 +31,18 @@ const RelationshipFields: CollectionConfig = {
},
},
},
{
name: 'relationNoHasManyNonPolymorphic',
relationTo: 'text-fields',
type: 'relationship',
hasMany: false,
},
{
name: 'relationHasManyNonPolymorphic',
relationTo: 'text-fields',
type: 'relationship',
hasMany: true,
},
{
name: 'relationToSelf',
relationTo: relationshipFieldsSlug,

View File

@@ -244,9 +244,6 @@ describe('Fields', () => {
},
})
const anyChildren = await payload.find({
collection: relationshipFieldsSlug,
})
const allChildren = await payload.find({
collection: relationshipFieldsSlug,
where: {
@@ -1557,7 +1554,47 @@ describe('Fields', () => {
})
})
describe('relationships', () => {
describe('relationship queries', () => {
let textDoc
let otherTextDoc
let relationshipDocWithNonPolyOne
let relationshipDocWithNonPolyTwo
const textDocText = 'text document'
const otherTextDocText = 'alt text'
beforeEach(async () => {
textDoc = await payload.create({
collection: 'text-fields',
data: {
text: textDocText,
},
})
otherTextDoc = await payload.create({
collection: 'text-fields',
data: {
text: otherTextDocText,
},
})
const relationship = { relationTo: 'text-fields', value: textDoc.id }
relationshipDocWithNonPolyOne = await payload.create({
collection: relationshipFieldsSlug,
data: {
relationship,
relationNoHasManyNonPolymorphic: textDoc.id,
relationHasManyNonPolymorphic: textDoc.id,
},
})
relationshipDocWithNonPolyTwo = await payload.create({
collection: relationshipFieldsSlug,
data: {
relationship,
relationNoHasManyNonPolymorphic: otherTextDoc.id,
relationHasManyNonPolymorphic: otherTextDoc.id,
},
})
})
it('should not crash if querying with empty in operator', async () => {
const query = await payload.find({
collection: 'relationship-fields',
@@ -1570,6 +1607,25 @@ describe('Fields', () => {
expect(query.docs).toBeDefined()
})
it('should properly query non-polymorphic relationship with not equals', async () => {
const withoutHasMany = await payload.find({
collection: relationshipFieldsSlug,
where: {
relationNoHasManyNonPolymorphic: { not_equals: otherTextDoc.id },
},
})
const withHasMany = await payload.find({
collection: relationshipFieldsSlug,
where: {
relationHasManyNonPolymorphic: { not_equals: textDoc.id },
},
})
expect(withoutHasMany.docs).toHaveLength(1)
expect(withHasMany.docs).toHaveLength(1)
})
})
describe('clearable fields - exists', () => {

View File

@@ -624,6 +624,8 @@ export interface TextField {
text: string
localizedText?: string | null
i18nText?: string | null
defaultString?: string | null
defaultEmptyString?: string | null
defaultFunction?: string | null
defaultAsync?: string | null
overrideLength?: string | null
@@ -864,6 +866,17 @@ export interface JsonField {
| number
| boolean
| null
group?: {
jsonWithinGroup?:
| {
[k: string]: unknown
}
| unknown[]
| string
| number
| boolean
| null
}
updatedAt: string
createdAt: string
}
@@ -942,6 +955,8 @@ export interface RelationshipField {
}
)[]
| null
relationNoHasManyNonPolymorphic?: (string | null) | TextField
relationHasManyNonPolymorphic?: (string | TextField)[] | null
relationToSelf?: (string | null) | RelationshipField
relationToSelfSelectOnly?: (string | null) | RelationshipField
relationWithDynamicDefault?: (string | null) | User

View File

@@ -11,11 +11,15 @@ import {
animatedTypeMedia,
audioSlug,
cropOnlySlug,
customFileNameMediaSlug,
enlargeSlug,
focalOnlySlug,
globalWithMedia,
mediaSlug,
mediaWithRelationPreviewSlug,
mediaWithoutRelationPreviewSlug,
reduceSlug,
relationPreviewSlug,
relationSlug,
versionSlug,
} from './shared'
@@ -200,6 +204,23 @@ export default buildConfigWithDefaults({
},
},
},
{
slug: customFileNameMediaSlug,
fields: [],
upload: {
imageSizes: [
{
name: 'custom',
height: 500,
width: 500,
generateImageName: ({ extension, height, width, sizeName }) =>
`${sizeName}-${width}x${height}.${extension}`,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: `./${customFileNameMediaSlug}`,
},
},
{
slug: cropOnlySlug,
fields: [],
@@ -583,6 +604,67 @@ export default buildConfigWithDefaults({
drafts: true,
},
},
{
slug: mediaWithRelationPreviewSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
upload: {
displayPreview: true,
},
},
{
slug: mediaWithoutRelationPreviewSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
upload: true,
},
{
slug: relationPreviewSlug,
fields: [
{
name: 'imageWithPreview1',
type: 'upload',
relationTo: mediaWithRelationPreviewSlug,
},
{
name: 'imageWithPreview2',
type: 'upload',
relationTo: mediaWithRelationPreviewSlug,
displayPreview: true,
},
{
name: 'imageWithoutPreview1',
type: 'upload',
relationTo: mediaWithRelationPreviewSlug,
displayPreview: false,
},
{
name: 'imageWithoutPreview2',
type: 'upload',
relationTo: mediaWithoutRelationPreviewSlug,
},
{
name: 'imageWithPreview3',
type: 'upload',
relationTo: mediaWithoutRelationPreviewSlug,
displayPreview: true,
},
{
name: 'imageWithoutPreview3',
type: 'upload',
relationTo: mediaWithoutRelationPreviewSlug,
displayPreview: false,
},
],
},
],
globals: [
{
@@ -707,6 +789,31 @@ export default buildConfigWithDefaults({
name: `thumb-${imageFile.name}`,
},
})
// Create media with and without relation preview
const { id: uploadedImageWithPreview } = await payload.create({
collection: mediaWithRelationPreviewSlug,
data: {},
file: imageFile,
})
const { id: uploadedImageWithoutPreview } = await payload.create({
collection: mediaWithoutRelationPreviewSlug,
data: {},
file: imageFile,
})
await payload.create({
collection: relationPreviewSlug,
data: {
imageWithPreview1: uploadedImageWithPreview,
imageWithPreview2: uploadedImageWithPreview,
imageWithoutPreview1: uploadedImageWithPreview,
imageWithoutPreview2: uploadedImageWithoutPreview,
imageWithPreview3: uploadedImageWithoutPreview,
imageWithoutPreview3: uploadedImageWithoutPreview,
},
})
},
serverURL: undefined,
})

View File

@@ -7,7 +7,7 @@ import type { Media } from './payload-types'
import payload from '../../packages/payload/src'
import wait from '../../packages/payload/src/utilities/wait'
import { initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers'
import { exactText, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers'
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers'
import { RESTClient } from '../helpers/rest'
@@ -19,10 +19,12 @@ import {
focalOnlySlug,
globalWithMedia,
mediaSlug,
relationPreviewSlug,
relationSlug,
withMetadataSlug,
withOnlyJPEGMetadataSlug,
withoutMetadataSlug,
customFileNameMediaSlug,
} from './shared'
const { beforeAll, describe } = test
@@ -38,6 +40,8 @@ let focalOnlyURL: AdminUrlUtil
let withMetadataURL: AdminUrlUtil
let withoutMetadataURL: AdminUrlUtil
let withOnlyJPEGMetadataURL: AdminUrlUtil
let relationPreviewURL: AdminUrlUtil
let customFileNameURL: AdminUrlUtil
describe('uploads', () => {
let page: Page
@@ -59,6 +63,8 @@ describe('uploads', () => {
withMetadataURL = new AdminUrlUtil(serverURL, withMetadataSlug)
withoutMetadataURL = new AdminUrlUtil(serverURL, withoutMetadataSlug)
withOnlyJPEGMetadataURL = new AdminUrlUtil(serverURL, withOnlyJPEGMetadataSlug)
relationPreviewURL = new AdminUrlUtil(serverURL, relationPreviewSlug)
customFileNameURL = new AdminUrlUtil(serverURL, customFileNameMediaSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -424,6 +430,25 @@ describe('uploads', () => {
expect(webpMediaDoc.sizes.sizeThree.filesize).toEqual(211638)
})
test('should have custom file name for image size', async () => {
await page.goto(customFileNameURL.create)
await page.setInputFiles('input[type="file"]', path.resolve(__dirname, './image.png'))
await expect(page.locator('.file-field__upload .thumbnail img')).toBeVisible()
await saveDocAndAssert(page)
await expect(page.locator('.file-details img')).toBeVisible()
await page.locator('.file-field__previewSizes').click()
const renamedImageSizeFile = page
.locator('.preview-sizes__list .preview-sizes__sizeOption')
.nth(1)
await expect(renamedImageSizeFile).toContainText('custom-500x500.png')
})
describe('image manipulation', () => {
test('should crop image correctly', async () => {
const positions = {
@@ -536,6 +561,50 @@ describe('uploads', () => {
})
})
test('should see upload previews in relation list if allowed in config', async () => {
await page.goto(relationPreviewURL.list)
await wait(110)
// Show all columns with relations
await page.locator('.list-controls__toggle-columns').click()
await expect(page.locator('.column-selector')).toBeVisible()
const imageWithoutPreview2Button = page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Image Without Preview2'),
})
const imageWithPreview3Button = page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Image With Preview3'),
})
const imageWithoutPreview3Button = page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Image Without Preview3'),
})
await imageWithoutPreview2Button.click()
await imageWithPreview3Button.click()
await imageWithoutPreview3Button.click()
// Wait for the columns to be displayed
await expect(page.locator('.cell-imageWithoutPreview3')).toBeVisible()
// collection's displayPreview: true, field's displayPreview: unset
const relationPreview1 = page.locator('.cell-imageWithPreview1 img')
await expect(relationPreview1).toBeVisible()
// collection's displayPreview: true, field's displayPreview: true
const relationPreview2 = page.locator('.cell-imageWithPreview2 img')
await expect(relationPreview2).toBeVisible()
// collection's displayPreview: true, field's displayPreview: false
const relationPreview3 = page.locator('.cell-imageWithoutPreview1 img')
await expect(relationPreview3).toBeHidden()
// collection's displayPreview: false, field's displayPreview: unset
const relationPreview4 = page.locator('.cell-imageWithoutPreview2 img')
await expect(relationPreview4).toBeHidden()
// collection's displayPreview: false, field's displayPreview: true
const relationPreview5 = page.locator('.cell-imageWithPreview3 img')
await expect(relationPreview5).toBeVisible()
// collection's displayPreview: false, field's displayPreview: false
const relationPreview6 = page.locator('.cell-imageWithoutPreview3 img')
await expect(relationPreview6).toBeHidden()
})
describe('globals', () => {
test('should be able to crop media from a global', async () => {
await page.goto(globalURL)

View File

@@ -6,9 +6,13 @@ export const focalOnlySlug = 'focal-only'
export const mediaSlug = 'media'
export const reduceSlug = 'reduce'
export const relationSlug = 'relation'
export const relationPreviewSlug = 'relation-preview'
export const mediaWithRelationPreviewSlug = 'media-with-relation-preview'
export const mediaWithoutRelationPreviewSlug = 'media-without-relation-preview'
export const versionSlug = 'versions'
export const globalWithMedia = 'global-with-media'
export const animatedTypeMedia = 'animated-type-media'
export const withMetadataSlug = 'with-meta-data'
export const withoutMetadataSlug = 'without-meta-data'
export const withOnlyJPEGMetadataSlug = 'with-only-jpeg-meta-data'
export const customFileNameMediaSlug = 'custom-file-name-media'