Compare commits

..

46 Commits

Author SHA1 Message Date
Elliot DeNolf
f00183029e chore(release): richtext-lexical/0.11.3 [skip ci] 2024-08-09 09:39:40 -04:00
Elliot DeNolf
b6c5aaa966 chore(release): db-mongodb/1.7.2 [skip ci] 2024-08-09 09:39:18 -04:00
Elliot DeNolf
517aaa0665 chore(release): payload/2.26.0 [skip ci] 2024-08-09 09:37:40 -04:00
Jarrod Flesch
2c2ffe406f chore: allow password to be mutated by hooks (#7537)
Fixes https://github.com/payloadcms/payload/issues/7531

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

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

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

## Type of change

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

## Checklist:

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

Fixes #6099

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

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

## Type of change

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

## Checklist:

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

Fixes #5212

Fixes #6278 

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

## Type of change

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

## Checklist:

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

Fixes #6880 

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

## Type of change

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

## Checklist:

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

Fixes #6034

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

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

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

## Type of change

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

## Checklist:

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

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

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

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


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

## Type of change

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

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

## Checklist:

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


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

Fixes #6108 

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

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


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

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

## Type of change

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

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-26 16:16:45 -04:00
Elliot DeNolf
a3085435ef chore(release): db-mongodb/1.7.1 [skip ci] 2024-07-26 11:38:00 -04:00
Elliot DeNolf
1466657e8f chore(release): payload/2.25.0 [skip ci] 2024-07-26 11:36:14 -04:00
James Mikrut
1348483648 fix: preserves objectids in deepCopyObject (#7385)
## Description

The `deepCopyObject` function was cannibalizing ObjectIDs, which
conflicted with the ability to surface them from the MongoDB adapter.
Now, the `deepCopyObject` function will simply pass through ObjectIDs
rather than break them.
2024-07-26 11:10:16 -04:00
Patrik
5321098d7e fix(payload): properly handles 0 value number fields in list view (#7364)
## Description

Fixes #5510 

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

## Type of change

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

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-25 15:35:35 -04:00
Kendell Joseph
c57591bc4f fix: supports null values in query strings (#5241)
Fixes issue where null values were not being handled properly from client/server

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-07-25 13:25:06 -04:00
Patrik
9750bc217e fix(db-mongodb): adds new optional collation feature flag behind mongodb collation option (#7359)
## Description

Fixes #7349 

Adds new `collation` prop to the mongodb adapter config to allow for
enabling the `mongodb` collation feature.

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

## Checklist:

- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-07-25 12:37:41 -04:00
Jarrod Flesch
468e5441f1 fix: relaxes equality check for relationship options in filter (#7344)
Fixes https://github.com/payloadcms/payload/issues/7103

When extracting the value from the querystring, it is always a string.
We were using a strict equality check which would cause the filter
options to never find the correct option. This caused an infinite loop
when using PG as ID's are numbers by default.
2024-07-24 15:53:09 -04:00
Patrik
3c5cce4c6f feat(payload): allows metadata to be appended to the file of the output media (#7295)
## Description

V3 PR [here](https://github.com/payloadcms/payload/pull/7293)

`Feat`: Adds new prop `withMetadata` to `uploads` config that allows the
user to allow media metadata to be appended to the file of the output
media.

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-07-24 15:32:54 -04:00
Elliot DeNolf
9f0f94893d chore(release): db-mongodb/1.7.0 [skip ci] 2024-07-24 14:17:35 -04:00
Elliot DeNolf
03b7892fc9 chore(release): payload/2.24.2 [skip ci] 2024-07-24 14:16:09 -04:00
James Mikrut
f96cf593ce feat: add jsonParse flag to mongooseAdapter that preserves existing, untracked MongoDB data types (#7338)
## Description

Preserves external data structures stored in MongoDB by avoiding the use
of `JSON.parse(JSON.stringify(mongooseDoc))`.

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

## Type of change

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

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-07-24 14:02:06 -04:00
Jarrod Flesch
8259611ce6 fix: fetches and sets permissions before setting user (#7337)
Fixes https://github.com/payloadcms/payload/issues/7330

Ensures permissions are always present before setting the user.
2024-07-24 12:30:10 -04:00
Jacob Fletcher
a3ed25a253 chore(live-preview): ensures dev points to src (#7340) 2024-07-24 12:17:03 -04:00
Jarrod Flesch
69e7b7a158 fix: allow autosave relationship drawers to function properly (#7325)
Fixes https://github.com/payloadcms/payload/issues/6887

Collections with autosave enabled would open and immediately close when
they were edited inside a relationship field. This PR threads onSave
through to autosave and checks the current drawer depth to determine if
it should call the onSave function or if it should redirect the user to
the doc page when autosave is triggered.
2024-07-23 17:00:27 -04:00
Jacob Fletcher
c6da99b4d1 fix(plugin-stripe): properly types async webhooks (#7316) 2024-07-23 13:56:29 -04:00
dependabot[bot]
ebd23caa56 chore(deps): bump ws from 7.5.9 to 7.5.10 in /examples/auth/payload (#7288) 2024-07-22 22:04:48 -04:00
dependabot[bot]
faa9b21824 chore(deps): bump nodemailer from 6.9.4 to 6.9.14 in /examples/email (#7289) 2024-07-22 22:04:31 -04:00
dependabot[bot]
1690560f11 chore(deps): bump braces from 3.0.2 to 3.0.3 in /examples/auth/next-app (#7286) 2024-07-22 22:04:12 -04:00
Patrik
0058660b3f fix(db-mongodb): removes precedence of regular chars over international chars in sort (#6923)
## Description

Fixes #6719 

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

## Type of change

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

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-07-22 16:55:35 -04:00
Elliot DeNolf
6d7ef919cb chore(release): payload/2.24.1 [skip ci] 2024-07-22 15:59:08 -04:00
Yosuf Ali
abffa37d85 chore(templates): fixes bug in e-commerce (#7258)
## Description

When following the documentation to run the E Commerce template locally,
you are asked to run `yarn stripe:webhooks` to work with webhooks.

However, when checking out your cart and a webhook is triggered, your
terminal receives the following error:

```
[ERROR] Failed to POST: Post "http://localhost:8000/stripe/webhooks": dial tcp 127.0.0.1:8000: connect: connection refused
```

I believe this is because the port is wrong, and it should be port
`3000`. There is no reference to a port `8000` anywhere in the code base
for this template, including in the docker-compose.yml file.

Making this changes allows webhook requests to be forwarded correctly:

```
--> customer.created [evt_...]
<-- [200] POST http://localhost:3000/stripe/webhooks [evt_...]
```

This PR makes this small change.

<!-- Please include a summary of the pull request and any related issues
it fixes. Please also include relevant motivation and context. -->

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

## Type of change

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

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-22 13:43:29 -04:00
Patrik
1b208c7add fix: resizes images first before applying focal point (#7278)
## Description

V3 (original) PR [here](https://github.com/payloadcms/payload/pull/7277)

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

## Type of change

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

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-22 12:28:55 -04:00
Rafał Nawojczyk
2840632161 docs: add warning about forbidden chars in custom ID field (#7059)
## Description

Add a warning text for users in DOCs, that will notify them about
forbidden characters while using `text` as a custom ID.
It resolves #7021 

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

## Type of change

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

- [x] Chore (non-breaking change which does not add functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)
- [ ] Change to the
[examples](https://github.com/payloadcms/payload/tree/main/examples)
directory (does not affect core functionality)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-07-21 22:23:03 -04:00
Patrik
0841d5a35e fix: uploads from drawer and focal point positioning (#7244)
## Description

V3 PR [here](https://github.com/payloadcms/payload/pull/7117)

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

## Type of change

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

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-07-19 15:01:06 -04:00
Jessica Chowdhury
bd19fcf259 chore(docs): expand on reserved field names (#7242)
## Description

Closes #6640

Note: Only updated for v2 as the v3 docs cover this topic already.

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

## Type of change

- [X] Chore (non-breaking change which does not add functionality)
2024-07-19 12:32:34 -04:00
Jacob Fletcher
18645771c8 fix: exports fallback hook types to ensure backwards compatibility (#7217) 2024-07-18 12:34:59 -04:00
Isak ✏ ⇝
20377bb22c fix: aliases AfterMe, AfterLogout, and AfterRefresh hook types (#7146)
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2024-07-18 10:56:56 -04:00
177 changed files with 2102 additions and 691 deletions

View File

@@ -1,3 +1,66 @@
## [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)
### Bug Fixes
* aliases AfterMe, AfterLogout, and AfterRefresh hook types ([#7146](https://github.com/payloadcms/payload/issues/7146)) ([20377bb](https://github.com/payloadcms/payload/commit/20377bb22c867552e412c1cafd16869399aadd68))
* exports fallback hook types to ensure backwards compatibility ([#7217](https://github.com/payloadcms/payload/issues/7217)) ([1864577](https://github.com/payloadcms/payload/commit/18645771c86664f1246f0fb599c8265a4cd1d6c0))
* resizes images first before applying focal point ([#7278](https://github.com/payloadcms/payload/issues/7278)) ([1b208c7](https://github.com/payloadcms/payload/commit/1b208c7addf56ae8a1af5e408b001b3e5f080a38))
* uploads from drawer and focal point positioning ([#7244](https://github.com/payloadcms/payload/issues/7244)) ([0841d5a](https://github.com/payloadcms/payload/commit/0841d5a35ee00650c703231a08fc9a361861ba67))
## [2.24.0](https://github.com/payloadcms/payload/compare/v2.23.1...v2.24.0) (2024-07-16)

View File

@@ -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,
},
})
```

View File

@@ -70,7 +70,16 @@ In addition to being able to define access control on a document-level, you can
### Field names
Some fields use their `name` property as a unique identifier to store and retrieve from the database. `__v`, `salt`, and `hash` are all reserved field names which are sanitized from Payload's config and cannot be used.
All fields require a `name` property. This is the key that will be used to store and retrieve the field's value in the database. This property must be unique within the Collection, Global, or nested group that it is defined in.
Payload reserves various field names for internal use. Using reserved field names will result in your field being sanitized from the config.
The following field names are forbidden and cannot be used:
- `__v`
- `salt`
- `hash`
- `file`
### Validation
@@ -145,6 +154,7 @@ const field: Field = {
Collections ID fields are generated automatically by default. An explicit `id` field can be declared in the `fields` array to override this behavior.
Users are then required to provide a custom ID value when creating a record through the Admin UI or API.
Valid ID types are `number` and `text`.
When using the text value, remember that it shouldn't contain the / (slash) sign, as the API will read it separately and this can result in unexpected behavior.
Example:

View File

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

View File

@@ -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._

View File

@@ -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"

View File

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

View File

@@ -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"

View File

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

View File

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

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,7 +91,7 @@
"prompts": "2.4.2",
"qs": "6.11.2",
"read-stream": "^2.1.1",
"rimraf": "3.0.2",
"rimraf": "4.4.1",
"semver": "^7.5.4",
"shelljs": "0.8.5",
"simple-git": "^3.20.0",

View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}),
}

View File

@@ -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

View File

@@ -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)
}),
}

View File

@@ -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
}

View File

@@ -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)
}),
}

View File

@@ -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: {},

View File

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

View File

@@ -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,

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.24.0",
"version": "2.26.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",
@@ -199,7 +199,7 @@
"postcss-loader": "6.2.1",
"postcss-preset-env": "9.0.0",
"release-it": "16.1.3",
"rimraf": "3.0.2",
"rimraf": "4.4.1",
"sass-loader": "12.6.0",
"serve-static": "1.15.0",
"swc-loader": "^0.2.3",

View File

@@ -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,

View File

@@ -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
}

View File

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

View File

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

View File

@@ -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>

View File

@@ -1,13 +1,12 @@
import { useModal } from '@faceless-ui/modal'
import React, { forwardRef, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactCrop, { type Crop as CropType } from 'react-image-crop'
import ReactCrop from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'
import type { Data } from '../../forms/Form/types'
import type { UploadEdits } from '../../../../uploads/types'
import Plus from '../../icons/Plus'
import { useUploadEdits } from '../../utilities/UploadEdits'
import { editDrawerSlug } from '../../views/collections/Edit/Upload'
import Button from '../Button'
import './index.scss'
@@ -42,58 +41,80 @@ type FocalPosition = {
y: number
}
export const EditUpload: React.FC<{
doc?: Data
export type EditUploadProps = {
fileName: string
fileSrc: string
imageCacheTag?: string
initialCrop?: UploadEdits['crop']
initialFocalPoint?: FocalPosition
onSave?: (uploadEdits: UploadEdits) => void
showCrop?: boolean
showFocalPoint?: boolean
}> = ({ doc, fileName, fileSrc, imageCacheTag, showCrop, showFocalPoint }) => {
}
const defaultCrop: UploadEdits['crop'] = {
height: 100,
unit: '%',
width: 100,
x: 0,
y: 0,
}
export const EditUpload: React.FC<EditUploadProps> = ({
fileName,
fileSrc,
imageCacheTag,
initialCrop,
initialFocalPoint,
onSave,
showCrop,
showFocalPoint,
}) => {
const { closeModal } = useModal()
const { t } = useTranslation(['general', 'upload'])
const { updateUploadEdits, uploadEdits } = useUploadEdits()
const [focalPosition, setFocalPosition] = useState<FocalPosition>({
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
})
const [crop, setCrop] = useState<UploadEdits['crop']>(() => ({
...defaultCrop,
...(initialCrop || {}),
}))
const defaultFocalPosition: FocalPosition = {
x: 50,
y: 50,
}
const [focalPosition, setFocalPosition] = useState<FocalPosition>(() => ({
...defaultFocalPosition,
...initialFocalPoint,
}))
const [checkBounds, setCheckBounds] = useState<boolean>(false)
const [originalHeight, setOriginalHeight] = useState<number>(0)
const [originalWidth, setOriginalWidth] = useState<number>(0)
const [uncroppedPixelHeight, setUncroppedPixelHeight] = useState<number>(0)
const [uncroppedPixelWidth, setUncroppedPixelWidth] = useState<number>(0)
const focalWrapRef = useRef<HTMLDivElement | undefined>()
const imageRef = useRef<HTMLImageElement | undefined>()
const cropRef = useRef<HTMLDivElement | undefined>()
const heightRef = useRef<HTMLInputElement | null>(null)
const widthRef = useRef<HTMLInputElement | null>(null)
const [crop, setCrop] = useState<CropType>({
height: 100,
heightPixels: 0,
unit: '%',
width: 100,
widthPixels: 0,
x: 0,
y: 0,
})
const heightInputRef = useRef<HTMLInputElement | null>(null)
const widthInputRef = useRef<HTMLInputElement | null>(null)
const [imageLoaded, setImageLoaded] = useState<boolean>(false)
const onImageLoad = (e) => {
setOriginalHeight(e.currentTarget.naturalHeight)
setOriginalWidth(e.currentTarget.naturalWidth)
// set the default image height/width on load
setUncroppedPixelHeight(e.currentTarget.naturalHeight)
setUncroppedPixelWidth(e.currentTarget.naturalWidth)
setImageLoaded(true)
}
const fineTuneCrop = ({ dimension, value }: { dimension: 'height' | 'width'; value: string }) => {
const intValue = parseInt(value)
if (dimension === 'width' && intValue >= originalWidth) return null
if (dimension === 'height' && intValue >= originalHeight) return null
if (dimension === 'width' && intValue >= uncroppedPixelWidth) return null
if (dimension === 'height' && intValue >= uncroppedPixelHeight) return null
const percentage = 100 * (intValue / (dimension === 'width' ? originalWidth : originalHeight))
const percentage =
100 * (intValue / (dimension === 'width' ? uncroppedPixelWidth : uncroppedPixelHeight))
if (percentage === 100 || percentage === 0) return null
@@ -117,16 +138,13 @@ export const EditUpload: React.FC<{
}
const saveEdits = () => {
updateUploadEdits({
crop: crop
? {
...crop,
heightPixels: Number(heightRef.current?.value ?? crop.heightPixels),
widthPixels: Number(widthRef.current?.value ?? crop.widthPixels),
}
: undefined,
focalPoint: focalPosition ? focalPosition : undefined,
})
if (typeof onSave === 'function')
onSave({
crop: crop ? crop : undefined,
focalPoint: focalPosition,
heightInPixels: Number(heightInputRef?.current?.value ?? uncroppedPixelHeight),
widthInPixels: Number(widthInputRef?.current?.value ?? uncroppedPixelWidth),
})
closeModal(editDrawerSlug)
}
@@ -181,7 +199,7 @@ export const EditUpload: React.FC<{
className={`${baseClass}__focal-wrapper`}
ref={focalWrapRef}
style={{
aspectRatio: `${originalWidth / originalHeight}`,
aspectRatio: `${uncroppedPixelWidth / uncroppedPixelHeight}`,
}}
>
{showCrop ? (
@@ -237,10 +255,8 @@ export const EditUpload: React.FC<{
onClick={() =>
setCrop({
height: 100,
heightPixels: originalHeight,
unit: '%',
width: 100,
widthPixels: originalWidth,
x: 0,
y: 0,
})
@@ -257,14 +273,14 @@ export const EditUpload: React.FC<{
<Input
name={`${t('upload:width')} (px)`}
onChange={(value) => fineTuneCrop({ dimension: 'width', value })}
ref={widthRef}
value={((crop.width / 100) * originalWidth).toFixed(0)}
ref={widthInputRef}
value={((crop.width / 100) * uncroppedPixelWidth).toFixed(0)}
/>
<Input
name={`${t('upload:height')} (px)`}
onChange={(value) => fineTuneCrop({ dimension: 'height', value })}
ref={heightRef}
value={((crop.height / 100) * originalHeight).toFixed(0)}
ref={heightInputRef}
value={((crop.height / 100) * uncroppedPixelHeight).toFixed(0)}
/>
</div>
</div>

View File

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

View File

@@ -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

View File

@@ -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,

View File

@@ -24,8 +24,7 @@ export const MultiValueLabel: React.FC<MultiValueProps<Option>> = (props) => {
draggableProps,
// @ts-expect-error // TODO Fix this - moduleResolution 16 breaks our declare module
setDrawerIsOpen,
// @ts-expect-error // TODO Fix this - moduleResolution 16 breaks our declare module
onSave,
// onSave,
} = {},
} = {},
} = props
@@ -71,7 +70,7 @@ export const MultiValueLabel: React.FC<MultiValueProps<Option>> = (props) => {
</Tooltip>
<Edit />
</DocumentDrawerToggler>
<DocumentDrawer onSave={onSave} />
<DocumentDrawer onSave={/* onSave */ null} />
</Fragment>
)}
</div>

View File

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

View File

@@ -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()
},

View File

@@ -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

View File

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

View File

@@ -25,7 +25,7 @@ const Logout: React.FC<{ inactivity?: boolean }> = (props) => {
const redirect = query.get('redirect')
useEffect(() => {
logOut()
void logOut()
}, [logOut])
return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'react-toastify'
import type { UploadEdits } from '../../../../../../uploads/types'
import type { Props } from './types'
import isImage from '../../../../../../uploads/isImage'
@@ -13,6 +14,7 @@ import FileDetails from '../../../../elements/FileDetails'
import PreviewSizes from '../../../../elements/PreviewSizes'
import Thumbnail from '../../../../elements/Thumbnail'
import Error from '../../../../forms/Error'
import { useForm } from '../../../../forms/Form/context'
import reduceFieldsToValues from '../../../../forms/Form/reduceFieldsToValues'
import { fieldBaseClass } from '../../../../forms/field-types/shared'
import useField from '../../../../forms/useField'
@@ -55,7 +57,8 @@ export const Upload: React.FC<Props> = (props) => {
const [replacingFile, setReplacingFile] = useState(false)
const [fileSrc, setFileSrc] = useState<null | string>(null)
const { t } = useTranslation(['upload', 'general'])
const { resetUploadEdits } = useUploadEdits()
const { setModified } = useForm()
const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits()
const [doc, setDoc] = useState(reduceFieldsToValues(internalState || {}, true))
const { docPermissions } = useDocumentInfo()
const { errorMessage, setValue, showError, value } = useField<File>({
@@ -133,6 +136,14 @@ export const Upload: React.FC<Props> = (props) => {
setShowUrlInput(false)
}, [handleFileChange, resetUploadEdits])
const onEditsSave = useCallback(
(args: UploadEdits) => {
setModified(true)
updateUploadEdits(args)
},
[setModified, updateUploadEdits],
)
const handlePasteUrlClick = () => {
setShowUrlInput((prev) => !prev)
}
@@ -222,7 +233,7 @@ export const Upload: React.FC<Props> = (props) => {
onClick={handleUrlSubmit}
type="button"
>
{t('upload:addImage')}
{t('upload:addFile')}
</button>
</div>
</div>
@@ -274,10 +285,15 @@ export const Upload: React.FC<Props> = (props) => {
{(value || doc.filename) && (
<Drawer header={null} slug={editDrawerSlug}>
<EditUpload
doc={doc || undefined}
fileName={value?.name || doc?.filename}
fileSrc={doc?.url || fileSrc}
imageCacheTag={doc.updatedAt}
initialCrop={uploadEdits?.crop ?? undefined}
initialFocalPoint={{
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
}}
onSave={onEditsSave}
showCrop={showCrop}
showFocalPoint={showFocalPoint}
/>

View File

@@ -4,15 +4,3 @@ export type IndexProps = {
collection: SanitizedCollectionConfig
isEditing?: boolean
}
export type UploadEdits = {
crop?: {
height?: number
width?: number
x?: number
y?: number
}
focalPoint?: {
x?: number
y?: number
}
}

View File

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

View File

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

View File

@@ -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>
)
}

View File

@@ -11,6 +11,7 @@ export type Props = {
onClick?: (Props) => void
rowData: {
[path: string]: unknown
id: number | string
}
}

View File

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

View File

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

View File

@@ -204,6 +204,7 @@ export type MeHook<T extends TypeWithID = any> = (args: {
args: MeArguments
user: T
}) => ({ exp: number; user: T } | void) | Promise<{ exp: number; user: T } | void>
export type AfterRefreshHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig

View File

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

View File

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

View File

@@ -31,8 +31,14 @@ export type {
AfterDeleteHook as CollectionAfterDeleteHook,
AfterForgotPasswordHook as CollectionAfterForgotPasswordHook,
AfterLoginHook as CollectionAfterLoginHook,
AfterLogoutHook,
AfterLogoutHook as CollectionAfterLogoutHook,
AfterMeHook,
AfterMeHook as CollectionAfterMeHook,
AfterOperationHook as CollectionAfterOperationHook,
AfterReadHook as CollectionAfterReadHook,
AfterRefreshHook,
AfterRefreshHook as CollectionAfterRefreshHook,
BeforeChangeHook as CollectionBeforeChangeHook,
BeforeDeleteHook as CollectionBeforeDeleteHook,
BeforeDuplicate,

View File

@@ -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,

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,10 @@ export const cloneDataFromOriginalDoc = (originalDocData: unknown): unknown => {
})
}
if (originalDocData instanceof Date) {
return originalDocData
}
if (typeof originalDocData === 'object' && originalDocData !== null) {
return { ...originalDocData }
}

View File

@@ -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

View File

@@ -283,7 +283,7 @@
"near": "قريب من"
},
"upload": {
"addImage": "إضافة صورة",
"addFile": "إضافة ملف",
"crop": "محصول",
"cropToolDescription": "اسحب الزوايا المحددة للمنطقة، رسم منطقة جديدة أو قم بضبط القيم أدناه.",
"dragAndDrop": "قم بسحب وإسقاط ملفّ",

View File

@@ -283,7 +283,7 @@
"near": "yaxın"
},
"upload": {
"addImage": "Şəkil əlavə et",
"addFile": "Fayl əlavə et",
"crop": "Məhsul",
"cropToolDescription": "Seçilmiş sahənin köşələrini sürükləyin, yeni bir sahə çəkin və ya aşağıdakı dəyərləri düzəltin.",
"dragAndDrop": "Faylı buraya sürükləyin və buraxın",

View File

@@ -283,7 +283,7 @@
"near": "близко"
},
"upload": {
"addImage": "Добавяне на изображение",
"addFile": "Добавяне на файл",
"crop": "Изрязване",
"cropToolDescription": "Плъзни ъглите на избраната област, избери нова област или коригирай стойностите по-долу.",
"dragAndDrop": "Дръпни и пусни файл",

View File

@@ -283,7 +283,7 @@
"near": "blízko"
},
"upload": {
"addImage": "Přidat obrázek",
"addFile": "Přidat soubor",
"crop": "Ořez",
"cropToolDescription": "Přetáhněte rohy vybrané oblasti, nakreslete novou oblast nebo upravte níže uvedené hodnoty.",
"dragAndDrop": "Přetáhněte soubor",

View File

@@ -283,7 +283,7 @@
"near": "in der Nähe"
},
"upload": {
"addImage": "Bild hinzufügen",
"addFile": "Datei hinzufügen",
"crop": "Zuschneiden",
"cropToolDescription": "Ziehen Sie die Ecken des ausgewählten Bereichs, zeichnen Sie einen neuen Bereich oder passen Sie die Werte unten an.",
"dragAndDrop": "Ziehen Sie eine Datei per Drag-and-Drop",

View File

@@ -283,7 +283,7 @@
"near": "near"
},
"upload": {
"addImage": "Add Image",
"addFile": "Add File",
"crop": "Crop",
"cropToolDescription": "Drag the corners of the selected area, draw a new area or adjust the values below.",
"dragAndDrop": "Drag and drop a file",

View File

@@ -283,7 +283,7 @@
"near": "cerca"
},
"upload": {
"addImage": "Añadir imagen",
"addFile": "Añadir archivo",
"crop": "Cultivo",
"cropToolDescription": "Arrastra las esquinas del área seleccionada, dibuja un nuevo área o ajusta los valores a continuación.",
"dragAndDrop": "Arrastra y suelta un archivo",

View File

@@ -283,7 +283,7 @@
"near": "نزدیک"
},
"upload": {
"addImage": "اضافه کردن تصویر",
"addFile": "اضافه کردن فایل",
"crop": "محصول",
"cropToolDescription": "گوشه‌های منطقه انتخاب شده را بکشید، یک منطقه جدید رسم کنید یا مقادیر زیر را تنظیم کنید.",
"dragAndDrop": "یک سند را بکشید و رها کنید",

View File

@@ -283,7 +283,7 @@
"near": "proche"
},
"upload": {
"addImage": "Ajouter une image",
"addFile": "Ajouter un fichier",
"crop": "Recadrer",
"cropToolDescription": "Faites glisser les coins de la zone sélectionnée, dessinez une nouvelle zone ou ajustez les valeurs ci-dessous.",
"dragAndDrop": "Glisser-déposer un fichier",

View File

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

View File

@@ -283,7 +283,7 @@
"near": "közel"
},
"upload": {
"addImage": "Kép hozzáadása",
"addFile": "Fájl hozzáadása",
"crop": "Termés",
"cropToolDescription": "Húzza a kijelölt terület sarkait, rajzoljon új területet, vagy igazítsa a lentebb található értékeket.",
"dragAndDrop": "Húzzon ide egy fájlt",

View File

@@ -284,7 +284,7 @@
"near": "vicino"
},
"upload": {
"addImage": "Aggiungi immagine",
"addFile": "Aggiungi file",
"crop": "Raccolto",
"cropToolDescription": "Trascina gli angoli dell'area selezionata, disegna una nuova area o regola i valori qui sotto.",
"dragAndDrop": "Trascina e rilascia un file",

View File

@@ -283,7 +283,7 @@
"near": "近く"
},
"upload": {
"addImage": "画像を追加",
"addFile": "ファイルを追加",
"crop": "クロップ",
"cropToolDescription": "選択したエリアのコーナーをドラッグしたり、新たなエリアを描画したり、下記の値を調整してください。",
"dragAndDrop": "ファイルをドラッグ アンド ドロップする",

View File

@@ -283,7 +283,7 @@
"near": "근처"
},
"upload": {
"addImage": "이미지 추가",
"addFile": "파일 추가",
"crop": "자르기",
"cropToolDescription": "선택한 영역의 모퉁이를 드래그하거나 새로운 영역을 그리거나 아래의 값을 조정하세요.",
"dragAndDrop": "파일을 끌어다 놓으세요",

View File

@@ -283,7 +283,7 @@
"near": "နီး"
},
"upload": {
"addImage": "ပုံ ထည့်ပါ",
"addFile": "ဖိုင်ထည့်ပါ",
"crop": "သုန်း",
"cropToolDescription": "ရွေးထားသည့်ဧရိယာတွင်မွေးလျှက်မှုများကိုဆွဲပြီး, အသစ်တည်ပြီးသို့မဟုတ်အောက်ပါတ",
"dragAndDrop": "ဖိုင်တစ်ဖိုင်ကို ဆွဲချလိုက်ပါ။",

View File

@@ -283,7 +283,7 @@
"near": "nær"
},
"upload": {
"addImage": "Legg til bilde",
"addFile": "Legg til fil",
"crop": "Beskjær",
"cropToolDescription": "Dra hjørnene av det valgte området, tegn et nytt område eller juster verdiene nedenfor.",
"dragAndDrop": "Dra og slipp en fil",

View File

@@ -283,7 +283,7 @@
"near": "nabij"
},
"upload": {
"addImage": "Afbeelding toevoegen",
"addFile": "Bestand toevoegen",
"crop": "Bijsnijden",
"cropToolDescription": "Sleep de hoeken van het geselecteerde gebied, teken een nieuw gebied of pas de waarden hieronder aan.",
"dragAndDrop": "Sleep een bestand",

View File

@@ -284,7 +284,7 @@
"near": "blisko"
},
"upload": {
"addImage": "Dodaj obraz",
"addFile": "Dodaj plik",
"crop": "Przytnij",
"cropToolDescription": "Przeciągnij narożniki wybranego obszaru, narysuj nowy obszar lub dostosuj poniższe wartości.",
"dragAndDrop": "Przeciągnij i upuść plik",

View File

@@ -283,7 +283,7 @@
"near": "perto"
},
"upload": {
"addImage": "Adicionar imagem",
"addFile": "Adicionar arquivo",
"crop": "Cultura",
"cropToolDescription": "Arraste as bordas da área selecionada, desenhe uma nova área ou ajuste os valores abaixo.",
"dragAndDrop": "Arraste e solte um arquivo",

View File

@@ -283,7 +283,7 @@
"near": "în apropiere de"
},
"upload": {
"addImage": "Adaugă imagine",
"addFile": "Adaugă fișier",
"crop": "Cultură",
"cropToolDescription": "Trageți colțurile zonei selectate, desenați o nouă zonă sau ajustați valorile de mai jos.",
"dragAndDrop": "Trageți și plasați un fișier",

Some files were not shown because too many files have changed in this diff Show More