Compare commits

...

98 Commits

Author SHA1 Message Date
Elliot DeNolf
06729a0a73 chore(release): db-postgres/0.3.0 [skip ci] 2024-01-09 16:30:49 -05:00
Elliot DeNolf
2bd7822a16 chore(release): db-mongodb/1.3.0 [skip ci] 2024-01-09 16:30:37 -05:00
Elliot DeNolf
bc7daf6b49 chore(release): payload/2.7.0 [skip ci] 2024-01-09 16:28:34 -05:00
James Mikrut
feab679ef7 Merge pull request #4615 from payloadcms/fix/relationship-field-number-ids-untitled
fix: relations with number based ids (postgres) show untitled ID: x
2024-01-09 15:30:41 -05:00
James Mikrut
be39ed4317 Merge pull request #4633 from payloadcms/fix/#3839-postgres-exist-json
fix(db-postgres): incorrect results querying json field using exists
2024-01-09 15:27:22 -05:00
James Mikrut
570e192eb4 Merge pull request #4741 from payloadcms/fix/#4591-migrate-down-batches
fix(db-postgres): migrate down not limited to latest batch
2024-01-09 13:50:45 -05:00
James Mikrut
22f4967dd4 Merge pull request #4726 from payloadcms/feat/migration-transactions
feat: improve transaction support by passing req to migrations
2024-01-09 13:50:00 -05:00
James Mikrut
4873c36129 Merge pull request #4722 from payloadcms/fix/#4719-migration-transaction-options
fix(db-mongodb): migration errors with transactionOptions false
2024-01-09 13:46:06 -05:00
James Mikrut
f0ec21cdda Merge pull request #4624 from payloadcms/fix/#3692-plugin-nested-docs-overrides
fix(plugin-nested-docs): custom overrides of breadcrumb and parent fields
2024-01-09 13:45:46 -05:00
dependabot[bot]
da737bdf8e chore(deps): bump follow-redirects in /examples/testing (#4735)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:18:29 -05:00
dependabot[bot]
40508880c1 chore(deps): bump follow-redirects in /templates/blank (#4736)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:18:18 -05:00
dependabot[bot]
8f420d841a chore(deps): bump follow-redirects in /templates/website (#4737)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:18:08 -05:00
dependabot[bot]
9022e27308 chore(deps): bump follow-redirects in /templates/ecommerce (#4734)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:17:59 -05:00
Patrik
acf2e41312 docs: updates verify to verifyEmail in local api (#4728) 2024-01-09 13:15:31 -05:00
Dan Ribbens
6acfae8ee7 fix(db-postgres): migrate down only runs latest batch size 2024-01-09 10:21:45 -05:00
Alessio Gravili
20bdd91da4 chore: upgrade @types/nodemailer from v6.4.8 to v6.4.14 (#4733) 2024-01-09 14:19:45 +01:00
Alessio Gravili
50502834c9 chore(richtext-lexical): upgrade lexical from v0.12.5 to v0.12.6 (#4732)
* chore(richtext-lexical): upgrade all lexical packages from 0.12.5 to 0.12.6

* fix(richtext-lexical): fix TypeScript errors

* fix indenting
2024-01-09 09:35:18 +01:00
Dan Ribbens
983733ad74 merge main 2024-01-08 17:12:02 -05:00
Dan Ribbens
555d02769a feat(db-postgres): improve transaction support by passing req to migrations 2024-01-08 15:50:22 -05:00
Dan Ribbens
682eca2186 feat(db-mongodb): improve transaction support by passing req to migrations 2024-01-08 15:50:09 -05:00
Dan Ribbens
1d14d9f8b8 feat: improve transaction support by passing req to migrations 2024-01-08 12:34:55 -05:00
Dan Ribbens
0abaddc2ef chore: remove payload migration endpoints 2024-01-08 12:24:53 -05:00
Dan Ribbens
21b9453cf4 fix(db-mongodb): migration error calling beginTransaction with transactionOptions false 2024-01-08 12:03:45 -05:00
Alessio Gravili
136993ec2b chore: undo adding DocumentInfo state to ActionsProvider (#4707) 2024-01-05 22:58:16 +01:00
Dan Ribbens
63bc4cabe1 fix(db-mongodb): querying plan for collections ignoring indexes (#4655)
* fix(db-mongodb): querying plan for collections ignoring indexes

* chore(db-mongodb): improve index hint comment
2024-01-05 16:21:19 -05:00
Alessio Gravili
6a8a6e4ef4 feat: provide document info to ActionsProvider (#4696) 2024-01-05 21:12:21 +01:00
Jessica Chowdhury
9828772890 fix: prevents row overflow (#4704) 2024-01-05 15:04:30 -05:00
Jessica Chowdhury
6116573164 docs: corrects type in useField example (#4705) 2024-01-05 15:03:38 -05:00
Dan Ribbens
cab6babd60 fix(db-postgres): validation prevents group fields in blocks (#4699)
* fix(db-postgres): validation prevents using group fields within blocks

* fix(db-postgres): validation of non-matching blocks in reverse order
2024-01-05 15:03:08 -05:00
Paul
55399424a1 fix(plugin-nested-docs): children wrongly publishing draft data (#4692)
* fix(plugin-nested-docs): handles child parent doc publishing

* Add tests

* Fix error failing on save hook

---------

Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
2024-01-05 15:02:15 -05:00
Jessica Chowdhury
28a30120dd fix(plugin-form-builder): slate serializer should replace curly braces in links (#4703) 2024-01-05 13:01:03 -05:00
Elliot DeNolf
40a0921597 docs: update 'accessing files outside of payload cloud' 2024-01-05 11:53:51 -05:00
Paul
0b80e4a403 chore: bump the mongodb-memory-server dependency to 9.x (#4693) 2024-01-05 09:35:35 -05:00
Seied Ali Mirkarimi
b378532ddf chore: rtl locale selector and sidebar button (#4684)
* chore: rtl locale popup selector and sidebar button

* chore(plugin-seo): add persian translation
2024-01-04 21:45:39 -05:00
Patrik
d419275fb5 fix: adds objectID validation to isValidID if of type text (#4689)
* fix: adds updated object-id validation to isValidID

* chore: adds check to see if value is of type string or object

* chore: needs to return false if value not of type object or string
2024-01-04 15:08:55 -05:00
Jessica Chowdhury
0fb3a9ca89 fix: allow json field to be saved empty and reflect value changes (#4687)
* fix: allow json field to be saved empty and reflect value changes

* fix: reverts change to json field validation

* chore: wraps more JSON field logic with a try/catch
2024-01-04 14:47:10 -05:00
Gokulsck
f43cf185d4 feat: hasMany property for text fields (#4605)
* fix for supporting hasMany property in text field

* Updated docs

* handle text case types for schema and graphql schema

* fix unit test for required failing

* add unit test for has many text field

* add end to end test for has many on text field creation

* support has many feature for text field on postgres

---------

Co-authored-by: Chris Heinz <chrisi.heinz@web.de>
2024-01-04 14:45:00 -05:00
Dan Ribbens
5d15955f83 fix: custom ids in versions (#4680)
* chore: scaffolds out fix for postgres issues with custom ids in versions

* fix(db-postgres): queryDrafts returns undefined doc.id

* chore(db-postgres): fix build

* fix: removes extra custom id field from  versions buildCollectionFields

* chore: comments test/versions seeding back in

* fix buildCollectionFields version group fields

* fix: id field can be edited after saving a document with custom ids

* chore: updates versions custom ID test

---------

Co-authored-by: PatrikKozak <patrik@payloadcms.com>
2024-01-04 13:05:10 -05:00
Elliot DeNolf
2d35e06667 ci(templates): generate types in ci (#4685) 2024-01-04 10:04:44 -05:00
Elliot DeNolf
d2de6db449 chore(release): eslint-config-payload/1.1.0 2024-01-04 09:56:42 -05:00
Alessio Gravili
a3e78161b5 fix: non-boolean condition result causes infinite looping (#4579) 2024-01-04 09:51:08 -05:00
Hulpoi George-Valentin
d543665995 fix: unlock user condition always passes due to seconds conversion (#4610)
* fix: unlock condition is always true

* test: extra call for locking user, therefor won't be a condition issue
2024-01-04 09:43:10 -05:00
Alessio Gravili
db7dddf1c5 chore: commit intellij run configurations (#4653)
* chore: update .gitignore

* chore: update .gitignore

* chore: commit IntelliJ run configurations
2024-01-04 09:35:01 -05:00
Paul
3027a03ad1 feat(plugin-seo): add i18n (#4665)
* Add i18n to plugin SEO

* Add new translations and e2e tests for the SEO plugin

* Update e2e tests to utilise a shared page ID from a create function
2024-01-04 09:18:09 -05:00
Paul
85e38b7cfd fix: sidebar fields not disabled by access permissions (#4682)
* Pass operation to sidebar fields too

* Add a test for sidebar field update permission
2024-01-03 20:04:40 -05:00
Elliot DeNolf
9090540ece chore(release): richtext-lexical/0.5.1 [skip ci] 2024-01-03 15:58:05 -05:00
Elliot DeNolf
46ef284f6b chore(release): db-postgres/0.2.3 [skip ci] 2024-01-03 15:57:54 -05:00
Elliot DeNolf
0727dcd963 chore(release): db-mongodb/1.2.0 [skip ci] 2024-01-03 15:57:22 -05:00
Elliot DeNolf
52f8d4f9f0 chore(release): payload/2.6.0 [skip ci] 2024-01-03 15:55:34 -05:00
Jessica Chowdhury
f1fa374ed1 fix: tab field error when using the same interface name (#4657)
* fix: tab field error when using the same interface name

* fix: removes unused tab types
2024-01-03 15:50:07 -05:00
Alessio Gravili
6b691eee43 chore(eslint-config-payload): improve perfectionist object sort order (#4678) 2024-01-03 21:45:34 +01:00
Paul
be3beabb9b fix: navigation locks when modal is closed with esc (#4664) 2024-01-03 12:06:09 -05:00
Seied Ali Mirkarimi
1fa00cc25c chore: rtl header locale selector (#4670) 2024-01-03 12:04:39 -05:00
James
f70943524b fix(templates): #4662, templates not building after having types generated 2024-01-02 19:49:09 -05:00
Jessica Chowdhury
a67080a291 Merge pull request #4574 from jschuur/patch-1
fix: adjusts json field joi schema to allow editorOptions
2024-01-02 21:58:34 +00:00
Jarrod Flesch
69a99445c9 fix: detect language from request headers accept-language (#4656) 2024-01-02 15:17:00 -05:00
Alessio Gravili
00d8480062 fix: "The punycode module is deprecated" warning by updating nodemailer 2024-01-02 18:26:52 +01:00
Dan Ribbens
7424ba9090 test: e2e await fix (#4646) 2024-01-01 14:09:48 -05:00
Dan Ribbens
ec4d2f97cb fix(db-postgres): query on json properties 2023-12-29 16:42:32 -05:00
Dan Ribbens
9d9ac0ec28 fix(db-postgres): incorrect results querying json field using exists operator 2023-12-29 11:35:12 -05:00
Dan Ribbens
635e7c26e8 fix(plugin-nested-docs): custom parent field slug 2023-12-28 14:30:36 -05:00
Dan Ribbens
c4a4678afb fix(plugin-nested-docs): parent filterOptions errors when specifying breadcrumbsFieldSlug 2023-12-28 14:01:31 -05:00
Dan Ribbens
a5a91c08a9 fix(plugin-nested-docs): breadcrumbsFieldSlug used in resaveSelfAfterCreate hook 2023-12-28 13:05:53 -05:00
Dan Ribbens
7db58b482b fix: custom overrides of breadcrumb and parent fields 2023-12-28 12:56:30 -05:00
Dan Ribbens
1b914083c8 fix: relations with number based ids (postgres) show untitled ID: x 2023-12-27 15:30:30 -05:00
Anthony Bouch
657d14c07b chore(plugin-search): adjusts code comment when attaching hooks (#4595) 2023-12-23 19:24:33 -05:00
yuc
fbf8ab72a4 docs: fix typo in Select Field example (#4593) 2023-12-23 19:15:44 -05:00
Zakher Masri
997f158149 chore(plugin-stripe): fixes broken link in README (#4602) 2023-12-23 19:08:25 -05:00
James Mikrut
c3be5d1d5e Merge pull request #4560 from payloadcms/fix/#4484-graphql-multiple-locales
fix: graphql cannot query multiple locales
2023-12-21 15:29:14 -05:00
James Mikrut
250bcd8189 Merge pull request #4526 from payloadcms/feat/locale-specific-fallbacks
feat: extend locales to have fallbackLocales
2023-12-21 15:25:55 -05:00
Jesse Sivonen
a71d37b398 fix(db-postgres): Wait for transaction to complete on commit (#4582)
* fix(db-postgres): Wait for transaction to complete on commit
* fix session types
2023-12-21 11:03:27 -05:00
Patrik
5c5523195c fix: resets actions array when navigating out of view with actions (#4585) 2023-12-21 10:48:04 -05:00
Joost Schuur
bff4cf518f fix: adjusts json field joi schema to allow editorOptions
Previous fix was not applied to json fields: https://github.com/payloadcms/payload/pull/2731/files
2023-12-21 00:47:14 +08:00
Alessio Gravili
8015e999cd fix(richtext-lexical): z-index issues (#4570) 2023-12-20 15:10:18 +01:00
Sajarin M
0c905f0da7 docs: typo in transactions page (#4565) 2023-12-20 01:31:50 -05:00
Dan Ribbens
e691a90a4c chore: fix failed test 2023-12-19 15:53:20 -05:00
James Mikrut
0b2da4fba7 Merge pull request #4467 from payloadcms/feat/mongodb-transaction-options
feat(db-mongodb): add transactionOptions
2023-12-19 15:30:23 -05:00
Elliot DeNolf
1c6d6788a3 chore: update changelog [skip ci] 2023-12-19 15:03:44 -05:00
Elliot DeNolf
ecc7978184 chore(release): plugin-nested-docs/1.0.10 [skip ci] 2023-12-19 14:49:38 -05:00
Elliot DeNolf
1b9ee64a67 chore(release): live-preview/0.2.2 [skip ci] 2023-12-19 14:48:20 -05:00
Elliot DeNolf
22b02226c3 chore(release): db-postgres/0.2.2 [skip ci] 2023-12-19 14:48:08 -05:00
Elliot DeNolf
e4102b88d8 chore(release): db-mongodb/1.1.1 [skip ci] 2023-12-19 14:47:41 -05:00
Elliot DeNolf
a099f55a69 chore(release): plugin-form-builder/1.1.0 [skip ci] 2023-12-19 14:46:49 -05:00
Elliot DeNolf
1f1445c798 chore(release): richtext-lexical/0.5.0 [skip ci] 2023-12-19 14:45:27 -05:00
Elliot DeNolf
741a5e3650 chore(release): payload/2.5.0 [skip ci] 2023-12-19 14:41:55 -05:00
Dan Ribbens
365047a3fb Merge branch 'feat/locale-specific-fallbacks' into fix/#4484-graphql-multiple-locales 2023-12-19 14:22:09 -05:00
Dan Ribbens
42c06acd18 docs: transaction options 2023-12-19 14:19:28 -05:00
Dan Ribbens
f2c8ac4a9a feat(db-mongodb): add transactionOptions 2023-12-19 14:19:12 -05:00
Dan Ribbens
05e8914db7 fix(db-mongodb): documentDB unique constraint throws incorrect error (#4513) 2023-12-19 14:14:51 -05:00
Dan Ribbens
35191bdd66 docs: improve docs for locales 2023-12-19 14:11:35 -05:00
Dan Ribbens
98890eee1f fix: graphql multiple locales 2023-12-19 14:00:06 -05:00
Ritsu
ef43629502 fix(db-postgres) incorrect currentTableName in find for blocks (#4524) 2023-12-19 10:30:13 -05:00
Dan Ribbens
c703497924 test: improve e2e locale change selector 2023-12-19 09:57:46 -05:00
Dan Ribbens
5caad706bb chore: consistent locale and fallback locale for globals 2023-12-19 09:40:26 -05:00
Dan Ribbens
aa048d5409 fix: req.locale and req.fallbackLocale get reassigned in local operations 2023-12-18 16:50:17 -05:00
Dan Ribbens
aafd538cf8 fix failing e2e test 2023-12-16 00:49:04 -05:00
Dan Ribbens
1b42bd207d fix failing tests 2023-12-16 00:18:59 -05:00
Dan Ribbens
9fac2ef24e feat: extend locales to have fallbackLocales 2023-12-15 23:52:12 -05:00
236 changed files with 4658 additions and 2408 deletions

View File

@@ -306,9 +306,10 @@ jobs:
with:
mongodb-version: 6.0
- name: Build Website
- name: Build Template
run: |
cd templates/${{ matrix.template }}
cp .env.example .env
yarn install
yarn build
yarn generate:types

101
.gitignore vendored
View File

@@ -1,7 +1,9 @@
coverage
package-lock.json
dist
.idea
/.idea/*
!/.idea/runConfigurations
test-results
.devcontainer
/migrations
@@ -230,119 +232,24 @@ GitHub.sublime-settings
.history
.ionide
### WebStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### WebStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Windows ###
# Windows thumbnail cache files
Thumbs.db
@@ -371,4 +278,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
/build
/build

View File

@@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev Fields" type="NodeJSConfigurationType" application-parameters="fields" path-to-js-file="node_modules/.pnpm/nodemon@3.0.1/node_modules/nodemon/bin/nodemon.js" working-dir="$PROJECT_DIR$">
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev _community" type="NodeJSConfigurationType" application-parameters="_community" path-to-js-file="node_modules/.pnpm/nodemon@3.0.1/node_modules/nodemon/bin/nodemon.js" working-dir="$PROJECT_DIR$">
<method v="2" />
</configuration>
</component>

View File

@@ -1,3 +1,186 @@
## [2.7.0](https://github.com/payloadcms/payload/compare/v2.6.0...v2.7.0) (2024-01-09)
### Features
* **db-mongodb:** improve transaction support by passing req to migrations ([682eca2](https://github.com/payloadcms/payload/commit/682eca21860a4e2b2ab0bfd85613818790247224))
* **db-postgres:** improve transaction support by passing req to migrations ([555d027](https://github.com/payloadcms/payload/commit/555d02769a8731aeebbff9b67f9b0e1022904ade))
* hasMany property for text fields ([#4605](https://github.com/payloadcms/payload/issues/4605)) ([f43cf18](https://github.com/payloadcms/payload/commit/f43cf185d45b3c75fa0d78acd91e6cb60d87f166))
* improve transaction support by passing req to migrations ([1d14d9f](https://github.com/payloadcms/payload/commit/1d14d9f8b8ed077691175030182f094bb300ed17))
* **plugin-seo:** add i18n ([#4665](https://github.com/payloadcms/payload/issues/4665)) ([3027a03](https://github.com/payloadcms/payload/commit/3027a03ad11ecd679278e44a013e4dea4aa42b8d))
* provide document info to ActionsProvider ([#4696](https://github.com/payloadcms/payload/issues/4696)) ([6a8a6e4](https://github.com/payloadcms/payload/commit/6a8a6e4ef4913e0889e4d2eac82b28b9e4e8db22))
### Bug Fixes
* adds objectID validation to isValidID if of type `text` ([#4689](https://github.com/payloadcms/payload/issues/4689)) ([d419275](https://github.com/payloadcms/payload/commit/d419275fb50f0922307f2d3b4c0fcf80ac5ec98b))
* allow json field to be saved empty and reflect value changes ([#4687](https://github.com/payloadcms/payload/issues/4687)) ([0fb3a9c](https://github.com/payloadcms/payload/commit/0fb3a9ca89d1b63faea179bfa9b5b3d0a69c9398))
* custom ids in versions ([#4680](https://github.com/payloadcms/payload/issues/4680)) ([5d15955](https://github.com/payloadcms/payload/commit/5d15955f839d3f0cc557d8a8d7cc3a9e52e2f6b1))
* custom overrides of breadcrumb and parent fields ([7db58b4](https://github.com/payloadcms/payload/commit/7db58b482bba7e715c5be23cfe1a84295e95da29))
* **db-mongodb:** migration error calling beginTransaction with transactionOptions false ([21b9453](https://github.com/payloadcms/payload/commit/21b9453cf4e6eebf145d89a0190942015658413d))
* **db-mongodb:** querying plan for collections ignoring indexes ([#4655](https://github.com/payloadcms/payload/issues/4655)) ([63bc4ca](https://github.com/payloadcms/payload/commit/63bc4cabe1dea5f233aa1d9d4e64f3af93a8e081))
* **db-postgres:** incorrect results querying json field using exists operator ([9d9ac0e](https://github.com/payloadcms/payload/commit/9d9ac0ec28c97281bfdc7d6fb78c52baea492380))
* **db-postgres:** migrate down only runs latest batch size ([6acfae8](https://github.com/payloadcms/payload/commit/6acfae8ee7614746797e1fa91e1fd41c0240fdcd))
* **db-postgres:** query on json properties ([ec4d2f9](https://github.com/payloadcms/payload/commit/ec4d2f97cbf1c89d837372059bf3bb77f3ea6594))
* **db-postgres:** validation prevents group fields in blocks ([#4699](https://github.com/payloadcms/payload/issues/4699)) ([cab6bab](https://github.com/payloadcms/payload/commit/cab6babd608daeaabf9b63b1b446fded6804b60f))
* non-boolean condition result causes infinite looping ([#4579](https://github.com/payloadcms/payload/issues/4579)) ([a3e7816](https://github.com/payloadcms/payload/commit/a3e78161b551e8286063a173645a1d3dee162ad1))
* **plugin-form-builder:** slate serializer should replace curly braces in links ([#4703](https://github.com/payloadcms/payload/issues/4703)) ([28a3012](https://github.com/payloadcms/payload/commit/28a30120dd1aa3279fb2133aa0a0b1638d144be4))
* **plugin-nested-docs:** breadcrumbsFieldSlug used in resaveSelfAfterCreate hook ([a5a91c0](https://github.com/payloadcms/payload/commit/a5a91c08a9ade1482c512d3fa4c4f519ad85cf74))
* **plugin-nested-docs:** children wrongly publishing draft data ([#4692](https://github.com/payloadcms/payload/issues/4692)) ([5539942](https://github.com/payloadcms/payload/commit/55399424a13b1e0532d9eeefd09d442c107c3eda))
* **plugin-nested-docs:** custom parent field slug ([635e7c2](https://github.com/payloadcms/payload/commit/635e7c26e8b3b5138cf5a9bcb29e8ddd4b1e69b6))
* **plugin-nested-docs:** parent filterOptions errors when specifying breadcrumbsFieldSlug ([c4a4678](https://github.com/payloadcms/payload/commit/c4a4678afb097cf94c682595a78e416767a1fea8))
* prevents row overflow ([#4704](https://github.com/payloadcms/payload/issues/4704)) ([9828772](https://github.com/payloadcms/payload/commit/98287728900cb88fa6a465899f030f81df28fc69))
* relations with number based ids (postgres) show untitled ID: x ([1b91408](https://github.com/payloadcms/payload/commit/1b914083c8ee0c1b1d64fa7d4471ede0a24cfdb7))
* sidebar fields not disabled by access permissions ([#4682](https://github.com/payloadcms/payload/issues/4682)) ([85e38b7](https://github.com/payloadcms/payload/commit/85e38b7cfd5c0772344c4a8fb5100f7c48eb508f))
* unlock user condition always passes due to seconds conversion ([#4610](https://github.com/payloadcms/payload/issues/4610)) ([d543665](https://github.com/payloadcms/payload/commit/d543665995410256f77fe136173339aee6dcc7da))
## [2.6.0](https://github.com/payloadcms/payload/compare/v2.5.0...v2.6.0) (2024-01-03)
### Features
* **db-mongodb:** add transactionOptions ([f2c8ac4](https://github.com/payloadcms/payload/commit/f2c8ac4a9aa9120339af6759170f5a708469698d))
* extend locales to have fallbackLocales ([9fac2ef](https://github.com/payloadcms/payload/commit/9fac2ef24e2ade4cf55b0d6a0e7f67e0edf57539))
### Bug Fixes
* "The punycode module is deprecated" warning by updating nodemailer ([00d8480](https://github.com/payloadcms/payload/commit/00d8480062d99dee56ef61a955f48a92efa6cbea))
* adjusts json field joi schema to allow editorOptions ([bff4cf5](https://github.com/payloadcms/payload/commit/bff4cf518f748efb9179f112c606d11d25db3d99))
* **db-postgres:** Wait for transaction to complete on commit ([#4582](https://github.com/payloadcms/payload/issues/4582)) ([a71d37b](https://github.com/payloadcms/payload/commit/a71d37b39806cd5956378a10246802d01d06c2dd))
* detect language from request headers accept-language ([#4656](https://github.com/payloadcms/payload/issues/4656)) ([69a9944](https://github.com/payloadcms/payload/commit/69a99445c9f1638a962a9c08ffe0bdc22e538bf6))
* graphql multiple locales ([98890ee](https://github.com/payloadcms/payload/commit/98890eee1f527c8f245b2353d7e1caca4d2a7d8c))
* navigation locks when modal is closed with esc ([#4664](https://github.com/payloadcms/payload/issues/4664)) ([be3beab](https://github.com/payloadcms/payload/commit/be3beabb9bafa137aa89e84cf47246017e969be8))
* req.locale and req.fallbackLocale get reassigned in local operations ([aa048d5](https://github.com/payloadcms/payload/commit/aa048d5409acd42b8f56367a16934085df9fbce2))
* resets actions array when navigating out of view with actions ([#4585](https://github.com/payloadcms/payload/issues/4585)) ([5c55231](https://github.com/payloadcms/payload/commit/5c5523195ccfa94a9bf42441e2a378f87836e64d))
* **richtext-lexical:** z-index issues ([#4570](https://github.com/payloadcms/payload/issues/4570)) ([8015e99](https://github.com/payloadcms/payload/commit/8015e999cd5834f532a200ef03fd392d04b3209f))
* tab field error when using the same interface name ([#4657](https://github.com/payloadcms/payload/issues/4657)) ([f1fa374](https://github.com/payloadcms/payload/commit/f1fa374ed12b50fdf210f17ae1dda603f09a9726))
## [2.5.0](https://github.com/payloadcms/payload/compare/v2.4.0...v2.5.0) (2023-12-19)
### Features
* add Chinese Traditional translation ([#4372](https://github.com/payloadcms/payload/issues/4372)) ([50253f6](https://github.com/payloadcms/payload/commit/50253f617c22d0d185bbac7f9d4304cddbc01f06))
* add context to auth and globals local API ([#4449](https://github.com/payloadcms/payload/issues/4449)) ([168d629](https://github.com/payloadcms/payload/commit/168d6296974042c3ff2a113f9f6c2bded7ba2b3e))
* adds new `actions` property to admin customization ([#4468](https://github.com/payloadcms/payload/issues/4468)) ([9e8f14a](https://github.com/payloadcms/payload/commit/9e8f14a897e77f6933eedb2410956a468f4187c3))
* async live preview urls ([#4339](https://github.com/payloadcms/payload/issues/4339)) ([5f17324](https://github.com/payloadcms/payload/commit/5f173241df6dc316d498767b1c81718e9c2b9a51))
* pass path to FieldDescription ([#4364](https://github.com/payloadcms/payload/issues/4364)) ([3b8a27d](https://github.com/payloadcms/payload/commit/3b8a27d199b3969cbca6ca750450798cb70f21e8))
* **plugin-form-builder:** Lexical support ([#4487](https://github.com/payloadcms/payload/issues/4487)) ([c6c5cab](https://github.com/payloadcms/payload/commit/c6c5cabfbb7eb954eea51170a6af7582b1f9b84b))
* prevent querying relationship when filterOptions returns false ([#4392](https://github.com/payloadcms/payload/issues/4392)) ([c1bd338](https://github.com/payloadcms/payload/commit/c1bd338d0d5e899f3892f1d18e355c00b265447a))
* **richtext-lexical:** improve floating select menu Dropdown classNames ([#4444](https://github.com/payloadcms/payload/issues/4444)) ([9331204](https://github.com/payloadcms/payload/commit/9331204295bfeaf7dd10bc075f42995b2cab2de4))
* **richtext-lexical:** improve link URL validation ([#4442](https://github.com/payloadcms/payload/issues/4442)) ([9babf68](https://github.com/payloadcms/payload/commit/9babf6804ce04d5828167eb8e7717727fe1cd472))
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server ([#4290](https://github.com/payloadcms/payload/issues/4290)) ([5de347f](https://github.com/payloadcms/payload/commit/5de347ffffca3bf38315d3d87d2ccc5c28cd2723))
* **richtext-lexical:** Link & Relationship Feature: field-level configurable allowed relationships ([#4182](https://github.com/payloadcms/payload/issues/4182)) ([7af8f29](https://github.com/payloadcms/payload/commit/7af8f29b4a8dddf389356e4db142f8d434cdc964))
* **richtext-lexical:** link node: change doc data format to be consistent with relationship field ([#4504](https://github.com/payloadcms/payload/issues/4504)) ([cc0ba89](https://github.com/payloadcms/payload/commit/cc0ba895188f40181c6ba3779f66d547d4ea66f9))
* **richtext-lexical:** rename TreeviewFeature into TreeViewFeature ([#4520](https://github.com/payloadcms/payload/issues/4520)) ([c49fd66](https://github.com/payloadcms/payload/commit/c49fd6692231b68ca61b079103a0fd7aa4673be1))
* **richtext-lexical:** Slate to Lexical converter: add blockquote conversion, convert custom link fields ([#4486](https://github.com/payloadcms/payload/issues/4486)) ([31f8f3c](https://github.com/payloadcms/payload/commit/31f8f3cac6bfd08f3adfa0a026a57c4b1b510045))
* **richtext-lexical:** Upload html serializer: Output picture element if the image has multiple sizes, improve absolute URL creation ([e558894](https://github.com/payloadcms/payload/commit/e55889480fceb8995646621923159d92de6e89c9))
### Bug Fixes
* adds bg color for year/month select options in datepicker ([#4508](https://github.com/payloadcms/payload/issues/4508)) ([07371b9](https://github.com/payloadcms/payload/commit/07371b9cad111999f2df4e1f709d6b95cd511c15))
* correctly fetches externally stored files when passing uploadEdits ([#4505](https://github.com/payloadcms/payload/issues/4505)) ([228d45c](https://github.com/payloadcms/payload/commit/228d45cf52e592cea6377cd93648fba75d73c88d))
* cursor jumping around inside json field ([#4453](https://github.com/payloadcms/payload/issues/4453)) ([6300037](https://github.com/payloadcms/payload/commit/63000373e66fb39443f882689e0ecf5c11ed8ad0))
* **db-mongodb:** documentDB unique constraint throws incorrect error ([#4513](https://github.com/payloadcms/payload/issues/4513)) ([05e8914](https://github.com/payloadcms/payload/commit/05e8914db70fa64bfb2d15ecfb58e9c229d71108))
* **db-postgres:** findOne correctly querying with where queries ([#4550](https://github.com/payloadcms/payload/issues/4550)) ([8bc31cd](https://github.com/payloadcms/payload/commit/8bc31cd5923517ab39ae1427aa0d0fb19d876dab))
* **db-postgres:** querying nested blocks fields ([#4404](https://github.com/payloadcms/payload/issues/4404)) ([6e9ae65](https://github.com/payloadcms/payload/commit/6e9ae65374124ee000cc2988ef77247c94b0dd18))
* **db-postgres:** sorting on a not-configured field throws error ([#4382](https://github.com/payloadcms/payload/issues/4382)) ([dbaecda](https://github.com/payloadcms/payload/commit/dbaecda0e92fcb0fa67b4c5ac085e025f02de53a))
* defaultValues computed on new globals ([#4380](https://github.com/payloadcms/payload/issues/4380)) ([b6cffce](https://github.com/payloadcms/payload/commit/b6cffcea07b9fa21698b00b8bbed6f27197ded41))
* disallow duplicate fieldNames to be used on the same level in the config ([#4381](https://github.com/payloadcms/payload/issues/4381)) ([a1d66b8](https://github.com/payloadcms/payload/commit/a1d66b83e0dbea21e8da549b73cd25c537a57938))
* ensure ui fields do not make it into gql schemas ([#4457](https://github.com/payloadcms/payload/issues/4457)) ([3a20ddc](https://github.com/payloadcms/payload/commit/3a20ddc5f85162a316006f22ba66ee1c7aab99e3))
* format fields within tab for list controls ([#4516](https://github.com/payloadcms/payload/issues/4516)) ([2650c70](https://github.com/payloadcms/payload/commit/2650c70960a7374307a8862c3940c97d337d1d30))
* formats locales with multiple labels for versions locale selector ([#4495](https://github.com/payloadcms/payload/issues/4495)) ([8257661](https://github.com/payloadcms/payload/commit/8257661c47b5b968a57fb2228d7045d876a3f484))
* graphql schema generation for fields without queryable subfields ([#4463](https://github.com/payloadcms/payload/issues/4463)) ([13e3e06](https://github.com/payloadcms/payload/commit/13e3e0671353ca34e603fece57a12199f2082ca0))
* handles null upload field values ([#4397](https://github.com/payloadcms/payload/issues/4397)) ([cf9a370](https://github.com/payloadcms/payload/commit/cf9a3704df21ce8b32feb0680793cba804cd66f7))
* **live-preview:** populates rte uploads and relationships ([#4379](https://github.com/payloadcms/payload/issues/4379)) ([4090aeb](https://github.com/payloadcms/payload/commit/4090aebb0e94e776258f0c1c761044a4744a1857))
* **live-preview:** sends raw js objects through window.postMessage instead of json ([#4354](https://github.com/payloadcms/payload/issues/4354)) ([03a3872](https://github.com/payloadcms/payload/commit/03a387233d1b8876a2fcaa5f3b3fd5ed512c0bc4))
* make admin navigation transition smoother ([#4217](https://github.com/payloadcms/payload/issues/4217)) ([eb6572e](https://github.com/payloadcms/payload/commit/eb6572e9e56e680cad331c1bc5da47e91306deb9))
* omit field default value if read access returns false ([#4518](https://github.com/payloadcms/payload/issues/4518)) ([3e9ef84](https://github.com/payloadcms/payload/commit/3e9ef849cd8e69e1e8d7f2f653f0647e93c8ab39))
* pin ts-node versions which are causing swc errors ([#4447](https://github.com/payloadcms/payload/issues/4447)) ([b9c0248](https://github.com/payloadcms/payload/commit/b9c024882350d14edd57f0f662a2269ed37975e3))
* properly spreads collection fields into non-tabbed configs [#50](https://github.com/payloadcms/payload/issues/50) ([#51](https://github.com/payloadcms/payload/issues/51)) ([7e88159](https://github.com/payloadcms/payload/commit/7e88159e99e2afdc10addc02cf299c11fe188be7))
* **plugin-form-builder:** removes use of slate in rich-text serializer ([#4451](https://github.com/payloadcms/payload/issues/4451)) ([3df52a8](https://github.com/payloadcms/payload/commit/3df52a88568622f8fafeabad47c7501229e4ea5f))
* **plugin-nested-docs:** properly exports field utilities ([#4462](https://github.com/payloadcms/payload/issues/4462)) ([1cc87bd](https://github.com/payloadcms/payload/commit/1cc87bd8ea575dfa2e1f5ce5b38414bbba95b2cb))
* **richtext-*:** loosen RichTextAdapter types due to re-occuring ts strict mode errors ([#4416](https://github.com/payloadcms/payload/issues/4416)) ([48f1299](https://github.com/payloadcms/payload/commit/48f1299fcba3e3811c6a7f31499f238537f9a5e3))
* **richtext-lexical:** Blocks field: should not prompt for unsaved changes due to value comparison between null and non-existent props ([#4450](https://github.com/payloadcms/payload/issues/4450)) ([548e78c](https://github.com/payloadcms/payload/commit/548e78c598cb6d029e7cc40f80d9855754f043bc))
* **richtext-lexical:** do not add unnecessary paragraph before upload, relationship and blocks nodes ([#4441](https://github.com/payloadcms/payload/issues/4441)) ([5c2739e](https://github.com/payloadcms/payload/commit/5c2739ebd144620cfd4ff02531f5812dd62ac61d))
* **richtext-lexical:** lexicalHTML field not working when used inside of Blocks field ([128f9c4](https://github.com/payloadcms/payload/commit/128f9c4e7e6e20dd1ee221f49428a5bce5179c5f))
* **richtext-lexical:** lexicalHTML field now works when used inside of row fields ([#4440](https://github.com/payloadcms/payload/issues/4440)) ([0421173](https://github.com/payloadcms/payload/commit/0421173f9e2d6db1b6a94b25884ea807921f2d09))
* **richtext-lexical:** not all types of URLs are validated correctly ([ac7f980](https://github.com/payloadcms/payload/commit/ac7f9809bc2b9fb6a52b48c10f7d51414801e4de))
* searching by id sends undefined in where query param ([#4464](https://github.com/payloadcms/payload/issues/4464)) ([46e8c01](https://github.com/payloadcms/payload/commit/46e8c01fbed68856be68804f2bd9744c4c6f5a95))
* simplifies query validation and fixes nested relationship fields ([#4391](https://github.com/payloadcms/payload/issues/4391)) ([4b5453e](https://github.com/payloadcms/payload/commit/4b5453e8e5484f7afcadbf5bccf8369b552969c6))
* updates return value of empty arrays in getDataByPath ([#4553](https://github.com/payloadcms/payload/issues/4553)) ([f3748a1](https://github.com/payloadcms/payload/commit/f3748a1534a13e6d844aadd9f0e3e6acbe483d03))
* upload editing error with plugin-cloud ([#4170](https://github.com/payloadcms/payload/issues/4170)) ([fcbe574](https://github.com/payloadcms/payload/commit/fcbe5744d945dc43642cdaa2007ddc252ecafafa))
* upload related issues, cropping, fetching local file, external preview image ([#4461](https://github.com/payloadcms/payload/issues/4461)) ([45c472d](https://github.com/payloadcms/payload/commit/45c472d6b35c41e597038089ad1755cdb88193b6))
* uploads files after validation ([#4218](https://github.com/payloadcms/payload/issues/4218)) ([65adfd2](https://github.com/payloadcms/payload/commit/65adfd21ed538b79628dc4f8ce9e1a5a1bba6aed))
### ⚠ BREAKING CHANGES
#### @payloadcms/richtext-lexical
* **richtext-lexical:** rename TreeviewFeature into TreeViewFeature ([#4520](https://github.com/payloadcms/payload/issues/4520)) ([c49fd66](https://github.com/payloadcms/payload/commit/c49fd6692231b68ca61b079103a0fd7aa4673be1))
If you import TreeviewFeature, you have to rename the import to use TreeViewFeature (capitalized "V")
* **richtext-lexical:** link node: change doc data format to be consistent with relationship field ([#4504](https://github.com/payloadcms/payload/issues/4504)) ([cc0ba89](https://github.com/payloadcms/payload/commit/cc0ba895188f40181c6ba3779f66d547d4ea66f9))
An unpopulated, internal link node no longer saves the doc id under fields.doc.value.id. Now, it saves it under fields.doc.value.
Migration inside of payload is automatic. If you are reading from the link node inside of your frontend though, you will have to adjust it.
* **richtext-lexical:** improve floating select menu Dropdown classNames ([#4444](https://github.com/payloadcms/payload/issues/4444)) ([9331204](https://github.com/payloadcms/payload/commit/9331204295bfeaf7dd10bc075f42995b2cab2de4))
Dropdown component has a new mandatory sectionKey prop
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server ([#4290](https://github.com/payloadcms/payload/issues/4290)) ([5de347f](https://github.com/payloadcms/payload/commit/5de347ffffca3bf38315d3d87d2ccc5c28cd2723))
1. Most important: If you are updating `@payloadcms/richtext-lexical` to v0.4.0 or higher, you will HAVE to update payload to the latest version as well. If you don't update it, payload likely won't start up due to validation errors. It's generally good practice to upgrade packages prefixed with @payloadcms/ together with payload and keep the versions in sync.
2. `@payloadcms/richtext-slate` is not affected by this.
3. Every single property in the `Feature` interface which accepts a React component now no longer accepts a React component, but a function which imports a React component instead. This is done to ensure no unnecessary client-only code is leaked to the server when importing Features on a server.
Here's an example migration:
Old:
```ts
import { BlockIcon } from '../../lexical/ui/icons/Block'
...
Icon: BlockIcon,
```
New:
```ts
// import { BlockIcon } from '../../lexical/ui/icons/Block' // <= Remove this import
...
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
```
Or alternatively, if you're using default exports instead of named exports:
```ts
// import BlockIcon from '../../lexical/ui/icons/Block' // <= Remove this import
...
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block'),
```
4. The types for `SanitizedEditorConfig` and `EditorConfig` have changed. Their respective `lexical` property no longer expects the `LexicalEditorConfig`. It now expects a function returning the `LexicalEditorConfig`. You will have to adjust this if you adjusted that property anywhere, e.g. when initializing the lexical field editor property, or when initializing a new headless editor.
5. The following exports are now exported from the `@payloadcms/richtext-lexical/components` subpath exports instead of `@payloadcms/richtext-lexical`:
- `ToolbarButton`
- `ToolbarDropdown`
- `RichTextCell`
- `RichTextField`
- `defaultEditorLexicalConfig`
You will have to adjust your imports, only if you import any of those properties in your project.
## @payloadcms/richtext-*
### [@payloadcms/richtext-lexical 0.4.1](https://github.com/payloadcms/payload/compare/richtext-lexical/0.4.0...richtext-lexical/0.4.1) (2023-12-07)

View File

@@ -534,14 +534,13 @@ When swapping out the `Field` component, you'll be responsible for sending and r
```tsx
import { useField } from 'payload/components/forms'
type Props = { path: string }
const CustomTextField: React.FC<Props> = ({ path }) => {
const CustomTextField: React.FC<{ path: string }> = ({ path }) => {
// highlight-start
const { value, setValue } = useField<Props>({ path })
const { value, setValue } = useField<string>({ path })
// highlight-end
return <input onChange={(e) => setValue(e.target.value)} value={value.path} />
return <input onChange={(e) => setValue(e.target.value)} value={value} />
}
```

View File

@@ -30,6 +30,26 @@ Payload Cloud gives you S3 file storage backed by Cloudflare as a CDN, and this
AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/plugin-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
#### Accessing Files Outside of Payload Cloud
If you'd like to access your files outside of Payload Cloud, you'll need to retrieve some values from your project's settings and put them into your environment variables. In Payload Cloud, navigate to the File Storage tab and copy the values using the copy button. Put these values in your .env file. Also copy the Cognito Password value separately and put into your .env file as well.
When you are done, you should have the following values in your .env file:
```env
PAYLOAD_CLOUD=true
PAYLOAD_CLOUD_ENVIRONMENT=prod
PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID=
PAYLOAD_CLOUD_COGNITO_USER_POOL_ID=
PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID=
PAYLOAD_CLOUD_PROJECT_ID=
PAYLOAD_CLOUD_BUCKET=
PAYLOAD_CLOUD_BUCKET_REGION=
PAYLOAD_CLOUD_COGNITO_PASSWORD=
```
The plugin will pick up these values and use them to access your files.
### Build Settings
You can update settings from your Projects Settings tab. Changes to your build settings will trigger a redeployment of your project.

View File

@@ -6,11 +6,13 @@ desc: Add and maintain as many locales as you need by adding Localization to you
keywords: localization, internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
Payload features deep field-based localization support. Maintaining as many locales as you need is easy. All localization support is opt-in by default. To do so, follow the two steps below.
Payload features deep field-based localization support. Maintaining as many locales as you need is easy. All
localization support is opt-in by default. To do so, follow the two steps below.
### Enabling in the Payload config
Add the `localization` property to your Payload config to enable localization project-wide. You'll need to provide a list of all locales that you'd like to support as well as set a few other options.
Add the `localization` property to your Payload config to enable localization project-wide. You'll need to provide a
list of all locales that you'd like to support as well as set a few other options.
**Example Payload config set up for localization:**
@@ -57,7 +59,8 @@ export default buildConfig({
})
```
**Example Payload config set up for localization with full locales objects (including [internationalization](/docs/configuration/i18n) support):**
**Example Payload config set up for localization with full locales objects (
including [internationalization](/docs/configuration/i18n) support):**
```ts
import { buildConfig } from 'payload/config'
@@ -93,35 +96,60 @@ export default buildConfig({
**`locales`**
Array-based list of all locales that you would like to support. These can be strings of locale codes or objects with a `label`, a locale `code`, and the `rtl` (right-to-left) property. The locale codes do not need to be in any specific format. It's up to you to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter language and country codes (ISO 31661) such as `en-US`, `en-UK`, `es-MX`, etc.
Array-based list of all the languages that you would like to support. This can be an array containing strings for each
language code you want your project to store and serve or objects with a `label`, a locale `code`, `rtl` (
right-to-left), and `fallbackLocale` property. The locale codes do not need to be in any specific format. It's up to you
to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter
language and country codes (ISO 31661) such as `en-US`, `en-UK`, `es-MX`, etc.
### Locale Properties:
| Option | Description |
|----------------------|--------------------------------------------------------------------------------------------------------------------------------|
| **`code`** \* | Unique code to identify the language throughout the APIs for `locale` and `fallbackLocale` |
| **`label`** | A string to use for the selector when choosing a language, or an object keyed on the i18n keys for different languages in use. |
| **`rtl`** | A boolean that when true will make the admin UI display in Right-To-Left. |
| **`fallbackLocale`** | The code for this language to fallback to when properties of a document are not present. |
_\* An asterisk denotes that a property is required._
**`defaultLocale`**
Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale.
Required string that matches one of the locale codes from the array provided. By default, if no locale is specified,
documents will be returned in this locale.
**`fallback`**
Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated.
Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a
localized value corresponding to the requested locale, then if this property is enabled, the document will automatically
fall back to the fallback locale value. If this property is not enabled, the value will not be populated.
### Field by field localization
Payload localization works on a **field** level—not a document level. In addition to configuring the base Payload config to support localization, you need to specify each field that you would like to localize.
Payload localization works on a **field** level—not a document level. In addition to configuring the base Payload config
to support localization, you need to specify each field that you would like to localize.
**Here is an example of how to enable localization for a field:**
```js
{
name: 'title',
type: 'text',
// highlight-start
localized: true,
type
:
'text',
// highlight-start
localized
:
true,
// highlight-end
}
```
With the above configuration, the `title` field will now be saved in the database as an object of all locales instead of a single string.
With the above configuration, the `title` field will now be saved in the database as an object of all locales instead of
a single string.
All field types with a `name` property support the `localized` property—even the more complex field types like `array`s and `block`s.
All field types with a `name` property support the `localized` property—even the more complex field types like `array`s
and `block`s.
<Banner>
<strong>Note:</strong>
@@ -143,7 +171,8 @@ All field types with a `name` property support the `localized` property—even t
### Retrieving localized docs
When retrieving documents, you can specify which locale you'd like to receive as well as which fallback locale should be used.
When retrieving documents, you can specify which locale you'd like to receive as well as which fallback locale should be
used.
##### REST API
@@ -155,7 +184,8 @@ Specify your desired locale by providing the `locale` query parameter directly i
**`?fallback-locale=`**
Specify fallback locale to be used by providing the `fallback-locale` query parameter. This can be provided as either a valid locale as provided to your base Payload config, or `'null'`, `'false'`, or `'none'` to disable falling back.
Specify fallback locale to be used by providing the `fallback-locale` query parameter. This can be provided as either a
valid locale as provided to your base Payload config, or `'null'`, `'false'`, or `'none'` to disable falling back.
**Example:**
@@ -167,7 +197,9 @@ fetch('https://localhost:3000/api/pages?locale=es&fallback-locale=none');
In the GraphQL API, you can specify `locale` and `fallbackLocale` args to all relevant queries and mutations.
The `locale` arg will only accept valid locales, but locales will be formatted automatically as valid GraphQL enum values (dashes or special characters will be converted to underscores, spaces will be removed, etc.). If you are curious to see how locales are auto-formatted, you can use the [GraphQL playground](/docs/graphql/overview#graphql-playground).
The `locale` arg will only accept valid locales, but locales will be formatted automatically as valid GraphQL enum
values (dashes or special characters will be converted to underscores, spaces will be removed, etc.). If you are curious
to see how locales are auto-formatted, you can use the [GraphQL playground](/docs/graphql/overview#graphql-playground).
The `fallbackLocale` arg will accept valid locales as well as `none` to disable falling back.
@@ -175,11 +207,11 @@ The `fallbackLocale` arg will accept valid locales as well as `none` to disable
```graphql
query {
Posts(locale: de, fallbackLocale: none) {
docs {
title
Posts(locale: de, fallbackLocale: none) {
docs {
title
}
}
}
}
```
@@ -191,7 +223,9 @@ query {
##### Local API
You can specify `locale` as well as `fallbackLocale` within the Local API as well as properties on the `options` argument. The `locale` property will accept any valid locale, and the `fallbackLocale` property will accept any valid locale as well as `'null'`, `'false'`, `false`, and `'none'`.
You can specify `locale` as well as `fallbackLocale` within the Local API as well as properties on the `options`
argument. The `locale` property will accept any valid locale, and the `fallbackLocale` property will accept any valid
locale as well as `'null'`, `'false'`, `false`, and `'none'`.
**Example:**

View File

@@ -6,7 +6,8 @@ keywords: database, migrations, ddl, sql, mongodb, postgres, documentation, Cont
desc: Payload features first-party database migrations all done in TypeScript.
---
Payload exposes a full suite of migration controls available for your use. Migration commands are accessible via the `npm run payload` command in your project directory.
Payload exposes a full suite of migration controls available for your use. Migration commands are accessible via
the `npm run payload` command in your project directory.
Ensure you have an npm script called "payload" in your `package.json` file.
@@ -24,33 +25,42 @@ Ensure you have an npm script called "payload" in your `package.json` file.
### Migration file contents
Payload stores all created migrations in a folder that you can specify. By default, migrations are stored in `./src/migrations`.
Payload stores all created migrations in a folder that you can specify. By default, migrations are stored
in `./src/migrations`.
A migration file has two exports - an `up` function, which is called when a migration is executed, and a `down` function that will be called if for some reason the migration fails to complete successfully. The `up` function should contain all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make.
A migration file has two exports - an `up` function, which is called when a migration is executed, and a `down` function
that will be called if for some reason the migration fails to complete successfully. The `up` function should contain
all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make.
For an added level of safety, migrations should leverage Payload [transactions](/docs/database/transactions).
For an added level of safety, migrations should leverage Payload [transactions](/docs/database/transactions). Migration
functions should make use of the `req` by adding it to the arguments of your payload local API calls such
as `payload.create` and database adapter methods like `payload.db.create`.
Here is an example migration file:
```ts
import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/your-db-adapter'
export async function up({ payload }: MigrateUpArgs): Promise<void> {
export async function up ({ payload, req }: MigrateUpArgs): Promise<void> {
// Perform changes to your database here.
// You have access to `payload` as an argument, and
// everything is done in TypeScript.
};
export async function down({ payload }: MigrateDownArgs): Promise<void> {
export async function down ({ payload, req }: MigrateDownArgs): Promise<void> {
// Do whatever you need to revert changes if the `up` function fails
};
```
### Migrations Directory
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc.
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be
stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your
migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc.
All database adapters should implement similar migration patterns, but there will be small differences based on the adapter and its specific needs. Below is a list of all migration commands that should be supported by your database adapter.
All database adapters should implement similar migration patterns, but there will be small differences based on the
adapter and its specific needs. Below is a list of all migration commands that should be supported by your database
adapter.
## Commands
@@ -64,7 +74,8 @@ npm run payload migrate
### Create
Create a new migration file in the migrations directory. You can optionally name the migration that will be created. By default, migrations will be named using a timestamp.
Create a new migration file in the migrations directory. You can optionally name the migration that will be created. By
default, migrations will be named using a timestamp.
```text
npm run payload migrate:create optional-name-here
@@ -72,7 +83,8 @@ npm run payload migrate:create optional-name-here
### Status
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run, and which migrations have not yet run.
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run,
and which migrations have not yet run.
`payload migrate:status`

View File

@@ -6,7 +6,8 @@ desc: Payload has supported MongoDB natively since we started. The flexible natu
keywords: MongoDB, documentation, typescript, Content Management System, cms, headless, javascript, node, react, express
---
To use Payload with MongoDB, install the package `@payloadcms/db-mongodb`. It will come with everything you need to store your Payload data in MongoDB.
To use Payload with MongoDB, install the package `@payloadcms/db-mongodb`. It will come with everything you need to
store your Payload data in MongoDB.
Then from there, pass it to your Payload config as follows:
@@ -29,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. |
| `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. |
| Option | Description |
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `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. | |
### Access to Mongoose models
After Payload is initialized, this adapter exposes all of your Mongoose models and they are available for you to work with directly.
After Payload is initialized, this adapter exposes all of your Mongoose models and they are available for you to work
with directly.
You can access Mongoose models as follows:

View File

@@ -48,7 +48,7 @@ const afterChange: CollectionAfterChangeHook = async ({ req }) => {
})
// Should this call fail, it will not rollback other changes
// because the req (and its transationID) is not passed through
// because the req (and its transactionID) is not passed through
const safelyIgnoredAsync = req.payload.create({
collection: 'my-slug',
data: {

View File

@@ -133,7 +133,7 @@ import * as React from 'react';
import { SelectInput, useField } from 'payload/components/forms';
import { useAuth } from 'payload/components/utilities';
type customSelectProps = {
type CustomSelectProps = {
path: string;
options: {
label: string;
@@ -164,7 +164,7 @@ export const CustomSelectComponent: React.FC<CustomSelectProps> = ({ path, optio
name={path}
options={adjustedOptions}
value={value}
onChange={() => setValue(e.value)}
onChange={(e) => setValue(e.value)}
/>
</div>
)

View File

@@ -38,7 +38,9 @@ keywords: text, fields, config, configuration, documentation, Content Management
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`hasMany`** | Makes this field an ordered array of text instead of just a single text. |
| **`minRows`** | Minimum number of texts in the array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of texts in the array, if `hasMany` is set to true. |
_\* An asterisk denotes that a property is required._
### Admin config

View File

@@ -357,7 +357,7 @@ const result = await payload.unlock({
```js
// Returned result will be a boolean representing success or failure
const result = await payload.verify({
const result = await payload.verifyEmail({
collection: 'users', // required
token: 'afh3o2jf2p3f...', // the token saved on the user as `_verificationToken`
})

View File

@@ -8,11 +8,21 @@ keywords: plugins, nested, documents, parent, child, sibling, relationship
[![NPM](https://img.shields.io/npm/v/@payloadcms/plugin-nested-docs)](https://www.npmjs.com/package/@payloadcms/plugin-nested-docs)
This plugin allows you to easily nest the documents of your application inside of one another. It does so by adding a new `parent` field onto each of your documents that, when selected, attaches itself to the parent's tree. When you edit the great-great-grandparent of a document, for instance, all of its descendants are recursively updated. This is an extremely powerful way of achieving hierarchy within a collection, such as parent/child relationship between pages.
This plugin allows you to easily nest the documents of your application inside of one another. It does so by adding a
new `parent` field onto each of your documents that, when selected, attaches itself to the parent's tree. When you edit
the great-great-grandparent of a document, for instance, all of its descendants are recursively updated. This is an
extremely powerful way of achieving hierarchy within a collection, such as parent/child relationship between pages.
Documents also receive a new `breadcrumbs` field. Once a parent is assigned, these breadcrumbs are populated based on each ancestor up the tree. Breadcrumbs allow you to dynamically generate labels and URLs based on the document's position in the hierarchy. Even if the slug of a parent document changes, or the entire tree is nested another level deep, changes will cascade down the entire tree and all breadcrumbs will reflect those changes.
Documents also receive a new `breadcrumbs` field. Once a parent is assigned, these breadcrumbs are populated based on
each ancestor up the tree. Breadcrumbs allow you to dynamically generate labels and URLs based on the document's
position in the hierarchy. Even if the slug of a parent document changes, or the entire tree is nested another level
deep, changes will cascade down the entire tree and all breadcrumbs will reflect those changes.
With this pattern you can perform whatever side-effects your applications needs on even the most deeply nested documents. For example, you could easily add a custom `fullTitle` field onto each document and inject the parent's title onto it, such as "Parent Title > Child Title". This would allow you to then perform searches and filters based on _that_ field instead of the original title. This is especially useful if you happen to have two documents with identical titles but different parents.
With this pattern you can perform whatever side-effects your applications needs on even the most deeply nested
documents. For example, you could easily add a custom `fullTitle` field onto each document and inject the parent's title
onto it, such as "Parent Title > Child Title". This would allow you to then perform searches and filters based on _that_
field instead of the original title. This is especially useful if you happen to have two documents with identical titles
but different parents.
<Banner type="info">
This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-nested-docs). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20nested-docs&template=bug_report.md&title=plugin-nested-docs%3A) with as much detail as possible.
@@ -29,7 +39,8 @@ With this pattern you can perform whatever side-effects your applications needs
## Installation
Install the plugin using any JavaScript package manager like [Yarn](https://yarnpkg.com), [NPM](https://npmjs.com), or [PNPM](https://pnpm.io):
Install the plugin using any JavaScript package manager like [Yarn](https://yarnpkg.com), [NPM](https://npmjs.com),
or [PNPM](https://pnpm.io):
```bash
yarn add @payloadcms/plugin-nested-docs
@@ -37,7 +48,8 @@ Install the plugin using any JavaScript package manager like [Yarn](https://yarn
## Basic Usage
In the `plugins` array of your [Payload config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options):
In the `plugins` array of your [Payload config](https://payloadcms.com/docs/configuration/overview), call the plugin
with [options](#options):
```ts
import { buildConfig } from 'payload/config'
@@ -75,16 +87,18 @@ export default config
#### Parent
The `parent` relationship field is automatically added to every document which allows editors to choose another document from the same collection to act as the direct parent.
The `parent` relationship field is automatically added to every document which allows editors to choose another document
from the same collection to act as the direct parent.
#### Breadcrumbs
The `breadcrumbs` field is an array which dynamically populates all parent relationships of a document up to the top level and stores the following fields.
The `breadcrumbs` field is an array which dynamically populates all parent relationships of a document up to the top
level and stores the following fields.
| Field | Description |
| ------------ | --------------------------------------------------------------------------- |
| `label` | The label of the breadcrumb. This field is automatically set to either the `collection.admin.useAsTitle` (if defined) or is set to the `ID` of the document. You can also dynamically define the `label` by passing a function to the options property of [`generateLabel`](#generateLabel). |
| `url` | The URL of the breadcrumb. By default, this field is undefined. You can manually define this field by passing a property called function to the plugin options property of [`generateURL`](#generateURL). |
| Field | Description |
|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `label` | The label of the breadcrumb. This field is automatically set to either the `collection.admin.useAsTitle` (if defined) or is set to the `ID` of the document. You can also dynamically define the `label` by passing a function to the options property of [`generateLabel`](#generateLabel). |
| `url` | The URL of the breadcrumb. By default, this field is undefined. You can manually define this field by passing a property called function to the plugin options property of [`generateURL`](#generateURL). |
### Options
@@ -94,7 +108,8 @@ An array of collections slugs to enable nested docs.
#### `generateLabel`
Each `breadcrumb` has a required `label` field. By default, its value will be set to the collection's `admin.useAsTitle` or fallback the the `ID` of the document.
Each `breadcrumb` has a required `label` field. By default, its value will be set to the collection's `admin.useAsTitle`
or fallback the the `ID` of the document.
You can also pass a function to dynamically set the `label` of your breadcrumb.
@@ -108,14 +123,16 @@ nestedDocs({
The function takes two arguments and returns a string:
| Argument | Type | Description |
| ------------ | -------- | --------------------------------------------------------------------------- |
| `docs` | `Array` | An array of the breadcrumbs up to that point |
| `doc` | `Object` | The current document being edited |
| Argument | Type | Description |
|----------|----------|----------------------------------------------|
| `docs` | `Array` | An array of the breadcrumbs up to that point |
| `doc` | `Object` | The current document being edited |
#### `generateURL`
A function that allows you to dynamically generate each breadcrumb `url`. Each `breadcrumb` has an optional `url` field which is undefined by default. For example, you might want to format a full URL to contain all of the breadcrumbs up to that point, like `/about-us/company/our-team`.
A function that allows you to dynamically generate each breadcrumb `url`. Each `breadcrumb` has an optional `url` field
which is undefined by default. For example, you might want to format a full URL to contain all breadcrumbs up to
that point, like `/about-us/company/our-team`.
```ts
// payload.config.ts
@@ -125,18 +142,21 @@ nestedDocs({
})
```
| Argument | Type | Description |
| ------------ | -------- | --------------------------------------------------------------------------- |
| `docs` | `Array` | An array of the breadcrumbs up to that point |
| `doc` | `Object` | The current document being edited |
| Argument | Type | Description |
|----------|----------|----------------------------------------------|
| `docs` | `Array` | An array of the breadcrumbs up to that point |
| `doc` | `Object` | The current document being edited |
#### `parentFieldSlug`
When defined, the `parent` field will not be provided for you automatically, and instead, expects you to add your own `parent` field to each collection manually. This gives you complete control over where you put the field in your admin dashboard, etc. Set this property to the `name` of your custom field.
When defined, the `parent` field will not be provided for you automatically, and instead, expects you to add your
own `parent` field to each collection manually. This gives you complete control over where you put the field in your
admin dashboard, etc. Set this property to the `name` of your custom field.
#### `breadcrumbsFieldSlug`
When defined, the `breadcrumbs` field will not be provided for you, and instead, expects your to add your own `breadcrumbs` field to each collection manually. Set this property to the `name` of your custom field.
When defined, the `breadcrumbs` field will not be provided for you, and instead, expects you to add your
own `breadcrumbs` field to each collection manually. Set this property to the `name` of your custom field.
<Banner type="info">
<strong>Note:</strong>
@@ -146,7 +166,8 @@ When defined, the `breadcrumbs` field will not be provided for you, and instead,
## Overrides
You can also extend the built-in `parent` and `breadcrumbs` fields per collection by using the `createParentField` and `createBreadcrumbField` methods. They will merge your customizations overtop the plugin's base field configurations.
You can also extend the built-in `parent` and `breadcrumbs` fields per collection by using the `createParentField`
and `createBreadcrumbField` methods. They will merge your customizations overtop the plugin's base field configurations.
```ts
import { CollectionConfig } from "payload/types";
@@ -187,9 +208,17 @@ const examplePageConfig: CollectionConfig = {
};
```
<Banner type="warning">
<strong>Note:</strong>
<br />
If overriding the `name` of either `breadcrumbs` or `parent` fields, you must specify the `breadcrumbsFieldSlug` or `parentFieldSlug` respectively.
</Banner>
## Localization
This plugin supports localization by default. If the `localization` property is set in your Payload config, the `breadcrumbs` field is automatically localized. For more details on how localization works in Payload, see the [Localization](https://payloadcms.com/docs/localization/overview) docs.
This plugin supports localization by default. If the `localization` property is set in your Payload config,
the `breadcrumbs` field is automatically localized. For more details on how localization works in Payload, see
the [Localization](https://payloadcms.com/docs/localization/overview) docs.
## TypeScript
@@ -201,4 +230,10 @@ import { PluginConfig, GenerateURL, GenerateLabel } from '@payloadcms/plugin-nes
## Examples
The [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) contains an official [Nested Docs Plugin Example](https://github.com/payloadcms/payload/tree/main/examples/nested-docs) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website) and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere), both of which use this plugin.
The [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) contains an
official [Nested Docs Plugin Example](https://github.com/payloadcms/payload/tree/main/examples/nested-docs) which
demonstrates exactly how to configure this plugin in Payload and implement it on your front-end.
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an
official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website)
and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere), both of which use this
plugin.

View File

@@ -287,5 +287,5 @@ import {
## Examples
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. You can also check out [How to Build An E-Commerce Site With Next.js](https://payloadcms.com/blog/how-to-build-an-e-commerce-site-with-nextjs) post for a bit more context around this template.
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. You can also check out [How to Build An E-Commerce Site With Next.js](https://payloadcms.com/blog/how-to-build-an-e-commerce-site-with-nextjs) post for a bit more context around this template.

View File

@@ -4301,9 +4301,9 @@ focus-trap@^6.9.2:
tabbable "^5.3.3"
follow-redirects@^1.15.2:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
for-each@^0.3.3:
version "0.3.3"

View File

@@ -80,7 +80,7 @@
"lexical": "0.12.5",
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "8.13.0",
"mongodb-memory-server": "^9",
"node-fetch": "2.6.12",
"nodemon": "3.0.2",
"prettier": "^3.0.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.1.0",
"version": "1.3.0",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -23,16 +23,18 @@
"bson-objectid": "2.0.4",
"deepmerge": "4.3.1",
"get-port": "5.1.1",
"mongoose": "6.12.0",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"http-status": "1.6.2",
"uuid": "9.0.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
"mongodb-memory-server": "8.13.0",
"mongodb": "4.17.1",
"mongodb-memory-server": "^9",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -48,6 +48,13 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
try {
this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection
const client = this.connection.getClient()
if (!client.options.replicaSet || this.transactionOptions === false) {
this.transactionOptions = false
this.beginTransaction = undefined
}
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info('---- DROPPING DATABASE ----')
await mongoose.connection.dropDatabase()

View File

@@ -3,9 +3,8 @@ import type { Document, PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from '.'
import handleError from './utilities/handleError'
import { withSession } from './withSession'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
export const create: Create = async function create(
this: MongooseAdapter,
@@ -17,18 +16,7 @@ export const create: Create = async function create(
try {
;[doc] = await Model.create([data], options)
} catch (error) {
// Handle uniqueness error from MongoDB
throw error.code === 11000 && error.keyValue
? new ValidationError(
[
{
field: Object.keys(error.keyValue)[0],
message: req.t('error:valueMustBeUnique'),
},
],
req?.t ?? i18nInit(this.payload.config.i18n).t,
)
: error
handleError(error, req)
}
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here

View File

@@ -55,8 +55,11 @@ export const find: Find = async function find(
useEstimatedCount,
}
if (!useEstimatedCount && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint.
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,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {

View File

@@ -74,8 +74,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
useEstimatedCount,
}
if (!useEstimatedCount && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint.
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,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {

View File

@@ -71,8 +71,11 @@ export const findVersions: FindVersions = async function findVersions(
useEstimatedCount,
}
if (!useEstimatedCount && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint.
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,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {

View File

@@ -1,3 +1,4 @@
import type { TransactionOptions } from 'mongodb'
import type { ClientSession, ConnectOptions, Connection } from 'mongoose'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
@@ -7,8 +8,6 @@ import mongoose from 'mongoose'
import path from 'path'
import { createDatabaseAdapter } from 'payload/database'
export type { MigrateDownArgs, MigrateUpArgs } from './types'
import type { CollectionModel, GlobalModel } from './types'
import { connect } from './connect'
@@ -39,6 +38,8 @@ import { updateGlobalVersion } from './updateGlobalVersion'
import { updateOne } from './updateOne'
import { updateVersion } from './updateVersion'
export type { MigrateDownArgs, MigrateUpArgs } from './types'
export interface Args {
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
autoPluralization?: boolean
@@ -50,6 +51,7 @@ export interface Args {
/** 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
migrationDir?: string
transactionOptions?: TransactionOptions | false
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
}
@@ -81,6 +83,7 @@ declare module 'payload' {
globals: GlobalModel
mongoMemoryServer: any
sessions: Record<number | string, ClientSession>
transactionOptions: TransactionOptions
versions: {
[slug: string]: CollectionModel
}
@@ -92,15 +95,21 @@ export function mongooseAdapter({
connectOptions,
disableIndexHints = false,
migrationDir: migrationDirArg,
transactionOptions,
url,
}: Args): MongooseAdapterResult {
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(migrationDirArg)
let beginTransactionFunction = beginTransaction
mongoose.set('strictQuery', false)
extendWebpackConfig(payload.config)
extendViteConfig(payload.config)
if (transactionOptions === false) {
beginTransactionFunction = () => null
}
return createDatabaseAdapter<MongooseAdapter>({
name: 'mongoose',
@@ -113,11 +122,12 @@ export function mongooseAdapter({
globals: undefined,
mongoMemoryServer: undefined,
sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url,
versions: {},
// DatabaseAdapter
beginTransaction,
beginTransaction: beginTransactionFunction,
commitTransaction,
connect,
create,

View File

@@ -1,6 +1,9 @@
import type { PayloadRequest } from 'payload/types'
import { readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import prompts from 'prompts'
import type { MongooseAdapter } from '.'
@@ -14,9 +17,9 @@ export async function migrateFresh(this: MongooseAdapter): Promise<void> {
const { confirm: acceptWarning } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
type: 'confirm',
},
{
onCancel: () => {
@@ -40,29 +43,29 @@ export async function migrateFresh(this: MongooseAdapter): Promise<void> {
msg: `Found ${migrationFiles.length} migration files.`,
})
let transactionID
const req = {} as PayloadRequest
// Run all migrate up
for (const migration of migrationFiles) {
payload.logger.info({ msg: `Migrating: ${migration.name}` })
try {
const start = Date.now()
transactionID = await this.beginTransaction()
await migration.up({ payload })
await initTransaction(req)
await migration.up({ payload, req })
await payload.create({
collection: 'payload-migrations',
data: {
name: migration.name,
batch: 1,
},
req: {
transactionID,
} as PayloadRequest,
req,
})
await this.commitTransaction(transactionID)
await commitTransaction(req)
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
} catch (err: unknown) {
await this.rollbackTransaction(transactionID)
await killTransaction(req)
payload.logger.error({
err,
msg: `Error running migration ${migration.name}. Rolling back.`,

View File

@@ -547,7 +547,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: field.hasMany ? [String] : String,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),

View File

@@ -58,8 +58,15 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
useEstimatedCount,
}
if (!useEstimatedCount && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint.
if (
!useEstimatedCount &&
Object.keys(versionQuery).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,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
VersionModel.countDocuments(versionQuery, {

View File

@@ -1,34 +1,30 @@
// @ts-expect-error // TODO: Fix this import
import type { TransactionOptions } from 'mongodb'
import type { BeginTransaction } from 'payload/database'
import { APIError } from 'payload/errors'
import { v4 as uuid } from 'uuid'
let transactionsNotAvailable: boolean
import type { MongooseAdapter } from '../index'
export const beginTransaction: BeginTransaction = async function beginTransaction(
options: TransactionOptions = {},
this: MongooseAdapter,
options: TransactionOptions,
) {
let id = null
if (!this.connection) {
throw new APIError('beginTransaction called while no connection to the database exists')
}
if (transactionsNotAvailable) return id
const client = this.connection.getClient()
if (!client.options.replicaSet) {
transactionsNotAvailable = true
} else {
id = uuid()
if (!this.sessions[id]) {
this.sessions[id] = await client.startSession()
}
if (this.sessions[id].inTransaction()) {
this.payload.logger.warn('beginTransaction called while transaction already exists')
} else {
await this.sessions[id].startTransaction(options)
}
const id = uuid()
if (!this.sessions[id]) {
this.sessions[id] = client.startSession()
}
if (this.sessions[id].inTransaction()) {
this.payload.logger.warn('beginTransaction called while transaction already exists')
} else {
this.sessions[id].startTransaction(options || (this.transactionOptions as TransactionOptions))
}
return id
}

View File

@@ -1,11 +1,9 @@
import type { UpdateOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
import type { MongooseAdapter } from '.'
import handleError from './utilities/handleError'
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
import { withSession } from './withSession'
@@ -31,18 +29,7 @@ export const updateOne: UpdateOne = async function updateOne(
try {
result = await Model.findOneAndUpdate(query, data, options)
} catch (error) {
// Handle uniqueness error from MongoDB
throw error.code === 11000 && error.keyValue
? new ValidationError(
[
{
field: Object.keys(error.keyValue)[0],
message: 'Value must be unique',
},
],
req?.t ?? i18nInit(this.payload.config.i18n).t,
)
: error
handleError(error, req)
}
result = JSON.parse(JSON.stringify(result))

View File

@@ -0,0 +1,23 @@
import httpStatus from 'http-status'
import { APIError, ValidationError } from 'payload/errors'
const handleError = (error, req) => {
// Handle uniqueness error from MongoDB
if (error.code === 11000 && error.keyValue) {
throw new ValidationError(
[
{
field: Object.keys(error.keyValue)[0],
message: req.t('error:valueMustBeUnique'),
},
],
req.t,
)
} else if (error.code === 11000) {
throw new APIError(req.t('error:valueMustBeUnique'), httpStatus.BAD_REQUEST)
} else {
throw error
}
}
export default handleError

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.2.1",
"version": "0.3.0",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -33,6 +33,16 @@ export const buildFindManyArgs = ({
},
}
if (adapter.tables[`${tableName}_texts`]) {
result.with._texts = {
columns: {
id: false,
parent: false,
},
orderBy: ({ order }, { asc: ASC }) => [ASC(order)],
}
}
if (adapter.tables[`${tableName}_numbers`]) {
result.with._numbers = {
columns: {

View File

@@ -128,15 +128,16 @@ export const traverseFields = ({
with: {},
}
if (adapter.tables[`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}_locales`])
withBlock.with._locales = _locales
const tableName = `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`
if (adapter.tables[`${tableName}_locales`]) withBlock.with._locales = _locales
topLevelArgs.with[blockKey] = withBlock
traverseFields({
_locales,
adapter,
currentArgs: withBlock,
currentTableName,
currentTableName: tableName,
depth,
fields: block.fields,
path: '',

View File

@@ -9,6 +9,7 @@ import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { buildTable } from './schema/build'
import { getConfigIDType } from './schema/getConfigIDType'
export const init: Init = async function init(this: PostgresAdapter) {
if (this.payload.config.localization) {
@@ -23,6 +24,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildTexts: true,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!collection?.versions?.drafts,
@@ -36,8 +38,11 @@ export const init: Init = async function init(this: PostgresAdapter) {
const versionsTableName = `_${tableName}_v`
const versionFields = buildVersionCollectionFields(collection)
const versionsParentIDColType = getConfigIDType(collection.fields)
buildTable({
adapter: this,
buildTexts: true,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!collection.versions?.drafts,
@@ -54,6 +59,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildTexts: true,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!global?.versions?.drafts,
@@ -69,6 +75,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildTexts: true,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!global.versions?.drafts,

View File

@@ -1,8 +1,12 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { Payload } from 'payload'
import type { Migration } from 'payload/database'
import type { PayloadRequest } from 'payload/dist/express/types'
import { readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import prompts from 'prompts'
import type { PostgresAdapter } from './types'
@@ -42,11 +46,11 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
const { confirm: runMigrations } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message:
"It looks like you've run Payload in dev mode, meaning you've dynamically pushed changes to your database.\n\n" +
"If you'd like to run migrations, data loss will occur. Would you like to proceed?",
type: 'confirm',
},
{
onCancel: () => {
@@ -79,6 +83,7 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
const { generateDrizzleJson } = require('drizzle-kit/utils')
const start = Date.now()
const req = {} as PayloadRequest
payload.logger.info({ msg: `Migrating: ${migration.name}` })
@@ -86,7 +91,8 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
const drizzleJSON = generateDrizzleJson(pgAdapter.schema)
try {
await migration.up({ payload })
await initTransaction(req)
await migration.up({ payload, req })
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
await payload.create({
collection: 'payload-migrations',
@@ -95,8 +101,11 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
batch,
schema: drizzleJSON,
},
req,
})
await commitTransaction(req)
} catch (err: unknown) {
await killTransaction(req)
payload.logger.error({
err,
msg: parseError(err, `Error running migration ${migration.name}`),

View File

@@ -2,6 +2,9 @@
import type { PayloadRequest } from 'payload/types'
import { getMigrations, readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import type { PostgresAdapter } from './types'
@@ -25,19 +28,21 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`,
})
for (const migration of existingMigrations) {
const latestBatchMigrations = existingMigrations.filter(({ batch }) => batch === latestBatch)
for (const migration of latestBatchMigrations) {
const migrationFile = migrationFiles.find((m) => m.name === migration.name)
if (!migrationFile) {
throw new Error(`Migration ${migration.name} not found locally.`)
}
const start = Date.now()
let transactionID
const req = {} as PayloadRequest
try {
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })
transactionID = await this.beginTransaction()
await migrationFile.down({ payload })
await initTransaction(req)
await migrationFile.down({ payload, req })
payload.logger.info({
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
})
@@ -47,15 +52,13 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
await payload.delete({
id: migration.id,
collection: 'payload-migrations',
req: {
transactionID,
} as PayloadRequest,
req,
})
}
await this.commitTransaction(transactionID)
await commitTransaction(req)
} catch (err: unknown) {
await this.rollbackTransaction(transactionID)
await killTransaction(req)
payload.logger.error({
err,

View File

@@ -2,6 +2,9 @@ import type { PayloadRequest } from 'payload/types'
import { sql } from 'drizzle-orm'
import { readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import prompts from 'prompts'
import type { PostgresAdapter } from './types'
@@ -17,9 +20,9 @@ export async function migrateFresh(this: PostgresAdapter): Promise<void> {
const { confirm: acceptWarning } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
type: 'confirm',
},
{
onCancel: () => {
@@ -36,36 +39,35 @@ export async function migrateFresh(this: PostgresAdapter): Promise<void> {
msg: `Dropping database.`,
})
await this.drizzle.execute(sql`drop schema public cascade;\ncreate schema public;`)
await this.drizzle.execute(sql`drop schema public cascade;
create schema public;`)
const migrationFiles = await readMigrationFiles({ payload })
payload.logger.debug({
msg: `Found ${migrationFiles.length} migration files.`,
})
let transactionID
const req = {} as PayloadRequest
// Run all migrate up
for (const migration of migrationFiles) {
payload.logger.info({ msg: `Migrating: ${migration.name}` })
try {
const start = Date.now()
transactionID = await this.beginTransaction()
await migration.up({ payload })
await initTransaction(req)
await migration.up({ payload, req })
await payload.create({
collection: 'payload-migrations',
data: {
name: migration.name,
batch: 1,
},
req: {
transactionID,
} as PayloadRequest,
req,
})
await this.commitTransaction(transactionID)
await commitTransaction(req)
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
} catch (err: unknown) {
await this.rollbackTransaction(transactionID)
await killTransaction(req)
payload.logger.error({
err,
msg: parseError(err, `Error running migration ${migration.name}. Rolling back`),

View File

@@ -2,7 +2,9 @@
import type { PayloadRequest } from 'payload/types'
import { getMigrations, readMigrationFiles } from 'payload/database'
import { DatabaseError } from 'pg'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import type { PostgresAdapter } from './types'
@@ -29,7 +31,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`,
})
let transactionID
const req = {} as PayloadRequest
// Reverse order of migrations to rollback
existingMigrations.reverse()
@@ -43,8 +45,8 @@ export async function migrateRefresh(this: PostgresAdapter) {
payload.logger.info({ msg: `Migrating down: ${migration.name}` })
const start = Date.now()
transactionID = await this.beginTransaction()
await migrationFile.down({ payload })
await initTransaction(req)
await migrationFile.down({ payload, req })
payload.logger.info({
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
})
@@ -53,9 +55,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
if (tableExists) {
await payload.delete({
collection: 'payload-migrations',
req: {
transactionID,
} as PayloadRequest,
req,
where: {
name: {
equals: migration.name,
@@ -63,8 +63,9 @@ export async function migrateRefresh(this: PostgresAdapter) {
},
})
}
await commitTransaction(req)
} catch (err: unknown) {
await this.rollbackTransaction(transactionID)
await killTransaction(req)
payload.logger.error({
err,
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),
@@ -78,23 +79,21 @@ export async function migrateRefresh(this: PostgresAdapter) {
payload.logger.info({ msg: `Migrating: ${migration.name}` })
try {
const start = Date.now()
transactionID = await this.beginTransaction()
await migration.up({ payload })
await initTransaction(req)
await migration.up({ payload, req })
await payload.create({
collection: 'payload-migrations',
data: {
name: migration.name,
executed: true,
},
req: {
transactionID,
} as PayloadRequest,
req,
})
await this.commitTransaction(transactionID)
await commitTransaction(req)
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
} catch (err: unknown) {
await this.rollbackTransaction(transactionID)
await killTransaction(req)
payload.logger.error({
err,
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),

View File

@@ -2,6 +2,9 @@
import type { PayloadRequest } from 'payload/types'
import { getMigrations, readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import type { PostgresAdapter } from './types'
@@ -21,10 +24,10 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
return
}
const req = {} as PayloadRequest
// Rollback all migrations in order
for (const migration of existingMigrations) {
let transactionID
const migrationFile = migrationFiles.find((m) => m.name === migration.name)
try {
if (!migrationFile) {
@@ -33,8 +36,8 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
const start = Date.now()
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })
transactionID = await this.beginTransaction()
await migrationFile.down({ payload })
await initTransaction(req)
await migrationFile.down({ payload, req })
payload.logger.info({
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
})
@@ -44,19 +47,17 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
await payload.delete({
id: migration.id,
collection: 'payload-migrations',
req: {
transactionID,
} as PayloadRequest,
req,
})
}
await this.commitTransaction(transactionID)
await commitTransaction(req)
} catch (err: unknown) {
let msg = `Error running migration ${migrationFile.name}.`
if (err instanceof Error) msg += ` ${err.message}`
await this.rollbackTransaction(transactionID)
await killTransaction(req)
payload.logger.error({
err,
msg,

View File

@@ -107,7 +107,11 @@ export async function parseParams({
}
})
if (['json', 'richText'].includes(field.type) && Array.isArray(pathSegments)) {
if (
['json', 'richText'].includes(field.type) &&
Array.isArray(pathSegments) &&
pathSegments.length > 1
) {
const segments = pathSegments.slice(1)
segments.unshift(table[columnName].name)
@@ -121,12 +125,28 @@ export async function parseParams({
})
constraints.push(sql.raw(jsonQuery))
break
}
if (field.type === 'json') {
const jsonQuery = convertPathToJSONTraversal(pathSegments)
constraints.push(sql.raw(`${table[columnName].name}${jsonQuery} = '%${val}%'`))
const jsonQuery = convertPathToJSONTraversal(pathSegments)
const operatorKeys = {
contains: { operator: 'ilike', wildcard: '%' },
equals: { operator: '=', wildcard: '' },
exists: { operator: val === true ? 'is not null' : 'is null' },
like: { operator: 'like', wildcard: '%' },
not_equals: { operator: '<>', wildcard: '' },
}
let formattedValue = `'${operatorKeys[operator].wildcard}${val}${operatorKeys[operator].wildcard}'`
if (operator === 'exists') {
formattedValue = ''
}
constraints.push(
sql.raw(
`${table[columnName].name}${jsonQuery} ${operatorKeys[operator].operator} ${formattedValue}`,
),
)
break
}

View File

@@ -19,6 +19,7 @@ import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, PostgresAdapter } from '../types'
import { getConfigIDType } from './getConfigIDType'
import { parentIDColumnMap } from './parentIDColumnMap'
import { traverseFields } from './traverseFields'
@@ -26,6 +27,7 @@ type Args = {
adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
buildTexts?: boolean
buildNumbers?: boolean
buildRelationships?: boolean
disableNotNull: boolean
@@ -40,6 +42,7 @@ type Args = {
}
type Result = {
hasManyTextField: 'index' | boolean
hasManyNumberField: 'index' | boolean
relationsToBuild: Map<string, string>
}
@@ -48,6 +51,7 @@ export const buildTable = ({
adapter,
baseColumns = {},
baseExtraConfig = {},
buildTexts,
buildNumbers,
buildRelationships,
disableNotNull,
@@ -66,12 +70,15 @@ export const buildTable = ({
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
let hasManyNumberField: 'index' | boolean = false
let hasLocalizedManyTextField = false
let hasLocalizedManyNumberField = false
const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable
let textsTable: GenericTable
let numbersTable: GenericTable
// Relationships to the base collection
@@ -82,30 +89,25 @@ export const buildTable = ({
// Drizzle relations
const relationsToBuild: Map<string, string> = new Map()
const idField = fields.find((field) => fieldAffectsData(field) && field.name === 'id')
let idColType = 'integer'
const idColType = getConfigIDType(fields)
if (idField) {
if (idField.type === 'number') {
idColType = 'numeric'
columns.id = numeric('id').primaryKey()
}
if (idField.type === 'text') {
idColType = 'varchar'
columns.id = varchar('id').primaryKey()
}
} else {
columns.id = serial('id').primaryKey()
const idColTypeMap = {
integer: serial,
numeric,
varchar,
}
columns.id = idColTypeMap[idColType]('id').primaryKey()
;({
hasLocalizedField,
hasLocalizedManyTextField,
hasLocalizedManyNumberField,
hasLocalizedRelationshipField,
hasManyTextField,
hasManyNumberField,
} = traverseFields({
adapter,
buildTexts,
buildNumbers,
buildRelationships,
columns,
@@ -190,6 +192,50 @@ export const buildTable = ({
adapter.relations[`relations_${localeTableName}`] = localesTableRelations
}
if (hasManyTextField && buildTexts) {
const textsTableName = `${rootTableName}_texts`
const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
text: varchar('text'),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
path: varchar('path').notNull(),
}
if (hasLocalizedManyTextField) {
columns.locale = adapter.enums.enum__locales('locale')
}
textsTable = pgTable(textsTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index('order_parent_idx').on(cols.order, cols.parent),
}
if (hasManyTextField === 'index') {
indexes.text_idx = index('text_idx').on(cols.text)
}
if (hasLocalizedManyTextField) {
indexes.localeParent = index('locale_parent').on(cols.locale, cols.parent)
}
return indexes
})
adapter.tables[textsTableName] = textsTable
const textsTableRelations = relations(textsTable, ({ one }) => ({
parent: one(table, {
fields: [textsTable.parent],
references: [table.id],
}),
}))
adapter.relations[`relations_${textsTableName}`] = textsTableRelations
}
if (hasManyNumberField && buildNumbers) {
const numbersTableName = `${rootTableName}_numbers`
const columns: Record<string, PgColumnBuilder> = {
@@ -317,6 +363,9 @@ export const buildTable = ({
result._locales = many(localesTable)
}
if (hasManyTextField) {
result._texts = many(textsTable)
}
if (hasManyNumberField) {
result._numbers = many(numbersTable)
}
@@ -332,5 +381,5 @@ export const buildTable = ({
adapter.relations[`relations_${tableName}`] = tableRelations
return { hasManyNumberField, relationsToBuild }
return { hasManyTextField, hasManyNumberField, relationsToBuild }
}

View File

@@ -0,0 +1,17 @@
import { type Field, fieldAffectsData } from 'payload/types'
export const getConfigIDType = (fields: Field[]): string => {
const idField = fields.find((field) => fieldAffectsData(field) && field.name === 'id')
if (idField) {
if (idField.type === 'number') {
return 'numeric'
}
if (idField.type === 'text') {
return 'varchar'
}
}
return 'integer'
}

View File

@@ -32,6 +32,7 @@ import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdent
type Args = {
adapter: PostgresAdapter
buildTexts: boolean
buildNumbers: boolean
buildRelationships: boolean
columnPrefix?: string
@@ -55,13 +56,16 @@ type Args = {
type Result = {
hasLocalizedField: boolean
hasLocalizedManyTextField: boolean
hasLocalizedManyNumberField: boolean
hasLocalizedRelationshipField: boolean
hasManyTextField: 'index' | boolean
hasManyNumberField: 'index' | boolean
}
export const traverseFields = ({
adapter,
buildTexts,
buildNumbers,
buildRelationships,
columnPrefix,
@@ -84,6 +88,8 @@ export const traverseFields = ({
}: Args): Result => {
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
let hasLocalizedManyTextField = false
let hasManyNumberField: 'index' | boolean = false
let hasLocalizedManyNumberField = false
@@ -135,7 +141,28 @@ export const traverseFields = ({
}
switch (field.type) {
case 'text':
case 'text': {
if (field.hasMany) {
if (field.localized) {
hasLocalizedManyTextField = true
}
if (field.index) {
hasManyTextField = 'index'
} else if (!hasManyTextField) {
hasManyTextField = true
}
if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in Postgres for hasMany text fields.',
)
}
} else {
targetTable[fieldName] = varchar(columnName)
}
break
}
case 'email':
case 'code':
case 'textarea': {
@@ -286,21 +313,28 @@ export const traverseFields = ({
baseExtraConfig._localeIdx = (cols) => index('_locale_idx').on(cols._locale)
}
const { hasManyNumberField: subHasManyNumberField, relationsToBuild: subRelationsToBuild } =
buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
})
const {
hasManyTextField: subHasManyTextField,
hasManyNumberField: subHasManyNumberField,
relationsToBuild: subRelationsToBuild,
} = buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
})
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index')
hasManyTextField = subHasManyTextField
}
if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index')
hasManyNumberField = subHasManyNumberField
@@ -361,6 +395,7 @@ export const traverseFields = ({
}
const {
hasManyTextField: subHasManyTextField,
hasManyNumberField: subHasManyNumberField,
relationsToBuild: subRelationsToBuild,
} = buildTable({
@@ -377,6 +412,11 @@ export const traverseFields = ({
tableName: blockTableName,
})
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index')
hasManyTextField = subHasManyTextField
}
if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index')
hasManyNumberField = subHasManyNumberField
@@ -425,11 +465,14 @@ export const traverseFields = ({
if (!('name' in field)) {
const {
hasLocalizedField: groupHasLocalizedField,
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
hasManyTextField: groupHasManyTextField,
hasManyNumberField: groupHasManyNumberField,
} = traverseFields({
adapter,
buildTexts,
buildNumbers,
buildRelationships,
columnPrefix,
@@ -453,6 +496,8 @@ export const traverseFields = ({
if (groupHasLocalizedField) hasLocalizedField = true
if (groupHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (groupHasManyTextField) hasManyTextField = true
if (groupHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (groupHasManyNumberField) hasManyNumberField = true
if (groupHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break
@@ -462,11 +507,14 @@ export const traverseFields = ({
const {
hasLocalizedField: groupHasLocalizedField,
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
hasManyTextField: groupHasManyTextField,
hasManyNumberField: groupHasManyNumberField,
} = traverseFields({
adapter,
buildTexts,
buildNumbers,
buildRelationships,
columnPrefix: `${columnName}_`,
@@ -490,6 +538,8 @@ export const traverseFields = ({
if (groupHasLocalizedField) hasLocalizedField = true
if (groupHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (groupHasManyTextField) hasManyTextField = true
if (groupHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (groupHasManyNumberField) hasManyNumberField = true
if (groupHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break
@@ -500,11 +550,14 @@ export const traverseFields = ({
const {
hasLocalizedField: tabHasLocalizedField,
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
hasManyTextField: tabHasManyTextField,
hasManyNumberField: tabHasManyNumberField,
} = traverseFields({
adapter,
buildTexts,
buildNumbers,
buildRelationships,
columnPrefix,
@@ -528,9 +581,10 @@ export const traverseFields = ({
if (tabHasLocalizedField) hasLocalizedField = true
if (tabHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (tabHasManyTextField) hasManyTextField = true
if (tabHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (tabHasManyNumberField) hasManyNumberField = true
if (tabHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break
}
@@ -539,11 +593,14 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
hasManyTextField: rowHasManyTextField,
hasManyNumberField: rowHasManyNumberField,
} = traverseFields({
adapter,
buildTexts,
buildNumbers,
buildRelationships,
columnPrefix,
@@ -567,6 +624,8 @@ export const traverseFields = ({
if (rowHasLocalizedField) hasLocalizedField = true
if (rowHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (rowHasManyTextField) hasManyTextField = true
if (rowHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (rowHasManyNumberField) hasManyNumberField = true
if (rowHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break
@@ -604,8 +663,10 @@ export const traverseFields = ({
return {
hasLocalizedField,
hasLocalizedManyTextField,
hasLocalizedManyNumberField,
hasLocalizedRelationshipField,
hasManyTextField,
hasManyNumberField,
}
}

View File

@@ -1,7 +1,7 @@
import type { Block } from 'payload/types'
import type { Block, Field } from 'payload/types'
import { InvalidConfiguration } from 'payload/errors'
import { flattenTopLevelFields } from 'payload/utilities'
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/types'
import type { GenericTable } from '../types'
@@ -12,6 +12,42 @@ type Args = {
table: GenericTable
}
const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[] => {
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
if (field.type === 'blocks') {
return fieldsToUse
}
if (fieldHasSubFields(field)) {
fieldPrefix = 'name' in field ? `${prefix}${field.name}.` : prefix
return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)]
}
if (field.type === 'tabs') {
return [
...fieldsToUse,
...field.tabs.reduce((tabFields, tab) => {
fieldPrefix = 'name' in tab ? `${prefix}.${tab.name}` : prefix
return [
...tabFields,
...(tabHasName(tab)
? [{ ...tab, type: 'tab' }]
: getFlattenedFieldNames(tab.fields, fieldPrefix)),
]
}, []),
]
}
if (fieldAffectsData(field)) {
return [...fieldsToUse, `${fieldPrefix?.replace('.', '_') || ''}${field.name}`]
}
return fieldsToUse
}, [])
}
export const validateExistingBlockIsIdentical = ({
block,
localized,
@@ -19,17 +55,23 @@ export const validateExistingBlockIsIdentical = ({
table,
}: Args): void => {
if (table) {
const fieldNames = flattenTopLevelFields(block.fields).flatMap((field) => field.name)
const fieldNames = getFlattenedFieldNames(block.fields)
Object.keys(table).forEach((fieldName) => {
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
if (fieldNames.indexOf(fieldName) === -1) {
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${fieldName}, while the other block does not.`,
)
const missingField =
// ensure every field from the config is in the matching table
fieldNames.find((name) => Object.keys(table).indexOf(name) === -1) ||
// ensure every table column is matched for every field from the config
Object.keys(table).find((fieldName) => {
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
return fieldNames.indexOf(fieldName) === -1
}
}
})
})
if (missingField) {
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${missingField}, while the other block does not.`,
)
}
if (Boolean(localized) !== Boolean(table._locale)) {
throw new InvalidConfiguration(

View File

@@ -11,11 +11,11 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
try {
id = uuid()
let reject: (value?: unknown) => void
let resolve: (value?: unknown) => void
let reject: () => Promise<void>
let resolve: () => Promise<void>
let transaction: DrizzleTransaction
let transactionReady: (value?: unknown) => void
let transactionReady: () => void
// Drizzle only exposes a transactions API that is sufficient if you
// can directly pass around the `tx` argument. But our operations are spread
@@ -24,13 +24,19 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
// and will call them in our respective transaction methods
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.drizzle
const done = this.drizzle
.transaction(async (tx) => {
transaction = tx
await new Promise((res, rej) => {
await new Promise<void>((res, rej) => {
resolve = () => {
res()
return done
}
reject = () => {
rej()
return done
}
transactionReady()
resolve = res
reject = rej
})
})
.catch(() => {
@@ -39,7 +45,7 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
// Need to wait until the transaction is ready
// before binding its `resolve` and `reject` methods below
await new Promise((resolve) => (transactionReady = resolve))
await new Promise<void>((resolve) => (transactionReady = resolve))
this.sessions[id] = {
db: transaction,

View File

@@ -7,9 +7,9 @@ export const commitTransaction: CommitTransaction = async function commitTransac
}
try {
this.sessions[id].resolve()
await this.sessions[id].resolve()
} catch (err: unknown) {
this.sessions[id].reject()
await this.sessions[id].reject()
}
delete this.sessions[id]

View File

@@ -0,0 +1,19 @@
/* eslint-disable no-param-reassign */
import type { TextField } from 'payload/types'
type Args = {
field: TextField
locale?: string
textRows: Record<string, unknown>[]
ref: Record<string, unknown>
}
export const transformHasManyText = ({ field, locale, textRows, ref }: Args) => {
const result = textRows.map(({ text }) => text)
if (locale) {
ref[field.name][locale] = result
} else {
ref[field.name] = result
}
}

View File

@@ -18,6 +18,7 @@ type TransformArgs = {
// into the shape Payload expects based on field schema
export const transform = <T extends TypeWithID>({ config, data, fields }: TransformArgs): T => {
let relationships: Record<string, Record<string, unknown>[]> = {}
let texts: Record<string, Record<string, unknown>[]> = {}
let numbers: Record<string, Record<string, unknown>[]> = {}
if ('_rels' in data) {
@@ -25,6 +26,11 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
delete data._rels
}
if ('_texts' in data) {
texts = createPathMap(data._texts)
delete data._texts
}
if ('_numbers' in data) {
numbers = createPathMap(data._numbers)
delete data._numbers
@@ -42,6 +48,7 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
deletions,
fieldPrefix: '',
fields,
texts,
numbers,
path: '',
relationships,

View File

@@ -8,6 +8,7 @@ import type { BlocksMap } from '../../utilities/createBlocksMap'
import { transformHasManyNumber } from './hasManyNumber'
import { transformRelationship } from './relationship'
import { transformHasManyText } from './hasManyText'
type TraverseFieldsArgs = {
/**
@@ -34,6 +35,10 @@ type TraverseFieldsArgs = {
* An array of Payload fields to traverse
*/
fields: (Field | TabAsField)[]
/**
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
/**
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path
*/
@@ -61,6 +66,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields,
texts,
numbers,
path,
relationships,
@@ -77,6 +83,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
texts,
numbers,
path,
relationships,
@@ -96,6 +103,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields: field.fields,
texts,
numbers,
path,
relationships,
@@ -127,6 +135,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: '',
fields: field.fields,
texts,
numbers,
path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships,
@@ -151,6 +160,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: '',
fields: field.fields,
texts,
numbers,
path: `${sanitizedPath}${field.name}.${i}`,
relationships,
@@ -194,6 +204,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: '',
fields: block.fields,
texts,
numbers,
path: `${blockFieldPath}.${row._order - 1}`,
relationships,
@@ -224,6 +235,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: '',
fields: block.fields,
texts,
numbers,
path: `${blockFieldPath}.${i}`,
relationships,
@@ -285,6 +297,40 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (field.type === 'text' && field?.hasMany) {
const textPathMatch = texts[`${sanitizedPath}${field.name}`]
if (!textPathMatch) return result
if (field.localized) {
result[field.name] = {}
const textsByLocale: Record<string, Record<string, unknown>[]> = {}
textPathMatch.forEach((row) => {
if (typeof row.locale === 'string') {
if (!textsByLocale[row.locale]) textsByLocale[row.locale] = []
textsByLocale[row.locale].push(row)
}
})
Object.entries(textsByLocale).forEach(([locale, texts]) => {
transformHasManyText({
field,
locale,
textRows: texts,
ref: result,
})
})
} else {
transformHasManyText({
field,
textRows: textPathMatch,
ref: result,
})
}
return result
}
if (field.type === 'number' && field.hasMany) {
const numberPathMatch = numbers[`${sanitizedPath}${field.name}`]
if (!numberPathMatch) return result
@@ -374,6 +420,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.fields,
texts,
numbers,
path: `${sanitizedPath}${field.name}`,
relationships,
@@ -390,6 +437,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.fields,
texts,
numbers,
path: `${sanitizedPath}${field.name}`,
relationships,
@@ -400,6 +448,21 @@ export const traverseFields = <T extends Record<string, unknown>>({
break
}
case 'text': {
let val = fieldData
if (typeof fieldData === 'string') {
val = String(fieldData)
}
if (typeof locale === 'string') {
ref[locale] = val
} else {
result[field.name] = val
}
break
}
case 'number': {
let val = fieldData
if (typeof fieldData === 'string') {

View File

@@ -18,6 +18,7 @@ type Args = {
data: unknown
field: ArrayField
locale?: string
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[]
path: string
relationships: Record<string, unknown>[]
@@ -36,6 +37,7 @@ export const transformArray = ({
data,
field,
locale,
texts,
numbers,
path,
relationships,
@@ -86,6 +88,7 @@ export const transformArray = ({
fieldPrefix: '',
fields: field.fields,
locales: newRow.locales,
texts,
numbers,
parentTableName: arrayTableName,
path: `${path || ''}${field.name}.${i}.`,

View File

@@ -18,6 +18,7 @@ type Args = {
data: Record<string, unknown>[]
field: BlockField
locale?: string
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[]
path: string
relationships: Record<string, unknown>[]
@@ -34,6 +35,7 @@ export const transformBlocks = ({
data,
field,
locale,
texts,
numbers,
path,
relationships,
@@ -84,6 +86,7 @@ export const transformBlocks = ({
fieldPrefix: '',
fields: matchedBlock.fields,
locales: newRow.locales,
texts,
numbers,
parentTableName: blockTableName,
path: `${path || ''}${field.name}.${i}.`,

View File

@@ -27,6 +27,7 @@ export const transformForWrite = ({
blocks: {},
blocksToDelete: new Set(),
locales: {},
texts: [],
numbers: [],
relationships: [],
relationshipsToDelete: [],
@@ -47,6 +48,7 @@ export const transformForWrite = ({
fieldPrefix: '',
fields,
locales: rowToInsert.locales,
texts: rowToInsert.texts,
numbers: rowToInsert.numbers,
parentTableName: tableName,
path,

View File

@@ -0,0 +1,15 @@
type Args = {
baseRow: Record<string, unknown>
data: unknown[]
texts: Record<string, unknown>[]
}
export const transformTexts = ({ baseRow, data, texts }: Args) => {
data.forEach((val, i) => {
texts.push({
...baseRow,
text: val,
order: i + 1,
})
})
}

View File

@@ -13,6 +13,7 @@ import { transformBlocks } from './blocks'
import { transformNumbers } from './numbers'
import { transformRelationship } from './relationships'
import { transformSelects } from './selects'
import { transformTexts } from './texts'
type Args = {
adapter: PostgresAdapter
@@ -44,6 +45,7 @@ type Args = {
locales: {
[locale: string]: Record<string, unknown>
}
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[]
/**
* This is the name of the parent table
@@ -71,6 +73,7 @@ export const traverseFields = ({
fields,
forcedLocale,
locales,
texts,
numbers,
parentTableName,
path,
@@ -108,6 +111,7 @@ export const traverseFields = ({
data: localeData,
field,
locale: localeKey,
texts,
numbers,
path,
relationships,
@@ -128,6 +132,7 @@ export const traverseFields = ({
blocksToDelete,
data: data[field.name],
field,
texts,
numbers,
path,
relationships,
@@ -158,6 +163,7 @@ export const traverseFields = ({
data: localeData,
field,
locale: localeKey,
texts,
numbers,
path,
relationships,
@@ -175,6 +181,7 @@ export const traverseFields = ({
blocksToDelete,
data: fieldData,
field,
texts,
numbers,
path,
relationships,
@@ -203,6 +210,7 @@ export const traverseFields = ({
fields: field.fields,
forcedLocale: localeKey,
locales,
texts,
numbers,
parentTableName,
path: `${path || ''}${field.name}.`,
@@ -225,6 +233,7 @@ export const traverseFields = ({
fieldPrefix: `${fieldName}_`,
fields: field.fields,
locales,
texts,
numbers,
parentTableName,
path: `${path || ''}${field.name}.`,
@@ -258,6 +267,7 @@ export const traverseFields = ({
fields: tab.fields,
forcedLocale: localeKey,
locales,
texts,
numbers,
parentTableName,
path: `${path || ''}${tab.name}.`,
@@ -280,6 +290,7 @@ export const traverseFields = ({
fieldPrefix: `${fieldPrefix || ''}${tab.name}_`,
fields: tab.fields,
locales,
texts,
numbers,
parentTableName,
path: `${path || ''}${tab.name}.`,
@@ -303,6 +314,7 @@ export const traverseFields = ({
fieldPrefix,
fields: tab.fields,
locales,
texts,
numbers,
parentTableName,
path,
@@ -328,6 +340,7 @@ export const traverseFields = ({
fieldPrefix,
fields: field.fields,
locales,
texts,
numbers,
parentTableName,
path,
@@ -382,6 +395,37 @@ export const traverseFields = ({
return
}
if (field.type === 'text' && field.hasMany) {
const textPath = `${path || ''}${field.name}`
if (field.localized) {
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
transformTexts({
baseRow: {
locale: localeKey,
path: textPath,
},
data: localeData,
texts,
})
}
})
}
} else if (Array.isArray(fieldData)) {
transformTexts({
baseRow: {
path: textPath,
},
data: fieldData,
texts,
})
}
return
}
if (field.type === 'number' && field.hasMany) {
const numberPath = `${path || ''}${field.name}`

View File

@@ -34,6 +34,7 @@ export type RowToInsert = {
locales: {
[locale: string]: Record<string, unknown>
}
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[]
relationships: Record<string, unknown>[]
relationshipsToDelete: RelationshipToDelete[]

View File

@@ -56,8 +56,8 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
sessions: {
[id: string]: {
db: DrizzleTransaction
reject: () => void
resolve: () => void
reject: () => Promise<void>
resolve: () => Promise<void>
}
}
tables: Record<string, GenericTable>
@@ -86,8 +86,8 @@ declare module 'payload' {
sessions: {
[id: string]: {
db: DrizzleTransaction
reject: () => void
resolve: () => void
reject: () => Promise<void>
resolve: () => Promise<void>
}
}
tables: Record<string, GenericTable>

View File

@@ -68,6 +68,7 @@ export const upsertRow = async <T extends TypeWithID>({
const localesToInsert: Record<string, unknown>[] = []
const relationsToInsert: Record<string, unknown>[] = []
const textsToInsert: Record<string, unknown>[] = []
const numbersToInsert: Record<string, unknown>[] = []
const blocksToInsert: { [blockType: string]: BlockRowToInsert[] } = {}
const selectsToInsert: { [selectTableName: string]: Record<string, unknown>[] } = {}
@@ -89,6 +90,14 @@ export const upsertRow = async <T extends TypeWithID>({
})
}
// If there are texts, add parent to each
if (rowToInsert.texts.length > 0) {
rowToInsert.texts.forEach((textRow) => {
textRow.parent = insertedRow.id
textsToInsert.push(textRow)
})
}
// If there are numbers, add parent to each
if (rowToInsert.numbers.length > 0) {
rowToInsert.numbers.forEach((numberRow) => {
@@ -161,6 +170,29 @@ export const upsertRow = async <T extends TypeWithID>({
await db.insert(adapter.tables[relationshipsTableName]).values(relationsToInsert)
}
// //////////////////////////////////
// INSERT hasMany TEXTS
// //////////////////////////////////
const textsTableName = `${tableName}_texts`
if (operation === 'update') {
await deleteExistingRowsByPath({
adapter,
db,
localeColumnName: 'locale',
parentColumnName: 'parent',
parentID: insertedRow.id,
pathColumnName: 'path',
rows: textsToInsert,
tableName: textsTableName,
})
}
if (textsToInsert.length > 0) {
await db.insert(adapter.tables[textsTableName]).values(textsToInsert).returning()
}
// //////////////////////////////////
// INSERT hasMany NUMBERS
// //////////////////////////////////

View File

@@ -62,7 +62,7 @@ module.exports = {
'partition-by-comment': true,
groups: ['top', 'unknown'],
'custom-groups': {
top: ['_id', 'id', 'name'],
top: ['_id', 'id', 'name', 'slug', 'type'],
},
},
],

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/eslint-config",
"version": "1.0.0",
"version": "1.1.0",
"description": "Payload styles for ESLint and Prettier",
"license": "MIT",
"author": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "0.2.1",
"version": "0.2.2",
"description": "The official live preview JavaScript SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.4.0",
"version": "2.7.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -105,7 +105,7 @@
"minimist": "1.2.8",
"mkdirp": "1.0.4",
"monaco-editor": "0.38.0",
"nodemailer": "6.9.4",
"nodemailer": "6.9.8",
"object-to-formdata": "4.5.1",
"passport": "0.6.0",
"passport-anonymous": "1.0.1",
@@ -166,7 +166,7 @@
"@types/minimist": "1.2.2",
"@types/mkdirp": "1.0.2",
"@types/node-fetch": "2.6.4",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.14",
"@types/passport": "1.0.12",
"@types/passport-anonymous": "1.0.3",
"@types/passport-jwt": "3.0.9",

View File

@@ -10,6 +10,7 @@ import { filterFields } from '../../forms/RenderFields/filterFields'
import { Gutter } from '../Gutter'
import ViewDescription from '../ViewDescription'
import './index.scss'
import { useOperation } from '../../utilities/OperationProvider'
const baseClass = 'document-fields'
@@ -34,12 +35,15 @@ export const DocumentFields: React.FC<{
permissions,
} = props
const operation = useOperation()
const sidebarFields = filterFields({
fieldSchema: fields,
fieldTypes,
filter: (field) => field?.admin?.position === 'sidebar',
permissions: permissions.fields,
readOnly: !hasSavePermission,
operation,
})
const hasSidebarFields = sidebarFields && sidebarFields.length > 0

View File

@@ -54,7 +54,10 @@ export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (prop
? checkIsActive
: location.pathname.startsWith(href)
if (!condition || (condition && condition({ collection, config, documentInfo, global }))) {
if (
!condition ||
(condition && Boolean(condition({ collection, config, documentInfo, global })))
) {
const labelToRender = typeof label === 'function' ? label({ t }) : label
const pillToRender = typeof pillLabel === 'function' ? pillLabel({ versions }) : pillLabel

View File

@@ -25,6 +25,12 @@ $header-height: base(5);
}
}
[dir='rtl']
&__actions {
margin-right: auto;
margin-left: unset;
}
&__actions {
min-width: 350px;
margin-left: auto;

View File

@@ -12,6 +12,10 @@
// place the localizer outside the `overflow: hidden` container so that the popup is visible
// this means we need to use a placeholder div so that the space is retained in the DOM
[dir='rtl'] &__localizer {
right: unset;
left: base(4.5)
}
&__localizer {
position: absolute;
top: 50%;

View File

@@ -133,7 +133,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1
useEffect(() => {
const { admin: { listSearchableFields } = {}, slug } = selectedCollectionConfig
const { slug, admin: { listSearchableFields } = {} } = selectedCollectionConfig
const params: {
cacheBust?: number
limit?: number

View File

@@ -6,7 +6,10 @@
flex-direction: column;
text-align: left;
gap: 3px;
[dir='rtl']
&__text-align--left {
text-align: right;
}
&__text-align--left {
text-align: left;
}
@@ -14,11 +17,15 @@
&__text-align--center {
text-align: center;
}
[dir='rtl']
&__text-align--right {
text-align: left;
}
&__text-align--right {
text-align: right;
}
&__button {
@extend %btn-reset;
padding-left: var(--list-button-padding);

View File

@@ -80,13 +80,18 @@
////////////////////////////////
// HORIZONTAL ALIGNMENT
////////////////////////////////
[dir='rtl']
&--h-align-left {
.popup__caret {
right: var(--popup-padding);
left: unset
}
}
&--h-align-left {
.popup__caret {
left: var(--popup-padding);
}
}
&--h-align-center {
.popup__content {
left: 50%;
@@ -99,6 +104,19 @@
}
}
[dir='rtl']
&--h-align-right {
.popup__content {
right: unset;
left:0
}
.popup__caret {
right: unset;
left: var(--popup-padding)
}
}
&--h-align-right {
.popup__content {
right: 0;

View File

@@ -97,7 +97,7 @@ const RelationshipField: React.FC<Props> = (props) => {
}
}
} else {
setErrorLoading(t('errors:unspecific'))
setErrorLoading(t('error:unspecific'))
}
}
}, Promise.resolve())

View File

@@ -47,7 +47,7 @@ export const iterateFields = async ({
if (!fieldIsPresentationalOnly(field) && !field?.admin?.disabled) {
const passesCondition = Boolean(
(field?.admin?.condition
? field.admin.condition(fullData || {}, initialData || {}, { user })
? Boolean(field.admin.condition(fullData || {}, initialData || {}, { user }))
: true) && parentPassesCondition,
)

View File

@@ -54,10 +54,10 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
// Besides those who still fail their own conditions
if (passesCondition && field.condition) {
passesCondition = field.condition(
reduceFieldsToValues(state, true),
getSiblingData(state, path),
{ user },
passesCondition = Boolean(
field.condition(reduceFieldsToValues(state, true), getSiblingData(state, path), {
user,
}),
)
}

View File

@@ -13,6 +13,7 @@
& > .field-type {
margin-bottom: var(--spacing-field);
max-width: 100%;
&[type='hidden'] {
margin-bottom: 0;

View File

@@ -56,12 +56,11 @@ const JSONField: React.FC<Props> = (props) => {
const handleChange = useCallback(
(val) => {
if (readOnly) return
console.log(val)
setStringValue(val)
try {
setValue(JSON.parse(val))
if (readOnly) return
setStringValue(val)
setValue(val ? JSON.parse(val) : '')
setJsonError(undefined)
} catch (e) {
setJsonError(e)
@@ -71,10 +70,18 @@ const JSONField: React.FC<Props> = (props) => {
)
useEffect(() => {
if (hasLoadedValue) return
setStringValue(JSON.stringify(value ? value : initialValue, null, 2))
setHasLoadedValue(true)
}, [initialValue, value])
try {
const hasValue = value && value.toString().length > 0
if (hasLoadedValue) {
setStringValue(hasValue ? JSON.stringify(value, null, 2) : '')
} else {
setStringValue(JSON.stringify(hasValue ? value : initialValue, null, 2))
setHasLoadedValue(true)
}
} catch (e) {
setJsonError(e)
}
}, [initialValue, value, hasLoadedValue])
return (
<div

View File

@@ -4,9 +4,11 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import type { TextField } from '../../../../../fields/config/types'
import type { Option } from '../../../elements/ReactSelect/types'
import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import ReactSelect from '../../../elements/ReactSelect'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
@@ -21,6 +23,7 @@ export type TextInputProps = Omit<TextField, 'type'> & {
className?: string
description?: Description
errorMessage?: string
hasMany?: boolean
inputRef?: React.MutableRefObject<HTMLInputElement>
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
@@ -32,6 +35,7 @@ export type TextInputProps = Omit<TextField, 'type'> & {
showError?: boolean
style?: React.CSSProperties
value?: string
valueToRender?: Option[]
width?: string
}
@@ -44,8 +48,11 @@ const TextInput: React.FC<TextInputProps> = (props) => {
className,
description,
errorMessage,
hasMany,
inputRef,
label,
maxRows,
minRows,
onChange,
onKeyDown,
path,
@@ -56,17 +63,25 @@ const TextInput: React.FC<TextInputProps> = (props) => {
showError,
style,
value,
valueToRender,
width,
} = props
const { i18n } = useTranslation()
const { i18n, t } = useTranslation()
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
return (
<div
className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only']
className={[
fieldBaseClass,
'text',
className,
showError && 'error',
readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
.join(' ')}
style={{
@@ -78,18 +93,45 @@ const TextInput: React.FC<TextInputProps> = (props) => {
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
disabled={readOnly}
filterOption={(option, rawInput) => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
return !isOverHasMany
}}
isClearable
isCreatable
isMulti
isSortable
noOptionsMessage={({ inputValue }) => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return t('general:noOptions')
}}
onChange={onChange}
options={[]}
placeholder={t('general:enterAValue')}
showError={showError}
value={valueToRender}
/>
) : (
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
)}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
<FieldDescription

View File

@@ -3,8 +3,10 @@
.field-type.text {
position: relative;
input {
@include formInput;
&:not(.has-many) {
input {
@include formInput;
}
}
}

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import type { Props } from './types'
@@ -24,11 +24,14 @@ const Text: React.FC<Props> = (props) => {
style,
width,
} = {},
hasMany,
inputRef,
label,
localized,
maxLength,
maxRows,
minLength,
minRows,
path: pathFromProps,
required,
validate = text,
@@ -58,6 +61,50 @@ const Text: React.FC<Props> = (props) => {
validate: memoizedValidate,
})
const handleOnChange = (e) => {
setValue(e.target.value)
}
const handleHasManyChange = useCallback(
(selectedOption) => {
if (!readOnly) {
let newValue
if (!selectedOption) {
newValue = []
} else if (Array.isArray(selectedOption)) {
newValue = selectedOption.map((option) => option.value?.value || option.value)
} else {
newValue = [selectedOption.value?.value || selectedOption.value]
}
setValue(newValue)
}
},
[readOnly, setValue],
)
const [valueToRender, setValueToRender] = useState<
{ id: string; label: string; value: { value: string } }[]
>([]) // Only for hasMany
// useeffect update valueToRender:
useEffect(() => {
if (hasMany && Array.isArray(value)) {
setValueToRender(
value.map((val, index) => {
return {
id: `${val}${index}`, // append index to avoid duplicate keys but allow duplicate numbers
label: `${val}`,
value: {
toString: () => `${val}${index}`,
value: val?.value || val,
}, // You're probably wondering, why the hell is this done that way? Well, React-select automatically uses "label-value" as a key, so we will get that react duplicate key warning if we just pass in the value as multiple values can be the same. So we need to append the index to the toString() of the value to avoid that warning, as it uses that as the key.
}
}),
)
}
}, [value, hasMany])
return (
<TextInput
Error={Error}
@@ -67,12 +114,13 @@ const Text: React.FC<Props> = (props) => {
className={className}
description={description}
errorMessage={errorMessage}
hasMany={hasMany}
inputRef={inputRef}
label={label}
maxRows={maxRows}
minRows={minRows}
name={name}
onChange={(e) => {
setValue(e.target.value)
}}
onChange={hasMany ? handleHasManyChange : handleOnChange}
path={path}
placeholder={placeholder}
readOnly={readOnly}
@@ -81,6 +129,7 @@ const Text: React.FC<Props> = (props) => {
showError={showError}
style={style}
value={value}
valueToRender={valueToRender}
width={width}
/>
)

View File

@@ -36,7 +36,7 @@ export const WatchCondition: React.FC<Props> = ({
data.id = id
const hasCondition = Boolean(condition)
const isPassingCondition = hasCondition ? condition(data, siblingData, { user }) : true
const isPassingCondition = hasCondition ? Boolean(condition(data, siblingData, { user })) : true
const field = fields[path]
const wasPassingCondition = field?.passesCondition

View File

@@ -17,9 +17,16 @@ const Component: React.FC<{
onCancel: () => void
onConfirm: () => void
}> = ({ isActive, onCancel, onConfirm }) => {
const { closeModal, openModal } = useModal()
const { closeModal, openModal, modalState } = useModal()
const { t } = useTranslation('general')
// Manually check for modal state as 'esc' key will not trigger the nav inactivity
useEffect(() => {
if (!modalState?.[modalSlug]?.isOpen && isActive) {
onCancel()
}
}, [modalState])
useEffect(() => {
if (isActive) openModal(modalSlug)
else closeModal(modalSlug)

View File

@@ -21,6 +21,11 @@
}
}
}
[dir='rtl']
&__nav-toggler-wrapper {
left: unset;
right: 0;
}
&__nav-toggler-wrapper {
position: fixed;

View File

@@ -219,6 +219,10 @@ export const API: React.FC<EditViewProps> = (props) => {
editConfig && 'API' in editConfig && 'actions' in editConfig.API ? editConfig.API.actions : []
setViewActions(apiActions)
return () => {
setViewActions([])
}
}, [collection, global, setViewActions])
const localeOptions =

View File

@@ -25,23 +25,27 @@ const AccountView: React.FC = () => {
const { user } = useAuth()
const userRef = useRef(user)
const [internalState, setInternalState] = useState<Fields>()
const { id, docPermissions, getDocPermissions, getDocPreferences, preferencesKey, slug } =
useDocumentInfo()
const {
id,
slug,
collection,
docPermissions,
getDocPermissions,
getDocPreferences,
preferencesKey,
} = useDocumentInfo()
const { getPreference } = usePreferences()
const config = useConfig()
const {
admin: { components: { views: { Account: CustomAccountComponent } = {} } = {} },
collections,
routes: { api },
serverURL,
} = useConfig()
const { t } = useTranslation('authentication')
const collection = collections.find((coll) => coll.slug === slug)
const { fields } = collection || {}
const [{ data, isLoading: isLoadingData }] = usePayloadAPI(`${serverURL}${api}/${slug}/${id}`, {

View File

@@ -58,6 +58,10 @@ const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
: []
setViewActions(defaultActions)
return () => {
setViewActions([])
}
}, [global.slug, location.pathname, global?.admin?.components?.views?.Edit, setViewActions])
return (

View File

@@ -40,7 +40,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
const { reportUpdate } = useDocumentEvents()
const { admin: { components: { views: { Edit: Edit } = {} } = {} } = {}, fields, slug } = global
const { slug, admin: { components: { views: { Edit: Edit } = {} } = {} } = {}, fields } = global
const onSave = useCallback(
async (json) => {

View File

@@ -193,6 +193,10 @@ export const LivePreviewView: React.FC<
: []
setViewActions(livePreviewActions)
return () => {
setViewActions([])
}
}, [collection, global, setViewActions])
const breakpoints: LivePreviewConfig['breakpoints'] = [

View File

@@ -184,6 +184,10 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
: []
setViewActions(versionActions)
return () => {
setViewActions([])
}
}, [collection, global, setViewActions])
let metaTitle: string

View File

@@ -136,6 +136,10 @@ const VersionsView: React.FC<IndexProps> = (props) => {
: []
setViewActions(versionsActions)
return () => {
setViewActions([])
}
}, [collection, global, setViewActions])
return (

View File

@@ -91,6 +91,10 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
: []
setViewActions(defaultActions)
return () => {
setViewActions([])
}
}, [id, location.pathname, collection?.admin?.components?.views?.Edit, setViewActions])
return (

View File

@@ -5,7 +5,7 @@ import { fieldAffectsData } from '../../../../../fields/config/types'
const formatFields = (collection: SanitizedCollectionConfig, isEditing?: boolean): Field[] =>
isEditing
? collection.fields.filter((field) => (fieldAffectsData(field) && field.name !== 'id') || true)
? collection.fields.filter((field) => !fieldAffectsData(field) || field.name !== 'id')
: collection.fields
export default formatFields

View File

@@ -26,7 +26,7 @@ import formatFields from './formatFields'
const EditView: React.FC<IndexProps> = (props) => {
const { collection: incomingCollection, isEditing } = props
const { admin: { components: { views: { Edit } = {} } = {} } = {}, slug: collectionSlug } =
const { slug: collectionSlug, admin: { components: { views: { Edit } = {} } = {} } = {} } =
incomingCollection
const [fields] = useState(() => formatFields(incomingCollection, isEditing))

View File

@@ -83,6 +83,10 @@ const ListView: React.FC<ListIndexProps> = (props) => {
if (CustomList && typeof CustomList === 'object' && 'actions' in CustomList) {
setViewActions(CustomList.actions || [])
}
return () => {
setViewActions([])
}
}, [CustomList, setViewActions])
useEffect(() => {

View File

@@ -40,8 +40,14 @@ export const formatUseAsTitle = (args: {
}
const field = fieldFromProps || getObjectDotNotation<FormField>(doc, collection.admin.useAsTitle)
let title = typeof field === 'string' ? field : (field?.value as string)
let title: string
if (typeof field === 'string') {
title = field
} else if (typeof field === 'number') {
title = String(field)
} else {
title = field?.value as string
}
const fieldConfig = collection?.fields?.find((f) => 'name' in f && f?.name === useAsTitle)
const isDate = fieldConfig?.type === 'date'

View File

@@ -1,7 +1,8 @@
import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload'
import formatName from '../../../graphql/utilities/formatName'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import access from '../../operations/access'
const formatConfigNames = (results, configs) => {
@@ -19,7 +20,7 @@ const formatConfigNames = (results, configs) => {
function accessResolver(payload: Payload) {
async function resolver(_, args, context) {
const options = {
req: isolateTransactionID(context.req),
req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
}
const accessResults = await access(options)

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import forgotPassword from '../../operations/forgotPassword'
function forgotPasswordResolver(collection: Collection): any {
@@ -12,7 +13,7 @@ function forgotPasswordResolver(collection: Collection): any {
},
disableEmail: args.disableEmail,
expiration: args.expiration,
req: isolateTransactionID(context.req),
req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
}
await forgotPassword(options)

View File

@@ -1,11 +1,13 @@
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import type { PayloadRequest } from '../../../express/types'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import init from '../../operations/init'
function initResolver(collection: string) {
async function resolver(_, args, context) {
const options = {
collection,
req: isolateTransactionID(context.req),
req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
}
return init(options)

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import login from '../../operations/login'
function loginResolver(collection: Collection) {
@@ -12,7 +13,7 @@ function loginResolver(collection: Collection) {
password: args.password,
},
depth: 0,
req: isolateTransactionID(context.req),
req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
res: context.res,
}

View File

@@ -1,13 +1,14 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import logout from '../../operations/logout'
function logoutResolver(collection: Collection): any {
async function resolver(_, args, context) {
const options = {
collection,
req: isolateTransactionID(context.req),
req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
res: context.res,
}

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import me from '../../operations/me'
function meResolver(collection: Collection): any {
@@ -8,7 +9,7 @@ function meResolver(collection: Collection): any {
const options = {
collection,
depth: 0,
req: isolateTransactionID(context.req),
req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
}
return me(options)
}

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import getExtractJWT from '../../getExtractJWT'
import refresh from '../../operations/refresh'
@@ -18,7 +19,7 @@ function refreshResolver(collection: Collection) {
const options = {
collection,
depth: 0,
req: isolateTransactionID(context.req),
req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
res: context.res,
token,
}

View File

@@ -1,20 +1,24 @@
/* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import resetPassword from '../../operations/resetPassword'
function resetPasswordResolver(collection: Collection) {
async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale
let { req } = context
req = isolateObjectProperty(req, 'locale')
req = isolateObjectProperty(req, 'fallbackLocale')
if (args.locale) req.locale = args.locale
if (args.fallbackLocale) req.fallbackLocale = args.fallbackLocale
const options = {
api: 'GraphQL',
collection,
data: args,
depth: 0,
req: isolateTransactionID(context.req),
req: isolateObjectProperty<PayloadRequest>(req, 'transactionID'),
res: context.res,
}

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