Compare commits
31 Commits
payload/2.
...
payload/2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab9074220a | ||
|
|
afa90a4362 | ||
|
|
bc0516da90 | ||
|
|
46daf473c8 | ||
|
|
337b8ccbf3 | ||
|
|
ba2e4c278f | ||
|
|
3196036ae9 | ||
|
|
9bc3ad5159 | ||
|
|
94d18e8d74 | ||
|
|
c624eea0d8 | ||
|
|
f97627092c | ||
|
|
f00183029e | ||
|
|
b6c5aaa966 | ||
|
|
517aaa0665 | ||
|
|
2c2ffe406f | ||
|
|
7f39afa192 | ||
|
|
fc4d24aa88 | ||
|
|
efa56cefc1 | ||
|
|
907d7d1d3a | ||
|
|
eca1517237 | ||
|
|
9865ae998b | ||
|
|
1a0ef4824b | ||
|
|
39e110e633 | ||
|
|
3e780b9815 | ||
|
|
a308d6384f | ||
|
|
492ed30cb8 | ||
|
|
fca5a404db | ||
|
|
b13f7e8843 | ||
|
|
25dfdb66cd | ||
|
|
9c9e6896a5 | ||
|
|
a3085435ef |
47
.github/dependabot.yml
vendored
Normal file
47
.github/dependabot.yml
vendored
Normal 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:
|
||||
- '*'
|
||||
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.has-many {
|
||||
.rs__input-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='light'] {
|
||||
.field-type.text {
|
||||
&.error {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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} />)}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": "Trenutačni nacrt",
|
||||
"currentPublishedVersion": "Trenutačno objavljena verzija",
|
||||
"currentDraft": "Trenutni nacrt",
|
||||
"currentPublishedVersion": "Trenutno objavljena verzija",
|
||||
"draft": "Nacrt",
|
||||
"draftSavedSuccessfully": "Nacrt uspješno spremljen.",
|
||||
"lastSavedAgo": "Zadnji put spremljeno prije {{distance}",
|
||||
|
||||
@@ -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}`
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import { mimeTypeValidator } from './mimeTypeValidator'
|
||||
const options = { siblingData: { filename: 'file.xyz' } } as ValidateOptions<
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
undefined,
|
||||
string
|
||||
>
|
||||
|
||||
describe('mimeTypeValidator', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
22726
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
9
test/_community/collections/Users/index.ts
Normal file
9
test/_community/collections/Users/index.ts
Normal 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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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}`,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
112
test/fields-relationship/int.spec.ts
Normal file
112
test/fields-relationship/int.spec.ts
Normal 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,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user