Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5139072c8 | ||
|
|
e004682799 | ||
|
|
c651835061 | ||
|
|
2255ebb64a | ||
|
|
e2ec2f7b97 | ||
|
|
00196a8631 | ||
|
|
2a09f15a15 | ||
|
|
0420b6dc27 | ||
|
|
10c30260dd | ||
|
|
25000261bd | ||
|
|
bb82cdcef4 | ||
|
|
027dff8363 | ||
|
|
31ca1ab379 | ||
|
|
33c1f287f3 | ||
|
|
cd4861afda | ||
|
|
9f56ac182f | ||
|
|
3301f59822 | ||
|
|
f9ca3a9f96 | ||
|
|
3ba7594a65 | ||
|
|
6a1b25ab30 | ||
|
|
17dbbc7775 | ||
|
|
7e25abf87a | ||
|
|
0591dfd05b | ||
|
|
17610adf36 | ||
|
|
91814777b0 | ||
|
|
09d793926d | ||
|
|
a9f2f0ec03 | ||
|
|
66bf8c3cbd | ||
|
|
3967c1233f | ||
|
|
c929725dd5 | ||
|
|
9c6098b191 | ||
|
|
6daab398da | ||
|
|
36ef3789fb | ||
|
|
14cbf2f079 | ||
|
|
87bbf4416b | ||
|
|
785b992c3e | ||
|
|
b4695e10b6 | ||
|
|
0b0d971491 | ||
|
|
02af6b90b2 | ||
|
|
2181bc84a1 | ||
|
|
036cd5f831 | ||
|
|
da9825cd99 | ||
|
|
4a43f95952 | ||
|
|
9af9b73132 | ||
|
|
7f7d3dbeef | ||
|
|
8ef9206001 | ||
|
|
21ba237135 | ||
|
|
3bda163e7b | ||
|
|
e4e4ad1b08 | ||
|
|
f7352a7d08 | ||
|
|
6f6f2f8e7b | ||
|
|
5ca5abab42 | ||
|
|
9a7553099c | ||
|
|
55d0c917e6 | ||
|
|
f52daeccf0 | ||
|
|
6c871c57fc | ||
|
|
5322ada9e6 | ||
|
|
ee83a50ea9 | ||
|
|
f6b19e074c | ||
|
|
6cc1d9e41b | ||
|
|
74863f9462 | ||
|
|
fdcf029da2 | ||
|
|
3e3d151e4c | ||
|
|
5da204b152 | ||
|
|
3d6c3f7339 | ||
|
|
8d49517004 | ||
|
|
d1c0f2b97b | ||
|
|
1bc42ae098 | ||
|
|
c6edb7f53a | ||
|
|
1e048fe037 | ||
|
|
8fabdce584 | ||
|
|
5c1a3fabee | ||
|
|
fe6d30210b | ||
|
|
93f71e621c | ||
|
|
39d1a09d5a | ||
|
|
74ae6fd1d5 | ||
|
|
bbbcf8c869 | ||
|
|
b379666dec | ||
|
|
6f40b5c9ab | ||
|
|
b329be7dc1 | ||
|
|
c2ec54a7cb | ||
|
|
3641dfd38a | ||
|
|
5bf1354741 | ||
|
|
b894b809bf | ||
|
|
a4504ca15b | ||
|
|
7926083732 | ||
|
|
534cd5ae53 | ||
|
|
fb329a99ba | ||
|
|
9e726d9b90 | ||
|
|
8d065d619d | ||
|
|
cbff1776e7 | ||
|
|
e517695000 | ||
|
|
4370cfca0c | ||
|
|
4135b618ef | ||
|
|
1cfce87549 | ||
|
|
c48283ac1d | ||
|
|
328be3e4bc | ||
|
|
b4becd1493 | ||
|
|
95fac0bd62 | ||
|
|
a30d9dc1d7 | ||
|
|
7bfcefbfea | ||
|
|
131b2796e7 | ||
|
|
debcb003bb | ||
|
|
6e1dfff1b8 | ||
|
|
a9ebb71a09 | ||
|
|
3e34e5216f | ||
|
|
2400c58219 | ||
|
|
90d504526c | ||
|
|
c97d4f9545 | ||
|
|
09a8144f3c | ||
|
|
00ef1700ae | ||
|
|
3e03b2b5df | ||
|
|
974f79e57e | ||
|
|
34f42083b5 | ||
|
|
c0cae1e834 | ||
|
|
3ce8ee4661 | ||
|
|
f9feff58d6 | ||
|
|
73848b6037 | ||
|
|
7fd8124df6 | ||
|
|
1c77455403 | ||
|
|
051a0fad84 | ||
|
|
8e53ef47a0 | ||
|
|
918130486e | ||
|
|
b454811698 | ||
|
|
f87c68f310 | ||
|
|
25006d44e8 | ||
|
|
d8e51dd200 | ||
|
|
f54210a528 | ||
|
|
96dab15cd1 | ||
|
|
4126843619 | ||
|
|
e0238ad393 | ||
|
|
aa0302c05e | ||
|
|
1040ad2cfe | ||
|
|
c64f15d4d9 | ||
|
|
22ea98ca33 | ||
|
|
75bab716d1 | ||
|
|
52a8e9624c | ||
|
|
52cd3b4a7e | ||
|
|
cc63167307 | ||
|
|
314671b3b7 | ||
|
|
ef83bdb709 | ||
|
|
f9b1b1fe7f | ||
|
|
813c46c86d |
11
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
11
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -8,20 +8,21 @@ labels: 'possible-bug'
|
||||
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
<!--- Tell us what should happen -->
|
||||
|
||||
## Current Behavior
|
||||
|
||||
<!--- Tell us what happens instead of the expected behavior -->
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
<!--- Tell us what you expected happen -->
|
||||
|
||||
## Possible Solution
|
||||
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
<!--- Optional. If familiar with the codebase, suggest a fix/reason for the bug. -->
|
||||
|
||||
## Steps to Reproduce
|
||||
<!--- Steps to reproduce this bug. Include any code, if relevant -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
v16.14.2
|
||||
133
CHANGELOG.md
133
CHANGELOG.md
@@ -1,3 +1,134 @@
|
||||
|
||||
|
||||
## [1.1.19](https://github.com/payloadcms/payload/compare/v1.1.18...v1.1.19) (2022-10-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#1307](https://github.com/payloadcms/payload/issues/1307), [#1321](https://github.com/payloadcms/payload/issues/1321) - bug with disableFormData and blocks field ([2a09f15](https://github.com/payloadcms/payload/commit/2a09f15a158ff30e89c5454f81aa140448f15d30))
|
||||
* [#1311](https://github.com/payloadcms/payload/issues/1311), select existing upload modal always updates state ([e2ec2f7](https://github.com/payloadcms/payload/commit/e2ec2f7b97ed308c4ff7deefbc58cf0df6ff0602))
|
||||
* [#1318](https://github.com/payloadcms/payload/issues/1318), improves popup positioning and logic ([c651835](https://github.com/payloadcms/payload/commit/c6518350617d14818dfc537b5b0a147274c1119b))
|
||||
* custom pino logger options ([#1299](https://github.com/payloadcms/payload/issues/1299)) ([2500026](https://github.com/payloadcms/payload/commit/25000261bd6ecb0f05ae79de9a0693078a0e3e0d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* enforce kebab-case slugs ([#1322](https://github.com/payloadcms/payload/issues/1322)) ([0420b6d](https://github.com/payloadcms/payload/commit/0420b6dc27319ee56d2eed9ab42352f221af3c72))
|
||||
* revert enforce kebab-case slugs ([#1322](https://github.com/payloadcms/payload/issues/1322)) ([#1325](https://github.com/payloadcms/payload/issues/1325)) ([2255ebb](https://github.com/payloadcms/payload/commit/2255ebb64a529b76a54e1e0ab40fba8149244b3d))
|
||||
|
||||
## [1.1.18](https://github.com/payloadcms/payload/compare/v1.1.17...v1.1.18) (2022-10-25)
|
||||
|
||||
## [1.1.17](https://github.com/payloadcms/payload/compare/v1.1.16...v1.1.17) (2022-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#1286](https://github.com/payloadcms/payload/issues/1286), uses defaultDepth in graphql rich text depth ([66bf8c3](https://github.com/payloadcms/payload/commit/66bf8c3cbd080ee5a28b7af521d427d3aae59ba2))
|
||||
* [#1290](https://github.com/payloadcms/payload/issues/1290), renders more than one rich text leaf where applicable ([a9f2f0e](https://github.com/payloadcms/payload/commit/a9f2f0ec03383ef4c3ef3ba98274b0abaaf962ed))
|
||||
* [#1291](https://github.com/payloadcms/payload/issues/1291), add inline relationship drafts ([3967c12](https://github.com/payloadcms/payload/commit/3967c1233fda00b48e9df15276502a6b14b737ff))
|
||||
* enforces depth: 0 in graphql resolvers ([3301f59](https://github.com/payloadcms/payload/commit/3301f598223d517ac310909bb74e455891c27693))
|
||||
* ensures field updates when disableFormData changes ([c929725](https://github.com/payloadcms/payload/commit/c929725dd565de08871dad655442ee9ac4f29dd5))
|
||||
* group + group styles within collapsible ([17dbbc7](https://github.com/payloadcms/payload/commit/17dbbc77757a7cd6e517bac443859561fee86e32))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added beforeLogin hook ([#1289](https://github.com/payloadcms/payload/issues/1289)) ([09d7939](https://github.com/payloadcms/payload/commit/09d793926dbb642bbcb6ab975735d069df355a8a))
|
||||
* adds default max length for text-based fields ([6a1b25a](https://github.com/payloadcms/payload/commit/6a1b25ab302cbdf7f312012b29b78288815810af))
|
||||
* specify node 14+ and yarn classic LTS ([#1240](https://github.com/payloadcms/payload/issues/1240)) ([9181477](https://github.com/payloadcms/payload/commit/91814777b0bf3830c4a468b76783ff6f42ad824a))
|
||||
|
||||
## [1.1.16](https://github.com/payloadcms/payload/compare/v1.1.15...v1.1.16) (2022-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* indexSortableFields not respected ([785b992](https://github.com/payloadcms/payload/commit/785b992c3ea31f7818f1c87c816b8b8de644851d))
|
||||
* obscure bug where upload collection has upload field relating to itself ([36ef378](https://github.com/payloadcms/payload/commit/36ef3789fbe00cafe8b3587d6c370e28efd5a187))
|
||||
|
||||
## [1.1.15](https://github.com/payloadcms/payload/compare/v1.1.14...v1.1.15) (2022-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensures svg mime type is always image/svg+xml ([0b0d971](https://github.com/payloadcms/payload/commit/0b0d9714917b1a56fb899a053e2e35c878a00992))
|
||||
|
||||
## [1.1.14](https://github.com/payloadcms/payload/compare/v1.1.13...v1.1.14) (2022-10-14)
|
||||
|
||||
## [1.1.11](https://github.com/payloadcms/payload/compare/v1.1.10...v1.1.11) (2022-10-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensures arrays and blocks mount as disableFormData: true, fixes [#1242](https://github.com/payloadcms/payload/issues/1242) ([5ca5aba](https://github.com/payloadcms/payload/commit/5ca5abab422ad1cdb1b449a8298f439c57dda464))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* builds beforeDuplicate admin hook, closes [#1243](https://github.com/payloadcms/payload/issues/1243) ([6f6f2f8](https://github.com/payloadcms/payload/commit/6f6f2f8e7b83821ae2f2d30d08460439746cc0c6))
|
||||
|
||||
## [1.1.10](https://github.com/payloadcms/payload/compare/v1.1.9...v1.1.10) (2022-10-11)
|
||||
|
||||
## [1.1.9](https://github.com/payloadcms/payload/compare/v1.1.8...v1.1.9) (2022-10-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improves access control typing ([5322ada](https://github.com/payloadcms/payload/commit/5322ada9e690544c4864abba202a14ec1f2f5e9d))
|
||||
|
||||
## [1.1.8](https://github.com/payloadcms/payload/compare/v1.1.7...v1.1.8) (2022-10-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adds ability to create related docs while editing another ([1e048fe](https://github.com/payloadcms/payload/commit/1e048fe03787577fe4d584cec9c2d7c78bc90a17))
|
||||
* implements use-context-selector for form field access ([5c1a3fa](https://github.com/payloadcms/payload/commit/5c1a3fabeef48b78f173af084f9117515e1297ba))
|
||||
|
||||
## [1.1.7](https://github.com/payloadcms/payload/compare/v1.1.6...v1.1.7) (2022-10-06)
|
||||
|
||||
## [1.1.6](https://github.com/payloadcms/payload/compare/v1.1.5...v1.1.6) (2022-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#1184](https://github.com/payloadcms/payload/issues/1184) ([c2ec54a](https://github.com/payloadcms/payload/commit/c2ec54a7cbd8cd94bcd4a68d885e35986fec7f18))
|
||||
* [#1189](https://github.com/payloadcms/payload/issues/1189) ([3641dfd](https://github.com/payloadcms/payload/commit/3641dfd38a147b24e0e3ef93a125b12ad7763f66))
|
||||
* [#1204](https://github.com/payloadcms/payload/issues/1204) ([b4becd1](https://github.com/payloadcms/payload/commit/b4becd1493d55aae887008ab573ab710c400103a))
|
||||
* [#940](https://github.com/payloadcms/payload/issues/940) ([7926083](https://github.com/payloadcms/payload/commit/7926083732fbaec78d87f67742cdbd8bd00cd48a))
|
||||
* ajusts how disabled states are being set on anchors and buttons ([00ef170](https://github.com/payloadcms/payload/commit/00ef1700ae41e68ff0831a587bf3f09fe6c2c966))
|
||||
* remove min-width from fileupload ([73848b6](https://github.com/payloadcms/payload/commit/73848b603790b3c3d8ad8c9dac81b33c0b65fc7e))
|
||||
* resize textarea only vertically ([6e1dfff](https://github.com/payloadcms/payload/commit/6e1dfff1b8195a1f81e6ea6ccf3b36dd5359c039))
|
||||
* richText e2e test, specific selectors ([09a8144](https://github.com/payloadcms/payload/commit/09a8144f3cc63f7ec15fd75f51b8ac8d0cf3f1b5))
|
||||
* styles readOnly RichTextEditor, removes interactivity within when readOnly ([9181304](https://github.com/payloadcms/payload/commit/918130486e1e38a3d57fb993f466207209c5c0bb))
|
||||
* **style:** system dark scrollbars ([a30d9dc](https://github.com/payloadcms/payload/commit/a30d9dc1d70340cc6c5ac5b3415a6f57bec117ae))
|
||||
* threads readOnly to ReactSelect ([b454811](https://github.com/payloadcms/payload/commit/b454811698c7ea0cee944ed50030c13163cf72c9))
|
||||
* upload xls renaming ext ([7fd8124](https://github.com/payloadcms/payload/commit/7fd8124df68d208813de46172c5cd3f479b9b8be))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* async admin access control ([1cfce87](https://github.com/payloadcms/payload/commit/1cfce8754947487e6c598ed5bc881526295acabf))
|
||||
* sort select and relationship fields by default ([813c46c](https://github.com/payloadcms/payload/commit/813c46c86d86f8b0a3ba7280d31f24e844c916b6))
|
||||
|
||||
## [1.1.5](https://github.com/payloadcms/payload/compare/v1.1.4...v1.1.5) (2022-09-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bug in useThrottledEffect ([3ce8ee4](https://github.com/payloadcms/payload/commit/3ce8ee4661bfa3825c5b8c41232d5da57f7591ed))
|
||||
|
||||
## [1.1.4](https://github.com/payloadcms/payload/compare/v1.1.3...v1.1.4) (2022-09-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* field level access for nested fields ([22ea98c](https://github.com/payloadcms/payload/commit/22ea98ca33770a0ec6652f814726454abb6da24e))
|
||||
* refine type generation for relationships ([ef83bdb](https://github.com/payloadcms/payload/commit/ef83bdb709ebde008b90930a6875b24f042a41b0))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* supports root endpoints ([52cd3b4](https://github.com/payloadcms/payload/commit/52cd3b4a7ed9bc85e93d753a3aaf190489ca98cd))
|
||||
|
||||
## [1.1.3](https://github.com/payloadcms/payload/compare/v1.1.2...v1.1.3) (2022-09-16)
|
||||
|
||||
|
||||
@@ -1934,4 +2065,4 @@ If none of your collections or globals should be publicly exposed, you don't nee
|
||||
* add blind index for encrypting API Keys ([9a1c1f6](https://github.com/payloadcms/payload/commit/9a1c1f64c0ea0066b679195f50e6cb1ac4bf3552))
|
||||
* add license key to access routej ([2565005](https://github.com/payloadcms/payload/commit/2565005cc099797a6e3b8995e0984c28b7837e82))
|
||||
|
||||
## [0.0.137](https://github.com/payloadcms/payload/commit/5c1e2846a2694a80cc8707703406c2ac1bb6af8a) (2020-11-12)
|
||||
## [0.0.137](https://github.com/payloadcms/payload/commit/5c1e2846a2694a80cc8707703406c2ac1bb6af8a) (2020-11-12)
|
||||
@@ -1,12 +1,21 @@
|
||||
export {
|
||||
useForm,
|
||||
/**
|
||||
* @deprecated useWatchForm is no longer preferred. If you need all form fields, prefer `useAllFormFields`.
|
||||
*/
|
||||
useWatchForm,
|
||||
useFormFields,
|
||||
useAllFormFields,
|
||||
useFormSubmitted,
|
||||
useFormProcessing,
|
||||
useFormModified,
|
||||
} from '../dist/admin/components/forms/Form/context';
|
||||
|
||||
export { default as useField } from '../dist/admin/components/forms/useField';
|
||||
|
||||
/**
|
||||
* @deprecated This method is now called useField. The useFieldType alias will be removed in an upcoming version.
|
||||
*/
|
||||
export { default as useFieldType } from '../dist/admin/components/forms/useField';
|
||||
|
||||
export { default as Form } from '../dist/admin/components/forms/Form';
|
||||
@@ -24,5 +33,6 @@ export { default as Submit } from '../dist/admin/components/forms/Submit';
|
||||
export { default as Label } from '../dist/admin/components/forms/Label';
|
||||
|
||||
export { default as reduceFieldsToValues } from '../dist/admin/components/forms/Form/reduceFieldsToValues';
|
||||
export { default as getSiblingData } from '../dist/admin/components/forms/Form/getSiblingData';
|
||||
|
||||
export { default as withCondition } from '../dist/admin/components/forms/withCondition';
|
||||
|
||||
@@ -49,7 +49,9 @@ The directory split up in this way specifically to reduce friction when creating
|
||||
|
||||
The following command will start Payload with your config: `yarn dev my-test-dir`. This command will start up Payload using your config and refresh a test database on every restart.
|
||||
|
||||
NOTE: It is recommended to add the test credentials to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart.
|
||||
When switching between test directories, you will want to remove your `node_modules/.cache ` manually or by running `yarn clean:cache`.
|
||||
|
||||
NOTE: It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@ keywords: overview, access control, permissions, documentation, Content Manageme
|
||||
|
||||
Access control within Payload is extremely powerful while remaining easy and intuitive to manage. Declaring who should have access to what documents is no more complex than writing a simple JavaScript function that either returns a `boolean` or a [`query`](/docs/queries/overview) constraint to restrict which documents users can interact with.
|
||||
|
||||
<YouTube
|
||||
id="DoPLyXG26Dg"
|
||||
title="Overview of Payload Access Control"
|
||||
/>
|
||||
|
||||
**Example use cases:**
|
||||
|
||||
- Allowing anyone `read` access to all `Post`s
|
||||
|
||||
@@ -97,11 +97,51 @@ All Payload fields support the ability to swap in your own React components. So,
|
||||
|
||||
**Fields support the following custom components:**
|
||||
|
||||
| Component | Description |
|
||||
| --------------- | -------------|
|
||||
| **`Filter`** | Override the text input that is presented in the `List` view when a user is filtering documents by the customized field. |
|
||||
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. |
|
||||
| **`Field`** | Swap out the field itself within all `Edit` views. |
|
||||
| Component | Description |
|
||||
| --------------- |------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`Filter`** | Override the text input that is presented in the `List` view when a user is filtering documents by the customized field. |
|
||||
| **`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) |
|
||||
|
||||
## Cell Component
|
||||
|
||||
These are the props that will be passed to your custom Cell to use in your own components.
|
||||
|
||||
| Property | Description |
|
||||
|--------------|-------------------------------------------------------------------|
|
||||
| **`field`** | An object that includes the field configuration. |
|
||||
| **`colIndex`** | A unique number for the column in the list. |
|
||||
| **`collection`** | An object with the config of the collection that the field is in. |
|
||||
| **`cellData`** | The data for the field that the cell represents. |
|
||||
| **`rowData`** | An object with all the field values for the row. |
|
||||
|
||||
#### Example
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import './index.scss';
|
||||
const baseClass = 'custom-cell';
|
||||
|
||||
const CustomCell: React.FC<Props> = (props) => {
|
||||
const {
|
||||
field,
|
||||
colIndex,
|
||||
collection,
|
||||
cellData,
|
||||
rowData,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span className={baseClass}>
|
||||
{ cellData }
|
||||
</span>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Field Component
|
||||
|
||||
When writing your own custom components you can make use of a number of hooks to set data, get reactive changes to other fields, get the id of the document or interact with a context from a custom provider.
|
||||
|
||||
### Sending and receiving values from the form
|
||||
|
||||
@@ -121,51 +161,9 @@ const CustomTextField: React.FC<Props> = ({ path }) => {
|
||||
}
|
||||
```
|
||||
|
||||
### Getting other field values from the form
|
||||
|
||||
There are times when a custom field component needs to have access to data from other fields. This can be done using `getDataByPath` from `useWatchForm` as follows:
|
||||
|
||||
```tsx
|
||||
import { useWatchForm } from 'payload/components/forms';
|
||||
|
||||
const DisplayFee: React.FC = () => {
|
||||
const { getDataByPath } = useWatchForm();
|
||||
|
||||
const amount = getDataByPath('amount');
|
||||
const feePercentage = getDataByPath('feePercentage');
|
||||
|
||||
if (amount && feePercentage) {
|
||||
return (
|
||||
<span>The fee is ${(amount * feePercentage) / 100}</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Getting the document ID
|
||||
|
||||
The document ID can be very useful for certain custom components. You can get the `id` from the `useDocumentInfo` hook. Here is an example of a `UI` field using `id` to link to related collections:
|
||||
|
||||
```tsx
|
||||
import { useDocumentInfo } from 'payload/components/utilities';
|
||||
|
||||
const LinkFromCategoryToPosts: React.FC = () => {
|
||||
// highlight-start
|
||||
const { id } = useDocumentInfo();
|
||||
// highlight-end
|
||||
|
||||
// id will be undefined on the create form
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={`/admin/collections/posts?where[or][0][and][0][category][in][0]=[${id}]`} >
|
||||
View posts
|
||||
</a>
|
||||
)
|
||||
};
|
||||
```
|
||||
<Banner type="success">
|
||||
For more information regarding the hooks that are available to you while you build custom components, including the <strong>useField</strong> hook, <a href="/docs/admin/hooks" style={{color: 'black'}}>click here</a>.
|
||||
</Banner>
|
||||
|
||||
## Custom routes
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Customizing CSS & SCSS
|
||||
label: Customizing CSS
|
||||
order: 30
|
||||
order: 40
|
||||
desc: Customize your Payload admin panel further by adding your own CSS or SCSS style sheet to the configuration, powerful theme and design options are waiting for you.
|
||||
keywords: admin, css, scss, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
271
docs/admin/hooks.mdx
Normal file
271
docs/admin/hooks.mdx
Normal file
@@ -0,0 +1,271 @@
|
||||
---
|
||||
title: React Hooks
|
||||
label: React Hooks
|
||||
order: 30
|
||||
desc: Make use of all of the powerful React hooks that Payload provides.
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
Payload provides a variety of powerful hooks that can be used within your own React components. With them, you can interface with Payload itself and build just about any type of complex customization you can think of—directly in familiar React code.
|
||||
|
||||
### useField
|
||||
|
||||
The `useField` hook is used internally within every applicable Payload field component, and it manages sending and receiving a field's state from its parent form.
|
||||
|
||||
Outside of internal use, its most common use-case is in custom `Field` components. When you build a custom React `Field` component, you'll be responsible for sending and receiving the field's `value` from the form itself. To do so, import the `useField` hook as follows:
|
||||
|
||||
```tsx
|
||||
import { useField } from 'payload/components/forms'
|
||||
|
||||
type Props = { path: string }
|
||||
|
||||
const CustomTextField: React.FC<Props> = ({ path }) => {
|
||||
// highlight-start
|
||||
const { value, setValue } = useField<string>({ path })
|
||||
// highlight-end
|
||||
|
||||
return <input onChange={e => setValue(e.target.value)} value={value.path} />
|
||||
}
|
||||
```
|
||||
|
||||
The `useField` hook accepts an `args` object and sends back information and helpers for you to make use of:
|
||||
|
||||
```ts
|
||||
const field = useField<string>({
|
||||
path: 'fieldPathHere', // required
|
||||
validate: myValidateFunc, // optional
|
||||
disableFormData?: false, // if true, the field's data will be ignored
|
||||
condition?: myConditionHere, // optional, used to skip validation if condition fails
|
||||
})
|
||||
|
||||
// Here is what `useField` sends back
|
||||
const {
|
||||
showError, // whether or not the field should show as errored
|
||||
errorMessage, // the error message to show, if showError
|
||||
value, // the current value of the field from the form
|
||||
formSubmitted, // if the form has been submitted
|
||||
formProcessing, // if the form is currently processing
|
||||
setValue, // method to set the field's value in form state
|
||||
initialValue, // the initial value that the field mounted with
|
||||
} = field;
|
||||
|
||||
// The rest of your component goes here
|
||||
```
|
||||
|
||||
### useFormFields
|
||||
|
||||
There are times when a custom field component needs to have access to data from other fields, and you have a few options to do so. The `useFormFields` hook is a powerful and highly performant way to retrieve a form's field state, as well as to retrieve the `dispatchFields` method, which can be helpful for setting other fields' form states from anywhere within a form.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>This hook is great for retrieving only certain fields from form state</strong> because it ensures that it will only cause a rerender when the items that you ask for change.
|
||||
</Banner>
|
||||
|
||||
Thanks to the awesome package [`use-context-selector`](https://github.com/dai-shi/use-context-selector), you can retrieve a specific field's state easily. This is ideal because you can ensure you have an up-to-date field state, and your component will only re-render when _that field's state_ changes.
|
||||
|
||||
You can pass a Redux-like selector into the hook, which will ensure that you retrieve only the field that you want. The selector takes an argument with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
|
||||
|
||||
```tsx
|
||||
import { useFormFields } from 'payload/components/forms';
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// Get only the `amount` field state, and only cause a rerender when that field changes
|
||||
const amount = useFormFields(([fields, dispatch]) => fields.amount);
|
||||
|
||||
// Do the same thing as above, but to the `feePercentage` field
|
||||
const feePercentage = useFormFields(([fields, dispatch]) => fields.feePercentage);
|
||||
|
||||
if (typeof amount?.value !== 'undefined' && typeof feePercentage?.value !== 'undefined') {
|
||||
return (
|
||||
<span>The fee is ${(amount.value * feePercentage.value) / 100}</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### useAllFormFields
|
||||
|
||||
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Your component will re-render when _any_ field changes, so use this hook only if you absolutely need to. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
|
||||
|
||||
You can do lots of powerful stuff by retrieving the full form state, like using built-in helper functions to reduce field state to values only, or to retrieve sibling data by path.
|
||||
|
||||
```tsx
|
||||
import { useAllFormFields, reduceFieldsToValues, getSiblingData } from 'payload/components/forms';
|
||||
|
||||
const ExampleComponent: React.FC = () => {
|
||||
// the `fields` const will be equal to all fields' state,
|
||||
// and the `dispatchFields` method is usable to send field state up to the form
|
||||
const [fields, dispatchFields] = useAllFormFields();
|
||||
|
||||
// Pass in fields, and indicate if you'd like to "unflatten" field data.
|
||||
// The result below will reflect the data stored in the form at the given time
|
||||
const formData = reduceFieldsToValues(fields, true);
|
||||
|
||||
// Pass in field state and a path,
|
||||
// and you will be sent all sibling data of the path that you've specified
|
||||
const siblingData = getSiblingData(fields, 'someFieldName');
|
||||
|
||||
return (
|
||||
// return some JSX here if necessary
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
##### Updating other fields' values
|
||||
|
||||
If you are building a custom component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useFormFields`.
|
||||
|
||||
You can send the following actions to the `dispatchFields` function.
|
||||
|
||||
| Action | Description |
|
||||
|------------------------|----------------------------------------------------------------------------|
|
||||
| **`ADD_ROW`** | Adds a row of data (useful in array / block field data) |
|
||||
| **`DUPLICATE_ROW`** | Duplicates a row of data (useful in array / block field data) |
|
||||
| **`MODIFY_CONDITION`** | Updates a field's conditional logic result (true / false) |
|
||||
| **`MOVE_ROW`** | Moves a row of data (useful in array / block field data) |
|
||||
| **`REMOVE`** | Removes a field from form state |
|
||||
| **`REMOVE_ROW`** | Removes a row of data from form state (useful in array / block field data) |
|
||||
| **`REPLACE_STATE`** | Completely replaces form state |
|
||||
| **`UPDATE`** | Update any property of a specific field's state |
|
||||
|
||||
To see types for each action supported within the `dispatchFields` hook, check out the Form types [here](https://github.com/payloadcms/payload/blob/master/src/admin/components/forms/Form/types.ts).
|
||||
|
||||
### useForm
|
||||
|
||||
The `useForm` hook can be used to interact with the form itself, and sends back many methods that can be used to reactively fetch form state without causing rerenders within your components each time a field is changed. This is useful if you have action-based callbacks that your components fire, and need to interact with form state _based on a user action_.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Warning:</strong><br/>
|
||||
This hook is optimized to avoid causing rerenders when fields change, and as such, its `fields` property will be out of date. You should only leverage this hook if you need to perform actions against the form in response to your users' actions. Do not rely on its returned "fields" as being up-to-date. They will be removed from this hook's response in an upcoming version.
|
||||
</Banner>
|
||||
|
||||
The `useForm` hook returns an object with the following properties:
|
||||
|
||||
| Action | Description |
|
||||
|----------------------|---------------------------------------------------------------------|
|
||||
| **`fields`** | Deprecated. This property cannot be relied on as up-to-date. |
|
||||
| **`submit`** | Method to trigger the form to submit |
|
||||
| **`dispatchFields`** | Dispatch actions to the form field state |
|
||||
| **`validateForm`** | Trigger a validation of the form state |
|
||||
| **`createFormData`** | Create a `multipart/form-data` object from the current form's state |
|
||||
| **`disabled`** | Boolean denoting whether or not the form is disabled |
|
||||
| **`getFields`** | Gets all fields from state |
|
||||
| **`getField`** | Gets a single field from state by path |
|
||||
| **`getData`** | Returns the data stored in the form |
|
||||
| **`getSiblingData`** | Returns form sibling data for the given field path |
|
||||
| **`setModified`** | Set the form's `modified` state |
|
||||
| **`setProcessing`** | Set the form's `processing` state |
|
||||
| **`setSubmitted`** | Set the form's `submitted` state |
|
||||
| **`formRef`** | The ref from the form HTML element |
|
||||
| **`reset`** | Method to reset the form to its initial state |
|
||||
|
||||
### useDocumentInfo
|
||||
|
||||
The `useDocumentInfo` hook provides lots of information about the document currently being edited, including the following:
|
||||
|
||||
| Property | Description |
|
||||
|---------------------------|------------------------------------------------------------------------------------|
|
||||
| **`collection`** | If the doc is a collection, its collection config will be returned |
|
||||
| **`global`** | If the doc is a global, its global config will be returned |
|
||||
| **`type`** | The type of document being edited (collection or global) |
|
||||
| **`id`** | If the doc is a collection, its ID will be returned |
|
||||
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences |
|
||||
| **`versions`** | Versions of the current doc |
|
||||
| **`unpublishedVersions`** | Unpublished versions of the current doc |
|
||||
| **`publishedDoc`** | The currently published version of the doc being edited |
|
||||
| **`getVersions`** | Method to trigger the retrieval of document versions |
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
import { useDocumentInfo } from 'payload/components/utilities';
|
||||
|
||||
const LinkFromCategoryToPosts: React.FC = () => {
|
||||
// highlight-start
|
||||
const { id } = useDocumentInfo();
|
||||
// highlight-end
|
||||
|
||||
// id will be undefined on the create form
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={`/admin/collections/posts?where[or][0][and][0][category][in][0]=[${id}]`} >
|
||||
View posts
|
||||
</a>
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
### useLocale
|
||||
|
||||
In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example:
|
||||
|
||||
```tsx
|
||||
import { useLocale } from 'payload/components/utilities';
|
||||
|
||||
const Greeting: React.FC = () => {
|
||||
// highlight-start
|
||||
const locale = useLocale();
|
||||
// highlight-end
|
||||
|
||||
const trans = {
|
||||
en: 'Hello',
|
||||
es: 'Hola',
|
||||
};
|
||||
|
||||
return (
|
||||
<span> { trans[locale] } </span>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### useAuth
|
||||
|
||||
Useful to retrieve info about the currently logged in user as well as methods for interacting with it. It sends back an object with the following properties:
|
||||
|
||||
| Property | Description |
|
||||
|---------------------|-----------------------------------------------------------------------------------------|
|
||||
| **`user`** | The currently logged in user |
|
||||
| **`logOut`** | A method to log out the currently logged in user |
|
||||
| **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token |
|
||||
| **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory |
|
||||
| **`token`** | The logged in user's token (useful for creating preview links, etc.) |
|
||||
| **`permissions`** | The permissions of the current user |
|
||||
|
||||
```tsx
|
||||
import { useAuth } from 'payload/components/utilities';
|
||||
import { User } from '../payload-types.ts';
|
||||
|
||||
const Greeting: React.FC = () => {
|
||||
// highlight-start
|
||||
const { user } = useConfig<User>();
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<span>Hi, {user.email}!</span>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### useConfig
|
||||
|
||||
Used to easily fetch the full Payload config.
|
||||
|
||||
```tsx
|
||||
import { useConfig } from 'payload/components/utilities';
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const config = useConfig();
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<span>{config.serverURL}</span>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### usePreferences
|
||||
|
||||
Returns methods to set and get user preferences. More info can be found [here](https://payloadcms.com/docs/admin/preferences).
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Managing User Preferences
|
||||
label: Preferences
|
||||
order: 40
|
||||
order: 50
|
||||
desc: Store the preferences of your users as they interact with the Admin panel.
|
||||
keywords: admin, preferences, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Webpack
|
||||
label: Webpack
|
||||
order: 50
|
||||
order: 60
|
||||
desc: The Payload admin panel uses Webpack 5 and supports many common functionalities such as SCSS and Typescript out of the box to give you more freedom.
|
||||
keywords: admin, webpack, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
@@ -155,6 +155,11 @@ export default {};
|
||||
|
||||
Now, when Webpack sees that you're attempting to import your `createStripeSubscriptionPath` file, it'll disregard that actual file and load your mock file instead. Not only will your Admin panel now bundle successfully, you will have optimized its filesize by removing unnecessary code! And you might have learned something about Webpack, too.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong><br/>
|
||||
If changes to your Webpack aliases are not surfacing, they might be [cached](https://webpack.js.org/configuration/cache/) in `node_modules/.cache/webpack`. Try deleting that folder and restarting your server.
|
||||
</Banner>
|
||||
|
||||
## Admin environment vars
|
||||
|
||||
<Banner type="warning">
|
||||
|
||||
@@ -57,6 +57,8 @@ const response = await fetch("http://localhost:3000/api/pages", {
|
||||
});
|
||||
```
|
||||
|
||||
Note: The label portion of the header is case-sensitive and will likely have a capitalized first character unless the label has been customized.
|
||||
|
||||
### Forgot Password
|
||||
|
||||
You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property:
|
||||
|
||||
@@ -59,17 +59,18 @@ You can find an assortment of [example collection configs](https://github.com/pa
|
||||
|
||||
You can customize the way that the Admin panel behaves on a collection-by-collection basis by defining the `admin` property on a collection's config.
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------------- | -------------|
|
||||
| `group` | Text used as a label for grouping collection links together in the navigation. |
|
||||
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |
|
||||
| `description` | Text or React component to display below the Collection label in the List view to give editors more information. |
|
||||
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this collection's List view. |
|
||||
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------|
|
||||
| `group` | Text used as a label for grouping collection links together in the navigation. |
|
||||
| `hooks` | Admin-specific hooks for this collection. [More](#admin-hooks) |
|
||||
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |
|
||||
| `description` | Text or React component to display below the Collection label in the List view to give editors more information. |
|
||||
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this collection's List view. |
|
||||
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
|
||||
| `enableRichTextRelationship` | The [Rich Text](/docs/fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `preview` | Function to generate preview URLS within the Admin panel that can point to your app. [More](#preview). |
|
||||
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
|
||||
| `listSearchableFields ` | Specify which fields should be searched in the List search view. [More](/docs/configuration/collections#list-searchable-fields) |
|
||||
| `preview` | Function to generate preview URLS within the Admin panel that can point to your app. [More](#preview). |
|
||||
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
|
||||
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More](#list-searchable-fields) |
|
||||
|
||||
### Preview
|
||||
|
||||
@@ -120,7 +121,7 @@ Hooks are a powerful way to extend collection functionality and execute your own
|
||||
|
||||
Collections support all field types that Payload has to offer—including simple fields like text and checkboxes all the way to more complicated layout-building field groups like Blocks. [Click here](/docs/fields/overview) to learn more about field types.
|
||||
|
||||
#### List Searchable Fields
|
||||
### List Searchable Fields
|
||||
|
||||
In the List view, there is a "search" box that allows you to quickly find a document with a search. By default, it searches on the ID field. If you have `admin.useAsTitle` defined, the list search will use that field. However, you can define more than one field to search to make it easier on your admin editors to find the data they need.
|
||||
|
||||
@@ -128,9 +129,53 @@ For example, let's say you have a Posts collection with `title`, `metaDescriptio
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong><br/>
|
||||
If you are adding <strong>listSearchableFields</strong>, make sure you index each of these fields so your admin queries can remain performant.
|
||||
If you are adding <strong>listSearchableFields</strong>, make sure you index each of these fields so your admin queries can remain performant.
|
||||
</Banner>
|
||||
|
||||
### Admin Hooks
|
||||
|
||||
In addition to collection hooks themselves, Payload provides for admin UI-specific hooks that you can leverage.
|
||||
|
||||
**`beforeDuplicate`**
|
||||
|
||||
The `beforeDuplicate` hook is an async function that accepts an object containing the data to duplicate, as well as the locale of the doc to duplicate. Within this hook, you can modify the data to be duplicated, which is useful in cases where you have unique fields that need to be incremented or similar, as well as if you want to automatically modify a document's `title`.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import { BeforeDuplicate, CollectionConfig } from 'payload/types';
|
||||
// Your auto-generated Page type
|
||||
import { Page } from '../payload-types.ts';
|
||||
|
||||
const beforeDuplicate: BeforeDuplicate<Page> = ({ data }) => {
|
||||
return {
|
||||
...data,
|
||||
title: `${data.title} Copy`,
|
||||
uniqueField: data.uniqueField ? `${data.uniqueField}-copy` : '',
|
||||
};
|
||||
};
|
||||
|
||||
export const Page: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
hooks: {
|
||||
beforeDuplicate,
|
||||
}
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'uniqueField',
|
||||
type: 'text',
|
||||
unique: true,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
You can import collection types as follows:
|
||||
|
||||
@@ -23,7 +23,7 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
|
||||
@@ -41,7 +41,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
|
||||
|
||||
**`layout`**
|
||||
|
||||
The `layout` property allows for the radio group to be styled as a horizonally or vertically distributed list.
|
||||
The `layout` property allows for the radio group to be styled as a horizonally or vertically distributed list. The default value is `horizontal`.
|
||||
|
||||
### Example
|
||||
|
||||
@@ -65,7 +65,7 @@ const ExampleCollection: CollectionConfig = {
|
||||
value: 'dark_gray',
|
||||
},
|
||||
],
|
||||
defaultValue: 'option_1',
|
||||
defaultValue: 'mint', // The first value in options.
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
}
|
||||
|
||||
@@ -174,6 +174,20 @@ const ExampleCollection: CollectionConfig = {
|
||||
]
|
||||
}
|
||||
],
|
||||
link: {
|
||||
// Inject your own fields into the Link element
|
||||
fields: [
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: [
|
||||
'noopener', 'noreferrer', 'nofollow',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
upload: {
|
||||
collections: {
|
||||
media: {
|
||||
|
||||
@@ -23,12 +23,12 @@ With this field, you can also inject custom `Cell` components that appear as add
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------------- | ----------- |
|
||||
| **`name`** * | A unique identifier for this field. |
|
||||
| **`label`** | Human-readable label for this UI field. |
|
||||
| **`admin.components.Field`** | React component to be rendered for this field within the Edit view. |
|
||||
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. |
|
||||
| Option | Description |
|
||||
| ---------------------------- |-------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** * | A unique identifier for this field. |
|
||||
| **`label`** | Human-readable label for this UI field. |
|
||||
| **`admin.components.Field`** | React component to be rendered for this field within the Edit view. [More](/admin/components/#field-component) |
|
||||
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More](/admin/components/#field-component) |
|
||||
|
||||
*\* An asterisk denotes that a property is required.*
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ const afterDeleteHook: CollectionAfterDeleteHook = async ({
|
||||
|
||||
### beforeLogin
|
||||
|
||||
For auth-enabled Collections, this hook runs after successful `login` operations. You can optionally modify the user that is returned.
|
||||
For auth-enabled Collections, this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation.
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeLoginHook } from 'payload/types';
|
||||
@@ -198,7 +198,6 @@ import { CollectionBeforeLoginHook } from 'payload/types';
|
||||
const beforeLoginHook: CollectionBeforeLoginHook = async ({
|
||||
req, // full express request
|
||||
user, // user being logged in
|
||||
token, // user token
|
||||
}) => {
|
||||
return user;
|
||||
}
|
||||
@@ -213,6 +212,8 @@ import { CollectionAfterLoginHook } from 'payload/types';
|
||||
|
||||
const afterLoginHook: CollectionAfterLoginHook = async ({
|
||||
req, // full express request
|
||||
user, // user that was logged in
|
||||
token, // user token
|
||||
}) => {...}
|
||||
```
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ const result = await payload.updateGlobal({
|
||||
|
||||
## TypeScript
|
||||
|
||||
Local API calls also support passing in a generic. This is especially useful if you generate your TS types using a [generate types script](/docs/typescript/generate-types).
|
||||
Local API calls also support passing in a generic. This is especially useful if you generate your TS types using a [generate types script](/docs/typescript/generating-types).
|
||||
|
||||
Here is an example of usage:
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Writing plugins is no more complex than writing regular JavaScript. If you know
|
||||
|
||||
### How to install plugins
|
||||
|
||||
The base Payload config allows for a `plugins` property which takes an `array` of [`Plugin`s](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21).
|
||||
The base Payload config allows for a `plugins` property which takes an `array` of [`Plugins`](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21).
|
||||
|
||||
```js
|
||||
import { buildConfig } from 'payload/config';
|
||||
@@ -134,7 +134,7 @@ const addLastModified: Plugin = (incomingConfig: Config): Config => {
|
||||
export default addLastModified;
|
||||
```
|
||||
|
||||
#### Available Plugins
|
||||
### Available Plugins
|
||||
|
||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
|
||||
|
||||
|
||||
@@ -22,15 +22,17 @@ All Payload API routes are mounted prefixed to your config's `routes.api` URL se
|
||||
|
||||
Each collection is mounted using its `slug` value. For example, if a collection's slug is `users`, all corresponding routes will be mounted on `/api/users`.
|
||||
|
||||
Note: Collection slugs must be formatted in kebab-case
|
||||
|
||||
**All CRUD operations are exposed as follows:**
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | --------------------------- | -------------------------------------- |
|
||||
| `GET` | `/api/{collectionSlug}` | Find paginated documents |
|
||||
| `GET` | `/api/{collectionSlug}/:id` | Find a specific document by ID |
|
||||
| `POST` | `/api/{collectionSlug}` | Create a new document |
|
||||
| `PATCH` | `/api/{collectionSlug}/:id` | Update a document by ID |
|
||||
| `DELETE` | `/api/{collectionSlug}/:id` | Delete an existing document by ID |
|
||||
| `GET` | `/api/{collection-slug}` | Find paginated documents |
|
||||
| `GET` | `/api/{collection-slug}/:id` | Find a specific document by ID |
|
||||
| `POST` | `/api/{collection-slug}` | Create a new document |
|
||||
| `PATCH` | `/api/{collection-slug}/:id` | Update a document by ID |
|
||||
| `DELETE` | `/api/{collection-slug}/:id` | Delete an existing document by ID |
|
||||
|
||||
##### Additional `find` query parameters
|
||||
|
||||
@@ -47,14 +49,14 @@ Auth enabled collections are also given the following endpoints:
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | --------------------------- | ----------- |
|
||||
| `POST` | `/api/{collectionSlug}/verify/:token` | [Email verification](/docs/authentication/operations#verify-by-email), if enabled. |
|
||||
| `POST` | `/api/{collectionSlug}/unlock` | [Unlock a user's account](/docs/authentication/operations#unlock), if enabled. |
|
||||
| `POST` | `/api/{collectionSlug}/login` | [Logs in](/docs/authentication/operations#login) a user with email / password. |
|
||||
| `POST` | `/api/{collectionSlug}/logout` | [Logs out](/docs/authentication/operations#logout) a user. |
|
||||
| `POST` | `/api/{collectionSlug}/refresh-token` | [Refreshes a token](/docs/authentication/operations#refresh) that has not yet expired. |
|
||||
| `GET` | `/api/{collectionSlug}/me` | [Returns the currently logged in user with token](/docs/authentication/operations#me). |
|
||||
| `POST` | `/api/{collectionSlug}/forgot-password` | [Password reset workflow](/docs/authentication/operations#forgot-password) entry point. |
|
||||
| `POST` | `/api/{collectionSlug}/reset-password` | [To reset the user's password](/docs/authentication/operations#reset-password). |
|
||||
| `POST` | `/api/{collection-slug}/verify/:token` | [Email verification](/docs/authentication/operations#verify-by-email), if enabled. |
|
||||
| `POST` | `/api/{collection-slug}/unlock` | [Unlock a user's account](/docs/authentication/operations#unlock), if enabled. |
|
||||
| `POST` | `/api/{collection-slug}/login` | [Logs in](/docs/authentication/operations#login) a user with email / password. |
|
||||
| `POST` | `/api/{collection-slug}/logout` | [Logs out](/docs/authentication/operations#logout) a user. |
|
||||
| `POST` | `/api/{collection-slug}/refresh-token` | [Refreshes a token](/docs/authentication/operations#refresh) that has not yet expired. |
|
||||
| `GET` | `/api/{collection-slug}/me` | [Returns the currently logged in user with token](/docs/authentication/operations#me). |
|
||||
| `POST` | `/api/{collection-slug}/forgot-password` | [Password reset workflow](/docs/authentication/operations#forgot-password) entry point. |
|
||||
| `POST` | `/api/{collection-slug}/reset-password` | [To reset the user's password](/docs/authentication/operations#reset-password). |
|
||||
|
||||
## Globals
|
||||
|
||||
@@ -86,6 +88,7 @@ Each endpoint object needs to have:
|
||||
| **`path`** | A string for the endpoint route after the collection or globals slug |
|
||||
| **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' |
|
||||
| **`handler`** | A function or array of functions to be called with **req**, **res** and **next** arguments. [Express](https://expressjs.com/en/guide/routing.html#route-handlers) |
|
||||
| **`root`** | When `true`, defines the endpoint on the root Express app, bypassing Payload handlers and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload config, endpoints defined on `collections` or `globals` cannot be root. |
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
18
package.json
18
package.json
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.19",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"yarn": ">=1.22 <2"
|
||||
},
|
||||
"author": {
|
||||
"email": "info@payloadcms.com",
|
||||
"name": "Payload CMS",
|
||||
@@ -41,6 +45,7 @@
|
||||
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:components": "cross-env jest --config=jest.components.config.js",
|
||||
"clean:cache": "rimraf node_modules/.cache",
|
||||
"clean": "rimraf dist",
|
||||
"release": "release-it",
|
||||
"release:patch": "release-it patch",
|
||||
@@ -85,7 +90,7 @@
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@babel/register": "^7.11.5",
|
||||
"@date-io/date-fns": "^2.10.6",
|
||||
"@faceless-ui/modal": "^2.0.0-alpha.4",
|
||||
"@faceless-ui/modal": "^2.0.1",
|
||||
"@faceless-ui/scroll-info": "^1.2.3",
|
||||
"@faceless-ui/window-info": "^2.0.2",
|
||||
"@types/is-plain-object": "^2.0.4",
|
||||
@@ -169,9 +174,9 @@
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-toastify": "^8.2.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sass": "^1.52.1",
|
||||
"sass": "^1.55.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sharp": "^0.29.3",
|
||||
"sharp": "^0.31.1",
|
||||
"slate": "^0.72.8",
|
||||
"slate-history": "^0.66.0",
|
||||
"slate-hyperscript": "^0.66.0",
|
||||
@@ -180,6 +185,7 @@
|
||||
"terser-webpack-plugin": "^5.0.3",
|
||||
"ts-essentials": "^7.0.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"use-context-selector": "^1.4.1",
|
||||
"uuid": "^8.1.0",
|
||||
"webpack": "^5.6.0",
|
||||
"webpack-bundle-analyzer": "^4.4.1",
|
||||
@@ -189,7 +195,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.23.1",
|
||||
"@release-it/conventional-changelog": "^2.0.0",
|
||||
"@release-it/conventional-changelog": "^5.1.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^13.0.1",
|
||||
"@trbl/eslint-config": "^1.2.4",
|
||||
@@ -274,7 +280,7 @@
|
||||
"mongodb-memory-server": "^7.2.0",
|
||||
"nodemon": "^2.0.6",
|
||||
"passport-strategy": "^1.0.0",
|
||||
"release-it": "^14.2.2",
|
||||
"release-it": "^15.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"serve-static": "^1.14.2",
|
||||
"shelljs": "^0.8.5",
|
||||
|
||||
@@ -3,15 +3,16 @@ import qs from 'qs';
|
||||
export const requests = {
|
||||
get: (url: string, params: unknown = {}): Promise<Response> => {
|
||||
const query = qs.stringify(params, { addQueryPrefix: true });
|
||||
return fetch(`${url}${query}`);
|
||||
return fetch(`${url}${query}`, { credentials: 'include' });
|
||||
},
|
||||
|
||||
post: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
|
||||
const headers = options && options.headers ? { ...options.headers } : {};
|
||||
|
||||
const formattedOptions = {
|
||||
const formattedOptions: RequestInit = {
|
||||
...options,
|
||||
method: 'post',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
@@ -23,9 +24,10 @@ export const requests = {
|
||||
put: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
|
||||
const headers = options && options.headers ? { ...options.headers } : {};
|
||||
|
||||
const formattedOptions = {
|
||||
const formattedOptions: RequestInit = {
|
||||
...options,
|
||||
method: 'put',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
@@ -37,9 +39,10 @@ export const requests = {
|
||||
patch: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
|
||||
const headers = options && options.headers ? { ...options.headers } : {};
|
||||
|
||||
const formattedOptions = {
|
||||
const formattedOptions: RequestInit = {
|
||||
...options,
|
||||
method: 'PATCH',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
@@ -50,12 +53,16 @@ export const requests = {
|
||||
|
||||
delete: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
|
||||
const headers = options && options.headers ? { ...options.headers } : {};
|
||||
return fetch(url, {
|
||||
|
||||
const formattedOptions: RequestInit = {
|
||||
...options,
|
||||
method: 'delete',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return fetch(url, formattedOptions);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useWatchForm, useFormModified } from '../../forms/Form/context';
|
||||
import { useFormModified, useAllFormFields } from '../../forms/Form/context';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { Props } from './types';
|
||||
import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues';
|
||||
@@ -17,7 +17,7 @@ const baseClass = 'autosave';
|
||||
const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdatedAt }) => {
|
||||
const { serverURL, routes: { api, admin } } = useConfig();
|
||||
const { versions, getVersions } = useDocumentInfo();
|
||||
const { fields, dispatchFields } = useWatchForm();
|
||||
const [fields] = useAllFormFields();
|
||||
const modified = useFormModified();
|
||||
const locale = useLocale();
|
||||
const { replace } = useHistory();
|
||||
@@ -39,6 +39,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
const createCollectionDoc = useCallback(async () => {
|
||||
const res = await fetch(`${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&draft=true`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -94,6 +95,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
setTimeout(async () => {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -112,7 +114,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
};
|
||||
|
||||
autosave();
|
||||
}, [debouncedFields, modified, serverURL, api, collection, global, id, dispatchFields, getVersions, locale]);
|
||||
}, [debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (versions?.docs?.[0]) {
|
||||
|
||||
@@ -37,7 +37,15 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
locale,
|
||||
depth: 0,
|
||||
});
|
||||
const data = await response.json();
|
||||
let data = await response.json();
|
||||
|
||||
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
|
||||
data = await collection.admin.hooks.beforeDuplicate({
|
||||
data,
|
||||
locale,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await requests.post(`${serverURL}${api}/${slug}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -65,7 +73,15 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
locale,
|
||||
depth: 0,
|
||||
});
|
||||
const localizedDoc = await res.json();
|
||||
let localizedDoc = await res.json();
|
||||
|
||||
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
|
||||
localizedDoc = await collection.admin.hooks.beforeDuplicate({
|
||||
data: localizedDoc,
|
||||
locale,
|
||||
});
|
||||
}
|
||||
|
||||
const patchResult = await requests.patch(`${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -97,7 +113,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
pathname: `${admin}/collections/${slug}/${duplicateID}`,
|
||||
});
|
||||
}, 10);
|
||||
}, [modified, localization, collection.labels.singular, setModified, toggleModal, modalSlug, serverURL, api, slug, id, push, admin]);
|
||||
}, [modified, localization, collection, setModified, toggleModal, modalSlug, serverURL, api, slug, id, push, admin]);
|
||||
|
||||
const confirm = useCallback(async () => {
|
||||
setHasClicked(false);
|
||||
|
||||
@@ -102,7 +102,7 @@ const FileDetails: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<Meta
|
||||
{...val}
|
||||
mimeType={mimeType}
|
||||
mimeType={val.mimeType}
|
||||
staticURL={staticURL}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: base(9);
|
||||
width: var(--nav-width);
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
|
||||
@@ -120,10 +120,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include large-break {
|
||||
width: base(8);
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
@include blur-bg;
|
||||
position: fixed;
|
||||
|
||||
@@ -2,9 +2,10 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useWindowInfo } from '@faceless-ui/window-info';
|
||||
import { Props } from './types';
|
||||
import PopupButton from './PopupButton';
|
||||
import useIntersect from '../../../hooks/useIntersect';
|
||||
|
||||
import './index.scss';
|
||||
import useIntersect from '../../../hooks/useIntersect';
|
||||
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
||||
|
||||
const baseClass = 'popup';
|
||||
|
||||
@@ -41,26 +42,20 @@ const Popup: React.FC<Props> = (props) => {
|
||||
const [verticalAlign, setVerticalAlign] = useState(verticalAlignFromProps);
|
||||
const [horizontalAlign, setHorizontalAlign] = useState(horizontalAlignFromProps);
|
||||
|
||||
const handleClickOutside = useCallback((e) => {
|
||||
if (contentRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActive(false);
|
||||
}, [contentRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const setPosition = useCallback(({ horizontal = false, vertical = false }) => {
|
||||
if (contentRef.current) {
|
||||
const bounds = contentRef.current.getBoundingClientRect();
|
||||
|
||||
const {
|
||||
left: contentLeftPos,
|
||||
right: contentRightPos,
|
||||
top: contentTopPos,
|
||||
bottom: contentBottomPos,
|
||||
} = contentRef.current.getBoundingClientRect();
|
||||
} = bounds;
|
||||
|
||||
let boundingTopPos = 100;
|
||||
let boundingRightPos = windowWidth;
|
||||
let boundingBottomPos = windowHeight;
|
||||
let boundingRightPos = window.innerWidth;
|
||||
let boundingBottomPos = window.innerHeight;
|
||||
let boundingLeftPos = 0;
|
||||
|
||||
if (boundingRef?.current) {
|
||||
@@ -72,19 +67,39 @@ const Popup: React.FC<Props> = (props) => {
|
||||
} = boundingRef.current.getBoundingClientRect());
|
||||
}
|
||||
|
||||
if (contentRightPos > boundingRightPos && contentLeftPos > boundingLeftPos) {
|
||||
setHorizontalAlign('right');
|
||||
} else if (contentLeftPos < boundingLeftPos && contentRightPos < boundingRightPos) {
|
||||
setHorizontalAlign('left');
|
||||
if (horizontal) {
|
||||
if (contentRightPos > boundingRightPos && contentLeftPos > boundingLeftPos) {
|
||||
setHorizontalAlign('right');
|
||||
} else if (contentLeftPos < boundingLeftPos && contentRightPos < boundingRightPos) {
|
||||
setHorizontalAlign('left');
|
||||
}
|
||||
}
|
||||
|
||||
if (contentTopPos < boundingTopPos && contentBottomPos < boundingBottomPos) {
|
||||
setVerticalAlign('bottom');
|
||||
} else if (contentBottomPos > boundingBottomPos && contentTopPos > boundingTopPos) {
|
||||
setVerticalAlign('top');
|
||||
if (vertical) {
|
||||
if (contentTopPos < boundingTopPos && contentBottomPos < boundingBottomPos) {
|
||||
setVerticalAlign('bottom');
|
||||
} else if (contentBottomPos > boundingBottomPos && contentTopPos > boundingTopPos) {
|
||||
setVerticalAlign('top');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [boundingRef, intersectionEntry, windowHeight, windowWidth]);
|
||||
}, [boundingRef]);
|
||||
|
||||
const handleClickOutside = useCallback((e) => {
|
||||
if (contentRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActive(false);
|
||||
}, [contentRef]);
|
||||
|
||||
useEffect(() => {
|
||||
setPosition({ horizontal: true });
|
||||
}, [intersectionEntry, setPosition, windowWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
setPosition({ vertical: true });
|
||||
}, [intersectionEntry, setPosition, windowHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof onToggleOpen === 'function') onToggleOpen(active);
|
||||
|
||||
@@ -116,4 +116,8 @@ div.react-select {
|
||||
background-color: var(--theme-error-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.rs--is-disabled .rs__control {
|
||||
background: var(--theme-elevation-200);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import IDLabel from '../IDLabel';
|
||||
|
||||
const baseClass = 'render-title';
|
||||
|
||||
const RenderTitle : React.FC<Props> = (props) => {
|
||||
const RenderTitle: React.FC<Props> = (props) => {
|
||||
const {
|
||||
useAsTitle,
|
||||
title: titleFromProps,
|
||||
@@ -14,10 +14,8 @@ const RenderTitle : React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
|
||||
const titleFromForm = useTitle(useAsTitle);
|
||||
const titleFromData = data && data[useAsTitle];
|
||||
|
||||
let title = titleFromData;
|
||||
if (!title) title = titleFromForm;
|
||||
let title = titleFromForm;
|
||||
if (!title) title = data?.id;
|
||||
if (!title) title = fallback;
|
||||
title = titleFromProps || title;
|
||||
|
||||
@@ -68,8 +68,9 @@ const SearchFilter: React.FC<Props> = (props) => {
|
||||
where: newWhere,
|
||||
}),
|
||||
});
|
||||
setPreviousSearch(debouncedSearch);
|
||||
}
|
||||
|
||||
setPreviousSearch(debouncedSearch);
|
||||
}
|
||||
}, [debouncedSearch, previousSearch, history, fieldName, params, handleChange, modifySearchQuery, listSearchableFields]);
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
const fieldToSearch = collection?.admin?.useAsTitle || 'id';
|
||||
const searchParam = searchArg ? `&where[${fieldToSearch}][like]=${searchArg}` : '';
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`);
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`, { credentials: 'include' });
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs<any> = await response.json();
|
||||
@@ -152,7 +152,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
|
||||
const addOptionByID = useCallback(async (id, relation) => {
|
||||
if (!errorLoading && id !== 'null') {
|
||||
const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`);
|
||||
const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`, { credentials: 'include' });
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import { Context } from './types';
|
||||
import { useContextSelector, createContext as createSelectorContext, useContext as useFullContext } from 'use-context-selector';
|
||||
import { Context, FormFieldsContext as FormFieldsContextType } from './types';
|
||||
|
||||
const FormContext = createContext({} as Context);
|
||||
const FormWatchContext = createContext({} as Context);
|
||||
const SubmittedContext = createContext(false);
|
||||
const ProcessingContext = createContext(false);
|
||||
const ModifiedContext = createContext(false);
|
||||
const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null]);
|
||||
|
||||
const useForm = (): Context => useContext(FormContext);
|
||||
const useWatchForm = (): Context => useContext(FormWatchContext);
|
||||
@@ -13,15 +15,21 @@ const useFormSubmitted = (): boolean => useContext(SubmittedContext);
|
||||
const useFormProcessing = (): boolean => useContext(ProcessingContext);
|
||||
const useFormModified = (): boolean => useContext(ModifiedContext);
|
||||
|
||||
const useFormFields = <Value = unknown>(selector: (context: FormFieldsContextType) => Value): Value => useContextSelector(FormFieldsContext, selector);
|
||||
const useAllFormFields = (): FormFieldsContextType => useFullContext(FormFieldsContext);
|
||||
|
||||
export {
|
||||
FormContext,
|
||||
FormWatchContext,
|
||||
SubmittedContext,
|
||||
ProcessingContext,
|
||||
ModifiedContext,
|
||||
useForm,
|
||||
useWatchForm,
|
||||
useFormSubmitted,
|
||||
useFormProcessing,
|
||||
useFormModified,
|
||||
useForm,
|
||||
FormContext,
|
||||
FormFieldsContext,
|
||||
useFormFields,
|
||||
useAllFormFields,
|
||||
FormWatchContext,
|
||||
useWatchForm,
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@ import { unflatten, flatten } from 'flatley';
|
||||
import flattenFilters from './flattenFilters';
|
||||
import getSiblingData from './getSiblingData';
|
||||
import reduceFieldsToValues from './reduceFieldsToValues';
|
||||
import { Fields } from './types';
|
||||
import { Field, FieldAction, Fields } from './types';
|
||||
import deepCopyObject from '../../../../utilities/deepCopyObject';
|
||||
|
||||
const unflattenRowsFromState = (state: Fields, path) => {
|
||||
const unflattenRowsFromState = (state: Fields, path: string) => {
|
||||
// Take a copy of state
|
||||
const remainingFlattenedState = { ...state };
|
||||
|
||||
@@ -37,7 +37,7 @@ const unflattenRowsFromState = (state: Fields, path) => {
|
||||
};
|
||||
};
|
||||
|
||||
function fieldReducer(state: Fields, action): Fields {
|
||||
function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
switch (action.type) {
|
||||
case 'REPLACE_STATE': {
|
||||
const newState = {};
|
||||
@@ -186,23 +186,27 @@ function fieldReducer(state: Fields, action): Fields {
|
||||
}, {});
|
||||
}
|
||||
|
||||
default: {
|
||||
const newField = {
|
||||
value: action.value,
|
||||
valid: action.valid,
|
||||
errorMessage: action.errorMessage,
|
||||
disableFormData: action.disableFormData,
|
||||
initialValue: action.initialValue,
|
||||
validate: action.validate,
|
||||
condition: action.condition,
|
||||
passesCondition: action.passesCondition,
|
||||
};
|
||||
case 'UPDATE': {
|
||||
const newField = Object.entries(action).reduce((field, [key, value]) => {
|
||||
if (['value', 'valid', 'errorMessage', 'disableFormData', 'initialValue', 'validate', 'condition', 'passesCondition'].includes(key)) {
|
||||
return {
|
||||
...field,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
|
||||
return field;
|
||||
}, state[action.path] || {} as Field);
|
||||
|
||||
return {
|
||||
...state,
|
||||
[action.path]: newField,
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Field } from '../../../../fields/config/types';
|
||||
import buildInitialState from './buildInitialState';
|
||||
import errorMessages from './errorMessages';
|
||||
import { Context as FormContextType, GetDataByPath, Props, SubmitOptions } from './types';
|
||||
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FormWatchContext } from './context';
|
||||
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FormFieldsContext, FormWatchContext } from './context';
|
||||
import buildStateFromSchema from './buildStateFromSchema';
|
||||
import { useOperation } from '../../utilities/OperationProvider';
|
||||
|
||||
@@ -63,12 +63,11 @@ const Form: React.FC<Props> = (props) => {
|
||||
if (formattedInitialData) initialFieldState = formattedInitialData;
|
||||
if (initialState) initialFieldState = initialState;
|
||||
|
||||
// Allow access to initialState for field types such as Blocks and Array
|
||||
contextRef.current.initialState = initialState;
|
||||
|
||||
const [fields, dispatchFields] = useReducer(fieldReducer, {}, () => initialFieldState);
|
||||
const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState);
|
||||
const [fields, dispatchFields] = fieldsReducer;
|
||||
|
||||
contextRef.current.fields = fields;
|
||||
contextRef.current.dispatchFields = dispatchFields;
|
||||
|
||||
const validateForm = useCallback(async () => {
|
||||
const validatedFieldState = {};
|
||||
@@ -111,7 +110,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}, [contextRef, id, user, operation]);
|
||||
}, [contextRef, id, user, operation, dispatchFields]);
|
||||
|
||||
const submit = useCallback(async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||
const {
|
||||
@@ -254,6 +253,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
|
||||
fieldErrors.forEach((err) => {
|
||||
dispatchFields({
|
||||
type: 'UPDATE',
|
||||
...(contextRef.current?.fields?.[err.field] || {}),
|
||||
valid: false,
|
||||
errorMessage: err.message,
|
||||
@@ -283,6 +283,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
action,
|
||||
disableSuccessStatus,
|
||||
disabled,
|
||||
dispatchFields,
|
||||
fields,
|
||||
handleResponse,
|
||||
history,
|
||||
@@ -298,7 +299,6 @@ const Form: React.FC<Props> = (props) => {
|
||||
const getData = useCallback(() => reduceFieldsToValues(contextRef.current.fields, true), [contextRef]);
|
||||
const getSiblingData = useCallback((path: string) => getSiblingDataFunc(contextRef.current.fields, path), [contextRef]);
|
||||
const getDataByPath = useCallback<GetDataByPath>((path: string) => getDataByPathFunc(contextRef.current.fields, path), [contextRef]);
|
||||
const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]);
|
||||
|
||||
const createFormData = useCallback((overrides: any = {}) => {
|
||||
const data = reduceFieldsToValues(contextRef.current.fields, true);
|
||||
@@ -328,16 +328,14 @@ const Form: React.FC<Props> = (props) => {
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation, locale });
|
||||
contextRef.current = { ...initContextState } as FormContextType;
|
||||
dispatchFields({ type: 'REPLACE_STATE', state });
|
||||
}, [id, user, operation, locale]);
|
||||
}, [id, user, operation, locale, dispatchFields]);
|
||||
|
||||
contextRef.current.dispatchFields = dispatchFields;
|
||||
contextRef.current.submit = submit;
|
||||
contextRef.current.getFields = getFields;
|
||||
contextRef.current.getField = getField;
|
||||
contextRef.current.getData = getData;
|
||||
contextRef.current.getSiblingData = getSiblingData;
|
||||
contextRef.current.getDataByPath = getDataByPath;
|
||||
contextRef.current.getUnflattenedValues = getUnflattenedValues;
|
||||
contextRef.current.validateForm = validateForm;
|
||||
contextRef.current.createFormData = createFormData;
|
||||
contextRef.current.setModified = setModified;
|
||||
@@ -352,7 +350,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
contextRef.current = { ...initContextState } as FormContextType;
|
||||
dispatchFields({ type: 'REPLACE_STATE', state: initialState });
|
||||
}
|
||||
}, [initialState]);
|
||||
}, [initialState, dispatchFields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
@@ -361,7 +359,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
setFormattedInitialData(builtState);
|
||||
dispatchFields({ type: 'REPLACE_STATE', state: builtState });
|
||||
}
|
||||
}, [initialData]);
|
||||
}, [initialData, dispatchFields]);
|
||||
|
||||
useThrottledEffect(() => {
|
||||
refreshCookie();
|
||||
@@ -403,7 +401,9 @@ const Form: React.FC<Props> = (props) => {
|
||||
<SubmittedContext.Provider value={submitted}>
|
||||
<ProcessingContext.Provider value={processing}>
|
||||
<ModifiedContext.Provider value={modified}>
|
||||
{children}
|
||||
<FormFieldsContext.Provider value={fieldsReducer}>
|
||||
{children}
|
||||
</FormFieldsContext.Provider>
|
||||
</ModifiedContext.Provider>
|
||||
</ProcessingContext.Provider>
|
||||
</SubmittedContext.Provider>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Submit,
|
||||
Context,
|
||||
GetSiblingData,
|
||||
GetUnflattenedValues,
|
||||
ValidateForm,
|
||||
CreateFormData,
|
||||
SetModified,
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
|
||||
const submit: Submit = () => undefined;
|
||||
const getSiblingData: GetSiblingData = () => undefined;
|
||||
const getUnflattenedValues: GetUnflattenedValues = () => ({});
|
||||
const dispatchFields: DispatchFields = () => undefined;
|
||||
const validateForm: ValidateForm = () => undefined;
|
||||
const createFormData: CreateFormData = () => undefined;
|
||||
@@ -28,12 +26,11 @@ const setSubmitted: SetSubmitted = () => undefined;
|
||||
const reset: Reset = () => undefined;
|
||||
|
||||
const initialContextState: Context = {
|
||||
getFields: (): Fields => ({ }),
|
||||
getFields: (): Fields => ({}),
|
||||
getField: (): Field => undefined,
|
||||
getData: (): Data => undefined,
|
||||
getSiblingData,
|
||||
getDataByPath: () => undefined,
|
||||
getUnflattenedValues,
|
||||
validateForm,
|
||||
createFormData,
|
||||
submit,
|
||||
@@ -41,7 +38,6 @@ const initialContextState: Context = {
|
||||
setModified,
|
||||
setProcessing,
|
||||
setSubmitted,
|
||||
initialState: {},
|
||||
fields: {},
|
||||
disabled: false,
|
||||
formRef: null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
import { Field as FieldConfig, Condition, Validate } from '../../../../fields/config/types';
|
||||
|
||||
export type Field = {
|
||||
@@ -50,14 +50,13 @@ export type SubmitOptions = {
|
||||
}
|
||||
|
||||
export type DispatchFields = React.Dispatch<any>
|
||||
export type Submit = (options?: SubmitOptions, e?: React.FormEvent<HTMLFormElement>) => void;
|
||||
export type Submit = (options?: SubmitOptions, e?: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
||||
export type ValidateForm = () => Promise<boolean>;
|
||||
export type CreateFormData = (overrides?: any) => FormData;
|
||||
export type GetFields = () => Fields;
|
||||
export type GetField = (path: string) => Field;
|
||||
export type GetData = () => Data;
|
||||
export type GetSiblingData = (path: string) => Data;
|
||||
export type GetUnflattenedValues = () => Data;
|
||||
export type GetDataByPath = <T = unknown>(path: string) => T;
|
||||
export type SetModified = (modified: boolean) => void;
|
||||
export type SetSubmitted = (submitted: boolean) => void;
|
||||
@@ -65,11 +64,73 @@ export type SetProcessing = (processing: boolean) => void;
|
||||
|
||||
export type Reset = (fieldSchema: FieldConfig[], data: unknown) => Promise<void>
|
||||
|
||||
export type REPLACE_STATE = {
|
||||
type: 'REPLACE_STATE'
|
||||
state: Fields
|
||||
}
|
||||
|
||||
export type REMOVE = {
|
||||
type: 'REMOVE'
|
||||
path: string
|
||||
}
|
||||
|
||||
export type REMOVE_ROW = {
|
||||
type: 'REMOVE_ROW'
|
||||
rowIndex: number
|
||||
path: string
|
||||
}
|
||||
|
||||
export type ADD_ROW = {
|
||||
type: 'ADD_ROW'
|
||||
rowIndex: number
|
||||
path: string
|
||||
subFieldState?: Fields
|
||||
blockType?: string
|
||||
}
|
||||
|
||||
export type DUPLICATE_ROW = {
|
||||
type: 'DUPLICATE_ROW'
|
||||
rowIndex: number
|
||||
path: string
|
||||
}
|
||||
|
||||
export type MOVE_ROW = {
|
||||
type: 'MOVE_ROW'
|
||||
moveFromIndex: number
|
||||
moveToIndex: number
|
||||
path: string
|
||||
}
|
||||
|
||||
export type MODIFY_CONDITION = {
|
||||
type: 'MODIFY_CONDITION'
|
||||
path: string
|
||||
result: boolean
|
||||
}
|
||||
|
||||
export type UPDATE = {
|
||||
type: 'UPDATE'
|
||||
path: string
|
||||
} & Partial<Field>
|
||||
|
||||
export type FieldAction =
|
||||
| REPLACE_STATE
|
||||
| REMOVE
|
||||
| REMOVE_ROW
|
||||
| ADD_ROW
|
||||
| DUPLICATE_ROW
|
||||
| MOVE_ROW
|
||||
| MODIFY_CONDITION
|
||||
| UPDATE
|
||||
|
||||
export type FormFieldsContext = [Fields, Dispatch<FieldAction>]
|
||||
|
||||
export type Context = {
|
||||
dispatchFields: DispatchFields
|
||||
submit: Submit
|
||||
/**
|
||||
* @deprecated Form context fields may be outdated and should not be relied on. Instead, prefer `useFormFields`.
|
||||
*/
|
||||
fields: Fields
|
||||
initialState: Fields
|
||||
submit: Submit
|
||||
dispatchFields: Dispatch<FieldAction>
|
||||
validateForm: ValidateForm
|
||||
createFormData: CreateFormData
|
||||
disabled: boolean
|
||||
@@ -77,7 +138,6 @@ export type Context = {
|
||||
getField: GetField
|
||||
getData: GetData
|
||||
getSiblingData: GetSiblingData
|
||||
getUnflattenedValues: GetUnflattenedValues
|
||||
getDataByPath: GetDataByPath
|
||||
setModified: SetModified
|
||||
setProcessing: SetProcessing
|
||||
|
||||
@@ -6,7 +6,7 @@ export type Props = {
|
||||
className?: string
|
||||
readOnly?: boolean
|
||||
forceRender?: boolean
|
||||
permissions?: {
|
||||
permissions?: FieldPermissions | {
|
||||
[field: string]: FieldPermissions
|
||||
}
|
||||
filter?: (field: Field) => boolean
|
||||
|
||||
@@ -70,14 +70,12 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
const locale = useLocale();
|
||||
const operation = useOperation();
|
||||
|
||||
const { dispatchFields } = formContext;
|
||||
const { dispatchFields, setModified } = formContext;
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
return validate(value, { ...options, minRows, maxRows, required });
|
||||
}, [maxRows, minRows, required, validate]);
|
||||
|
||||
const [disableFormData, setDisableFormData] = useState(false);
|
||||
|
||||
const {
|
||||
showError,
|
||||
errorMessage,
|
||||
@@ -86,7 +84,6 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
} = useField({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
disableFormData,
|
||||
condition,
|
||||
});
|
||||
|
||||
@@ -120,7 +117,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => {
|
||||
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
|
||||
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
|
||||
}, [dispatchRows, dispatchFields, path]);
|
||||
setModified(true);
|
||||
}, [dispatchRows, dispatchFields, path, setModified]);
|
||||
|
||||
const onDragEnd = useCallback((result) => {
|
||||
if (!result.destination) return;
|
||||
@@ -194,14 +192,17 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
}, [formContext, path, getPreference, preferencesKey, initCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(rows?.length || 0, true);
|
||||
if (typeof rows !== 'undefined') {
|
||||
const disableFormData = rows.length > 0;
|
||||
|
||||
if (rows?.length === 0) {
|
||||
setDisableFormData(false);
|
||||
} else {
|
||||
setDisableFormData(true);
|
||||
dispatchFields({
|
||||
type: 'UPDATE',
|
||||
path,
|
||||
disableFormData,
|
||||
value: rows.length,
|
||||
});
|
||||
}
|
||||
}, [rows, setValue]);
|
||||
}, [rows, dispatchFields, path]);
|
||||
|
||||
const hasMaxRows = maxRows && rows?.length >= maxRows;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const labelDefaults = {
|
||||
plural: 'Blocks',
|
||||
};
|
||||
|
||||
const Index: React.FC<Props> = (props) => {
|
||||
const BlocksField: React.FC<Props> = (props) => {
|
||||
const {
|
||||
label,
|
||||
name,
|
||||
@@ -70,13 +70,12 @@ const Index: React.FC<Props> = (props) => {
|
||||
const { id } = useDocumentInfo();
|
||||
const locale = useLocale();
|
||||
const operation = useOperation();
|
||||
const { dispatchFields } = formContext;
|
||||
const { dispatchFields, setModified } = formContext;
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
return validate(value, { ...options, minRows, maxRows, required });
|
||||
}, [maxRows, minRows, required, validate]);
|
||||
|
||||
const [disableFormData, setDisableFormData] = useState(false);
|
||||
const [selectorIndexOpen, setSelectorIndexOpen] = useState<number>();
|
||||
|
||||
const {
|
||||
@@ -87,7 +86,6 @@ const Index: React.FC<Props> = (props) => {
|
||||
} = useField<number>({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
disableFormData,
|
||||
condition,
|
||||
});
|
||||
|
||||
@@ -128,7 +126,8 @@ const Index: React.FC<Props> = (props) => {
|
||||
const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => {
|
||||
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
|
||||
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
|
||||
}, [dispatchRows, dispatchFields, path]);
|
||||
setModified(true);
|
||||
}, [dispatchRows, dispatchFields, path, setModified]);
|
||||
|
||||
const onDragEnd = useCallback((result) => {
|
||||
if (!result.destination) return;
|
||||
@@ -200,14 +199,17 @@ const Index: React.FC<Props> = (props) => {
|
||||
}, [formContext, path, getPreference, preferencesKey, initCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(rows?.length || 0, true);
|
||||
if (typeof rows !== 'undefined') {
|
||||
const disableFormData = rows.length > 0;
|
||||
|
||||
if (rows?.length === 0) {
|
||||
setDisableFormData(false);
|
||||
} else {
|
||||
setDisableFormData(true);
|
||||
dispatchFields({
|
||||
type: 'UPDATE',
|
||||
path,
|
||||
disableFormData,
|
||||
value: rows.length,
|
||||
});
|
||||
}
|
||||
}, [rows, setValue]);
|
||||
}, [rows, dispatchFields, path]);
|
||||
|
||||
const hasMaxRows = maxRows && rows?.length >= maxRows;
|
||||
|
||||
@@ -419,4 +421,4 @@ const Index: React.FC<Props> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default withCondition(Index);
|
||||
export default withCondition(BlocksField);
|
||||
|
||||
@@ -60,7 +60,6 @@ const Code: React.FC<Props> = (props) => {
|
||||
} = useField({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
condition,
|
||||
});
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
|
||||
<RenderFields
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
permissions={permissions?.fields}
|
||||
permissions={permissions}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
|
||||
@@ -2,15 +2,15 @@ import React, { useCallback } from 'react';
|
||||
import useField from '../../useField';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
import { useWatchForm } from '../../Form/context';
|
||||
import { useFormFields } from '../../Form/context';
|
||||
|
||||
import './index.scss';
|
||||
import { Field } from '../../Form/types';
|
||||
|
||||
const ConfirmPassword: React.FC = () => {
|
||||
const { getField } = useWatchForm();
|
||||
const password = getField('password');
|
||||
const password = useFormFields<Field>(([fields]) => fields.password);
|
||||
|
||||
const validate = useCallback((value) => {
|
||||
const validate = useCallback((value: string) => {
|
||||
if (!value) {
|
||||
return 'This field is required';
|
||||
}
|
||||
@@ -31,7 +31,6 @@ const ConfirmPassword: React.FC = () => {
|
||||
path: 'confirm-password',
|
||||
disableFormData: true,
|
||||
validate,
|
||||
enableDebouncedValue: true,
|
||||
});
|
||||
|
||||
const classes = [
|
||||
|
||||
@@ -37,7 +37,6 @@ const Email: React.FC<Props> = (props) => {
|
||||
const fieldType = useField({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
condition,
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.group-field + .group-field {
|
||||
.group-field+.group-field {
|
||||
margin-top: base(-2);
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.group-field--within-collapsible+.group-field--within-collapsible {
|
||||
margin-top: base(-1);
|
||||
}
|
||||
@@ -44,7 +44,6 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
} = useField({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
condition,
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ const Password: React.FC<Props> = (props) => {
|
||||
} = useField({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
});
|
||||
|
||||
const classes = [
|
||||
|
||||
@@ -44,7 +44,6 @@ const PointField: React.FC<Props> = (props) => {
|
||||
} = useField<[number, number]>({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
condition,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
@import '../../../../../../scss/styles.scss';
|
||||
|
||||
.relationship-add-new-modal {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
|
||||
&__blur-bg {
|
||||
@include blur-bg();
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
.collection-edit {
|
||||
@include blur-bg();
|
||||
transform: translateX(#{base(4)});
|
||||
opacity: 0;
|
||||
transition: all 300ms ease-out;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.collection-edit__form {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.collection-edit__document-actions {
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--animated {
|
||||
|
||||
.collection-edit,
|
||||
.relationship-add-new-modal__blur-bg,
|
||||
.relationship-add-new-modal__close {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.collection-edit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.collection-edit__document-actions {
|
||||
margin-top: base(2.75);
|
||||
}
|
||||
|
||||
&__close {
|
||||
@extend %btn-reset;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
text-indent: -9999px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 300ms ease-in-out;
|
||||
transition-delay: 100ms;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-top: base(2.5);
|
||||
}
|
||||
|
||||
&__header-close {
|
||||
svg {
|
||||
width: base(2.5);
|
||||
height: base(2.5);
|
||||
position: relative;
|
||||
top: base(-.5);
|
||||
right: base(-.75);
|
||||
|
||||
.stroke {
|
||||
stroke-width: .5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__header-close {
|
||||
svg {
|
||||
top: base(-.75);
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
width: base(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme=dark] {
|
||||
.relationship-add-new-modal__close {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useWindowInfo } from '@faceless-ui/window-info';
|
||||
import Button from '../../../../../elements/Button';
|
||||
import { Props } from './types';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import RenderCustomComponent from '../../../../../utilities/RenderCustomComponent';
|
||||
import { useLocale } from '../../../../../utilities/Locale';
|
||||
import { useConfig } from '../../../../../utilities/Config';
|
||||
import DefaultEdit from '../../../../../views/collections/Edit/Default';
|
||||
import X from '../../../../../icons/X';
|
||||
import { Fields } from '../../../../Form/types';
|
||||
import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
|
||||
import { EditDepthContext, useEditDepth } from '../../../../../utilities/EditDepth';
|
||||
import { DocumentInfoProvider } from '../../../../../utilities/DocumentInfo';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'relationship-add-new-modal';
|
||||
|
||||
export const AddNewRelationModal: React.FC<Props> = ({ modalCollection, onSave, modalSlug }) => {
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { toggleModal } = useModal();
|
||||
const { breakpoints: { m: midBreak } } = useWindowInfo();
|
||||
const locale = useLocale();
|
||||
const { permissions, user } = useAuth();
|
||||
const [initialState, setInitialState] = useState<Fields>();
|
||||
const [isAnimated, setIsAnimated] = useState(false);
|
||||
const editDepth = useEditDepth();
|
||||
|
||||
const modalAction = `${serverURL}${api}/${modalCollection.slug}?locale=${locale}&depth=0&fallback-locale=null`;
|
||||
|
||||
useEffect(() => {
|
||||
const buildState = async () => {
|
||||
const state = await buildStateFromSchema({ fieldSchema: modalCollection.fields, data: {}, user, operation: 'create', locale });
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
buildState();
|
||||
}, [modalCollection, locale, user]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimated(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
className={[
|
||||
baseClass,
|
||||
isAnimated && `${baseClass}--animated`,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{editDepth === 1 && (
|
||||
<div className={`${baseClass}__blur-bg`} />
|
||||
)}
|
||||
<DocumentInfoProvider collection={modalCollection}>
|
||||
<EditDepthContext.Provider value={editDepth + 1}>
|
||||
<button
|
||||
className={`${baseClass}__close`}
|
||||
type="button"
|
||||
onClick={() => toggleModal(modalSlug)}
|
||||
style={{
|
||||
width: `calc(${midBreak ? 'var(--gutter-h)' : 'var(--nav-width)'} + ${editDepth - 1} * 25px)`,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
Close
|
||||
</span>
|
||||
</button>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultEdit}
|
||||
CustomComponent={modalCollection.admin?.components?.views?.Edit}
|
||||
componentProps={{
|
||||
isLoading: !initialState,
|
||||
data: {},
|
||||
collection: modalCollection,
|
||||
permissions: permissions.collections[modalCollection.slug],
|
||||
isEditing: false,
|
||||
onSave,
|
||||
initialState,
|
||||
hasSavePermission: true,
|
||||
action: modalAction,
|
||||
disableEyebrow: true,
|
||||
disableActions: true,
|
||||
disableLeaveWithoutSaving: true,
|
||||
customHeader: (
|
||||
<div className={`${baseClass}__header`}>
|
||||
<h2>
|
||||
Add new
|
||||
{' '}
|
||||
{modalCollection.labels.singular}
|
||||
</h2>
|
||||
<Button
|
||||
buttonStyle="none"
|
||||
className={`${baseClass}__header-close`}
|
||||
onClick={() => toggleModal(modalSlug)}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EditDepthContext.Provider>
|
||||
</DocumentInfoProvider>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../collections/config/types';
|
||||
|
||||
export type Props = {
|
||||
modalSlug: string
|
||||
modalCollection: SanitizedCollectionConfig
|
||||
onSave: (json: Record<string, unknown>) => void
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
@import '../../../../../scss/styles.scss';
|
||||
|
||||
.relationship-add-new {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
.popup__wrapper {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__add-button {
|
||||
@include formInput;
|
||||
height: 100%;
|
||||
margin-left: -1px;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
|
||||
.btn__content,
|
||||
.btn__label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn__content,
|
||||
.btn__label {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn__label {
|
||||
padding: 0 base(.5);
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__relations {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li:not(:last-child) {
|
||||
margin-bottom: base(.375);
|
||||
}
|
||||
}
|
||||
|
||||
&__relation-button {
|
||||
@extend %btn-reset;
|
||||
cursor: pointer;
|
||||
@extend %btn-reset;
|
||||
display: block;
|
||||
padding: base(.125) 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import Button from '../../../../elements/Button';
|
||||
import { Props } from './types';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
import Popup from '../../../../elements/Popup';
|
||||
import { useRelatedCollections } from './useRelatedCollections';
|
||||
import { useAuth } from '../../../../utilities/Auth';
|
||||
import { AddNewRelationModal } from './Modal';
|
||||
import { useEditDepth } from '../../../../utilities/EditDepth';
|
||||
import Plus from '../../../../icons/Plus';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'relationship-add-new';
|
||||
|
||||
export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, value, setValue, dispatchOptions }) => {
|
||||
const relatedCollections = useRelatedCollections(relationTo);
|
||||
const { toggleModal, isModalOpen } = useModal();
|
||||
const { permissions } = useAuth();
|
||||
const [hasPermission, setHasPermission] = useState(false);
|
||||
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>();
|
||||
const [popupOpen, setPopupOpen] = useState(false);
|
||||
const editDepth = useEditDepth();
|
||||
|
||||
const modalSlug = `${path}-add-modal-depth-${editDepth}`;
|
||||
|
||||
const openModal = useCallback(async (collection: SanitizedCollectionConfig) => {
|
||||
setModalCollection(collection);
|
||||
toggleModal(modalSlug);
|
||||
}, [toggleModal, modalSlug]);
|
||||
|
||||
const onSave = useCallback((json) => {
|
||||
const newValue = Array.isArray(relationTo) ? {
|
||||
relationTo: modalCollection.slug,
|
||||
value: json.doc.id,
|
||||
} : json.doc.id;
|
||||
|
||||
dispatchOptions({
|
||||
type: 'ADD',
|
||||
hasMultipleRelations: Array.isArray(relationTo),
|
||||
collection: modalCollection,
|
||||
docs: [
|
||||
json.doc,
|
||||
],
|
||||
sort: true,
|
||||
});
|
||||
|
||||
if (hasMany) {
|
||||
setValue([...(Array.isArray(value) ? value : []), newValue]);
|
||||
} else {
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
setModalCollection(undefined);
|
||||
toggleModal(modalSlug);
|
||||
}, [relationTo, modalCollection, hasMany, toggleModal, modalSlug, setValue, value, dispatchOptions]);
|
||||
|
||||
const onPopopToggle = useCallback((state) => {
|
||||
setPopupOpen(state);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (permissions) {
|
||||
if (relatedCollections.length === 1) {
|
||||
setHasPermission(permissions.collections[relatedCollections[0].slug].create.permission);
|
||||
} else {
|
||||
setHasPermission(relatedCollections.some((collection) => permissions.collections[collection.slug].create.permission));
|
||||
}
|
||||
}
|
||||
}, [permissions, relatedCollections]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen(modalSlug)) {
|
||||
setModalCollection(undefined);
|
||||
}
|
||||
}, [isModalOpen, modalSlug]);
|
||||
|
||||
return hasPermission ? (
|
||||
<div
|
||||
className={baseClass}
|
||||
id={`${path}-add-new`}
|
||||
>
|
||||
{relatedCollections.length === 1 && (
|
||||
<Button
|
||||
className={`${baseClass}__add-button`}
|
||||
onClick={() => openModal(relatedCollections[0])}
|
||||
buttonStyle="none"
|
||||
tooltip={`Add new ${relatedCollections[0].labels.singular}`}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
)}
|
||||
{relatedCollections.length > 1 && (
|
||||
<Popup
|
||||
buttonType="custom"
|
||||
horizontalAlign="center"
|
||||
onToggleOpen={onPopopToggle}
|
||||
button={(
|
||||
<Button
|
||||
className={`${baseClass}__add-button`}
|
||||
buttonStyle="none"
|
||||
tooltip={popupOpen ? undefined : 'Add new'}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
)}
|
||||
render={({ close: closePopup }) => (
|
||||
<ul className={`${baseClass}__relations`}>
|
||||
{relatedCollections.map((relatedCollection) => {
|
||||
if (permissions.collections[relatedCollection.slug].create.permission) {
|
||||
return (
|
||||
<li key={relatedCollection.slug}>
|
||||
<button
|
||||
className={`${baseClass}__relation-button ${baseClass}__relation-button--${relatedCollection.slug}`}
|
||||
type="button"
|
||||
onClick={() => { closePopup(); openModal(relatedCollection); }}
|
||||
>
|
||||
{relatedCollection.labels.singular}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{modalCollection && (
|
||||
<AddNewRelationModal
|
||||
{...{ onSave, modalSlug, modalCollection }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Action } from '../types';
|
||||
|
||||
export type Props = {
|
||||
hasMany: boolean
|
||||
relationTo: string | string[]
|
||||
path: string
|
||||
value: unknown
|
||||
setValue: (value: unknown) => void
|
||||
dispatchOptions: React.Dispatch<Action>
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
import { useConfig } from '../../../../utilities/Config';
|
||||
|
||||
export const useRelatedCollections = (relationTo: string | string[]): SanitizedCollectionConfig[] => {
|
||||
const config = useConfig();
|
||||
const [relatedCollections] = useState(() => {
|
||||
const relations = typeof relationTo === 'string' ? [relationTo] : relationTo;
|
||||
return relations.map((relation) => config.collections.find((collection) => collection.slug === relation));
|
||||
});
|
||||
|
||||
return relatedCollections;
|
||||
};
|
||||
@@ -5,18 +5,21 @@
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
.relationship__error-loading {
|
||||
border: 1px solid var(--theme-error-500);
|
||||
min-height: base(2);
|
||||
padding: base(.5) base(.75);
|
||||
background-color: var(--theme-error-500);
|
||||
color: var(--theme-elevation-0);
|
||||
}
|
||||
.relationship {
|
||||
&__wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.relationship--read-only {
|
||||
div.react-select {
|
||||
div.rs__control {
|
||||
background: var(--theme-elevation-100);
|
||||
div.react-select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__error-loading {
|
||||
border: 1px solid var(--theme-error-500);
|
||||
min-height: base(2);
|
||||
padding: base(.5) base(.75);
|
||||
background-color: var(--theme-error-500);
|
||||
color: var(--theme-elevation-0);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import FieldDescription from '../../FieldDescription';
|
||||
import { relationship } from '../../../../../fields/validations';
|
||||
import { Where } from '../../../../../types';
|
||||
import { PaginatedDocs } from '../../../../../mongoose/types';
|
||||
import { useFormProcessing, useWatchForm } from '../../Form/context';
|
||||
import { useFormProcessing, useAllFormFields } from '../../Form/context';
|
||||
import optionsReducer from './optionsReducer';
|
||||
import { Props, Option, ValueWithRelation, GetResults } from './types';
|
||||
import { createRelationMap } from './createRelationMap';
|
||||
@@ -23,6 +23,9 @@ import { useDebouncedCallback } from '../../../../hooks/useDebouncedCallback';
|
||||
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||
import { getFilterOptionsQuery } from '../getFilterOptionsQuery';
|
||||
import wordBoundariesRegex from '../../../../../utilities/wordBoundariesRegex';
|
||||
import reduceFieldsToValues from '../../Form/reduceFieldsToValues';
|
||||
import getSiblingData from '../../Form/getSiblingData';
|
||||
import { AddNewRelation } from './AddNew';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -47,7 +50,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
width,
|
||||
description,
|
||||
condition,
|
||||
isSortable,
|
||||
isSortable = true,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
@@ -61,7 +64,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
|
||||
const { id } = useDocumentInfo();
|
||||
const { user, permissions } = useAuth();
|
||||
const { getData, getSiblingData } = useWatchForm();
|
||||
const [fields] = useAllFormFields();
|
||||
const formProcessing = useFormProcessing();
|
||||
const hasMultipleRelations = Array.isArray(relationTo);
|
||||
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: null, label: 'None' }]);
|
||||
@@ -73,6 +76,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [enableWordBoundarySearch, setEnableWordBoundarySearch] = useState(false);
|
||||
const firstRun = useRef(true);
|
||||
const fieldsRef = useRef(fields);
|
||||
|
||||
const memoizedValidate = useCallback((value, validationOptions) => {
|
||||
return validate(value, { ...validationOptions, required });
|
||||
@@ -90,6 +94,13 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
condition,
|
||||
});
|
||||
|
||||
const getFormData = useCallback(() => {
|
||||
return [
|
||||
reduceFieldsToValues(fieldsRef.current, true),
|
||||
getSiblingData(fieldsRef.current, path),
|
||||
];
|
||||
}, [fieldsRef, path]);
|
||||
|
||||
const getResults: GetResults = useCallback(async ({
|
||||
lastFullyLoadedRelation: lastFullyLoadedRelationArg,
|
||||
lastLoadedPage: lastLoadedPageArg,
|
||||
@@ -148,13 +159,13 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
query.where.and.push(optionFilters[relation]);
|
||||
}
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`);
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, { credentials: 'include' });
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs<unknown> = await response.json();
|
||||
if (data.docs.length > 0) {
|
||||
resultsFetched += data.docs.length;
|
||||
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, sort });
|
||||
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort });
|
||||
setLastLoadedPage(data.page);
|
||||
|
||||
if (!data.nextPage) {
|
||||
@@ -170,7 +181,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
} else if (response.status === 403) {
|
||||
setLastFullyLoadedRelation(relations.indexOf(relation));
|
||||
lastLoadedPageToUse = 1;
|
||||
dispatchOptions({ type: 'ADD', data: { docs: [] } as PaginatedDocs<unknown>, relation, hasMultipleRelations, collection, sort, ids: relationMap[relation] });
|
||||
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort, ids: relationMap[relation] });
|
||||
} else {
|
||||
setErrorLoading('An error has occurred.');
|
||||
}
|
||||
@@ -251,11 +262,11 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
setSearch(searchArg);
|
||||
}, [getResults]);
|
||||
|
||||
const handleInputChange = (searchArg: string, valueArg: unknown) => {
|
||||
const handleInputChange = useCallback((searchArg: string, valueArg: unknown) => {
|
||||
if (search !== searchArg) {
|
||||
updateSearch(searchArg, valueArg);
|
||||
}
|
||||
};
|
||||
}, [search, updateSearch]);
|
||||
|
||||
// ///////////////////////////
|
||||
// Fetch value options when initialValue changes
|
||||
@@ -284,13 +295,13 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
if (!errorLoading) {
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`);
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, { credentials: 'include' });
|
||||
const collection = collections.find((coll) => coll.slug === relation);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, sort: true, ids });
|
||||
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort: true, ids });
|
||||
} else if (response.status === 403) {
|
||||
dispatchOptions({ type: 'ADD', data: { docs: [] } as PaginatedDocs, relation, hasMultipleRelations, collection, sort: true, ids });
|
||||
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort: true, ids });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,20 +312,21 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!filterOptions) {
|
||||
return;
|
||||
}
|
||||
if (!filterOptions) return;
|
||||
|
||||
const [data, siblingData] = getFormData();
|
||||
|
||||
const newOptionFilters = getFilterOptionsQuery(filterOptions, {
|
||||
id,
|
||||
data: getData(),
|
||||
data,
|
||||
relationTo,
|
||||
siblingData: getSiblingData(path),
|
||||
siblingData,
|
||||
user,
|
||||
});
|
||||
if (!equal(newOptionFilters, optionFilters)) {
|
||||
setOptionFilters(newOptionFilters);
|
||||
}
|
||||
}, [relationTo, filterOptions, optionFilters, id, getData, getSiblingData, path, user]);
|
||||
}, [relationTo, filterOptions, optionFilters, id, getFormData, path, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (optionFilters || !filterOptions) {
|
||||
@@ -380,50 +392,57 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
required={required}
|
||||
/>
|
||||
{!errorLoading && (
|
||||
<ReactSelect
|
||||
isDisabled={readOnly}
|
||||
onInputChange={(newSearch) => handleInputChange(newSearch, value)}
|
||||
onChange={!readOnly ? (selected) => {
|
||||
if (hasMany) {
|
||||
setValue(selected ? selected.map((option) => {
|
||||
if (hasMultipleRelations) {
|
||||
return {
|
||||
relationTo: option.relationTo,
|
||||
value: option.value,
|
||||
};
|
||||
}
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<ReactSelect
|
||||
isDisabled={readOnly}
|
||||
onInputChange={(newSearch) => handleInputChange(newSearch, value)}
|
||||
onChange={!readOnly ? (selected) => {
|
||||
if (hasMany) {
|
||||
setValue(selected ? selected.map((option) => {
|
||||
if (hasMultipleRelations) {
|
||||
return {
|
||||
relationTo: option.relationTo,
|
||||
value: option.value,
|
||||
};
|
||||
}
|
||||
|
||||
return option.value;
|
||||
}) : null);
|
||||
} else if (hasMultipleRelations) {
|
||||
setValue({
|
||||
relationTo: selected.relationTo,
|
||||
value: selected.value,
|
||||
return option.value;
|
||||
}) : null);
|
||||
} else if (hasMultipleRelations) {
|
||||
setValue({
|
||||
relationTo: selected.relationTo,
|
||||
value: selected.value,
|
||||
});
|
||||
} else {
|
||||
setValue(selected.value);
|
||||
}
|
||||
} : undefined}
|
||||
onMenuScrollToBottom={() => {
|
||||
getResults({
|
||||
lastFullyLoadedRelation,
|
||||
lastLoadedPage: lastLoadedPage + 1,
|
||||
search,
|
||||
value: initialValue,
|
||||
sort: false,
|
||||
});
|
||||
} else {
|
||||
setValue(selected.value);
|
||||
}
|
||||
} : undefined}
|
||||
onMenuScrollToBottom={() => {
|
||||
getResults({
|
||||
lastFullyLoadedRelation,
|
||||
lastLoadedPage: lastLoadedPage + 1,
|
||||
search,
|
||||
value: initialValue,
|
||||
sort: false,
|
||||
});
|
||||
}}
|
||||
value={valueToRender}
|
||||
showError={showError}
|
||||
disabled={formProcessing}
|
||||
options={options}
|
||||
isMulti={hasMany}
|
||||
isSortable={isSortable}
|
||||
filterOption={enableWordBoundarySearch ? (item, searchFilter) => {
|
||||
const r = wordBoundariesRegex(searchFilter || '');
|
||||
return r.test(item.label);
|
||||
} : undefined}
|
||||
/>
|
||||
}}
|
||||
value={valueToRender}
|
||||
showError={showError}
|
||||
disabled={formProcessing}
|
||||
options={options}
|
||||
isMulti={hasMany}
|
||||
isSortable={isSortable}
|
||||
filterOption={enableWordBoundarySearch ? (item, searchFilter) => {
|
||||
const r = wordBoundariesRegex(searchFilter || '');
|
||||
return r.test(item.label);
|
||||
} : undefined}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<AddNewRelation
|
||||
{...{ path, hasMany, relationTo, value, setValue, dispatchOptions }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{errorLoading && (
|
||||
<div className={`${baseClass}__error-loading`}>
|
||||
|
||||
@@ -29,7 +29,8 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
}
|
||||
|
||||
case 'ADD': {
|
||||
const { hasMultipleRelations, collection, relation, data, sort, ids = [] } = action;
|
||||
const { hasMultipleRelations, collection, docs, sort, ids = [] } = action;
|
||||
const relation = collection.slug;
|
||||
|
||||
const labelKey = collection.admin.useAsTitle || 'id';
|
||||
|
||||
@@ -38,20 +39,19 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
if (!hasMultipleRelations) {
|
||||
const options = [
|
||||
...state,
|
||||
...data.docs.reduce((docs, doc) => {
|
||||
...docs.reduce((docOptions, doc) => {
|
||||
if (loadedIDs.indexOf(doc.id) === -1) {
|
||||
loadedIDs.push(doc.id);
|
||||
return [
|
||||
...docs,
|
||||
...docOptions,
|
||||
{
|
||||
label: doc[labelKey] || `Untitled - ID: ${doc.id}`,
|
||||
value: doc.id,
|
||||
},
|
||||
];
|
||||
}
|
||||
return docs;
|
||||
},
|
||||
[]),
|
||||
return docOptions;
|
||||
}, []),
|
||||
];
|
||||
|
||||
ids.forEach((id) => {
|
||||
@@ -69,12 +69,12 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
const newOptions = [...state];
|
||||
const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === collection.labels.plural);
|
||||
|
||||
const newSubOptions = data.docs.reduce((docs, doc) => {
|
||||
const newSubOptions = docs.reduce((docSubOptions, doc) => {
|
||||
if (loadedIDs.indexOf(doc.id) === -1) {
|
||||
loadedIDs.push(doc.id);
|
||||
|
||||
return [
|
||||
...docs,
|
||||
...docSubOptions,
|
||||
{
|
||||
label: doc[labelKey] || `Untitled - ID: ${doc.id}`,
|
||||
relationTo: relation,
|
||||
@@ -83,7 +83,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
];
|
||||
}
|
||||
|
||||
return docs;
|
||||
return docSubOptions;
|
||||
}, []);
|
||||
|
||||
ids.forEach((id) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { PaginatedDocs } from '../../../../../mongoose/types';
|
||||
import { RelationshipField } from '../../../../../fields/config/types';
|
||||
|
||||
export type Props = Omit<RelationshipField, 'type'> & {
|
||||
@@ -19,8 +18,7 @@ type CLEAR = {
|
||||
|
||||
type ADD = {
|
||||
type: 'ADD'
|
||||
data: PaginatedDocs<any>
|
||||
relation: string
|
||||
docs: any[]
|
||||
hasMultipleRelations: boolean
|
||||
collection: SanitizedCollectionConfig
|
||||
sort?: boolean
|
||||
|
||||
@@ -70,6 +70,7 @@ const RichText: React.FC<Props> = (props) => {
|
||||
const [enabledLeaves, setEnabledLeaves] = useState({});
|
||||
const [initialValueKey, setInitialValueKey] = useState('');
|
||||
const editorRef = useRef(null);
|
||||
const toolbarRef = useRef(null);
|
||||
|
||||
const renderElement = useCallback(({ attributes, children, element }) => {
|
||||
const matchedElement = enabledElements[element?.type];
|
||||
@@ -93,22 +94,27 @@ const RichText: React.FC<Props> = (props) => {
|
||||
}, [enabledElements, path, props]);
|
||||
|
||||
const renderLeaf = useCallback(({ attributes, children, leaf }) => {
|
||||
const matchedLeafName = Object.keys(enabledLeaves).find((leafName) => leaf[leafName]);
|
||||
const matchedLeaves = Object.entries(enabledLeaves).filter(([leafName]) => leaf[leafName]);
|
||||
|
||||
if (enabledLeaves[matchedLeafName]?.Leaf) {
|
||||
const { Leaf } = enabledLeaves[matchedLeafName];
|
||||
if (matchedLeaves.length > 0) {
|
||||
return matchedLeaves.reduce((result, [leafName], i) => {
|
||||
if (enabledLeaves[leafName]?.Leaf) {
|
||||
const Leaf = enabledLeaves[leafName]?.Leaf;
|
||||
return (
|
||||
<Leaf
|
||||
key={i}
|
||||
leaf={leaf}
|
||||
path={path}
|
||||
fieldProps={props}
|
||||
editorRef={editorRef}
|
||||
>
|
||||
{result}
|
||||
</Leaf>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Leaf
|
||||
attributes={attributes}
|
||||
leaf={leaf}
|
||||
path={path}
|
||||
fieldProps={props}
|
||||
editorRef={editorRef}
|
||||
>
|
||||
{children}
|
||||
</Leaf>
|
||||
);
|
||||
return result;
|
||||
}, <span {...attributes}>{children}</span>);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -176,6 +182,31 @@ const RichText: React.FC<Props> = (props) => {
|
||||
setInitialValueKey(JSON.stringify(initialValue || ''));
|
||||
}, [initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
function setClickableState(clickState: 'disabled' | 'enabled') {
|
||||
const selectors = 'button, a, [role="button"]';
|
||||
const toolbarButtons: (HTMLButtonElement | HTMLAnchorElement)[] = toolbarRef.current?.querySelectorAll(selectors);
|
||||
const editorButtons: (HTMLButtonElement | HTMLAnchorElement)[] = editorRef.current?.querySelectorAll(selectors);
|
||||
|
||||
[...(toolbarButtons || []), ...(editorButtons || [])].forEach((child) => {
|
||||
const isButton = child.tagName === 'BUTTON';
|
||||
const isDisabling = clickState === 'disabled';
|
||||
child.setAttribute('tabIndex', isDisabling ? '-1' : '0');
|
||||
if (isButton) child.setAttribute('disabled', isDisabling ? 'disabled' : null);
|
||||
});
|
||||
}
|
||||
|
||||
if (loaded && readOnly) {
|
||||
setClickableState('disabled');
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (loaded && readOnly) {
|
||||
setClickableState('enabled');
|
||||
}
|
||||
};
|
||||
}, [loaded, readOnly]);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
@@ -215,13 +246,16 @@ const RichText: React.FC<Props> = (props) => {
|
||||
editor={editor}
|
||||
value={valueToRender as any[]}
|
||||
onChange={(val) => {
|
||||
if (val !== defaultValue && val !== value) {
|
||||
if (!readOnly && val !== defaultValue && val !== value) {
|
||||
setValue(val);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__toolbar`}>
|
||||
<div
|
||||
className={`${baseClass}__toolbar`}
|
||||
ref={toolbarRef}
|
||||
>
|
||||
<div className={`${baseClass}__toolbar-wrap`}>
|
||||
{elements.map((element, i) => {
|
||||
let elementName: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { useConfig } from '../../../../../../../utilities/Config';
|
||||
import { useAuth } from '../../../../../../../utilities/Auth';
|
||||
import { useWatchForm } from '../../../../../../Form/context';
|
||||
import { useFormFields } from '../../../../../../Form/context';
|
||||
import Relationship from '../../../../../Relationship';
|
||||
import Select from '../../../../../Select';
|
||||
|
||||
@@ -24,9 +24,7 @@ const RelationshipFields = () => {
|
||||
const { permissions } = useAuth();
|
||||
|
||||
const [options, setOptions] = useState(() => createOptions(collections, permissions));
|
||||
|
||||
const { getData } = useWatchForm();
|
||||
const { relationTo } = getData();
|
||||
const relationTo = useFormFields<string>(([fields]) => fields.relationTo?.value as string);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(createOptions(collections, permissions));
|
||||
|
||||
@@ -42,7 +42,7 @@ const insertUpload = (editor, { value, relationTo }) => {
|
||||
};
|
||||
|
||||
const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
const { toggleModal, modalState } = useModal();
|
||||
const { toggleModal, isModalOpen } = useModal();
|
||||
const editor = useSlate();
|
||||
const { serverURL, routes: { api }, collections } = useConfig();
|
||||
const [availableCollections] = useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
|
||||
@@ -65,7 +65,7 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
|
||||
const modalSlug = `${path}-add-upload`;
|
||||
const moreThanOneAvailableCollection = availableCollections.length > 1;
|
||||
const isOpen = modalState[modalSlug]?.isOpen;
|
||||
const isOpen = isModalOpen(modalSlug);
|
||||
|
||||
// If modal is open, get active page of upload gallery
|
||||
const apiURL = isOpen ? `${serverURL}${api}/${modalCollection.slug}` : null;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
.rich-text {
|
||||
margin-bottom: base(2);
|
||||
display: flex;
|
||||
isolation: isolate;
|
||||
|
||||
&__toolbar {
|
||||
@include blur-bg(var(--theme-elevation-0));
|
||||
@@ -103,6 +104,29 @@
|
||||
.rich-text__editor {
|
||||
background-color: var(--theme-elevation-150);
|
||||
padding: base(.5);
|
||||
|
||||
.popup button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.rich-text__toolbar {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--theme-elevation-150);
|
||||
opacity: .85;
|
||||
z-index: 2;
|
||||
backdrop-filter: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const Row: React.FC<Props> = (props) => {
|
||||
<RenderFields
|
||||
readOnly={readOnly}
|
||||
className={classes}
|
||||
permissions={permissions?.fields}
|
||||
permissions={permissions}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
|
||||
@@ -3,12 +3,4 @@
|
||||
.field-type.select {
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
.select--read-only {
|
||||
div.react-select {
|
||||
div.rs__control {
|
||||
background: var(--theme-elevation-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ const Select: React.FC<Props> = (props) => {
|
||||
description,
|
||||
isClearable,
|
||||
condition,
|
||||
isSortable
|
||||
isSortable = true,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
@@ -95,6 +95,7 @@ const Select: React.FC<Props> = (props) => {
|
||||
showError={showError}
|
||||
errorMessage={errorMessage}
|
||||
required={required}
|
||||
readOnly={readOnly}
|
||||
description={description}
|
||||
style={style}
|
||||
className={className}
|
||||
|
||||
@@ -71,7 +71,7 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
key={String(activeTab.label)}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
permissions={permissions?.fields}
|
||||
permissions={tabHasName(activeTab) ? permissions[activeTab.name].fields : permissions}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={activeTab.fields.map((field) => ({
|
||||
...field,
|
||||
|
||||
@@ -35,7 +35,6 @@ const Text: React.FC<Props> = (props) => {
|
||||
const field = useField<string>({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
condition,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@include formInput();
|
||||
height: auto;
|
||||
min-height: base(3);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
&.error {
|
||||
|
||||
@@ -42,7 +42,6 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
} = useField({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
condition,
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FieldTypes } from '..';
|
||||
import AddModal from './Add';
|
||||
import SelectExistingModal from './SelectExisting';
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { useEditDepth, EditDepthContext } from '../../../utilities/EditDepth';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -58,13 +59,15 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
filterOptions,
|
||||
} = props;
|
||||
|
||||
const { toggleModal } = useModal();
|
||||
const { toggleModal, modalState } = useModal();
|
||||
const editDepth = useEditDepth();
|
||||
|
||||
const addModalSlug = `${path}-add`;
|
||||
const selectExistingModalSlug = `${path}-select-existing`;
|
||||
const addModalSlug = `${path}-add-depth-${editDepth}`;
|
||||
const selectExistingModalSlug = `${path}-select-existing-depth-${editDepth}`;
|
||||
|
||||
const [file, setFile] = useState(undefined);
|
||||
const [missingFile, setMissingFile] = useState(false);
|
||||
const [modalToRender, setModalToRender] = useState<string>();
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
@@ -77,7 +80,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
useEffect(() => {
|
||||
if (typeof value === 'string' && value !== '') {
|
||||
const fetchFile = async () => {
|
||||
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`);
|
||||
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, { credentials: 'include' });
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
setFile(json);
|
||||
@@ -98,6 +101,12 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
serverURL,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!modalState[addModalSlug]?.isOpen && !modalState[selectExistingModalSlug]?.isOpen) {
|
||||
setModalToRender(undefined);
|
||||
}
|
||||
}, [modalState, addModalSlug, selectExistingModalSlug]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
@@ -132,6 +141,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
buttonStyle="secondary"
|
||||
onClick={() => {
|
||||
toggleModal(addModalSlug);
|
||||
setModalToRender(addModalSlug);
|
||||
}}
|
||||
>
|
||||
Upload new
|
||||
@@ -142,33 +152,40 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
buttonStyle="secondary"
|
||||
onClick={() => {
|
||||
toggleModal(selectExistingModalSlug);
|
||||
setModalToRender(selectExistingModalSlug);
|
||||
}}
|
||||
>
|
||||
Choose from existing
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<AddModal
|
||||
{...{
|
||||
collection,
|
||||
slug: addModalSlug,
|
||||
fieldTypes,
|
||||
setValue: (e) => {
|
||||
setMissingFile(false);
|
||||
onChange(e);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<SelectExistingModal
|
||||
{...{
|
||||
collection,
|
||||
slug: selectExistingModalSlug,
|
||||
setValue: onChange,
|
||||
addModalSlug,
|
||||
filterOptions,
|
||||
path,
|
||||
}}
|
||||
/>
|
||||
<EditDepthContext.Provider value={editDepth + 1}>
|
||||
{modalToRender === addModalSlug && (
|
||||
<AddModal
|
||||
{...{
|
||||
collection,
|
||||
slug: addModalSlug,
|
||||
fieldTypes,
|
||||
setValue: (e) => {
|
||||
setMissingFile(false);
|
||||
onChange(e);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{modalToRender === selectExistingModalSlug && (
|
||||
<SelectExistingModal
|
||||
{...{
|
||||
collection,
|
||||
slug: selectExistingModalSlug,
|
||||
setValue: onChange,
|
||||
addModalSlug,
|
||||
filterOptions,
|
||||
path,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EditDepthContext.Provider>
|
||||
<FieldDescription
|
||||
value={file}
|
||||
description={description}
|
||||
|
||||
@@ -15,7 +15,7 @@ import PerPage from '../../../../elements/PerPage';
|
||||
import formatFields from '../../../../views/collections/List/formatFields';
|
||||
import { getFilterOptionsQuery } from '../../getFilterOptionsQuery';
|
||||
import { useDocumentInfo } from '../../../../utilities/DocumentInfo';
|
||||
import { useWatchForm } from '../../../Form/context';
|
||||
import { useForm } from '../../../Form/context';
|
||||
import ViewDescription from '../../../../elements/ViewDescription';
|
||||
|
||||
import './index.scss';
|
||||
@@ -43,8 +43,8 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { id } = useDocumentInfo();
|
||||
const { user } = useAuth();
|
||||
const { getData, getSiblingData } = useWatchForm();
|
||||
const { toggleModal, modalState } = useModal();
|
||||
const { getData, getSiblingData } = useForm();
|
||||
const { toggleModal, isModalOpen } = useModal();
|
||||
const [fields] = useState(() => formatFields(collection));
|
||||
const [limit, setLimit] = useState(defaultLimit);
|
||||
const [sort, setSort] = useState(null);
|
||||
@@ -56,7 +56,7 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
baseClass,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const isOpen = modalState[modalSlug]?.isOpen;
|
||||
const isOpen = isModalOpen(modalSlug);
|
||||
|
||||
const apiURL = isOpen ? `${serverURL}${api}/${collectionSlug}` : null;
|
||||
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
.upload {
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
min-width: base(15);
|
||||
|
||||
&__wrap {
|
||||
display: flex;
|
||||
padding: base(1.5) base(1.5) $baseline;
|
||||
background: var(--theme-elevation-50);
|
||||
flex-wrap: wrap;
|
||||
|
||||
.btn {
|
||||
margin: 0 $baseline base(.5) 0;
|
||||
min-width: base(7);
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
min-width: calc(100vw - #{base(2)});
|
||||
|
||||
&__wrap {
|
||||
display: block;
|
||||
padding: $baseline $baseline base(.5);
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
min-width: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +1,38 @@
|
||||
import {
|
||||
useCallback, useEffect, useState,
|
||||
} from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../Form/context';
|
||||
import useDebounce from '../../../hooks/useDebounce';
|
||||
import { useFormProcessing, useFormSubmitted, useFormModified, useForm, useFormFields } from '../Form/context';
|
||||
import { Options, FieldType } from './types';
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||
import { useOperation } from '../../utilities/OperationProvider';
|
||||
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
||||
import { UPDATE } from '../Form/types';
|
||||
|
||||
const useField = <T extends unknown>(options: Options): FieldType<T> => {
|
||||
const {
|
||||
path,
|
||||
validate,
|
||||
enableDebouncedValue,
|
||||
disableFormData = false,
|
||||
condition,
|
||||
} = options;
|
||||
|
||||
const formContext = useForm();
|
||||
const submitted = useFormSubmitted();
|
||||
const processing = useFormProcessing();
|
||||
const modified = useFormModified();
|
||||
const { user } = useAuth();
|
||||
const { id } = useDocumentInfo();
|
||||
const operation = useOperation();
|
||||
const field = useFormFields(([fields]) => fields[path]);
|
||||
const dispatchField = useFormFields(([_, dispatch]) => dispatch);
|
||||
|
||||
const {
|
||||
dispatchFields,
|
||||
getField,
|
||||
getData,
|
||||
getSiblingData,
|
||||
setModified,
|
||||
} = formContext || {};
|
||||
|
||||
// Get field by path
|
||||
const field = getField(path);
|
||||
const { getData, getSiblingData, setModified } = useForm();
|
||||
|
||||
const value = field?.value as T;
|
||||
const initialValue = field?.initialValue as T;
|
||||
|
||||
const [internalInitialValue, setInternalInitialValue] = useState(() => field?.initialValue as T);
|
||||
const [internalValue, setInternalValue] = useState(() => field?.value as T);
|
||||
const [internallyValid, setInternallyValid] = useState<boolean>(undefined);
|
||||
|
||||
// Debounce internal values to update form state only every 60ms
|
||||
const debouncedValue = useDebounce(internalValue, 120);
|
||||
|
||||
// Validation is defined by two ways -
|
||||
// 1. by field state
|
||||
// 2. maintained locally to reflect instant validation state changes
|
||||
let valid = true;
|
||||
|
||||
if (field && typeof field.valid === 'boolean') {
|
||||
valid = field.valid;
|
||||
}
|
||||
|
||||
if (typeof internallyValid === 'boolean') {
|
||||
valid = internallyValid;
|
||||
}
|
||||
|
||||
const valid = typeof field?.valid === 'boolean' ? field.valid : true;
|
||||
const showError = valid === false && submitted;
|
||||
|
||||
// Method to send update field values from field component(s)
|
||||
// Should only be used internally
|
||||
const sendField = useCallback(async (valueToSend) => {
|
||||
const fieldToDispatch = {
|
||||
path,
|
||||
disableFormData,
|
||||
initialValue,
|
||||
validate,
|
||||
condition,
|
||||
value: valueToSend,
|
||||
valid: false,
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
const validateOptions = {
|
||||
id,
|
||||
user,
|
||||
data: getData(),
|
||||
siblingData: getSiblingData(path),
|
||||
operation,
|
||||
};
|
||||
|
||||
const validationResult = typeof validate === 'function' ? await validate(valueToSend, validateOptions) : true;
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
fieldToDispatch.errorMessage = validationResult;
|
||||
fieldToDispatch.valid = false;
|
||||
setInternallyValid(false);
|
||||
} else {
|
||||
fieldToDispatch.valid = validationResult;
|
||||
fieldToDispatch.errorMessage = undefined;
|
||||
setInternallyValid(true);
|
||||
}
|
||||
|
||||
if (typeof dispatchFields === 'function') {
|
||||
dispatchFields(fieldToDispatch);
|
||||
}
|
||||
}, [
|
||||
condition,
|
||||
disableFormData,
|
||||
dispatchFields,
|
||||
getData,
|
||||
getSiblingData,
|
||||
id,
|
||||
initialValue,
|
||||
operation,
|
||||
path,
|
||||
user,
|
||||
validate,
|
||||
]);
|
||||
|
||||
// Method to return from `useField`, used to
|
||||
// update internal field values from field component(s)
|
||||
// as fast as they arrive. NOTE - this method is NOT debounced
|
||||
// update field values from field component(s)
|
||||
const setValue = useCallback((e, disableModifyingForm = false) => {
|
||||
const val = (e && e.target) ? e.target.value : e;
|
||||
|
||||
@@ -122,49 +41,87 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
|
||||
setModified(true);
|
||||
}
|
||||
}
|
||||
setInternalValue(val);
|
||||
|
||||
dispatchField({
|
||||
type: 'UPDATE',
|
||||
path,
|
||||
value: val,
|
||||
disableFormData,
|
||||
});
|
||||
}, [
|
||||
setModified,
|
||||
modified,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (internalInitialValue !== initialValue) {
|
||||
setInternalValue(initialValue);
|
||||
setInternalInitialValue(initialValue);
|
||||
}
|
||||
|
||||
setInternallyValid(undefined);
|
||||
}, [initialValue, internalInitialValue]);
|
||||
|
||||
// The only time that the FORM value should be updated
|
||||
// is when the debounced value updates. So, when the debounced value updates,
|
||||
// send it up to the form
|
||||
|
||||
const valueToSend = enableDebouncedValue ? debouncedValue : internalValue;
|
||||
|
||||
useEffect(() => {
|
||||
if ((field?.value !== valueToSend && valueToSend !== undefined) || disableFormData !== field?.disableFormData) {
|
||||
sendField(valueToSend);
|
||||
}
|
||||
}, [
|
||||
path,
|
||||
valueToSend,
|
||||
sendField,
|
||||
field,
|
||||
dispatchField,
|
||||
disableFormData,
|
||||
]);
|
||||
|
||||
return {
|
||||
...options,
|
||||
// Store result from hook as ref
|
||||
// to prevent unnecessary rerenders
|
||||
const result = useMemo(() => ({
|
||||
showError,
|
||||
errorMessage: field?.errorMessage,
|
||||
value: internalValue,
|
||||
value,
|
||||
formSubmitted: submitted,
|
||||
formProcessing: processing,
|
||||
setValue,
|
||||
initialValue,
|
||||
};
|
||||
}), [field, processing, setValue, showError, submitted, value, initialValue]);
|
||||
|
||||
// Throttle the validate function
|
||||
useThrottledEffect(() => {
|
||||
const validateField = async () => {
|
||||
const action: UPDATE = {
|
||||
type: 'UPDATE',
|
||||
path,
|
||||
disableFormData,
|
||||
validate,
|
||||
condition,
|
||||
value,
|
||||
valid: false,
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
const validateOptions = {
|
||||
id,
|
||||
user,
|
||||
data: getData(),
|
||||
siblingData: getSiblingData(path),
|
||||
operation,
|
||||
};
|
||||
|
||||
const validationResult = typeof validate === 'function' ? await validate(value, validateOptions) : true;
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
action.errorMessage = validationResult;
|
||||
action.valid = false;
|
||||
} else {
|
||||
action.valid = validationResult;
|
||||
action.errorMessage = undefined;
|
||||
}
|
||||
|
||||
if (action.valid !== valid && typeof dispatchField === 'function') {
|
||||
dispatchField(action);
|
||||
}
|
||||
};
|
||||
|
||||
validateField();
|
||||
}, 150, [
|
||||
value,
|
||||
condition,
|
||||
disableFormData,
|
||||
dispatchField,
|
||||
getData,
|
||||
getSiblingData,
|
||||
id,
|
||||
operation,
|
||||
path,
|
||||
user,
|
||||
validate,
|
||||
valid,
|
||||
]);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default useField;
|
||||
|
||||
@@ -3,9 +3,7 @@ import { Condition, Validate } from '../../../../fields/config/types';
|
||||
export type Options = {
|
||||
path: string
|
||||
validate?: Validate
|
||||
enableDebouncedValue?: boolean
|
||||
disableFormData?: boolean
|
||||
ignoreWhileFlattening?: boolean
|
||||
condition?: Condition
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { FieldBase } from '../../../../fields/config/types';
|
||||
import { useWatchForm } from '../Form/context';
|
||||
import { useAllFormFields } from '../Form/context';
|
||||
import getSiblingData from '../Form/getSiblingData';
|
||||
import reduceFieldsToValues from '../Form/reduceFieldsToValues';
|
||||
|
||||
const withCondition = <P extends Record<string, unknown>>(Field: React.ComponentType<P>): React.FC<P> => {
|
||||
const CheckForCondition: React.FC<P> = (props) => {
|
||||
@@ -30,13 +32,13 @@ const withCondition = <P extends Record<string, unknown>>(Field: React.Component
|
||||
|
||||
const path = typeof pathFromProps === 'string' ? pathFromProps : name;
|
||||
|
||||
const { getData, getSiblingData, getField, dispatchFields } = useWatchForm();
|
||||
const [fields, dispatchFields] = useAllFormFields();
|
||||
|
||||
const data = getData();
|
||||
const siblingData = getSiblingData(path);
|
||||
const data = reduceFieldsToValues(fields, true);
|
||||
const siblingData = getSiblingData(fields, path);
|
||||
const hasCondition = Boolean(condition);
|
||||
const currentlyPassesCondition = hasCondition ? condition(data, siblingData) : true;
|
||||
const field = getField(path);
|
||||
const field = fields[path];
|
||||
const existingConditionPasses = field?.passesCondition;
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.leave-without-saving {
|
||||
@include blur-bg;
|
||||
position: fixed;
|
||||
z-index: var(--z-modal);
|
||||
z-index: calc(var(--z-modal) + 1);
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
@@ -17,4 +17,4 @@
|
||||
.btn {
|
||||
margin-right: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,4 @@
|
||||
padding: 0 0 $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,14 +112,14 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
if (shouldFetch) {
|
||||
let publishedJSON = await fetch(publishedFetchURL).then((res) => res.json());
|
||||
let publishedJSON = await fetch(publishedFetchURL, { credentials: 'include' }).then((res) => res.json());
|
||||
|
||||
if (collection) {
|
||||
publishedJSON = publishedJSON?.docs?.[0];
|
||||
}
|
||||
|
||||
if (shouldFetchVersions) {
|
||||
versionJSON = await fetch(`${versionFetchURL}?${qs.stringify(versionParams)}`).then((res) => res.json());
|
||||
versionJSON = await fetch(`${versionFetchURL}?${qs.stringify(versionParams)}`, { credentials: 'include' }).then((res) => res.json());
|
||||
|
||||
if (publishedJSON?.updatedAt) {
|
||||
const newerVersionParams = {
|
||||
@@ -138,7 +138,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
// Get any newer versions available
|
||||
const newerVersionRes = await fetch(`${versionFetchURL}?${qs.stringify(newerVersionParams)}`);
|
||||
const newerVersionRes = await fetch(`${versionFetchURL}?${qs.stringify(newerVersionParams)}`, { credentials: 'include' });
|
||||
|
||||
if (newerVersionRes.status === 200) {
|
||||
unpublishedVersionJSON = await newerVersionRes.json();
|
||||
|
||||
5
src/admin/components/utilities/EditDepth/index.tsx
Normal file
5
src/admin/components/utilities/EditDepth/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useContext, createContext } from 'react';
|
||||
|
||||
export const EditDepthContext = createContext(0);
|
||||
|
||||
export const useEditDepth = (): number => useContext(EditDepthContext);
|
||||
@@ -80,9 +80,9 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
|
||||
{label}
|
||||
</h1>
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={description} />
|
||||
</div>
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={description} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<RenderFields
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
&__collection-actions,
|
||||
&__document-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields {
|
||||
@@ -88,7 +87,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__document-actions--with-preview {
|
||||
&__document-actions--has-2 {
|
||||
display: flex;
|
||||
|
||||
>* {
|
||||
@@ -106,8 +105,8 @@
|
||||
.form-submit {
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding-left: base(2);
|
||||
padding-right: base(2);
|
||||
padding-left: base(.5);
|
||||
padding-right: base(.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,4 +214,4 @@
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const Verify: React.FC<{ collection: SanitizedCollectionConfig }> = ({ collectio
|
||||
|
||||
useEffect(() => {
|
||||
async function verifyToken() {
|
||||
const result = await fetch(`${serverURL}/api/${collectionSlug}/verify/${token}`, { method: 'POST' });
|
||||
const result = await fetch(`${serverURL}/api/${collectionSlug}/verify/${token}`, { method: 'POST', credentials: 'include' });
|
||||
setVerifyResult(result);
|
||||
}
|
||||
verifyToken();
|
||||
|
||||
@@ -61,7 +61,7 @@ const CompareVersion: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
const search = qs.stringify(query);
|
||||
const response = await fetch(`${baseURL}?${search}`);
|
||||
const response = await fetch(`${baseURL}?${search}`, { credentials: 'include' });
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs<any> = await response.json();
|
||||
|
||||
@@ -22,7 +22,7 @@ const generateLabelFromValue = (
|
||||
): string => {
|
||||
let relation: string;
|
||||
let relatedDoc: RelationshipValue;
|
||||
let valueToReturn = '';
|
||||
let valueToReturn = '' as any;
|
||||
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (typeof value === 'object') {
|
||||
@@ -58,7 +58,7 @@ const generateLabelFromValue = (
|
||||
return valueToReturn;
|
||||
};
|
||||
|
||||
const Relationship: React.FC<Props & { field: RelationshipField}> = ({ field, version, comparison }) => {
|
||||
const Relationship: React.FC<Props & { field: RelationshipField }> = ({ field, version, comparison }) => {
|
||||
const { collections } = useConfig();
|
||||
const locale = useLocale();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import useField from '../../../../forms/useField';
|
||||
import Label from '../../../../forms/Label';
|
||||
import CopyToClipboard from '../../../../elements/CopyToClipboard';
|
||||
import { text } from '../../../../../../fields/validations';
|
||||
import { useWatchForm } from '../../../../forms/Form/context';
|
||||
import { useFormFields } from '../../../../forms/Form/context';
|
||||
|
||||
import GenerateConfirmation from '../../../../elements/GenerateConfirmation';
|
||||
|
||||
@@ -16,9 +16,7 @@ const APIKey: React.FC = () => {
|
||||
const [initialAPIKey, setInitialAPIKey] = useState(null);
|
||||
const [highlightedField, setHighlightedField] = useState(false);
|
||||
|
||||
const { getField } = useWatchForm();
|
||||
|
||||
const apiKey = getField(path);
|
||||
const apiKey = useFormFields(([fields]) => fields[path]);
|
||||
|
||||
const apiKeyValue = apiKey?.value;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import Password from '../../../../forms/field-types/Password';
|
||||
import Checkbox from '../../../../forms/field-types/Checkbox';
|
||||
import Button from '../../../../elements/Button';
|
||||
import ConfirmPassword from '../../../../forms/field-types/ConfirmPassword';
|
||||
import { useWatchForm, useFormModified } from '../../../../forms/Form/context';
|
||||
import { useFormModified, useFormFields } from '../../../../forms/Form/context';
|
||||
import { Props } from './types';
|
||||
|
||||
import APIKey from './APIKey';
|
||||
@@ -18,11 +18,10 @@ const baseClass = 'auth-fields';
|
||||
const Auth: React.FC<Props> = (props) => {
|
||||
const { useAPIKey, requirePassword, verify, collection: { slug }, collection, email, operation } = props;
|
||||
const [changingPassword, setChangingPassword] = useState(requirePassword);
|
||||
const { getField, dispatchFields } = useWatchForm();
|
||||
const enableAPIKey = useFormFields(([fields]) => fields.enableAPIKey);
|
||||
const dispatchFields = useFormFields((reducer) => reducer[1]);
|
||||
const modified = useFormModified();
|
||||
|
||||
const enableAPIKey = getField('enableAPIKey');
|
||||
|
||||
const {
|
||||
serverURL,
|
||||
routes: {
|
||||
@@ -42,6 +41,7 @@ const Auth: React.FC<Props> = (props) => {
|
||||
const unlock = useCallback(async () => {
|
||||
const url = `${serverURL}${api}/${slug}/unlock`;
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link, useRouteMatch } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import format from 'date-fns/format';
|
||||
import { useConfig } from '../../../utilities/Config';
|
||||
import Eyebrow from '../../../elements/Eyebrow';
|
||||
@@ -20,7 +20,6 @@ import VersionsCount from '../../../elements/VersionsCount';
|
||||
import Upload from './Upload';
|
||||
import { Props } from './types';
|
||||
import Autosave from '../../../elements/Autosave';
|
||||
|
||||
import Status from '../../../elements/Status';
|
||||
import Publish from '../../../elements/Publish';
|
||||
import SaveDraft from '../../../elements/SaveDraft';
|
||||
@@ -33,7 +32,6 @@ import './index.scss';
|
||||
const baseClass = 'collection-edit';
|
||||
|
||||
const DefaultEditView: React.FC<Props> = (props) => {
|
||||
const { params: { id } = {} } = useRouteMatch<Record<string, string>>();
|
||||
const { admin: { dateFormat }, routes: { admin } } = useConfig();
|
||||
const { publishedDoc } = useDocumentInfo();
|
||||
|
||||
@@ -48,6 +46,11 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
apiURL,
|
||||
action,
|
||||
hasSavePermission,
|
||||
disableEyebrow,
|
||||
disableActions,
|
||||
disableLeaveWithoutSaving,
|
||||
customHeader,
|
||||
id,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@@ -93,31 +96,36 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
description={`${isEditing ? 'Editing' : 'Creating'} - ${collection.labels.singular}`}
|
||||
keywords={`${collection.labels.singular}, Payload, CMS`}
|
||||
/>
|
||||
<Eyebrow />
|
||||
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && (
|
||||
<LeaveWithoutSaving />
|
||||
{!disableEyebrow && (
|
||||
<Eyebrow />
|
||||
)}
|
||||
{(!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && !disableLeaveWithoutSaving) && (
|
||||
<LeaveWithoutSaving />
|
||||
)}
|
||||
<Gutter className={`${baseClass}__edit`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
|
||||
</h1>
|
||||
{customHeader && customHeader}
|
||||
{!customHeader && (
|
||||
<h1>
|
||||
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
|
||||
</h1>
|
||||
)}
|
||||
</header>
|
||||
{auth && (
|
||||
<Auth
|
||||
useAPIKey={auth.useAPIKey}
|
||||
requirePassword={!isEditing}
|
||||
verify={auth.verify}
|
||||
collection={collection}
|
||||
email={data?.email}
|
||||
operation={operation}
|
||||
/>
|
||||
<Auth
|
||||
useAPIKey={auth.useAPIKey}
|
||||
requirePassword={!isEditing}
|
||||
verify={auth.verify}
|
||||
collection={collection}
|
||||
email={data?.email}
|
||||
operation={operation}
|
||||
/>
|
||||
)}
|
||||
{upload && (
|
||||
<Upload
|
||||
data={data}
|
||||
collection={collection}
|
||||
/>
|
||||
<Upload
|
||||
data={data}
|
||||
collection={collection}
|
||||
/>
|
||||
)}
|
||||
<RenderFields
|
||||
readOnly={!hasSavePermission}
|
||||
@@ -131,80 +139,82 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__sidebar-wrap`}>
|
||||
<div className={`${baseClass}__sidebar`}>
|
||||
<div className={`${baseClass}__sidebar-sticky-wrap`}>
|
||||
<ul className={`${baseClass}__collection-actions`}>
|
||||
{(permissions?.create?.permission) && (
|
||||
<React.Fragment>
|
||||
<li>
|
||||
<Link
|
||||
id="action-create"
|
||||
to={`${admin}/collections/${slug}/create`}
|
||||
>
|
||||
Create New
|
||||
</Link>
|
||||
</li>
|
||||
{!disableDuplicate && isEditing && (
|
||||
<li>
|
||||
<DuplicateDocument
|
||||
collection={collection}
|
||||
id={id}
|
||||
slug={slug}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{permissions?.delete?.permission && (
|
||||
<li>
|
||||
<DeleteDocument
|
||||
collection={collection}
|
||||
id={id}
|
||||
buttonId="action-delete"
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<div className={`${baseClass}__document-actions${((collection.versions?.drafts && !collection.versions?.drafts?.autosave) || (isEditing && preview)) ? ` ${baseClass}__document-actions--has-2` : ''}`}>
|
||||
{(preview && (!collection.versions?.drafts || collection.versions?.drafts?.autosave)) && (
|
||||
<PreviewButton
|
||||
generatePreviewURL={preview}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
{hasSavePermission && (
|
||||
<React.Fragment>
|
||||
{collection.versions?.drafts && (
|
||||
{!disableActions && (
|
||||
<ul className={`${baseClass}__collection-actions`}>
|
||||
{(permissions?.create?.permission) && (
|
||||
<React.Fragment>
|
||||
{!collection.versions.drafts.autosave && (
|
||||
<SaveDraft />
|
||||
<li>
|
||||
<Link
|
||||
id="action-create"
|
||||
to={`${admin}/collections/${slug}/create`}
|
||||
>
|
||||
Create New
|
||||
</Link>
|
||||
</li>
|
||||
{!disableDuplicate && isEditing && (
|
||||
<li>
|
||||
<DuplicateDocument
|
||||
collection={collection}
|
||||
id={id}
|
||||
slug={slug}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
<Publish />
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!collection.versions?.drafts && (
|
||||
<FormSubmit buttonId="action-save">Save</FormSubmit>
|
||||
{permissions?.delete?.permission && (
|
||||
<li>
|
||||
<DeleteDocument
|
||||
collection={collection}
|
||||
id={id}
|
||||
buttonId="action-delete"
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</ul>
|
||||
)}
|
||||
<div className={`${baseClass}__document-actions${((collection.versions?.drafts && !collection.versions?.drafts?.autosave) || (isEditing && preview)) ? ` ${baseClass}__document-actions--has-2` : ''}`}>
|
||||
{(preview && (!collection.versions?.drafts || collection.versions?.drafts?.autosave)) && (
|
||||
<PreviewButton
|
||||
generatePreviewURL={preview}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
{hasSavePermission && (
|
||||
<React.Fragment>
|
||||
{collection.versions?.drafts && (
|
||||
<React.Fragment>
|
||||
{!collection.versions.drafts.autosave && (
|
||||
<SaveDraft />
|
||||
)}
|
||||
<Publish />
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!collection.versions?.drafts && (
|
||||
<FormSubmit buttonId="action-save">Save</FormSubmit>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
{(isEditing && preview && (collection.versions?.drafts && !collection.versions?.drafts?.autosave)) && (
|
||||
<PreviewButton
|
||||
generatePreviewURL={preview}
|
||||
data={data}
|
||||
/>
|
||||
<PreviewButton
|
||||
generatePreviewURL={preview}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
{collection.versions?.drafts && (
|
||||
<React.Fragment>
|
||||
<Status />
|
||||
{(collection.versions?.drafts.autosave && hasSavePermission) && (
|
||||
<Autosave
|
||||
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
|
||||
collection={collection}
|
||||
id={id}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
<React.Fragment>
|
||||
<Status />
|
||||
{(collection.versions?.drafts.autosave && hasSavePermission) && (
|
||||
<Autosave
|
||||
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
|
||||
collection={collection}
|
||||
id={id}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
<RenderFields
|
||||
readOnly={!hasSavePermission}
|
||||
permissions={permissions.fields}
|
||||
@@ -213,51 +223,53 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
fieldSchema={fields}
|
||||
/>
|
||||
</div>
|
||||
{isEditing && (
|
||||
<ul className={`${baseClass}__meta`}>
|
||||
{!hideAPIURL && (
|
||||
<li className={`${baseClass}__api-url`}>
|
||||
<span className={`${baseClass}__label`}>
|
||||
API URL
|
||||
{' '}
|
||||
<CopyToClipboard value={apiURL} />
|
||||
</span>
|
||||
<a
|
||||
href={apiURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{apiURL}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{versions && (
|
||||
<li>
|
||||
<div className={`${baseClass}__label`}>Versions</div>
|
||||
<VersionsCount
|
||||
collection={collection}
|
||||
id={id}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
{timestamps && (
|
||||
<React.Fragment>
|
||||
{data.updatedAt && (
|
||||
<li>
|
||||
<div className={`${baseClass}__label`}>Last Modified</div>
|
||||
<div>{format(new Date(data.updatedAt), dateFormat)}</div>
|
||||
{
|
||||
isEditing && (
|
||||
<ul className={`${baseClass}__meta`}>
|
||||
{!hideAPIURL && (
|
||||
<li className={`${baseClass}__api-url`}>
|
||||
<span className={`${baseClass}__label`}>
|
||||
API URL
|
||||
{' '}
|
||||
<CopyToClipboard value={apiURL} />
|
||||
</span>
|
||||
<a
|
||||
href={apiURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{apiURL}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{(publishedDoc?.createdAt || data?.createdAt) && (
|
||||
{versions && (
|
||||
<li>
|
||||
<div className={`${baseClass}__label`}>Created</div>
|
||||
<div>{format(new Date(publishedDoc?.createdAt || data?.createdAt), dateFormat)}</div>
|
||||
<div className={`${baseClass}__label`}>Versions</div>
|
||||
<VersionsCount
|
||||
collection={collection}
|
||||
id={id}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
{timestamps && (
|
||||
<React.Fragment>
|
||||
{data.updatedAt && (
|
||||
<li>
|
||||
<div className={`${baseClass}__label`}>Last Modified</div>
|
||||
<div>{format(new Date(data.updatedAt), dateFormat)}</div>
|
||||
</li>
|
||||
)}
|
||||
{(publishedDoc?.createdAt || data?.createdAt) && (
|
||||
<li>
|
||||
<div className={`${baseClass}__label`}>Created</div>
|
||||
<div>{format(new Date(publishedDoc?.createdAt || data?.createdAt), dateFormat)}</div>
|
||||
</li>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { StepNavItem } from '../../../elements/StepNav/types';
|
||||
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||
import { Fields } from '../../../forms/Form/types';
|
||||
import { usePreferences } from '../../../utilities/Preferences';
|
||||
import { EditDepthContext } from '../../../utilities/EditDepth';
|
||||
|
||||
const EditView: React.FC<IndexProps> = (props) => {
|
||||
const { collection: incomingCollection, isEditing } = props;
|
||||
@@ -130,22 +131,25 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
const hasSavePermission = (isEditing && collectionPermissions?.update?.permission) || (!isEditing && collectionPermissions?.create?.permission);
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultEdit}
|
||||
CustomComponent={CustomEdit}
|
||||
componentProps={{
|
||||
isLoading: !initialState,
|
||||
data: dataToRender,
|
||||
collection,
|
||||
permissions: collectionPermissions,
|
||||
isEditing,
|
||||
onSave,
|
||||
initialState,
|
||||
hasSavePermission,
|
||||
apiURL,
|
||||
action,
|
||||
}}
|
||||
/>
|
||||
<EditDepthContext.Provider value={1}>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultEdit}
|
||||
CustomComponent={CustomEdit}
|
||||
componentProps={{
|
||||
id,
|
||||
isLoading: !initialState,
|
||||
data: dataToRender,
|
||||
collection,
|
||||
permissions: collectionPermissions,
|
||||
isEditing,
|
||||
onSave,
|
||||
initialState,
|
||||
hasSavePermission,
|
||||
apiURL,
|
||||
action,
|
||||
}}
|
||||
/>
|
||||
</EditDepthContext.Provider>
|
||||
);
|
||||
};
|
||||
export default EditView;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { CollectionPermission } from '../../../../../auth/types';
|
||||
import { Document } from '../../../../../types';
|
||||
@@ -11,6 +12,7 @@ export type IndexProps = {
|
||||
export type Props = IndexProps & {
|
||||
data: Document
|
||||
onSave?: () => void
|
||||
id?: string
|
||||
permissions: CollectionPermission
|
||||
isLoading: boolean
|
||||
initialState?: Fields
|
||||
@@ -18,4 +20,8 @@ export type Props = IndexProps & {
|
||||
action: string
|
||||
hasSavePermission: boolean
|
||||
autosaveEnabled: boolean
|
||||
disableEyebrow?: boolean
|
||||
disableActions?: boolean
|
||||
disableLeaveWithoutSaving?: boolean
|
||||
customHeader?: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const RelationshipProvider: React.FC<{ children?: React.ReactNode }> = ({
|
||||
};
|
||||
|
||||
const query = querystring.stringify(params, { addQueryPrefix: true });
|
||||
const result = await fetch(`${url}${query}`);
|
||||
const result = await fetch(`${url}${query}`, { credentials: 'include' });
|
||||
if (result.ok) {
|
||||
const json = await result.json();
|
||||
if (json.docs) {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useEffect, useRef } from 'react';
|
||||
type useThrottledEffect = (callback: React.EffectCallback, delay: number, deps: React.DependencyList) => void;
|
||||
|
||||
const useThrottledEffect: useThrottledEffect = (callback, delay, deps = []) => {
|
||||
const lastRan = useRef<number>(null);
|
||||
const lastRan = useRef(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (lastRan) {
|
||||
useEffect(
|
||||
() => {
|
||||
const handler = setTimeout(() => {
|
||||
if (Date.now() - lastRan.current >= delay) {
|
||||
callback();
|
||||
@@ -18,12 +18,9 @@ const useThrottledEffect: useThrottledEffect = (callback, delay, deps = []) => {
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}
|
||||
|
||||
callback();
|
||||
lastRan.current = Date.now();
|
||||
return () => null;
|
||||
}, [delay, ...deps]);
|
||||
},
|
||||
[delay, ...deps],
|
||||
);
|
||||
};
|
||||
|
||||
export default useThrottledEffect;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useWatchForm } from '../components/forms/Form/context';
|
||||
import { useFormFields } from '../components/forms/Form/context';
|
||||
|
||||
const useTitle = (useAsTitle: string): string => {
|
||||
const { getField } = useWatchForm();
|
||||
const titleField = getField(useAsTitle);
|
||||
const titleField = useFormFields(([fields]) => fields[useAsTitle]);
|
||||
return titleField?.value as string;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const Index = () => (
|
||||
<Router>
|
||||
<ModalProvider
|
||||
classPrefix="payload"
|
||||
zIndex={50}
|
||||
zIndex="var(--z-modal)"
|
||||
transTime={0}
|
||||
>
|
||||
<AuthProvider>
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
--breakpoint-m-width : #{$breakpoint-m-width};
|
||||
--breakpoint-l-width : #{$breakpoint-l-width};
|
||||
--scrollbar-width: 17px;
|
||||
--nav-width: #{base(9)};
|
||||
|
||||
--theme-bg: var(--theme-elevation-0);
|
||||
--theme-input-bg: var(--theme-elevation-0);
|
||||
--theme-text: var(--theme-elevation-800);
|
||||
--theme-baseline: #{$baseline-px};
|
||||
--theme-baseline-body-size: #{$baseline-body-size};
|
||||
--font-body: 'Suisse Intl', system-ui;
|
||||
--font-mono: monospace;
|
||||
--font-serif: 'Merriweather', serif;
|
||||
@@ -26,10 +29,12 @@
|
||||
|
||||
@include large-break {
|
||||
--gutter-h: #{base(3)};
|
||||
--nav-width: #{base(8)};
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
--gutter-h: #{base(2)};
|
||||
--nav-width: 100%;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
@@ -54,6 +59,7 @@ html {
|
||||
--theme-bg: var(--theme-elevation-0);
|
||||
--theme-text: var(--theme-elevation-1000);
|
||||
--theme-input-bg: var(--theme-elevation-50);
|
||||
color-scheme: dark;
|
||||
|
||||
::selection {
|
||||
color: var(--color-base-1000);
|
||||
@@ -171,4 +177,4 @@ dialog {
|
||||
z-index: var(--z-modal);
|
||||
}
|
||||
|
||||
@import '~payload-user-css';
|
||||
@import '~payload-user-css';
|
||||
@@ -18,7 +18,7 @@ $baseline-body-size : 13px !default;
|
||||
$baseline : math.div($baseline-px, $baseline-body-size)+rem;
|
||||
|
||||
@function base($multiplier) {
|
||||
@return math.div($baseline-px, $baseline-body-size) * $multiplier +rem;
|
||||
@return (math.div($baseline-px, $baseline-body-size) * $multiplier)+rem;
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Permissions } from '../types';
|
||||
import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/events/adminInit';
|
||||
import { tabHasName } from '../../fields/config/types';
|
||||
|
||||
const allOperations = ['create', 'read', 'update', 'delete'];
|
||||
|
||||
@@ -66,13 +67,18 @@ async function accessOperation(args: Arguments): Promise<Permissions> {
|
||||
executeFieldPolicies(updatedObj, field.fields, operation);
|
||||
} else if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
executeFieldPolicies(updatedObj, tab.fields, operation);
|
||||
if (tabHasName(tab)) {
|
||||
if (!updatedObj[tab.name]) updatedObj[tab.name] = { fields: {} };
|
||||
executeFieldPolicies(updatedObj[tab.name].fields, tab.fields, operation);
|
||||
} else {
|
||||
executeFieldPolicies(updatedObj, tab.fields, operation);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const executeEntityPolicies = (entity, operations, type) => {
|
||||
const executeEntityPolicies = async (entity, operations, type) => {
|
||||
if (!results[type]) results[type] = {};
|
||||
|
||||
results[type][entity.slug] = {
|
||||
@@ -93,7 +99,7 @@ async function accessOperation(args: Arguments): Promise<Permissions> {
|
||||
};
|
||||
|
||||
if (userCollectionConfig) {
|
||||
results.canAccessAdmin = userCollectionConfig.access.admin ? userCollectionConfig.access.admin(args) : isLoggedIn;
|
||||
results.canAccessAdmin = userCollectionConfig.access.admin ? await userCollectionConfig.access.admin(args) : isLoggedIn;
|
||||
} else {
|
||||
results.canAccessAdmin = false;
|
||||
}
|
||||
|
||||
@@ -133,6 +133,15 @@ async function login<T>(incomingArgs: Arguments): Promise<Result & { user: T}> {
|
||||
collection: collectionConfig.slug,
|
||||
});
|
||||
|
||||
await collectionConfig.hooks.beforeLogin.reduce(async (priorHook, hook) => {
|
||||
await priorHook;
|
||||
|
||||
user = (await hook({
|
||||
user,
|
||||
req: args.req,
|
||||
})) || user;
|
||||
}, Promise.resolve());
|
||||
|
||||
const token = jwt.sign(
|
||||
fieldsToSign,
|
||||
secret,
|
||||
@@ -166,7 +175,7 @@ async function login<T>(incomingArgs: Arguments): Promise<Result & { user: T}> {
|
||||
await priorHook;
|
||||
|
||||
user = await hook({
|
||||
doc: user,
|
||||
user,
|
||||
req: args.req,
|
||||
token,
|
||||
}) || user;
|
||||
|
||||
@@ -131,33 +131,50 @@ function generateFieldTypes(config: SanitizedConfig, fields: Field[]): {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: field.relationTo.map((relation) => {
|
||||
const idFieldType = getCollectionIDType(config.collections, relation);
|
||||
oneOf: [
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: field.relationTo.map((relation) => {
|
||||
const idFieldType = getCollectionIDType(config.collections, relation);
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
value: {
|
||||
oneOf: [
|
||||
{
|
||||
return {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
value: {
|
||||
type: idFieldType,
|
||||
},
|
||||
{
|
||||
relationTo: {
|
||||
const: relation,
|
||||
},
|
||||
},
|
||||
required: ['value', 'relationTo'],
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: field.relationTo.map((relation) => {
|
||||
return {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
value: {
|
||||
$ref: `#/definitions/${relation}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
relationTo: {
|
||||
const: relation,
|
||||
},
|
||||
},
|
||||
required: ['value', 'relationTo'],
|
||||
};
|
||||
}),
|
||||
},
|
||||
relationTo: {
|
||||
const: relation,
|
||||
},
|
||||
},
|
||||
required: ['value', 'relationTo'],
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
fieldSchema = {
|
||||
@@ -192,17 +209,20 @@ function generateFieldTypes(config: SanitizedConfig, fields: Field[]): {
|
||||
|
||||
if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: idFieldType,
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: `#/definitions/${field.relationTo}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
fieldSchema = {
|
||||
|
||||
@@ -12,6 +12,7 @@ const buildCollectionSchema = (collection: SanitizedCollectionConfig, config: Sa
|
||||
{
|
||||
draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts),
|
||||
options: { timestamps: collection.timestamps !== false, ...schemaOptions },
|
||||
indexSortableFields: config.indexSortableFields,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ const collectionSchema = joi.object().keys({
|
||||
joi.string(),
|
||||
componentSchema,
|
||||
),
|
||||
hooks: joi.object({
|
||||
beforeDuplicate: joi.func(),
|
||||
}),
|
||||
enableRichTextRelationship: joi.boolean(),
|
||||
components: joi.object({
|
||||
views: joi.object({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user