Compare commits
52 Commits
v2.24.1
...
plugin-clo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aca567634b | ||
|
|
1f0934877c | ||
|
|
61da010991 | ||
|
|
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 | ||
|
|
1466657e8f | ||
|
|
1348483648 | ||
|
|
5321098d7e | ||
|
|
c57591bc4f | ||
|
|
9750bc217e | ||
|
|
468e5441f1 | ||
|
|
3c5cce4c6f | ||
|
|
9f0f94893d | ||
|
|
03b7892fc9 | ||
|
|
f96cf593ce | ||
|
|
8259611ce6 | ||
|
|
a3ed25a253 | ||
|
|
69e7b7a158 | ||
|
|
c6da99b4d1 | ||
|
|
ebd23caa56 | ||
|
|
faa9b21824 | ||
|
|
1690560f11 | ||
|
|
0058660b3f |
31
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
31
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -9,43 +9,38 @@ body:
|
||||
description: Want us to look into your issue faster? Follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: version
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Payload Version
|
||||
description: What version of Payload are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: Node Version
|
||||
description: What version of Node are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: nextjs-version
|
||||
attributes:
|
||||
label: Next.js Version
|
||||
description: What version of Next.js are you running?
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` (>= beta.92) _or_ Payload, Node.js, and Next.js versions.
|
||||
render: text
|
||||
placeholder: |
|
||||
Payload:
|
||||
Node.js:
|
||||
Next.js:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: adapters-plugins
|
||||
attributes:
|
||||
label: Adapters and Plugins
|
||||
description: What adapters and plugins are you using if relevant? ie. db-mongodb, db-postgres, storage-vercel-blob, etc.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
|
||||
|
||||
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
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -1,3 +1,69 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allows metadata to be appended to the file of the output media ([#7295](https://github.com/payloadcms/payload/issues/7295)) ([3c5cce4](https://github.com/payloadcms/payload/commit/3c5cce4c6f108f87e87b091bbfec976423de73a2))
|
||||
* **db-mongodb:** adds new optional `collation` feature flag behind mongodb collation option ([#7359](https://github.com/payloadcms/payload/issues/7359)) ([9750bc2](https://github.com/payloadcms/payload/commit/9750bc217ee7d63732a34908c84eb88b88dac0a8)), closes [#7349](https://github.com/payloadcms/payload/issues/7349)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly handles `0` value number fields in list view ([#7364](https://github.com/payloadcms/payload/issues/7364)) ([5321098](https://github.com/payloadcms/payload/commit/5321098d7eada43838f6d5c69f3233c150fe0afa)), closes [#5510](https://github.com/payloadcms/payload/issues/5510)
|
||||
* preserves objectids in deepCopyObject ([#7385](https://github.com/payloadcms/payload/issues/7385)) ([1348483](https://github.com/payloadcms/payload/commit/134848364801c72cc773ef7b48854306d1b9bac3))
|
||||
* relaxes equality check for relationship options in filter ([#7344](https://github.com/payloadcms/payload/issues/7344)) ([468e544](https://github.com/payloadcms/payload/commit/468e5441f16775134d915ec7caddb17b817d3408))
|
||||
* supports null values in query strings ([#5241](https://github.com/payloadcms/payload/issues/5241)) ([c57591b](https://github.com/payloadcms/payload/commit/c57591bc4fb8d28b7de16a111faffea7d3e11f8d))
|
||||
|
||||
## [2.24.2](https://github.com/payloadcms/payload/compare/v2.24.1...v2.24.2) (2024-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **db-mongodb:** add jsonParse flag to mongooseAdapter that preserves existing, untracked MongoDB data types ([#7338](https://github.com/payloadcms/payload/issues/7338)) ([f96cf59](https://github.com/payloadcms/payload/commit/f96cf593cedcae0d8ed55f9a70e8e4e77917a876))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow autosave relationship drawers to function properly ([#7325](https://github.com/payloadcms/payload/issues/7325)) ([69e7b7a](https://github.com/payloadcms/payload/commit/69e7b7a158c38058ece54a97bfa79e65192774a6))
|
||||
* **db-mongodb:** removes precedence of regular chars over international chars in sort ([#6923](https://github.com/payloadcms/payload/issues/6923)) ([0058660](https://github.com/payloadcms/payload/commit/0058660b3f8bd820abb4494ff53fa67f49f0f6b4)), closes [#6719](https://github.com/payloadcms/payload/issues/6719)
|
||||
* fetches and sets permissions before setting user ([#7337](https://github.com/payloadcms/payload/issues/7337)) ([8259611](https://github.com/payloadcms/payload/commit/8259611ce60e23f6298a07564d5f6dd2966d61ff))
|
||||
* **plugin-stripe:** properly types async webhooks ([#7316](https://github.com/payloadcms/payload/issues/7316)) ([c6da99b](https://github.com/payloadcms/payload/commit/c6da99b4d1b986089bb697486a7825db66323078))
|
||||
|
||||
## [2.24.1](https://github.com/payloadcms/payload/compare/v2.24.0...v2.24.1) (2024-07-22)
|
||||
|
||||
|
||||
|
||||
@@ -30,16 +30,18 @@ export default buildConfig({
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `schemaOptions` | Customize schema options for all Mongoose schemas created internally. |
|
||||
| `collections` | Options on a collection-by-collection basis. [More](#collections-options) |
|
||||
| `globals` | Options for the Globals collection created by Payload. [More](#globals-options) |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. | |
|
||||
| Option | Description |
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `schemaOptions` | Customize schema options for all Mongoose schemas created internally. |
|
||||
| `jsonParse` | Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. |
|
||||
| `collections` | Options on a collection-by-collection basis. [More](#collections-options) |
|
||||
| `globals` | Options for the Globals collection created by Payload. [More](#globals-options) |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
|
||||
### Access to Mongoose models
|
||||
|
||||
@@ -63,7 +65,7 @@ const db = mongooseAdapter({
|
||||
url: 'your-url-here',
|
||||
collections: {
|
||||
users: {
|
||||
//
|
||||
//
|
||||
schemaOptions: {
|
||||
strict: false,
|
||||
}
|
||||
@@ -74,4 +76,28 @@ const db = mongooseAdapter({
|
||||
|
||||
### Global Options
|
||||
|
||||
Payload automatically creates a single `globals` collection that correspond with any Payload globals that you define. When you initialize the `mongooseAdapter`, you can specify settings here for your globals in a similar manner to how you can for collections above. Right now, the only property available is `schemaOptions` but more may be added in the future.
|
||||
Payload automatically creates a single `globals` collection that correspond with any Payload globals that you define. When you initialize the `mongooseAdapter`, you can specify settings here for your globals in a similar manner to how you can for collections above. Right now, the only property available is `schemaOptions` but more may be added in the future.
|
||||
|
||||
### Preserving externally managed data
|
||||
|
||||
You can use Payload in conjunction with an existing MongoDB database, where you might have some fields "tracked" in Payload via corresponding field configs, and other fields completely unknown to Payload.
|
||||
|
||||
If you have external field data in existing MongoDB collections which you'd like to use in combination with Payload, and you don't want to lose those external fields, you can configure Payload to "preserve" that data while it makes updates to your existing documents.
|
||||
|
||||
To do this, the first step is to configure Mongoose's `strict` property, which tells Mongoose to write all data that it receives (and not disregard any data that it does not know about).
|
||||
|
||||
The second step is to disable Payload's automatic JSON parsing of documents it receives from MongoDB.
|
||||
|
||||
Here's an example for how to configure your Mongoose adapter to preserve external collection fields that are not tracked by Payload:
|
||||
|
||||
```ts
|
||||
mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
// Disable the JSON parsing that Payload performs
|
||||
jsonParse: false,
|
||||
// Disable strict mode for Mongoose
|
||||
schemaOptions: {
|
||||
strict: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -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) |
|
||||
@@ -56,6 +57,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
|
||||
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
|
||||
|
||||
_An asterisk denotes that a property above is required._
|
||||
|
||||
@@ -166,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.
|
||||
|
||||
@@ -481,11 +481,11 @@ brace-expansion@^1.1.7:
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
fill-range "^7.1.1"
|
||||
|
||||
busboy@1.6.0:
|
||||
version "1.6.0"
|
||||
@@ -1071,10 +1071,10 @@ file-entry-cache@^6.0.1:
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -8304,9 +8304,9 @@ wrappy@1:
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
ws@^7.3.1:
|
||||
version "7.5.9"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
|
||||
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
||||
version "7.5.10"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
xss@^1.0.6:
|
||||
version "1.0.14"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5141,9 +5141,9 @@ node-releases@^2.0.12:
|
||||
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
|
||||
|
||||
nodemailer@^6.9.0:
|
||||
version "6.9.4"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.4.tgz#93bd4a60eb0be6fa088a0483340551ebabfd2abf"
|
||||
integrity sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==
|
||||
version "6.9.14"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75"
|
||||
integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==
|
||||
|
||||
nodemon@^2.0.6:
|
||||
version "2.0.22"
|
||||
|
||||
@@ -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.6.0",
|
||||
"version": "1.7.2",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Create } from 'payload/database'
|
||||
import type { Document, PayloadRequest } from 'payload/types'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import handleError from './utilities/handleError'
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const create: Create = async function create(
|
||||
@@ -19,15 +20,13 @@ export const create: Create = async function create(
|
||||
handleError(error, req)
|
||||
}
|
||||
|
||||
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -19,10 +19,8 @@ export const createGlobal: CreateGlobal = async function createGlobal(
|
||||
|
||||
let [result] = (await Model.create([global], options)) as any
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result.toObject()
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CreateGlobalVersion } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
|
||||
@@ -52,13 +52,12 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CreateVersion } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const createVersion: CreateVersion = async function createVersion(
|
||||
@@ -60,13 +60,13 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { DeleteOne } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
@@ -19,13 +18,9 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOneAndDelete(query, options).lean()
|
||||
let doc = await Model.findOneAndDelete(query, options).lean()
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,14 @@ export const find: Find = async function find(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -82,13 +90,12 @@ export const find: Find = async function find(
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -30,12 +30,16 @@ export const findGlobal: FindGlobal = async function findGlobal(
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.jsonParse) {
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
}
|
||||
|
||||
if (doc._id) {
|
||||
doc.id = doc._id
|
||||
doc.id = JSON.parse(JSON.stringify(doc._id))
|
||||
delete doc._id
|
||||
}
|
||||
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
doc = sanitizeInternalFields(doc)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -74,6 +74,14 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -101,13 +109,12 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { MongooseQueryOptions } from 'mongoose'
|
||||
import type { FindOne } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
@@ -24,17 +23,15 @@ export const findOne: FindOne = async function findOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOne(query, {}, options)
|
||||
let doc = await Model.findOne(query, {}, options)
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
doc = sanitizeInternalFields(doc)
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -70,6 +70,14 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -97,13 +105,12 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TransactionOptions } from 'mongodb'
|
||||
import type { CollationOptions, TransactionOptions } from 'mongodb'
|
||||
import type { ClientSession, ConnectOptions, Connection, SchemaOptions } from 'mongoose'
|
||||
import type { Payload } from 'payload'
|
||||
import type { BaseDatabaseAdapter } from 'payload/database'
|
||||
@@ -42,6 +42,30 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types'
|
||||
export interface Args {
|
||||
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
|
||||
autoPluralization?: boolean
|
||||
/**
|
||||
* If enabled, collation allows for language-specific rules for string comparison.
|
||||
* This configuration can include the following options:
|
||||
*
|
||||
* - `strength` (number): Comparison level (1: Primary, 2: Secondary, 3: Tertiary (default), 4: Quaternary, 5: Identical)
|
||||
* - `caseLevel` (boolean): Include case comparison at strength level 1 or 2.
|
||||
* - `caseFirst` (string): Sort order of case differences during tertiary level comparisons ("upper", "lower", "off").
|
||||
* - `numericOrdering` (boolean): Compare numeric strings as numbers.
|
||||
* - `alternate` (string): Consider whitespace and punctuation as base characters ("non-ignorable", "shifted").
|
||||
* - `maxVariable` (string): Characters considered ignorable when `alternate` is "shifted" ("punct", "space").
|
||||
* - `backwards` (boolean): Sort strings with diacritics from back of the string.
|
||||
* - `normalization` (boolean): Check if text requires normalization and perform normalization.
|
||||
*
|
||||
* Available on MongoDB version 3.4 and up.
|
||||
* The locale that gets passed is your current project's locale but defaults to "en".
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* strength: 3
|
||||
* }
|
||||
*
|
||||
* Defaults to disabled.
|
||||
*/
|
||||
collation?: Omit<CollationOptions, 'locale'>
|
||||
/** Define Mongoose options on a collection-by-collection basis.
|
||||
*/
|
||||
collections?: {
|
||||
@@ -56,6 +80,7 @@ export interface Args {
|
||||
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
|
||||
useFacet?: boolean
|
||||
}
|
||||
|
||||
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
|
||||
disableIndexHints?: boolean
|
||||
/** Define Mongoose options for the globals collection.
|
||||
@@ -63,6 +88,8 @@ export interface Args {
|
||||
globals?: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
/** Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. */
|
||||
jsonParse?: boolean
|
||||
migrationDir?: string
|
||||
/** Define default Mongoose schema options for all schemas created.
|
||||
*/
|
||||
@@ -87,6 +114,7 @@ export type MongooseAdapter = BaseDatabaseAdapter &
|
||||
globalsOptions: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
jsonParse: boolean
|
||||
mongoMemoryServer: any
|
||||
schemaOptions?: SchemaOptions
|
||||
sessions: Record<number | string, ClientSession>
|
||||
@@ -114,6 +142,7 @@ declare module 'payload' {
|
||||
globalsOptions: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
jsonParse: boolean
|
||||
mongoMemoryServer: any
|
||||
schemaOptions?: SchemaOptions
|
||||
|
||||
@@ -131,6 +160,7 @@ export function mongooseAdapter({
|
||||
connectOptions,
|
||||
disableIndexHints = false,
|
||||
globals,
|
||||
jsonParse = true,
|
||||
migrationDir: migrationDirArg,
|
||||
schemaOptions,
|
||||
transactionOptions = {},
|
||||
@@ -153,6 +183,7 @@ export function mongooseAdapter({
|
||||
disableIndexHints,
|
||||
globals: undefined,
|
||||
globalsOptions: globals || {},
|
||||
jsonParse,
|
||||
mongoMemoryServer: undefined,
|
||||
schemaOptions: schemaOptions || {},
|
||||
sessions: {},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!useEstimatedCount &&
|
||||
Object.keys(versionQuery).length === 0 &&
|
||||
@@ -83,12 +91,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
}
|
||||
|
||||
const result = await VersionModel.paginate(versionQuery, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
||||
doc = {
|
||||
_id: doc.parent,
|
||||
id: doc.parent,
|
||||
|
||||
@@ -21,10 +21,8 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
|
||||
result = await Model.findOneAndUpdate({ globalType: slug }, data, options)
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
@@ -30,16 +31,14 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
let doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
doc._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
handleError(error, req)
|
||||
}
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
@@ -23,16 +24,14 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
let doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
doc._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const internalFields = ['__v']
|
||||
|
||||
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T =>
|
||||
Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
|
||||
if (key === '_id') {
|
||||
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T => {
|
||||
const id = incomingDoc._id ? JSON.parse(JSON.stringify(incomingDoc._id)) : incomingDoc.id
|
||||
|
||||
return Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
|
||||
if (key === '_id' || key === 'id') {
|
||||
return {
|
||||
...newDoc,
|
||||
id: val,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +20,6 @@ const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc:
|
||||
[key]: val,
|
||||
}
|
||||
}, {} as T)
|
||||
}
|
||||
|
||||
export default sanitizeInternalFields
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.24.1",
|
||||
"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",
|
||||
|
||||
@@ -17,11 +17,12 @@ import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues'
|
||||
import { reduceFieldsToValuesWithValidation } from '../../forms/Form/reduceFieldsToValuesWithValidation'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
||||
import { useEditDepth } from '../../utilities/EditDepth'
|
||||
import { useLocale } from '../../utilities/Locale'
|
||||
import './index.scss'
|
||||
const baseClass = 'autosave'
|
||||
|
||||
const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdatedAt }) => {
|
||||
const Autosave: React.FC<Props> = ({ id, collection, global, onSave, publishedDocUpdatedAt }) => {
|
||||
const {
|
||||
routes: { admin, api },
|
||||
serverURL,
|
||||
@@ -34,6 +35,7 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
|
||||
const { dispatchFields, setSubmitted } = useForm()
|
||||
const history = useHistory()
|
||||
const { i18n, t } = useTranslation('version')
|
||||
const depth = useEditDepth()
|
||||
|
||||
let interval = 800
|
||||
const validateDrafts =
|
||||
@@ -77,15 +79,19 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
|
||||
|
||||
if (res.status === 201) {
|
||||
const json = await res.json()
|
||||
history.replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
|
||||
state: {
|
||||
data: json.doc,
|
||||
},
|
||||
})
|
||||
if (depth === 1) {
|
||||
history.replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
|
||||
state: {
|
||||
data: json.doc,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
onSave(json)
|
||||
}
|
||||
} else {
|
||||
toast.error(t('error:autosaving'))
|
||||
}
|
||||
}, [serverURL, api, collection?.slug, locale, i18n.language, history, admin, t])
|
||||
}, [serverURL, api, collection?.slug, locale, i18n.language, history, admin, t, depth, onSave])
|
||||
|
||||
useEffect(() => {
|
||||
// If no ID, but this is used for a collection doc,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
|
||||
import type { CollectionEditViewProps } from '../../views/types'
|
||||
|
||||
export type Props = {
|
||||
collection?: SanitizedCollectionConfig
|
||||
global?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
onSave?: CollectionEditViewProps['onSave']
|
||||
publishedDocUpdatedAt: string
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { CollectionPermission, GlobalPermission } from '../../../../auth'
|
||||
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
|
||||
import type { CollectionEditViewProps } from '../../views/types'
|
||||
|
||||
import { getTranslation } from '../../../../utilities/getTranslation'
|
||||
import { formatDate } from '../../../utilities/formatDate'
|
||||
@@ -34,6 +35,7 @@ export const DocumentControls: React.FC<{
|
||||
id?: string
|
||||
isAccountView?: boolean
|
||||
isEditing?: boolean
|
||||
onSave?: CollectionEditViewProps['onSave']
|
||||
permissions?: CollectionPermission | GlobalPermission
|
||||
}> = (props) => {
|
||||
const {
|
||||
@@ -45,6 +47,7 @@ export const DocumentControls: React.FC<{
|
||||
hasSavePermission,
|
||||
isAccountView,
|
||||
isEditing,
|
||||
onSave,
|
||||
permissions,
|
||||
} = props
|
||||
|
||||
@@ -111,6 +114,7 @@ export const DocumentControls: React.FC<{
|
||||
collection={collection}
|
||||
global={global}
|
||||
id={id}
|
||||
onSave={onSave}
|
||||
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -187,7 +187,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
options.forEach((opt) => {
|
||||
if (opt.options) {
|
||||
opt.options.some((subOpt) => {
|
||||
if (subOpt?.value === val.value) {
|
||||
if (subOpt?.value == val.value) {
|
||||
matchedOption = subOpt
|
||||
return true
|
||||
}
|
||||
@@ -200,7 +200,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
return matchedOption
|
||||
}
|
||||
|
||||
return options.find((opt) => opt.value === val)
|
||||
return options.find((opt) => opt.value == val)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
options.forEach((opt) => {
|
||||
if (opt?.options) {
|
||||
opt.options.some((subOpt) => {
|
||||
if (subOpt?.value === valueWithRelation.value) {
|
||||
if (subOpt?.value == valueWithRelation.value) {
|
||||
matchedOption = subOpt
|
||||
return true
|
||||
}
|
||||
@@ -227,7 +227,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
return matchedOption
|
||||
}
|
||||
|
||||
return options.find((opt) => opt.value === value)
|
||||
return options.find((opt) => opt.value == value)
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -139,7 +139,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
})
|
||||
|
||||
if (!errorLoading) {
|
||||
relationsToFetch.reduce(async (priorRelation, relation) => {
|
||||
await relationsToFetch.reduce(async (priorRelation, relation) => {
|
||||
const relationFilterOption = filterOptionsResult?.[relation]
|
||||
let lastLoadedPageToUse
|
||||
if (search !== searchArg) {
|
||||
@@ -197,12 +197,17 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
query.where.and.push(relationFilterOption)
|
||||
}
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
const response = await fetch(
|
||||
`${serverURL}${api}/${relation}?${qs.stringify(query, {
|
||||
strictNullHandling: true,
|
||||
})}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs<unknown> = await response.json()
|
||||
@@ -269,7 +274,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const updateSearch = useDebouncedCallback((searchArg: string, valueArg: Value | Value[]) => {
|
||||
getResults({ search: searchArg, sort: true, value: valueArg })
|
||||
void getResults({ search: searchArg, sort: true, value: valueArg })
|
||||
setSearch(searchArg)
|
||||
}, 300)
|
||||
|
||||
@@ -280,7 +285,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
updateSearch(searchArg, valueArg, searchArg !== '')
|
||||
}
|
||||
},
|
||||
[search, updateSearch],
|
||||
[initialLoadedPageState, search, updateSearch],
|
||||
)
|
||||
|
||||
// ///////////////////////////////////
|
||||
@@ -294,15 +299,14 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
value,
|
||||
})
|
||||
|
||||
Object.entries(relationMap).reduce(async (priorRelation, [relation, ids]) => {
|
||||
void Object.entries(relationMap).reduce(async (priorRelation, [relation, ids]) => {
|
||||
await priorRelation
|
||||
|
||||
const idsToLoad = ids.filter((id) => {
|
||||
return !options.find(
|
||||
(optionGroup) =>
|
||||
optionGroup?.options?.find(
|
||||
(option) => option.value === id && option.relationTo === relation,
|
||||
),
|
||||
return !options.find((optionGroup) =>
|
||||
optionGroup?.options?.find(
|
||||
(option) => option.value === id && option.relationTo === relation,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -320,12 +324,17 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
if (!errorLoading) {
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
const response = await fetch(
|
||||
`${serverURL}${api}/${relation}?${qs.stringify(query, {
|
||||
strictNullHandling: true,
|
||||
})}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const collection = collections.find((coll) => coll.slug === relation)
|
||||
let docs = []
|
||||
@@ -507,7 +516,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
onMenuOpen={() => {
|
||||
if (!hasLoadedFirstPage) {
|
||||
setIsLoading(true)
|
||||
getResults({
|
||||
void getResults({
|
||||
onSuccess: () => {
|
||||
setHasLoadedFirstPage(true)
|
||||
setIsLoading(false)
|
||||
@@ -517,7 +526,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}
|
||||
}}
|
||||
onMenuScrollToBottom={() => {
|
||||
getResults({
|
||||
void getResults({
|
||||
lastFullyLoadedRelation,
|
||||
search,
|
||||
sort: false,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -71,10 +71,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
)
|
||||
|
||||
const setActiveUser = React.useCallback(
|
||||
(user: User | null) => {
|
||||
setUser(user)
|
||||
userIDRef.current = user?.id || null
|
||||
void refreshPermissions()
|
||||
async (userToSet: User | null) => {
|
||||
if ((userIDRef.current && !userToSet?.id) || userToSet?.id) {
|
||||
// refresh on logout and login
|
||||
await refreshPermissions()
|
||||
}
|
||||
userIDRef.current = userToSet?.id || null
|
||||
setUser(userToSet)
|
||||
},
|
||||
[refreshPermissions],
|
||||
)
|
||||
@@ -129,10 +132,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (request.status === 200) {
|
||||
const json = await request.json()
|
||||
setActiveUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
setTokenAndExpiration(json)
|
||||
} else {
|
||||
setActiveUser(null)
|
||||
await setActiveUser(null)
|
||||
redirectToInactivityRoute()
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -165,13 +168,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (request.status === 200) {
|
||||
const json = await request.json()
|
||||
if (!skipSetUser) {
|
||||
setActiveUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
setTokenAndExpiration(json)
|
||||
}
|
||||
return json.user
|
||||
}
|
||||
|
||||
setActiveUser(null)
|
||||
await setActiveUser(null)
|
||||
redirectToInactivityRoute()
|
||||
return null
|
||||
} catch (e) {
|
||||
@@ -190,8 +193,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
],
|
||||
)
|
||||
|
||||
const logOut = useCallback(() => {
|
||||
setActiveUser(null)
|
||||
const logOut = useCallback(async () => {
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
void requests.post(`${serverURL}${api}/${userSlug}/logout`)
|
||||
}, [serverURL, api, userSlug, revokeTokenAndExpire, setActiveUser])
|
||||
@@ -208,7 +211,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const json = await request.json()
|
||||
|
||||
if (json?.user) {
|
||||
setActiveUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
if (json?.token) {
|
||||
setTokenAndExpiration(json)
|
||||
}
|
||||
@@ -227,16 +230,16 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
})
|
||||
if (autoLoginResult.status === 200) {
|
||||
const autoLoginJson = await autoLoginResult.json()
|
||||
setActiveUser(autoLoginJson.user)
|
||||
await setActiveUser(autoLoginJson.user)
|
||||
if (autoLoginJson?.token) {
|
||||
setTokenAndExpiration(autoLoginJson)
|
||||
}
|
||||
} else {
|
||||
setActiveUser(null)
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
}
|
||||
} else {
|
||||
setActiveUser(null)
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
}
|
||||
}
|
||||
@@ -298,8 +301,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (remainingTime > 0) {
|
||||
forceLogOut = setTimeout(
|
||||
() => {
|
||||
setActiveUser(null)
|
||||
async () => {
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
redirectToInactivityRoute()
|
||||
},
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { Permissions, User } from '../../../../auth/types'
|
||||
|
||||
export type AuthContext<T = User> = {
|
||||
fetchFullUser: () => Promise<void>
|
||||
logOut: () => void
|
||||
logOut: () => Promise<void>
|
||||
permissions?: Permissions
|
||||
refreshCookie: (forceRefresh?: boolean) => void
|
||||
refreshCookieAsync: () => Promise<User>
|
||||
refreshPermissions: ({ locale }?: { locale?: string }) => Promise<void>
|
||||
setUser: (user: T) => void
|
||||
setUser: (user: T) => Promise<void>
|
||||
strategy?: string
|
||||
token?: string
|
||||
tokenExpiration?: number
|
||||
|
||||
@@ -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,7 @@ const Logout: React.FC<{ inactivity?: boolean }> = (props) => {
|
||||
const redirect = query.get('redirect')
|
||||
|
||||
useEffect(() => {
|
||||
logOut()
|
||||
void logOut()
|
||||
}, [logOut])
|
||||
|
||||
return (
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export const DefaultCollectionEdit: React.FC<
|
||||
hasSavePermission,
|
||||
internalState,
|
||||
isEditing,
|
||||
onSave,
|
||||
permissions,
|
||||
} = props
|
||||
|
||||
@@ -68,6 +69,7 @@ export const DefaultCollectionEdit: React.FC<
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
isEditing={isEditing}
|
||||
onSave={onSave}
|
||||
permissions={permissions}
|
||||
/>
|
||||
<DocumentFields
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
<WrapElement {...wrapElementProps}>
|
||||
<CodeCell
|
||||
collection={collection}
|
||||
data={`ID: ${cellData}`}
|
||||
data={`ID: ${String(cellData)}`}
|
||||
field={field as CodeField}
|
||||
nowrap
|
||||
rowData={rowData}
|
||||
@@ -68,13 +68,22 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
let CellComponent: React.FC<CellComponentProps> = cellData && cellComponents[field.type]
|
||||
let CellComponent: React.FC<CellComponentProps> | false =
|
||||
(cellData || typeof cellData === 'boolean') &&
|
||||
cellData !== null &&
|
||||
typeof cellData !== 'undefined' &&
|
||||
cellComponents[field.type]
|
||||
|
||||
if (!CellComponent) {
|
||||
if (collection.upload && fieldAffectsData(field) && field.name === 'filename') {
|
||||
CellComponent = cellComponents.File
|
||||
} else {
|
||||
if (!cellData && 'label' in field) {
|
||||
if (
|
||||
(cellData === undefined ||
|
||||
cellData === null ||
|
||||
(typeof cellData === 'string' && cellData.trim() === '')) &&
|
||||
'label' in field
|
||||
) {
|
||||
return (
|
||||
<WrapElement {...wrapElementProps}>
|
||||
{t('noLabel', {
|
||||
@@ -85,7 +94,7 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
})}
|
||||
</WrapElement>
|
||||
)
|
||||
} else if (typeof cellData === 'string' || typeof cellData === 'number') {
|
||||
} else if (['number', 'string'].includes(typeof cellData)) {
|
||||
return <WrapElement {...wrapElementProps}>{cellData}</WrapElement>
|
||||
} else if (typeof cellData === 'object') {
|
||||
return <WrapElement {...wrapElementProps}>{JSON.stringify(cellData)}</WrapElement>
|
||||
@@ -95,7 +104,9 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<WrapElement {...wrapElementProps}>
|
||||
<CellComponent collection={collection} data={cellData} field={field} rowData={rowData} />
|
||||
{CellComponent ? (
|
||||
<CellComponent collection={collection} data={cellData} field={field} rowData={rowData} />
|
||||
) : null}
|
||||
</WrapElement>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Props = {
|
||||
onClick?: (Props) => void
|
||||
rowData: {
|
||||
[path: string]: unknown
|
||||
id: number | string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
@@ -217,6 +218,7 @@ const collectionSchema = joi.object().keys({
|
||||
joi.number(),
|
||||
),
|
||||
useTempFiles: joi.bool(),
|
||||
withMetadata: joi.alternatives().try(joi.boolean(), joi.func()),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,7 +45,7 @@ const middleware = (payload: Payload): any => {
|
||||
i18nMiddleware(payload.config.i18n),
|
||||
identifyAPI('REST'),
|
||||
methodOverride('X-HTTP-Method-Override'),
|
||||
qsMiddleware({ arrayLimit: 1000, depth: 10 }),
|
||||
qsMiddleware({ arrayLimit: 1000, depth: 10, strictNullHandling: true }),
|
||||
bodyParser.urlencoded({ extended: true }),
|
||||
compression(payload.config.express.compression),
|
||||
localizationMiddleware,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,6 +11,10 @@ export const cloneDataFromOriginalDoc = (originalDocData: unknown): unknown => {
|
||||
})
|
||||
}
|
||||
|
||||
if (originalDocData instanceof Date) {
|
||||
return originalDocData
|
||||
}
|
||||
|
||||
if (typeof originalDocData === 'object' && originalDocData !== null) {
|
||||
return { ...originalDocData }
|
||||
}
|
||||
|
||||
@@ -209,6 +209,10 @@ export const checkbox: Validate<unknown, unknown, CheckboxField> = (
|
||||
}
|
||||
|
||||
export const date: Validate<unknown, unknown, DateField> = (value, { required, t }) => {
|
||||
if (value instanceof Date) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value && !isNaN(Date.parse(value.toString()))) {
|
||||
/* eslint-disable-line */
|
||||
return true
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { UploadedFile } from 'express-fileupload'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { SharpOptions } from 'sharp'
|
||||
|
||||
import sharp from 'sharp'
|
||||
|
||||
import type { UploadEdits } from './types'
|
||||
|
||||
import { type WithMetadata, optionallyAppendMetadata } from './optionallyAppendMetadata'
|
||||
|
||||
export const percentToPixel = (value, dimension): number => {
|
||||
if (!value) return 0
|
||||
return Math.floor((parseFloat(value) / 100) * dimension)
|
||||
@@ -15,14 +18,18 @@ type CropImageArgs = {
|
||||
dimensions: { height: number; width: number }
|
||||
file: UploadedFile
|
||||
heightInPixels: number
|
||||
req?: PayloadRequest
|
||||
widthInPixels: number
|
||||
withMetadata?: WithMetadata
|
||||
}
|
||||
export async function cropImage({
|
||||
cropData,
|
||||
dimensions,
|
||||
file,
|
||||
heightInPixels,
|
||||
req,
|
||||
widthInPixels,
|
||||
withMetadata,
|
||||
}: CropImageArgs) {
|
||||
try {
|
||||
const { x, y } = cropData
|
||||
@@ -40,9 +47,15 @@ export async function cropImage({
|
||||
width: Number(widthInPixels),
|
||||
}
|
||||
|
||||
const cropped = sharp(file.tempFilePath || file.data, sharpOptions).extract(formattedCropData)
|
||||
let cropped = sharp(file.tempFilePath || file.data, sharpOptions).extract(formattedCropData)
|
||||
|
||||
return await cropped.toBuffer({
|
||||
cropped = await optionallyAppendMetadata({
|
||||
req,
|
||||
sharpFile: cropped,
|
||||
withMetadata,
|
||||
})
|
||||
|
||||
return await cropped.withMetadata().toBuffer({
|
||||
resolveWithObject: true,
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import getImageSize from './getImageSize'
|
||||
import getSafeFileName from './getSafeFilename'
|
||||
import resizeAndTransformImageSizes from './imageResizer'
|
||||
import isImage from './isImage'
|
||||
import { optionallyAppendMetadata } from './optionallyAppendMetadata'
|
||||
|
||||
type Args<T> = {
|
||||
collection: Collection
|
||||
@@ -74,6 +75,7 @@ export const generateFileData = async <T>({
|
||||
resizeOptions,
|
||||
staticDir,
|
||||
trimOptions,
|
||||
withMetadata,
|
||||
} = collectionConfig.upload
|
||||
|
||||
let staticPath = staticDir
|
||||
@@ -169,6 +171,11 @@ export const generateFileData = async <T>({
|
||||
|
||||
if (sharpFile) {
|
||||
const metadata = await sharpFile.metadata()
|
||||
sharpFile = await optionallyAppendMetadata({
|
||||
req,
|
||||
sharpFile,
|
||||
withMetadata,
|
||||
})
|
||||
fileBuffer = await sharpFile.toBuffer({ resolveWithObject: true })
|
||||
;({ ext, mime } = await fromBuffer(fileBuffer.data)) // This is getting an incorrect gif height back.
|
||||
fileData.width = fileBuffer.info.width
|
||||
@@ -216,7 +223,9 @@ export const generateFileData = async <T>({
|
||||
dimensions,
|
||||
file,
|
||||
heightInPixels: uploadEdits.heightInPixels,
|
||||
req,
|
||||
widthInPixels: uploadEdits.widthInPixels,
|
||||
withMetadata,
|
||||
})
|
||||
|
||||
filesToSave.push({
|
||||
@@ -280,6 +289,7 @@ export const generateFileData = async <T>({
|
||||
savedFilename: fsSafeName || file.name,
|
||||
staticPath,
|
||||
uploadEdits,
|
||||
withMetadata,
|
||||
})
|
||||
|
||||
fileData.sizes = sizeData
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
|
||||
import { isNumber } from '../utilities/isNumber'
|
||||
import fileExists from './fileExists'
|
||||
import { type WithMetadata, optionallyAppendMetadata } from './optionallyAppendMetadata'
|
||||
|
||||
type ResizeArgs = {
|
||||
config: SanitizedCollectionConfig
|
||||
@@ -29,6 +30,7 @@ type ResizeArgs = {
|
||||
savedFilename: string
|
||||
staticPath: string
|
||||
uploadEdits?: UploadEdits
|
||||
withMetadata?: WithMetadata
|
||||
}
|
||||
|
||||
/** Result from resizing and transforming the requested image sizes */
|
||||
@@ -270,6 +272,7 @@ export default async function resizeAndTransformImageSizes({
|
||||
savedFilename,
|
||||
staticPath,
|
||||
uploadEdits,
|
||||
withMetadata,
|
||||
}: ResizeArgs): Promise<ImageSizesResult> {
|
||||
const { focalPoint: focalPointEnabled = true, imageSizes } = config.upload
|
||||
|
||||
@@ -344,8 +347,15 @@ export default async function resizeAndTransformImageSizes({
|
||||
width: prioritizeHeight ? undefined : resizeWidth,
|
||||
})
|
||||
|
||||
// must read from buffer, resize.metadata will return the original image metadata
|
||||
const { info } = await resized.toBuffer({ resolveWithObject: true })
|
||||
const metadataAppendedFile = await optionallyAppendMetadata({
|
||||
req,
|
||||
sharpFile: resized,
|
||||
withMetadata,
|
||||
})
|
||||
|
||||
// Must read from buffer, resized.metadata will return the original image metadata
|
||||
const { info } = await metadataAppendedFile.toBuffer({ resolveWithObject: true })
|
||||
|
||||
resizeImageMeta.height = extractHeightFromImage({
|
||||
...originalImageMeta,
|
||||
height: info.height,
|
||||
@@ -403,7 +413,13 @@ export default async function resizeAndTransformImageSizes({
|
||||
resized = resized.trim(imageResizeConfig.trimOptions)
|
||||
}
|
||||
|
||||
const { data: bufferData, info: bufferInfo } = await resized.toBuffer({
|
||||
const metadataAppendedFile = await optionallyAppendMetadata({
|
||||
req,
|
||||
sharpFile: resized,
|
||||
withMetadata,
|
||||
})
|
||||
|
||||
const { data: bufferData, info: bufferInfo } = await metadataAppendedFile.toBuffer({
|
||||
resolveWithObject: true,
|
||||
})
|
||||
|
||||
@@ -415,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', () => {
|
||||
|
||||
28
packages/payload/src/uploads/optionallyAppendMetadata.ts
Normal file
28
packages/payload/src/uploads/optionallyAppendMetadata.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Sharp, Metadata as SharpMetadata } from 'sharp'
|
||||
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
|
||||
export type WithMetadata =
|
||||
| ((options: { metadata: SharpMetadata; req: PayloadRequest }) => Promise<boolean>)
|
||||
| boolean
|
||||
|
||||
export async function optionallyAppendMetadata({
|
||||
req,
|
||||
sharpFile,
|
||||
withMetadata,
|
||||
}: {
|
||||
req: PayloadRequest
|
||||
sharpFile: Sharp
|
||||
withMetadata: WithMetadata
|
||||
}): Promise<Sharp> {
|
||||
const metadata = await sharpFile.metadata()
|
||||
if (withMetadata === true) {
|
||||
return sharpFile.withMetadata()
|
||||
} else if (typeof withMetadata === 'function') {
|
||||
const useMetadata = await withMetadata({ metadata, req })
|
||||
|
||||
if (useMetadata) return sharpFile.withMetadata()
|
||||
}
|
||||
|
||||
return sharpFile
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import type express from 'express'
|
||||
import type serveStatic from 'serve-static'
|
||||
import type { ResizeOptions, Sharp } from 'sharp'
|
||||
|
||||
import type { WithMetadata } from './optionallyAppendMetadata'
|
||||
|
||||
export type FileSize = {
|
||||
filename: null | string
|
||||
filesize: null | number
|
||||
@@ -49,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
|
||||
/**
|
||||
@@ -75,6 +89,7 @@ export type IncomingUploadType = {
|
||||
adminThumbnail?: GetAdminThumbnail | string
|
||||
crop?: boolean
|
||||
disableLocalStorage?: boolean
|
||||
displayPreview?: boolean
|
||||
/**
|
||||
* Accepts existing headers and can filter/modify them.
|
||||
*
|
||||
@@ -93,12 +108,14 @@ export type IncomingUploadType = {
|
||||
staticOptions?: serveStatic.ServeStaticOptions<express.Response<any, Record<string, any>>>
|
||||
staticURL?: string
|
||||
trimOptions?: ImageUploadTrimOptions
|
||||
withMetadata?: WithMetadata
|
||||
}
|
||||
|
||||
export type Upload = {
|
||||
adminThumbnail?: GetAdminThumbnail | string
|
||||
crop?: boolean
|
||||
disableLocalStorage?: boolean
|
||||
displayPreview?: boolean
|
||||
filesRequiredOnCreate?: boolean
|
||||
focalPoint?: boolean
|
||||
formatOptions?: ImageUploadFormatOptions
|
||||
@@ -110,6 +127,7 @@ export type Upload = {
|
||||
staticOptions?: serveStatic.ServeStaticOptions<express.Response<any, Record<string, any>>>
|
||||
staticURL: string
|
||||
trimOptions?: ImageUploadTrimOptions
|
||||
withMetadata?: WithMetadata
|
||||
}
|
||||
|
||||
export type File = {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import ObjectID from 'bson-objectid'
|
||||
|
||||
export const deepCopyObject = (inObject) => {
|
||||
if (ObjectID.isValid(inObject)) return inObject
|
||||
|
||||
if (inObject instanceof Date) return inObject
|
||||
|
||||
if (inObject instanceof Set) return new Set(inObject)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -29,18 +29,27 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
||||
collCacheConfig?.enabled !== false
|
||||
|
||||
return async (req, res, next) => {
|
||||
const filename = req.params.filename
|
||||
let fileKeyWithPrefix = ''
|
||||
|
||||
if (!filename) {
|
||||
req.payload.logger.warn({
|
||||
msg: `No filename provided for static file against collection: ${collection.slug}`,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const { identityID, storageClient } = await getStorageClient()
|
||||
|
||||
const Key = createKey({
|
||||
fileKeyWithPrefix = createKey({
|
||||
collection: collection.slug,
|
||||
filename: req.params.filename,
|
||||
filename,
|
||||
identityID,
|
||||
})
|
||||
|
||||
const object = await storageClient.getObject({
|
||||
Bucket: process.env.PAYLOAD_CLOUD_BUCKET,
|
||||
Key,
|
||||
Key: fileKeyWithPrefix,
|
||||
})
|
||||
|
||||
res.set({
|
||||
@@ -56,7 +65,10 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
||||
|
||||
return next()
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error({ err, msg: 'Error getting file from cloud storage' })
|
||||
req.payload.logger.error({
|
||||
err,
|
||||
msg: `Error getting file from cloud storage: '${fileKeyWithPrefix}'`,
|
||||
})
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type StripeWebhookHandler<T = any> = (args: {
|
||||
payload: Payload
|
||||
stripe: Stripe
|
||||
stripeConfig?: StripeConfig
|
||||
}) => void
|
||||
}) => Promise<void> | void
|
||||
|
||||
export interface StripeWebhookHandlers {
|
||||
[webhookName: string]: StripeWebhookHandler
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,6 +28,9 @@ const databaseAdapters = {
|
||||
mongoose: mongooseAdapter({
|
||||
migrationDir,
|
||||
url: 'mongodb://127.0.0.1/payloadtests',
|
||||
collation: {
|
||||
strength: 1,
|
||||
},
|
||||
}),
|
||||
postgres: postgresAdapter({
|
||||
migrationDir,
|
||||
|
||||
BIN
test/collections-graphql/media/test-image-1.jpg
Normal file
BIN
test/collections-graphql/media/test-image-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user