Compare commits
151 Commits
db-mongodb
...
plugin-nes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9963b8d945 | ||
|
|
9afb838182 | ||
|
|
2dad129022 | ||
|
|
6af1c4d45d | ||
|
|
4e41dd1bf2 | ||
|
|
de02490231 | ||
|
|
1510baf46e | ||
|
|
c10db332cd | ||
|
|
0af9c4d398 | ||
|
|
fc137c0f53 | ||
|
|
a24f2be4a6 | ||
|
|
57999adfe2 | ||
|
|
7857043d03 | ||
|
|
3b93af734b | ||
|
|
50fab902bd | ||
|
|
724d80b7f4 | ||
|
|
b406e6afb9 | ||
|
|
3f46b21eb2 | ||
|
|
eed4f4361c | ||
|
|
05f3169a75 | ||
|
|
94f1443ce4 | ||
|
|
fc26275b7a | ||
|
|
e57f5e2aa0 | ||
|
|
dbfc83520c | ||
|
|
c068a8784e | ||
|
|
989c10e0e0 | ||
|
|
3bf2b7a3fe | ||
|
|
a6b486007d | ||
|
|
4e03ee7079 | ||
|
|
b91711a74a | ||
|
|
191c13a409 | ||
|
|
b210af4696 | ||
|
|
8cebd2ccce | ||
|
|
195a952c43 | ||
|
|
4bc5fa7086 | ||
|
|
2c8d34d2aa | ||
|
|
4ec5643dd7 | ||
|
|
45e9a559bb | ||
|
|
d6233cbf42 | ||
|
|
ad3e23b345 | ||
|
|
782e118569 | ||
|
|
dbfe4af993 | ||
|
|
859c2f4a6d | ||
|
|
a34d0f8274 | ||
|
|
967eff1aab | ||
|
|
b7041d6ab1 | ||
|
|
78b7bd62cd | ||
|
|
7329b1babd | ||
|
|
c87969b7f9 | ||
|
|
09f17f4450 | ||
|
|
615702b858 | ||
|
|
f0642ce031 | ||
|
|
56db87d2ec | ||
|
|
45c42724a4 | ||
|
|
a6d5f2e3de | ||
|
|
73b8549ef5 | ||
|
|
e22b95bdf3 | ||
|
|
56ddd2c388 | ||
|
|
803a37eaa9 | ||
|
|
d308bb3421 | ||
|
|
cbc4752ecb | ||
|
|
c51f9d01cb | ||
|
|
d19d8fd232 | ||
|
|
5dbbd8f88b | ||
|
|
47bd3894c4 | ||
|
|
a57c68cd04 | ||
|
|
acad2888cd | ||
|
|
db2da71357 | ||
|
|
cbb4ce2f51 | ||
|
|
47efd3b92e | ||
|
|
348a70cc33 | ||
|
|
9f873f8630 | ||
|
|
949e265cd9 | ||
|
|
687f4850ac | ||
|
|
1f851f21b1 | ||
|
|
dbc4ce71e6 | ||
|
|
cef4cbb0ee | ||
|
|
7059a71243 | ||
|
|
01559ef34b | ||
|
|
8488f7b8db | ||
|
|
a92a160a13 | ||
|
|
77a7c83251 | ||
|
|
2ad7340154 | ||
|
|
c462df38f6 | ||
|
|
fff377ad22 | ||
|
|
a2cb946155 | ||
|
|
c39472259a | ||
|
|
e2d36c3cab | ||
|
|
0e682a32c3 | ||
|
|
266c3274d0 | ||
|
|
67b3baaa44 | ||
|
|
55659c7c36 | ||
|
|
6a0a859563 | ||
|
|
57da3c99a7 | ||
|
|
611438177b | ||
|
|
d068ef7e24 | ||
|
|
7a9af4417a | ||
|
|
8d14c213c8 | ||
|
|
182c57b191 | ||
|
|
15459fb8e3 | ||
|
|
3ca71c4def | ||
|
|
64136a6b17 | ||
|
|
acba5e482b | ||
|
|
571f190f34 | ||
|
|
131d89c3f5 | ||
|
|
55c38a8934 | ||
|
|
2abb46f4f1 | ||
|
|
f41780ef33 | ||
|
|
105392cf07 | ||
|
|
fa2e68ad1c | ||
|
|
2053e4eeab | ||
|
|
432794fa55 | ||
|
|
6787f0dfd9 | ||
|
|
0b0a40c9fb | ||
|
|
95c43a2ab4 | ||
|
|
f4037a6bdc | ||
|
|
c4d173ae0f | ||
|
|
3e5149bc43 | ||
|
|
17f7b94555 | ||
|
|
04850694c1 | ||
|
|
eb42c031ef | ||
|
|
dc253676e8 | ||
|
|
926372f15a | ||
|
|
c2f379f139 | ||
|
|
b008b6c646 | ||
|
|
a67a9379ce | ||
|
|
3e9826d7ae | ||
|
|
f8a095e7f4 | ||
|
|
9c046d049a | ||
|
|
0871f299ef | ||
|
|
a074a5b376 | ||
|
|
abf3378441 | ||
|
|
c1b41b75c4 | ||
|
|
19706617e5 | ||
|
|
cc28df1324 | ||
|
|
607d345eb2 | ||
|
|
e41515564b | ||
|
|
f615b8cdf2 | ||
|
|
9182e79c2d | ||
|
|
ed95722a50 | ||
|
|
d7adb094a5 | ||
|
|
717e01bbbf | ||
|
|
3987953947 | ||
|
|
b7c750220e | ||
|
|
33f9357e58 | ||
|
|
9109f7094b | ||
|
|
92bd914966 | ||
|
|
b210551e96 | ||
|
|
5e64e52dab | ||
|
|
90e9dd7f47 | ||
|
|
f867d7a615 |
50
.github/CODEOWNERS
vendored
Normal file
50
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
|
||||
### Catch-all ###
|
||||
* @denolfe @jmikrut @DanRibbens
|
||||
.* @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Core ###
|
||||
/packages/payload/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/payload/src/uploads/ @denolfe
|
||||
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
|
||||
|
||||
### Adapters ###
|
||||
/packages/bundler-*/ @denolfe @jmikrut @DanRibbens @JarrodMFlesch
|
||||
/packages/db-*/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Plugins ###
|
||||
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens @jacobsfletch @JarrodMFlesch @AlessioGr
|
||||
/packages/plugin-cloud*/ @denolfe
|
||||
/packages/plugin-form-builder/ @jacobsfletch
|
||||
/packages/plugin-live-preview*/ @jacobsfletch
|
||||
/packages/plugin-nested-docs/ @jacobsfletch
|
||||
/packages/plugin-password-protection/ @jmikrut
|
||||
/packages/plugin-redirects/ @jacobsfletch
|
||||
/packages/plugin-search/ @jacobsfletch
|
||||
/packages/plugin-sentry/ @JessChowdhury
|
||||
/packages/plugin-seo/ @jacobsfletch
|
||||
/packages/plugin-stripe/ @jacobsfletch
|
||||
/packages/plugin-zapier/ @JarrodMFlesch
|
||||
|
||||
### Examples ###
|
||||
/examples/ @jacobsfletch
|
||||
/examples/testing/ @JarrodMFlesch
|
||||
/examples/email/ @JessChowdhury
|
||||
/examples/whitelabel/ @JessChowdhury
|
||||
|
||||
### Templates ###
|
||||
/templates/ @jacobsfletch
|
||||
/templates/blank/ @denolfe
|
||||
|
||||
### Misc ###
|
||||
/packages/create-payload-app/ @denolfe
|
||||
/packages/eslint-config-payload/ @denolfe
|
||||
/packages/payload-admin-bar/ @jacobsfletch
|
||||
|
||||
### Root ###
|
||||
/package.json @denolfe
|
||||
/scripts/ @denolfe
|
||||
/.github/ @denolfe
|
||||
/.github/CODEOWNERS @denolfe
|
||||
5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
needs_build:
|
||||
- '.github/workflows/**'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
@@ -131,6 +132,7 @@ jobs:
|
||||
- name: Integration Tests
|
||||
run: pnpm test:int
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
PAYLOAD_DATABASE: ${{ matrix.database }}
|
||||
POSTGRES_URL: ${{ env.POSTGRES_URL }}
|
||||
|
||||
@@ -140,7 +142,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
part: [1/4, 2/4, 3/4, 4/4]
|
||||
part: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8]
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
@@ -253,6 +255,7 @@ jobs:
|
||||
- plugin-form-builder
|
||||
- plugin-nested-docs
|
||||
- plugin-search
|
||||
- plugin-sentry
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
|
||||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@@ -47,6 +47,13 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev uploads",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Uploads",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -57,6 +64,13 @@
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm run test:int live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Live Preview Integration",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts build",
|
||||
"env": {
|
||||
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -1,3 +1,77 @@
|
||||
## [2.2.0](https://github.com/payloadcms/payload/compare/v2.1.1...v2.2.0) (2023-11-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow richtext adapters to control type generation, improve generated lexical types ([#4036](https://github.com/payloadcms/payload/issues/4036)) ([989c10e](https://github.com/payloadcms/payload/commit/989c10e0e0b36a8c34822263b19f5cb4b9ed6e72))
|
||||
* hide publish button based on permissions ([#4203](https://github.com/payloadcms/payload/issues/4203)) ([de02490](https://github.com/payloadcms/payload/commit/de02490231fbc8936973c1b81ac87add39878d8b))
|
||||
* **richtext-lexical:** Add new position: 'top' property for plugins ([eed4f43](https://github.com/payloadcms/payload/commit/eed4f4361cd012adf4e777820adbe7ad330ffef6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fully define the define property for esbuild string replacement ([#4099](https://github.com/payloadcms/payload/issues/4099)) ([e22b95b](https://github.com/payloadcms/payload/commit/e22b95bdf3b2911ae67a07a76ec109c76416ea56))
|
||||
* **i18n:** polish translations ([#4134](https://github.com/payloadcms/payload/issues/4134)) ([782e118](https://github.com/payloadcms/payload/commit/782e1185698abb2fff3556052fd16d2b725611b9))
|
||||
* improves live preview breakpoints and zoom options in dark mode ([#4090](https://github.com/payloadcms/payload/issues/4090)) ([b91711a](https://github.com/payloadcms/payload/commit/b91711a74ad9379ed820b6675060209626b1c2d0))
|
||||
* **plugin-nested-docs:** await populate breadcrumbs on resaveChildren ([#4226](https://github.com/payloadcms/payload/issues/4226)) ([4e41dd1](https://github.com/payloadcms/payload/commit/4e41dd1bf2706001fa03130adb1c69403795ac96))
|
||||
* rename tab button classname to prevent unintentional styling ([#4121](https://github.com/payloadcms/payload/issues/4121)) ([967eff1](https://github.com/payloadcms/payload/commit/967eff1aabcc9ba7f29573fc2706538d691edfdd))
|
||||
* **richtext-lexical:** add missing 'use client' to TestRecorder feature plugin ([fc26275](https://github.com/payloadcms/payload/commit/fc26275b7a85fd34f424f7693b8383ad4efe0121))
|
||||
* **richtext-lexical:** Blocks: Array row data is not removed ([#4209](https://github.com/payloadcms/payload/issues/4209)) ([0af9c4d](https://github.com/payloadcms/payload/commit/0af9c4d3985a6c46a071ef5ac28c8359cb320571))
|
||||
* **richtext-lexical:** Blocks: fields without fulfilled condition are now skipped for validation ([50fab90](https://github.com/payloadcms/payload/commit/50fab902bd7baa1702ae0d995b4f58c1f5fca374))
|
||||
* **richtext-lexical:** Blocks: make sure fields are wrapped in a uniquely-named group, change block node data format, fix react key error ([#3995](https://github.com/payloadcms/payload/issues/3995)) ([c068a87](https://github.com/payloadcms/payload/commit/c068a8784ec5780dbdca5416b25ba654afd05458))
|
||||
* **richtext-lexical:** Blocks: z-index issue, e.g. select field dropdown in blocks hidden behind blocks below, or slash menu inside nested editor hidden behind blocks below ([09f17f4](https://github.com/payloadcms/payload/commit/09f17f44508539cfcb8722f7f462ef40d9ed54fd))
|
||||
* **richtext-lexical:** Floating Select Toolbar: Buttons and Dropdown Buttons not clickable in nested editors ([615702b](https://github.com/payloadcms/payload/commit/615702b858e76994a174159cb69f034ef811e016)), closes [#4025](https://github.com/payloadcms/payload/issues/4025)
|
||||
* **richtext-lexical:** HTMLConverter: cannot find nested lexical fields ([#4103](https://github.com/payloadcms/payload/issues/4103)) ([a6d5f2e](https://github.com/payloadcms/payload/commit/a6d5f2e3dea178e1fbde90c0d6a5ce254a8db0d1)), closes [#4034](https://github.com/payloadcms/payload/issues/4034)
|
||||
* **richtext-lexical:** incorrect caret positioning when selecting second line of multi-line paragraph ([#4165](https://github.com/payloadcms/payload/issues/4165)) ([b210af4](https://github.com/payloadcms/payload/commit/b210af46968b77d96ffd6ef60adc3b8d8bdc9376))
|
||||
* **richtext-lexical:** make lexicalHTML() function work for globals ([dbfc835](https://github.com/payloadcms/payload/commit/dbfc83520ca8b5e55198a3c4b517ae3a80f9cac6))
|
||||
* **richtext-lexical:** nested editor may lose focus when writing ([#4139](https://github.com/payloadcms/payload/issues/4139)) ([859c2f4](https://github.com/payloadcms/payload/commit/859c2f4a6d299a42e572133502b3841a74a11002))
|
||||
* **richtext-lexical:** remove optional chaining after `this` as transpilers are not handling it well ([#4145](https://github.com/payloadcms/payload/issues/4145)) ([2c8d34d](https://github.com/payloadcms/payload/commit/2c8d34d2aadf2fcaf0655c0abef233f341d9945f))
|
||||
* **richtext-lexical:** visual bug after rearranging blocks ([a6b4860](https://github.com/payloadcms/payload/commit/a6b486007dc26195adc5d576d937e35471c2868f))
|
||||
* simplifies block/array/hasMany-number field validations ([#4052](https://github.com/payloadcms/payload/issues/4052)) ([803a37e](https://github.com/payloadcms/payload/commit/803a37eaa947397fa0a93b9f4f7d702c6b94ceaa))
|
||||
* synchronous transaction errors ([#4164](https://github.com/payloadcms/payload/issues/4164)) ([1510baf](https://github.com/payloadcms/payload/commit/1510baf46e33540c72784f2d3f98330a8ff90923))
|
||||
* thread locale through to access routes from admin panel ([#4183](https://github.com/payloadcms/payload/issues/4183)) ([05f3169](https://github.com/payloadcms/payload/commit/05f3169a75b3b62962e7fe7842fbb6df6699433d))
|
||||
* transactionID isolation for GraphQL ([#4095](https://github.com/payloadcms/payload/issues/4095)) ([195a952](https://github.com/payloadcms/payload/commit/195a952c4314e0d53fd579517035373b49d6ccae))
|
||||
* upload fit not accounted for when editing focal point or crop ([#4142](https://github.com/payloadcms/payload/issues/4142)) ([45e9a55](https://github.com/payloadcms/payload/commit/45e9a559bbb16b2171465c8a439044011cebf102))
|
||||
|
||||
## [2.1.1](https://github.com/payloadcms/payload/compare/v2.1.0...v2.1.1) (2023-11-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* conditionally hide dot menu in DocumentControls ([#4075](https://github.com/payloadcms/payload/issues/4075)) ([cef4cbb](https://github.com/payloadcms/payload/commit/cef4cbb0ee59e1b0b806808d79b402dce114755f))
|
||||
* disable editing option for svg image types ([#4071](https://github.com/payloadcms/payload/issues/4071)) ([949e265](https://github.com/payloadcms/payload/commit/949e265cd9c95b7d4063336dde86177008d54839))
|
||||
* fixes creation of related documents within a transaction if filterOptions is used ([#4087](https://github.com/payloadcms/payload/issues/4087)) ([acad288](https://github.com/payloadcms/payload/commit/acad2888cd9a13d5fb9e4c686b2267ea69454eaf))
|
||||
* hide empty image sizes from the preview drawer ([#3946](https://github.com/payloadcms/payload/issues/3946)) ([687f485](https://github.com/payloadcms/payload/commit/687f4850acf073df0a649ef6182bfc8387857173))
|
||||
* **live-preview:** ensures field schema exists before traversing fields ([#4074](https://github.com/payloadcms/payload/issues/4074)) ([7059a71](https://github.com/payloadcms/payload/commit/7059a71243a8f98dcc89af0bfe502247db9e4123))
|
||||
* **live-preview:** field recursion and relationship population ([#4045](https://github.com/payloadcms/payload/issues/4045)) ([2ad7340](https://github.com/payloadcms/payload/commit/2ad73401546ef6608fd67d1f00b537f149640d6a))
|
||||
* **live-preview:** properly handles apiRoute ([#4076](https://github.com/payloadcms/payload/issues/4076)) ([1f851f2](https://github.com/payloadcms/payload/commit/1f851f21b18c9a5076d9afc9a31abc7a97fcb0df))
|
||||
* **plugin-nested-docs:** sync write transaction errors ([#4084](https://github.com/payloadcms/payload/issues/4084)) ([47efd3b](https://github.com/payloadcms/payload/commit/47efd3b92e99594dd5b61f0017f4eb76e1d36eb7))
|
||||
* possible issue with access control not using req ([#4086](https://github.com/payloadcms/payload/issues/4086)) ([348a70c](https://github.com/payloadcms/payload/commit/348a70cc33409b0b48aff3acd2b94c2df5d88f3b))
|
||||
* **richtext-lexical:** Blocks: unnecessary saving node value when initially opening a document & new lexical tests ([#4059](https://github.com/payloadcms/payload/issues/4059)) ([fff377a](https://github.com/payloadcms/payload/commit/fff377ad22cce3b26142cde8f4125fcee95aa072))
|
||||
* **richtext-lexical:** floating select toolbar caret not positioned correctly if first line is selected ([#4062](https://github.com/payloadcms/payload/issues/4062)) ([c462df3](https://github.com/payloadcms/payload/commit/c462df38f65b155e131e6a7b46b2bb16cd090e45))
|
||||
|
||||
## [2.1.0](https://github.com/payloadcms/payload/compare/v2.0.15...v2.1.0) (2023-11-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add internationalization (i18n) to locales ([#4005](https://github.com/payloadcms/payload/issues/4005)) ([6a0a859](https://github.com/payloadcms/payload/commit/6a0a859563ed9e742260ea51a1839a1ef0f61fce))
|
||||
* Custom Error, Label, and before/after field components ([#3747](https://github.com/payloadcms/payload/issues/3747)) ([266c327](https://github.com/payloadcms/payload/commit/266c3274d03e4fd52c692eeef1ee9248dcf66189))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* error on graphql multiple queries ([#3985](https://github.com/payloadcms/payload/issues/3985)) ([57da3c9](https://github.com/payloadcms/payload/commit/57da3c99a7e4ce5d3d1e17315e3691815f363704))
|
||||
* focal and cropping issues, adds test ([#4039](https://github.com/payloadcms/payload/issues/4039)) ([acba5e4](https://github.com/payloadcms/payload/commit/acba5e482b7ddc6e3dc6ba9b7736022770d69a55))
|
||||
* handle invalid tokens in refresh token operation ([#3647](https://github.com/payloadcms/payload/issues/3647)) ([131d89c](https://github.com/payloadcms/payload/commit/131d89c3f50c237e1ab2d7cd32d7a8226a9f8ce3))
|
||||
* hasMany number and select fields unable to save within arrays ([#4047](https://github.com/payloadcms/payload/issues/4047)) ([182c57b](https://github.com/payloadcms/payload/commit/182c57b191010ce3dcf659f39c1dc2f7cf80662e))
|
||||
* injects array and block ids into fieldSchemaToJSON ([#4043](https://github.com/payloadcms/payload/issues/4043)) ([d068ef7](https://github.com/payloadcms/payload/commit/d068ef7e2483d49dc41bdd7735042ddcaa0a684c))
|
||||
* parse predefined migrations via file arg or name prefix ([#4001](https://github.com/payloadcms/payload/issues/4001)) ([eb42c03](https://github.com/payloadcms/payload/commit/eb42c031ef980558ed051d4163925aa28d6ab090))
|
||||
* polymorphic hasMany relationships missing in postgres admin ([#4053](https://github.com/payloadcms/payload/issues/4053)) ([7a9af44](https://github.com/payloadcms/payload/commit/7a9af4417a56c621f01195f9a2904b9adffaad7a))
|
||||
* resets list filter row when the filter on field is changed ([#3956](https://github.com/payloadcms/payload/issues/3956)) ([8d14c21](https://github.com/payloadcms/payload/commit/8d14c213c878a1afda2b3bf03431fed5aa2a44e3))
|
||||
* Update API Views ([b008b6c](https://github.com/payloadcms/payload/commit/b008b6c6463c9dc3d8e61eaa0a9210aa1a189442))
|
||||
* vite not replacing env vars correctly when building ([67b3baa](https://github.com/payloadcms/payload/commit/67b3baaa445a13246be8178d57eaeba92888bef1))
|
||||
|
||||
## [2.0.15](https://github.com/payloadcms/payload/compare/v2.0.14...v2.0.15) (2023-11-03)
|
||||
|
||||
|
||||
|
||||
24
README.md
24
README.md
@@ -1,24 +1,14 @@
|
||||
<a href="https://payloadcms.com">
|
||||
<img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" />
|
||||
</a>
|
||||
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" /></a>
|
||||
<br />
|
||||
<br />
|
||||
<p align="left">
|
||||
<a href="https://github.com/payloadcms/payload/actions">
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/main.yml?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/payloadcms/payload/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/main.yml?style=flat-square"></a>
|
||||
|
||||
<a href="https://discord.gg/payload">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" />
|
||||
</a>
|
||||
<a href="https://discord.gg/payload"><img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" /></a>
|
||||
|
||||
<a href="https://www.npmjs.com/package/payload">
|
||||
<img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" /></a>
|
||||
|
||||
<a href="https://twitter.com/payloadcms">
|
||||
<img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" />
|
||||
</a>
|
||||
<a href="https://twitter.com/payloadcms"><img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" /></a>
|
||||
</p>
|
||||
<hr/>
|
||||
<h4>
|
||||
@@ -27,7 +17,7 @@
|
||||
<hr/>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 🎉 <strong>Payload 2.0 is now available!<strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
|
||||
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
|
||||
|
||||
<h3>Benefits over a regular CMS</h3>
|
||||
<ul>
|
||||
@@ -51,7 +41,7 @@ Create a cloud account, connect your GitHub, and [deploy in minutes](https://pay
|
||||
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/getting-started/installation).
|
||||
|
||||
```text
|
||||
npx create-payload-app
|
||||
npx create-payload-app@latest
|
||||
```
|
||||
|
||||
Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch).
|
||||
|
||||
@@ -432,6 +432,15 @@ All Payload fields support the ability to swap in your own React components. So,
|
||||
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) |
|
||||
| **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) |
|
||||
|
||||
As an alternative to replacing the entire Field component, you may want to keep the majority of the default Field component and only swap components within. This allows you to replace the **`Label`** or **`Error`** within a field component or add additional components inside the field with **`beforeInput`** or **`afterInput`**. **`beforeInput`** and **`afterInput`** are allowed in any fields that don't contain other fields, except [UI](/docs/fields/ui) and [Rich Text](/docs/fields/rich-text).
|
||||
|
||||
| Component | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Label`** | Override the default Label in the Field Component. [More](#label-component) |
|
||||
| **`Error`** | Override the default Label in the Field Component. [More](#error-component) |
|
||||
| **`beforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
| **`afterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
|
||||
## Cell Component
|
||||
|
||||
These are the props that will be passed to your custom Cell to use in your own components.
|
||||
@@ -487,6 +496,101 @@ const CustomTextField: React.FC<Props> = ({ path }) => {
|
||||
components, including the <strong>useField</strong> hook, [click here](/docs/admin/hooks).
|
||||
</Banner>
|
||||
|
||||
## Label Component
|
||||
|
||||
These are the props that will be passed to your custom Label.
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | ---------------------------------------------------------------- |
|
||||
| **`htmlFor`** | Property used to set `for` attribute for label. |
|
||||
| **`label`** | Label value provided in field, it can be used with i18n. |
|
||||
| **`required`** | A boolean value that represents if the field is required or not. |
|
||||
|
||||
#### Example
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { getTranslation } from 'payload/utilities/getTranslation'
|
||||
|
||||
type Props = {
|
||||
htmlFor?: string
|
||||
label?: Record<string, string> | false | string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
const CustomLabel: React.FC<Props> = (props) => {
|
||||
const { htmlFor, label, required = false } = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
if (label) {
|
||||
return (<span>
|
||||
{getTranslation(label, i18n)}
|
||||
{required && <span className="required">*</span>}
|
||||
</span>);
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
## Error Component
|
||||
|
||||
These are the props that will be passed to your custom Error.
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | ------------------------------------------------------------- |
|
||||
| **`message`** | The error message. |
|
||||
| **`showError`** | A boolean value that represents if the error should be shown. |
|
||||
|
||||
#### Example
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
message: string
|
||||
showError?: boolean
|
||||
}
|
||||
|
||||
const CustomError: React.FC<Props> = (props) => {
|
||||
const { message, showError } = props
|
||||
|
||||
if (showError) {
|
||||
return <p style={{color: 'red'}}>{message}</p>
|
||||
} else return null;
|
||||
}
|
||||
```
|
||||
|
||||
## afterInput and beforeInput
|
||||
|
||||
With these properties you can add multiple components before and after the input element. For example, you can add an absolutely positioned button to clear the current field value.
|
||||
|
||||
#### Example
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import './style.scss'
|
||||
|
||||
const ClearButton: React.FC = () => {
|
||||
return <button onClick={() => {/* ... */}}>X</button>
|
||||
}
|
||||
|
||||
const fieldField: Field = {
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [ClearButton]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default titleField;
|
||||
```
|
||||
|
||||
## Custom providers
|
||||
|
||||
As your admin customizations gets more complex you may want to share state between fields or other components. You can add custom providers to do add your own context to any Payload app for use in other custom components within the admin panel. Within your config add `admin.components.providers`, these can be used to share context or provide other custom functionality. Read the [React context](https://reactjs.org/docs/context.html) docs to learn more.
|
||||
|
||||
@@ -758,3 +758,29 @@ const MyComponent: React.FC = () => {
|
||||
### usePreferences
|
||||
|
||||
Returns methods to set and get user preferences. More info can be found [here](https://payloadcms.com/docs/admin/preferences).
|
||||
|
||||
### useTableColumns
|
||||
|
||||
Returns methods to manipulate table columns
|
||||
|
||||
```tsx
|
||||
import { useTableColumns } from 'payload/components/hooks'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { setActiveColumns } = useTableColumns()
|
||||
|
||||
const resetColumns = () => {
|
||||
setActiveColumns(['id', 'createdAt', 'updatedAt'])
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetColumns}
|
||||
>
|
||||
Reset columns
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,38 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
**Example Payload config set up for localization with full locales objects (including [internationalization](/docs/configuration/i18n) support):**
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
// collections go here
|
||||
],
|
||||
localization: {
|
||||
locales: [
|
||||
{
|
||||
label: {
|
||||
en: 'English', // English label
|
||||
nb: 'Engelsk', // Norwegian label
|
||||
},
|
||||
code: 'en',
|
||||
},
|
||||
{
|
||||
label: {
|
||||
en: 'Norwegian', // English label
|
||||
nb: 'Norsk', // Norwegian label
|
||||
},
|
||||
code: 'nb',
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Here is a brief explanation of each of the options available within the `localization` property:**
|
||||
|
||||
**`locales`**
|
||||
|
||||
@@ -108,7 +108,7 @@ export default buildConfig({
|
||||
|
||||
#### Full example config
|
||||
|
||||
You can see a full [example config](https://github.com/payloadcms/public-demo/blob/master/src/payload.config.ts) in the Public Demo source code on GitHub.
|
||||
You can see a full [example config](https://github.com/payloadcms/public-demo/blob/master/src/payload/payload.config.ts) in the Public Demo source code on GitHub.
|
||||
|
||||
### Using environment variables in your config
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export default buildConfig({
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `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. |
|
||||
|
||||
### Access to Mongoose models
|
||||
@@ -43,4 +44,4 @@ You can access Mongoose models as follows:
|
||||
|
||||
- Collection models - `payload.db.collections[myCollectionSlug]`
|
||||
- Globals model - `payload.db.globals`
|
||||
- Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]`
|
||||
- Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]`
|
||||
|
||||
@@ -55,6 +55,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
|
||||
| **`date.maxDate`** \* | Max date value to allow. |
|
||||
| **`date.minTime`** \* | Min time value to allow. |
|
||||
| **`date.maxTime`** \* | Max date value to allow. |
|
||||
| **`date.overrides`** \* | Pass any valid props directly to the [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md) |
|
||||
| **`date.timeIntervals`** \* | Time intervals to display. Defaults to 30 minutes. |
|
||||
| **`date.timeFormat`** \* | Determines time format. Defaults to `'h:mm aa'`. |
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Payload requires the following software:
|
||||
To quickly scaffold a new Payload app in the fastest way possible, you can use [create-payload-app](https://npmjs.com/package/create-payload-app). To do so, run the following command:
|
||||
|
||||
```
|
||||
npx create-payload-app
|
||||
npx create-payload-app@latest
|
||||
```
|
||||
|
||||
Then just follow the prompts! You'll get set up with a new folder and a functioning Payload app inside.
|
||||
|
||||
@@ -10,13 +10,14 @@ While using Live Preview, the Admin panel emits a new `window.postMessage` event
|
||||
|
||||
Wiring your front-end into Live Preview is easy. If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides. In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
|
||||
|
||||
By default, all hooks require the following args:
|
||||
By default, all hooks accept the following args:
|
||||
|
||||
| Path | Description |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`serverURL`** \* | The URL of your Payload server. |
|
||||
| **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. |
|
||||
| **`depth`** | The depth of the relationships to fetch. Defaults to `0`. |
|
||||
| **`apiRoute`** | The path of your API route as defined in `routes.api`. Defaults to `/api`. |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ If you're starting from scratch, you can easily setup a dev environment lik
|
||||
```
|
||||
mkdir dev
|
||||
cd dev
|
||||
npx create-payload-app
|
||||
npx create-payload-app@latest
|
||||
```
|
||||
|
||||
If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config()`.
|
||||
|
||||
@@ -314,13 +314,31 @@ import {
|
||||
|
||||
const yourEditorConfig; // <= your editor config here
|
||||
|
||||
const headlessEditor = await createHeadlessEditor({
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: sanitizeEditorConfig(yourEditorConfig),
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
### Getting the editor config
|
||||
|
||||
As you can see, you need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
|
||||
|
||||
To get the editor config, simply import the default editor config and adjust it - just like you did inside of the `editor: lexicalEditor({})` property:
|
||||
|
||||
```ts
|
||||
import { defaultEditorConfig, defaultEditorFeatures } from '@payloadcms/richtext-lexical' // <= make sure this package is installed
|
||||
|
||||
const yourEditorConfig = defaultEditorConfig
|
||||
|
||||
// If you made changes to the features of the field's editor config, you should also make those changes here:
|
||||
yourEditorConfig.features = [
|
||||
...defaultEditorFeatures,
|
||||
// Add your custom features here
|
||||
]
|
||||
```
|
||||
|
||||
### HTML => Lexical
|
||||
|
||||
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
|
||||
@@ -328,13 +346,14 @@ Once you have your headless editor instance, you can use it to convert HTML to L
|
||||
```ts
|
||||
import { $generateNodesFromDOM } from '@lexical/html'
|
||||
import { $getRoot,$getSelection } from 'lexical'
|
||||
import JSDOM from 'jsdom'
|
||||
|
||||
headlessEditor.update(() => {
|
||||
// In a headless environment you can use a package such as JSDom to parse the HTML string.
|
||||
const dom = new JSDOM(htmlString)
|
||||
|
||||
// Once you have the DOM instance it's easy to generate LexicalNodes.
|
||||
const nodes = $generateNodesFromDOM(editor, dom.window.document)
|
||||
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
|
||||
|
||||
// Select the root
|
||||
$getRoot().select()
|
||||
@@ -348,6 +367,8 @@ headlessEditor.update(() => {
|
||||
const editorJSON = headlessEditor.getEditorState().toJSON()
|
||||
```
|
||||
|
||||
Functions prefixed with a `$` can only be run inside of an `editor.update()` or `editorState.read()` callback.
|
||||
|
||||
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
|
||||
|
||||
<Banner type="success">
|
||||
@@ -395,7 +416,6 @@ try {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||
return ''
|
||||
}
|
||||
|
||||
// Export to markdown
|
||||
@@ -407,6 +427,35 @@ headlessEditor.getEditorState().read(() => {
|
||||
|
||||
The `.setEditorState()` function immediately updates your editor state. Thus, there's no need for the `discrete: true` flag when reading the state afterward.
|
||||
|
||||
|
||||
### Lexical => Plain Text
|
||||
|
||||
Export content from the Lexical editor into plain text using these steps:
|
||||
|
||||
1. Import your current editor state into the headless editor.
|
||||
2. Convert and fetch the resulting plain text string.
|
||||
|
||||
Here's the code for it:
|
||||
|
||||
```ts
|
||||
import type { SerializedEditorState } from "lexical"
|
||||
import { $getRoot } from "lexical"
|
||||
|
||||
const yourEditorState: SerializedEditorState // <= your current editor state here
|
||||
|
||||
// Import editor state into your headless editor
|
||||
try {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||
}
|
||||
|
||||
// Export to plain text
|
||||
const plainTextContent = headlessEditor.getEditorState().read(() => {
|
||||
return $getRoot().getTextContent()
|
||||
}) || ''
|
||||
```
|
||||
|
||||
## Migrating from Slate
|
||||
|
||||
While both Slate and Lexical save the editor state in JSON, the structure of the JSON is different.
|
||||
|
||||
@@ -9,7 +9,7 @@ keywords: headless cms, typescript, documentation, Content Management System, cm
|
||||
Payload supports TypeScript natively, and not only that, the entirety of the CMS is built with TypeScript. To get started developing with Payload and TypeScript, you can use one of Payload's built-in boilerplates in one line via `create-payload-app`:
|
||||
|
||||
```
|
||||
npx create-payload-app
|
||||
npx create-payload-app@latest
|
||||
```
|
||||
|
||||
Pick a TypeScript project type to get started easily.
|
||||
|
||||
@@ -38,7 +38,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
|
||||
</strong> on that collection.
|
||||
</Banner>
|
||||
|
||||
#### Collection Upload Options
|
||||
### Collection Upload Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -47,14 +47,14 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
|
||||
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
|
||||
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
|
||||
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
|
||||
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
|
||||
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
|
||||
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
|
||||
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
|
||||
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
|
||||
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
|
||||
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) format) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
|
||||
_An asterisk denotes that a property above is required._
|
||||
|
||||
@@ -148,6 +148,23 @@ All auto-resized images are exposed to be re-used in hooks and similar via an ob
|
||||
|
||||
The object will have keys for each size generated, and each key will be set equal to a buffer containing the file data.
|
||||
|
||||
##### Handling Image Enlargement
|
||||
|
||||
When an uploaded image is smaller than the defined image size, we have 3 options:
|
||||
|
||||
`withoutEnlargement: undefined | false | true`
|
||||
|
||||
1.`undefined` [default]: uploading images with smaller width AND height than the image size will return null
|
||||
2. `false`: always enlarge images to the image size
|
||||
3. `true`: if the image is smaller than the image size, return the original image
|
||||
|
||||
<Banner type="error">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
By default, the image size will return NULL when the uploaded image is smaller than the defined image size.
|
||||
Use the `withoutEnlargement` prop to change this.
|
||||
</Banner>
|
||||
|
||||
### Crop and Focal Point Selector
|
||||
|
||||
This feature is only available for image file types.
|
||||
|
||||
@@ -21,7 +21,7 @@ export const Country: React.FC<
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.select}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<Controller
|
||||
@@ -37,6 +37,7 @@ export const Country: React.FC<
|
||||
onChange={val => onChange(val.value)}
|
||||
className={classes.reactSelect}
|
||||
classNamePrefix="rs"
|
||||
inputId={name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -19,13 +19,14 @@ export const Email: React.FC<
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.wrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
className={classes.input}
|
||||
id={name}
|
||||
{...register(name, { required: requiredFromProps, pattern: /^\S+@\S+$/i })}
|
||||
/>
|
||||
{requiredFromProps && errors[name] && <Error />}
|
||||
|
||||
@@ -19,12 +19,13 @@ export const Number: React.FC<
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.wrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className={classes.input}
|
||||
id={name}
|
||||
{...register(name, { required: requiredFromProps })}
|
||||
/>
|
||||
{requiredFromProps && errors[name] && <Error />}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const Select: React.FC<
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.select}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<Controller
|
||||
@@ -36,6 +36,7 @@ export const Select: React.FC<
|
||||
onChange={val => onChange(val.value)}
|
||||
className={classes.reactSelect}
|
||||
classNamePrefix="rs"
|
||||
inputId={name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const State: React.FC<
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.select}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<Controller
|
||||
@@ -37,6 +37,7 @@ export const State: React.FC<
|
||||
onChange={val => onChange(val.value)}
|
||||
className={classes.reactSelect}
|
||||
classNamePrefix="rs"
|
||||
id={name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -19,12 +19,13 @@ export const Text: React.FC<
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.wrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={classes.input}
|
||||
id={name}
|
||||
{...register(name, { required: requiredFromProps })}
|
||||
/>
|
||||
{requiredFromProps && errors[name] && <Error />}
|
||||
|
||||
@@ -20,12 +20,13 @@ export const Textarea: React.FC<
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.wrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<textarea
|
||||
rows={rows}
|
||||
className={classes.textarea}
|
||||
id={name}
|
||||
{...register(name, { required: requiredFromProps })}
|
||||
/>
|
||||
{requiredFromProps && errors[name] && <Error />}
|
||||
|
||||
13
package.json
13
package.json
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@playwright/test": "1.38.1",
|
||||
"@playwright/test": "1.39.0",
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@swc/register": "0.1.10",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@types/conventional-changelog-core": "^4.2.5",
|
||||
"@types/conventional-changelog-preset-loader": "^2.3.4",
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/jest": "29.5.7",
|
||||
"@types/minimist": "1.2.2",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/prompts": "^2.4.5",
|
||||
@@ -64,6 +64,7 @@
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "8.6.0",
|
||||
"drizzle-orm": "0.28.5",
|
||||
"express": "4.18.2",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
@@ -73,9 +74,10 @@
|
||||
"graphql-request": "6.1.0",
|
||||
"husky": "^8.0.3",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"jest": "29.6.4",
|
||||
"jest-environment-jsdom": "29.6.4",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jwt-decode": "3.1.2",
|
||||
"lexical": "0.12.2",
|
||||
"lint-staged": "^14.0.1",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "8.12.2",
|
||||
@@ -111,5 +113,8 @@
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/react": "^7.77.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/bundler-vite",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"description": "The officially supported Vite bundler adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -63,13 +63,14 @@ export const getViteConfig = async (payloadConfig: SanitizedConfig): Promise<Inl
|
||||
'module.hot': 'undefined',
|
||||
'process.argv': '[]',
|
||||
'process.cwd': 'function () { return "/" }',
|
||||
'process.env': '{}',
|
||||
'process?.cwd': 'function () { return "/" }',
|
||||
}
|
||||
|
||||
Object.entries(process.env).forEach(([key, val]) => {
|
||||
if (key.indexOf('PAYLOAD_PUBLIC_') === 0) {
|
||||
define[`process.env.${key}`] = `'${val}'`
|
||||
} else {
|
||||
define[`process.env.${key}`] = `''`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.8",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -30,8 +30,11 @@ export const createMigration: CreateMigration = async function createMigration({
|
||||
|
||||
let migrationFileContent: string | undefined
|
||||
|
||||
// Check for predefined migration
|
||||
if (file) {
|
||||
// Check for predefined migration.
|
||||
// Either passed in via --file or prefixed with @payloadcms/db-mongodb/
|
||||
if (file || migrationName.startsWith('@payloadcms/db-mongodb/')) {
|
||||
if (!file) file = migrationName
|
||||
|
||||
const predefinedMigrationName = file.replace('@payloadcms/db-mongodb/', '')
|
||||
migrationName = predefinedMigrationName
|
||||
const cleanPath = path.join(__dirname, `../predefinedMigrations/${predefinedMigrationName}.js`)
|
||||
|
||||
@@ -55,7 +55,7 @@ export const find: Find = async function find(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (!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.
|
||||
paginationOptions.useCustomCountFn = () => {
|
||||
return Promise.resolve(
|
||||
|
||||
@@ -74,7 +74,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (!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.
|
||||
paginationOptions.useCustomCountFn = () => {
|
||||
return Promise.resolve(
|
||||
|
||||
@@ -71,7 +71,7 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (!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.
|
||||
paginationOptions.useCustomCountFn = () => {
|
||||
return Promise.resolve(
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface Args {
|
||||
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
|
||||
useFacet?: boolean
|
||||
}
|
||||
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
|
||||
disableIndexHints?: boolean
|
||||
migrationDir?: string
|
||||
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
|
||||
url: false | string
|
||||
@@ -87,6 +89,7 @@ declare module 'payload' {
|
||||
export function mongooseAdapter({
|
||||
autoPluralization = true,
|
||||
connectOptions,
|
||||
disableIndexHints = false,
|
||||
migrationDir: migrationDirArg,
|
||||
url,
|
||||
}: Args): MongooseAdapterResult {
|
||||
@@ -105,6 +108,7 @@ export function mongooseAdapter({
|
||||
collections: {},
|
||||
connectOptions: connectOptions || {},
|
||||
connection: undefined,
|
||||
disableIndexHints,
|
||||
globals: undefined,
|
||||
mongoMemoryServer: undefined,
|
||||
sessions: {},
|
||||
|
||||
@@ -58,7 +58,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (!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.
|
||||
paginationOptions.useCustomCountFn = () => {
|
||||
return Promise.resolve(
|
||||
|
||||
@@ -4,6 +4,7 @@ export const commitTransaction: CommitTransaction = async function commitTransac
|
||||
if (!this.sessions[id]?.inTransaction()) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.sessions[id].commitTransaction()
|
||||
await this.sessions[id].endSession()
|
||||
delete this.sessions[id]
|
||||
|
||||
@@ -3,10 +3,20 @@ import type { RollbackTransaction } from 'payload/database'
|
||||
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
|
||||
id = '',
|
||||
) {
|
||||
if (!this.sessions[id]?.inTransaction()) {
|
||||
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
|
||||
// if multiple operations are using the same transaction, the first will flow through and delete the session.
|
||||
// subsequent calls should be ignored.
|
||||
if (!this.sessions[id]) {
|
||||
return
|
||||
}
|
||||
|
||||
// when session exists but is not inTransaction something unexpected is happening to the session
|
||||
if (!this.sessions[id].inTransaction()) {
|
||||
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
|
||||
delete this.sessions[id]
|
||||
return
|
||||
}
|
||||
|
||||
// the first call for rollback should be aborted and deleted causing any other operations with the same transaction to fail
|
||||
await this.sessions[id].abortTransaction()
|
||||
await this.sessions[id].endSession()
|
||||
delete this.sessions[id]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.1.11",
|
||||
"version": "0.1.13",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -23,6 +23,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
buildNumbers: true,
|
||||
buildRelationships: true,
|
||||
disableNotNull: !!collection?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
@@ -37,6 +38,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
buildNumbers: true,
|
||||
buildRelationships: true,
|
||||
disableNotNull: !!collection.versions?.drafts,
|
||||
disableUnique: true,
|
||||
@@ -52,6 +54,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
buildNumbers: true,
|
||||
buildRelationships: true,
|
||||
disableNotNull: !!global?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
@@ -66,6 +69,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
buildNumbers: true,
|
||||
buildRelationships: true,
|
||||
disableNotNull: !!global.versions?.drafts,
|
||||
disableUnique: true,
|
||||
|
||||
@@ -26,6 +26,7 @@ type Args = {
|
||||
adapter: PostgresAdapter
|
||||
baseColumns?: Record<string, PgColumnBuilder>
|
||||
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
|
||||
buildNumbers?: boolean
|
||||
buildRelationships?: boolean
|
||||
disableNotNull: boolean
|
||||
disableUnique: boolean
|
||||
@@ -39,6 +40,7 @@ type Args = {
|
||||
}
|
||||
|
||||
type Result = {
|
||||
hasManyNumberField: 'index' | boolean
|
||||
relationsToBuild: Map<string, string>
|
||||
}
|
||||
|
||||
@@ -46,6 +48,7 @@ export const buildTable = ({
|
||||
adapter,
|
||||
baseColumns = {},
|
||||
baseExtraConfig = {},
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
disableNotNull,
|
||||
disableUnique = false,
|
||||
@@ -53,10 +56,11 @@ export const buildTable = ({
|
||||
rootRelationsToBuild,
|
||||
rootRelationships,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
rootTableName: incomingRootTableName,
|
||||
tableName,
|
||||
timestamps,
|
||||
}: Args): Result => {
|
||||
const rootTableName = incomingRootTableName || tableName
|
||||
const columns: Record<string, PgColumnBuilder> = baseColumns
|
||||
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
|
||||
|
||||
@@ -102,6 +106,7 @@ export const buildTable = ({
|
||||
hasManyNumberField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
columns,
|
||||
disableNotNull,
|
||||
@@ -116,7 +121,7 @@ export const buildTable = ({
|
||||
relationships,
|
||||
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
|
||||
rootTableIDColType: rootTableIDColType || idColType,
|
||||
rootTableName: rootTableName || tableName,
|
||||
rootTableName,
|
||||
}))
|
||||
|
||||
if (timestamps) {
|
||||
@@ -185,8 +190,8 @@ export const buildTable = ({
|
||||
adapter.relations[`relations_${localeTableName}`] = localesTableRelations
|
||||
}
|
||||
|
||||
if (hasManyNumberField) {
|
||||
const numbersTableName = `${tableName}_numbers`
|
||||
if (hasManyNumberField && buildNumbers) {
|
||||
const numbersTableName = `${rootTableName}_numbers`
|
||||
const columns: Record<string, PgColumnBuilder> = {
|
||||
id: serial('id').primaryKey(),
|
||||
number: numeric('number'),
|
||||
@@ -327,5 +332,5 @@ export const buildTable = ({
|
||||
|
||||
adapter.relations[`relations_${tableName}`] = tableRelations
|
||||
|
||||
return { relationsToBuild }
|
||||
return { hasManyNumberField, relationsToBuild }
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { Relation } from 'drizzle-orm'
|
||||
import { relations } from 'drizzle-orm'
|
||||
import type { IndexBuilder, PgColumnBuilder, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
|
||||
import type { Field, TabAsField } from 'payload/types'
|
||||
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
PgNumericBuilder,
|
||||
PgVarcharBuilder,
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
pgEnum,
|
||||
PgNumericBuilder,
|
||||
PgVarcharBuilder,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { Field, TabAsField } from 'payload/types'
|
||||
import { fieldAffectsData, optionIsObject } from 'payload/types'
|
||||
import { InvalidConfiguration } from 'payload/errors'
|
||||
import { fieldAffectsData, optionIsObject } from 'payload/types'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { GenericColumns, PostgresAdapter } from '../types'
|
||||
@@ -31,6 +32,7 @@ import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdent
|
||||
|
||||
type Args = {
|
||||
adapter: PostgresAdapter
|
||||
buildNumbers: boolean
|
||||
buildRelationships: boolean
|
||||
columnPrefix?: string
|
||||
columns: Record<string, PgColumnBuilder>
|
||||
@@ -60,6 +62,7 @@ type Result = {
|
||||
|
||||
export const traverseFields = ({
|
||||
adapter,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
columnPrefix,
|
||||
columns,
|
||||
@@ -283,19 +286,25 @@ export const traverseFields = ({
|
||||
baseExtraConfig._localeIdx = (cols) => index('_locale_idx').on(cols._locale)
|
||||
}
|
||||
|
||||
const { relationsToBuild: subRelationsToBuild } = buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fields: disableUnique ? idToUUID(field.fields) : field.fields,
|
||||
rootRelationsToBuild,
|
||||
rootRelationships: relationships,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
tableName: arrayTableName,
|
||||
})
|
||||
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,
|
||||
})
|
||||
|
||||
if (subHasManyNumberField) {
|
||||
if (!hasManyNumberField || subHasManyNumberField === 'index')
|
||||
hasManyNumberField = subHasManyNumberField
|
||||
}
|
||||
|
||||
relationsToBuild.set(fieldName, arrayTableName)
|
||||
|
||||
@@ -351,7 +360,10 @@ export const traverseFields = ({
|
||||
baseExtraConfig._localeIdx = (cols) => index('locale_idx').on(cols._locale)
|
||||
}
|
||||
|
||||
const { relationsToBuild: subRelationsToBuild } = buildTable({
|
||||
const {
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
} = buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
@@ -365,6 +377,11 @@ export const traverseFields = ({
|
||||
tableName: blockTableName,
|
||||
})
|
||||
|
||||
if (subHasManyNumberField) {
|
||||
if (!hasManyNumberField || subHasManyNumberField === 'index')
|
||||
hasManyNumberField = subHasManyNumberField
|
||||
}
|
||||
|
||||
const blockTableRelations = relations(
|
||||
adapter.tables[blockTableName],
|
||||
({ many, one }) => {
|
||||
@@ -413,6 +430,7 @@ export const traverseFields = ({
|
||||
hasManyNumberField: groupHasManyNumberField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
columnPrefix,
|
||||
columns,
|
||||
@@ -449,6 +467,7 @@ export const traverseFields = ({
|
||||
hasManyNumberField: groupHasManyNumberField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
columnPrefix: `${columnName}_`,
|
||||
columns,
|
||||
@@ -486,6 +505,7 @@ export const traverseFields = ({
|
||||
hasManyNumberField: tabHasManyNumberField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
columnPrefix,
|
||||
columns,
|
||||
@@ -524,6 +544,7 @@ export const traverseFields = ({
|
||||
hasManyNumberField: rowHasManyNumberField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
columnPrefix,
|
||||
columns,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CommitTransaction } from 'payload/database'
|
||||
|
||||
export const commitTransaction: CommitTransaction = async function commitTransaction(id) {
|
||||
// if the session was deleted it has already been aborted
|
||||
if (!this.sessions[id]) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ import type { RollbackTransaction } from 'payload/database'
|
||||
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
|
||||
id = '',
|
||||
) {
|
||||
// if multiple operations are using the same transaction, the first will flow through and delete the session.
|
||||
// subsequent calls should be ignored.
|
||||
if (!this.sessions[id]) {
|
||||
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
|
||||
return
|
||||
}
|
||||
|
||||
// end the session promise in failure by calling reject
|
||||
await this.sessions[id].reject()
|
||||
|
||||
// delete the session causing any other operations with the same transaction to fail
|
||||
delete this.sessions[id]
|
||||
}
|
||||
|
||||
@@ -3,16 +3,18 @@ import { isArrayOfRows } from '../../utilities/isArrayOfRows'
|
||||
|
||||
type Args = {
|
||||
data: unknown
|
||||
id?: unknown
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export const transformSelects = ({ data, locale }: Args) => {
|
||||
export const transformSelects = ({ id, data, locale }: Args) => {
|
||||
const newRows: Record<string, unknown>[] = []
|
||||
|
||||
if (isArrayOfRows(data)) {
|
||||
data.forEach((value, i) => {
|
||||
const newRow: Record<string, unknown> = {
|
||||
order: i + 1,
|
||||
parent: id,
|
||||
value,
|
||||
}
|
||||
|
||||
|
||||
@@ -422,6 +422,7 @@ export const traverseFields = ({
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
if (Array.isArray(localeData)) {
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
data: localeData,
|
||||
locale: localeKey,
|
||||
})
|
||||
@@ -432,6 +433,7 @@ export const traverseFields = ({
|
||||
}
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
data: data[field.name],
|
||||
})
|
||||
|
||||
|
||||
@@ -102,7 +102,9 @@ export const upsertRow = async <T extends TypeWithID>({
|
||||
if (Object.keys(rowToInsert.selects).length > 0) {
|
||||
Object.entries(rowToInsert.selects).forEach(([selectTableName, selectRows]) => {
|
||||
selectRows.forEach((row) => {
|
||||
row.parent = insertedRow.id
|
||||
if (typeof row.parent === 'undefined') {
|
||||
row.parent = insertedRow.id
|
||||
}
|
||||
if (!selectsToInsert[selectTableName]) selectsToInsert[selectTableName] = []
|
||||
selectsToInsert[selectTableName].push(row)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
// you can conditionally render loading UI based on the `isLoading` state
|
||||
|
||||
export const useLivePreview = <T extends any>(props: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
initialData: T
|
||||
serverURL: string
|
||||
@@ -14,7 +15,7 @@ export const useLivePreview = <T extends any>(props: {
|
||||
data: T
|
||||
isLoading: boolean
|
||||
} => {
|
||||
const { depth = 0, initialData, serverURL } = props
|
||||
const { apiRoute, depth, initialData, serverURL } = props
|
||||
const [data, setData] = useState<T>(initialData)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const hasSentReadyMessage = useRef<boolean>(false)
|
||||
@@ -26,6 +27,7 @@ export const useLivePreview = <T extends any>(props: {
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = subscribe({
|
||||
apiRoute,
|
||||
callback: onChange,
|
||||
depth,
|
||||
initialData,
|
||||
@@ -43,7 +45,7 @@ export const useLivePreview = <T extends any>(props: {
|
||||
return () => {
|
||||
unsubscribe(subscription)
|
||||
}
|
||||
}, [serverURL, onChange, depth, initialData])
|
||||
}, [serverURL, onChange, depth, initialData, apiRoute])
|
||||
|
||||
return {
|
||||
data,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -7,12 +7,14 @@ import { mergeData } from '.'
|
||||
let payloadLivePreviewFieldSchema = undefined // TODO: type this from `fieldSchemaToJSON` return type
|
||||
|
||||
export const handleMessage = async <T>(args: {
|
||||
depth: number
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
event: MessageEvent
|
||||
initialData: T
|
||||
serverURL: string
|
||||
}): Promise<T> => {
|
||||
const { depth, event, initialData, serverURL } = args
|
||||
const { apiRoute, depth, event, initialData, serverURL } = args
|
||||
|
||||
if (event.origin === serverURL && event.data) {
|
||||
const eventData = JSON.parse(event?.data)
|
||||
|
||||
@@ -21,7 +23,17 @@ export const handleMessage = async <T>(args: {
|
||||
payloadLivePreviewFieldSchema = eventData.fieldSchemaJSON
|
||||
}
|
||||
|
||||
if (!payloadLivePreviewFieldSchema) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
|
||||
)
|
||||
|
||||
return initialData
|
||||
}
|
||||
|
||||
const mergedData = await mergeData<T>({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema: payloadLivePreviewFieldSchema,
|
||||
incomingData: eventData.data,
|
||||
|
||||
@@ -2,23 +2,29 @@ import type { fieldSchemaToJSON } from 'payload/utilities'
|
||||
|
||||
import { traverseFields } from './traverseFields'
|
||||
|
||||
export type MergeLiveDataArgs<T> = {
|
||||
export const mergeData = async <T>(args: {
|
||||
apiRoute?: string
|
||||
depth: number
|
||||
depth?: number
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
incomingData: Partial<T>
|
||||
initialData: T
|
||||
returnNumberOfRequests?: boolean
|
||||
serverURL: string
|
||||
}
|
||||
}): Promise<
|
||||
T & {
|
||||
_numberOfRequests?: number
|
||||
}
|
||||
> => {
|
||||
const {
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema,
|
||||
incomingData,
|
||||
initialData,
|
||||
returnNumberOfRequests,
|
||||
serverURL,
|
||||
} = args
|
||||
|
||||
export const mergeData = async <T>({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema,
|
||||
incomingData,
|
||||
initialData,
|
||||
serverURL,
|
||||
}: MergeLiveDataArgs<T>): Promise<T> => {
|
||||
const result = { ...initialData }
|
||||
|
||||
const populationPromises: Promise<void>[] = []
|
||||
@@ -35,5 +41,8 @@ export const mergeData = async <T>({
|
||||
|
||||
await Promise.all(populationPromises)
|
||||
|
||||
return result
|
||||
return {
|
||||
...result,
|
||||
...(returnNumberOfRequests ? { _numberOfRequests: populationPromises.length } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type Args = {
|
||||
export const promise = async (args: {
|
||||
accessor: number | string
|
||||
apiRoute?: string
|
||||
collection: string
|
||||
@@ -6,20 +6,23 @@ type Args = {
|
||||
id: number | string
|
||||
ref: Record<string, unknown>
|
||||
serverURL: string
|
||||
}
|
||||
}): Promise<void> => {
|
||||
const { id, accessor, apiRoute, collection, depth, ref, serverURL } = args
|
||||
|
||||
export const promise = async ({
|
||||
id,
|
||||
accessor,
|
||||
apiRoute,
|
||||
collection,
|
||||
depth,
|
||||
ref,
|
||||
serverURL,
|
||||
}: Args): Promise<void> => {
|
||||
const res: any = await fetch(
|
||||
`${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`,
|
||||
).then((res) => res.json())
|
||||
const url = `${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`
|
||||
|
||||
let res: Record<string, unknown> | null | undefined = null
|
||||
|
||||
try {
|
||||
res = await fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((res) => res.json())
|
||||
} catch (err) {
|
||||
console.error(err) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
ref[accessor] = res
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { handleMessage } from '.'
|
||||
|
||||
export const subscribe = <T>(args: {
|
||||
apiRoute?: string
|
||||
callback: (data: T) => void
|
||||
depth: number
|
||||
depth?: number
|
||||
initialData: T
|
||||
serverURL: string
|
||||
}): ((event: MessageEvent) => void) => {
|
||||
const { callback, depth, initialData, serverURL } = args
|
||||
const { apiRoute, callback, depth, initialData, serverURL } = args
|
||||
|
||||
const onMessage = async (event: MessageEvent) => {
|
||||
const mergedData = await handleMessage<T>({ depth, event, initialData, serverURL })
|
||||
const mergedData = await handleMessage<T>({ apiRoute, depth, event, initialData, serverURL })
|
||||
callback(mergedData)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,51 +2,52 @@ import type { fieldSchemaToJSON } from 'payload/utilities'
|
||||
|
||||
import { promise } from './promise'
|
||||
|
||||
type Args<T> = {
|
||||
export const traverseFields = <T>(args: {
|
||||
apiRoute?: string
|
||||
depth: number
|
||||
depth?: number
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
incomingData: T
|
||||
populationPromises: Promise<void>[]
|
||||
result: T
|
||||
serverURL: string
|
||||
}
|
||||
}): void => {
|
||||
const {
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema: fieldSchemas,
|
||||
incomingData,
|
||||
populationPromises,
|
||||
result,
|
||||
serverURL,
|
||||
} = args
|
||||
|
||||
export const traverseFields = <T>({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema,
|
||||
incomingData,
|
||||
populationPromises,
|
||||
result,
|
||||
serverURL,
|
||||
}: Args<T>): void => {
|
||||
fieldSchema.forEach((fieldJSON) => {
|
||||
if ('name' in fieldJSON && typeof fieldJSON.name === 'string') {
|
||||
const fieldName = fieldJSON.name
|
||||
fieldSchemas.forEach((fieldSchema) => {
|
||||
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
|
||||
const fieldName = fieldSchema.name
|
||||
|
||||
switch (fieldJSON.type) {
|
||||
switch (fieldSchema.type) {
|
||||
case 'array':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((row, i) => {
|
||||
const hasExistingRow =
|
||||
Array.isArray(result[fieldName]) &&
|
||||
typeof result[fieldName][i] === 'object' &&
|
||||
result[fieldName][i] !== null
|
||||
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
const newRow = hasExistingRow ? { ...result[fieldName][i] } : {}
|
||||
if (!result[fieldName][i]) {
|
||||
result[fieldName][i] = {}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema: fieldJSON.fields,
|
||||
incomingData: row,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingRow,
|
||||
populationPromises,
|
||||
result: newRow,
|
||||
result: result[fieldName][i],
|
||||
serverURL,
|
||||
})
|
||||
|
||||
return newRow
|
||||
return result[fieldName][i]
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -54,18 +55,21 @@ export const traverseFields = <T>({
|
||||
case 'blocks':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
|
||||
const incomingBlockJSON = fieldJSON.blocks[incomingBlock.blockType]
|
||||
const incomingBlockJSON = fieldSchema.blocks[incomingBlock.blockType]
|
||||
|
||||
// Compare the index and id to determine if this block already exists in the result
|
||||
// If so, we want to use the existing block as the base, otherwise take the incoming block
|
||||
// Either way, we will traverse the fields of the block to populate relationships
|
||||
const isExistingBlock =
|
||||
Array.isArray(result[fieldName]) &&
|
||||
typeof result[fieldName][i] === 'object' &&
|
||||
result[fieldName][i] !== null &&
|
||||
result[fieldName][i].id === incomingBlock.id
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
const block = isExistingBlock ? result[fieldName][i] : incomingBlock
|
||||
if (
|
||||
!result[fieldName][i] ||
|
||||
result[fieldName][i].id !== incomingBlock.id ||
|
||||
result[fieldName][i].blockType !== incomingBlock.blockType
|
||||
) {
|
||||
result[fieldName][i] = {
|
||||
blockType: incomingBlock.blockType,
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
@@ -73,11 +77,11 @@ export const traverseFields = <T>({
|
||||
fieldSchema: incomingBlockJSON.fields,
|
||||
incomingData: incomingBlock,
|
||||
populationPromises,
|
||||
result: block,
|
||||
result: result[fieldName][i],
|
||||
serverURL,
|
||||
})
|
||||
|
||||
return block
|
||||
return result[fieldName][i]
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = []
|
||||
@@ -94,7 +98,7 @@ export const traverseFields = <T>({
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema: fieldJSON.fields,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingData[fieldName] || {},
|
||||
populationPromises,
|
||||
result: result[fieldName],
|
||||
@@ -105,31 +109,35 @@ export const traverseFields = <T>({
|
||||
|
||||
case 'upload':
|
||||
case 'relationship':
|
||||
if (fieldJSON.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||
const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : []
|
||||
result[fieldName] = Array.isArray(result[fieldName])
|
||||
? [...result[fieldName]].slice(0, incomingData[fieldName].length)
|
||||
: []
|
||||
// Handle `hasMany` relationships
|
||||
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
incomingData[fieldName].forEach((relation, i) => {
|
||||
incomingData[fieldName].forEach((incomingRelation, i) => {
|
||||
// Handle `hasMany` polymorphic
|
||||
if (Array.isArray(fieldJSON.relationTo)) {
|
||||
const existingID = existingValue[i]?.value?.id
|
||||
|
||||
if (
|
||||
existingID !== relation.value ||
|
||||
existingValue[i]?.relationTo !== relation.relationTo
|
||||
) {
|
||||
if (Array.isArray(fieldSchema.relationTo)) {
|
||||
// if the field doesn't exist on the result, create it
|
||||
// the value will be populated later
|
||||
if (!result[fieldName][i]) {
|
||||
result[fieldName][i] = {
|
||||
relationTo: relation.relationTo,
|
||||
relationTo: incomingRelation.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
const oldID = result[fieldName][i]?.value?.id
|
||||
const oldRelation = result[fieldName][i]?.relationTo
|
||||
const newID = incomingRelation.value
|
||||
const newRelation = incomingRelation.relationTo
|
||||
|
||||
if (oldID !== newID || oldRelation !== newRelation) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: relation.value,
|
||||
id: incomingRelation.value,
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: relation.relationTo,
|
||||
collection: newRelation,
|
||||
depth,
|
||||
ref: result[fieldName][i],
|
||||
serverURL,
|
||||
@@ -138,15 +146,13 @@ export const traverseFields = <T>({
|
||||
}
|
||||
} else {
|
||||
// Handle `hasMany` monomorphic
|
||||
const existingID = existingValue[i]?.id
|
||||
|
||||
if (existingID !== relation) {
|
||||
if (result[fieldName][i]?.id !== incomingRelation) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: relation,
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
apiRoute,
|
||||
collection: String(fieldJSON.relationTo),
|
||||
collection: String(fieldSchema.relationTo),
|
||||
depth,
|
||||
ref: result[fieldName],
|
||||
serverURL,
|
||||
@@ -157,29 +163,49 @@ export const traverseFields = <T>({
|
||||
})
|
||||
} else {
|
||||
// Handle `hasOne` polymorphic
|
||||
if (Array.isArray(fieldJSON.relationTo)) {
|
||||
if (Array.isArray(fieldSchema.relationTo)) {
|
||||
// if the field doesn't exist on the result, create it
|
||||
// the value will be populated later
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = {
|
||||
relationTo: incomingData[fieldName]?.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
const hasNewValue =
|
||||
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
|
||||
incomingData[fieldName] &&
|
||||
typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName] !== null
|
||||
|
||||
const hasOldValue =
|
||||
typeof result[fieldName] === 'object' && result[fieldName] !== null
|
||||
result[fieldName] &&
|
||||
typeof result[fieldName] === 'object' &&
|
||||
result[fieldName] !== null
|
||||
|
||||
const newID = hasNewValue
|
||||
? typeof incomingData[fieldName].value === 'object'
|
||||
? incomingData[fieldName].value.id
|
||||
: incomingData[fieldName].value
|
||||
: ''
|
||||
|
||||
const oldID = hasOldValue
|
||||
? typeof result[fieldName].value === 'object'
|
||||
? result[fieldName].value.id
|
||||
: result[fieldName].value
|
||||
: ''
|
||||
|
||||
const newValue = hasNewValue ? incomingData[fieldName].value : ''
|
||||
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
|
||||
|
||||
const oldValue = hasOldValue ? result[fieldName].value : ''
|
||||
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
|
||||
|
||||
if (newValue !== oldValue || newRelation !== oldRelation) {
|
||||
if (newValue) {
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = {
|
||||
relationTo: newRelation,
|
||||
}
|
||||
}
|
||||
|
||||
// if the new value/relation is different from the old value/relation
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (newID !== oldID || newRelation !== oldRelation) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: newValue,
|
||||
id: newID,
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: newRelation,
|
||||
@@ -188,34 +214,36 @@ export const traverseFields = <T>({
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
} else {
|
||||
// Handle `hasOne` monomorphic
|
||||
const newID: string =
|
||||
(typeof incomingData[fieldName] === 'string' && incomingData[fieldName]) ||
|
||||
(typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName] !== null &&
|
||||
const newID: number | string | undefined =
|
||||
(incomingData[fieldName] &&
|
||||
typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName].id) ||
|
||||
''
|
||||
incomingData[fieldName]
|
||||
|
||||
const oldID: string =
|
||||
(typeof result[fieldName] === 'string' && result[fieldName]) ||
|
||||
(typeof result[fieldName] === 'object' &&
|
||||
result[fieldName] !== null &&
|
||||
const oldID: number | string | undefined =
|
||||
(result[fieldName] &&
|
||||
typeof result[fieldName] === 'object' &&
|
||||
result[fieldName].id) ||
|
||||
''
|
||||
result[fieldName]
|
||||
|
||||
// if the new value is different from the old value
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (newID !== oldID) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
apiRoute,
|
||||
collection: String(fieldJSON.relationTo),
|
||||
collection: String(fieldSchema.relationTo),
|
||||
depth,
|
||||
ref: result as Record<string, unknown>,
|
||||
serverURL,
|
||||
@@ -235,6 +263,4 @@ export const traverseFields = <T>({
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -26,9 +26,8 @@
|
||||
</h4>
|
||||
<hr/>
|
||||
|
||||
<h3>
|
||||
🎉 Payload 2.0 is now available! Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>
|
||||
</h3>
|
||||
> [!IMPORTANT]
|
||||
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
|
||||
|
||||
<h3>Benefits over a regular CMS</h3>
|
||||
<ul>
|
||||
@@ -52,7 +51,7 @@ Create a cloud account, connect your GitHub, and [deploy in minutes](https://pay
|
||||
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/getting-started/installation).
|
||||
|
||||
```text
|
||||
npx create-payload-app
|
||||
npx create-payload-app@latest
|
||||
```
|
||||
|
||||
Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.0.15",
|
||||
"version": "2.2.0",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -135,7 +135,7 @@
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-essentials": "7.0.3",
|
||||
"use-context-selector": "1.4.1",
|
||||
"uuid": "8.3.2"
|
||||
"uuid": "9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
|
||||
@@ -56,7 +56,12 @@ export const DocumentControls: React.FC<{
|
||||
|
||||
const { i18n, t } = useTranslation('general')
|
||||
|
||||
const showDotMenu = Boolean(collection && id && !disableActions)
|
||||
const hasCreatePermission = 'create' in permissions && permissions.create?.permission
|
||||
const hasDeletePermission = 'delete' in permissions && permissions.delete?.permission
|
||||
|
||||
const showDotMenu = Boolean(
|
||||
collection && id && !disableActions && (hasCreatePermission || hasDeletePermission),
|
||||
)
|
||||
|
||||
return (
|
||||
<Gutter className={baseClass}>
|
||||
@@ -203,7 +208,7 @@ export const DocumentControls: React.FC<{
|
||||
verticalAlign="bottom"
|
||||
>
|
||||
<PopupList.ButtonGroup>
|
||||
{'create' in permissions && permissions?.create?.permission && (
|
||||
{hasCreatePermission && (
|
||||
<React.Fragment>
|
||||
<PopupList.Button
|
||||
id="action-create"
|
||||
@@ -217,7 +222,7 @@ export const DocumentControls: React.FC<{
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{'delete' in permissions && permissions?.delete?.permission && id && (
|
||||
{hasDeletePermission && (
|
||||
<DeleteDocument buttonId="action-delete" collection={collection} id={id} />
|
||||
)}
|
||||
</PopupList.ButtonGroup>
|
||||
|
||||
@@ -1,107 +1,109 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../Button';
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../Button'
|
||||
|
||||
import './index.scss';
|
||||
import './index.scss'
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const baseClass = 'dropzone';
|
||||
const baseClass = 'dropzone'
|
||||
|
||||
type Props = {
|
||||
onChange: (e: FileList) => void;
|
||||
className?: string;
|
||||
mimeTypes?: string[];
|
||||
onChange: (e: FileList) => void
|
||||
className?: string
|
||||
mimeTypes?: string[]
|
||||
}
|
||||
|
||||
export const Dropzone: React.FC<Props> = ({ onChange, className, mimeTypes }) => {
|
||||
const dropRef = React.useRef<HTMLDivElement>(null);
|
||||
const [dragging, setDragging] = React.useState(false);
|
||||
const inputRef = React.useRef(null);
|
||||
const dropRef = React.useRef<HTMLDivElement>(null)
|
||||
const [dragging, setDragging] = React.useState(false)
|
||||
const inputRef = React.useRef(null)
|
||||
|
||||
const { t } = useTranslation(['upload', 'general']);
|
||||
const { t } = useTranslation(['upload', 'general'])
|
||||
|
||||
const handlePaste = React.useCallback((e: ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const handlePaste = React.useCallback(
|
||||
(e: ClipboardEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.clipboardData.files && e.clipboardData.files.length > 0) {
|
||||
onChange(e.clipboardData.files);
|
||||
}
|
||||
}, [onChange]);
|
||||
if (e.clipboardData.files && e.clipboardData.files.length > 0) {
|
||||
onChange(e.clipboardData.files)
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
|
||||
const handleDragEnter = React.useCallback((e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragging(true);
|
||||
}, []);
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragging(true)
|
||||
}, [])
|
||||
|
||||
const handleDragLeave = React.useCallback((e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragging(false);
|
||||
}, []);
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragging(false)
|
||||
}, [])
|
||||
|
||||
const handleDrop = React.useCallback((e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragging(false);
|
||||
const handleDrop = React.useCallback(
|
||||
(e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragging(false)
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
onChange(e.dataTransfer.files);
|
||||
setDragging(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
onChange(e.dataTransfer.files)
|
||||
setDragging(false)
|
||||
|
||||
e.dataTransfer.clearData();
|
||||
}
|
||||
}, [onChange]);
|
||||
e.dataTransfer.clearData()
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
|
||||
const handleFileSelection = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
onChange(e.target.files);
|
||||
}
|
||||
}, [onChange]);
|
||||
const handleFileSelection = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
onChange(e.target.files)
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const div = dropRef.current;
|
||||
const div = dropRef.current
|
||||
|
||||
if (div) {
|
||||
div.addEventListener('dragenter', handleDragEnter);
|
||||
div.addEventListener('dragleave', handleDragLeave);
|
||||
div.addEventListener('dragover', handleDragOver);
|
||||
div.addEventListener('drop', handleDrop);
|
||||
div.addEventListener('paste', handlePaste);
|
||||
div.addEventListener('dragenter', handleDragEnter)
|
||||
div.addEventListener('dragleave', handleDragLeave)
|
||||
div.addEventListener('dragover', handleDragOver)
|
||||
div.addEventListener('drop', handleDrop)
|
||||
div.addEventListener('paste', handlePaste)
|
||||
|
||||
return () => {
|
||||
div.removeEventListener('dragenter', handleDragEnter);
|
||||
div.removeEventListener('dragleave', handleDragLeave);
|
||||
div.removeEventListener('dragover', handleDragOver);
|
||||
div.removeEventListener('drop', handleDrop);
|
||||
div.removeEventListener('paste', handlePaste);
|
||||
};
|
||||
div.removeEventListener('dragenter', handleDragEnter)
|
||||
div.removeEventListener('dragleave', handleDragLeave)
|
||||
div.removeEventListener('dragover', handleDragOver)
|
||||
div.removeEventListener('drop', handleDrop)
|
||||
div.removeEventListener('paste', handlePaste)
|
||||
}
|
||||
}
|
||||
|
||||
return () => null;
|
||||
}, [handleDragEnter, handleDragLeave, handleDrop, handlePaste]);
|
||||
return () => null
|
||||
}, [handleDragEnter, handleDragLeave, handleDrop, handlePaste])
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
className,
|
||||
dragging ? 'dragging' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
const classes = [baseClass, className, dragging ? 'dragging' : ''].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dropRef}
|
||||
className={classes}
|
||||
>
|
||||
<div ref={dropRef} className={classes}>
|
||||
<Button
|
||||
size="small"
|
||||
buttonStyle="secondary"
|
||||
onClick={() => {
|
||||
inputRef.current.click();
|
||||
inputRef.current.click()
|
||||
}}
|
||||
className={`${baseClass}__file-button`}
|
||||
>
|
||||
@@ -117,10 +119,8 @@ export const Dropzone: React.FC<Props> = ({ onChange, className, mimeTypes }) =>
|
||||
/>
|
||||
|
||||
<p className={`${baseClass}__label`}>
|
||||
{t('or')}
|
||||
{' '}
|
||||
{t('dragAndDrop')}
|
||||
{t('general:or')} {t('dragAndDrop')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ $header-height: base(5);
|
||||
}
|
||||
}
|
||||
|
||||
&__draggable {
|
||||
@include btn-reset;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__focalPoint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
@@ -83,7 +83,7 @@ export const EditUpload: React.FC<{
|
||||
setFormQueryParams({
|
||||
...formQueryParams,
|
||||
uploadEdits: {
|
||||
crop: crop ? crop : undefined,
|
||||
crop: crop || undefined,
|
||||
focalPoint: pointPosition ? pointPosition : undefined,
|
||||
},
|
||||
})
|
||||
@@ -164,7 +164,15 @@ export const EditUpload: React.FC<{
|
||||
/>
|
||||
</ReactCrop>
|
||||
) : (
|
||||
<img alt={t('upload:setFocalPoint')} ref={imageRef} src={fileSrcToUse} />
|
||||
<img
|
||||
alt={t('upload:setFocalPoint')}
|
||||
onLoad={(e) => {
|
||||
setOriginalHeight(e.currentTarget.naturalHeight)
|
||||
setOriginalWidth(e.currentTarget.naturalWidth)
|
||||
}}
|
||||
ref={imageRef}
|
||||
src={fileSrcToUse}
|
||||
/>
|
||||
)}
|
||||
{showFocalPoint && (
|
||||
<DraggableElement
|
||||
@@ -273,7 +281,7 @@ const DraggableElement = ({
|
||||
}) => {
|
||||
const [position, setPosition] = useState({ x: initialPosition.x, y: initialPosition.y })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const dragRef = useRef<HTMLDivElement | undefined>()
|
||||
const dragRef = useRef<HTMLButtonElement | undefined>()
|
||||
|
||||
const getCoordinates = React.useCallback(
|
||||
(mouseXArg?: number, mouseYArg?: number, recenter?: boolean) => {
|
||||
@@ -319,7 +327,7 @@ const DraggableElement = ({
|
||||
|
||||
return { x, y }
|
||||
},
|
||||
[],
|
||||
[boundsRef, containerRef],
|
||||
)
|
||||
|
||||
const handleMouseDown = (event) => {
|
||||
@@ -349,7 +357,7 @@ const DraggableElement = ({
|
||||
setCheckBounds(false)
|
||||
return
|
||||
}
|
||||
}, [getCoordinates, isDragging, checkBounds, setCheckBounds, position.x, position.y])
|
||||
}, [getCoordinates, isDragging, checkBounds, setCheckBounds, position.x, position.y, onDragEnd])
|
||||
|
||||
React.useEffect(() => {
|
||||
setPosition({ x: initialPosition.x, y: initialPosition.y })
|
||||
@@ -365,15 +373,16 @@ const DraggableElement = ({
|
||||
.join(' ')}
|
||||
onMouseMove={handleMouseMove}
|
||||
>
|
||||
<div
|
||||
<button
|
||||
className={[`${baseClass}__draggable`, className].filter(Boolean).join(' ')}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={onDrop}
|
||||
ref={dragRef}
|
||||
style={{ left: `${position.x}%`, position: 'absolute', top: `${position.y}%` }}
|
||||
style={{ left: `${position.x}%`, top: `${position.y}%` }}
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
<div />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ const FileDetails: React.FC<Props> = (props) => {
|
||||
width={width as number}
|
||||
/>
|
||||
|
||||
{isImage(mimeType as string) && (
|
||||
{isImage(mimeType as string) && mimeType !== 'image/svg+xml' && (
|
||||
<UploadActions canEdit={canEdit} showSizePreviews={hasImageSizes && doc.filename} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import React from 'react'
|
||||
import { Chevron } from '../../..'
|
||||
import { useLocale } from '../../../utilities/Locale'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Chevron } from '../../..'
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import { useLocale } from '../../../utilities/Locale'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'localizer-button'
|
||||
|
||||
export const LocalizerLabel: React.FC<{
|
||||
className?: string
|
||||
ariaLabel?: string
|
||||
className?: string
|
||||
}> = (props) => {
|
||||
const { className, ariaLabel } = props
|
||||
const { ariaLabel, className } = props
|
||||
const locale = useLocale()
|
||||
const { t } = useTranslation('general')
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[baseClass, className].filter(Boolean).join(' ')}
|
||||
aria-label={ariaLabel || t('locale')}
|
||||
className={[baseClass, className].filter(Boolean).join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__label`}>{`${t('locale')}:`}</div>
|
||||
|
||||
<span className={`${baseClass}__current-label`}>{`${locale.label}`}</span>
|
||||
<span className={`${baseClass}__current-label`}>{`${getTranslation(
|
||||
locale.label,
|
||||
i18n,
|
||||
)}`}</span>
|
||||
|
||||
<Chevron className={`${baseClass}__chevron`} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import qs from 'qs'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { getTranslation } from '../../../../utilities/getTranslation'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import { useLocale } from '../../utilities/Locale'
|
||||
import { useSearchParams } from '../../utilities/SearchParams'
|
||||
@@ -18,9 +20,12 @@ const Localizer: React.FC<{
|
||||
const config = useConfig()
|
||||
const { localization } = config
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const localeLabel = getTranslation(locale.label, i18n)
|
||||
|
||||
if (localization) {
|
||||
const { locales } = localization
|
||||
|
||||
@@ -44,8 +49,8 @@ const Localizer: React.FC<{
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{locale.label}
|
||||
{locale.label !== locale.code && ` (${locale.code})`}
|
||||
{localeLabel}
|
||||
{localeLabel !== locale.code && ` (${locale.code})`}
|
||||
</PopupList.Button>
|
||||
) : null}
|
||||
|
||||
@@ -57,11 +62,12 @@ const Localizer: React.FC<{
|
||||
locale: localeOption.code,
|
||||
}
|
||||
const search = qs.stringify(newParams)
|
||||
const localeOptionLabel = getTranslation(localeOption.label, i18n)
|
||||
|
||||
return (
|
||||
<PopupList.Button key={localeOption.code} onClick={close} to={{ search }}>
|
||||
{localeOption.label}
|
||||
{localeOption.label !== localeOption.code && ` (${localeOption.code})`}
|
||||
{localeOptionLabel}
|
||||
{localeOptionLabel !== localeOption.code && ` (${localeOption.code})`}
|
||||
</PopupList.Button>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
&__sizeOption {
|
||||
padding: base(0.5);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: base(1);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import type { SanitizedCollectionConfig } from '../../../../exports/types'
|
||||
import type { FileSizes, Upload } from '../../../../uploads/types'
|
||||
@@ -23,9 +23,49 @@ const sortSizes = (sizes: FileSizes, imageSizes: Upload['imageSizes']) => {
|
||||
return orderedSizes
|
||||
}
|
||||
|
||||
type PreviewSizeCardProps = {
|
||||
active: boolean
|
||||
baseURL: string
|
||||
meta: FileSizes[0]
|
||||
name: string
|
||||
onClick?: () => void
|
||||
previewSrc: string
|
||||
}
|
||||
const PreviewSizeCard: React.FC<PreviewSizeCardProps> = ({
|
||||
name,
|
||||
active,
|
||||
baseURL,
|
||||
meta,
|
||||
onClick,
|
||||
previewSrc,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={[`${baseClass}__sizeOption`, active && `${baseClass}--selected`]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={typeof onClick === 'function' ? onClick : undefined}
|
||||
onKeyDown={(e) => {
|
||||
if (typeof onClick !== 'function') return
|
||||
if (e.key === 'Enter') onClick()
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className={`${baseClass}__image`}>
|
||||
<img alt={meta.filename} src={previewSrc} />
|
||||
</div>
|
||||
<div className={`${baseClass}__sizeMeta`}>
|
||||
<div className={`${baseClass}__sizeName`}>{name}</div>
|
||||
<Meta {...meta} staticURL={baseURL} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PreviewSizes: React.FC<{
|
||||
collection: SanitizedCollectionConfig
|
||||
doc?: Data & {
|
||||
doc: Data & {
|
||||
sizes?: FileSizes
|
||||
}
|
||||
imageCacheTag?: string
|
||||
@@ -36,9 +76,7 @@ const PreviewSizes: React.FC<{
|
||||
const { sizes } = doc
|
||||
|
||||
const [orderedSizes, setOrderedSizes] = useState<FileSizes>(() => sortSizes(sizes, imageSizes))
|
||||
const [selectedSize, setSelectedSize] = useState<null | string>(
|
||||
orderedSizes?.[imageSizes[0]?.name]?.filename ? imageSizes[0]?.name : null,
|
||||
)
|
||||
const [selectedSize, setSelectedSize] = useState<null | string>(null)
|
||||
|
||||
const generateImageUrl = (filename) => {
|
||||
return `${staticURL}/${filename}${imageCacheTag ? `?${imageCacheTag}` : ''}`
|
||||
@@ -47,47 +85,60 @@ const PreviewSizes: React.FC<{
|
||||
setOrderedSizes(sortSizes(sizes, imageSizes))
|
||||
}, [sizes, imageSizes, imageCacheTag])
|
||||
|
||||
const mainPreviewSrc = generateImageUrl(`${orderedSizes[selectedSize]?.filename}`)
|
||||
const mainPreviewSrc = selectedSize
|
||||
? generateImageUrl(`${orderedSizes[selectedSize]?.filename}`)
|
||||
: generateImageUrl(doc.filename)
|
||||
|
||||
const originalImage = useMemo(
|
||||
(): FileSizes[0] => ({
|
||||
filename: doc.filename,
|
||||
filesize: doc.filesize,
|
||||
height: doc.height,
|
||||
mimeType: doc.mimeType,
|
||||
width: doc.width,
|
||||
}),
|
||||
[doc],
|
||||
)
|
||||
const originalFilename = 'Original'
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__imageWrap`}>
|
||||
<div className={`${baseClass}__meta`}>
|
||||
<div className={`${baseClass}__sizeName`}>{selectedSize}</div>
|
||||
<Meta {...(selectedSize && orderedSizes[selectedSize])} staticURL={staticURL} />
|
||||
<div className={`${baseClass}__sizeName`}>{selectedSize || originalFilename}</div>
|
||||
<Meta
|
||||
{...(selectedSize ? orderedSizes[selectedSize] : originalImage)}
|
||||
staticURL={staticURL}
|
||||
/>
|
||||
</div>
|
||||
<img alt={doc.filename} className={`${baseClass}__preview`} src={mainPreviewSrc} />
|
||||
</div>
|
||||
<div className={`${baseClass}__listWrap`}>
|
||||
<div className={`${baseClass}__list`}>
|
||||
<PreviewSizeCard
|
||||
active={!selectedSize}
|
||||
baseURL={staticURL}
|
||||
meta={originalImage}
|
||||
name={originalFilename}
|
||||
onClick={() => setSelectedSize(null)}
|
||||
previewSrc={generateImageUrl(doc.filename)}
|
||||
/>
|
||||
|
||||
{Object.entries(orderedSizes).map(([key, val]) => {
|
||||
const selected = selectedSize === key
|
||||
const previewSrc = generateImageUrl(val.filename)
|
||||
const previewSrc = val.filename ? generateImageUrl(val.filename) : undefined
|
||||
|
||||
if (previewSrc) {
|
||||
return (
|
||||
<div
|
||||
className={[`${baseClass}__sizeOption`, selected && `${baseClass}--selected`]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
<PreviewSizeCard
|
||||
active={selected}
|
||||
baseURL={staticURL}
|
||||
key={key}
|
||||
meta={val}
|
||||
name={key}
|
||||
onClick={() => setSelectedSize(key)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13) {
|
||||
setSelectedSize(key)
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className={`${baseClass}__image`}>
|
||||
<img alt={val.filename} src={previewSrc} />
|
||||
</div>
|
||||
<div className={`${baseClass}__sizeMeta`}>
|
||||
<div className={`${baseClass}__sizeName`}>{key}</div>
|
||||
<Meta {...val} staticURL={staticURL} />
|
||||
</div>
|
||||
</div>
|
||||
previewSrc={previewSrc}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import qs from 'qs'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useForm, useFormModified } from '../../forms/Form/context'
|
||||
import FormSubmit from '../../forms/Submit'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
||||
import { useLocale } from '../../utilities/Locale'
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent'
|
||||
|
||||
export type CustomPublishButtonProps = React.ComponentType<
|
||||
@@ -12,6 +15,7 @@ export type CustomPublishButtonProps = React.ComponentType<
|
||||
}
|
||||
>
|
||||
export type DefaultPublishButtonProps = {
|
||||
canPublish: boolean
|
||||
disabled: boolean
|
||||
id?: string
|
||||
label: string
|
||||
@@ -19,10 +23,13 @@ export type DefaultPublishButtonProps = {
|
||||
}
|
||||
const DefaultPublishButton: React.FC<DefaultPublishButtonProps> = ({
|
||||
id,
|
||||
canPublish,
|
||||
disabled,
|
||||
label,
|
||||
publish,
|
||||
}) => {
|
||||
if (!canPublish) return null
|
||||
|
||||
return (
|
||||
<FormSubmit buttonId={id} disabled={disabled} onClick={publish} size="small" type="button">
|
||||
{label}
|
||||
@@ -35,22 +42,68 @@ type Props = {
|
||||
}
|
||||
|
||||
export const Publish: React.FC<Props> = ({ CustomComponent }) => {
|
||||
const { publishedDoc, unpublishedVersions } = useDocumentInfo()
|
||||
const { submit } = useForm()
|
||||
const { code } = useLocale()
|
||||
const { id, collection, global, publishedDoc, unpublishedVersions } = useDocumentInfo()
|
||||
const [hasPublishPermission, setHasPublishPermission] = React.useState(false)
|
||||
const { getData, submit } = useForm()
|
||||
const modified = useFormModified()
|
||||
const {
|
||||
routes: { api },
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
const { t } = useTranslation('version')
|
||||
|
||||
const hasNewerVersions = unpublishedVersions?.totalDocs > 0
|
||||
const canPublish = modified || hasNewerVersions || !publishedDoc
|
||||
|
||||
const publish = useCallback(() => {
|
||||
submit({
|
||||
void submit({
|
||||
overrides: {
|
||||
_status: 'published',
|
||||
},
|
||||
})
|
||||
}, [submit])
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchPublishAccess = async () => {
|
||||
let docAccessURL: string
|
||||
let operation = 'update'
|
||||
|
||||
const params = {
|
||||
locale: code || undefined,
|
||||
}
|
||||
if (global) {
|
||||
docAccessURL = `/globals/${global.slug}/access`
|
||||
} else if (collection) {
|
||||
if (!id) operation = 'create'
|
||||
docAccessURL = `/${collection.slug}/access${id ? `/${id}` : ''}`
|
||||
}
|
||||
|
||||
if (docAccessURL) {
|
||||
const data = getData()
|
||||
|
||||
const res = await fetch(`${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`, {
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
_status: 'published',
|
||||
}),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'post',
|
||||
})
|
||||
const json = await res.json()
|
||||
const result = Boolean(json?.[operation]?.permission)
|
||||
setHasPublishPermission(result)
|
||||
} else {
|
||||
setHasPublishPermission(true)
|
||||
}
|
||||
}
|
||||
|
||||
void fetchPublishAccess()
|
||||
}, [api, code, collection, getData, global, id, serverURL])
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={CustomComponent}
|
||||
@@ -58,6 +111,7 @@ export const Publish: React.FC<Props> = ({ CustomComponent }) => {
|
||||
componentProps={{
|
||||
id: 'action-save',
|
||||
DefaultButton: DefaultPublishButton,
|
||||
canPublish: hasPublishPermission,
|
||||
disabled: !canPublish,
|
||||
label: t('publishChanges'),
|
||||
publish,
|
||||
|
||||
@@ -6,9 +6,9 @@ import DatePicker from '../../../DatePicker'
|
||||
|
||||
const baseClass = 'condition-value-date'
|
||||
|
||||
const DateField: React.FC<Props> = ({ onChange, value }) => (
|
||||
const DateField: React.FC<Props> = ({ disabled, onChange, value }) => (
|
||||
<div className={baseClass}>
|
||||
<DatePicker onChange={onChange} value={value} />
|
||||
<DatePicker onChange={onChange} readOnly={disabled} value={value} />
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Props = {
|
||||
disabled?: boolean
|
||||
onChange: () => void
|
||||
value: Date
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'condition-value-number'
|
||||
|
||||
const NumberField: React.FC<Props> = ({ onChange, value }) => {
|
||||
const NumberField: React.FC<Props> = ({ disabled, onChange, value }) => {
|
||||
const { t } = useTranslation('general')
|
||||
return (
|
||||
<input
|
||||
className={baseClass}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={t('enterAValue')}
|
||||
type="number"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Props = {
|
||||
disabled?: boolean
|
||||
onChange: (e: string) => void
|
||||
value: string
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const baseClass = 'condition-value-relationship'
|
||||
const maxResultsPerRequest = 10
|
||||
|
||||
const RelationshipField: React.FC<Props> = (props) => {
|
||||
const { admin: { isSortable } = {}, hasMany, onChange, relationTo, value } = props
|
||||
const { admin: { isSortable } = {}, disabled, hasMany, onChange, relationTo, value } = props
|
||||
|
||||
const {
|
||||
collections,
|
||||
@@ -261,6 +261,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
<div className={classes}>
|
||||
{!errorLoading && (
|
||||
<ReactSelect
|
||||
disabled={disabled}
|
||||
isMulti={hasMany}
|
||||
isSortable={isSortable}
|
||||
onChange={(selected) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { PaginatedDocs } from '../../../../../../database/types'
|
||||
import type { RelationshipField } from '../../../../../../fields/config/types'
|
||||
|
||||
export type Props = {
|
||||
disabled?: boolean
|
||||
onChange: (val: unknown) => void
|
||||
value: unknown
|
||||
} & RelationshipField
|
||||
|
||||
@@ -20,6 +20,7 @@ const formatOptions = (options: Option[]): OptionObject[] =>
|
||||
})
|
||||
|
||||
export const Select: React.FC<Props> = ({
|
||||
disabled,
|
||||
onChange,
|
||||
operator,
|
||||
options: optionsFromProps,
|
||||
@@ -79,6 +80,7 @@ export const Select: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
disabled={disabled}
|
||||
isMulti={isMulti}
|
||||
onChange={onSelect}
|
||||
options={options.map((option) => ({ ...option, label: getTranslation(option.label, i18n) }))}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Option } from '../../../../../../fields/config/types'
|
||||
import type { Operator } from '../../../../../../types'
|
||||
|
||||
export type Props = {
|
||||
disabled?: boolean
|
||||
onChange: (val: string) => void
|
||||
operator: Operator
|
||||
options: Option[]
|
||||
|
||||
@@ -7,11 +7,12 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'condition-value-text'
|
||||
|
||||
const Text: React.FC<Props> = ({ onChange, value }) => {
|
||||
const Text: React.FC<Props> = ({ disabled, onChange, value }) => {
|
||||
const { t } = useTranslation('general')
|
||||
return (
|
||||
<input
|
||||
className={baseClass}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={t('enterAValue')}
|
||||
type="text"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Props = {
|
||||
disabled?: boolean
|
||||
onChange: (val: string) => void
|
||||
value: string
|
||||
}
|
||||
|
||||
@@ -26,25 +26,29 @@ const baseClass = 'condition'
|
||||
|
||||
const Condition: React.FC<Props> = (props) => {
|
||||
const { andIndex, dispatch, fields, orIndex, value } = props
|
||||
const fieldValue = Object.keys(value)[0]
|
||||
const operatorAndValue = value?.[fieldValue] ? Object.entries(value[fieldValue])[0] : undefined
|
||||
|
||||
const operatorValue = operatorAndValue?.[0]
|
||||
const queryValue = operatorAndValue?.[1]
|
||||
|
||||
const fieldName = Object.keys(value)[0]
|
||||
const [activeField, setActiveField] = useState<FieldCondition>(() =>
|
||||
fields.find((field) => fieldValue === field.value),
|
||||
fields.find((field) => fieldName === field.value),
|
||||
)
|
||||
|
||||
const operatorAndValue = value?.[fieldName] ? Object.entries(value[fieldName])[0] : undefined
|
||||
const queryValue = operatorAndValue?.[1]
|
||||
const operatorValue = operatorAndValue?.[0]
|
||||
|
||||
const [internalValue, setInternalValue] = useState(queryValue)
|
||||
const [internalOperatorField, setInternalOperatorField] = useState(operatorValue)
|
||||
|
||||
const debouncedValue = useDebounce(internalValue, 300)
|
||||
|
||||
useEffect(() => {
|
||||
const newActiveField = fields.find((field) => fieldValue === field.value)
|
||||
const newActiveField = fields.find(({ value: name }) => name === fieldName)
|
||||
|
||||
if (newActiveField) {
|
||||
if (newActiveField && newActiveField !== activeField) {
|
||||
setActiveField(newActiveField)
|
||||
setInternalOperatorField(null)
|
||||
setInternalValue('')
|
||||
}
|
||||
}, [fieldValue, fields])
|
||||
}, [fieldName, fields, activeField])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
@@ -73,21 +77,23 @@ const Condition: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__inputs`}>
|
||||
<div className={`${baseClass}__field`}>
|
||||
<ReactSelect
|
||||
onChange={(field) =>
|
||||
isClearable={false}
|
||||
onChange={(field) => {
|
||||
dispatch({
|
||||
andIndex,
|
||||
field: field?.value || undefined,
|
||||
orIndex,
|
||||
andIndex: andIndex,
|
||||
field: field?.value,
|
||||
orIndex: orIndex,
|
||||
type: 'update',
|
||||
})
|
||||
}
|
||||
}}
|
||||
options={fields}
|
||||
value={fields.find((field) => fieldValue === field.value)}
|
||||
value={fields.find((field) => fieldName === field.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__operator`}>
|
||||
<ReactSelect
|
||||
disabled={!fieldValue}
|
||||
disabled={!fieldName}
|
||||
isClearable={false}
|
||||
onChange={(operator) => {
|
||||
dispatch({
|
||||
andIndex,
|
||||
@@ -95,9 +101,14 @@ const Condition: React.FC<Props> = (props) => {
|
||||
orIndex,
|
||||
type: 'update',
|
||||
})
|
||||
setInternalOperatorField(operator.value)
|
||||
}}
|
||||
options={activeField.operators}
|
||||
value={activeField.operators.find((operator) => operatorValue === operator.value)}
|
||||
value={
|
||||
activeField.operators.find(
|
||||
(operator) => internalOperatorField === operator.value,
|
||||
) || null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__value`}>
|
||||
@@ -106,6 +117,7 @@ const Condition: React.FC<Props> = (props) => {
|
||||
DefaultComponent={ValueComponent}
|
||||
componentProps={{
|
||||
...activeField?.props,
|
||||
disabled: !operatorValue,
|
||||
onChange: setInternalValue,
|
||||
operator: operatorValue,
|
||||
options: valueOptions,
|
||||
|
||||
@@ -59,17 +59,17 @@ const reducer = (state: Where[], action: Action): Where[] => {
|
||||
|
||||
if (field) {
|
||||
newState[orIndex].and[andIndex] = {
|
||||
[field]: {
|
||||
[Object.keys(existingCondition)[0]]: Object.values(existingCondition)[0],
|
||||
},
|
||||
[field]: operator ? { [operator]: value } : {},
|
||||
}
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
newState[orIndex].and[andIndex] = {
|
||||
[existingFieldName]: {
|
||||
[Object.keys(existingCondition)[0]]: value,
|
||||
},
|
||||
[existingFieldName]: Object.keys(existingCondition)[0]
|
||||
? {
|
||||
[Object.keys(existingCondition)[0]]: value,
|
||||
}
|
||||
: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,9 +281,9 @@ export const addFieldStatePromise = async ({
|
||||
return {
|
||||
relationTo: relationship.relationTo,
|
||||
value:
|
||||
typeof relationship.value === 'string'
|
||||
? relationship.value
|
||||
: relationship.value?.id,
|
||||
relationship.value && typeof relationship.value === 'object'
|
||||
? relationship.value?.id
|
||||
: relationship.value,
|
||||
}
|
||||
}
|
||||
if (typeof relationship === 'object' && relationship !== null) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.section-title {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Props as LabelProps } from '../../Label/types'
|
||||
|
||||
import Check from '../../../icons/Check'
|
||||
import Line from '../../../icons/Line'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'checkbox-input'
|
||||
|
||||
type CheckboxInputProps = {
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
'aria-label'?: string
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
checked?: boolean
|
||||
className?: string
|
||||
id?: string
|
||||
@@ -25,7 +30,10 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
Label,
|
||||
afterInput,
|
||||
'aria-label': ariaLabel,
|
||||
beforeInput,
|
||||
checked,
|
||||
className,
|
||||
inputRef,
|
||||
@@ -36,6 +44,8 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
required,
|
||||
} = props
|
||||
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
@@ -48,6 +58,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__input`}>
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
aria-label={ariaLabel}
|
||||
defaultChecked={Boolean(checked)}
|
||||
@@ -58,12 +69,13 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
ref={inputRef}
|
||||
type="checkbox"
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
<span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
|
||||
{!partialChecked && <Check />}
|
||||
{partialChecked && <Line />}
|
||||
</span>
|
||||
</div>
|
||||
{label && <Label htmlFor={id} label={label} required={required} />}
|
||||
{label && <LabelComp htmlFor={id} label={label} required={required} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Props } from './types'
|
||||
|
||||
import { checkbox } from '../../../../../fields/validations'
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
@@ -18,7 +18,15 @@ const baseClass = 'checkbox'
|
||||
const Checkbox: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
admin: { className, condition, description, readOnly, style, width } = {},
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
disableFormData,
|
||||
label,
|
||||
onChange,
|
||||
@@ -27,6 +35,8 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
validate = checkbox,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const path = pathFromProps || name
|
||||
@@ -72,9 +82,12 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error alignCaret="left" message={errorMessage} showError={showError} />
|
||||
<ErrorComp alignCaret="left" message={errorMessage} showError={showError} />
|
||||
</div>
|
||||
<CheckboxInput
|
||||
Label={Label}
|
||||
afterInput={afterInput}
|
||||
beforeInput={beforeInput}
|
||||
checked={Boolean(value)}
|
||||
id={fieldID}
|
||||
label={getTranslation(label || name, i18n)}
|
||||
|
||||
@@ -4,13 +4,13 @@ import type { Props } from './types'
|
||||
|
||||
import { code } from '../../../../../fields/validations'
|
||||
import { CodeEditor } from '../../../elements/CodeEditor'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const prismToMonacoLanguageMap = {
|
||||
js: 'javascript',
|
||||
@@ -24,6 +24,7 @@ const Code: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label } = {},
|
||||
condition,
|
||||
description,
|
||||
editorOptions,
|
||||
@@ -38,6 +39,9 @@ const Code: React.FC<Props> = (props) => {
|
||||
validate = code,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
const path = pathFromProps || name
|
||||
|
||||
const memoizedValidate = useCallback(
|
||||
@@ -69,8 +73,8 @@ const Code: React.FC<Props> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={`field-${path}`} label={label} required={required} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
|
||||
<CodeEditor
|
||||
defaultLanguage={prismToMonacoLanguageMap[language] || language}
|
||||
onChange={readOnly ? () => null : (val) => setValue(val)}
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { Description } from '../../FieldDescription/types'
|
||||
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import DatePicker from '../../../elements/DatePicker'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
@@ -16,6 +16,12 @@ const baseClass = 'date-time-field'
|
||||
|
||||
export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
|
||||
className?: string
|
||||
components: {
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
datePickerProps?: DateField['admin']['date']
|
||||
description?: Description
|
||||
errorMessage?: string
|
||||
@@ -33,6 +39,7 @@ export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
|
||||
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
const {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
datePickerProps,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -48,6 +55,9 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
width,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
return (
|
||||
@@ -67,10 +77,11 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
</div>
|
||||
<Label htmlFor={path} label={label} required={required} />
|
||||
<LabelComp htmlFor={path} label={label} required={required} />
|
||||
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<DatePicker
|
||||
{...datePickerProps}
|
||||
onChange={onChange}
|
||||
@@ -78,6 +89,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
readOnly={readOnly}
|
||||
value={value}
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,17 @@ import './index.scss'
|
||||
const DateTime: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
admin: { className, condition, date, description, placeholder, readOnly, style, width } = {},
|
||||
admin: {
|
||||
className,
|
||||
components,
|
||||
condition,
|
||||
date,
|
||||
description,
|
||||
placeholder,
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
required,
|
||||
@@ -36,6 +46,7 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<DateTimeInput
|
||||
className={className}
|
||||
components={components}
|
||||
datePickerProps={date}
|
||||
description={description}
|
||||
errorMessage={errorMessage}
|
||||
|
||||
@@ -5,13 +5,13 @@ import type { Props } from './types'
|
||||
|
||||
import { email } from '../../../../../fields/validations'
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const Email: React.FC<Props> = (props) => {
|
||||
const {
|
||||
@@ -19,6 +19,7 @@ const Email: React.FC<Props> = (props) => {
|
||||
admin: {
|
||||
autoComplete,
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -51,6 +52,9 @@ const Email: React.FC<Props> = (props) => {
|
||||
|
||||
const { errorMessage, setValue, showError, value } = fieldType
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[fieldBaseClass, 'email', className, showError && 'error', readOnly && 'read-only']
|
||||
@@ -61,18 +65,22 @@ const Email: React.FC<Props> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<input
|
||||
autoComplete={autoComplete}
|
||||
disabled={Boolean(readOnly)}
|
||||
id={`field-${path.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={setValue}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
type="email"
|
||||
value={(value as string) || ''}
|
||||
/>
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<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
|
||||
autoComplete={autoComplete}
|
||||
disabled={Boolean(readOnly)}
|
||||
id={`field-${path.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={setValue}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
type="email"
|
||||
value={(value as string) || ''}
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { Props } from './types'
|
||||
|
||||
import { json } from '../../../../../fields/validations'
|
||||
import { CodeEditor } from '../../../elements/CodeEditor'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
@@ -17,13 +17,25 @@ const baseClass = 'json-field'
|
||||
const JSONField: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
admin: { className, condition, description, editorOptions, readOnly, style, width } = {},
|
||||
admin: {
|
||||
className,
|
||||
condition,
|
||||
description,
|
||||
editorOptions,
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
required,
|
||||
validate = json,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
const path = pathFromProps || name
|
||||
const [stringValue, setStringValue] = useState<string>()
|
||||
const [jsonError, setJsonError] = useState<string>()
|
||||
@@ -76,8 +88,8 @@ const JSONField: React.FC<Props> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={`field-${path}`} label={label} required={required} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
|
||||
<CodeEditor
|
||||
defaultLanguage="json"
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -8,18 +8,28 @@ import { number } from '../../../../../fields/validations'
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import { isNumber } from '../../../../../utilities/isNumber'
|
||||
import ReactSelect from '../../../elements/ReactSelect'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const NumberField: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
admin: { className, condition, description, placeholder, readOnly, step, style, width } = {},
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
readOnly,
|
||||
step,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
hasMany,
|
||||
label,
|
||||
max,
|
||||
@@ -31,6 +41,9 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
validate = number,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const path = pathFromProps || name
|
||||
@@ -118,8 +131,8 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
{hasMany ? (
|
||||
<ReactSelect
|
||||
className={`field-${path.replace(/\./g, '__')}`}
|
||||
@@ -148,21 +161,25 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
value={valueToRender as Option[]}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-${path.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={handleChange}
|
||||
onWheel={(e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
e.target.blur()
|
||||
}}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
step={step}
|
||||
type="number"
|
||||
value={typeof value === 'number' ? value : ''}
|
||||
/>
|
||||
<div className="input-wrapper">
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-${path.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={handleChange}
|
||||
onWheel={(e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
e.target.blur()
|
||||
}}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
step={step}
|
||||
type="number"
|
||||
value={typeof value === 'number' ? value : ''}
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FieldDescription description={description} value={value} />
|
||||
|
||||
@@ -5,26 +5,39 @@ import type { Props } from './types'
|
||||
|
||||
import { point } from '../../../../../fields/validations'
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'point'
|
||||
|
||||
const PointField: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
admin: { className, condition, description, placeholder, readOnly, step, style, width } = {},
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
readOnly,
|
||||
step,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
required,
|
||||
validate = point,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
const path = pathFromProps || name
|
||||
|
||||
const { i18n, t } = useTranslation('fields')
|
||||
@@ -76,41 +89,49 @@ const PointField: React.FC<Props> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<ul className={`${baseClass}__wrap`}>
|
||||
<li>
|
||||
<Label
|
||||
<LabelComp
|
||||
htmlFor={`field-longitude-${path.replace(/\./g, '__')}`}
|
||||
label={`${getTranslation(label || name, i18n)} - ${t('longitude')}`}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-longitude-${path.replace(/\./g, '__')}`}
|
||||
name={`${path}.longitude`}
|
||||
onChange={(e) => handleChange(e, 0)}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
step={step}
|
||||
type="number"
|
||||
value={value && typeof value[0] === 'number' ? value[0] : ''}
|
||||
/>
|
||||
<div className="input-wrapper">
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-longitude-${path.replace(/\./g, '__')}`}
|
||||
name={`${path}.longitude`}
|
||||
onChange={(e) => handleChange(e, 0)}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
step={step}
|
||||
type="number"
|
||||
value={value && typeof value[0] === 'number' ? value[0] : ''}
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Label
|
||||
<LabelComp
|
||||
htmlFor={`field-latitude-${path.replace(/\./g, '__')}`}
|
||||
label={`${getTranslation(label || name, i18n)} - ${t('latitude')}`}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-latitude-${path.replace(/\./g, '__')}`}
|
||||
name={`${path}.latitude`}
|
||||
onChange={(e) => handleChange(e, 1)}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
step={step}
|
||||
type="number"
|
||||
value={value && typeof value[1] === 'number' ? value[1] : ''}
|
||||
/>
|
||||
<div className="input-wrapper">
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-latitude-${path.replace(/\./g, '__')}`}
|
||||
name={`${path}.latitude`}
|
||||
onChange={(e) => handleChange(e, 1)}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
step={step}
|
||||
type="number"
|
||||
value={value && typeof value[1] === 'number' ? value[1] : ''}
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<FieldDescription description={description} value={value} />
|
||||
|
||||
@@ -5,9 +5,9 @@ import type { Description } from '../../FieldDescription/types'
|
||||
import type { OnChange } from './types'
|
||||
|
||||
import { optionIsObject } from '../../../../../fields/config/types'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import RadioInput from './RadioInput'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
@@ -28,6 +28,8 @@ export type RadioGroupInputProps = Omit<RadioField, 'type'> & {
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
}
|
||||
|
||||
const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
|
||||
@@ -47,8 +49,13 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
const path = pathFromProps || name
|
||||
|
||||
return (
|
||||
@@ -69,9 +76,9 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
</div>
|
||||
<Label htmlFor={`field-${path}`} label={label} required={required} />
|
||||
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
|
||||
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
|
||||
{options.map((option) => {
|
||||
let optionValue = ''
|
||||
|
||||
@@ -18,6 +18,7 @@ const RadioGroup: React.FC<Props> = (props) => {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
label,
|
||||
options,
|
||||
@@ -57,6 +58,8 @@ const RadioGroup: React.FC<Props> = (props) => {
|
||||
style={style}
|
||||
value={value}
|
||||
width={width}
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@ import { useAuth } from '../../../utilities/Auth'
|
||||
import { useConfig } from '../../../utilities/Config'
|
||||
import { GetFilterOptions } from '../../../utilities/GetFilterOptions'
|
||||
import { useLocale } from '../../../utilities/Locale'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import { useFormProcessing } from '../../Form/context'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import { AddNewRelation } from './AddNew'
|
||||
import { createRelationMap } from './createRelationMap'
|
||||
import { findOptionsByValue } from './findOptionsByValue'
|
||||
@@ -28,7 +29,6 @@ import './index.scss'
|
||||
import optionsReducer from './optionsReducer'
|
||||
import { MultiValueLabel } from './select-components/MultiValueLabel'
|
||||
import { SingleValue } from './select-components/SingleValue'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
|
||||
const maxResultsPerRequest = 10
|
||||
|
||||
@@ -46,6 +46,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
filterOptions,
|
||||
hasMany,
|
||||
@@ -56,6 +57,9 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
validate = relationship,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
const config = useConfig()
|
||||
|
||||
const {
|
||||
@@ -391,6 +395,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}, [])
|
||||
|
||||
const valueToRender = findOptionsByValue({ options, value })
|
||||
|
||||
if (!Array.isArray(valueToRender) && valueToRender?.value === 'null') valueToRender.value = null
|
||||
|
||||
return (
|
||||
@@ -411,8 +416,8 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={pathOrName} label={label} required={required} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={pathOrName} label={label} required={required} />
|
||||
<GetFilterOptions
|
||||
{...{
|
||||
filterOptions,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
|
||||
import type { PayloadRequest } from '../../../../../express/types'
|
||||
import type { RichTextField, Validate } from '../../../../../fields/config/types'
|
||||
import type { CellComponentProps } from '../../../views/collections/List/Cell/types'
|
||||
@@ -29,6 +31,13 @@ export type RichTextAdapter<
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => Promise<void> | null
|
||||
|
||||
outputSchema: ({
|
||||
field,
|
||||
isRequired,
|
||||
}: {
|
||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||
isRequired: boolean
|
||||
}) => JSONSchema4
|
||||
populationPromise?: (data: {
|
||||
currentDepth?: number
|
||||
depth: number
|
||||
|
||||
@@ -7,9 +7,9 @@ import type { Description } from '../../FieldDescription/types'
|
||||
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import ReactSelect from '../../../elements/ReactSelect'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
@@ -29,6 +29,8 @@ export type SelectInputProps = Omit<SelectField, 'options' | 'type' | 'value'> &
|
||||
style?: React.CSSProperties
|
||||
value?: string | string[]
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
}
|
||||
|
||||
const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
@@ -50,10 +52,15 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
let valueToRender = defaultValue
|
||||
|
||||
if (hasMany && Array.isArray(value)) {
|
||||
@@ -89,8 +96,8 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<ReactSelect
|
||||
disabled={readOnly}
|
||||
isClearable={isClearable}
|
||||
|
||||
@@ -32,6 +32,7 @@ const Select: React.FC<Props> = (props) => {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
hasMany,
|
||||
label,
|
||||
@@ -103,6 +104,8 @@ const Select: React.FC<Props> = (props) => {
|
||||
style={style}
|
||||
value={value as string | string[]}
|
||||
width={width}
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -166,7 +166,9 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
className={[
|
||||
`${baseClass}__tab`,
|
||||
activeTabConfig.label &&
|
||||
`${baseClass}__tab-${toKebabCase(getTranslation(activeTabConfig.label, i18n))}`,
|
||||
`${baseClass}__tabConfigLabel-${toKebabCase(
|
||||
getTranslation(activeTabConfig.label, i18n),
|
||||
)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
|
||||
@@ -7,13 +7,17 @@ import type { TextField } from '../../../../../fields/config/types'
|
||||
import type { Description } from '../../FieldDescription/types'
|
||||
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import DefaultLabel from '../../Label'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
export type TextInputProps = Omit<TextField, 'type'> & {
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
className?: string
|
||||
description?: Description
|
||||
errorMessage?: string
|
||||
@@ -33,6 +37,10 @@ export type TextInputProps = Omit<TextField, 'type'> & {
|
||||
|
||||
const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
const {
|
||||
Error,
|
||||
Label,
|
||||
afterInput,
|
||||
beforeInput,
|
||||
className,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -53,6 +61,9 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only']
|
||||
@@ -63,20 +74,24 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<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 || ''}
|
||||
/>
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<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 || ''}
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
description={description}
|
||||
|
||||
@@ -13,7 +13,17 @@ import TextInput from './Input'
|
||||
const Text: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
admin: { className, condition, description, placeholder, readOnly, rtl, style, width } = {},
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
readOnly,
|
||||
rtl,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
inputRef,
|
||||
label,
|
||||
localized,
|
||||
@@ -50,6 +60,10 @@ const Text: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
afterInput={afterInput}
|
||||
beforeInput={beforeInput}
|
||||
className={className}
|
||||
description={description}
|
||||
errorMessage={errorMessage}
|
||||
|
||||
@@ -7,13 +7,17 @@ import type { TextareaField } from '../../../../../fields/config/types'
|
||||
import type { Description } from '../../FieldDescription/types'
|
||||
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import Error from '../../Error'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import './index.scss'
|
||||
import DefaultLabel from '../../Label'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
className?: string
|
||||
description?: Description
|
||||
errorMessage?: string
|
||||
@@ -32,6 +36,10 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
|
||||
|
||||
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
const {
|
||||
Error,
|
||||
Label,
|
||||
afterInput,
|
||||
beforeInput,
|
||||
className,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -51,6 +59,9 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
const LabelComp = Label || DefaultLabel
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
@@ -67,11 +78,12 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
|
||||
<div className="textarea-inner">
|
||||
<div className="textarea-clone" data-value={value || placeholder || ''} />
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<textarea
|
||||
className="textarea-element"
|
||||
data-rtl={rtl}
|
||||
@@ -83,6 +95,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
rows={rows}
|
||||
value={value || ''}
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</label>
|
||||
<FieldDescription description={description} value={value} />
|
||||
|
||||
@@ -18,6 +18,7 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -64,6 +65,10 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<TextareaInput
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
afterInput={afterInput}
|
||||
beforeInput={beforeInput}
|
||||
className={className}
|
||||
description={description}
|
||||
errorMessage={errorMessage}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user