Compare commits
74 Commits
live-previ
...
fix/mongod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43cdb45620 | ||
|
|
6364afb1dd | ||
|
|
56a4692662 | ||
|
|
ef6b8e4235 | ||
|
|
5f5290341a | ||
|
|
62403584ad | ||
|
|
19fcfc27af | ||
|
|
dcf14f5f71 | ||
|
|
3a784a06cc | ||
|
|
6eeae9d53b | ||
|
|
6044f810bd | ||
|
|
e68ca9363f | ||
|
|
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 |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -12,8 +12,8 @@
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Change to the [templates](../templates/) directory (does not affect core functionality)
|
||||
- [ ] Change to the [examples](../examples/) directory (does not affect core functionality)
|
||||
- [ ] Change to the [templates](https://github.com/payloadcms/payload/tree/main/templates) directory (does not affect core functionality)
|
||||
- [ ] Change to the [examples](https://github.com/payloadcms/payload/tree/main/examples) directory (does not affect core functionality)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## Checklist:
|
||||
|
||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,3 +1,45 @@
|
||||
## [2.2.1](https://github.com/payloadcms/payload/compare/v2.2.0...v2.2.1) (2023-11-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make outputSchema optional on richtext config ([#4230](https://github.com/payloadcms/payload/issues/4230)) ([3a784a0](https://github.com/payloadcms/payload/commit/3a784a06cc6c42c96b8d6cf023d942e6661be7b5))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
20
README.md
20
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>
|
||||
|
||||
@@ -432,14 +432,14 @@ 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).
|
||||
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) |
|
||||
| **`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
|
||||
|
||||
@@ -530,7 +530,7 @@ const CustomLabel: React.FC<Props> = (props) => {
|
||||
{getTranslation(label, i18n)}
|
||||
{required && <span className="required">*</span>}
|
||||
</span>);
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -564,7 +564,7 @@ const CustomError: React.FC<Props> = (props) => {
|
||||
}
|
||||
```
|
||||
|
||||
## AfterInput and BeforeInput
|
||||
## 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.
|
||||
|
||||
@@ -583,9 +583,7 @@ const fieldField: Field = {
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
AfterInput: [
|
||||
<ClearButton />
|
||||
]
|
||||
afterInput: [ClearButton]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,20 +172,34 @@ export default buildConfig({
|
||||
collections: [Subscriptions],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
vite: (config) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
// highlight-start
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
// remember, vite aliases are exact-match only
|
||||
'./hooks/createStripeSubscription': mockModulePath,
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
vite: (incomingViteConfig) => {
|
||||
const existingAliases = incomingViteConfig?.resolve?.alias || {};
|
||||
let aliasArray: { find: string | RegExp; replacement: string; }[] = [];
|
||||
|
||||
// Pass the existing Vite aliases
|
||||
if (Array.isArray(existingAliases)) {
|
||||
aliasArray = existingAliases;
|
||||
} else {
|
||||
aliasArray = Object.values(existingAliases);
|
||||
}
|
||||
|
||||
|
||||
// highlight-start
|
||||
// Add your own aliases using the find and replacement keys
|
||||
// remember, vite aliases are exact-match only
|
||||
aliasArray.push({
|
||||
find: '../server-only-module',
|
||||
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
|
||||
});
|
||||
// highlight-end
|
||||
|
||||
return {
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: aliasArray,
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -764,7 +764,7 @@ Returns methods to set and get user preferences. More info can be found [here](h
|
||||
Returns methods to manipulate table columns
|
||||
|
||||
```tsx
|
||||
import { useTableColumns } from 'payload/components/utilities'
|
||||
import { useTableColumns } from 'payload/components/hooks'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
|
||||
@@ -60,17 +60,33 @@ export const buildConfig({
|
||||
collections: [],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
vite: (incomingViteConfig) => ({
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: {
|
||||
...(incomingViteConfig?.resolve?.alias || {}),
|
||||
'../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
|
||||
'../../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
|
||||
}
|
||||
vite: (incomingViteConfig) => {
|
||||
const existingAliases = incomingViteConfig?.resolve?.alias || {};
|
||||
let aliasArray: { find: string | RegExp; replacement: string; }[] = [];
|
||||
|
||||
// Pass the existing Vite aliases
|
||||
if (Array.isArray(existingAliases)) {
|
||||
aliasArray = existingAliases;
|
||||
} else {
|
||||
aliasArray = Object.values(existingAliases);
|
||||
}
|
||||
})
|
||||
|
||||
// Add your own aliases using the find and replacement keys
|
||||
aliasArray.push({
|
||||
find: '../server-only-module',
|
||||
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
|
||||
find: '../../server-only-module',
|
||||
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
|
||||
});
|
||||
|
||||
return {
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: aliasArray,
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -90,7 +106,8 @@ That plugin should create an alias to support Vite as follows:
|
||||
```ts
|
||||
{
|
||||
// aliases go here
|
||||
'payload-plugin-cool': path.resolve(__dirname, './my-admin-plugin.js')
|
||||
find: 'payload-plugin-cool',
|
||||
replacement: path.resolve(__dirname, './my-admin-plugin.js')
|
||||
}
|
||||
|
||||
```
|
||||
@@ -108,22 +125,36 @@ export const buildConfig({
|
||||
collections: [],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
vite: (incomingViteConfig) => ({
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: {
|
||||
...(incomingViteConfig?.resolve?.alias || {}),
|
||||
// custom aliases go here
|
||||
'../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
|
||||
}
|
||||
vite: (incomingViteConfig) => {
|
||||
const existingAliases = incomingViteConfig?.resolve?.alias || {};
|
||||
let aliasArray: { find: string | RegExp; replacement: string; }[] = [];
|
||||
|
||||
// Pass the existing Vite aliases
|
||||
if (Array.isArray(existingAliases)) {
|
||||
aliasArray = existingAliases;
|
||||
} else {
|
||||
aliasArray = Object.values(existingAliases);
|
||||
}
|
||||
})
|
||||
|
||||
// Add your own aliases using the find and replacement keys
|
||||
aliasArray.push({
|
||||
find: '../server-only-module',
|
||||
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
|
||||
});
|
||||
|
||||
return {
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: aliasArray,
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Learn more about [aliasing server-only modules](http://localhost:3000/docs/admin/excluding-server-code#aliasing-server-only-modules).
|
||||
Learn more about [aliasing server-only modules](https://payloadcms.com/docs/admin/excluding-server-code#aliasing-server-only-modules).
|
||||
|
||||
Even though there is a new property for Vite configs specifically, we have implemented some "compatibility" between Webpack and Vite out-of-the-box.
|
||||
|
||||
|
||||
@@ -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'`. |
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"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",
|
||||
|
||||
@@ -69,6 +69,8 @@ export const getViteConfig = async (payloadConfig: SanitizedConfig): Promise<Inl
|
||||
Object.entries(process.env).forEach(([key, val]) => {
|
||||
if (key.indexOf('PAYLOAD_PUBLIC_') === 0) {
|
||||
define[`process.env.${key}`] = `'${val}'`
|
||||
} else {
|
||||
define[`process.env.${key}`] = `''`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import type { RollbackTransaction } from 'payload/database'
|
||||
|
||||
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
|
||||
id = '',
|
||||
) {
|
||||
export const rollbackTransaction: RollbackTransaction = 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]) {
|
||||
return
|
||||
}
|
||||
|
||||
// when session exists but is not inTransaction something unexpected is happening to the session
|
||||
// when session exists but inTransaction is false, it is no longer used and can be deleted
|
||||
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()
|
||||
try {
|
||||
// null coalesce needed when rollback is called multiple times with the same id synchronously
|
||||
this.sessions?.[id].abortTransaction().then(() => {
|
||||
// not supported by DocumentDB
|
||||
this.sessions?.[id].endSession()
|
||||
})
|
||||
} catch (e) {
|
||||
// no action needed
|
||||
}
|
||||
delete this.sessions[id]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.1",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.section-title {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
|
||||
@@ -10,10 +10,10 @@ import './index.scss'
|
||||
const baseClass = 'checkbox-input'
|
||||
|
||||
type CheckboxInputProps = {
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
'aria-label'?: string
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
checked?: boolean
|
||||
className?: string
|
||||
id?: string
|
||||
@@ -30,10 +30,10 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
AfterInput,
|
||||
BeforeInput,
|
||||
Label,
|
||||
afterInput,
|
||||
'aria-label': ariaLabel,
|
||||
beforeInput,
|
||||
checked,
|
||||
className,
|
||||
inputRef,
|
||||
@@ -58,7 +58,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__input`}>
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
aria-label={ariaLabel}
|
||||
defaultChecked={Boolean(checked)}
|
||||
@@ -69,7 +69,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
ref={inputRef}
|
||||
type="checkbox"
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
<span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
|
||||
{!partialChecked && <Check />}
|
||||
{partialChecked && <Line />}
|
||||
|
||||
@@ -20,12 +20,12 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
disableFormData,
|
||||
label,
|
||||
@@ -85,15 +85,15 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
<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)}
|
||||
name={path}
|
||||
onToggle={onToggle}
|
||||
readOnly={readOnly}
|
||||
Label={Label}
|
||||
BeforeInput={BeforeInput}
|
||||
AfterInput={AfterInput}
|
||||
required={required}
|
||||
/>
|
||||
<FieldDescription description={description} value={value} />
|
||||
|
||||
@@ -9,8 +9,8 @@ import FieldDescription from '../../FieldDescription'
|
||||
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,
|
||||
@@ -31,7 +32,6 @@ const Code: React.FC<Props> = (props) => {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
|
||||
@@ -17,10 +17,10 @@ const baseClass = 'date-time-field'
|
||||
export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
|
||||
className?: string
|
||||
components: {
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
datePickerProps?: DateField['admin']['date']
|
||||
description?: Description
|
||||
@@ -39,7 +39,7 @@ export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
|
||||
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
const {
|
||||
className,
|
||||
components: { AfterInput, BeforeInput, Error, Label } = {},
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
datePickerProps,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -81,7 +81,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
</div>
|
||||
<LabelComp htmlFor={path} label={label} required={required} />
|
||||
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<DatePicker
|
||||
{...datePickerProps}
|
||||
onChange={onChange}
|
||||
@@ -89,7 +89,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
readOnly={readOnly}
|
||||
value={value}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ import FieldDescription from '../../FieldDescription'
|
||||
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,13 +19,13 @@ const Email: React.FC<Props> = (props) => {
|
||||
admin: {
|
||||
autoComplete,
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
@@ -68,7 +68,7 @@ const Email: React.FC<Props> = (props) => {
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
autoComplete={autoComplete}
|
||||
disabled={Boolean(readOnly)}
|
||||
@@ -79,7 +79,7 @@ const Email: React.FC<Props> = (props) => {
|
||||
type="email"
|
||||
value={(value as string) || ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
|
||||
@@ -13,14 +13,15 @@ import FieldDescription from '../../FieldDescription'
|
||||
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,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -28,7 +29,6 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
step,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
hasMany,
|
||||
label,
|
||||
@@ -162,7 +162,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
/>
|
||||
) : (
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-${path.replace(/\./g, '__')}`}
|
||||
@@ -178,7 +178,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
type="number"
|
||||
value={typeof value === 'number' ? value : ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import FieldDescription from '../../FieldDescription'
|
||||
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'
|
||||
|
||||
@@ -20,6 +20,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -27,7 +28,6 @@ const PointField: React.FC<Props> = (props) => {
|
||||
step,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
@@ -98,7 +98,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
required={required}
|
||||
/>
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-longitude-${path.replace(/\./g, '__')}`}
|
||||
@@ -109,7 +109,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
type="number"
|
||||
value={value && typeof value[0] === 'number' ? value[0] : ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
@@ -119,7 +119,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
required={required}
|
||||
/>
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-latitude-${path.replace(/\./g, '__')}`}
|
||||
@@ -130,7 +130,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
type="number"
|
||||
value={value && typeof value[1] === 'number' ? value[1] : ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(' ')}
|
||||
|
||||
@@ -14,6 +14,10 @@ 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
|
||||
@@ -29,14 +33,14 @@ export type TextInputProps = Omit<TextField, 'type'> & {
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
}
|
||||
|
||||
const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
const {
|
||||
Error,
|
||||
Label,
|
||||
afterInput,
|
||||
beforeInput,
|
||||
className,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -53,10 +57,6 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
BeforeInput,
|
||||
AfterInput,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -77,7 +77,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
data-rtl={rtl}
|
||||
disabled={readOnly}
|
||||
@@ -90,7 +90,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
type="text"
|
||||
value={value || ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
|
||||
@@ -15,6 +15,7 @@ const Text: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -22,7 +23,6 @@ const Text: React.FC<Props> = (props) => {
|
||||
rtl,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
inputRef,
|
||||
label,
|
||||
@@ -60,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}
|
||||
@@ -78,10 +82,6 @@ const Text: React.FC<Props> = (props) => {
|
||||
style={style}
|
||||
value={value}
|
||||
width={width}
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
BeforeInput={BeforeInput}
|
||||
AfterInput={AfterInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,14 @@ import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import DefaultLabel from '../../Label'
|
||||
import './index.scss'
|
||||
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
|
||||
@@ -28,14 +32,14 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
}
|
||||
|
||||
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
const {
|
||||
Error,
|
||||
Label,
|
||||
afterInput,
|
||||
beforeInput,
|
||||
className,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -51,10 +55,6 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
BeforeInput,
|
||||
AfterInput,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -83,7 +83,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
|
||||
<div className="textarea-inner">
|
||||
<div className="textarea-clone" data-value={value || placeholder || ''} />
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<textarea
|
||||
className="textarea-element"
|
||||
data-rtl={rtl}
|
||||
@@ -95,7 +95,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
rows={rows}
|
||||
value={value || ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{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,
|
||||
@@ -26,7 +27,6 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
rtl,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
label,
|
||||
localized,
|
||||
@@ -65,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}
|
||||
@@ -83,10 +87,6 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
style={style}
|
||||
value={value as string}
|
||||
width={width}
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
BeforeInput={BeforeInput}
|
||||
AfterInput={AfterInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import qs from 'qs'
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
@@ -10,6 +11,7 @@ import type { AuthContext } from './types'
|
||||
import { requests } from '../../../api'
|
||||
import useDebounce from '../../../hooks/useDebounce'
|
||||
import { useConfig } from '../Config'
|
||||
import { useLocale } from '../Locale'
|
||||
|
||||
const Context = createContext({} as AuthContext)
|
||||
|
||||
@@ -21,6 +23,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const [tokenExpiration, setTokenExpiration] = useState<number>()
|
||||
const { pathname } = useLocation()
|
||||
const { push } = useHistory()
|
||||
const { code } = useLocale()
|
||||
|
||||
const config = useConfig()
|
||||
|
||||
@@ -144,8 +147,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}, [serverURL, api, userSlug, revokeTokenAndExpire])
|
||||
|
||||
const refreshPermissions = useCallback(async () => {
|
||||
const params = {
|
||||
locale: code,
|
||||
}
|
||||
try {
|
||||
const request = await requests.get(`${serverURL}${api}/access`, {
|
||||
const request = await requests.get(`${serverURL}${api}/access?${qs.stringify(params)}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
@@ -160,7 +166,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
} catch (e) {
|
||||
toast.error(`Refreshing permissions failed: ${e.message}`)
|
||||
}
|
||||
}, [serverURL, api, i18n])
|
||||
}, [serverURL, api, i18n, code])
|
||||
|
||||
const fetchFullUser = React.useCallback(async () => {
|
||||
try {
|
||||
|
||||
@@ -199,6 +199,9 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
|
||||
const getDocPermissions = React.useCallback(async () => {
|
||||
let docAccessURL: string
|
||||
const params = {
|
||||
locale: code || undefined,
|
||||
}
|
||||
if (pluralType === 'globals') {
|
||||
docAccessURL = `/globals/${slug}/access`
|
||||
} else if (pluralType === 'collections' && id) {
|
||||
@@ -206,7 +209,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
if (docAccessURL) {
|
||||
const res = await fetch(`${serverURL}${api}${docAccessURL}`, {
|
||||
const res = await fetch(`${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
@@ -219,7 +222,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
// (i.e. create has no id)
|
||||
setDocPermissions(permissions[pluralType][slug])
|
||||
}
|
||||
}, [serverURL, api, pluralType, slug, id, permissions, i18n.language])
|
||||
}, [serverURL, api, pluralType, slug, id, permissions, i18n.language, code])
|
||||
|
||||
const getDocPreferences = useCallback(async () => {
|
||||
return getPreference<DocumentPreferences>(preferencesKey)
|
||||
|
||||
@@ -51,4 +51,9 @@
|
||||
justify-content: center;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.popup-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@ import React from 'react'
|
||||
|
||||
import type { EditViewProps } from '../../../types'
|
||||
|
||||
import { X } from '../../../..'
|
||||
import { Chevron, Popup, X } from '../../../..'
|
||||
import * as PopupList from '../../../../elements/Popup/PopupButtonList'
|
||||
import { ExternalLinkIcon } from '../../../../graphics/ExternalLink'
|
||||
import { useLivePreviewContext } from '../../Context/context'
|
||||
import { PreviewFrameSizeInput } from '../SizeInput'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'live-preview-toolbar-controls'
|
||||
const zoomOptions = [50, 75, 100, 125, 150, 200]
|
||||
const customOption = {
|
||||
label: 'Custom', // TODO: Add i18n to this string
|
||||
value: 'custom',
|
||||
}
|
||||
|
||||
export const ToolbarControls: React.FC<EditViewProps> = () => {
|
||||
const { breakpoint, breakpoints, setBreakpoint, setPreviewWindowType, setZoom, url, zoom } =
|
||||
@@ -17,23 +23,51 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{breakpoints?.length > 0 && (
|
||||
<select
|
||||
<Popup
|
||||
className={`${baseClass}__breakpoint`}
|
||||
name="live-preview-breakpoint"
|
||||
onChange={(e) => setBreakpoint(e.target.value)}
|
||||
value={breakpoint}
|
||||
>
|
||||
{breakpoints.map((bp) => (
|
||||
<option key={bp.name} value={bp.name}>
|
||||
{bp.label}
|
||||
</option>
|
||||
))}
|
||||
{breakpoint === 'custom' && (
|
||||
// Dynamically add this option so that it only appears when the width and height inputs are explicitly changed
|
||||
// TODO: Translate this string
|
||||
<option value="custom">Custom</option>
|
||||
button={
|
||||
<>
|
||||
<span>
|
||||
{breakpoints.find((bp) => bp.name == breakpoint)?.label ?? customOption.label}
|
||||
</span>
|
||||
|
||||
<Chevron className={`${baseClass}__chevron`} />
|
||||
</>
|
||||
}
|
||||
render={({ close }) => (
|
||||
<PopupList.ButtonGroup>
|
||||
<React.Fragment>
|
||||
{breakpoints.map((bp) => (
|
||||
<PopupList.Button
|
||||
key={bp.name}
|
||||
active={bp.name == breakpoint}
|
||||
onClick={() => {
|
||||
setBreakpoint(bp.name)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{bp.label}
|
||||
</PopupList.Button>
|
||||
))}
|
||||
{/* Dynamically add this option so that it only appears when the width and height inputs are explicitly changed */}
|
||||
{breakpoint === 'custom' && (
|
||||
<PopupList.Button
|
||||
active={breakpoint == customOption.value}
|
||||
onClick={() => {
|
||||
setBreakpoint(customOption.value)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{customOption.label}
|
||||
</PopupList.Button>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</PopupList.ButtonGroup>
|
||||
)}
|
||||
</select>
|
||||
showScrollbar
|
||||
verticalAlign="bottom"
|
||||
horizontalAlign="right"
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__device-size`}>
|
||||
<PreviewFrameSizeInput axis="x" />
|
||||
@@ -42,18 +76,37 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
|
||||
</span>
|
||||
<PreviewFrameSizeInput axis="y" />
|
||||
</div>
|
||||
<select
|
||||
<Popup
|
||||
className={`${baseClass}__zoom`}
|
||||
onChange={(e) => setZoom(Number(e.target.value) / 100)}
|
||||
value={zoom * 100}
|
||||
>
|
||||
<option value={50}>50%</option>
|
||||
<option value={75}>75%</option>
|
||||
<option value={100}>100%</option>
|
||||
<option value={125}>125%</option>
|
||||
<option value={150}>150%</option>
|
||||
<option value={200}>200%</option>
|
||||
</select>
|
||||
button={
|
||||
<>
|
||||
<span>{zoom * 100}%</span>
|
||||
|
||||
<Chevron className={`${baseClass}__chevron`} />
|
||||
</>
|
||||
}
|
||||
render={({ close }) => (
|
||||
<PopupList.ButtonGroup>
|
||||
<React.Fragment>
|
||||
{zoomOptions.map((zoomValue) => (
|
||||
<PopupList.Button
|
||||
key={zoomValue}
|
||||
active={zoom * 100 == zoomValue}
|
||||
onClick={() => {
|
||||
setZoom(zoomValue / 100)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{zoomValue}%
|
||||
</PopupList.Button>
|
||||
))}
|
||||
</React.Fragment>
|
||||
</PopupList.ButtonGroup>
|
||||
)}
|
||||
showScrollbar
|
||||
verticalAlign="bottom"
|
||||
horizontalAlign="right"
|
||||
/>
|
||||
<a
|
||||
className={`${baseClass}__external`}
|
||||
href={url}
|
||||
|
||||
@@ -2,9 +2,10 @@ import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import type { CodeField } from '../../../../../../fields/config/types'
|
||||
import type { CellComponentProps, Props } from './types'
|
||||
|
||||
import { CodeField, fieldAffectsData } from '../../../../../../fields/config/types'
|
||||
import { fieldAffectsData } from '../../../../../../fields/config/types'
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation'
|
||||
import { useConfig } from '../../../../utilities/Config'
|
||||
import RenderCustomComponent from '../../../../utilities/RenderCustomComponent'
|
||||
@@ -60,8 +61,8 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
collection={collection}
|
||||
data={`ID: ${cellData}`}
|
||||
field={field as CodeField}
|
||||
rowData={rowData}
|
||||
nowrap
|
||||
rowData={rowData}
|
||||
/>
|
||||
</WrapElement>
|
||||
)
|
||||
|
||||
@@ -43,6 +43,15 @@
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
[class^="cell"] > p, [class^="cell"] > span, [class^="cell"] > a {
|
||||
line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
#heading-_select,
|
||||
.cell-_select {
|
||||
min-width: unset;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Payload } from '../../../payload'
|
||||
|
||||
import formatName from '../../../graphql/utilities/formatName'
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import access from '../../operations/access'
|
||||
|
||||
const formatConfigNames = (results, configs) => {
|
||||
@@ -19,7 +19,7 @@ const formatConfigNames = (results, configs) => {
|
||||
function accessResolver(payload: Payload) {
|
||||
async function resolver(_, args, context) {
|
||||
const options = {
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const accessResults = await access(options)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import forgotPassword from '../../operations/forgotPassword'
|
||||
|
||||
function forgotPasswordResolver(collection: Collection): any {
|
||||
@@ -12,7 +12,7 @@ function forgotPasswordResolver(collection: Collection): any {
|
||||
},
|
||||
disableEmail: args.disableEmail,
|
||||
expiration: args.expiration,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
await forgotPassword(options)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import init from '../../operations/init'
|
||||
|
||||
function initResolver(collection: string) {
|
||||
async function resolver(_, args, context) {
|
||||
const options = {
|
||||
collection,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
return init(options)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import login from '../../operations/login'
|
||||
|
||||
function loginResolver(collection: Collection) {
|
||||
@@ -12,7 +12,7 @@ function loginResolver(collection: Collection) {
|
||||
password: args.password,
|
||||
},
|
||||
depth: 0,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
res: context.res,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import logout from '../../operations/logout'
|
||||
|
||||
function logoutResolver(collection: Collection): any {
|
||||
async function resolver(_, args, context) {
|
||||
const options = {
|
||||
collection,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
res: context.res,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import me from '../../operations/me'
|
||||
|
||||
function meResolver(collection: Collection): any {
|
||||
@@ -8,7 +8,7 @@ function meResolver(collection: Collection): any {
|
||||
const options = {
|
||||
collection,
|
||||
depth: 0,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
return me(options)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import getExtractJWT from '../../getExtractJWT'
|
||||
import refresh from '../../operations/refresh'
|
||||
|
||||
@@ -18,7 +18,7 @@ function refreshResolver(collection: Collection) {
|
||||
const options = {
|
||||
collection,
|
||||
depth: 0,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
res: context.res,
|
||||
token,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import resetPassword from '../../operations/resetPassword'
|
||||
|
||||
function resetPasswordResolver(collection: Collection) {
|
||||
@@ -14,7 +14,7 @@ function resetPasswordResolver(collection: Collection) {
|
||||
collection,
|
||||
data: args,
|
||||
depth: 0,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
res: context.res,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import unlock from '../../operations/unlock'
|
||||
|
||||
function unlockResolver(collection: Collection) {
|
||||
@@ -8,7 +8,7 @@ function unlockResolver(collection: Collection) {
|
||||
const options = {
|
||||
collection,
|
||||
data: { email: args.email },
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await unlock(options)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import verifyEmail from '../../operations/verifyEmail'
|
||||
|
||||
function verifyEmailResolver(collection: Collection) {
|
||||
@@ -12,7 +12,7 @@ function verifyEmailResolver(collection: Collection) {
|
||||
const options = {
|
||||
api: 'GraphQL',
|
||||
collection,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
res: context.res,
|
||||
token: args.token,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { PayloadRequest } from '../../express/types'
|
||||
import type { AllOperations } from '../../types'
|
||||
import type { Permissions } from '../types'
|
||||
|
||||
import { commitTransaction } from '../../utilities/commitTransaction'
|
||||
import { getEntityPolicies } from '../../utilities/getEntityPolicies'
|
||||
import { initTransaction } from '../../utilities/initTransaction'
|
||||
import { killTransaction } from '../../utilities/killTransaction'
|
||||
import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/events/adminInit'
|
||||
|
||||
const allOperations: AllOperations[] = ['create', 'read', 'update', 'delete']
|
||||
@@ -38,57 +41,64 @@ async function accessOperation(args: Arguments): Promise<Permissions> {
|
||||
results.canAccessAdmin = false
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
config.collections.map(async (collection) => {
|
||||
const collectionOperations = [...allOperations]
|
||||
try {
|
||||
const shouldCommit = await initTransaction(req)
|
||||
await Promise.all(
|
||||
config.collections.map(async (collection) => {
|
||||
const collectionOperations = [...allOperations]
|
||||
|
||||
if (
|
||||
collection.auth &&
|
||||
typeof collection.auth.maxLoginAttempts !== 'undefined' &&
|
||||
collection.auth.maxLoginAttempts !== 0
|
||||
) {
|
||||
collectionOperations.push('unlock')
|
||||
}
|
||||
if (
|
||||
collection.auth &&
|
||||
typeof collection.auth.maxLoginAttempts !== 'undefined' &&
|
||||
collection.auth.maxLoginAttempts !== 0
|
||||
) {
|
||||
collectionOperations.push('unlock')
|
||||
}
|
||||
|
||||
if (collection.versions) {
|
||||
collectionOperations.push('readVersions')
|
||||
}
|
||||
if (collection.versions) {
|
||||
collectionOperations.push('readVersions')
|
||||
}
|
||||
|
||||
const collectionPolicy = await getEntityPolicies({
|
||||
entity: collection,
|
||||
operations: collectionOperations,
|
||||
req,
|
||||
type: 'collection',
|
||||
})
|
||||
results.collections = {
|
||||
...results.collections,
|
||||
[collection.slug]: collectionPolicy,
|
||||
}
|
||||
}),
|
||||
)
|
||||
const collectionPolicy = await getEntityPolicies({
|
||||
entity: collection,
|
||||
operations: collectionOperations,
|
||||
req,
|
||||
type: 'collection',
|
||||
})
|
||||
results.collections = {
|
||||
...results.collections,
|
||||
[collection.slug]: collectionPolicy,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
config.globals.map(async (global) => {
|
||||
const globalOperations: AllOperations[] = ['read', 'update']
|
||||
await Promise.all(
|
||||
config.globals.map(async (global) => {
|
||||
const globalOperations: AllOperations[] = ['read', 'update']
|
||||
|
||||
if (global.versions) {
|
||||
globalOperations.push('readVersions')
|
||||
}
|
||||
if (global.versions) {
|
||||
globalOperations.push('readVersions')
|
||||
}
|
||||
|
||||
const globalPolicy = await getEntityPolicies({
|
||||
entity: global,
|
||||
operations: globalOperations,
|
||||
req,
|
||||
type: 'global',
|
||||
})
|
||||
results.globals = {
|
||||
...results.globals,
|
||||
[global.slug]: globalPolicy,
|
||||
}
|
||||
}),
|
||||
)
|
||||
const globalPolicy = await getEntityPolicies({
|
||||
entity: global,
|
||||
operations: globalOperations,
|
||||
req,
|
||||
type: 'global',
|
||||
})
|
||||
results.globals = {
|
||||
...results.globals,
|
||||
[global.slug]: globalPolicy,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return results
|
||||
if (shouldCommit) await commitTransaction(req)
|
||||
return results
|
||||
} catch (e: unknown) {
|
||||
await killTransaction(req)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export default accessOperation
|
||||
|
||||
@@ -129,6 +129,16 @@ const buildEndpoints = (collection: SanitizedCollectionConfig): Endpoint[] => {
|
||||
method: 'get',
|
||||
path: '/access/:id',
|
||||
},
|
||||
{
|
||||
handler: docAccessRequestHandler,
|
||||
method: 'post',
|
||||
path: '/access/:id',
|
||||
},
|
||||
{
|
||||
handler: docAccessRequestHandler,
|
||||
method: 'post',
|
||||
path: '/access',
|
||||
},
|
||||
{
|
||||
handler: deprecatedUpdate,
|
||||
method: 'put',
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { GeneratedTypes } from '../../../'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Collection } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import create from '../../operations/create'
|
||||
|
||||
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
|
||||
@@ -37,7 +38,7 @@ export default function createResolver<TSlug extends keyof GeneratedTypes['colle
|
||||
data: args.data,
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await create(options)
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { GeneratedTypes } from '../../../'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Collection } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import deleteByID from '../../operations/deleteByID'
|
||||
|
||||
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
|
||||
@@ -30,7 +31,7 @@ export default function getDeleteResolver<TSlug extends keyof GeneratedTypes['co
|
||||
id: args.id,
|
||||
collection,
|
||||
depth: 0,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await deleteByID(options)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CollectionPermission, GlobalPermission } from '../../../auth'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import { docAccess } from '../../operations/docAccess'
|
||||
|
||||
export type Resolver = (
|
||||
@@ -18,7 +19,7 @@ export function docAccessResolver(): Resolver {
|
||||
async function resolver(_, args, context) {
|
||||
return docAccess({
|
||||
id: args.id,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Where } from '../../../types'
|
||||
import type { Collection } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import find from '../../operations/find'
|
||||
|
||||
export type Resolver = (
|
||||
@@ -36,7 +37,7 @@ export default function findResolver(collection: Collection): Resolver {
|
||||
draft: args.draft,
|
||||
limit: args.limit,
|
||||
page: args.page,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
sort: args.sort,
|
||||
where: args.where,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { GeneratedTypes } from '../../../'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Collection } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import findByID from '../../operations/findByID'
|
||||
|
||||
export type Resolver<T> = (
|
||||
@@ -31,7 +32,7 @@ export default function findByIDResolver<T extends keyof GeneratedTypes['collect
|
||||
collection,
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
req,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await findByID(options)
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { PayloadRequest } from '../../../express/types'
|
||||
import type { TypeWithVersion } from '../../../versions/types'
|
||||
import type { Collection, TypeWithID } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import findVersionByID from '../../operations/findVersionByID'
|
||||
|
||||
export type Resolver<T extends TypeWithID = any> = (
|
||||
@@ -31,7 +32,7 @@ export default function findVersionByIDResolver(collection: Collection): Resolve
|
||||
collection,
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await findVersionByID(options)
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Where } from '../../../types'
|
||||
import type { Collection } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import findVersions from '../../operations/findVersions'
|
||||
|
||||
export type Resolver = (
|
||||
@@ -35,7 +36,7 @@ export default function findVersionsResolver(collection: Collection): Resolver {
|
||||
depth: 0,
|
||||
limit: args.limit,
|
||||
page: args.page,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
sort: args.sort,
|
||||
where: args.where,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Response } from 'express'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Collection } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import restoreVersion from '../../operations/restoreVersion'
|
||||
|
||||
export type Resolver = (
|
||||
@@ -23,7 +24,7 @@ export default function restoreVersionResolver(collection: Collection): Resolver
|
||||
id: args.id,
|
||||
collection,
|
||||
depth: 0,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await restoreVersion(options)
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { GeneratedTypes } from '../../../'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Collection } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import updateByID from '../../operations/updateByID'
|
||||
|
||||
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
|
||||
@@ -36,7 +37,7 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['colle
|
||||
data: args.data,
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await updateByID<TSlug>(options)
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { CollectionPermission } from '../../auth'
|
||||
import type { PayloadRequest } from '../../express/types'
|
||||
import type { AllOperations } from '../../types'
|
||||
|
||||
import { commitTransaction } from '../../utilities/commitTransaction'
|
||||
import { getEntityPolicies } from '../../utilities/getEntityPolicies'
|
||||
import { initTransaction } from '../../utilities/initTransaction'
|
||||
import { killTransaction } from '../../utilities/killTransaction'
|
||||
|
||||
const allOperations: AllOperations[] = ['create', 'read', 'update', 'delete']
|
||||
|
||||
@@ -34,11 +37,22 @@ export async function docAccess(args: Arguments): Promise<CollectionPermission>
|
||||
collectionOperations.push('readVersions')
|
||||
}
|
||||
|
||||
return getEntityPolicies({
|
||||
id,
|
||||
entity: config,
|
||||
operations: collectionOperations,
|
||||
req,
|
||||
type: 'collection',
|
||||
})
|
||||
try {
|
||||
const shouldCommit = await initTransaction(req)
|
||||
|
||||
const result = await getEntityPolicies({
|
||||
id,
|
||||
entity: config,
|
||||
operations: collectionOperations,
|
||||
req,
|
||||
type: 'collection',
|
||||
})
|
||||
|
||||
if (shouldCommit) await commitTransaction(req)
|
||||
|
||||
return result
|
||||
} catch (e: unknown) {
|
||||
await killTransaction(req)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ export default joi.object({
|
||||
CellComponent: component.required(),
|
||||
FieldComponent: component.required(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
outputSchema: joi.func().optional(),
|
||||
populationPromise: joi.func().optional(),
|
||||
validate: joi.func().required(),
|
||||
})
|
||||
|
||||
@@ -151,7 +151,7 @@ export type BeginTransaction = (
|
||||
options?: Record<string, unknown>,
|
||||
) => Promise<null | number | string>
|
||||
|
||||
export type RollbackTransaction = (id: number | string) => Promise<void>
|
||||
export type RollbackTransaction = (id: number | string) => Promise<void> | void
|
||||
|
||||
export type CommitTransaction = (id: number | string) => Promise<void>
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ export { extractTranslations } from '../translations/extractTranslations'
|
||||
|
||||
export { i18nInit } from '../translations/init'
|
||||
export { combineMerge } from '../utilities/combineMerge'
|
||||
export { configToJSONSchema, entityToJSONSchema } from '../utilities/configToJSONSchema'
|
||||
export {
|
||||
configToJSONSchema,
|
||||
entityToJSONSchema,
|
||||
withNullableJSONSchemaType,
|
||||
} from '../utilities/configToJSONSchema'
|
||||
|
||||
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
|
||||
export { deepCopyObject } from '../utilities/deepCopyObject'
|
||||
|
||||
@@ -59,6 +59,10 @@ export declare type PayloadRequest<U = any> = Request & {
|
||||
* Identifier for the database transaction for interactions in a single, all-or-nothing operation.
|
||||
*/
|
||||
transactionID?: number | string
|
||||
/**
|
||||
* Used to ensure consistency when multiple operations try to create a transaction concurrently on the same request
|
||||
*/
|
||||
transactionIDPromise?: Promise<void>
|
||||
/** The signed in user */
|
||||
user: (U & User) | null
|
||||
}
|
||||
|
||||
@@ -71,16 +71,16 @@ export const text = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi
|
||||
.alternatives()
|
||||
.try(joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
|
||||
rtl: joi.boolean(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
maxLength: joi.number(),
|
||||
@@ -92,20 +92,20 @@ export const number = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
placeholder: joi.string(),
|
||||
step: joi.number(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi
|
||||
Label: componentSchema,
|
||||
afterInput: joi
|
||||
.array()
|
||||
.items(componentSchema)
|
||||
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
|
||||
AfterInput: joi
|
||||
beforeInput: joi
|
||||
.array()
|
||||
.items(componentSchema)
|
||||
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
step: joi.number(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.number(), joi.func()),
|
||||
hasMany: joi.boolean().default(false),
|
||||
@@ -119,15 +119,15 @@ export const number = baseField.keys({
|
||||
export const textarea = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
rows: joi.number(),
|
||||
rtl: joi.boolean(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
maxLength: joi.number(),
|
||||
@@ -139,13 +139,13 @@ export const email = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
placeholder: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
maxLength: joi.number(),
|
||||
@@ -156,12 +156,12 @@ export const email = baseField.keys({
|
||||
export const code = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
editorOptions: joi.object().unknown(), // Editor['options'] @monaco-editor/react
|
||||
language: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
type: joi.string().valid('code').required(),
|
||||
@@ -171,8 +171,8 @@ export const json = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.array(), joi.object()),
|
||||
@@ -182,12 +182,12 @@ export const json = baseField.keys({
|
||||
export const select = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
isClearable: joi.boolean().default(false),
|
||||
isSortable: joi.boolean().default(false),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi
|
||||
.alternatives()
|
||||
@@ -214,11 +214,11 @@ export const select = baseField.keys({
|
||||
export const radio = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
layout: joi.string().valid('vertical', 'horizontal'),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
layout: joi.string().valid('vertical', 'horizontal'),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
|
||||
options: joi
|
||||
@@ -318,8 +318,8 @@ export const upload = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
|
||||
@@ -333,10 +333,10 @@ export const checkbox = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.boolean(), joi.func()),
|
||||
@@ -347,10 +347,10 @@ export const point = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.array().items(joi.number()).max(2).min(2), joi.func()),
|
||||
@@ -361,11 +361,11 @@ export const relationship = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
allowCreate: joi.boolean().default(true),
|
||||
isSortable: joi.boolean().default(false),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
isSortable: joi.boolean().default(false),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.func()),
|
||||
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
|
||||
@@ -434,6 +434,7 @@ export const richText = baseField.keys({
|
||||
CellComponent: componentSchema.required(),
|
||||
FieldComponent: componentSchema.required(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
outputSchema: joi.func().optional(),
|
||||
populationPromise: joi.func().optional(),
|
||||
validate: joi.func().required(),
|
||||
})
|
||||
@@ -444,6 +445,12 @@ export const richText = baseField.keys({
|
||||
export const date = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
date: joi.object({
|
||||
displayFormat: joi.string(),
|
||||
maxDate: joi.date(),
|
||||
@@ -451,17 +458,12 @@ export const date = baseField.keys({
|
||||
minDate: joi.date(),
|
||||
minTime: joi.date(),
|
||||
monthsToShow: joi.number(),
|
||||
overrides: joi.object().unknown(),
|
||||
pickerAppearance: joi.string(),
|
||||
timeFormat: joi.string(),
|
||||
timeIntervals: joi.number(),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
type: joi.string().valid('date').required(),
|
||||
|
||||
@@ -4,8 +4,12 @@ import type { TFunction } from 'i18next'
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode)
|
||||
import type React from 'react'
|
||||
|
||||
import type { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types'
|
||||
import type { Props as ErrorProps } from '../../admin/components/forms/Error/types'
|
||||
import type { Description } from '../../admin/components/forms/FieldDescription/types'
|
||||
import type { Props as LabelProps } from '../../admin/components/forms/Label/types'
|
||||
import type { RowLabel } from '../../admin/components/forms/RowLabel/types'
|
||||
import type { RichTextAdapter } from '../../admin/components/forms/field-types/RichText/types'
|
||||
import type { User } from '../../auth'
|
||||
@@ -15,8 +19,6 @@ import type { PayloadRequest, RequestContext } from '../../express/types'
|
||||
import type { SanitizedGlobalConfig } from '../../globals/config/types'
|
||||
import type { Payload } from '../../payload'
|
||||
import type { Operation, Where } from '../../types'
|
||||
import type { Props as ErrorProps } from '../../admin/components/forms/Error/types'
|
||||
import type { Props as LabelProps } from '../../admin/components/forms/Label/types'
|
||||
|
||||
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
|
||||
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
|
||||
@@ -52,10 +54,23 @@ export type FieldHook<T extends TypeWithID = any, P = any, S = any> = (
|
||||
) => P | Promise<P>
|
||||
|
||||
export type FieldAccess<T extends TypeWithID = any, P = any, U = any> = (args: {
|
||||
/**
|
||||
* The incoming data used to `create` or `update` the document with. `data` is undefined during the `read` operation.
|
||||
*/
|
||||
data?: Partial<T>
|
||||
/**
|
||||
* The original data of the document before the `update` is applied. `doc` is undefined during the `create` operation.
|
||||
*/
|
||||
doc?: T
|
||||
/**
|
||||
* The `id` of the current document being read or updated. `id` is undefined during the `create` operation.
|
||||
*/
|
||||
id?: number | string
|
||||
/** The `Express` request object containing the currently authenticated `user` */
|
||||
req: PayloadRequest<U>
|
||||
/**
|
||||
* Immediately adjacent data to this field. For example, if this is a `group` field, then `siblingData` will be the other fields within the group.
|
||||
*/
|
||||
siblingData?: Partial<P>
|
||||
}) => Promise<boolean> | boolean
|
||||
|
||||
@@ -85,6 +100,10 @@ type Admin = {
|
||||
Field?: React.ComponentType<any>
|
||||
Filter?: React.ComponentType<any>
|
||||
}
|
||||
/**
|
||||
* You can programmatically show / hide fields based on what other fields are doing.
|
||||
* This is also run on the server, to determine if the field should be validated.
|
||||
*/
|
||||
condition?: Condition
|
||||
description?: Description
|
||||
disableBulkEdit?: boolean
|
||||
@@ -157,16 +176,16 @@ export type NumberField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
/** Set this property to a string that will be used for browser autocomplete. */
|
||||
autoComplete?: string
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
/** Set this property to define a placeholder string for the field. */
|
||||
placeholder?: Record<string, string> | string
|
||||
/** Set a value for the number field to increment / decrement using browser controls. */
|
||||
step?: number
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
}
|
||||
}
|
||||
/** Maximum value accepted. Used in the default `validation` function. */
|
||||
max?: number
|
||||
@@ -195,14 +214,14 @@ export type NumberField = FieldBase & {
|
||||
export type TextField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
autoComplete?: string
|
||||
placeholder?: Record<string, string> | string
|
||||
rtl?: boolean
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
placeholder?: Record<string, string> | string
|
||||
rtl?: boolean
|
||||
}
|
||||
maxLength?: number
|
||||
minLength?: number
|
||||
@@ -212,28 +231,28 @@ export type TextField = FieldBase & {
|
||||
export type EmailField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
autoComplete?: string
|
||||
placeholder?: Record<string, string> | string
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
placeholder?: Record<string, string> | string
|
||||
}
|
||||
type: 'email'
|
||||
}
|
||||
|
||||
export type TextareaField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
placeholder?: Record<string, string> | string
|
||||
rows?: number
|
||||
rtl?: boolean
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
placeholder?: Record<string, string> | string
|
||||
rows?: number
|
||||
rtl?: boolean
|
||||
}
|
||||
maxLength?: number
|
||||
minLength?: number
|
||||
@@ -241,27 +260,27 @@ export type TextareaField = FieldBase & {
|
||||
}
|
||||
|
||||
export type CheckboxField = FieldBase & {
|
||||
type: 'checkbox'
|
||||
admin?: Admin & {
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
}
|
||||
type: 'checkbox'
|
||||
}
|
||||
|
||||
export type DateField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
date?: ConditionalDateProps
|
||||
placeholder?: Record<string, string> | string
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
date?: ConditionalDateProps
|
||||
placeholder?: Record<string, string> | string
|
||||
}
|
||||
type: 'date'
|
||||
}
|
||||
@@ -369,12 +388,12 @@ export type UploadField = FieldBase & {
|
||||
}
|
||||
|
||||
type CodeAdmin = Admin & {
|
||||
editorOptions?: EditorProps['options']
|
||||
language?: string
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
editorOptions?: EditorProps['options']
|
||||
language?: string
|
||||
}
|
||||
|
||||
export type CodeField = Omit<FieldBase, 'admin'> & {
|
||||
@@ -385,11 +404,11 @@ export type CodeField = Omit<FieldBase, 'admin'> & {
|
||||
}
|
||||
|
||||
type JSONAdmin = Admin & {
|
||||
editorOptions?: EditorProps['options']
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
editorOptions?: EditorProps['options']
|
||||
}
|
||||
|
||||
export type JSONField = Omit<FieldBase, 'admin'> & {
|
||||
@@ -399,12 +418,12 @@ export type JSONField = Omit<FieldBase, 'admin'> & {
|
||||
|
||||
export type SelectField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
isClearable?: boolean
|
||||
isSortable?: boolean
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
isClearable?: boolean
|
||||
isSortable?: boolean
|
||||
}
|
||||
hasMany?: boolean
|
||||
options: Option[]
|
||||
@@ -414,11 +433,11 @@ export type SelectField = FieldBase & {
|
||||
export type RelationshipField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
allowCreate?: boolean
|
||||
isSortable?: boolean
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
isSortable?: boolean
|
||||
}
|
||||
filterOptions?: FilterOptions
|
||||
hasMany?: boolean
|
||||
@@ -502,11 +521,11 @@ export type ArrayField = FieldBase & {
|
||||
|
||||
export type RadioField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
layout?: 'horizontal' | 'vertical'
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
layout?: 'horizontal' | 'vertical'
|
||||
}
|
||||
options: Option[]
|
||||
type: 'radio'
|
||||
|
||||
@@ -413,6 +413,11 @@ describe('Field Validations', () => {
|
||||
const result = number(val, numberOptions)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('should validate 0', () => {
|
||||
const val = 0
|
||||
const result = number(val, { ...numberOptions, required: true })
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('should validate 2', () => {
|
||||
const val = 1.5
|
||||
const result = number(val, numberOptions)
|
||||
@@ -431,7 +436,7 @@ describe('Field Validations', () => {
|
||||
it('should handle required value', () => {
|
||||
const val = ''
|
||||
const result = number(val, { ...numberOptions, required: true })
|
||||
expect(result).toBe('validation:enterNumber')
|
||||
expect(result).toBe('validation:required')
|
||||
})
|
||||
it('should validate minValue', () => {
|
||||
const val = 2.4
|
||||
@@ -461,12 +466,12 @@ describe('Field Validations', () => {
|
||||
it('should validate an array of numbers using minRows', async () => {
|
||||
const val = [1.25, 2.5]
|
||||
const result = number(val, { ...numberOptions, hasMany: true, minRows: 4 })
|
||||
expect(result).toBe('validation:lessThanMin')
|
||||
expect(result).toBe('validation:requiresAtLeast')
|
||||
})
|
||||
it('should validate an array of numbers using maxRows', async () => {
|
||||
const val = [1.25, 2.5, 3.5]
|
||||
const result = number(val, { ...numberOptions, hasMany: true, maxRows: 2 })
|
||||
expect(result).toBe('validation:greaterThanMax')
|
||||
expect(result).toBe('validation:requiresNoMoreThan')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,67 +22,13 @@ import type {
|
||||
|
||||
import canUseDOM from '../utilities/canUseDOM'
|
||||
import { getIDType } from '../utilities/getIDType'
|
||||
import { isNumber } from '../utilities/isNumber'
|
||||
import { isValidID } from '../utilities/isValidID'
|
||||
import { fieldAffectsData } from './config/types'
|
||||
|
||||
export const number: Validate<unknown, unknown, NumberField> = (
|
||||
value: number | number[],
|
||||
{ hasMany, max, maxRows, min, minRows, required, t },
|
||||
) => {
|
||||
const toValidate: number[] = Array.isArray(value) ? value : [value]
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const valueToValidate of toValidate) {
|
||||
const floatValue = parseFloat(valueToValidate as unknown as string)
|
||||
if (
|
||||
(value && typeof floatValue !== 'number') ||
|
||||
(required && Number.isNaN(floatValue)) ||
|
||||
(value && Number.isNaN(floatValue))
|
||||
) {
|
||||
return t('validation:enterNumber')
|
||||
}
|
||||
|
||||
if (typeof max === 'number' && floatValue > max) {
|
||||
return t('validation:greaterThanMax', { label: t('value'), max, value })
|
||||
}
|
||||
|
||||
if (typeof min === 'number' && floatValue < min) {
|
||||
return t('validation:lessThanMin', { label: t('value'), min, value })
|
||||
}
|
||||
|
||||
if (required && typeof floatValue !== 'number') {
|
||||
return t('validation:required')
|
||||
}
|
||||
}
|
||||
|
||||
if (required && toValidate.length === 0) {
|
||||
return t('validation:required')
|
||||
}
|
||||
|
||||
if (hasMany === true) {
|
||||
if (minRows && toValidate.length < minRows) {
|
||||
return t('validation:lessThanMin', {
|
||||
label: t('rows'),
|
||||
min: minRows,
|
||||
value: toValidate.length,
|
||||
})
|
||||
}
|
||||
|
||||
if (maxRows && toValidate.length > maxRows) {
|
||||
return t('validation:greaterThanMax', {
|
||||
label: t('rows'),
|
||||
max: maxRows,
|
||||
value: toValidate.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const text: Validate<unknown, unknown, TextField> = (
|
||||
value: string,
|
||||
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
|
||||
{ config, maxLength: fieldMaxLength, minLength, required, t },
|
||||
) => {
|
||||
let maxLength: number
|
||||
|
||||
@@ -220,9 +166,87 @@ export const richText: Validate<object, unknown, RichTextField, RichTextField> =
|
||||
return await editor.validate(value, options)
|
||||
}
|
||||
|
||||
const validateArrayLength: any = (
|
||||
value,
|
||||
options: {
|
||||
maxRows?: number
|
||||
minRows?: number
|
||||
required?: boolean
|
||||
t: (key: string, options?: { [key: string]: number | string }) => string
|
||||
},
|
||||
) => {
|
||||
const { maxRows, minRows, required, t } = options
|
||||
|
||||
const arrayLength = Array.isArray(value) ? value.length : 0
|
||||
|
||||
if (!required && arrayLength === 0) return true
|
||||
|
||||
if (minRows && arrayLength < minRows) {
|
||||
return t('validation:requiresAtLeast', { count: minRows, label: t('rows') })
|
||||
}
|
||||
|
||||
if (maxRows && arrayLength > maxRows) {
|
||||
return t('validation:requiresNoMoreThan', { count: maxRows, label: t('rows') })
|
||||
}
|
||||
|
||||
if (required && !arrayLength) {
|
||||
return t('validation:requiresAtLeast', { count: 1, label: t('row') })
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const number: Validate<unknown, unknown, NumberField> = (
|
||||
value: number | number[],
|
||||
{ hasMany, max, maxRows, min, minRows, required, t },
|
||||
) => {
|
||||
if (hasMany === true) {
|
||||
const lengthValidationResult = validateArrayLength(value, { maxRows, minRows, required, t })
|
||||
if (typeof lengthValidationResult === 'string') return lengthValidationResult
|
||||
}
|
||||
|
||||
if (!value && !isNumber(value)) {
|
||||
// if no value is present, validate based on required
|
||||
if (required) return t('validation:required')
|
||||
if (!required) return true
|
||||
}
|
||||
|
||||
const numbersToValidate: number[] = Array.isArray(value) ? value : [value]
|
||||
|
||||
for (const number of numbersToValidate) {
|
||||
if (!isNumber(number)) return t('validation:enterNumber')
|
||||
|
||||
const numberValue = parseFloat(number as unknown as string)
|
||||
|
||||
if (typeof max === 'number' && numberValue > max) {
|
||||
return t('validation:greaterThanMax', { label: t('value'), max, value })
|
||||
}
|
||||
|
||||
if (typeof min === 'number' && numberValue < min) {
|
||||
return t('validation:lessThanMin', { label: t('value'), min, value })
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const array: Validate<unknown, unknown, ArrayField> = (
|
||||
value,
|
||||
{ maxRows, minRows, required, t },
|
||||
) => {
|
||||
return validateArrayLength(value, { maxRows, minRows, required, t })
|
||||
}
|
||||
|
||||
export const blocks: Validate<unknown, unknown, BlockField> = (
|
||||
value,
|
||||
{ maxRows, minRows, required, t },
|
||||
) => {
|
||||
return validateArrayLength(value, { maxRows, minRows, required, t })
|
||||
}
|
||||
|
||||
const validateFilterOptions: Validate = async (
|
||||
value,
|
||||
{ id, data, filterOptions, payload, relationTo, siblingData, t, user, req },
|
||||
{ id, data, filterOptions, payload, relationTo, req, siblingData, t, user },
|
||||
) => {
|
||||
if (!canUseDOM && typeof filterOptions !== 'undefined' && value) {
|
||||
const options: {
|
||||
@@ -344,7 +368,7 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
|
||||
return t('validation:required')
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
if (minRows && value.length < minRows) {
|
||||
return t('validation:lessThanMin', { label: t('rows'), min: minRows, value: value.length })
|
||||
}
|
||||
@@ -398,27 +422,6 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
|
||||
return validateFilterOptions(value, options)
|
||||
}
|
||||
|
||||
export const array: Validate<unknown, unknown, ArrayField> = (
|
||||
value,
|
||||
{ maxRows, minRows, required, t },
|
||||
) => {
|
||||
const arrayLength = Array.isArray(value) ? value.length : 0
|
||||
|
||||
if (minRows && arrayLength < minRows) {
|
||||
return t('validation:requiresAtLeast', { count: minRows, label: t('rows') })
|
||||
}
|
||||
|
||||
if (maxRows && arrayLength > maxRows) {
|
||||
return t('validation:requiresNoMoreThan', { count: maxRows, label: t('rows') })
|
||||
}
|
||||
|
||||
if (!arrayLength && required) {
|
||||
return t('validation:requiresAtLeast', { count: 1, label: t('row') })
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const select: Validate<unknown, unknown, SelectField> = (
|
||||
value,
|
||||
{ hasMany, options, required, t },
|
||||
@@ -467,27 +470,6 @@ export const radio: Validate<unknown, unknown, RadioField> = (value, { options,
|
||||
return required ? t('validation:required') : true
|
||||
}
|
||||
|
||||
export const blocks: Validate<unknown, unknown, BlockField> = (
|
||||
value,
|
||||
{ maxRows, minRows, required, t },
|
||||
) => {
|
||||
const arrayLength = Array.isArray(value) ? value.length : 0
|
||||
|
||||
if (minRows && arrayLength < minRows) {
|
||||
return t('validation:requiresAtLeast', { count: minRows, label: t('rows') })
|
||||
}
|
||||
|
||||
if (maxRows && arrayLength > maxRows) {
|
||||
return t('validation:requiresNoMoreThan', { count: maxRows, label: t('rows') })
|
||||
}
|
||||
|
||||
if (!arrayLength && required) {
|
||||
return t('validation:requiresAtLeast', { count: 1, label: t('row') })
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const point: Validate<unknown, unknown, PointField> = (
|
||||
value: [number | string, number | string] = ['', ''],
|
||||
{ required, t },
|
||||
|
||||
@@ -38,6 +38,11 @@ const buildEndpoints = (global: SanitizedGlobalConfig): Endpoint[] => {
|
||||
method: 'get',
|
||||
path: '/access',
|
||||
},
|
||||
{
|
||||
handler: async (req, res, next) => docAccessRequestHandler(req, res, next, global),
|
||||
method: 'post',
|
||||
path: '/access',
|
||||
},
|
||||
{
|
||||
handler: findOne(global),
|
||||
method: 'get',
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CollectionPermission, GlobalPermission } from '../../../auth'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
import type { SanitizedGlobalConfig } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import { docAccess } from '../../operations/docAccess'
|
||||
|
||||
export type Resolver = (
|
||||
@@ -16,7 +17,7 @@ export function docAccessResolver(global: SanitizedGlobalConfig): Resolver {
|
||||
async function resolver(_, context) {
|
||||
return docAccess({
|
||||
globalConfig: global,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import type { Document, PayloadRequest } from '../../../types'
|
||||
import type { Document } from '../../../types'
|
||||
import type { SanitizedGlobalConfig } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import findOne from '../../operations/findOne'
|
||||
|
||||
export default function findOneResolver(globalConfig: SanitizedGlobalConfig): Document {
|
||||
@@ -16,7 +17,7 @@ export default function findOneResolver(globalConfig: SanitizedGlobalConfig): Do
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
globalConfig,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
slug,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Document } from '../../../types'
|
||||
import type { SanitizedGlobalConfig } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import findVersionByID from '../../operations/findVersionByID'
|
||||
|
||||
export type Resolver = (
|
||||
@@ -31,7 +32,7 @@ export default function findVersionByIDResolver(globalConfig: SanitizedGlobalCon
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
globalConfig,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await findVersionByID(options)
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Document, Where } from '../../../types'
|
||||
import type { SanitizedGlobalConfig } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import findVersions from '../../operations/findVersions'
|
||||
|
||||
export type Resolver = (
|
||||
@@ -29,7 +30,7 @@ export default function findVersionsResolver(globalConfig: SanitizedGlobalConfig
|
||||
globalConfig,
|
||||
limit: args.limit,
|
||||
page: args.page,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
sort: args.sort,
|
||||
where: args.where,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { PayloadRequest } from '../../../express/types'
|
||||
import type { Document } from '../../../types'
|
||||
import type { SanitizedGlobalConfig } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import restoreVersion from '../../operations/restoreVersion'
|
||||
|
||||
type Resolver = (
|
||||
@@ -22,7 +23,7 @@ export default function restoreVersionResolver(globalConfig: SanitizedGlobalConf
|
||||
id: args.id,
|
||||
depth: 0,
|
||||
globalConfig,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
}
|
||||
|
||||
const result = await restoreVersion(options)
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { GeneratedTypes } from '../../../'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
import type { SanitizedGlobalConfig } from '../../config/types'
|
||||
|
||||
import isolateTransactionID from '../../../utilities/isolateTransactionID'
|
||||
import update from '../../operations/update'
|
||||
|
||||
type Resolver<TSlug extends keyof GeneratedTypes['globals']> = (
|
||||
@@ -35,7 +36,7 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['globa
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
globalConfig,
|
||||
req: { ...context.req } as PayloadRequest,
|
||||
req: isolateTransactionID(context.req),
|
||||
slug,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import type { PayloadRequest } from '../../express/types'
|
||||
import type { AllOperations } from '../../types'
|
||||
import type { SanitizedGlobalConfig } from '../config/types'
|
||||
|
||||
import { commitTransaction } from '../../utilities/commitTransaction'
|
||||
import { getEntityPolicies } from '../../utilities/getEntityPolicies'
|
||||
import { initTransaction } from '../../utilities/initTransaction'
|
||||
import { killTransaction } from '../../utilities/killTransaction'
|
||||
|
||||
type Arguments = {
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
@@ -19,10 +22,18 @@ export async function docAccess(args: Arguments): Promise<GlobalPermission> {
|
||||
globalOperations.push('readVersions')
|
||||
}
|
||||
|
||||
return getEntityPolicies({
|
||||
entity: globalConfig,
|
||||
operations: globalOperations,
|
||||
req,
|
||||
type: 'global',
|
||||
})
|
||||
try {
|
||||
const shouldCommit = await initTransaction(req)
|
||||
const result = await getEntityPolicies({
|
||||
entity: globalConfig,
|
||||
operations: globalOperations,
|
||||
req,
|
||||
type: 'global',
|
||||
})
|
||||
if (shouldCommit) await commitTransaction(req)
|
||||
return result
|
||||
} catch (e: unknown) {
|
||||
await killTransaction(req)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import initGlobals from '../globals/graphql/init'
|
||||
import buildFallbackLocaleInputType from './schema/buildFallbackLocaleInputType'
|
||||
import buildLocaleInputType from './schema/buildLocaleInputType'
|
||||
import buildPoliciesType from './schema/buildPoliciesType'
|
||||
import { wrapCustomFields } from './utilities/wrapCustomResolver'
|
||||
|
||||
export default function registerGraphQLSchema(payload: Payload): void {
|
||||
payload.types = {
|
||||
@@ -55,7 +56,7 @@ export default function registerGraphQLSchema(payload: Payload): void {
|
||||
...payload.Query,
|
||||
fields: {
|
||||
...payload.Query.fields,
|
||||
...(customQueries || {}),
|
||||
...wrapCustomFields((customQueries || {}) as never),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -66,7 +67,7 @@ export default function registerGraphQLSchema(payload: Payload): void {
|
||||
...payload.Mutation,
|
||||
fields: {
|
||||
...payload.Mutation.fields,
|
||||
...(customMutations || {}),
|
||||
...wrapCustomFields((customMutations || {}) as never),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
28
packages/payload/src/graphql/utilities/wrapCustomResolver.ts
Normal file
28
packages/payload/src/graphql/utilities/wrapCustomResolver.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { ObjMap } from 'graphql/jsutils/ObjMap'
|
||||
import type { GraphQLFieldResolver } from 'graphql/type/definition'
|
||||
import type { GraphQLFieldConfig } from 'graphql/type/definition'
|
||||
|
||||
import type { PayloadRequest } from '../../express/types'
|
||||
|
||||
import isolateTransactionID from '../../utilities/isolateTransactionID'
|
||||
|
||||
type PayloadContext = { req: PayloadRequest }
|
||||
|
||||
function wrapCustomResolver<TSource, TArgs, TResult>(
|
||||
resolver: GraphQLFieldResolver<TSource, PayloadContext, TArgs, TResult>,
|
||||
): GraphQLFieldResolver<TSource, PayloadContext, TArgs, TResult> {
|
||||
return (source, args, context, info) => {
|
||||
return resolver(source, args, { ...context, req: isolateTransactionID(context.req) }, info)
|
||||
}
|
||||
}
|
||||
|
||||
export function wrapCustomFields<TSource>(
|
||||
fields: ObjMap<GraphQLFieldConfig<TSource, PayloadContext>>,
|
||||
): ObjMap<GraphQLFieldConfig<TSource, PayloadContext>> {
|
||||
for (const key in fields) {
|
||||
if (fields[key].resolve) {
|
||||
fields[key].resolve = wrapCustomResolver(fields[key].resolve)
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -97,7 +97,7 @@
|
||||
"addLink": "Dodaj Link",
|
||||
"addNew": "Dodaj nowy",
|
||||
"addNewLabel": "Dodaj nowy {{label}}",
|
||||
"addRelationship": "Dodaj Relacje",
|
||||
"addRelationship": "Dodaj Relację",
|
||||
"addUpload": "Dodaj ładowanie",
|
||||
"block": "Blok",
|
||||
"blockType": "Typ Bloku",
|
||||
@@ -110,21 +110,21 @@
|
||||
"customURL": "Niestandardowy adres URL",
|
||||
"editLabelData": "Edytuj dane {{label}}",
|
||||
"editLink": "Edytuj Link",
|
||||
"editRelationship": "Edytuj Relacje",
|
||||
"editRelationship": "Edytuj Relację",
|
||||
"enterURL": "Wpisz adres URL",
|
||||
"internalLink": "Link wewnętrzny",
|
||||
"itemsAndMore": "{{items}} i {{count}} więcej",
|
||||
"labelRelationship": "Relacja {{label}}",
|
||||
"latitude": "Szerokość",
|
||||
"linkType": "Typ łącza",
|
||||
"linkedTo": "Połączony z <0>{{etykietą}}</0>",
|
||||
"linkedTo": "Połączony z <0>{{label}}</0>",
|
||||
"longitude": "Długość geograficzna",
|
||||
"newLabel": "Nowy {{label}}",
|
||||
"openInNewTab": "Otwórz w nowej karcie",
|
||||
"passwordsDoNotMatch": "Hasła nie pasują",
|
||||
"relatedDocument": "Powiązany dokument",
|
||||
"relationTo": "Powiązany z",
|
||||
"removeRelationship": "Usuń Związek",
|
||||
"removeRelationship": "Usuń Relację",
|
||||
"removeUpload": "Usuń Wrzucone",
|
||||
"saveChanges": "Zapisz zmiany",
|
||||
"searchForBlock": "Szukaj bloku",
|
||||
@@ -142,29 +142,29 @@
|
||||
"aboutToDeleteCount_many": "Zamierzasz usunąć {{count}} {{label}}",
|
||||
"aboutToDeleteCount_one": "Zamierzasz usunąć {{count}} {{label}}",
|
||||
"aboutToDeleteCount_other": "Zamierzasz usunąć {{count}} {{label}}",
|
||||
"addBelow": "Dodaj boniżej",
|
||||
"addBelow": "Dodaj poniżej",
|
||||
"addFilter": "Dodaj filtr",
|
||||
"adminTheme": "Motyw administratora",
|
||||
"and": "I",
|
||||
"and": "i",
|
||||
"applyChanges": "Zastosuj zmiany",
|
||||
"ascending": "Rosnąco",
|
||||
"automatic": "Automatyczny",
|
||||
"backToDashboard": "Powrót do panelu",
|
||||
"cancel": "Anuluj",
|
||||
"changesNotSaved": "Twoje zmiany nie zostały zapisane. Jeśli teraz wyjdziesz, stracisz swoje zmiany.",
|
||||
"close": "Zamknąć",
|
||||
"collapse": "Zawalenie",
|
||||
"close": "Zamknij",
|
||||
"collapse": "Zwiń",
|
||||
"collection": "Kolekcja",
|
||||
"collections": "Kolekcje",
|
||||
"columnToSort": "Kolumna sortowania",
|
||||
"columns": "Kolumny",
|
||||
"confirm": "Potiwerdź",
|
||||
"confirmDeletion": "Potiwerdź usunięcie",
|
||||
"confirm": "Potwierdź",
|
||||
"confirmDeletion": "Potwierdź usunięcie",
|
||||
"confirmDuplication": "Potwierdź duplikację",
|
||||
"copied": "Skopiowano",
|
||||
"copy": "Skopiuj",
|
||||
"create": "Stwórz",
|
||||
"createNew": "Stwórzy nowy",
|
||||
"createNew": "Stwórz nowy",
|
||||
"createNewLabel": "Stwórz nowy {{label}}",
|
||||
"created": "Utworzono",
|
||||
"createdAt": "Data utworzenia",
|
||||
@@ -173,8 +173,8 @@
|
||||
"dark": "Ciemny",
|
||||
"dashboard": "Panel",
|
||||
"delete": "Usuń",
|
||||
"deletedCountSuccessfully": "",
|
||||
"deletedSuccessfully": "Skutecznie usunięte.",
|
||||
"deletedCountSuccessfully": "Pomyślnie usunięto {{count}} {{label}}.",
|
||||
"deletedSuccessfully": "Pomyślnie usunięto.",
|
||||
"deleting": "Usuwanie...",
|
||||
"descending": "Malejąco",
|
||||
"deselectAllRows": "Odznacz wszystkie wiersze",
|
||||
@@ -191,7 +191,7 @@
|
||||
"enterAValue": "Wpisz wartość",
|
||||
"error": "Błąd",
|
||||
"errors": "Błędy",
|
||||
"fallbackToDefaultLocale": "Powrót do domyślnego locale",
|
||||
"fallbackToDefaultLocale": "Powrót do domyślnych ustawień regionalnych",
|
||||
"filter": "Filtr",
|
||||
"filterWhere": "Filtruj gdzie",
|
||||
"filters": "Filtry",
|
||||
@@ -203,8 +203,8 @@
|
||||
"light": "Jasny",
|
||||
"livePreview": "Podgląd",
|
||||
"loading": "Ładowanie",
|
||||
"locale": "Lokalizacja",
|
||||
"locales": "Lokalne",
|
||||
"locale": "Ustawienia regionalne",
|
||||
"locales": "Ustawienia regionalne",
|
||||
"menu": "Menu",
|
||||
"moveDown": "Przesuń niżej",
|
||||
"moveUp": "Przesuń wyżej",
|
||||
@@ -219,7 +219,7 @@
|
||||
"nothingFound": "Nic nie znaleziono",
|
||||
"of": "z",
|
||||
"open": "Otwórz",
|
||||
"or": "Lub",
|
||||
"or": "lub",
|
||||
"order": "Kolejność",
|
||||
"pageNotFound": "Strona nie znaleziona",
|
||||
"password": "Hasło",
|
||||
@@ -232,18 +232,18 @@
|
||||
"save": "Zapisz",
|
||||
"saving": "Zapisywanie...",
|
||||
"searchBy": "Szukaj według",
|
||||
"selectAll": "Wybierz wszystkie {{liczba}} {{etykieta}}",
|
||||
"selectAll": "Wybierz wszystkie {{count}} {{label}}",
|
||||
"selectAllRows": "Wybierz wszystkie wiersze",
|
||||
"selectValue": "Wybierz wartość",
|
||||
"selectedCount": "Wybrano {{count}} {{label}}",
|
||||
"showAllLabel": "Pokaż wszystkie {{label}}",
|
||||
"sorryNotFound": "Przepraszamy — nie ma nic, co odpowiadałoby twojej prośbie.",
|
||||
"sorryNotFound": "Przepraszamy — nie ma nic, co odpowiadałoby twojemu zapytaniu.",
|
||||
"sort": "Sortuj",
|
||||
"sortByLabelDirection": "Sortuj według {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Pozostań na stronie",
|
||||
"submissionSuccessful": "Zgłoszenie zakończone powodzeniem.",
|
||||
"submit": "Zatwierdź",
|
||||
"successfullyCreated": "{{label}} successfully created.",
|
||||
"successfullyCreated": "Pomyślnie utworzono {{label}}.",
|
||||
"successfullyDuplicated": "Pomyślnie zduplikowano {{label}}",
|
||||
"thisLanguage": "Polski",
|
||||
"titleDeleted": "Pomyślnie usunięto {{label}} {{title}}",
|
||||
@@ -254,7 +254,7 @@
|
||||
"updatedCountSuccessfully": "Pomyślnie zaktualizowano {{count}} {{label}}.",
|
||||
"updatedSuccessfully": "Aktualizacja zakończona sukcesem.",
|
||||
"updating": "Aktualizacja",
|
||||
"uploading": "Wgrywanie",
|
||||
"uploading": "Przesyłanie",
|
||||
"user": "użytkownik",
|
||||
"users": "użytkownicy",
|
||||
"value": "Wartość",
|
||||
@@ -309,7 +309,7 @@
|
||||
"longerThanMin": "Ta wartość musi być dłuższa niż minimalna długość znaków: {{minLength}}.",
|
||||
"notValidDate": "\"{{value}}\" nie jest prawidłową datą.",
|
||||
"required": "To pole jest wymagane.",
|
||||
"requiresAtLeast": "This field requires at least {{count}} {{label}}.",
|
||||
"requiresAtLeast": "To pole wymaga co najmniej {{count}} {{label}}.",
|
||||
"requiresNoMoreThan": "To pole może posiadać co najmniej {{count}} {{label}}.",
|
||||
"requiresTwoNumbers": "To pole wymaga dwóch liczb.",
|
||||
"shorterThanMax": "Ta wartość musi być krótsza niż maksymalna długość znaków: {{maxLength}}.",
|
||||
@@ -349,9 +349,9 @@
|
||||
"revertToPublished": "Przywróć do opublikowanego",
|
||||
"reverting": "Cofanie...",
|
||||
"saveDraft": "Zapisz szkic",
|
||||
"selectLocales": "Wybierz lokalizacje do wyświetlenia",
|
||||
"selectLocales": "Wybierz ustawienia regionalne do wyświetlenia",
|
||||
"selectVersionToCompare": "Wybierz wersję do porównania",
|
||||
"showLocales": "Pokaż lokalizacje:",
|
||||
"showLocales": "Pokaż ustawienia regionalne:",
|
||||
"showingVersionsFor": "Wyświetlanie wersji dla:",
|
||||
"status": "Status",
|
||||
"type": "Typ",
|
||||
@@ -370,4 +370,4 @@
|
||||
"viewingVersions": "Przeglądanie wersji {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Przeglądanie wersji dla globalnej kolekcji {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ const preventResize = (
|
||||
|
||||
const isWidthOrHeightNotDefined = !desiredHeight || !desiredWidth
|
||||
if (isWidthOrHeightNotDefined) {
|
||||
// If with and height are not defined, it means there is a format conversion
|
||||
// If width and height are not defined, it means there is a format conversion
|
||||
// and the image needs to be "resized" (transformed).
|
||||
return false // needs resize
|
||||
}
|
||||
@@ -156,9 +156,10 @@ const preventResize = (
|
||||
* @returns true if the image should passed directly to sharp
|
||||
*/
|
||||
const applyPayloadAdjustments = (
|
||||
{ height, width, withoutEnlargement, withoutReduction }: ImageSize,
|
||||
{ fit, height, width, withoutEnlargement, withoutReduction }: ImageSize,
|
||||
original: ProbedImageSize,
|
||||
) => {
|
||||
if (fit === 'contain' || fit === 'inside') return false
|
||||
if (!isNumber(height) && !isNumber(width)) return false
|
||||
|
||||
const targetAspectRatio = width / height
|
||||
|
||||
@@ -46,9 +46,6 @@ function buildOptionEnums(options: Option[]): string[] {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used for generating the TypeScript types (payload-types.ts) with the payload generate:types command.
|
||||
*/
|
||||
function generateEntitySchemas(
|
||||
entities: (SanitizedCollectionConfig | SanitizedGlobalConfig)[],
|
||||
): JSONSchema4 {
|
||||
@@ -68,7 +65,10 @@ function generateEntitySchemas(
|
||||
}
|
||||
}
|
||||
|
||||
function withNullableType(
|
||||
/**
|
||||
* Returns a JSON Schema Type with 'null' added if the field is not required.
|
||||
*/
|
||||
export function withNullableJSONSchemaType(
|
||||
fieldType: JSONSchema4TypeName,
|
||||
isRequired: boolean,
|
||||
): JSONSchema4TypeName | JSONSchema4TypeName[] {
|
||||
@@ -103,7 +103,7 @@ function fieldsToJSONSchema(
|
||||
case 'code':
|
||||
case 'email':
|
||||
case 'date': {
|
||||
fieldSchema = { type: withNullableType('string', isRequired) }
|
||||
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
|
||||
break
|
||||
}
|
||||
|
||||
@@ -111,16 +111,16 @@ function fieldsToJSONSchema(
|
||||
if (field.hasMany === true) {
|
||||
fieldSchema = {
|
||||
items: { type: 'number' },
|
||||
type: withNullableType('array', isRequired),
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
}
|
||||
} else {
|
||||
fieldSchema = { type: withNullableType('number', isRequired) }
|
||||
fieldSchema = { type: withNullableJSONSchemaType('number', isRequired) }
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'checkbox': {
|
||||
fieldSchema = { type: withNullableType('boolean', isRequired) }
|
||||
fieldSchema = { type: withNullableJSONSchemaType('boolean', isRequired) }
|
||||
break
|
||||
}
|
||||
|
||||
@@ -132,11 +132,19 @@ function fieldsToJSONSchema(
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
fieldSchema = {
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
type: withNullableType('array', isRequired),
|
||||
if (field.editor.outputSchema) {
|
||||
fieldSchema = field.editor.outputSchema({
|
||||
field,
|
||||
isRequired,
|
||||
})
|
||||
} else {
|
||||
// Maintain backwards compatibility with existing rich text editors
|
||||
fieldSchema = {
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
@@ -145,7 +153,7 @@ function fieldsToJSONSchema(
|
||||
case 'radio': {
|
||||
fieldSchema = {
|
||||
enum: buildOptionEnums(field.options),
|
||||
type: withNullableType('string', isRequired),
|
||||
type: withNullableJSONSchemaType('string', isRequired),
|
||||
}
|
||||
|
||||
break
|
||||
@@ -160,12 +168,12 @@ function fieldsToJSONSchema(
|
||||
enum: optionEnums,
|
||||
type: 'string',
|
||||
},
|
||||
type: withNullableType('array', isRequired),
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
enum: optionEnums,
|
||||
type: withNullableType('string', isRequired),
|
||||
type: withNullableJSONSchemaType('string', isRequired),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +192,7 @@ function fieldsToJSONSchema(
|
||||
],
|
||||
maxItems: 2,
|
||||
minItems: 2,
|
||||
type: withNullableType('array', isRequired),
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -217,7 +225,7 @@ function fieldsToJSONSchema(
|
||||
}
|
||||
}),
|
||||
},
|
||||
type: withNullableType('array', isRequired),
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
@@ -240,7 +248,7 @@ function fieldsToJSONSchema(
|
||||
},
|
||||
},
|
||||
required: ['value', 'relationTo'],
|
||||
type: withNullableType('object', isRequired),
|
||||
type: withNullableJSONSchemaType('object', isRequired),
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -257,13 +265,16 @@ function fieldsToJSONSchema(
|
||||
},
|
||||
],
|
||||
},
|
||||
type: withNullableType('array', isRequired),
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
oneOf: [
|
||||
{
|
||||
type: withNullableType(collectionIDFieldTypes[field.relationTo], isRequired),
|
||||
type: withNullableJSONSchemaType(
|
||||
collectionIDFieldTypes[field.relationTo],
|
||||
isRequired,
|
||||
),
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${field.relationTo}`,
|
||||
@@ -323,7 +334,7 @@ function fieldsToJSONSchema(
|
||||
return blockSchema
|
||||
}),
|
||||
},
|
||||
type: withNullableType('array', isRequired),
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -339,7 +350,7 @@ function fieldsToJSONSchema(
|
||||
interfaceNameDefinitions,
|
||||
),
|
||||
},
|
||||
type: withNullableType('array', isRequired),
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
}
|
||||
|
||||
if (field.interfaceName) {
|
||||
@@ -497,6 +508,9 @@ export function entityToJSONSchema(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used for generating the TypeScript types (payload-types.ts) with the payload generate:types command.
|
||||
*/
|
||||
export function configToJSONSchema(
|
||||
config: SanitizedConfig,
|
||||
defaultIDType?: 'number' | 'text',
|
||||
|
||||
@@ -7,6 +7,13 @@ import {
|
||||
tabHasName,
|
||||
} from '../fields/config/types'
|
||||
|
||||
/**
|
||||
* Flattens a collection's fields into a single array of fields, as long
|
||||
* as the fields do not affect data.
|
||||
*
|
||||
* @param fields
|
||||
* @param keepPresentationalFields if true, will skip flattening fields that are presentational only
|
||||
*/
|
||||
const flattenFields = (
|
||||
fields: Field[],
|
||||
keepPresentationalFields?: boolean,
|
||||
|
||||
@@ -100,7 +100,10 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
|
||||
if (accessLevel === 'field' && docBeingAccessed === undefined) {
|
||||
docBeingAccessed = await getEntityDoc()
|
||||
}
|
||||
const accessResult = await access({ id, doc: docBeingAccessed, req })
|
||||
|
||||
const data = req?.body
|
||||
|
||||
const accessResult = await access({ id, data, doc: docBeingAccessed, req })
|
||||
|
||||
if (typeof accessResult === 'object' && !disableWhere) {
|
||||
mutablePolicies[operation] = {
|
||||
|
||||
@@ -5,12 +5,24 @@ import type { PayloadRequest } from '../express/types'
|
||||
* @returns true if beginning a transaction and false when req already has a transaction to use
|
||||
*/
|
||||
export async function initTransaction(req: PayloadRequest): Promise<boolean> {
|
||||
const { payload, transactionID } = req
|
||||
if (!transactionID && typeof payload.db.beginTransaction === 'function') {
|
||||
req.transactionID = await payload.db.beginTransaction()
|
||||
if (req.transactionID) {
|
||||
return true
|
||||
}
|
||||
const { payload, transactionID, transactionIDPromise } = req
|
||||
if (transactionID) {
|
||||
// we already have a transaction, we're not in charge of committing it
|
||||
return false
|
||||
}
|
||||
if (transactionIDPromise) {
|
||||
// wait for whoever else is already creating the transaction
|
||||
await transactionIDPromise
|
||||
return false
|
||||
}
|
||||
if (typeof payload.db.beginTransaction === 'function') {
|
||||
// create a new transaction
|
||||
req.transactionIDPromise = payload.db.beginTransaction().then((transactionID) => {
|
||||
req.transactionID = transactionID
|
||||
delete req.transactionIDPromise
|
||||
})
|
||||
await req.transactionIDPromise
|
||||
return !!req.transactionID
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
29
packages/payload/src/utilities/isolateTransactionID.ts
Normal file
29
packages/payload/src/utilities/isolateTransactionID.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { PayloadRequest } from '../express/types'
|
||||
|
||||
/**
|
||||
* Creates a proxy for the given request that has its own TransactionID
|
||||
*/
|
||||
export default function isolateTransactionID(req: PayloadRequest): PayloadRequest {
|
||||
const delegate = {}
|
||||
const handler: ProxyHandler<PayloadRequest> = {
|
||||
deleteProperty(target, p): boolean {
|
||||
return Reflect.deleteProperty(p === 'transactionID' ? delegate : target, p)
|
||||
},
|
||||
get(target, p, receiver) {
|
||||
return Reflect.get(p === 'transactionID' ? delegate : target, p, receiver)
|
||||
},
|
||||
has(target, p) {
|
||||
return Reflect.has(p === 'transactionID' ? delegate : target, p)
|
||||
},
|
||||
set(target, p, newValue, receiver) {
|
||||
if (p === 'transactionID') {
|
||||
// in case of transactionID we must ignore any receiver, because
|
||||
// "If provided and target does not have a setter for propertyKey, the property will be set on receiver instead."
|
||||
return Reflect.set(delegate, p, newValue)
|
||||
} else {
|
||||
return Reflect.set(target, p, newValue, receiver)
|
||||
}
|
||||
},
|
||||
}
|
||||
return new Proxy(req, handler)
|
||||
}
|
||||
@@ -10,14 +10,14 @@
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "yarn clean && yarn build",
|
||||
"prepublishOnly": "pnpm clean && pnpm build",
|
||||
"test": "echo \"No tests available.\""
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.142.0",
|
||||
"@aws-sdk/lib-storage": "^3.267.0",
|
||||
"@azure/storage-blob": "^12.11.0",
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/storage-blob": "^12.11.0",
|
||||
"@google-cloud/storage": "^6.4.1",
|
||||
"payload": "^1.7.2 || ^2.0.0"
|
||||
},
|
||||
@@ -49,6 +49,7 @@
|
||||
"@azure/storage-blob": "^12.11.0",
|
||||
"@google-cloud/storage": "^6.4.1",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/find-node-modules": "^2.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"nodemon": "^2.0.6",
|
||||
@@ -58,6 +59,7 @@
|
||||
"webpack": "^5.78.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"find-node-modules": "^2.1.3",
|
||||
"range-parser": "^1.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
import type { Configuration as WebpackConfig } from 'webpack'
|
||||
|
||||
import findNodeModules from 'find-node-modules'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const packageName = '@payloadcms/plugin-cloud-storage'
|
||||
|
||||
const nodeModulesPaths = findNodeModules({ cwd: __dirname, relative: false })
|
||||
|
||||
export const extendWebpackConfig = (existingWebpackConfig: WebpackConfig): WebpackConfig => {
|
||||
let nodeModulesPath = nodeModulesPaths.find((p) => {
|
||||
const guess = path.resolve(p, `${packageName}/dist`)
|
||||
|
||||
if (fs.existsSync(guess)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (!nodeModulesPath) {
|
||||
nodeModulesPath = process.cwd()
|
||||
}
|
||||
|
||||
const newConfig: WebpackConfig = {
|
||||
...existingWebpackConfig,
|
||||
resolve: {
|
||||
...(existingWebpackConfig.resolve || {}),
|
||||
alias: {
|
||||
...(existingWebpackConfig.resolve?.alias ? existingWebpackConfig.resolve.alias : {}),
|
||||
'@payloadcms/plugin-cloud-storage/s3': path.resolve(__dirname, './mock.js'),
|
||||
'@payloadcms/plugin-cloud-storage/s3$': path.resolve(
|
||||
nodeModulesPath,
|
||||
`./${packageName}/dist/adapters/s3/mock.js`,
|
||||
),
|
||||
},
|
||||
fallback: {
|
||||
...(existingWebpackConfig.resolve?.fallback ? existingWebpackConfig.resolve.fallback : {}),
|
||||
|
||||
@@ -29,7 +29,7 @@ export const extendWebpackConfig =
|
||||
},
|
||||
}
|
||||
|
||||
return Object.entries(options.collections).reduce(
|
||||
const modifiedConfig = Object.entries(options.collections).reduce(
|
||||
(resultingWebpackConfig, [slug, collectionOptions]) => {
|
||||
const matchedCollection = config.collections?.find((coll) => coll.slug === slug)
|
||||
|
||||
@@ -47,4 +47,6 @@ export const extendWebpackConfig =
|
||||
},
|
||||
newConfig,
|
||||
)
|
||||
|
||||
return modifiedConfig
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -32,7 +32,7 @@ const resaveChildren =
|
||||
collection: collection.slug,
|
||||
data: {
|
||||
...child,
|
||||
breadcrumbs: populateBreadcrumbs(req, pluginConfig, collection, child),
|
||||
breadcrumbs: await populateBreadcrumbs(req, pluginConfig, collection, child),
|
||||
},
|
||||
depth: 0,
|
||||
draft: updateAsDraft,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-sentry",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"homepage:": "https://payloadcms.com",
|
||||
"repository": "git@github.com:payloadcms/plugin-sentry.git",
|
||||
"description": "Sentry plugin for Payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.1.16",
|
||||
"version": "0.2.0",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -19,26 +19,26 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@faceless-ui/modal": "2.0.1",
|
||||
"@lexical/headless": "0.12.2",
|
||||
"@lexical/link": "0.12.2",
|
||||
"@lexical/list": "0.12.2",
|
||||
"@lexical/mark": "0.12.2",
|
||||
"@lexical/markdown": "0.12.2",
|
||||
"@lexical/react": "0.12.2",
|
||||
"@lexical/rich-text": "0.12.2",
|
||||
"@lexical/selection": "0.12.2",
|
||||
"@lexical/utils": "0.12.2",
|
||||
"@lexical/headless": "0.12.4",
|
||||
"@lexical/link": "0.12.4",
|
||||
"@lexical/list": "0.12.4",
|
||||
"@lexical/mark": "0.12.4",
|
||||
"@lexical/markdown": "0.12.4",
|
||||
"@lexical/react": "0.12.4",
|
||||
"@lexical/rich-text": "0.12.4",
|
||||
"@lexical/selection": "0.12.4",
|
||||
"@lexical/utils": "0.12.4",
|
||||
"bson-objectid": "2.0.4",
|
||||
"classnames": "^2.3.2",
|
||||
"deep-equal": "2.2.3",
|
||||
"i18next": "22.5.1",
|
||||
"lexical": "0.12.2",
|
||||
"lexical": "0.12.4",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-i18next": "11.18.6",
|
||||
"ts-essentials": "7.0.3",
|
||||
"deep-equal": "2.2.2"
|
||||
"ts-essentials": "7.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
@@ -47,7 +47,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "^2.0.14"
|
||||
"payload": "^2.2.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -63,12 +63,7 @@ export const RichTextCell: React.FC<
|
||||
return $getRoot().getTextContent()
|
||||
}) || ''
|
||||
|
||||
// Limit preview to 150 characters
|
||||
if (textContent.length > 150) {
|
||||
setPreview(textContent.slice(0, 150) + '...')
|
||||
return
|
||||
}
|
||||
|
||||
// Limiting the number of characters shown is done in a CSS rule
|
||||
setPreview(textContent)
|
||||
}, [data, editorConfig])
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
import type { FieldProps } from '../types'
|
||||
|
||||
import { defaultRichTextValueV2 } from '../populate/defaultValue'
|
||||
import { richTextValidateHOC } from '../validate'
|
||||
import './index.scss'
|
||||
import { LexicalProvider } from './lexical/LexicalProvider'
|
||||
@@ -25,7 +24,6 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
style,
|
||||
width,
|
||||
},
|
||||
defaultValue: defaultValueFromProps,
|
||||
editorConfig,
|
||||
label,
|
||||
path: pathFromProps,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block, Data, Fields } from 'payload/types'
|
||||
import type { Block, Data, Field, Fields } from 'payload/types'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import isDeepEqual from 'deep-equal'
|
||||
@@ -21,7 +21,8 @@ type Props = {
|
||||
baseClass: string
|
||||
block: Block
|
||||
field: FieldProps
|
||||
fields: BlockFields
|
||||
formData: BlockFields
|
||||
formSchema: Field[]
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
@@ -31,7 +32,14 @@ type Props = {
|
||||
* not the whole document.
|
||||
*/
|
||||
export const BlockContent: React.FC<Props> = (props) => {
|
||||
const { baseClass, block, field, fields, nodeKey } = props
|
||||
const {
|
||||
baseClass,
|
||||
block: { labels },
|
||||
field,
|
||||
formData,
|
||||
formSchema,
|
||||
nodeKey,
|
||||
} = props
|
||||
const { i18n } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
// Used for saving collapsed to preferences (and gettin' it from there again)
|
||||
@@ -47,9 +55,9 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
|
||||
const collapsedMap: { [key: string]: boolean } = currentFieldPreferences?.collapsed
|
||||
|
||||
if (collapsedMap && collapsedMap[fields.data.id] !== undefined) {
|
||||
setCollapsed(collapsedMap[fields.data.id])
|
||||
initialState = collapsedMap[fields.data.id]
|
||||
if (collapsedMap && collapsedMap[formData.id] !== undefined) {
|
||||
setCollapsed(collapsedMap[formData.id])
|
||||
initialState = collapsedMap[formData.id]
|
||||
}
|
||||
})
|
||||
return initialState
|
||||
@@ -70,13 +78,19 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
const path = '' as const
|
||||
|
||||
const onFormChange = useCallback(
|
||||
({ fields: formFields, formData }: { fields: Fields; formData: Data }) => {
|
||||
({
|
||||
fullFieldsWithValues,
|
||||
newFormData,
|
||||
}: {
|
||||
fullFieldsWithValues: Fields
|
||||
newFormData: Data
|
||||
}) => {
|
||||
// Recursively remove all undefined values from even being present in formData, as they will
|
||||
// cause isDeepEqual to return false if, for example, formData has a key that fields.data
|
||||
// does not have, even if it's undefined.
|
||||
// Currently, this happens if a block has another sub-blocks field. Inside of formData, that sub-blocks field has an undefined blockName property.
|
||||
// Inside of fields.data however, that sub-blocks blockName property does not exist at all.
|
||||
function removeUndefinedRecursively(obj: any) {
|
||||
function removeUndefinedRecursively(obj: object) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (obj[key] && typeof obj[key] === 'object') {
|
||||
removeUndefinedRecursively(obj[key])
|
||||
@@ -85,26 +99,30 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
removeUndefinedRecursively(newFormData)
|
||||
removeUndefinedRecursively(formData)
|
||||
removeUndefinedRecursively(fields.data)
|
||||
|
||||
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
|
||||
// which would trigger the "Leave without saving" dialog unnecessarily
|
||||
if (!isDeepEqual(fields.data, formData)) {
|
||||
editor.update(() => {
|
||||
const node: BlockNode = $getNodeByKey(nodeKey)
|
||||
if (node) {
|
||||
node.setFields({
|
||||
data: formData as any,
|
||||
})
|
||||
}
|
||||
})
|
||||
if (!isDeepEqual(formData, newFormData)) {
|
||||
// Running this in the next tick in the meantime fixes this issue: https://github.com/payloadcms/payload/issues/4108
|
||||
// I don't know why. When this is called immediately, it might focus out of a nested lexical editor field if an update is made there.
|
||||
// My hypothesis is that the nested editor might not have fully finished its update cycle yet. By updating in the next tick, we
|
||||
// ensure that the nested editor has finished its update cycle before we update the block node.
|
||||
setTimeout(() => {
|
||||
editor.update(() => {
|
||||
const node: BlockNode = $getNodeByKey(nodeKey)
|
||||
if (node) {
|
||||
node.setFields(newFormData as BlockFields)
|
||||
}
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// update error count
|
||||
if (hasSubmitted) {
|
||||
let rowErrorCount = 0
|
||||
for (const formField of Object.values(formFields)) {
|
||||
for (const formField of Object.values(fullFieldsWithValues)) {
|
||||
if (formField?.valid === false) {
|
||||
rowErrorCount++
|
||||
}
|
||||
@@ -112,7 +130,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
setErrorCount(rowErrorCount)
|
||||
}
|
||||
},
|
||||
[editor, nodeKey, hasSubmitted],
|
||||
[editor, nodeKey, hasSubmitted, formData],
|
||||
)
|
||||
|
||||
const onCollapsedChange = useCallback(() => {
|
||||
@@ -124,13 +142,13 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
const newCollapsed: { [key: string]: boolean } =
|
||||
collapsedMap && collapsedMap?.size ? collapsedMap : {}
|
||||
|
||||
newCollapsed[fields.data.id] = !collapsed
|
||||
newCollapsed[formData.id] = !collapsed
|
||||
|
||||
setDocFieldPreferences(field.name, {
|
||||
collapsed: newCollapsed,
|
||||
})
|
||||
})
|
||||
}, [collapsed, getDocPreferences, field.name, setDocFieldPreferences, fields.data.id])
|
||||
}, [collapsed, getDocPreferences, field.name, setDocFieldPreferences, formData.id])
|
||||
|
||||
const removeBlock = useCallback(() => {
|
||||
editor.update(() => {
|
||||
@@ -138,6 +156,11 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
})
|
||||
}, [editor, nodeKey])
|
||||
|
||||
const fieldSchemaWithPath = formSchema.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(null, field),
|
||||
}))
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Collapsible
|
||||
@@ -148,10 +171,10 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<div>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${fields?.data?.blockType}`}
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{getTranslation(block.labels.singular, i18n)}
|
||||
{getTranslation(labels.singular, i18n)}
|
||||
</Pill>
|
||||
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} withMessage />}
|
||||
@@ -180,25 +203,16 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
fieldSchema={block.fields.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(null, field),
|
||||
}))}
|
||||
fieldSchema={fieldSchemaWithPath}
|
||||
fieldTypes={field.fieldTypes}
|
||||
forceRender
|
||||
margins="small"
|
||||
permissions={field.permissions?.blocks?.[fields?.data?.blockType]?.fields}
|
||||
permissions={field.permissions?.blocks?.[formData?.blockType]?.fields}
|
||||
readOnly={field.admin.readOnly}
|
||||
/>
|
||||
</Collapsible>
|
||||
|
||||
<FormSavePlugin
|
||||
fieldSchema={block.fields.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(null, field),
|
||||
}))}
|
||||
onChange={onFormChange}
|
||||
/>
|
||||
<FormSavePlugin onChange={onFormChange} />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user